Skip to content

Commit f1306ba

Browse files
committed
Improve tests
1 parent afb72d0 commit f1306ba

File tree

3 files changed

+286
-6
lines changed

3 files changed

+286
-6
lines changed

src/asgi/lifespan.rs

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

src/asgi/mod.rs

Lines changed: 7 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,10 @@ 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(
315+
lib_cstring.as_ptr() as *const c_char,
316+
RTLD_NOW | RTLD_GLOBAL,
317+
);
315318
if !handle.is_null() {
316319
// Successfully loaded Python library with RTLD_GLOBAL
317320
return;
@@ -327,7 +330,7 @@ fn ensure_python_symbols_global() {
327330
// Try a range of Python versions (3.9 to 3.100 should cover future versions)
328331
for minor in 9..=100 {
329332
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);
333+
let handle = dlopen(lib_name.as_ptr() as *const c_char, RTLD_NOW | RTLD_GLOBAL);
331334
if !handle.is_null() {
332335
// Successfully loaded Python library with RTLD_GLOBAL
333336
return;

src/lib.rs

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#![warn(clippy::dbg_macro, clippy::print_stdout)]
99
#![warn(missing_docs)]
1010

11+
use std::ffi::c_char;
1112
#[cfg(feature = "napi-support")]
1213
use std::sync::Arc;
1314

@@ -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,115 @@ 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!(
391+
result.unwrap_err(),
392+
"Invalid format, expected \"file:function\""
393+
);
394+
395+
// Multiple colons
396+
let result = PythonHandlerTarget::try_from("too:many:colons");
397+
assert!(result.is_err());
398+
assert_eq!(
399+
result.unwrap_err(),
400+
"Invalid format, expected \"file:function\""
401+
);
402+
403+
// Empty string
404+
let result = PythonHandlerTarget::try_from("");
405+
assert!(result.is_err());
406+
assert_eq!(
407+
result.unwrap_err(),
408+
"Invalid format, expected \"file:function\""
409+
);
410+
411+
// Only colon - this actually succeeds with empty file and function
412+
// The current implementation allows this: ":" -> file="", function=""
413+
let result = PythonHandlerTarget::try_from(":");
414+
assert!(result.is_ok());
415+
let target = result.unwrap();
416+
assert_eq!(target.file, "");
417+
assert_eq!(target.function, "");
418+
419+
// Test with empty parts in different ways
420+
let result = PythonHandlerTarget::try_from(":function");
421+
assert!(result.is_ok());
422+
let target = result.unwrap();
423+
assert_eq!(target.file, "");
424+
assert_eq!(target.function, "function");
425+
426+
let result = PythonHandlerTarget::try_from("file:");
427+
assert!(result.is_ok());
428+
let target = result.unwrap();
429+
assert_eq!(target.file, "file");
430+
assert_eq!(target.function, "");
431+
}
432+
433+
#[test]
434+
fn test_python_handler_target_from_string_conversion() {
435+
let target = PythonHandlerTarget {
436+
file: "test_module".to_string(),
437+
function: "test_function".to_string(),
438+
};
439+
let result: String = target.into();
440+
assert_eq!(result, "test_module:test_function");
441+
}
442+
443+
#[test]
444+
fn test_python_handler_target_default() {
445+
let target = PythonHandlerTarget::default();
446+
assert_eq!(target.file, "main");
447+
assert_eq!(target.function, "app");
448+
}
449+
450+
#[test]
451+
fn test_python_handler_target_debug_clone_eq_hash() {
452+
let target1 = PythonHandlerTarget {
453+
file: "test".to_string(),
454+
function: "app".to_string(),
455+
};
456+
let target2 = target1.clone();
457+
458+
// Test Debug
459+
let debug_str = format!("{:?}", target1);
460+
assert!(debug_str.contains("test"));
461+
assert!(debug_str.contains("app"));
462+
463+
// Test Clone and PartialEq
464+
assert_eq!(target1, target2);
465+
466+
// Test inequality
467+
let target3 = PythonHandlerTarget {
468+
file: "different".to_string(),
469+
function: "app".to_string(),
470+
};
471+
assert_ne!(target1, target3);
472+
473+
// Test Hash by putting in a HashSet
474+
use std::collections::HashSet;
475+
let mut set = HashSet::new();
476+
set.insert(target1);
477+
set.insert(target2); // Should not increase size due to equality
478+
assert_eq!(set.len(), 1);
479+
}
480+
}

0 commit comments

Comments
 (0)