Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 53 additions & 12 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,22 @@ jobs:
- host: macos-latest
target: x86_64-apple-darwin
build: pnpm run build --target x86_64-apple-darwin
- host: macos-latest
target: aarch64-apple-darwin
build: pnpm run build --target aarch64-apple-darwin
- host: ubuntu-latest
target: x86_64-unknown-linux-gnu
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
build: pnpm run build --target x86_64-unknown-linux-gnu
- host: macos-latest
target: aarch64-apple-darwin
build: pnpm run build --target aarch64-apple-darwin
# Need to deal with this build failure before we can enable arm64 Linux builds.
# Probably also should be public first so we can use the arm runners to test it anyway.
# error: PYO3_CROSS_PYTHON_VERSION or an abi3-py3* feature must be specified when cross-compiling and PYO3_CROSS_LIB_DIR is not set.
# help: see the PyO3 user guide for more information: https://pyo3.rs/v0.25.1/building-and-distribution.html#cross-compiling
#
# - host: ubuntu-latest
# target: aarch64-unknown-linux-gnu
# docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
# build: pnpm run build --target aarch64-unknown-linux-gnu
name: stable - ${{ matrix.settings.target }} - node@20
runs-on: ${{ matrix.settings.host }}
steps:
Expand All @@ -59,15 +68,13 @@ jobs:
- uses: actions/setup-node@v4
if: ${{ !matrix.settings.docker }}
with:
node-version: 20
- name: Install
uses: dtolnay/rust-toolchain@stable
node-version: 24
- uses: dtolnay/rust-toolchain@stable
if: ${{ !matrix.settings.docker }}
with:
toolchain: stable
targets: ${{ matrix.settings.target }}
- name: Cache cargo
uses: actions/cache@v4
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
Expand Down Expand Up @@ -161,18 +168,32 @@ jobs:
if-no-files-found: error

test-macOS-windows-binding:
name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }}
name: Test ${{ matrix.settings.target }} - node@${{ matrix.node }} + python@${{ matrix.python }}
needs:
- build
strategy:
fail-fast: false
matrix:
settings:
- host: macos-latest
- host: macos-13
target: x86_64-apple-darwin
architecture: x64
- host: macos-latest
target: aarch64-apple-darwin
architecture: arm64
node:
- '18'
- '20'
- '22'
- '24'
python:
- '3.8'
- '3.9'
- '3.10'
- '3.11'
- '3.12'
- '3.13'
# - '3.14-rc'
runs-on: ${{ matrix.settings.host }}
steps:
- uses: actions/checkout@v4
Expand All @@ -187,8 +208,12 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
architecture: ${{ matrix.settings.architecture }}
cache: pnpm
architecture: x64
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python }}
architecture: ${{ matrix.settings.architecture }}
- run: pnpm install
- uses: actions/download-artifact@v4
with:
Expand All @@ -209,7 +234,7 @@ jobs:
- run: pnpm test

test-linux-binding:
name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }}
name: Test ${{ matrix.settings.target }} - node@${{ matrix.node }} + python@${{ matrix.python }}
needs:
- build
strategy:
Expand All @@ -218,6 +243,7 @@ jobs:
settings:
- host: ubuntu-22.04
target: x86_64-unknown-linux-gnu
architecture: x64
# Not supported yet.
# - host: ubuntu-22.04
# target: x86_64-unknown-linux-musl
Expand All @@ -229,6 +255,16 @@ jobs:
node:
- '18'
- '20'
- '22'
- '24'
python:
- '3.8'
- '3.9'
- '3.10'
- '3.11'
- '3.12'
- '3.13'
# - '3.14-rc'
runs-on: ${{ matrix.settings.host }}
steps:
- uses: actions/checkout@v4
Expand All @@ -239,7 +275,12 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
architecture: ${{ matrix.settings.architecture }}
cache: pnpm
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python }}
architecture: ${{ matrix.settings.architecture }}
- name: Install dependencies
run: pnpm install
- name: Fix soname
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pyo3 = { version = "0.25.1", features = ["experimental-async"] }
pyo3-async-runtimes = { version = "0.25.0", features = ["tokio-runtime"] }
thiserror = "2.0.12"
tokio = { version = "1.45.1", features = ["full"] }
libc = "0.2"

[build-dependencies]
napi-build = { version = "2", optional = true }
Expand Down
3 changes: 3 additions & 0 deletions npm/linux-arm64-gnu/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `python-node-linux-arm64-gnu`

This is the **aarch64-unknown-linux-gnu** binary for `python-node`
26 changes: 26 additions & 0 deletions npm/linux-arm64-gnu/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "python-node-linux-arm64-gnu",
"version": "0.0.0",
"os": [
"linux"
],
"cpu": [
"arm64"
],
"main": "python-node.linux-arm64-gnu.node",
"files": [
"python-node.linux-arm64-gnu.node",
"fix-python-soname.js",
"fix-python-soname.wasm"
],
"scripts": {
"postinstall": "node fix-python-soname.js"
},
"license": "MIT",
"engines": {
"node": ">= 10"
},
"libc": [
"glibc"
]
}
164 changes: 164 additions & 0 deletions src/asgi/lifespan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,168 @@ mod tests {
assert_eq!(message, LifespanSendMessage::LifespanStartupComplete);
});
}

#[test]
fn test_lifespan_send_message_from_pyobject_error_cases() {
Python::with_gil(|py| {
// Test missing 'type' key
let dict = PyDict::new(py);
let result: Result<LifespanSendMessage, _> = dict.extract();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Missing 'type' key")
);

// Test unknown message type
let dict = PyDict::new(py);
dict.set_item("type", "unknown.message.type").unwrap();
let result: Result<LifespanSendMessage, _> = dict.extract();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Unknown Lifespan send message type")
);

// Test non-dict object
let list = py.eval(c"[]", None, None).unwrap();
let result: Result<LifespanSendMessage, _> = list.extract();
assert!(result.is_err());

// Test invalid type value (not string)
let dict = PyDict::new(py);
dict.set_item("type", 123).unwrap();
let result: Result<LifespanSendMessage, _> = dict.extract();
assert!(result.is_err());
});
}

#[test]
fn test_lifespan_send_message_traits() {
// Test Debug trait
let msg1 = LifespanSendMessage::LifespanStartupComplete;
let msg2 = LifespanSendMessage::LifespanShutdownComplete;

let debug1 = format!("{:?}", msg1);
let debug2 = format!("{:?}", msg2);
assert!(debug1.contains("LifespanStartupComplete"));
assert!(debug2.contains("LifespanShutdownComplete"));

// Test Clone
let cloned1 = msg1.clone();
let cloned2 = msg2.clone();

// Test PartialEq and Eq
assert_eq!(msg1, cloned1);
assert_eq!(msg2, cloned2);
assert_ne!(msg1, msg2);

// Test Hash
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(msg1);
set.insert(cloned1); // Should not increase size due to equality
set.insert(msg2);
assert_eq!(set.len(), 2); // Only unique messages
}

#[test]
fn test_lifespan_receive_message_traits() {
// Test all the derive traits for LifespanReceiveMessage
let msg1 = LifespanReceiveMessage::LifespanStartup;
let msg2 = LifespanReceiveMessage::LifespanShutdown;

// Test Debug
let debug1 = format!("{:?}", msg1);
let debug2 = format!("{:?}", msg2);
assert!(debug1.contains("LifespanStartup"));
assert!(debug2.contains("LifespanShutdown"));

// Test Clone and Copy
let cloned1 = msg1.clone();
let copied1 = msg1;

// Test PartialEq and Eq
assert_eq!(msg1, cloned1);
assert_eq!(msg1, copied1);
assert_ne!(msg1, msg2);

// Test Hash
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(msg1);
set.insert(copied1); // Should not increase size due to equality
set.insert(msg2);
assert_eq!(set.len(), 2); // Only unique messages
}

#[test]
fn test_lifespan_scope_with_populated_state() {
Python::with_gil(|py| {
// Create a state dictionary with some data
let state_dict = PyDict::new(py);
state_dict.set_item("initialized", true).unwrap();
state_dict.set_item("counter", 42).unwrap();

let lifespan_scope = LifespanScope {
state: Some(state_dict.unbind()),
};

let py_obj = lifespan_scope.into_pyobject(py).unwrap();

// Verify the scope structure
assert_eq!(
dict_extract!(py_obj, "type", String),
"lifespan".to_string()
);

// Verify ASGI info is present
let asgi_info = dict_get!(py_obj, "asgi");
let asgi_dict = asgi_info.downcast::<PyDict>().unwrap();
assert_eq!(
asgi_dict
.get_item("version")
.unwrap()
.unwrap()
.extract::<String>()
.unwrap(),
"3.0"
);
assert_eq!(
asgi_dict
.get_item("spec_version")
.unwrap()
.unwrap()
.extract::<String>()
.unwrap(),
"2.0"
);

// Verify state is preserved
let state_obj = dict_get!(py_obj, "state");
let state_dict = state_obj.downcast::<PyDict>().unwrap();
assert_eq!(
state_dict
.get_item("initialized")
.unwrap()
.unwrap()
.extract::<bool>()
.unwrap(),
true
);
assert_eq!(
state_dict
.get_item("counter")
.unwrap()
.unwrap()
.extract::<i32>()
.unwrap(),
42
);
});
}
}
Loading
Loading