Skip to content

Commit a60da8e

Browse files
committed
Improve tests
1 parent afb72d0 commit a60da8e

File tree

3 files changed

+244
-6
lines changed

3 files changed

+244
-6
lines changed

src/asgi/lifespan.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,138 @@ mod tests {
160160
assert_eq!(message, LifespanSendMessage::LifespanStartupComplete);
161161
});
162162
}
163+
164+
#[test]
165+
fn test_lifespan_send_message_from_pyobject_error_cases() {
166+
Python::with_gil(|py| {
167+
// Test missing 'type' key
168+
let dict = PyDict::new(py);
169+
let result: Result<LifespanSendMessage, _> = dict.extract();
170+
assert!(result.is_err());
171+
assert!(result.unwrap_err().to_string().contains("Missing 'type' key"));
172+
173+
// Test unknown message type
174+
let dict = PyDict::new(py);
175+
dict.set_item("type", "unknown.message.type").unwrap();
176+
let result: Result<LifespanSendMessage, _> = dict.extract();
177+
assert!(result.is_err());
178+
assert!(result.unwrap_err().to_string().contains("Unknown Lifespan send message type"));
179+
180+
// Test non-dict object
181+
let list = py.eval(c"[]", None, None).unwrap();
182+
let result: Result<LifespanSendMessage, _> = list.extract();
183+
assert!(result.is_err());
184+
185+
// Test invalid type value (not string)
186+
let dict = PyDict::new(py);
187+
dict.set_item("type", 123).unwrap();
188+
let result: Result<LifespanSendMessage, _> = dict.extract();
189+
assert!(result.is_err());
190+
});
191+
}
192+
193+
#[test]
194+
fn test_lifespan_send_message_traits() {
195+
// Test Debug trait
196+
let msg1 = LifespanSendMessage::LifespanStartupComplete;
197+
let msg2 = LifespanSendMessage::LifespanShutdownComplete;
198+
199+
let debug1 = format!("{:?}", msg1);
200+
let debug2 = format!("{:?}", msg2);
201+
assert!(debug1.contains("LifespanStartupComplete"));
202+
assert!(debug2.contains("LifespanShutdownComplete"));
203+
204+
// Test Clone
205+
let cloned1 = msg1.clone();
206+
let cloned2 = msg2.clone();
207+
208+
// Test PartialEq and Eq
209+
assert_eq!(msg1, cloned1);
210+
assert_eq!(msg2, cloned2);
211+
assert_ne!(msg1, msg2);
212+
213+
// Test Hash
214+
use std::collections::HashSet;
215+
let mut set = HashSet::new();
216+
set.insert(msg1);
217+
set.insert(cloned1); // Should not increase size due to equality
218+
set.insert(msg2);
219+
assert_eq!(set.len(), 2); // Only unique messages
220+
}
221+
222+
#[test]
223+
fn test_lifespan_receive_message_traits() {
224+
// Test all the derive traits for LifespanReceiveMessage
225+
let msg1 = LifespanReceiveMessage::LifespanStartup;
226+
let msg2 = LifespanReceiveMessage::LifespanShutdown;
227+
228+
// Test Debug
229+
let debug1 = format!("{:?}", msg1);
230+
let debug2 = format!("{:?}", msg2);
231+
assert!(debug1.contains("LifespanStartup"));
232+
assert!(debug2.contains("LifespanShutdown"));
233+
234+
// Test Clone and Copy
235+
let cloned1 = msg1.clone();
236+
let copied1 = msg1;
237+
238+
// Test PartialEq and Eq
239+
assert_eq!(msg1, cloned1);
240+
assert_eq!(msg1, copied1);
241+
assert_ne!(msg1, msg2);
242+
243+
// Test Hash
244+
use std::collections::HashSet;
245+
let mut set = HashSet::new();
246+
set.insert(msg1);
247+
set.insert(copied1); // Should not increase size due to equality
248+
set.insert(msg2);
249+
assert_eq!(set.len(), 2); // Only unique messages
250+
}
251+
252+
#[test]
253+
fn test_lifespan_scope_with_populated_state() {
254+
Python::with_gil(|py| {
255+
// Create a state dictionary with some data
256+
let state_dict = PyDict::new(py);
257+
state_dict.set_item("initialized", true).unwrap();
258+
state_dict.set_item("counter", 42).unwrap();
259+
260+
let lifespan_scope = LifespanScope {
261+
state: Some(state_dict.unbind())
262+
};
263+
264+
let py_obj = lifespan_scope.into_pyobject(py).unwrap();
265+
266+
// Verify the scope structure
267+
assert_eq!(
268+
dict_extract!(py_obj, "type", String),
269+
"lifespan".to_string()
270+
);
271+
272+
// Verify ASGI info is present
273+
let asgi_info = dict_get!(py_obj, "asgi");
274+
let asgi_dict = asgi_info.downcast::<PyDict>().unwrap();
275+
assert_eq!(
276+
asgi_dict.get_item("version").unwrap().unwrap().extract::<String>().unwrap(),
277+
"3.0"
278+
);
279+
assert_eq!(
280+
asgi_dict.get_item("spec_version").unwrap().unwrap().extract::<String>().unwrap(),
281+
"2.0"
282+
);
283+
284+
// Verify state is preserved
285+
let state_obj = dict_get!(py_obj, "state");
286+
let state_dict = state_obj.downcast::<PyDict>().unwrap();
287+
assert_eq!(
288+
state_dict.get_item("initialized").unwrap().unwrap().extract::<bool>().unwrap(),
289+
true
290+
);
291+
assert_eq!(
292+
state_dict.get_item("counter").unwrap().unwrap().extract::<i32>().unwrap(),
293+
42
294+
);
295+
});
296+
}
163297
}

src/asgi/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ use std::{
77
};
88

99
#[cfg(target_os = "linux")]
10-
use std::os::raw::c_void;
10+
use std::os::raw::{c_char, c_void};
1111

1212
#[cfg(target_os = "linux")]
1313
unsafe extern "C" {
14-
fn dlopen(filename: *const i8, flag: i32) -> *mut c_void;
14+
fn dlopen(filename: *const c_char, flag: i32) -> *mut c_void;
1515
}
1616

1717
use bytes::BytesMut;
@@ -311,7 +311,7 @@ fn ensure_python_symbols_global() {
311311
let output_str = String::from_utf8_lossy(&output.stdout);
312312
for lib_path in output_str.lines() {
313313
if let Ok(lib_cstring) = CString::new(lib_path) {
314-
let handle = dlopen(lib_cstring.as_ptr(), RTLD_NOW | RTLD_GLOBAL);
314+
let handle = dlopen(lib_cstring.as_ptr() as *const c_char, RTLD_NOW | RTLD_GLOBAL);
315315
if !handle.is_null() {
316316
// Successfully loaded Python library with RTLD_GLOBAL
317317
return;
@@ -327,7 +327,7 @@ fn ensure_python_symbols_global() {
327327
// Try a range of Python versions (3.9 to 3.100 should cover future versions)
328328
for minor in 9..=100 {
329329
let lib_name = format!("libpython3.{}.so.1.0\0", minor);
330-
let handle = dlopen(lib_name.as_ptr() as *const i8, RTLD_NOW | RTLD_GLOBAL);
330+
let handle = dlopen(lib_name.as_ptr() as *const c_char, RTLD_NOW | RTLD_GLOBAL);
331331
if !handle.is_null() {
332332
// Successfully loaded Python library with RTLD_GLOBAL
333333
return;

src/lib.rs

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
#[cfg(feature = "napi-support")]
1212
use std::sync::Arc;
13+
use std::ffi::c_char;
1314

1415
#[cfg(feature = "napi-support")]
1516
use http_handler::napi::{Request as NapiRequest, Response as NapiResponse};
@@ -95,7 +96,7 @@ impl FromNapiValue for PythonHandlerTarget {
9596
check_status!(sys::napi_get_value_string_utf8(
9697
env,
9798
napi_val,
98-
buffer.as_mut_ptr() as *mut i8,
99+
buffer.as_mut_ptr() as *mut c_char,
99100
length + 1,
100101
&mut length
101102
))
@@ -142,7 +143,7 @@ impl ToNapiValue for PythonHandlerTarget {
142143
unsafe {
143144
check_status!(sys::napi_create_string_utf8(
144145
env,
145-
full_str.as_ptr() as *const i8,
146+
full_str.as_ptr() as *const c_char,
146147
full_str.len() as isize,
147148
&mut result
148149
))
@@ -365,3 +366,106 @@ impl Task for PythonRequestTask {
365366
Ok(Into::<NapiResponse>::into(output))
366367
}
367368
}
369+
370+
#[cfg(test)]
371+
mod tests {
372+
use super::*;
373+
374+
#[test]
375+
fn test_python_handler_target_try_from_str_valid() {
376+
let target = PythonHandlerTarget::try_from("main:app").unwrap();
377+
assert_eq!(target.file, "main");
378+
assert_eq!(target.function, "app");
379+
380+
let target = PythonHandlerTarget::try_from("my_module:my_function").unwrap();
381+
assert_eq!(target.file, "my_module");
382+
assert_eq!(target.function, "my_function");
383+
}
384+
385+
#[test]
386+
fn test_python_handler_target_try_from_str_invalid() {
387+
// No colon
388+
let result = PythonHandlerTarget::try_from("invalid");
389+
assert!(result.is_err());
390+
assert_eq!(result.unwrap_err(), "Invalid format, expected \"file:function\"");
391+
392+
// Multiple colons
393+
let result = PythonHandlerTarget::try_from("too:many:colons");
394+
assert!(result.is_err());
395+
assert_eq!(result.unwrap_err(), "Invalid format, expected \"file:function\"");
396+
397+
// Empty string
398+
let result = PythonHandlerTarget::try_from("");
399+
assert!(result.is_err());
400+
assert_eq!(result.unwrap_err(), "Invalid format, expected \"file:function\"");
401+
402+
// Only colon - this actually succeeds with empty file and function
403+
// The current implementation allows this: ":" -> file="", function=""
404+
let result = PythonHandlerTarget::try_from(":");
405+
assert!(result.is_ok());
406+
let target = result.unwrap();
407+
assert_eq!(target.file, "");
408+
assert_eq!(target.function, "");
409+
410+
// Test with empty parts in different ways
411+
let result = PythonHandlerTarget::try_from(":function");
412+
assert!(result.is_ok());
413+
let target = result.unwrap();
414+
assert_eq!(target.file, "");
415+
assert_eq!(target.function, "function");
416+
417+
let result = PythonHandlerTarget::try_from("file:");
418+
assert!(result.is_ok());
419+
let target = result.unwrap();
420+
assert_eq!(target.file, "file");
421+
assert_eq!(target.function, "");
422+
}
423+
424+
#[test]
425+
fn test_python_handler_target_from_string_conversion() {
426+
let target = PythonHandlerTarget {
427+
file: "test_module".to_string(),
428+
function: "test_function".to_string(),
429+
};
430+
let result: String = target.into();
431+
assert_eq!(result, "test_module:test_function");
432+
}
433+
434+
#[test]
435+
fn test_python_handler_target_default() {
436+
let target = PythonHandlerTarget::default();
437+
assert_eq!(target.file, "main");
438+
assert_eq!(target.function, "app");
439+
}
440+
441+
#[test]
442+
fn test_python_handler_target_debug_clone_eq_hash() {
443+
let target1 = PythonHandlerTarget {
444+
file: "test".to_string(),
445+
function: "app".to_string(),
446+
};
447+
let target2 = target1.clone();
448+
449+
// Test Debug
450+
let debug_str = format!("{:?}", target1);
451+
assert!(debug_str.contains("test"));
452+
assert!(debug_str.contains("app"));
453+
454+
// Test Clone and PartialEq
455+
assert_eq!(target1, target2);
456+
457+
// Test inequality
458+
let target3 = PythonHandlerTarget {
459+
file: "different".to_string(),
460+
function: "app".to_string(),
461+
};
462+
assert_ne!(target1, target3);
463+
464+
// Test Hash by putting in a HashSet
465+
use std::collections::HashSet;
466+
let mut set = HashSet::new();
467+
set.insert(target1);
468+
set.insert(target2); // Should not increase size due to equality
469+
assert_eq!(set.len(), 1);
470+
}
471+
}

0 commit comments

Comments
 (0)