From 727555ca293fec129e93f9a8cb4077ab206d972d Mon Sep 17 00:00:00 2001 From: Tanish Manku Date: Wed, 14 Oct 2026 13:37:03 -0700 Subject: [PATCH] fix(xtask): let a standalone crate dev-depend on test support The standalone rule ran before the testkit exemption and returned early, so `cos`, `geom`, `filters`, `crypt` and `arlington` could not use `pdfcraft-testkit` or `pdfcraft-oracle` even as a dev-dependency. Both crates were already registered in the layering table as `Class::Testkit`, so the first real use of either from a standalone crate would have been rejected. A dev-dependency is not part of the graph a standalone crate publishes, which is what the rule protects; a normal dependency on test support, and any dependency on a product crate, are still rejected. The existing test covered neither direction of the standalone case. Co-Authored-By: Claude Opus 5 --- xtask/src/layers.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/xtask/src/layers.rs b/xtask/src/layers.rs index 1bda1dc4d..a0907cb1a 100644 --- a/xtask/src/layers.rs +++ b/xtask/src/layers.rs @@ -198,11 +198,17 @@ pub fn check(crates: &[Crate]) -> Vec { continue; } if d.workspace { - if matches!(class, Class::Standalone(_)) && !standalone_allowed(&c.name, &d.name) { + let dep_class = classify(&d.name); + // Test support is a dev-dependency only (`Class::Testkit`) and so never reaches + // the product graph a standalone crate publishes. Checking it against the + // standalone allow-list would stop `cos` using the shared oracles in its own + // tests, which is the one thing those crates exist for. + let testkit_dev = dep_class == Some(Class::Testkit) && d.kind == DepKind::Dev; + if matches!(class, Class::Standalone(_)) && !testkit_dev && !standalone_allowed(&c.name, &d.name) { out.push(Violation::StandaloneHasWorkspaceDep { krate: c.name.clone(), dep: d.name.clone() }); continue; } - match classify(&d.name) { + match dep_class { None => {} Some(Class::Testkit) if d.kind != DepKind::Dev && class != Class::Testkit => { out.push(Violation::TestkitAsNormalDep { krate: c.name.clone(), dep: d.name.clone() }); @@ -322,6 +328,17 @@ mod tests { assert!(check(&[c("pdfcraft-render", &[("pdfcraft-oracle", Dev, true)])]).is_empty()); let v = check(&[c("pdfcraft-render", &[("pdfcraft-oracle", Normal, true)])]); assert!(matches!(v[..], [Violation::TestkitAsNormalDep { .. }])); + + // A standalone crate may use test support in its own tests: a dev-dependency is not part + // of the graph the standalone rule protects. `cos` is where the qpdf oracle is needed. + assert!(check(&[c("pdfcraft-cos", &[("pdfcraft-oracle", Dev, true)])]).is_empty()); + assert!(check(&[c("pdfcraft-geom", &[("pdfcraft-testkit", Dev, true)])]).is_empty()); + // ...but only as a dev-dependency, and only for test support. Product crates are still + // off-limits to a standalone crate however they are depended on. + let v = check(&[c("pdfcraft-cos", &[("pdfcraft-oracle", Normal, true)])]); + assert!(matches!(v[..], [Violation::StandaloneHasWorkspaceDep { .. }]), "{v:?}"); + let v = check(&[c("pdfcraft-cos", &[("pdfcraft-model", Dev, true)])]); + assert!(matches!(v[..], [Violation::StandaloneHasWorkspaceDep { .. }]), "{v:?}"); } #[test]