Skip to content

Commit 4e2f9e1

Browse files
authored
Improve tests (#4)
* Improve tests * Just fixing a line in the arm64 Linux readme
1 parent afb72d0 commit 4e2f9e1

File tree

8 files changed

+389
-61
lines changed

8 files changed

+389
-61
lines changed

.github/workflows/CI.yml

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,22 @@ jobs:
3737
- host: macos-latest
3838
target: x86_64-apple-darwin
3939
build: pnpm run build --target x86_64-apple-darwin
40+
- host: macos-latest
41+
target: aarch64-apple-darwin
42+
build: pnpm run build --target aarch64-apple-darwin
4043
- host: ubuntu-latest
4144
target: x86_64-unknown-linux-gnu
4245
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
4346
build: pnpm run build --target x86_64-unknown-linux-gnu
44-
- host: macos-latest
45-
target: aarch64-apple-darwin
46-
build: pnpm run build --target aarch64-apple-darwin
47+
# Need to deal with this build failure before we can enable arm64 Linux builds.
48+
# Probably also should be public first so we can use the arm runners to test it anyway.
49+
# error: PYO3_CROSS_PYTHON_VERSION or an abi3-py3* feature must be specified when cross-compiling and PYO3_CROSS_LIB_DIR is not set.
50+
# help: see the PyO3 user guide for more information: https://pyo3.rs/v0.25.1/building-and-distribution.html#cross-compiling
51+
#
52+
# - host: ubuntu-latest
53+
# target: aarch64-unknown-linux-gnu
54+
# docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
55+
# build: pnpm run build --target aarch64-unknown-linux-gnu
4756
name: stable - ${{ matrix.settings.target }} - node@20
4857
runs-on: ${{ matrix.settings.host }}
4958
steps:
@@ -59,15 +68,13 @@ jobs:
5968
- uses: actions/setup-node@v4
6069
if: ${{ !matrix.settings.docker }}
6170
with:
62-
node-version: 20
63-
- name: Install
64-
uses: dtolnay/rust-toolchain@stable
71+
node-version: 24
72+
- uses: dtolnay/rust-toolchain@stable
6573
if: ${{ !matrix.settings.docker }}
6674
with:
6775
toolchain: stable
6876
targets: ${{ matrix.settings.target }}
69-
- name: Cache cargo
70-
uses: actions/cache@v4
77+
- uses: actions/cache@v4
7178
with:
7279
path: |
7380
~/.cargo/registry/index/
@@ -161,18 +168,32 @@ jobs:
161168
if-no-files-found: error
162169

163170
test-macOS-windows-binding:
164-
name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }}
171+
name: Test ${{ matrix.settings.target }} - node@${{ matrix.node }} + python@${{ matrix.python }}
165172
needs:
166173
- build
167174
strategy:
168175
fail-fast: false
169176
matrix:
170177
settings:
171-
- host: macos-latest
178+
- host: macos-13
172179
target: x86_64-apple-darwin
180+
architecture: x64
181+
- host: macos-latest
182+
target: aarch64-apple-darwin
183+
architecture: arm64
173184
node:
174185
- '18'
175186
- '20'
187+
- '22'
188+
- '24'
189+
python:
190+
- '3.8'
191+
- '3.9'
192+
- '3.10'
193+
- '3.11'
194+
- '3.12'
195+
- '3.13'
196+
# - '3.14-rc'
176197
runs-on: ${{ matrix.settings.host }}
177198
steps:
178199
- uses: actions/checkout@v4
@@ -187,8 +208,12 @@ jobs:
187208
- uses: actions/setup-node@v4
188209
with:
189210
node-version: ${{ matrix.node }}
211+
architecture: ${{ matrix.settings.architecture }}
190212
cache: pnpm
191-
architecture: x64
213+
- uses: actions/setup-python@v6
214+
with:
215+
python-version: ${{ matrix.python }}
216+
architecture: ${{ matrix.settings.architecture }}
192217
- run: pnpm install
193218
- uses: actions/download-artifact@v4
194219
with:
@@ -209,7 +234,7 @@ jobs:
209234
- run: pnpm test
210235

211236
test-linux-binding:
212-
name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }}
237+
name: Test ${{ matrix.settings.target }} - node@${{ matrix.node }} + python@${{ matrix.python }}
213238
needs:
214239
- build
215240
strategy:
@@ -218,6 +243,7 @@ jobs:
218243
settings:
219244
- host: ubuntu-22.04
220245
target: x86_64-unknown-linux-gnu
246+
architecture: x64
221247
# Not supported yet.
222248
# - host: ubuntu-22.04
223249
# target: x86_64-unknown-linux-musl
@@ -229,6 +255,16 @@ jobs:
229255
node:
230256
- '18'
231257
- '20'
258+
- '22'
259+
- '24'
260+
python:
261+
- '3.8'
262+
- '3.9'
263+
- '3.10'
264+
- '3.11'
265+
- '3.12'
266+
- '3.13'
267+
# - '3.14-rc'
232268
runs-on: ${{ matrix.settings.host }}
233269
steps:
234270
- uses: actions/checkout@v4
@@ -239,7 +275,12 @@ jobs:
239275
uses: actions/setup-node@v4
240276
with:
241277
node-version: ${{ matrix.node }}
278+
architecture: ${{ matrix.settings.architecture }}
242279
cache: pnpm
280+
- uses: actions/setup-python@v6
281+
with:
282+
python-version: ${{ matrix.python }}
283+
architecture: ${{ matrix.settings.architecture }}
243284
- name: Install dependencies
244285
run: pnpm install
245286
- name: Fix soname

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pyo3 = { version = "0.25.1", features = ["experimental-async"] }
3535
pyo3-async-runtimes = { version = "0.25.0", features = ["tokio-runtime"] }
3636
thiserror = "2.0.12"
3737
tokio = { version = "1.45.1", features = ["full"] }
38+
libc = "0.2"
3839

3940
[build-dependencies]
4041
napi-build = { version = "2", optional = true }

npm/linux-arm64-gnu/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# `python-node-linux-arm64-gnu`
2+
3+
This is the **aarch64-unknown-linux-gnu** binary for `python-node`

npm/linux-arm64-gnu/package.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "python-node-linux-arm64-gnu",
3+
"version": "0.0.0",
4+
"os": [
5+
"linux"
6+
],
7+
"cpu": [
8+
"arm64"
9+
],
10+
"main": "python-node.linux-arm64-gnu.node",
11+
"files": [
12+
"python-node.linux-arm64-gnu.node",
13+
"fix-python-soname.js",
14+
"fix-python-soname.wasm"
15+
],
16+
"scripts": {
17+
"postinstall": "node fix-python-soname.js"
18+
},
19+
"license": "MIT",
20+
"engines": {
21+
"node": ">= 10"
22+
},
23+
"libc": [
24+
"glibc"
25+
]
26+
}

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
}

0 commit comments

Comments
 (0)