diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38a8e2bddee..e3dd03c8d90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,8 @@ jobs: run: nox -s rustfmt - name: Check markdown formatting (rumdl) run: nox -s rumdl + - name: Check `required-features` in Cargo.toml + run: nox -s check-test-features resolve: runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index 3139aacad38..bb707bf06f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -229,3 +229,179 @@ bare_urls = "warn" [lints] workspace = true + +# Many tests are only relevant on certain feature combinations; +# CI is marginally more efficient if `required-feature` is specified to avoid +# building and launching empty test suites. + +[[test]] +name = "test_anyhow" +required-features = ["anyhow"] + +[[test]] +name = "test_append_to_inittab" +required-features = ["macros"] + +[[test]] +name = "test_arithmetics" +required-features = ["macros"] + +[[test]] +name = "test_buffer" +required-features = ["macros"] + +[[test]] +name = "test_buffer_protocol" +required-features = ["macros"] + +[[test]] +name = "test_bytes" +required-features = ["macros"] + +[[test]] +name = "test_class_attributes" +required-features = ["macros"] + +[[test]] +name = "test_class_basics" +required-features = ["macros"] + +[[test]] +name = "test_class_comparisons" +required-features = ["macros"] + +[[test]] +name = "test_class_conversion" +required-features = ["macros"] + +[[test]] +name = "test_class_formatting" +required-features = ["macros"] + +[[test]] +name = "test_class_init" +required-features = ["macros"] + +[[test]] +name = "test_class_new" +required-features = ["macros"] + +[[test]] +name = "test_compile_error" +required-features = ["macros"] + +[[test]] +name = "test_coroutine" +required-features = ["experimental-async"] + +[[test]] +name = "test_declarative_module" +required-features = ["macros"] + +[[test]] +name = "test_default_impls" +required-features = ["macros"] + +[[test]] +name = "test_enum" +required-features = ["macros"] + +[[test]] +name = "test_exceptions" +required-features = ["macros"] + +[[test]] +name = "test_field_cfg" +required-features = ["macros"] + +[[test]] +name = "test_frompy_intopy_roundtrip" +required-features = ["macros"] + +[[test]] +name = "test_frompyobject" +required-features = ["macros"] + +[[test]] +name = "test_gc" +required-features = ["macros"] + +[[test]] +name = "test_getter_setter" +required-features = ["macros"] + +[[test]] +name = "test_inheritance" +required-features = ["macros"] + +[[test]] +name = "test_intopyobject" +required-features = ["macros"] + +[[test]] +name = "test_macro_docs" +required-features = ["macros"] + +[[test]] +name = "test_macros" +required-features = ["macros"] + +[[test]] +name = "test_mapping" +required-features = ["macros"] + +[[test]] +name = "test_methods" +required-features = ["macros"] + +[[test]] +name = "test_module" +required-features = ["macros"] + +[[test]] +name = "test_multiple_pymethods" +required-features = ["multiple-pymethods"] + +[[test]] +name = "test_proto_methods" +required-features = ["macros"] + +[[test]] +name = "test_pyfunction" +required-features = ["macros"] + +[[test]] +name = "test_pyself" +required-features = ["macros"] + +[[test]] +name = "test_sequence" +required-features = ["macros"] + +[[test]] +name = "test_serde" +required-features = ["serde"] + +[[test]] +name = "test_static_slots" +required-features = ["macros"] + +[[test]] +name = "test_string" +required-features = ["macros"] + +[[test]] +name = "test_super" +required-features = ["macros"] + +[[test]] +name = "test_text_signature" +required-features = ["macros"] + +[[test]] +name = "test_variable_arguments" +required-features = ["macros"] + +[[test]] +name = "test_various" +required-features = ["macros"] diff --git a/noxfile.py b/noxfile.py index dcea53ff427..01093ea3931 100644 --- a/noxfile.py +++ b/noxfile.py @@ -980,6 +980,82 @@ def format_ffi_extern(session: nox.Session): _format_ffi_extern(session) +_INNER_CFG_RE = re.compile(r"^#!\[\s*cfg\s*\((.*)\)\s*\]\s*$") +_FEATURE_RE = re.compile(r'feature\s*=\s*"([^"]+)"') + + +def _read_inner_cfgs(path: Path) -> List[str]: + """Return the bodies of leading `#![cfg(...)]` inner attributes.""" + cfgs: List[str] = [] + with path.open("r", encoding="utf-8") as f: + for line in f: + stripped = line.strip() + if stripped == "" or stripped.startswith("//"): + continue + if not stripped.startswith("#!["): + break + m = _INNER_CFG_RE.match(stripped) + if m: + cfgs.append(m.group(1).strip()) + return cfgs + + +@nox.session(name="check-test-features", venv_backend="none") +def check_test_features(session: nox.Session) -> None: + if toml is None: + session.error("requires Python 3.11 or `toml` to be installed") + + expected_tests = {} + errors = [] + for path in sorted((PYO3_DIR / "tests").glob("test_*.rs")): + features = set() + for body in _read_inner_cfgs(path): + feature = _FEATURE_RE.search(body) + if not feature: + continue + + if feature.group(0) != body: + # To keep the this check simple, expect a single simple `cfg` per required feature + errors.append( + f'{path}: please use simple `#![cfg(feature = "...")] for feature predicates, got `#![cfg({body})]`' + ) + + features.add(feature.group(1)) + + if features: + expected_tests[path.stem] = sorted(features) + + declared_tests = { + test["name"]: test + for test in toml.loads((PYO3_DIR / "Cargo.toml").read_text()).get("test", []) + } + all_test_names = sorted(expected_tests.keys() | declared_tests.keys()) + + for test in all_test_names: + if (expected := expected_tests.get(test)) is None: + errors.append(f"Remove [[test]] entry for {test!r} from Cargo.toml") + continue + + declared = declared_tests.get(test, {}) + declared_features = declared.get("required-features", []) + + if set(declared_features) != set(expected): + errors.append( + f"""- Update Cargo.toml for test {test}: + + [[test]] + name = "{test}" + required-features = [{", ".join(f'"{f}"' for f in expected)}] + """ + ) + + if errors: + print("Errors found in test feature predicates:", file=sys.stderr) + for error in errors: + print(" " + error, file=sys.stderr) + session.error() + + @nox.session(name="address-sanitizer", venv_backend="none") def address_sanitizer(session: nox.Session): _run_cargo( diff --git a/tests/test_append_to_inittab.rs b/tests/test_append_to_inittab.rs index e147967a0c7..1ff7b5d698b 100644 --- a/tests/test_append_to_inittab.rs +++ b/tests/test_append_to_inittab.rs @@ -1,4 +1,5 @@ -#![cfg(all(feature = "macros", not(PyPy)))] +#![cfg(feature = "macros")] +#![cfg(not(PyPy))] use pyo3::prelude::*; diff --git a/tests/test_serde.rs b/tests/test_serde.rs index ec4d859084e..1c8954abe68 100644 --- a/tests/test_serde.rs +++ b/tests/test_serde.rs @@ -1,80 +1,79 @@ -#[cfg(feature = "serde")] -mod test_serde { - use pyo3::prelude::*; +#![cfg(feature = "serde")] - use serde::{Deserialize, Serialize}; +use pyo3::prelude::*; - #[pyclass] - #[derive(Debug, Serialize, Deserialize)] - struct Group { - name: String, - } +use serde::{Deserialize, Serialize}; - #[pyclass] - #[derive(Debug, Serialize, Deserialize)] - struct User { - username: String, - group: Option>, - friends: Vec>, - } +#[pyclass] +#[derive(Debug, Serialize, Deserialize)] +struct Group { + name: String, +} - #[test] - fn test_serialize() { - let friend1 = User { - username: "friend 1".into(), - group: None, - friends: vec![], - }; - let friend2 = User { - username: "friend 2".into(), - group: None, - friends: vec![], - }; +#[pyclass] +#[derive(Debug, Serialize, Deserialize)] +struct User { + username: String, + group: Option>, + friends: Vec>, +} - let user = Python::attach(|py| { - let py_friend1 = Py::new(py, friend1).expect("failed to create friend 1"); - let py_friend2 = Py::new(py, friend2).expect("failed to create friend 2"); +#[test] +fn test_serialize() { + let friend1 = User { + username: "friend 1".into(), + group: None, + friends: vec![], + }; + let friend2 = User { + username: "friend 2".into(), + group: None, + friends: vec![], + }; - let friends = vec![py_friend1, py_friend2]; - let py_group = Py::new( - py, - Group { - name: "group name".into(), - }, - ) - .unwrap(); + let user = Python::attach(|py| { + let py_friend1 = Py::new(py, friend1).expect("failed to create friend 1"); + let py_friend2 = Py::new(py, friend2).expect("failed to create friend 2"); - User { - username: "danya".into(), - group: Some(py_group), - friends, - } - }); + let friends = vec![py_friend1, py_friend2]; + let py_group = Py::new( + py, + Group { + name: "group name".into(), + }, + ) + .unwrap(); - let serialized = serde_json::to_string(&user).expect("failed to serialize"); - assert_eq!( - serialized, - r#"{"username":"danya","group":{"name":"group name"},"friends":[{"username":"friend 1","group":null,"friends":[]},{"username":"friend 2","group":null,"friends":[]}]}"# - ); - } + User { + username: "danya".into(), + group: Some(py_group), + friends, + } + }); - #[test] - fn test_deserialize() { - let serialized = r#"{"username": "danya", "friends": + let serialized = serde_json::to_string(&user).expect("failed to serialize"); + assert_eq!( + serialized, + r#"{"username":"danya","group":{"name":"group name"},"friends":[{"username":"friend 1","group":null,"friends":[]},{"username":"friend 2","group":null,"friends":[]}]}"# + ); +} + +#[test] +fn test_deserialize() { + let serialized = r#"{"username": "danya", "friends": [{"username": "friend", "group": {"name": "danya's friends"}, "friends": []}]}"#; - let user: User = serde_json::from_str(serialized).expect("failed to deserialize"); + let user: User = serde_json::from_str(serialized).expect("failed to deserialize"); - assert_eq!(user.username, "danya"); - assert!(user.group.is_none()); - assert_eq!(user.friends.len(), 1usize); - let friend = user.friends.first().unwrap(); + assert_eq!(user.username, "danya"); + assert!(user.group.is_none()); + assert_eq!(user.friends.len(), 1usize); + let friend = user.friends.first().unwrap(); - Python::attach(|py| { - assert_eq!(friend.borrow(py).username, "friend"); - assert_eq!( - friend.borrow(py).group.as_ref().unwrap().borrow(py).name, - "danya's friends" - ) - }); - } + Python::attach(|py| { + assert_eq!(friend.borrow(py).username, "friend"); + assert_eq!( + friend.borrow(py).group.as_ref().unwrap().borrow(py).name, + "danya's friends" + ) + }); } diff --git a/tests/test_super.rs b/tests/test_super.rs index 644365e8829..3a3ce5f6390 100644 --- a/tests/test_super.rs +++ b/tests/test_super.rs @@ -1,4 +1,5 @@ -#![cfg(all(feature = "macros", not(any(PyPy, GraalPy))))] +#![cfg(feature = "macros")] +#![cfg(not(any(PyPy, GraalPy)))] use pyo3::{prelude::*, types::PySuper};