Skip to content

Commit aff6e42

Browse files
committed
Improve tests
1 parent afb72d0 commit aff6e42

File tree

4 files changed

+323
-11
lines changed

4 files changed

+323
-11
lines changed

.github/workflows/CI.yml

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ jobs:
5959
- uses: actions/setup-node@v4
6060
if: ${{ !matrix.settings.docker }}
6161
with:
62-
node-version: 20
62+
node-version: 24
6363
- name: Install
6464
uses: dtolnay/rust-toolchain@stable
6565
if: ${{ !matrix.settings.docker }}
@@ -161,18 +161,31 @@ jobs:
161161
if-no-files-found: error
162162

163163
test-macOS-windows-binding:
164-
name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }}
164+
name: Test ${{ matrix.settings.target }} - node@${{ matrix.node }} + python@${{ matrix.python }}
165165
needs:
166166
- build
167167
strategy:
168168
fail-fast: false
169169
matrix:
170170
settings:
171-
- host: macos-latest
171+
- host: macos-13
172172
target: x86_64-apple-darwin
173+
architecture: x64
174+
- host: macos-latest
175+
target: aarch64-apple-darwin
176+
architecture: arm64
173177
node:
174178
- '18'
175179
- '20'
180+
- '22'
181+
- '24'
182+
python:
183+
- '3.8'
184+
- '3.9'
185+
- '3.10'
186+
- '3.11'
187+
- '3.12'
188+
- '3.13'
176189
runs-on: ${{ matrix.settings.host }}
177190
steps:
178191
- uses: actions/checkout@v4
@@ -187,8 +200,12 @@ jobs:
187200
- uses: actions/setup-node@v4
188201
with:
189202
node-version: ${{ matrix.node }}
203+
architecture: ${{ matrix.settings.architecture }}
190204
cache: pnpm
191-
architecture: x64
205+
- uses: actions/setup-python@v6
206+
with:
207+
python-version: ${{ matrix.python }}
208+
architecture: ${{ matrix.settings.architecture }}
192209
- run: pnpm install
193210
- uses: actions/download-artifact@v4
194211
with:
@@ -209,7 +226,7 @@ jobs:
209226
- run: pnpm test
210227

211228
test-linux-binding:
212-
name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }}
229+
name: Test ${{ matrix.settings.target }} - node@${{ matrix.node }} + python@${{ matrix.python }}
213230
needs:
214231
- build
215232
strategy:
@@ -218,6 +235,7 @@ jobs:
218235
settings:
219236
- host: ubuntu-22.04
220237
target: x86_64-unknown-linux-gnu
238+
architecture: x64
221239
# Not supported yet.
222240
# - host: ubuntu-22.04
223241
# target: x86_64-unknown-linux-musl
@@ -229,6 +247,15 @@ jobs:
229247
node:
230248
- '18'
231249
- '20'
250+
- '22'
251+
- '24'
252+
python:
253+
- '3.8'
254+
- '3.9'
255+
- '3.10'
256+
- '3.11'
257+
- '3.12'
258+
- '3.13'
232259
runs-on: ${{ matrix.settings.host }}
233260
steps:
234261
- uses: actions/checkout@v4
@@ -239,7 +266,12 @@ jobs:
239266
uses: actions/setup-node@v4
240267
with:
241268
node-version: ${{ matrix.node }}
269+
architecture: ${{ matrix.settings.architecture }}
242270
cache: pnpm
271+
- uses: actions/setup-python@v6
272+
with:
273+
python-version: ${{ matrix.python }}
274+
architecture: ${{ matrix.settings.architecture }}
243275
- name: Install dependencies
244276
run: pnpm install
245277
- name: Fix soname

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;

0 commit comments

Comments
 (0)