diff --git a/AGENTS.md b/AGENTS.md index 707b3ecd7..24c684b64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -218,7 +218,9 @@ directive anywhere. ```sh RUSTDOCFLAGS="--cfg docsrs -D warnings" \ - uv run --no-project --python $(PYTHON_BASELINE) scripts/doc-coverage.py --threshold 80 + uv run --no-project --python $(PYTHON_BASELINE) \ + scripts/doc-coverage.py --toolchain "$DOC_COVERAGE_TOOLCHAIN" \ + --threshold "$DOC_COVERAGE_THRESHOLD" ``` measuring the share of documented items across every workspace library diff --git a/Cargo.lock b/Cargo.lock index d057abcf6..ac4d5ec4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] @@ -19,9 +19,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -40,9 +40,9 @@ checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" [[package]] name = "anstream" -version = "0.6.19" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -55,50 +55,53 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] [[package]] name = "arraydeque" @@ -115,7 +118,7 @@ dependencies = [ "anstyle", "bstr", "libc", - "predicates 3.1.3", + "predicates 3.1.4", "predicates-core", "predicates-tree", "wait-timeout", @@ -132,23 +135,23 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "backtrace" -version = "0.3.75" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.8.9", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -192,18 +195,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "block-buffer" @@ -216,41 +210,41 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" dependencies = [ "memchr", "regex-automata", - "serde", + "serde_core", ] [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" [[package]] name = "camino" -version = "1.2.4" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" dependencies = [ "serde_core", ] [[package]] name = "cap-primitives" -version = "3.4.4" +version = "3.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a1e394ed14f39f8bc26f59d4c0c010dbe7f0a1b9bafff451b1f98b67c8af62a" +checksum = "8e0bf07d379916947be6c4a07f43684153d710a2896c31f9e97781362895596c" dependencies = [ "ambient-authority", "fs-set-times", @@ -266,9 +260,9 @@ dependencies = [ [[package]] name = "cap-primitives" -version = "4.0.2" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdadbd7c002d3a484b35243669abdae85a0ebaded5a61117169dc3400f9a7ff0" +checksum = "8b5f74729fd2f44701d1a8eb47e906cdb3ccd9ec0f02baad85a744b791940b18" dependencies = [ "ambient-authority", "fs-set-times", @@ -284,12 +278,12 @@ dependencies = [ [[package]] name = "cap-std" -version = "3.4.4" +version = "3.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c0355ca583dd58f176c3c12489d684163861ede3c9efa6fd8bba314c984189" +checksum = "a59e59fa26472d29680ece6a9f8ee8b0551a719a33df2f5240bde065ecbddfd7" dependencies = [ "camino", - "cap-primitives 3.4.4", + "cap-primitives 3.4.6", "io-extras 0.18.4", "io-lifetimes 2.0.4", "rustix", @@ -297,12 +291,12 @@ dependencies = [ [[package]] name = "cap-std" -version = "4.0.2" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7281235d6e96d3544ca18bba9049be92f4190f8d923e3caef1b5f66cfa752608" +checksum = "c1ec78e242cfa2cfe276807ac2ecc00315a6c97786977414bcd1c3963b6c91b8" dependencies = [ "camino", - "cap-primitives 4.0.2", + "cap-primitives 4.0.3", "io-extras 0.19.0", "io-lifetimes 3.0.1", "rustix", @@ -310,25 +304,25 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.40" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", - "shlex 1.3.0", + "shlex", ] [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.5.60" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -348,9 +342,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstream", "anstyle", @@ -369,14 +363,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] @@ -387,9 +381,9 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "clap_mangen" -version = "0.3.0" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82842b45bf9f6a3be090dd860095ac30728042c08e0d6261ca7259b5d850f07" +checksum = "211d617eaa4b735c96c9e0228fcbdb5120ef623f2b8cb67ffb84c3e02dbc28a4" dependencies = [ "clap", "roff", @@ -397,9 +391,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "console" @@ -409,7 +403,7 @@ checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" dependencies = [ "encode_unicode", "libc", - "unicode-width 0.2.1", + "unicode-width 0.2.2", "windows-sys 0.61.2", ] @@ -436,27 +430,18 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" dependencies = [ "cfg-if", ] @@ -476,16 +461,6 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "crypto-common" version = "0.2.2" @@ -507,12 +482,11 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.3" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -540,25 +514,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.6", -] - [[package]] name = "digest" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.1", + "block-buffer", "const-oid", - "crypto-common 0.2.2", + "crypto-common", ] [[package]] @@ -605,13 +569,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] @@ -628,9 +592,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.15.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "encode_unicode" @@ -652,19 +616,19 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "fastrand" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "figment" @@ -693,18 +657,19 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.3" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "flate2" -version = "1.1.4" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.9.1", + "zlib-rs", ] [[package]] @@ -747,7 +712,7 @@ dependencies = [ "intl_pluralrules", "rustc-hash", "self_cell", - "smallvec 1.15.1", + "smallvec 1.15.2", "unic-langid", ] @@ -767,7 +732,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" dependencies = [ "memchr", - "thiserror 2.0.17", + "thiserror 2.0.20", ] [[package]] @@ -799,9 +764,12 @@ dependencies = [ [[package]] name = "fragile" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" +checksum = "8878864ba14bb86e818a412bfd6f18f9eabd4ec0f008a28e8f7eb61db532fcf9" +dependencies = [ + "futures-core", +] [[package]] name = "fs-set-times" @@ -826,15 +794,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -843,72 +811,72 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-timer" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", "futures-macro", "futures-task", "pin-project-lite", - "pin-utils", "slab", ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "getrandom" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ - "typenum", - "version_check", + "cfg-if", + "libc", + "wasi", ] [[package]] name = "getrandom" -version = "0.2.16" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "r-efi 5.3.0", + "wasip2", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "r-efi 6.0.0", ] [[package]] @@ -930,9 +898,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "glob" @@ -965,9 +933,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash 0.1.5", ] @@ -1000,7 +968,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.15.4", + "hashbrown 0.15.5", ] [[package]] @@ -1017,9 +985,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hybrid-array" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "typenum", ] @@ -1073,12 +1041,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1086,9 +1055,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -1099,57 +1068,53 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", - "smallvec 1.15.1", + "smallvec 1.15.2", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -1164,15 +1129,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", - "smallvec 1.15.1", + "smallvec 1.15.2", "utf8_iter", ] [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1180,9 +1145,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -1198,7 +1163,7 @@ checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" dependencies = [ "console", "portable-atomic", - "unicode-width 0.2.1", + "unicode-width 0.2.2", "unit-prefix", "web-time", ] @@ -1245,9 +1210,9 @@ dependencies = [ [[package]] name = "inventory" -version = "0.3.20" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" dependencies = [ "rustversion", ] @@ -1269,7 +1234,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f" dependencies = [ "io-lifetimes 3.0.1", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1284,22 +1249,11 @@ version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" -[[package]] -name = "io-uring" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" -dependencies = [ - "bitflags", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "is_ci" @@ -1309,9 +1263,9 @@ checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -1339,11 +1293,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -1361,11 +1316,10 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "d7955dfc218a8afb29dfeffd540e3a6e96baeb94fe7138228dd7cc6937fbbf96" dependencies = [ - "bitflags", "libc", ] @@ -1377,31 +1331,30 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lru" -version = "0.18.1" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6" +checksum = "0d317b4b9eb398e6acce275758ec6125535505e7a146fb1a9b8bda2451b0ff4c" dependencies = [ "hashbrown 0.17.1", ] @@ -1419,14 +1372,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" dependencies = [ "cfg-if", - "digest 0.11.3", + "digest", ] [[package]] name = "memchr" -version = "2.7.5" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memo-map" @@ -1495,11 +1448,27 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minijinja" -version = "2.21.0" +version = "2.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3d648e68cea56d9858d535ee28f9538404e2dd8cb08ed0bd05dca379477f39" +checksum = "86886cf6dbf4e614b19c9a1eec9775f021869d7eadde0fc73921a81b90c9b4c9" dependencies = [ "memo-map", "serde", @@ -1512,18 +1481,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", - "simd-adler32", ] [[package]] -name = "mio" -version = "1.0.4" +name = "miniz_oxide" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "adler2", + "simd-adler32", ] [[package]] @@ -1576,12 +1543,12 @@ dependencies = [ "anyhow", "assert_cmd", "camino", - "cap-primitives 3.4.4", - "cap-std 3.4.4", + "cap-primitives 3.4.6", + "cap-std 3.4.6", "clap", "clap_complete", "clap_mangen", - "digest 0.11.3", + "digest", "dunce", "fluent-bundle", "fs4", @@ -1603,7 +1570,7 @@ dependencies = [ "mockall", "monotony", "ortho_config", - "predicates 3.1.3", + "predicates 3.1.4", "pretty_assertions", "proptest", "regex", @@ -1619,9 +1586,9 @@ dependencies = [ "serde_yaml", "serial_test", "sha1", - "sha2 0.11.0", + "sha2", "shell-quote", - "shlex 2.0.1", + "shlex", "strip-ansi-escapes", "sys-locale", "tempfile", @@ -1632,7 +1599,7 @@ dependencies = [ "toml 0.8.23", "tracing", "tracing-subscriber", - "unicode-width 0.2.1", + "unicode-width 0.2.2", "ureq", "url", "wait-timeout", @@ -1651,7 +1618,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" dependencies = [ - "smallvec 1.15.1", + "smallvec 1.15.2", ] [[package]] @@ -1677,9 +1644,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-traits" @@ -1692,24 +1659,24 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "option-ext" @@ -1719,9 +1686,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ordered-float" -version = "5.3.0" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +checksum = "c860fd3227ca4ac3cc032e2cd20cba3f02ccdf4b610538f8ee6584d56bb62e96" dependencies = [ "num-traits", ] @@ -1733,7 +1700,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1e1275151857af166ec59cab125a6fec5d2d0ddceda70a8c50c83c45456b953" dependencies = [ "camino", - "cap-std 4.0.2", + "cap-std 4.0.3", "clap", "clap-dispatch", "directories", @@ -1745,8 +1712,8 @@ dependencies = [ "ortho_config_macros", "serde", "serde_json", - "thiserror 2.0.17", - "toml 1.1.3+spec-1.1.0", + "thiserror 2.0.20", + "toml 1.1.4+spec-1.1.0", "tracing", "uncased", "unic-langid", @@ -1767,15 +1734,15 @@ dependencies = [ [[package]] name = "owo-colors" -version = "4.2.2" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" +checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1783,15 +1750,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", - "smallvec 1.15.1", - "windows-targets 0.52.6", + "smallvec 1.15.2", + "windows-link", ] [[package]] @@ -1852,27 +1819,21 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -1908,9 +1869,9 @@ dependencies = [ [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "difflib", @@ -1922,15 +1883,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", @@ -1948,11 +1909,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", + "toml_edit 0.25.13+spec-1.1.0", ] [[package]] @@ -2036,7 +1997,7 @@ dependencies = [ "libc", "once_cell", "raw-cpuid", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "web-sys", "winapi", ] @@ -2062,6 +2023,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radix_trie" version = "0.2.1" @@ -2074,9 +2041,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha", "rand_core", @@ -2098,7 +2065,7 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] @@ -2121,9 +2088,9 @@ dependencies = [ [[package]] name = "rapidhash" -version = "4.4.1" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" +checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e" dependencies = [ "rustversion", ] @@ -2139,9 +2106,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] @@ -2152,7 +2119,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", "thiserror 1.0.69", ] @@ -2163,16 +2130,16 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.20", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -2182,9 +2149,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -2193,9 +2160,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "relative-path" @@ -2211,7 +2178,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -2265,7 +2232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe104196f61dc8911a8da1b10005e9401e7c2e14ad9302e2de6688311c0beec7" dependencies = [ "camino", - "cap-std 3.4.4", + "cap-std 3.4.6", "cfg-if", "convert_case 0.6.0", "gherkin", @@ -2319,9 +2286,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.9.0" +version = "8.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca" +checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -2330,10 +2297,11 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.9.0" +version = "8.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2" +checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097" dependencies = [ + "mime_guess", "proc-macro2", "quote", "rust-embed-utils", @@ -2343,25 +2311,25 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "8.9.0" +version = "8.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" +checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0" dependencies = [ - "sha2 0.10.8", + "sha2", "walkdir", ] [[package]] name = "rustc-demangle" -version = "0.1.25" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -2397,9 +2365,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.32" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "log", "once_cell", @@ -2412,18 +2380,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.7" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", @@ -2450,15 +2418,15 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "ryu-js" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd29631678d6fb0903b69223673e122c32e9ae559d0960a38d574695ebc0ea15" +checksum = "04d056b875a9d2e6cb9a61d127afee9ac5999b9f87bcb32079d1318e505be714" [[package]] name = "same-file" @@ -2487,9 +2455,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "self_cell" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" [[package]] name = "semver" @@ -2524,7 +2492,7 @@ dependencies = [ "saphyr-parser", "serde", "serde_json", - "smallvec 2.0.0-alpha.11", + "smallvec 2.0.0-alpha.12", ] [[package]] @@ -2544,14 +2512,14 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "indexmap", "itoa", @@ -2563,9 +2531,9 @@ dependencies = [ [[package]] name = "serde_json_canonicalizer" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18032888bfda612a88f6b9c7c7a12e8168686936702fe584e3f1b1fc7848443a" +checksum = "fe52319a927259afbfa5180c5157cd8167edfd3e8c254f9558c7fef44c5649f2" dependencies = [ "ryu-js", "serde", @@ -2635,19 +2603,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", -] - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", + "cpufeatures", + "digest", ] [[package]] @@ -2657,8 +2614,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", + "cpufeatures", + "digest", ] [[package]] @@ -2676,12 +2633,6 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb502615975ae2365825521fa1529ca7648fd03ce0b0746604e0683856ecd7e4" -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "shlex" version = "2.0.1" @@ -2690,9 +2641,9 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "similar" @@ -2708,27 +2659,27 @@ checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "smallvec" -version = "2.0.0-alpha.11" +version = "2.0.0-alpha.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87b96efa4bd6bdd2ff0c6615cc36fc4970cbae63cfd46ddff5cee35a1b4df570" +checksum = "ef784004ca8777809dcdad6ac37629f0a97caee4c685fcea805278d81dd8b857" [[package]] name = "smawk" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" [[package]] name = "stable_deref_trait" @@ -2768,9 +2719,9 @@ dependencies = [ [[package]] name = "supports-hyperlinks" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" [[package]] name = "supports-unicode" @@ -2802,9 +2753,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", @@ -2838,7 +2789,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -2846,12 +2797,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2867,12 +2818,12 @@ dependencies = [ "anyhow", "assert_cmd", "camino", - "cap-std 3.4.4", + "cap-std 3.4.6", "mockable", "netsuke-build", "proptest", "rstest", - "sha2 0.11.0", + "sha2", "tempfile", "thiserror 1.0.69", "tracing", @@ -2887,7 +2838,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ "smawk", "unicode-linebreak", - "unicode-width 0.2.1", + "unicode-width 0.2.2", ] [[package]] @@ -2901,11 +2852,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.20", ] [[package]] @@ -2921,50 +2872,49 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] [[package]] name = "time" -version = "0.3.44" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -2972,38 +2922,34 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] [[package]] name = "tokio" -version = "1.46.1" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ - "backtrace", - "io-uring", - "libc", - "mio", "pin-project-lite", - "slab", "tokio-macros", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] @@ -3029,9 +2975,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.3+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap", "serde_core", @@ -3039,7 +2985,7 @@ dependencies = [ "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -3051,15 +2997,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -3080,28 +3017,28 @@ dependencies = [ "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", - "winnow 0.7.14", + "winnow 0.7.15", ] [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 0.7.14", + "winnow 1.0.4", ] [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -3167,7 +3104,7 @@ checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "nu-ansi-term", "sharded-slab", - "smallvec 1.15.1", + "smallvec 1.15.2", "thread_local", "tracing-core", "tracing-log", @@ -3267,11 +3204,17 @@ dependencies = [ "unic-langid-impl", ] +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-linebreak" @@ -3281,9 +3224,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -3293,9 +3236,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unit-prefix" @@ -3402,19 +3345,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", @@ -3425,9 +3368,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3435,9 +3378,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", @@ -3448,18 +3391,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ "js-sys", "wasm-bindgen", @@ -3481,14 +3424,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.3", + "webpki-roots 1.0.9", ] [[package]] name = "webpki-roots" -version = "1.0.3" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -3511,11 +3454,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3557,6 +3500,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -3590,13 +3542,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3609,6 +3578,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3621,6 +3596,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3633,12 +3614,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3651,6 +3644,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3663,6 +3662,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3675,6 +3680,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -3687,20 +3698,29 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] [[package]] name = "winx" @@ -3713,19 +3733,16 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "xdg" @@ -3741,11 +3758,10 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -3753,9 +3769,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -3765,18 +3781,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", @@ -3785,18 +3801,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -3806,15 +3822,15 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -3823,10 +3839,11 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", @@ -3834,15 +3851,21 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + [[package]] name = "zmij" version = "1.0.23" diff --git a/docs/developers-guide.md b/docs/developers-guide.md index ad69c1bb5..d89a285f6 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -116,6 +116,12 @@ script recipe bodies untouched. This boundary is what permits a recipe to contain a build-only helper without causing `help targets` to execute or otherwise evaluate that helper; it does not alter full-render behaviour. +The standard-library registration boundary owns MiniJinja's value formatter. It +preserves the historical lowercase `true` and `false` spelling when a Boolean +helper result is interpolated into a string field, while delegating all +non-Boolean values to MiniJinja's `escape_formatter`. Keep this as one +registration-wide policy: do not add per-helper or per-call formatter variants. + Helpers excluded from the query allowlist are registered as deliberate query-disabled stubs by the standard-library adapter. The stubs return a stable, classified MiniJinja operation error. Manifest expansion recognizes @@ -2345,6 +2351,37 @@ targets: command: "cargo clippy --manifest-path {{ item }}/Cargo.toml" ``` +### Testing conditional manifest boundaries + +Keep command-availability tests deterministic and at the narrowest useful +boundary: + +- For a present bare command, create a fake cross-platform executable in a + temporary directory and inject that directory with + `StdlibConfig::with_path_override`. +- For an absent bare command, combine an empty `path_override`, a + guaranteed-absent name, and `cwd_mode="never"`. The empty override alone is + insufficient because an empty PATH entry can resolve to the current directory. +- External integration tests cannot inject a `StdlibConfig` through the public + manifest loader. Use a guaranteed-absent direct path containing a separator + in those fixtures; direct resolution bypasses PATH traversal. Do not mutate + process PATH to simulate absence. + +Use `googletest` only for the in-crate white-box conditional-expansion tests. +When combining it with `rstest`, place `#[googletest::test]` before `#[rstest]` +so each generated case is registered once. Return `googletest::Result<()>`, use +matchers for the behavioural assertions, and convert fallible fixture setup with +`.or_fail()?`. Existing integration and behavioural tests retain the +`anyhow::ensure!` convention; use `pretty_assertions` only where an ordinary +value-equality diff is materially clearer, never for snapshot comparison. + +`StdlibState::is_impure()` is the observable for selection-time boundary tests. +An absent `command_available` branch should leave it `false`; a control that +invokes `shell` from a `when` expression should make it `true`. This flag +covers all impure stdlib helpers (`shell`, `grep`, and `fetch`), so fixtures +must avoid the latter two and describe the assertion as "no impure helper ran", +not as a shell-specific counter. + ## Manifest glob module boundary Glob expansion lives in `src/manifest/glob/`, and `glob_paths` is its only diff --git a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md new file mode 100644 index 000000000..b94c714fb --- /dev/null +++ b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md @@ -0,0 +1,1156 @@ +# Regression coverage for conditional action dependency manifests (3.14.5) + +This ExecPlan (execution plan) is a living document. The sections `Constraints`, +`Tolerances`, `Risks`, `Progress`, `Surprises & Discoveries`, `Decision Log`, +and `Outcomes & Retrospective` must be kept up to date as work proceeds. + +Status: COMPLETE + +## Purpose / big picture + +Netsuke already implements manifest-time conditional planning: actions and +targets may carry `foreach` (fan-out) and `when` (filter) keys that are +evaluated before the typed Abstract Syntax Tree (AST) is built (roadmap 3.14.1 +and 3.14.2); `command_available(name, **kwargs)` is a non-throwing probe that +returns `false` for absent commands instead of raising (roadmap 3.14.4); and +target/action `deps` lower into a separate *implicit dependency* class in the +intermediate representation (IR) and into Ninja's `|` implicit-dependency +syntax (roadmap 3.14.3). + +The behaviour works. What is missing is a deliberate, durable *regression net* +that pins these behaviours together as a single coherent contract: "a manifest +can select exactly one of two complementary actions based on tool availability, +the selection happens without running any shell command, and the selected +action's declared dependencies survive lowering into the IR and the generated +Ninja file." Roadmap item 3.14.5 asks for precisely this coverage. + +After this change a contributor can run `make test` and see new tests that fail +if any of the following regress: + +1. Action-level `when` filtering or action-level `foreach` fan-out. +2. Complementary `command_available(...)` / `not command_available(...)` + branches (the motivating real-world case is "use `cargo nextest` when it is + installed, otherwise fall back to `cargo test`") selecting *exactly one* + action. +3. The absent-command fallback path being chosen *without invoking the + `shell()` helper* — that is, conditional selection depends only on the + executable-discovery boundary, never on the command-execution boundary. +4. `deps` lowering into IR `implicit_deps` and into Ninja `build` statements, + including for conditionally-selected actions. + +The deliverable is test code plus small deterministic fixtures and +documentation. The branch also carries two compatibility boundaries: the +`parse_offset` guard rejects offsets whose absolute whole-hour component is 24 +or greater after the dependency update accepted values beyond a civil day, and +`register_legacy_boolean_formatter` preserves lowercase Boolean interpolation +for existing manifests. Together these preserve the documented ISO 8601 input +and historical MiniJinja rendering contracts while leaving other behaviour +unchanged. + +## Constraints + +Hard invariants that must hold throughout implementation. Violation requires +escalation, not workarounds. + +- This is primarily a test-and-documentation change, with two production + compatibility boundaries in scope. `parse_offset` in `src/stdlib/time/mod.rs` + must reject parsed offsets whose absolute whole-hour component is 24 or + greater because the dependency update accepts values beyond a civil day. + `register_legacy_boolean_formatter` in `src/stdlib/register.rs` must preserve + lowercase Boolean interpolation for existing manifests. Do not modify other + production semantics in `src/manifest/`, `src/ir/`, `src/ninja_gen.rs`, or + `src/stdlib/`. Any further production change requires escalation. The single + test-only seam exception remains available only if an existing public or + `pub(crate)` surface cannot drive a required scenario; any such seam must be + recorded in the Decision Log and must not change runtime behaviour. +- Do not weaken or delete existing tests. New tests are additive. Existing + passing tests must continue to pass unchanged. +- Preserve the existing implicit `phony: true` behaviour for actions; tests + must observe it, not alter it. +- All commit gateways must pass before each CodeRabbit review and before each + milestone is declared complete: `make check-fmt`, `make typecheck`, + `make lint`, and `make test`. +- British English with Oxford spelling in all prose and doc comments + (per the documentation style guide and `en-gb-oxendict`). +- Determinism: no new test may depend on the host's real `PATH` contents. + "Present" cases must inject a temporary directory containing a fake + executable via the public `StdlibConfig::with_path_override(...)` seam. + "Absent" cases using a bare command name must combine **all three** guards + together: an empty `path_override`, a guaranteed-absent command name (for + example a UUID-suffixed name), *and* `cwd_mode="never"`. An empty + `path_override` alone is **not** sufficient: `parse_path_entries` maps an + empty PATH component to the current directory and the default `CwdMode::Auto` + scans the cwd (`src/stdlib/which/env.rs:91`, + `src/stdlib/which/options.rs:11`), so without `cwd_mode="never"` the + temporary workspace's own contents could shadow the lookup. Integration + fixtures that cannot inject `path_override` must instead use a + guaranteed-absent **direct path** containing a separator; `resolve_direct` + bypasses PATH traversal entirely + (`src/stdlib/which/lookup/mod.rs:44,107-120`). + +## Tolerances (exception triggers) + +- Scope: if production (non-test) source files require changes beyond a single + additive test-only seam, stop and escalate. If the total new/changed line + count exceeds roughly 900 lines net, stop and reassess granularity. +- Interface: if any existing public API signature must change to make a + scenario testable, stop and escalate. +- Dependencies: adding `googletest` and `pretty_assertions` as + `[dev-dependencies]` is pre-authorized by the task brief (see Decision Log). + Use of `googletest` is **confined to the two in-crate white-box test files** + and is **gated on a Stage A interop spike** (`#[googletest::test]`+ + `#[rstest]` under the pinned `rstest` 0.26.1); if the spike fails, fall back + to bare `verify_that!(...)?` returning `googletest::Result<()>` without the + googletest test attribute, or to `anyhow::ensure!`. `tests/` integration and + snapshot files stay on the existing `ensure!`+`insta` idiom. Any *other* new + dependency triggers escalation. +- Iterations: if a new test cannot be made to pass after 3 focused attempts + and the production behaviour appears correct, stop and escalate with the + evidence (the test may be encoding a wrong expectation). +- Sabotage check: if a newly-added test still passes after the targeted + production line it guards is deliberately broken (see "Validation"), the test + is vacuous; stop and redesign it before proceeding. +- Ambiguity: if the "exactly one action" or "without invoking `shell()`" + requirement turns out to be satisfiable in materially different ways, present + the options with trade-offs. + +## Risks + +- Risk: Tests of `command_available` accidentally depend on the host PATH and + become flaky in CI. Severity: high. Likelihood: medium. Mitigation: drive the + real `which` resolver through the **public** + `StdlibConfig::with_path_override(...)` seam (threaded into the resolver by + `register_with_config`, `src/stdlib/register.rs:104`) for present cases, and + the three-guard absent recipe (empty `path_override`, a guaranteed-absent + name, and `cwd_mode="never"`) for absent cases. Precedent: + `tests/which_diagnostic_snapshot_tests.rs:30` already uses + `StdlibConfig::new(..).with_path_override(OsString::new())`. Note: the private + `mod which` makes `WhichConfig` unreachable from tests; do **not** attempt + to use it. + +- Risk: The `which` resolver cache is mistaken for process-global state and + someone "fixes" non-existent cross-test contamination with `fresh=true` or + serialization. Severity: low. Likelihood: medium. Mitigation: the + `WhichResolver` (with its `Arc>`) is constructed fresh per + `minijinja::Environment` (per `register`/`register_with_config` call); there + is no static/`OnceLock`/`thread_local` resolver. Each test builds its own + environment, so the cache is env-scoped and cannot leak across tests. The + cache key is also partitioned by `path_override`/`cwd`. The new tests + therefore need no `#[serial]` and may run in parallel. + +- Risk: `googletest`/`pretty_assertions` clash with the codebase's existing + uniform `anyhow::ensure!` style and create inconsistency. Severity: low. + Likelihood: high. Mitigation: confine the new assertion crates to the new + 3.14.5 test files; keep using `ensure!` where it already reads well; document + the convention in `docs/developers-guide.md` so future tests are consistent. + +- Risk: New Ninja snapshots are environment-sensitive (path separators, + ordering) and churn. Severity: medium. Likelihood: low. Mitigation: reuse the + established `insta` settings and `tests/snapshots/ninja/` location; keep + fixtures POSIX-path only; rely on the IR's deterministic ordering (outputs + sorted) already exercised by existing snapshots. + +- Risk: Overlap with property/bounded-verification roadmap items (4.2.x Kani, + 4.3.2 Proptest for expansion invariants) leads to scope creep or duplicated + intent. Severity: medium. Likelihood: medium. Mitigation: 3.14.5 delivers + *example-based* regression coverage only. Property and bounded-model coverage + of the same invariants is explicitly deferred to 4.2.x/4.3.x and noted in the + Decision Log. + +- Risk: The "without invoking `shell()`" assertion is driven through the + `StdlibState::is_impure()` flag, which flips for **any** impure helper — + `shell()`, `grep()`, *and* `fetch()` (network) + (`src/stdlib/command/mod.rs:81-102`, `src/stdlib/network/mod.rs:94,177`). It + is therefore a proxy for "no impure stdlib helper ran during selection", + which is strictly weaker than "the command-execution port specifically was + not driven". Severity: low. Likelihood: low. Mitigation: keep the no-shell + fixtures minimal (only `command_available` and plain `command:` recipes, no + `grep`/`fetch`) so the flag cleanly means "no impure helper ran during + selection", and state this scoping honestly in both the test module and the + Hexagonal framing. If a finer-grained, shell-specific observable is ever + required, escalate rather than overloading `is_impure()`. + +## Progress + +- [x] (2026-07-28 22:03Z) Stage A: confirmed interfaces, dependency ordering, + and googletest/rstest interoperability; retained only the approved dev + dependencies after reverting the throwaway spike. +- [x] (2026-07-28 23:14Z) Stage B: added B.1–B.5 tests and fixtures with a + passing focused run and assertion-level sabotage evidence for each. +- [x] (2026-08-30) Stage C: updated the users' guide, developers' guide, + component design, and roadmap; recorded the Boolean formatter contract + and why no ADR is warranted. +- [x] (2026-08-30) Stage D: passed the full gate stack and final + CodeRabbit review; roadmap item 3.14.5 is complete. +- [x] (2026-08-31) Reviewed the `now(offset=...)` compatibility guard after + review feedback; recorded its production scope, documented the accepted + and rejected offset forms, added focused property-based boundary + coverage, and added the boundary to the validation scope. +- [x] (2026-08-31) Added explicit example-based `parse_offset("Z")` and + `parse_offset("z")` regression cases, each asserting UTC. +- [x] (2026-08-31) Rebased onto + `origin/chore/enforce-markdown-table-formatting`; retained its canonical + non-mutating Markdown check and regenerated the lockfile from that + target baseline before running the full rebase validation stack. + +(Timestamps to be added as work proceeds.) + +## Surprises & discoveries + +- Observation: the `path_override` seam is **publicly reachable**, but + `expand_foreach` is `pub(crate)`. Evidence: + `StdlibConfig::with_path_override(...)` is `pub` (`src/stdlib/config.rs:240`) + and `register_with_config` is `pub` and returns the `StdlibState`; by contrast + `mod which` is private (`src/stdlib/mod.rs:18`), so `WhichConfig` is + unreachable, and `src/manifest/expand.rs:38` shows + `pub(crate) fn expand_foreach`. Impact: path injection needs **no new seam** + — use the public `with_path_override`/`register_with_config` pairing. The + deterministic real-resolver scenarios (nextest-vs-legacy and no-shell) must + still be **in-crate white-box tests** under + `src/manifest/expand_test_cases/`, but the reason is solely that they call the + `pub(crate)` `expand_foreach`, *not* the path override. The earlier + speculative "test-only `WhichConfig` constructor seam" is dropped as + unnecessary. + +- Observation: a clean observability seam already exists for "no impure helper + ran": `StdlibState::is_impure()` is `pub`, and `shell()`, `grep()`, and + `fetch()` set the shared `impure: Arc` when they execute. The + flag is set **eagerly** (the first statement in the `shell` closure, before + any spawn), so merely *invoking* the helper flips it regardless of success. + Evidence: `src/stdlib/mod.rs:44` (`pub fn is_impure`), + `src/stdlib/command/mod.rs:84` (eager store), `src/stdlib/network/mod.rs:94`. + Impact: the "absent-command fallback without invoking `shell()`" requirement + is testable by asserting `is_impure() == false` after expansion of a fixture + that uses no impure helpers; and the control sub-case can flip the flag + deterministically with no binary dependency. Scope caveat: `expand_foreach` + evaluates only `when:`/`foreach:` expressions, not `command:` recipes, so the + assertion proves *selection-time* purity (exactly the 3.14.5 boundary), not + whole-pipeline purity. + +- Observation: meaningful coverage already exists and must not be duplicated. + Evidence and inventory are in "Context and orientation". Impact: 3.14.5 is a + *gap-fill*, not a green-field test suite. + +- Observation: the MiniJinja dependency update changed Boolean interpolation + spelling in string fields. The registration-wide formatter is therefore the + narrow compatibility boundary for existing manifests; non-Boolean values + continue through MiniJinja's normal formatter. Date/Author: 2026-08-30, + review follow-up. + +- Observation: `#[googletest::test]` and `#[rstest]` interoperate under the + current dependency set when the googletest attribute comes first. Evidence: + the throwaway Stage A spike ran exactly one generated case and reported + `1 passed; 0 failed; 436 filtered out`. Impact: use both attributes in the + two planned white-box test modules; do not use the shorter speculative + `#[gtest]` spelling from the draft plan. + +- Observation: `DisplayEdge::fmt` preserves the stored vector order while + emitting explicit inputs, implicit dependencies, then order-only + dependencies. Evidence: `src/ninja_gen.rs:286-304`. Impact: keep fixture + dependency lists deliberately ordered so the new snapshot is deterministic. + +- Observation: googletest's `OrFail` trait is the intended bridge from fixture + setup errors into `googletest::Result`. Evidence: the first B.1 compile + attempt could not convert `TestAssertionFailure` into `anyhow::Error`; + changing the test return type to `googletest::Result<()>` and applying + `.or_fail()?` to setup operations preserved native matcher failures and + passed both parameterized cases. Impact: use the same error boundary in B.2 + instead of flattening matcher diagnostics into `anyhow`. + +- Observation: a successful shell control can avoid external-program + discovery while remaining cross-platform by invoking the platform shell's + no-op built-in (`:` on Unix, `exit /b 0` on Windows). Evidence: B.2 evaluates + the built-in from a `when` expression and the expansion succeeds while + `StdlibState::is_impure()` flips to `true`. Impact: the control verifies the + same expansion boundary without an `ensure_binaries_available` skip. + +- Observation: the public integration-level manifest loaders do not expose + `StdlibConfig::with_path_override`; `from_path_with_policy` constructs its + own config. Evidence: `src/manifest/mod.rs:216-235`. Impact: the B.3 fixture + uses a guaranteed-absent direct path containing `/`, which routes through + `resolve_direct` and bypasses PATH traversal entirely. The Constraints + section now distinguishes this stronger direct-path guard from the three + guards required for bare command names. + +- Observation: a manually-created output is not enough for Ninja's no-op check, + even when its modification time is newer than every prerequisite, because no + `.ninja_log` entry records the command metadata. Evidence: B.4's first two + focused attempts remained dirty until one harmless real pass populated the + log; the following `-n` pass then reported `no work to do`. Impact: + real-Ninja validation creates a future-dated output, runs the selected target + once to record its command, and then asserts the second pass is a no-op. + +## Decision log + +- Decision: Treat 3.14.5 as primarily test-and-documentation work; the + implementation (3.14.2–3.14.4) is already complete. Rationale: the roadmap + marks 3.14.2/3.14.3/3.14.4 done; recon confirms the behaviour is present. The + roadmap bullet text is literally "Test …". Date/Author: 2026-06-15, planning + agent. + +- Decision: Adopt `googletest` in a **confined, spike-gated** form, plus + `pretty_assertions` broadly, rather than using googletest throughout. + Specifics: `googletest` (`verify_that!`/`assert_that!` with matchers such as + `eq`, `len`, `contains`, `unordered_elements_are!`, `not`, `none`) is used + **only** in the two in-crate white-box files + (`command_available_selection_cases.rs`, + `command_available_no_shell_cases.rs`), and only after a Stage A spike + confirms `#[googletest::test]`+`#[rstest]` compile and run under the pinned + `rstest` 0.26.1. `tests/` integration and snapshot files keep the established + `anyhow::ensure!`+`insta` idiom. `pretty_assertions` 1.4.x (drop-in + `assert_eq!`/`assert_ne!` shadow; human-facing diffs only, never used for + snapshot comparison) may be used wherever a value-equality diff improves + readability. Rationale: the brief requests these crates for "clear test + semantics", but the community-of-experts review unanimously flagged that (a) + googletest has zero prior use in this repo (which is uniformly + `ensure!`-based), (b) its googletest/rstest interop was unverified against + `rstest` 0.26.1 before Stage A, and (c) the 3.14.5 assertions are trivial. + Confining googletest to the two files that most benefit (real-world matcher + ergonomics for the selection/no-shell cases) honours the brief while + containing the inconsistency and interop risk; the Stage A spike with a + defined fallback removes the "assume it works" hazard. Date/Author: + 2026-06-15, planning agent. **OPEN QUESTION FOR THE APPROVER (see "Open + questions"):** the crew's recommendation was stronger — drop `googletest` + entirely and keep only `pretty_assertions`. This plan keeps googletest + (confined) to respect the brief; the approver may instead elect the crew's + recommendation at the approval gate. + +- Decision: Inject `path_override` via the public + `StdlibConfig::with_path_override`; do not use `WhichConfig`. Rationale: + `mod which` is private, so `WhichConfig` is unreachable from tests; the public + `with_path_override`+`register_with_config` pairing already drives the real + resolver (precedent: `tests/which_diagnostic_snapshot_tests.rs:30`), needing + no new seam. In-crate placement is forced only by `expand_foreach` being + `pub(crate)`. Date/Author: 2026-06-15, planning agent. + +- Decision: Document, but do not work around, the env-scoped `which` cache. + Rationale: the resolver cache is per-`Environment`, not process-global; the + new tests build their own environments and need no `#[serial]` and no + `fresh=true`. Recording this prevents future cargo-culted serialization. + Date/Author: 2026-06-15, planning agent. + +- Decision: Defer property-based and bounded-model coverage of the manifest + expansion invariants to roadmap 4.2.x (Kani) and 4.3.2 (Proptest for manifest + expansion invariants). Rationale: 3.14.5 is scoped to + example/behavioural/snapshot regression coverage; 4.3.2 already owns "foreach + preserves non-control fields", "when is removed after evaluation", and + "item/index injected". Duplicating that here would create two owners for one + invariant. Date/Author: 2026-06-15, planning agent. + +- Decision: `ortho_config` is out of scope for this item. + Rationale: `ortho_config` governs layered CLI/configuration precedence + (roadmap 3.11.x) and localized help; 3.14.5 concerns manifest-time expansion + and lowering, which do not read layered configuration. The brief's mention is + acknowledged and explicitly scoped out to prevent creep. Date/Author: + 2026-06-15, planning agent. + +- Decision: Place deterministic real-resolver tests in-crate; place the + end-to-end deps+Ninja snapshot test in `tests/` using a guaranteed-absent + command for determinism. Rationale: `WhichConfig`/`expand_foreach` are + `pub(crate)` (white-box only); an absent command resolves to `false` + deterministically regardless of host, so a `tests/`-level snapshot can select + the fallback branch reproducibly. Date/Author: 2026-06-15, planning agent. + +- Decision: Use `#[googletest::test]` immediately before `#[rstest]` in the + confined white-box tests. Rationale: the Stage A spike proved the ordering + registers each rstest case exactly once with googletest 0.14.3 and rstest + 0.26.1, matching the documented interoperability contract supplied by the + approver. Date/Author: 2026-07-28, implementation agent. + +- Decision: Use a direct guaranteed-absent command path in external integration + fixtures. Rationale: public integration loaders intentionally own stdlib + configuration and do not accept a PATH override; adding a production API or + mutating process PATH would violate the test-only and isolation constraints. + A command containing `/` takes the resolver's direct path and never traverses + PATH, which is stronger than empty-PATH isolation for this absent-only + fixture. Date/Author: 2026-07-28, implementation agent. + +- Decision: Do not add an ADR for the regression boundary. Rationale: Stage C + documents and tests existing architecture—the executable-discovery resolver, + command-execution helpers, and shared impurity marker—without introducing or + selecting a new port, adapter, interface, or runtime policy. The design and + developers' guides are the appropriate homes for this test contract. + Date/Author: 2026-07-28, implementation agent. + +- Decision: Retain the `parse_offset` absolute-hour guard as a compatibility + boundary. Rationale: the dependency update accepts offsets beyond a civil + day, but the user-facing `now(offset=...)` contract is ISO 8601 and accepts + only offsets below 24 hours in absolute value. The guard therefore belongs at + parsing, where both `+24:00` and `-24:00` are rejected before timestamp + conversion. Date/Author: 2026-08-31, review follow-up. + +- Decision: Register `register_legacy_boolean_formatter` at the standard + library registration boundary as one global policy. Rationale: this keeps + Boolean values interpolated into string fields at the historical lowercase + `true`/`false` spelling across all helpers, while delegating non-Boolean + values to MiniJinja's normal formatter. Per-helper or per-call formatters + would create inconsistent manifest behaviour. Date/Author: 2026-08-30, review + follow-up. + +- Decision: Adopt the target branch's canonical Markdown-formatting contract + during the rebase. Rationale: `make check-fmt` now checks Rust, Python, and + Markdown formatting, while `scripts/check-markdown-format.sh` formats + isolated copies with `mdtablefix` and accepts only the exact LF or CRLF + rendering. Two replayed formatter commits were therefore superseded and + skipped: the target already includes that safer implementation and its CI + coverage. Date/Author: 2026-08-31, rebase follow-up. + +## Outcomes & retrospective + +Stage A completed without requiring a new seam or changing production code +outside the two in-scope compatibility boundaries: the `parse_offset` guard and +the registration-wide legacy Boolean formatter. The approved dev dependencies +resolve to googletest 0.14.3 and pretty_assertions 1.4.1. The real resolver +injection, expansion entry point, and deterministic Ninja dependency ordering +all remain available as planned. + +B.1 now pins complementary action selection through the real resolver in both +deterministic worlds. The present case also combines action `foreach` with +`when`; both cases assert exactly one action survives and that expansion +removes control fields. No production seam or semantic change was required. + +B.2 now proves the absent-tool fallback runs without invoking any impure stdlib +helper. Its paired shell-in-`when` control proves the shared impurity +observable is live during the same expansion boundary. The claim remains +deliberately broader than shell alone, as recorded under Risks and Hexagonal +framing. + +B.3 now combines conditional selection with action `foreach` and dependency +lowering. The selected alpha/beta actions preserve item-substituted explicit, +implicit, and order-only dependency classes and remain phony; the selected +target preserves the same classes without becoming phony. Filtered-branch paths +are absent from the graph. + +B.4 now pins the generated Ninja text for the shared fixture. Assertions and +the reviewed snapshot preserve `|` implicit and `||` order-only syntax for both +foreach actions and the selected target; real Ninja accepts and queries the +selected edges and reaches a no-op second pass. Filtered paths remain absent. + +B.5 completes the behavioural layer with one combined scenario assembled +entirely from existing steps. It observes the selected action's explicit and +implicit IR inputs and the emitted `|`/`||` Ninja segment without adding step +matcher maintenance. + +Stage C documentation now explains the selected action's `deps` semantics to +users, records deterministic conditional-test conventions for developers, +states the manifest/stdlib port boundary in the design, and marks roadmap item +3.14.5 and its four sub-items complete. No ADR is warranted because runtime +architecture and behaviour are unchanged. + +The completed change is primarily test-and-documentation work, with the +`parse_offset` guard and registration-wide legacy Boolean formatter explicitly +retained as the two production compatibility boundaries. It adds deterministic +real-resolver unit coverage, selection-time impurity coverage, conditional IR +dependency assertions, a reviewed Ninja snapshot with real-Ninja validation, +and one combined BDD scenario. It also adds property-based coverage showing +that signed offsets below 24 hours are accepted while both signs at 24 hours +and above are rejected. All five focused sabotage checks failed at the intended +assertion. The prior branch-wide formatting, documentation, type, lint, and +test gates passed, and CodeRabbit reported no actionable findings. The current +follow-up also passes all recorded validation gates: `make check-fmt`, +`make lint-clippy`, `make typecheck`, and `make lint`; the corrected +generated-name selection run passes 2/2 after the requested positional filter +selected 0 tests, time passes 31/31, Ninja passes 7/7, IR passes 23/23, doc +coverage is 99.04%, and `make test` passes 2,654/2,654 tests plus 33 doctests. + +## Context and orientation + +This section assumes no prior knowledge of the repository. + +### Vocabulary + +- **Manifest**: the user's YAML build description (`Netsukefile` / `*.yml`). +- **Typed AST**: the deserialized Rust representation of a manifest + (`src/ast.rs`: `NetsukeManifest`, `Target`, `Rule`, `Recipe`, `StringOrList`). +- **Manifest-time expansion**: evaluation of `foreach`/`when` on the *raw* + manifest value before the typed AST exists (`src/manifest/expand.rs`). +- **IR (intermediate representation)**: the build graph + (`src/ir/graph.rs`: `BuildGraph`, `Action`, `BuildEdge`). +- **Implicit dependency**: a `deps` entry that affects rebuild/ordering but is + not passed to the recipe as `$in`; emitted in Ninja after a `|` separator. +- **`command_available(name, **kwargs)`**: a non-throwing Jinja function that + returns `true`/`false` for executable presence (`src/stdlib/which/mod.rs`). +- **`shell()`**: an *impure* Jinja filter that runs a command during rendering + and sets the stdlib `impure` flag (`src/stdlib/command/`). + +### Pipeline (data flow) + +`manifest::from_str/from_path` → parse YAML to a raw `ManifestValue` → register +the stdlib into a Jinja `Environment` (this is where `command_available` and +`shell` are bound), capturing a `StdlibState` → +`expand::expand_foreach(&mut doc, &env)` removes `when:false` entries and fans +out `foreach`, injecting `item`/`index` → deserialize to the typed AST +(`deserialize_actions` stamps `phony: true`) → `render::render_manifest` +evaluates remaining string templates → `ir::BuildGraph::from_manifest` lowers +`sources`→`inputs`, `deps`→`implicit_deps`, `order_only_deps`→ +`order_only_deps`, and runs cycle detection over `inputs` + `implicit_deps` → +`ninja_gen::generate` formats `build` lines as +`build [| ]: [| ] [|| ]`. + +Key files and symbols (full paths relative to repo root): + +- `src/ast.rs`: `Target` (fields `sources`, `deps`, `order_only_deps`, + `phony`), `Rule.deps`, `StringOrList`, `deserialize_actions`. +- `src/manifest/expand.rs`: `expand_foreach` (`pub(crate)`), + `expand_section`, `expand_target`, `when_allows`, `eval_when`, `when_context`, + `inject_iteration_vars`, `FilteringStats`. +- `src/manifest/mod.rs`: `from_str`, `from_path`, `from_str_named` + (takes `stdlib_config: Option`). +- `src/stdlib/mod.rs`: `StdlibState::is_impure` / `reset_impure` (`pub`). +- `src/stdlib/register.rs`: `register`, `register_with_config` (`pub`). +- `src/stdlib/which/mod.rs`: `WhichConfig::new` (`pub(crate)`), + `command_available_with`, `is_command_available`. +- `src/ir/from_manifest.rs`: `BuildGraph::from_manifest`, `process_targets` + (`to_paths(&target.sources)`→inputs, `to_paths(&target.deps)`→implicit_deps). +- `src/ir/graph.rs`: + `BuildEdge { inputs, implicit_deps, order_only_deps, ... }`. +- `src/ninja_gen.rs`: `generate`, `generate_into`, `DisplayEdge` (the `|`/`||` + formatting). + +### Existing coverage (do not duplicate; extend or complement) + +- `src/manifest/expand_test_cases/condition_cases.rs`: parameterized + `targets`/`actions` cases for `when:false` removal before typed AST, + `foreach` iteration-var injection, static action `when` dropping, sequence + `foreach`, truthiness table, invalid-`when` errors, and **complementary + `command_available` branches for actions** — but the latter uses a *stubbed* + `command_available` function, not the real resolver. +- `src/manifest/expand_test_cases/target_command_available_cases.rs`: the same + complementary pattern for targets, also stubbed. +- `tests/data/actions_command_available_absent.yml` + BDD scenarios in + `tests/features/manifest.feature` ("Selecting a fallback action when a + command is unavailable", "Parsing fails when command availability receives + invalid options") and steps in `tests/bdd/steps/conditional_manifest.rs`: + exercise the *real* resolver for the absent case, but do **not** assert that + `shell()` was not invoked, and do not use the nextest-vs-legacy framing. +- `tests/ir_from_manifest_tests.rs`: `manifest_deps_populate_implicit_deps`, + `manifest_deps_do_not_contribute_to_recipe_inputs` — static targets only. +- `tests/ninja_snapshot_tests.rs`: `conditional_manifest_ninja_snapshot` + (foreach+when on a target), `implicit_deps_manifest_ninja_snapshot` + (`tests/data/implicit_deps.yml`) — but the two concerns (conditional + selection and `deps` lowering) are never combined in one fixture. + +### Gap analysis (what 3.14.5 must add) + +1. **Action-level `when` and `foreach`** — mostly covered. Gap: a single + integrated regression fixture exercising an action that carries `foreach`, + `when`, *and* `deps` together, end-to-end to Ninja. +2. **Complementary nextest/legacy branches select exactly one action** — not + covered. The existing complementary tests are stubbed and use generic names. + New: a real-resolver, nextest-vs-legacy scenario proving exactly one action + survives in both the present and absent worlds. +3. **Absent-command fallback without invoking `shell()`** — not covered. New: + assert `StdlibState::is_impure() == false` after selecting the fallback. +4. **`deps` lowering for conditional actions, in IR and Ninja** — partially + covered for static targets only. New: assert that a *conditionally-selected* + action's `deps` become `implicit_deps` in the IR and `| ...` in Ninja, and + that the *unselected* branch's deps never appear. + +### Hexagonal framing (boundaries this suite protects) + +Treat manifest-time conditional expansion as **domain/policy** logic. It +depends on two **driven ports**: + +- *Executable discovery* (the `which` resolver behind `command_available`). +- *Command execution* (the `shell()` helper). + +The core regression contract is a boundary assertion: **conditional selection +drives no impure side-effecting port.** `StdlibState::is_impure()` is the +observable proxy, but it is honest about its breadth — it flips for *any* +impure helper (`shell()`, `grep()`, or `fetch()`), so the precise statement the +test pins is "selection executed no impure stdlib helper", which subsumes (and +is slightly broader than) "the command-execution port was not driven". The +no-shell fixtures deliberately use only `command_available` and plain +`command:` recipes so the flag's meaning is unambiguous in context. No new +ports or adapters are introduced; the tests pin the existing boundary so future +refactors cannot quietly make selection depend on running commands. This plan +uses hexagonal thinking to choose *what to assert at the boundary*, not to +restructure code. (If a strictly shell-only observable is ever needed, that is +an escalation, not an overload of `is_impure()`.) + +## Plan of work + +### Stage A — confirm interfaces, run the interop spike, finalize gap analysis + +This stage adds **one throwaway spike** (immediately reverted) and otherwise +changes no code. + +1. **googletest interop spike (go/no-go for the googletest decision).** Add a + single throwaway test under the lib target: + + ```rust + // scratch, never committed + use googletest::prelude::*; + use rstest::rstest; + #[googletest::test] + #[rstest] + #[case(1)] + fn spike(#[case] n: i32) -> googletest::Result<()> { verify_that!(n, eq(1)) } + ``` + + Run it. If it compiles and passes under `rstest` 0.26.1, proceed with the + confined googletest approach. If it fails, adopt the fallback (bare + `verify_that!(...)?` in a plain `#[rstest] -> googletest::Result<()>` + without the googletest test attribute, or `ensure!`), and record the outcome + in the Decision Log. Revert the spike before any milestone commit. + +2. **Confirm the injection seam (public, no new seam).** Verify + `crate::stdlib::register_with_config(&mut Environment, StdlibConfig) + -> anyhow::Result` + and that `StdlibConfig::with_path_override` threads into the resolver + (`src/stdlib/register.rs:104`). Mirror the precedent in + `tests/which_diagnostic_snapshot_tests.rs:30`. Do **not** use `WhichConfig` + (private module). + +3. **Confirm `expand_foreach` drives selection** in a white-box test with the + *real* `command_available` registered (existing cases use a stubbed + `Environment::new()` function; the new cases register the full stdlib so the + real resolver runs). + +4. **Confirm deterministic deps ordering** in `DisplayEdge::fmt` + (`src/ninja_gen.rs`): record the exact `build` line shape and confirm + `implicit_deps`/`order_only_deps` are emitted in a deterministic order + before writing any snapshot. If ordering is insertion-order rather than + sorted, fixtures must use already-sorted deps so the snapshot is stable. + +Go/no-go: if the spike fails *and* no acceptable fallback compiles, or any +required seam is genuinely missing, escalate per Tolerances. + +### Stage B — red/green tests with sabotage evidence + +Because the production behaviour already exists, classic Red-Green-Refactor is +adapted: each new test is written, shown to **pass** against current code, and +then proven **non-vacuous** by a *sabotage check* — temporarily breaking the +single production line it guards and confirming the test fails. The sabotage +evidence must capture the **assertion-level failure message** (the specific +`verify_that!`/`ensure!`/`assert_snapshot!` line that fired and a diff that +corresponds to the guarded behaviour, e.g. "expected len 1, got 2", or +`implicit_deps` content appearing under `inputs`), not merely "test failed" — +this proves the test fails for the *right reason*, which a bare pass/fail +toggle does not. The sabotage diff is reverted with an explicit +`git checkout -- ` (recorded so a resumed agent can confirm a clean tree) +and is never committed; the transcript is recorded under "Artefacts and notes". +This is the documented substitute for the red stage (see the execplans guidance +on observable substitutes). + +Add the following, smallest first: + +1. **In-crate: nextest-vs-legacy exactly-one-action (real resolver).** + New file + `src/manifest/expand_test_cases/command_available_selection_cases.rs` (wired + into the `expand_test_cases` module). An `#[rstest]` parameterized over two + worlds, driving the *real* `command_available` (registered via + `register_with_config` with a `StdlibConfig`): + - *present*: a `tempfile::TempDir` containing a fake `cargo-nextest` + executable, injected via `StdlibConfig::with_path_override`. Reuse the + repo's existing cross-platform helper idiom from + `tests/which_diagnostic_snapshot_tests.rs:48-75` (`tool_filename` adds + `.cmd` on Windows; `mark_executable` sets `0o755` on Unix) so the present + branch runs on **both** platforms rather than a bare `#[cfg(unix)]` — the + Windows PATHEXT path (`src/stdlib/which/env.rs`) is the part most likely to + regress. Expect exactly the `run-tests-nextest` action (`command: cargo + nextest run`) to survive. + - *absent*: the three-guard recipe — empty `path_override` + + guaranteed-absent command name + `cwd_mode="never"`. Expect exactly the + `run-tests-legacy` action (`command: cargo test`) to survive. + Drive `expand_foreach`, then assert (googletest, per Stage A spike): + `verify_that!(actions, len(eq(1)))?`, the surviving action's `name`/ + `command` via `matches_pattern!`/`eq`, and that `when` was removed. This + also explicitly pins **bullet 1** for actions: include one case where the + surviving action additionally carries a `foreach` so the assertion covers + action-level fan-out and `when` together (complementing, not duplicating, + `condition_cases.rs`). + +2. **In-crate: absent fallback without `shell()`.** + New file + `src/manifest/expand_test_cases/command_available_no_shell_cases.rs`. + Register the full stdlib (so `command_available`, `shell`, `grep`, `fetch` + all exist), capture `StdlibState`, expand a manifest whose two actions use + `command_available("")` / `not command_available(...)` + and whose recipes are plain `command:` strings (no impure helpers), then + assert: exactly one action (the fallback) survives, `when` removed, and + `verify_that!(state.is_impure(), eq(false))?`. Add a contrasting *control* + sub-case proving the proxy is non-vacuous: a fixture that calls + `shell('true')` **inside a `when:` expression** (e.g. + `when: "{{ '' | shell('true') }} == ''"`) — because `expand_foreach` + evaluates `when:`/`foreach:` but not `command:` recipes, the helper must be + in a `when:` to flip the flag during the same call under test. The impure + flag is set eagerly on invocation, so assert `is_impure()==true` + deterministically with **no** `ensure_binaries_available` gate (avoiding a + skip-driven hole). + +3. **Integration + IR: conditional action carries `deps` into `implicit_deps`, + including the `item`-in-`deps` interaction.** New fixture + `tests/data/conditional_action_deps.yml`: a target and an action, each + selected via a complementary `command_available` pair using a + guaranteed-absent command (`cwd_mode="never"` retained) so the fallback + branch is deterministic, each carrying `sources`, `deps`, and + `order_only_deps`. Crucially, the action uses `foreach` and interpolates + `{{ item }}` (or `{{ index }}`) into **one `deps` entry** (e.g. + `deps: [build/{{ item }}.o]`) — the per-item implicit-dependency case is the + single most likely real-world regression and is currently untested anywhere. + **Add the new tests to the existing `tests/ir_from_manifest_tests.rs`** + (beside the static-deps cases `manifest_deps_populate_implicit_deps` / + `manifest_deps_do_not_contribute_to_recipe_inputs`, keeping all + deps-lowering tests in one place). Assert that each selected edge has + `implicit_deps` equal (unordered) to the declared/substituted `deps`, + `inputs` equal to `sources` only, `order_only_deps` carried through, the + expected per-`item` substitution, and that none of the *unselected* branch's + paths appear in any edge. (Cycle detection over `implicit_deps` is already + owned by `src/ir/cycle.rs` unit tests and is not re-tested here.) + +4. **Snapshot + real-ninja: conditional action deps reach the Ninja file.** + New test `conditional_action_deps_ninja_snapshot` **added to + `tests/ninja_snapshot_tests.rs`** using the fixture from B.3. Assert the + selected `build` line contains `| ` (implicit) **and** the + `|| ` segment (make `order_only_deps` definitely present in the + fixture so this is not "where applicable"); assert the unselected branch's + outputs/deps are absent; then `insta::assert_snapshot!` into + `tests/snapshots/ninja/` reusing the existing `Settings::set_snapshot_path`. + Use `insta::assert_snapshot!` only for the rendered Ninja text — never + `pretty_assertions::assert_eq!`. Where `ninja`/`python3` are available, also + run `ninja -t query`/`-n` to prove the file is valid and reaches a no-op + second pass, mirroring `touch_manifest_ninja_validation`; surface any skip + via `eprintln!` (captured by the harness) in addition to `tracing::warn!` so + a binary-less CI run does not silently validate nothing (see Risks / + Doggylump S2). + +5. **BDD: the combined conditional-action-with-deps scenario only.** + The unit (B.1), IR (B.3), and snapshot+real-ninja (B.4) layers already pin + selection and deps emission; existing BDD scenarios + (`tests/features/manifest.feature:132`, `tests/features/ninja.feature:31`) + already cover fallback-selection and deps-emission *separately*. To avoid + redundant step-matcher maintenance, B.5 adds **only** the genuinely new + externally-observable case: a single scenario where a + *conditionally-selected* action's `deps` appear as Ninja implicit + dependencies end-to-end. Reuse existing steps in + `tests/bdd/steps/conditional_manifest.rs` and `tests/bdd/steps/ninja.rs`; + add at most one new `Then` (the selected action exposes a given implicit + dependency) if no existing step fits. If even this is fully covered by + reframing an existing scenario, prefer reframing over a new scenario. + +Each of B.1–B.5 ends with the focused test passing, its sabotage check +recorded, and the relevant gate (`make test`) green. + +### Stage C — documentation + +- `docs/users-guide.md`: ensure the conditional-action / `deps` semantics are + described from a user's perspective (selection by tool availability; `deps` + as implicit dependencies that affect rebuilds but not recipe arguments). Add + a short worked nextest-vs-legacy example if not already present. (Behaviour + is unchanged, so this is clarification, not new UI.) +- `docs/developers-guide.md`: document the test conventions introduced here — + when to reach for `googletest`/`pretty_assertions` vs `ensure!`, the + `path_override`/`cwd_mode` pattern for deterministic `command_available` + tests, and the `is_impure()`-as-boundary-proxy idiom. +- Component architecture doc for the manifest/stdlib boundary: record the + port framing (executable-discovery vs command-execution) and that 3.14.5 pins + it. If the decision to standardize the boundary assertion is judged + substantive, capture it as an ADR using the `arch-decision-records` + Y-Statement format and reference it from this plan and the design doc. +- `docs/roadmap.md`: tick 3.14.5 and its four sub-bullets on completion. + +### Stage D — gates, review, finalize + +Run the full gate suite, then `coderabbit review --agent`; clear all concerns +before declaring done. Update Progress/Outcomes. + +## Concrete steps + +Run from the repository root. Use `tee` to a per-action log under `/tmp` so +truncated output can be reviewed: + +```bash +# focused red/green loop for a single new test (example) +cargo test --test ir_from_manifest_tests conditional_action_deps \ + 2>&1 | tee /tmp/test-netsuke-$(git branch --show-current).out +``` + +```bash +# in-crate (white-box) expansion tests live in the library target +cargo test --lib expand_test_cases::command_available \ + 2>&1 | tee /tmp/test-netsuke-$(git branch --show-current).out +``` + +```bash +# update/inspect snapshots deliberately (review before accepting) +cargo insta test --review # local authoring only +# CI / gates must never auto-accept a changed snapshot: +INSTA_UPDATE=no make test +``` + +```bash +# milestone gates (run sequentially to benefit from build caching) +make check-fmt 2>&1 | tee /tmp/check-fmt-netsuke-$(git branch --show-current).out +make typecheck 2>&1 | tee /tmp/typecheck-netsuke-$(git branch --show-current).out +make lint 2>&1 | tee /tmp/lint-netsuke-$(git branch --show-current).out +make test 2>&1 | tee /tmp/test-netsuke-$(git branch --show-current).out +``` + +Expected: each gate reports success; `make test` shows the new tests passing +and the existing suite unchanged. + +## Validation and acceptance + +Validation status for the current review follow-up is complete. +`make check-fmt`, `make lint-clippy`, `make typecheck`, and `make lint` passed; +the corrected generated-name selection run passed 2/2 after the requested +positional filter selected 0 tests, time passed 31/31, Ninja passed 7/7, IR +passed 23/23, doc coverage was 99.04%, and `make test` passed 2,654/2,654 tests +plus 33 doctests. + +Acceptance is behavioural: + +1. `make test` passes with the new tests present. +2. For each new test, the recorded **sabotage check** demonstrates the test + fails when its guarded production line is broken and passes when restored. + Representative sabotage points: + - selection: force `is_command_available` to always return `true` (or always + `false`) → the exactly-one-action and nextest/legacy tests must fail. + - no-shell: make conditional selection call `shell()` (or force + `impure` true) → the `is_impure()==false` test must fail. + - deps: make `process_targets` route `to_paths(&target.deps)` into `inputs` + instead of `implicit_deps` → the IR and Ninja deps tests must fail. +3. The new Ninja snapshot matches and, where `ninja`/`python3` are available, + the generated file builds and a second pass reports "no work to do". +4. BDD scenarios for selection and deps emission pass via the `bdd_tests` + harness. +5. Time parsing has explicit example-based regression cases for both `Z` and + `z`, each successfully producing UTC; property-based cases cover signed + numeric offsets below and at or beyond the 24-hour boundary. + +Quality criteria ("done"): + +- Tests: all new unit (`rstest` + `googletest`), integration, snapshot + (`insta`), and BDD (`rstest-bdd`) tests pass; existing tests unchanged. +- Lint/typecheck/format: `make lint`, `make typecheck`, `make check-fmt` clean + with `-D warnings`. +- Docs: users-guide, developers-guide, component architecture (and ADR if + created) updated; markdown lint clean. +- Review: `coderabbit review --agent` concerns all resolved after the current + documentation and test follow-up is validated. + +## Idempotence and recovery + +All steps are additive and re-runnable. Snapshots are created deliberately via +`cargo insta` and reviewed before acceptance; a wrong snapshot is corrected by +re-reviewing, not by force-accepting. Sabotage checks are always reverted +(`git checkout -- `) and never committed. Commit after each of B.1–B.5 so +any step can be rolled back independently. + +## Artefacts and notes + +Record here, as work proceeds: the exact surviving-action assertions, the +generated Ninja snapshot for the conditional-deps fixture, and the sabotage +transcripts (broken-line diff + failing test name + restored pass) that prove +each new test is non-vacuous. + +Stage A interoperability evidence: + +```plaintext +running 1 test +...googletest_runs_each_rstest_case_once::case_1 ... ok +test result: ok. 1 passed; 0 failed; 436 filtered out +``` + +The throwaway module and its wiring were removed immediately after this run. +The complete transcript is in +`/tmp/stage-a-googletest-rstest-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out`. + +Stage B.1 real-resolver and sabotage evidence: + +```plaintext +passing baseline: +2 passed; 0 failed; 436 filtered out + +temporary sabotage: +is_command_available(NotFound | DirectNotFound): Ok(false) -> Ok(true) +case_2_nextest_absent failed at the surviving-action name assertion: +expected "run-tests-legacy" +actual "run-tests-nextest" +1 passed; 1 failed; 436 filtered out +``` + +The production line was restored immediately with an explicit inverse patch; +`git diff -- src/stdlib/which/mod.rs` was empty afterwards. Transcripts: +`/tmp/b1-focused-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out` +and +`/tmp/b1-sabotage-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out`. + +Stage B.2 impurity-boundary and sabotage evidence: + +```plaintext +passing baseline: +2 passed; 0 failed; 438 filtered out + +temporary sabotage: +StdlibState::is_impure(): load(...) -> !load(...) +command_available_fallback_does_not_invoke_impure_helpers: + expected false, actual true +shell_in_when_marks_selection_impure: + expected true, actual false +0 passed; 2 failed; 438 filtered out +``` + +The production line was restored immediately with an explicit inverse patch. +Transcripts: +`/tmp/b2-focused-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out` +and +`/tmp/b2-sabotage-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out`. + +Stage B.3 IR-lowering and sabotage evidence: + +```plaintext +passing baseline: +1 passed; 0 failed; 15 filtered out + +temporary sabotage: +implicit_deps = to_paths(&target.deps) -> Vec::new() +conditional_action_deps_populate_distinct_ir_classes: + fallback-alpha expected ["build/alpha.o", "shared/action.cfg"] + actual [] +0 passed; 1 failed; 15 filtered out +``` + +The production line was restored immediately with an explicit inverse patch. +Transcripts: +`/tmp/b3-focused-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out` +and +`/tmp/b3-sabotage-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out`. + +Stage B.4 Ninja-emission and sabotage evidence: + +```plaintext +passing baseline: +1 passed; 0 failed; 3 filtered out +snapshot matched; Ninja query/build passed; second pass was a no-op + +temporary sabotage: +write implicit deps as " {}" instead of " | {}" +conditional_action_deps_ninja_snapshot: + fallback-alpha build line lacked "|" before build/alpha.o +0 passed; 1 failed; 3 filtered out +``` + +The production line was restored immediately with an explicit inverse patch. +Transcripts: +`/tmp/b4-focused-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out` +and +`/tmp/b4-sabotage-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out`. + +Stage B.5 combined-BDD and sabotage evidence: + +```plaintext +passing baseline: +features_scenarios::ninja_selected_conditional_action_deps_become_implicit_ninja_dependencies +1 passed; 0 failed; 241 filtered out + +temporary sabotage: +write implicit deps as " {}" instead of " | {}" +failing step 5: +Then the ninja file contains +" src/alpha.in | build/alpha.o shared/action.cfg || order/alpha.stamp" +0 passed; 1 failed; 241 filtered out +``` + +The production line was restored immediately with an explicit inverse patch. +Transcripts: +`/tmp/b5-focused-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out` +and +`/tmp/b5-sabotage-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out`. + +## Interfaces and dependencies + +New `[dev-dependencies]` in `Cargo.toml` (pre-authorized by the brief): + +```toml +# Cargo.toml +googletest = "0.14" +pretty_assertions = "1.4" +``` + +Existing crates reused: `rstest` 0.26, `rstest-bdd`/`rstest-bdd-macros` 0.5, +`insta` 1 (yaml), `tempfile`, `test_support`, `anyhow` (`ensure!`). +(`serial_test` is **not** needed — the new tests build their own environments +and inject `path_override`, so they never touch process-global PATH; see the +env-scoped-cache Decision.) + +Seams relied upon (all already public/`pub(crate)` and present; **no new seam +required**): + +- `crate::manifest::expand::expand_foreach` accepts a mutable `ManifestValue` + and an `Environment`, then returns `anyhow::Result`. It is + `pub(crate)`, which forces in-crate placement of the selection/no-shell tests. +- `crate::stdlib::register_with_config(&mut Environment, StdlibConfig) -> anyhow::Result` + (`pub`; note the `Result`), + `StdlibConfig::with_path_override(impl Into)` (`pub`, + `src/stdlib/config.rs:240`), and `StdlibState::is_impure(&self) -> bool` + (`pub`). Do **not** reference `WhichConfig` (private module). +- `netsuke::ir::BuildGraph::from_manifest` and + `netsuke::ninja_gen::generate(&BuildGraph) -> Result` + are public seams for the integration/snapshot tests. + +New/changed test files and fixtures (final names to be confirmed in Stage A): + +- new `src/manifest/expand_test_cases/command_available_selection_cases.rs` +- new `src/manifest/expand_test_cases/command_available_no_shell_cases.rs` + (both wired into the `expand_test_cases` module). +- new `tests/data/conditional_action_deps.yml`. +- additions to existing `tests/ir_from_manifest_tests.rs` (deps lowering) and + `tests/ninja_snapshot_tests.rs` (Ninja snapshot + real-ninja) — kept beside + their static-deps counterparts, not in a separate file. +- new + `tests/snapshots/ninja/ninja_snapshot_tests__conditional_action_deps_ninja.snap`. +- one new combined scenario in `tests/features/ninja.feature` (or + `manifest.feature`) plus at most one new step. +- new `[dev-dependencies]` entries `googletest = "0.14"` and + `pretty_assertions = "1.4"` in `Cargo.toml`. + +## Open questions for the approver + +1. **googletest.** The brief mandates googletest; the community-of-experts + review unanimously recommended dropping it (keeping only + `pretty_assertions`) because it had no prior use here and the assertions are + trivial. Stage A has now verified `#[googletest::test]`+`#[rstest]` + interoperability under `rstest` 0.26.1. This plan keeps googletest in a + confined form to respect the brief. The approver may instead choose: (a) + confined + spike-gated (as planned), (b) `pretty_assertions` only, no + googletest, or (c) googletest throughout. +2. **BDD scope.** Reviewers judged a full BDD layer largely redundant; the plan + reduces B.5 to the single combined scenario. Confirm this is the desired + depth, or request fuller BDD coverage. + +## Signposted documentation and skills + +Documentation to consult while implementing: + +- `docs/netsuke-design.md` §2.5 (manifest-time `foreach`/`when` semantics), + §2.4 and §5.3 (dependency classes and Ninja lowering), and the "executable + discovery" section (`command_available`). +- `docs/rust-testing-with-rstest-fixtures.md` — fixture and `#[case]` patterns. +- `docs/reliable-testing-in-rust-via-dependency-injection.md` — the + `path_override`/resolver-injection approach used for deterministic + `command_available` tests. +- `docs/rust-doctest-dry-guide.md` — if any doc examples are added. +- `docs/rstest-bdd-users-guide.md` — feature/step wiring for the BDD additions. +- `docs/ortho-config-users-guide.md` — referenced by the brief; explicitly + out of scope here (see Decision Log). + +Skills to load while implementing: + +- `rust-router` then `rust-unit-testing` (assertion helpers, fixtures, table + tests, `googletest`/`pretty_assertions`/`insta` usage). +- `hexagonal-architecture` — to keep the boundary assertions honest. +- `nextest` — for the nextest-vs-legacy framing and running the suite. +- `leta` — for navigation/refactors. +- `arch-decision-records` — only if the boundary decision warrants an ADR. +- `proptest` / `kani` — *not* used here; their invariants are owned by 4.3.2 / + 4.2.x (recorded in the Decision Log). + +## Revision note + +- 2026-08-30 — Recorded the registration-wide + `register_legacy_boolean_formatter` policy and its user-visible contract: + Boolean interpolation in string fields retains lowercase `true`/`false`, + while non-Boolean values use MiniJinja's normal formatter. Added the + corresponding users' guide example and retained the `parse_offset` guard as + the production compatibility boundary. + +- 2026-08-31 — Review follow-up records that `parse_offset` rejects parsed + offsets whose absolute whole-hour component is 24 or greater, aligning + `now(offset=...)` with the ISO 8601 civil-day limit. User documentation and + property-based boundary coverage are present. Explicit example-based `Z` and + `z` cases now assert UTC, and the updated documentation and test follow-up + passes its complete validation stack. + +- 2026-06-15 — Revised after a community-of-experts review (Logisphere crew: + structural, contract/correctness, alternatives/DX, reliability/ops lenses). + What changed and why: + - Corrected the injection seam: `WhichConfig` is in a private module and + unreachable; switched to the public `StdlibConfig::with_path_override` + +`register_with_config` pairing (precedent in + `tests/which_diagnostic_snapshot_tests.rs`). Removed the speculative + test-only seam. In-crate placement is now justified solely by + `expand_foreach` being `pub(crate)`. + - Restated the `is_impure()` boundary proxy honestly: it flips for `shell()`, + `grep()`, *and* `fetch()`; downgraded the hexagonal claim accordingly. + - Added a Stage A `#[googletest::test]`+`#[rstest]` interop spike (rstest + 0.26.1 risk) + with a defined fallback; confined googletest to the two white-box files and + surfaced the crew's "drop googletest" recommendation as an approver choice. + - Hardened determinism: the absent case now requires empty `path_override` + + guaranteed-absent name + `cwd_mode="never"` together. + - Fixed the `shell()` control sub-case to live in a `when:` expression + (expansion evaluates `when`/`foreach`, not `command:` recipes) and removed + its binary-availability gate (the impure flag is set eagerly). + - Added the `item`-in-`deps` interaction (highest-value uncaught regression), + pinned action-level `foreach` explicitly, and folded the IR/Ninja tests + into existing files. Trimmed the BDD layer to the single combined scenario. + - Added CI snapshot discipline (`INSTA_UPDATE=no`), skip visibility via + `eprintln!`, the env-scoped-cache note (no `serial_test`), corrected + `register_with_config -> anyhow::Result`, and an + "Open questions for the approver" section. + Effect on remaining work: scope and file count are essentially unchanged; the + plan is now anchored to reachable seams and verifiable assumptions. No code + has been written; the plan remains in DRAFT pending approval. + +- 2026-07-28 — Rebased onto `origin/main`. Updated the plan for the repository's + `rstest` 0.26.1 upgrade and removed the resolved `mod_backup.rs` cleanup + question because that file was deleted upstream. No implementation scope or + architectural boundary changed. + +- 2026-07-28 — Marked the approved plan in progress and recorded Stage A. + Confirmed the existing seams, deterministic dependency emission order, and + googletest/rstest interoperability with the approver-specified attribute + order. Remaining work starts at Stage B.1; no new test seam is required. + +- 2026-07-28 — Completed Stage B.1 implementation and its non-vacuity check. + Added deterministic present/absent real-resolver cases, including the + action-level `foreach` interaction, and recorded the assertion-level sabotage + failure. Remaining work starts at Stage B.2. + +- 2026-07-28 — Completed Stage B.2 implementation and its non-vacuity check. + Added a pure absent-command selection case and a cross-platform shell + control, then recorded both assertion-level failures under an inverted + impurity observable. Remaining work starts at Stage B.3. + +- 2026-07-28 — Completed Stage B.3 implementation and its non-vacuity check. + Added the combined conditional dependency fixture and verified selected + foreach actions and the selected target lower dependency classes correctly. + Recorded the direct-path determinism decision and assertion-level sabotage + failure. Remaining work starts at Stage B.4. + +- 2026-07-28 — Completed Stage B.4 implementation and its non-vacuity check. + Added the conditional-dependency Ninja snapshot, explicit dependency-class + assertions, and real-Ninja query/build/no-op validation. Recorded the + `.ninja_log` finding and missing-separator sabotage failure. Remaining work + starts at Stage B.5. + +- 2026-07-28 — Completed Stage B.5 and Stage B overall. Added one combined BDD + scenario using only existing steps, then recorded the public Gherkin + missing-separator failure. Remaining work starts at Stage C documentation. + +- 2026-07-28 — Drafted Stage C documentation across the users' guide, + developers' guide, design, and roadmap. Recorded that no ADR is warranted + because this work pins existing boundaries rather than creating a new + architectural decision. Stage C remains pending until its gates and + CodeRabbit review pass. + +- 2026-07-29 — Completed Stage C after the full gate stack and CodeRabbit + returned clean. Remaining work is Stage D final branch-wide validation, + review, publication, and ExecPlan closure. + +- 2026-07-29 — Completed Stage D and closed the ExecPlan. The final branch-wide + gate stack passed with snapshot mutation disabled, and CodeRabbit returned + zero actionable findings across the complete committed diff. + +- 2026-08-31 — Recorded the `now(offset=...)` compatibility guard and its + user-facing contract after review. Validation scope now includes `Z`/`z`, + signed offsets below 24 hours, and rejection of both signs at `24:00` and + beyond; the explicit examples, property cases, and full gate stack now pass + as recorded under Validation and acceptance. + +- 2026-08-31 — Rebased onto `origin/chore/enforce-markdown-table-formatting`. + Kept the target's canonical `mdtablefix` checker instead of replaying older + duplicate formatter commits, then reset `Cargo.lock` to the target baseline + and regenerated it before the rebase gates. diff --git a/docs/netsuke-design.md b/docs/netsuke-design.md index c7d9c7641..e19db1bf4 100644 --- a/docs/netsuke-design.md +++ b/docs/netsuke-design.md @@ -481,6 +481,17 @@ therefore contain only the selected entries. Build-time branching remains the responsibility of the recipe command or script unless a separate future feature explicitly models runtime conditions. +Conditional selection crosses the executable-discovery boundary only. +`command_available(...)` drives the existing `which` resolver and converts a +typed search miss into `false`; it does not drive the command-execution +boundary used by `shell()`. Regression tests observe this separation through +`StdlibState::is_impure()`: a minimal availability-only selection keeps the +flag clear, while a shell-in-`when` control sets it. The flag also covers other +impure helpers such as `grep()` and `fetch()`, so it is intentionally a broader +assertion that no impure stdlib helper ran during selection, rather than a +shell-specific invocation count. No new port or adapter is introduced for this +test contract. + ```yaml - foreach: glob('assets/svg/*.svg') when: item | basename != 'logo.svg' @@ -1238,6 +1249,13 @@ filters, and functions are available to every template and give concise access to common filesystem queries, path manipulations, collection utilities, and network operations. +The standard-library registration boundary installs one MiniJinja value +formatter for all helpers. Boolean results interpolated into string fields +retain the historical lowercase `true`/`false` spelling; every non-Boolean +value continues through MiniJinja's `escape_formatter`. This formatter is owned +by standard-library registration and is reused across calls, so helpers must +not introduce per-helper or per-call formatter variants. + #### File-system tests | Test | True when the operand… | @@ -2096,7 +2114,7 @@ This transformation involves several steps: traversal are logged, collected, and returned alongside any cycle to aid diagnostics. -### 5.4 Ninja file synthesis (`src/ninja_gen/mod.rs`) — direct generation +### 5.4 Ninja file synthesis (`src/ninja_gen/mod.rs`) The final step is to synthesize the `build.ninja` file from the `BuildGraph` IR. This process is a straightforward, mechanical translation from the IR data @@ -2164,278 +2182,7 @@ structures to the Ninja file syntax. build my_app: link foo.o bar.o | lib_dependency.a ``` - A `BuildEdge` whose `dependency_order` is `serial` and has more than one - implicit dependency is an exception to this direct rendering. The generator - lowers it into staged phony gates, with one content-addressed Ninja dyndep - sidecar per dependency. A gate can reveal exactly one real dependency; the - gate edge associated with the next sidecar depends on the preceding gate. - This makes each later dependency unavailable to Ninja until the previous one - succeeds, while preserving one Ninja scheduler and its shared-work - memoization. The runner materializes every sidecar file before Ninja starts; - no Ninja edge produces sidecar content. - - The generated result is a bundle, not merely a string: generation is an - effect-free query that returns the main Ninja text and its `.netsuke/dyndep` - sidecars. Each runner command then materializes those sidecars through an - injected effective-working-directory capability before it writes or runs the - main file. The main file declares `ninja_required_version = 1.10` only when - it contains such staged serial ordering. `.netsuke/serial` and - `.netsuke/dyndep` are reserved for generated state. `serial` applies only to - direct implicit dependencies; it does not delay an independently reachable - node elsewhere in the graph. - -Figure: Runner-owned serial dyndep bundle generation and execution. - -```mermaid -sequenceDiagram - accTitle: Runner-owned serial dependency generation and execution - accDescr { - The runner generates a Ninja bundle and materializes its dyndep sidecars. - Generate writes the manifest without invoking Ninja. Build invokes Ninja - for execution, and clean invokes Ninja in clean tool mode. - } - actor User - participant Runner as runner.generate_ninja - participant NinjaGen as ninja_gen.generate_bundle - participant Dyndep as runner.materialize_dyndep_bundle - participant Ninja - - User->>Runner: netsuke build / clean / generate - Runner->>NinjaGen: generate_bundle(graph) - NinjaGen-->>Runner: GeneratedNinja (build_file, dyndep_files) - Runner->>Dyndep: materialize_dyndep_bundle(cli, bundle) - Dyndep-->>Runner: dyndep sidecars materialized - alt generate - Runner-->>User: write generated Ninja manifest without invoking Ninja - else build - Runner->>Ninja: invoke with bundle.build_file() - Ninja-->>User: serial deps run in order, parallel elsewhere - else clean - Runner->>Ninja: invoke with bundle.build_file() in clean tool mode - Ninja-->>User: clean completed - end -``` - -The runner holds a capability-scoped exclusive lease on the dyndep directory -from sidecar materialization through Ninja consumption or generated-output -consumption. While the lease is held, stale `.tmp` files are removed and -retention preserves the current bundle plus at most 32 obsolete `.dd` files and -1 MiB of obsolete `.dd` bytes. `build` and `generate` prune after -materialization; `clean` prunes only after successful `ninja -t clean` and not -on failure. Sidecars remain immutable and content-addressed. Consequently, an -older arbitrary `generate --output` manifest may lose its sidecars after a -later command and must be regenerated. See -[ADR-012](adr-012-bound-dyndep-sidecar-retention.md) for this policy. - -4\. **Write Defaults:** Finally, write the `default` statement, listing all -paths from `graph.default_targets`. - -```ninja -default my_app -``` - -### 5.4 Ninja file synthesis (`src/ninja_gen/mod.rs`) — serial generation - -The final step is to synthesize the `build.ninja` file from the `BuildGraph` -IR. This process is a straightforward, mechanical translation from the IR data -structures to the Ninja file syntax. - -1. **Write Variables:** Any global variables that need to be passed to Ninja can - be written at the top of the file (e.g., `msvc_deps_prefix` for Windows - -2. **Write Rules:** Sort `graph.actions` by action ID and write a corresponding - Ninja `rule` statement for each executable `ir::Action`. Dependency-only - actions are omitted because they have no command to execute. Their edges - select Ninja's built-in `phony` rule. The IR already contains ordinary - command text: its input and output paths have replaced Netsuke's `ins`/ - `outs` and `$in`/`$out` placeholders during lowering. Scalar commands are - emitted as-is. List commands are emitted as the brace-group, `eval`, and - `&&` chain described in §2.3, including the bounded failure marker for each - one-based entry. - - When an action's `recipe` is a script, the generated rule wraps the script - in the configured platform-specific interpreter. Unix uses `/bin/sh -e -c`, - while Windows defaults to `powershell -Command`; an explicit manifest - `interpreter` overrides the platform default. - - Command and script text must be converted from IR text to backend text at - this stage. After Netsuke placeholders have been resolved, remaining literal - dollar signs are escaped as `$$` for Ninja so shell variables survive to the - shell. Structured `exec` recipes are rendered by quoting each argv element - as one argument for the selected backend. - - Resolved environment bindings are emitted as backend-specific command - prefixes or generated wrapper script assignments. The implementation must - avoid exposing Ninja variables as the user-facing environment API. - - Code snippet - - ```ninja - # Generated from an ir::Action - rule cc - command = gcc -c -o $out $in - description = CC $out - ``` - - The planned `deps_from` manifest field will populate `ir::Action.depfile` and - `ir::Action.deps_format`, allowing this rule writer to emit Ninja's - `depfile` and `deps` attributes without overloading target prerequisites. - -3. **Write Build Edges:** Iterate through the `graph.targets` map. For each - `ir::BuildEdge`, write a corresponding Ninja `build` statement. This - involves formatting the lists of explicit outputs, implicit outputs, inputs, - implicit dependencies, and order-only dependencies using the correct Ninja - syntax (`:`, `|`, and `||`).[^7] Use Ninja's built-in `phony` rule when - `phony` is `true`. For an `always` edge, either generate a `phony` build - with no outputs or emit a dummy output marked `restat = 1` and depend on a - permanently dirty target so the command runs on each invocation. - - Code snippet - - ```ninja - # Generated from an ir::BuildEdge - build foo.o: cc foo.c - build bar.o: cc bar.c - build my_app: link foo.o bar.o | lib_dependency.a - ``` - - A `BuildEdge` whose `dependency_order` is `serial` and has more than one - implicit dependency is an exception to this direct rendering. The generator - lowers it into staged phony gates, with one content-addressed Ninja dyndep - sidecar per dependency. A gate can reveal exactly one real dependency; the - gate edge associated with the next sidecar depends on the preceding gate. - This makes each later dependency unavailable to Ninja until the previous one - succeeds, while preserving one Ninja scheduler and its shared-work - memoization. The runner materializes every sidecar file before Ninja starts; - no Ninja edge produces sidecar content. - - The generated result is a bundle, not merely a string: generation is an - effect-free query that returns the main Ninja text and its `.netsuke/dyndep` - sidecars. Each runner command then materializes those sidecars through an - injected effective-working-directory capability before it writes or runs the - main file. The main file declares `ninja_required_version = 1.10` only when - it contains such staged serial ordering. `.netsuke/serial` and - `.netsuke/dyndep` are reserved for generated state. `serial` applies only to - direct implicit dependencies; it does not delay an independently reachable - node elsewhere in the graph. - -Figure: Runner-owned serial dyndep bundle generation and execution. - -```mermaid -sequenceDiagram - accTitle: Runner-owned serial dependency generation and execution - accDescr { - The runner generates a Ninja bundle and materializes its dyndep sidecars. - Generate writes the manifest without invoking Ninja. Build invokes Ninja - for execution, and clean invokes Ninja in clean tool mode. - } - actor User - participant Runner as runner.generate_ninja - participant NinjaGen as ninja_gen.generate_bundle - participant Dyndep as runner.materialize_dyndep_bundle - participant Ninja - - User->>Runner: netsuke build / clean / generate - Runner->>NinjaGen: generate_bundle(graph) - NinjaGen-->>Runner: GeneratedNinja (build_file, dyndep_files) - Runner->>Dyndep: materialize_dyndep_bundle(cli, bundle) - Dyndep-->>Runner: dyndep sidecars materialized - alt generate - Runner-->>User: write generated Ninja manifest without invoking Ninja - else build - Runner->>Ninja: invoke with bundle.build_file() - Ninja-->>User: serial deps run in order, parallel elsewhere - else clean - Runner->>Ninja: invoke with bundle.build_file() in clean tool mode - Ninja-->>User: clean completed - end -``` - -The runner holds a capability-scoped exclusive lease on the dyndep directory -from sidecar materialization through Ninja consumption or generated-output -consumption. While the lease is held, stale `.tmp` files are removed and -retention preserves the current bundle plus at most 32 obsolete `.dd` files and -1 MiB of obsolete `.dd` bytes. `build` and `generate` prune after -materialization; `clean` prunes only after successful `ninja -t clean` and not -on failure. Sidecars remain immutable and content-addressed. Consequently, an -older arbitrary `generate --output` manifest may lose its sidecars after a -later command and must be regenerated. See -[ADR-012](adr-012-bound-dyndep-sidecar-retention.md) for this policy. - -4\. **Write Defaults:** Finally, write the `default` statement, listing all -paths from `graph.default_targets`. - -```ninja -default my_app -``` - -### 5.4 Ninja file synthesis (`src/ninja_gen/mod.rs`) — generated bundles - -The final step is to synthesize the `build.ninja` file from the `BuildGraph` -IR. This process is a straightforward, mechanical translation from the IR data -structures to the Ninja file syntax. - -1. **Write Variables:** Any global variables that need to be passed to Ninja can - be written at the top of the file (e.g., `msvc_deps_prefix` for Windows - -2. **Write Rules:** Sort `graph.actions` by action ID and write a corresponding - Ninja `rule` statement for each executable `ir::Action`. Dependency-only - actions are omitted because they have no command to execute. Their edges - select Ninja's built-in `phony` rule. The IR already contains ordinary - command text: its input and output paths have replaced Netsuke's `ins`/ - `outs` and `$in`/`$out` placeholders during lowering. Scalar commands are - emitted as-is. List commands are emitted as the brace-group, `eval`, and - `&&` chain described in §2.3, including the bounded failure marker for each - one-based entry. - - When an action's `recipe` is a script, the generated rule wraps the script - in the configured platform-specific interpreter. Unix uses `/bin/sh -e -c`, - while Windows defaults to `powershell -Command`; an explicit manifest - `interpreter` overrides the platform default. - - Command and script text must be converted from IR text to backend text at - this stage. After Netsuke placeholders have been resolved, remaining literal - dollar signs are escaped as `$$` for Ninja so shell variables survive to the - shell. The conversion rejects newline, carriage-return, and NUL characters - and will accept a completed shell-text value once. Structured `exec` recipes - are rendered by quoting each argv element as one argument for the selected - backend. Metadata fields are escaped at their Ninja emission boundary, while - the IR remains backend-neutral. - - Resolved environment bindings are emitted as backend-specific command - prefixes or generated wrapper script assignments. The implementation must - avoid exposing Ninja variables as the user-facing environment API. - - Code snippet - - ```ninja - # Generated from an ir::Action - rule cc - command = gcc -c -o $out $in - description = CC $out - ``` - - The planned `deps_from` manifest field will populate `ir::Action.depfile` and - `ir::Action.deps_format`, allowing this rule writer to emit Ninja's - `depfile` and `deps` attributes without overloading target prerequisites. - -3. **Write Build Edges:** Iterate through the `graph.targets` map. For each - `ir::BuildEdge`, write a corresponding Ninja `build` statement. This - involves formatting the lists of explicit outputs, implicit outputs, inputs, - implicit dependencies, and order-only dependencies using the correct Ninja - syntax (`:`, `|`, and `||`).[^7] Use Ninja's built-in `phony` rule when - `phony` is `true`. For an `always` edge, either generate a `phony` build - with no outputs or emit a dummy output marked `restat = 1` and depend on a - permanently dirty target so the command runs on each invocation. - - Code snippet - - ```ninja - # Generated from an ir::BuildEdge - build foo.o: cc foo.c - build bar.o: cc bar.c - build my_app: link foo.o bar.o | lib_dependency.a - ``` +#### Serial dependency ordering and generated bundles A `BuildEdge` whose `dependency_order` is `serial` and has more than one implicit dependency is an exception to this direct rendering. The generator diff --git a/docs/roadmap.md b/docs/roadmap.md index b8ae58c52..241be7a0f 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -232,19 +232,13 @@ and agents. - [x] Return `false` for absent commands instead of raising `netsuke::jinja::which::not_found`. - [x] Preserve argument validation diagnostics for invalid options. -- [ ] 3.14.5. Add regression coverage for conditional action dependency - manifests. - - [ ] Test action-level `when` and action-level `foreach`. - - [ ] Test complementary nextest and legacy branches select exactly one +- [x] 3.14.5. Add regression coverage for conditional action dependency + manifests. Depends on 3.14.2, 3.14.3, and 3.14.4. + - [x] Test action-level `when` and action-level `foreach`. + - [x] Test complementary nextest and legacy branches select exactly one action. - - [ ] Test absent-command fallback without invoking `shell()`. - - [ ] Test `deps` lowering in the IR and emitted Ninja build statements. - - Note: the manifest-expansion building blocks already exist in - `src/manifest/expand_test_cases/action_condition_cases.rs` and the BDD - scenarios in `tests/bdd/steps/conditional_manifest.rs`. The outstanding gap - is end-to-end coverage that traces a conditionally selected action through - `deps` lowering into emitted Ninja, plus the named nextest-versus-legacy - scenario (existing tests use a generic `preferred-tool`). + - [x] Test absent-command fallback without invoking `shell()`. + - [x] Test `deps` lowering in the IR and emitted Ninja build statements. - [ ] 3.14.6. Add rule-level `deps_from` for compiler dependency imports. Requires 3.14.3. See [netsuke-design.md §2.3](netsuke-design.md#planned-compiler-dependency-import). diff --git a/docs/stdlib-yaml-and-jinja-guide.md b/docs/stdlib-yaml-and-jinja-guide.md index c7e1a5d23..13848c887 100644 --- a/docs/stdlib-yaml-and-jinja-guide.md +++ b/docs/stdlib-yaml-and-jinja-guide.md @@ -260,9 +260,11 @@ defaults: ## Work with time - `now([offset=...])` is host-observing and returns the current timestamp. - With no offset it uses UTC; `offset` accepts `Z` or a signed offset such as - `+02:00`. The value exposes `iso8601`, `unix_timestamp`, and `offset`. - Example: `{{ now(offset='+02:00').iso8601 }}`. + With no offset it uses UTC; `offset` accepts `Z` or `z` for UTC and signed + ISO 8601 numeric offsets such as `+02:00`. The absolute whole-hour component + must be below 24; `+24:00` and `-24:00` are invalid and return the existing + invalid-offset error. The value exposes `iso8601`, `unix_timestamp`, and + `offset`. Example: `{{ now(offset='+02:00').iso8601 }}`. - `timedelta(**components)` is pure. It accepts `weeks`, `days`, `hours`, `minutes`, `seconds`, `milliseconds`, `microseconds`, and `nanoseconds`. Every component defaults to zero and may be negative. The result exposes diff --git a/docs/users-guide.md b/docs/users-guide.md index 72dbd2c44..23e95192c 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -627,10 +627,14 @@ netsuke_version: "1.0.0" actions: - name: test-fast command: "cargo nextest run" + deps: + - config/test-profile.toml when: command_available("cargo-nextest") - name: test-fast command: "cargo test" + deps: + - config/test-profile.toml when: not command_available("cargo-nextest") targets: [] @@ -639,6 +643,12 @@ defaults: - test-fast ``` +Netsuke evaluates both guards while loading the manifest, without running +either recipe, so exactly one `test-fast` action enters the build graph. The +selected action's `deps` become Ninja implicit dependencies: changes to +`config/test-profile.toml` make the action stale, but the path is not appended +to `cargo nextest run` or `cargo test` as a recipe argument. + Both helpers accept: - `all=true`: return all `which` matches. It does not change the boolean result @@ -814,6 +824,26 @@ every helper's signature, defaults, purity, platform caveats, and executable examples. Host-observing helpers belong only in trusted manifests: Netsuke bounds command and network output, but does not sandbox template evaluation. +When a Boolean is interpolated into a string field, Netsuke renders it as +lowercase `true` or `false`. For example, this writes `true` to `status.txt`: + + + +```yaml +netsuke_version: "1.0.0" + +vars: + enabled: true + +targets: + - name: status.txt + command: "printf '%s\\n' '{{ enabled }}' > {{ outs }}" +``` + +The `now(offset=...)` helper accepts `Z` or `z` for UTC and signed ISO 8601 +offsets whose absolute hour component is below 24. Offsets such as `+24:00`, +`-24:00`, and larger absolute hour values are rejected as invalid. + One helper deserves a note here because its result depends on the host's environment. `path | expanduser` expands a leading `~` against the home directory, resolved from `HOME` then `USERPROFILE` on POSIX hosts, and from diff --git a/src/diagnostic_json_tests.rs b/src/diagnostic_json_tests.rs index 5230046a1..ca4e375a8 100644 --- a/src/diagnostic_json_tests.rs +++ b/src/diagnostic_json_tests.rs @@ -139,7 +139,7 @@ fn render_plain_error_json_records_cause_chain() -> Result<()> { .get("labels") .context("labels should be present")?; - ensure!(schema_version == 1, "schema version should be stable",); + ensure!(schema_version == 1, "schema version should be stable"); ensure!( generator_name == "netsuke", "generator name should be present", diff --git a/src/manifest/expand_test_cases/command_available_no_shell_cases.rs b/src/manifest/expand_test_cases/command_available_no_shell_cases.rs new file mode 100644 index 000000000..7b1acb8e2 --- /dev/null +++ b/src/manifest/expand_test_cases/command_available_no_shell_cases.rs @@ -0,0 +1,116 @@ +//! Boundary coverage for pure command-availability selection. + +use std::ffi::OsString; + +use anyhow::{Context, Result, anyhow}; +use camino::Utf8PathBuf; +use cap_std::{ambient_authority, fs_utf8::Dir}; +use googletest::prelude::*; +use minijinja::Environment; +use rstest::rstest; + +use super::{actions, expand_foreach}; +use crate::{ + manifest::ManifestValue, + stdlib::{self, StdlibConfig, StdlibState}, +}; + +const ABSENT_COMMAND: &str = "netsuke-guaranteed-absent-command-no-shell-3-14-5"; + +struct ExpansionWorld { + _temp: tempfile::TempDir, + environment: Environment<'static>, + state: StdlibState, +} + +impl ExpansionWorld { + fn new() -> Result { + let temp = tempfile::tempdir().context("create expansion workspace")?; + let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) + .map_err(|path| anyhow!("temporary path should be UTF-8: {path:?}"))?; + let workspace = Dir::open_ambient_dir(&root, ambient_authority()) + .with_context(|| format!("open expansion workspace {root}"))?; + let config = StdlibConfig::new(workspace)? + .with_workspace_root_path(root)? + .with_path_override(OsString::new()); + let mut environment = Environment::new(); + let state = stdlib::register_with_config(&mut environment, config)?; + Ok(Self { + _temp: temp, + environment, + state, + }) + } +} + +#[googletest::test] +#[rstest] +fn command_available_fallback_does_not_invoke_impure_helpers() -> googletest::Result<()> { + let world = ExpansionWorld::new().or_fail()?; + let yaml = format!( + "actions: + - name: unavailable-tool + command: unavailable-tool + when: command_available({ABSENT_COMMAND:?}, cwd_mode='never') + - name: fallback + command: cargo test + when: not command_available({ABSENT_COMMAND:?}, cwd_mode='never') +targets: [] +" + ); + let mut doc: ManifestValue = serde_saphyr::from_str(&yaml).or_fail()?; + + expand_foreach(&mut doc, &world.environment).or_fail()?; + + let selected_actions = actions(&doc).or_fail()?; + verify_that!(selected_actions, len(eq(1)))?; + let selected = selected_actions.first().or_fail()?; + let selected_map = selected.as_object().or_fail()?; + verify_that!(string_field(selected, "name").or_fail()?, eq("fallback"))?; + verify_that!(selected_map.contains_key("when"), eq(false))?; + verify_that!(world.state.is_impure(), eq(false))?; + Ok(()) +} + +#[googletest::test] +#[rstest] +fn shell_in_when_marks_selection_impure() -> googletest::Result<()> { + let world = ExpansionWorld::new().or_fail()?; + let command = shell_control_command(); + let yaml = format!( + "actions: + - name: shell-control + command: echo control + when: >- + ('' | shell({command:?})) == '' +targets: [] +" + ); + let mut doc: ManifestValue = serde_saphyr::from_str(&yaml).or_fail()?; + + expand_foreach(&mut doc, &world.environment).or_fail()?; + + verify_that!(actions(&doc).or_fail()?, len(eq(1)))?; + verify_that!(world.state.is_impure(), eq(true))?; + Ok(()) +} + +fn string_field<'a>(value: &'a ManifestValue, field: &str) -> Result<&'a str> { + value + .as_object() + .context("selected action map")? + .get(field) + .and_then(ManifestValue::as_str) + .with_context(|| format!("selected action {field}")) +} + +const fn shell_control_command() -> &'static str { + #[cfg(windows)] + { + "exit /b 0" + } + #[cfg(not(windows))] + { + ":" + } +} diff --git a/src/manifest/expand_test_cases/command_available_selection_cases.rs b/src/manifest/expand_test_cases/command_available_selection_cases.rs new file mode 100644 index 000000000..22ee9521e --- /dev/null +++ b/src/manifest/expand_test_cases/command_available_selection_cases.rs @@ -0,0 +1,149 @@ +//! Real-resolver coverage for complementary action selection. + +use std::ffi::OsString; + +use anyhow::{Context, Result, anyhow}; +use camino::Utf8PathBuf; +use cap_std::{ambient_authority, fs_utf8::Dir}; +use googletest::prelude::*; +use minijinja::Environment; +use rstest::rstest; +use test_support::exec::write_exec_with_content; + +use super::{actions, expand_foreach}; +use crate::{ + manifest::ManifestValue, + stdlib::{self, StdlibConfig}, +}; + +const ABSENT_COMMAND: &str = "netsuke-guaranteed-absent-command-3-14-5"; + +#[cfg(windows)] +const NEXTEST_TOOL_NAME: &str = "cargo-nextest.cmd"; +#[cfg(not(windows))] +const NEXTEST_TOOL_NAME: &str = "cargo-nextest"; +#[cfg(windows)] +const NEXTEST_TOOL_CONTENT: &str = "@echo off\r\n"; +#[cfg(not(windows))] +const NEXTEST_TOOL_CONTENT: &str = "#!/bin/sh\nexit 0\n"; + +/// Capture the selected action's observable expansion result. +struct ExpectedAction { + name: &'static str, + command: &'static str, + has_iteration_vars: bool, +} + +impl ExpectedAction { + /// Build the expected action selected when `cargo-nextest` is available. + const fn nextest() -> Self { + Self { + name: "run-tests-nextest", + command: "cargo nextest run", + has_iteration_vars: true, + } + } + + /// Build the expected fallback action when `cargo-nextest` is unavailable. + const fn legacy() -> Self { + Self { + name: "run-tests-legacy", + command: "cargo test", + has_iteration_vars: false, + } + } +} + +/// Hold the temporary workspace used to exercise command resolution. +struct ResolverWorld { + _temp: tempfile::TempDir, + root: Utf8PathBuf, +} + +impl ResolverWorld { + /// Create an isolated resolver workspace with an optional `cargo-nextest` stub. + fn new(has_nextest: bool) -> Result { + let temp = tempfile::tempdir().context("create resolver workspace")?; + let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) + .map_err(|path| anyhow!("temporary path should be UTF-8: {}", path.display()))?; + if has_nextest { + write_exec_with_content(root.as_std_path(), NEXTEST_TOOL_NAME, NEXTEST_TOOL_CONTENT) + .with_context(|| format!("write fixture tool {}", root.join(NEXTEST_TOOL_NAME)))?; + } + Ok(Self { _temp: temp, root }) + } + + /// Build a configured template environment for the selected resolver state. + fn environment(&self, has_nextest: bool) -> Result> { + let workspace = Dir::open_ambient_dir(&self.root, ambient_authority()) + .with_context(|| format!("open resolver workspace {}", self.root))?; + let path_override = if has_nextest { + OsString::from(self.root.as_str()) + } else { + OsString::new() + }; + let config = StdlibConfig::new(workspace)? + .with_workspace_root_path(&self.root)? + .with_path_override(path_override) + .with_pathext_override(".CMD"); + let mut env = Environment::new(); + stdlib::register_with_config(&mut env, config)?; + Ok(env) + } +} + +/// Verify that complementary command guards select exactly one action. +#[googletest::test] +#[rstest] +#[case::nextest_present(true, "cargo-nextest", ExpectedAction::nextest())] +#[case::nextest_absent(false, ABSENT_COMMAND, ExpectedAction::legacy())] +fn real_command_available_selects_exactly_one_action( + #[case] has_nextest: bool, + #[case] command_name: &str, + #[case] expected: ExpectedAction, +) -> googletest::Result<()> { + let world = ResolverWorld::new(has_nextest).or_fail()?; + let env = world.environment(has_nextest).or_fail()?; + let yaml = format!( + "actions: + - name: run-tests-nextest + command: cargo nextest run + foreach: [nextest] + when: command_available({command_name:?}, cwd_mode='never') + - name: run-tests-legacy + command: cargo test + when: not command_available({command_name:?}, cwd_mode='never') +targets: [] +" + ); + let mut doc: ManifestValue = serde_saphyr::from_str(&yaml).or_fail()?; + + expand_foreach(&mut doc, &env).or_fail()?; + + let selected_actions = actions(&doc).or_fail()?; + verify_that!(selected_actions, len(eq(1)))?; + let selected = selected_actions.first().or_fail()?; + let selected_map = selected.as_object().or_fail()?; + verify_that!(string_field(selected, "name").or_fail()?, eq(expected.name))?; + verify_that!( + string_field(selected, "command").or_fail()?, + eq(expected.command) + )?; + verify_that!(selected_map.contains_key("when"), eq(false))?; + verify_that!(selected_map.contains_key("foreach"), eq(false))?; + verify_that!( + selected_map.contains_key("vars"), + eq(expected.has_iteration_vars) + )?; + Ok(()) +} + +/// Read a named string field from an expanded action. +fn string_field<'a>(value: &'a ManifestValue, field: &str) -> Result<&'a str> { + value + .as_object() + .context("selected action map")? + .get(field) + .and_then(ManifestValue::as_str) + .with_context(|| format!("selected action {field}")) +} diff --git a/src/manifest/expand_test_cases/structure_cases.rs b/src/manifest/expand_test_cases/structure_cases.rs index 164d76009..ae3ed277c 100644 --- a/src/manifest/expand_test_cases/structure_cases.rs +++ b/src/manifest/expand_test_cases/structure_cases.rs @@ -26,8 +26,7 @@ fn expand_foreach_preserves_object_key_order() -> Result<()> { let keys: Vec<&str> = map.keys().map(String::as_str).collect(); anyhow::ensure!( keys == ["name", "vars", "after"], - "key order should remain stable: {:?}", - keys + "key order should remain stable: {keys:?}" ); } Ok(()) diff --git a/src/manifest/expand_tests.rs b/src/manifest/expand_tests.rs index baeab5999..83f591201 100644 --- a/src/manifest/expand_tests.rs +++ b/src/manifest/expand_tests.rs @@ -8,6 +8,11 @@ use rstest::fixture; mod a_tracing_capture; #[path = "expand_test_cases/action_condition_cases.rs"] mod action_condition_cases; + +#[path = "expand_test_cases/command_available_no_shell_cases.rs"] +mod command_available_no_shell_cases; +#[path = "expand_test_cases/command_available_selection_cases.rs"] +mod command_available_selection_cases; #[path = "expand_test_cases/condition_cases.rs"] mod condition_cases; diff --git a/src/manifest/glob/tests/pattern.rs b/src/manifest/glob/tests/pattern.rs index b93fefdb0..42763d5c9 100644 --- a/src/manifest/glob/tests/pattern.rs +++ b/src/manifest/glob/tests/pattern.rs @@ -13,7 +13,7 @@ use test_support::fluent::normalize_fluent_isolates; /// Helper to assert that a pattern produces a syntax error. fn assert_syntax_error(pattern: &str, context_msg: &str) -> Result<()> { match validate_brace_matching(pattern) { - Ok(()) => Err(anyhow!("{}", context_msg)), + Ok(()) => Err(anyhow!("{context_msg}")), Err(err) => { ensure!( err.kind() == ErrorKind::SyntaxError, diff --git a/src/runner/tests.rs b/src/runner/tests.rs index 4689bc607..8f0c598e9 100644 --- a/src/runner/tests.rs +++ b/src/runner/tests.rs @@ -97,8 +97,7 @@ fn generation_steps_run_without_reporter() -> anyhow::Result<()> { ); anyhow::ensure!( ninja_text.contains("build hello:"), - "expected generated Ninja to contain the hello build edge:\n{}", - ninja_text + "expected generated Ninja to contain the hello build edge:\n{ninja_text}" ); Ok(()) } diff --git a/src/stdlib/network/tests_support.rs b/src/stdlib/network/tests_support.rs index 1b65ee315..57e28a160 100644 --- a/src/stdlib/network/tests_support.rs +++ b/src/stdlib/network/tests_support.rs @@ -78,8 +78,7 @@ pub(super) fn assert_cache_entry_exists( let entry = workspace.join(cache_relative).join(entry_name); ensure!( fs::exists(entry.as_std_path()), - "entry {} should exist", - entry + "entry {entry} should exist" ); Ok(()) } diff --git a/src/stdlib/register.rs b/src/stdlib/register.rs index 4f63c7bee..4f0f4acc6 100644 --- a/src/stdlib/register.rs +++ b/src/stdlib/register.rs @@ -16,8 +16,8 @@ use camino::Utf8Path; use cap_std::fs::FileTypeExt; use cap_std::{ambient_authority, fs, fs_utf8::Dir}; use minijinja::{ - Environment, Error, ErrorKind, State, - value::{Kwargs, Value}, + Environment, Error, ErrorKind, State, escape_formatter, + value::{Kwargs, Value, ValueKind}, }; use std::sync::Arc; @@ -102,6 +102,7 @@ pub fn register_with_config( env: &mut Environment<'_>, config: StdlibConfig, ) -> anyhow::Result { + register_legacy_boolean_formatter(env); let state = StdlibState::default(); register_read_only_helpers(env, &config); time::register_functions(env); @@ -112,6 +113,18 @@ pub fn register_with_config( Ok(state) } +/// Preserve lowercase Boolean interpolation for existing manifests. +fn register_legacy_boolean_formatter(env: &mut Environment<'_>) { + env.set_formatter(|out, state, value| { + if value.kind() == ValueKind::Bool { + out.write_str(if value.is_true() { "true" } else { "false" }) + .map_err(Error::from) + } else { + escape_formatter(out, state, value) + } + }); +} + /// Register helpers suitable for manifest queries that must avoid side effects. /// /// The registration preserves only lexical path filters, collection helpers, diff --git a/src/stdlib/time/mod.rs b/src/stdlib/time/mod.rs index bff7beeb8..2e2672b15 100644 --- a/src/stdlib/time/mod.rs +++ b/src/stdlib/time/mod.rs @@ -79,7 +79,13 @@ fn parse_offset(raw: &str) -> Result { return Err(invalid_offset(raw)); } - UtcOffset::parse(trimmed, OFFSET_FMT).map_err(|_| invalid_offset(raw)) + let parsed = UtcOffset::parse(trimmed, OFFSET_FMT).map_err(|_| invalid_offset(raw))?; + // `time` accepts offsets beyond a civil day; templates expose ISO 8601 offsets. + if parsed.whole_hours().abs() >= 24 { + return Err(invalid_offset(raw)); + } + + Ok(parsed) } /// Build the invalid-offset error for the given string. diff --git a/src/stdlib/time/tests.rs b/src/stdlib/time/tests.rs index 57766cea0..0125ce86f 100644 --- a/src/stdlib/time/tests.rs +++ b/src/stdlib/time/tests.rs @@ -6,6 +6,7 @@ use super::*; use anyhow::{Context, Result, anyhow, ensure}; use minijinja::{Environment, ErrorKind, context, value::Value}; +use proptest::prelude::*; use rstest::{fixture, rstest}; use time::{Duration, OffsetDateTime, UtcOffset, macros::datetime}; @@ -71,6 +72,17 @@ fn now_applies_custom_offset(env: Environment<'static>) -> Result<()> { Ok(()) } +/// Accept upper- and lower-case UTC shorthand offsets. +#[rstest] +#[case::uppercase("Z")] +#[case::lowercase("z")] +fn now_accepts_utc_shorthand(env: Environment<'static>, #[case] offset: &str) -> Result<()> { + let value = eval_expression(&env, &format!("now(offset='{offset}')"))?; + let captured = value_as_timestamp(&value)?; + ensure!(captured.offset() == UtcOffset::UTC); + Ok(()) +} + #[rstest] #[case::nonsense("bogus")] #[case::missing_sign("01:00")] @@ -90,6 +102,32 @@ fn now_rejects_invalid_offset(env: Environment<'static>, #[case] offset: &str) - } } +proptest! { + /// Accept signed offsets whose hour component stays within one civil day. + #[test] + fn parse_offset_accepts_hours_below_a_civil_day( + sign in prop_oneof![Just("+"), Just("-")], + hour in 0_u8..24, + minute in 0_u8..60, + second in 0_u8..60, + ) { + let offset = format!("{sign}{hour:02}:{minute:02}:{second:02}"); + prop_assert!(parse_offset(&offset).is_ok(), "expected {offset} to be accepted"); + } + + /// Reject signed offsets at or beyond the one-civil-day boundary. + #[test] + fn parse_offset_rejects_a_civil_day_or_more( + sign in prop_oneof![Just("+"), Just("-")], + hour in 24_u8..=48, + minute in 0_u8..60, + second in 0_u8..60, + ) { + let offset = format!("{sign}{hour:02}:{minute:02}:{second:02}"); + prop_assert!(parse_offset(&offset).is_err(), "expected {offset} to be rejected"); + } +} + #[rstest] fn timedelta_defaults_to_zero(env: Environment<'static>) -> Result<()> { let value = eval_expression(&env, "timedelta()")?; diff --git a/src/stdlib/which/lookup/tests.rs b/src/stdlib/which/lookup/tests.rs index 1bb2737a9..dac046e7d 100644 --- a/src/stdlib/which/lookup/tests.rs +++ b/src/stdlib/which/lookup/tests.rs @@ -17,7 +17,7 @@ impl TempWorkspace { fn new() -> Result { let tempdir = TempDir::new().context("create tempdir")?; let root = Utf8PathBuf::from_path_buf(tempdir.path().to_path_buf()) - .map_err(|path| anyhow!("utf8 path required, got {:?}", path))?; + .map_err(|path| anyhow!("utf8 path required, got {path:?}"))?; Ok(Self { root, _tempdir: tempdir, diff --git a/tests/advanced_usage/config_precedence.rs b/tests/advanced_usage/config_precedence.rs index 1aff48ee8..947feb497 100644 --- a/tests/advanced_usage/config_precedence.rs +++ b/tests/advanced_usage/config_precedence.rs @@ -106,16 +106,14 @@ fn assert_config_metrics_snapshot(stderr: &str) -> Result<()> { let expected_record = expected_counter.record; ensure!( contains_metric_record(snapshot, expected_counter), - "expected counter {:?} in snapshot: {snapshot}", - expected_record, + "expected counter {expected_record:?} in snapshot: {snapshot}", ); } for expected_histogram in expected_histograms { let expected_record = expected_histogram.record; ensure!( contains_non_empty_histogram_record(snapshot, expected_histogram), - "expected histogram {:?} in snapshot: {snapshot}", - expected_record, + "expected histogram {expected_record:?} in snapshot: {snapshot}", ); } Ok(()) diff --git a/tests/ast_tests/string_or_list.rs b/tests/ast_tests/string_or_list.rs index e1bc736f4..88c56f3d5 100644 --- a/tests/ast_tests/string_or_list.rs +++ b/tests/ast_tests/string_or_list.rs @@ -68,9 +68,7 @@ fn string_or_list_variants() -> Result<()> { let expected = vec!["hello".to_owned(), "world".to_owned()]; ensure!( names == &expected, - "unexpected names: got {:?}, expected {:?}", - names, - expected + "unexpected names: got {names:?}, expected {expected:?}" ); } other => bail!("Expected List variant, got: {other:?}"), diff --git a/tests/bdd/steps/cli_verify.rs b/tests/bdd/steps/cli_verify.rs index 93e252b75..b505c13c2 100644 --- a/tests/bdd/steps/cli_verify.rs +++ b/tests/bdd/steps/cli_verify.rs @@ -150,8 +150,7 @@ pub(super) fn verify_cli_policy_allows(world: &TestWorld, url: &UrlString) -> Re let parsed = url.parse().context("parse URL for CLI policy check")?; ensure!( policy.evaluate(&parsed).is_ok(), - "expected CLI policy to allow {}", - url, + "expected CLI policy to allow {url}", ); Ok(()) } @@ -164,14 +163,13 @@ pub(super) fn verify_cli_policy_rejects( let policy = cli_network_policy(world)?; let parsed = url.parse().context("parse URL for CLI policy check")?; let Err(err) = policy.evaluate(&parsed) else { - bail!("expected CLI policy to reject {}", url); + bail!("expected CLI policy to reject {url}"); }; let normalized_error = normalize_fluent_isolates(&err.to_string()); let normalized_message = normalize_fluent_isolates(message.as_str()); ensure!( normalized_error.contains(&normalized_message), - "expected error to mention '{}', got '{err}'", - message, + "expected error to mention '{message}', got '{err}'", ); Ok(()) } @@ -201,8 +199,7 @@ pub(super) fn verify_error_contains(world: &TestWorld, fragment: &ErrorFragment) let normalized_fragment = normalize_fluent_isolates(fragment.as_str()); ensure!( normalized_error.contains(&normalized_fragment), - "Error message '{error}' does not contain expected '{}'", - fragment + "Error message '{error}' does not contain expected '{fragment}'" ); Ok(()) } diff --git a/tests/bdd/steps/fs.rs b/tests/bdd/steps/fs.rs index a5f4dc438..1d880631d 100644 --- a/tests/bdd/steps/fs.rs +++ b/tests/bdd/steps/fs.rs @@ -15,7 +15,7 @@ use std::os::unix::fs::FileTypeExt; fn setup_workspace() -> Result<(tempfile::TempDir, Utf8PathBuf, Dir)> { let temp = tempfile::tempdir().context("create tempdir for file-type workspace")?; let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) - .map_err(|path| anyhow!("workspace path is not valid UTF-8: {path:?}"))?; + .map_err(|path| anyhow!("workspace path is not valid UTF-8: {}", path.display()))?; let handle = Dir::open_ambient_dir(&root, ambient_authority()) .context("open ambient dir for file-type workspace")?; Ok((temp, root, handle)) @@ -55,7 +55,7 @@ fn find_block_device_fallback() -> Result { if file_type.is_block_device() { let path = entry.path(); let utf8 = Utf8PathBuf::from_path_buf(path) - .map_err(|p| anyhow!("block device path is not valid UTF-8: {p:?}"))?; + .map_err(|p| anyhow!("block device path is not valid UTF-8: {}", p.display()))?; return Ok(utf8); } } diff --git a/tests/bdd/steps/manifest_command_tests.rs b/tests/bdd/steps/manifest_command_tests.rs index 278fbedde..c959f8013 100644 --- a/tests/bdd/steps/manifest_command_tests.rs +++ b/tests/bdd/steps/manifest_command_tests.rs @@ -41,8 +41,7 @@ fn world_env_vars_with_value_are_applied(prepared_world: Result) -> R let val = env_value(&cmd, "NETSUKE_TEST_FLAG").expect("NETSUKE_TEST_FLAG should be present"); ensure!( val == OsStr::new("enabled"), - "expected NETSUKE_TEST_FLAG to be 'enabled', got {:?}", - val + "expected NETSUKE_TEST_FLAG to be 'enabled', got {val:?}" ); Ok(()) } @@ -77,8 +76,7 @@ fn scenario_path_override_is_forwarded_and_netsuke_executable_is_used( env_value(&cmd, "PATH").expect("PATH should be explicitly forwarded to the command"); ensure!( path_val == OsStr::new("/fake/bin"), - "expected PATH to be '/fake/bin', got {:?}", - path_val + "expected PATH to be '/fake/bin', got {path_val:?}" ); // Command should use the resolved netsuke_executable(), not rely on PATH lookup. diff --git a/tests/bdd/steps/ninja.rs b/tests/bdd/steps/ninja.rs index cf8e86e8f..d675983f8 100644 --- a/tests/bdd/steps/ninja.rs +++ b/tests/bdd/steps/ninja.rs @@ -53,9 +53,7 @@ fn compare_tokens(actual: &[String], expected: &TokenList) -> Result<()> { let expected_vec = expected.to_vec(); ensure!( actual == expected_vec, - "expected tokens {:?}, got {:?}", - expected_vec, - actual + "expected tokens {expected_vec:?}, got {actual:?}" ); Ok(()) } @@ -116,7 +114,7 @@ fn run_generated_ninja_target(world: &TestWorld, target: &str, value: &str) -> R .context("host PATH is required to run Ninja")?; let workspace = ninja_integration_workspace().context("Ninja is required for BDD execution")?; let path = Utf8PathBuf::from_path_buf(workspace.path().to_path_buf()) - .map_err(|non_utf8| anyhow!("non-UTF-8 temporary path: {non_utf8:?}"))?; + .map_err(|non_utf8| anyhow!("non-UTF-8 temporary path: {}", non_utf8.display()))?; let directory = Dir::open_ambient_dir(&path, ambient_authority()) .with_context(|| format!("open Ninja workspace {path}"))?; directory diff --git a/tests/bdd/steps/stdlib/workspace.rs b/tests/bdd/steps/stdlib/workspace.rs index db5a6cb9f..cd569e8eb 100644 --- a/tests/bdd/steps/stdlib/workspace.rs +++ b/tests/bdd/steps/stdlib/workspace.rs @@ -34,8 +34,12 @@ pub(crate) fn ensure_workspace(world: &TestWorld) -> Result { return Ok(root); } let temp = tempfile::tempdir().context("create stdlib workspace")?; - let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) - .map_err(|path| anyhow!("stdlib workspace path is not valid UTF-8: {path:?}"))?; + let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()).map_err(|path| { + anyhow!( + "stdlib workspace path is not valid UTF-8: {}", + path.display() + ) + })?; let handle = Dir::open_ambient_dir(&root, ambient_authority()) .context("open stdlib workspace directory")?; handle diff --git a/tests/cli_tests/config_precedence_ladder.rs b/tests/cli_tests/config_precedence_ladder.rs index c4d6e90a1..f7646edd3 100644 --- a/tests/cli_tests/config_precedence_ladder.rs +++ b/tests/cli_tests/config_precedence_ladder.rs @@ -151,9 +151,9 @@ impl LadderExpectation { fn assert_ladder(merged: &Cli, expected: LadderExpectation) -> Result<()> { ensure!( merged.file.as_path() == Path::new(expected.file), - "manifest path should be {:?}, got {:?}", + "manifest path should be {:?}, got {}", expected.file, - merged.file + merged.file.display() ); ensure!( merged.emoji == expected.emoji, diff --git a/tests/cli_tests/merge.rs b/tests/cli_tests/merge.rs index eeae5c55d..45ffe457d 100644 --- a/tests/cli_tests/merge.rs +++ b/tests/cli_tests/merge.rs @@ -152,7 +152,7 @@ fn cli_merge_layers_respects_precedence_and_appends_lists( merged.progress == ProgressPolicy::Never, "CLI layer should override progress setting", ); - ensure!(merged.json, "CLI layer should override json setting",); + ensure!(merged.json, "CLI layer should override json setting"); ensure!( merged.locale.as_deref() == Some("en-US"), "file layer should populate locale when CLI does not override", diff --git a/tests/cli_tests/parsing.rs b/tests/cli_tests/parsing.rs index 3b8426d55..e17ffdf68 100644 --- a/tests/cli_tests/parsing.rs +++ b/tests/cli_tests/parsing.rs @@ -192,7 +192,7 @@ fn parse_cli(#[case] case: CliCase) -> Result<()> { cli.locale.as_deref() == case.locale, "locale should match input", ); - ensure!(cli.json == case.json, "json flag should match input",); + ensure!(cli.json == case.json, "json flag should match input"); ensure!(cli.no_input(), "no-input should remain enabled"); ensure!( cli.fetch_allow_scheme == case.allow_scheme, @@ -226,8 +226,8 @@ fn parse_cli(#[case] case: CliCase) -> Result<()> { cli.fetch_default_deny == case.default_deny, "default-deny flag should match input", ); - ensure!(cli.color == case.color, "color policy should match input",); - ensure!(cli.emoji == case.emoji, "emoji policy should match input",); + ensure!(cli.color == case.color, "color policy should match input"); + ensure!(cli.emoji == case.emoji, "emoji policy should match input"); ensure!( cli.progress == case.progress, "progress policy should match input", diff --git a/tests/data/conditional_action_deps.yml b/tests/data/conditional_action_deps.yml new file mode 100644 index 000000000..5aa51e8bd --- /dev/null +++ b/tests/data/conditional_action_deps.yml @@ -0,0 +1,32 @@ +netsuke_version: "1.0.0" +actions: + - name: preferred-action + command: echo preferred + sources: preferred/action.src + deps: preferred/action.dep + order_only_deps: preferred/action.order + when: command_available("./.netsuke-missing/tool-3-14-5", cwd_mode="never") + - name: "fallback-{{ item }}" + command: "echo {{ item }}" + foreach: + - alpha + - beta + sources: "src/{{ item }}.in" + deps: + - "build/{{ item }}.o" + - shared/action.cfg + order_only_deps: "order/{{ item }}.stamp" + when: not command_available("./.netsuke-missing/tool-3-14-5", cwd_mode="never") +targets: + - name: preferred-target + command: echo preferred + sources: preferred/target.src + deps: preferred/target.dep + order_only_deps: preferred/target.order + when: command_available("./.netsuke-missing/tool-3-14-5", cwd_mode="never") + - name: out/fallback + command: echo fallback + sources: src/target.in + deps: include/fallback.h + order_only_deps: order/target.stamp + when: not command_available("./.netsuke-missing/tool-3-14-5", cwd_mode="never") diff --git a/tests/documentation_examples_tests.rs b/tests/documentation_examples_tests.rs index e8aa836a8..bf422fd23 100644 --- a/tests/documentation_examples_tests.rs +++ b/tests/documentation_examples_tests.rs @@ -18,6 +18,7 @@ use test_support::netsuke::{NetsukeRun, run_netsuke_in, run_netsuke_in_with_env} const EXPECTED_EXAMPLE_IDS: &[&str] = &[ "guide-accessible-output", "guide-binstall-install", + "guide-boolean-string-interpolation", "guide-cli-usage", "guide-command-available-manifest", "guide-command-list", @@ -158,6 +159,7 @@ fn every_documented_fence_has_a_known_unique_identifier() -> Result<()> { #[rstest] #[case("readme-first-build-manifest")] #[case("guide-first-build-manifest")] +#[case("guide-boolean-string-interpolation")] #[case("guide-complete-manifest")] #[case("guide-foreach-manifest")] #[case("guide-macro-manifest")] diff --git a/tests/features/ninja.feature b/tests/features/ninja.feature index 82e158d36..d9c9199be 100644 --- a/tests/features/ninja.feature +++ b/tests/features/ninja.feature @@ -36,3 +36,11 @@ Feature: Ninja file generation And the ninja file contains "build regenerate: " And the ninja file contains " | schemas/user.yml tools/generator" And the ninja file contains "command = echo src/main.c src/main.c > out/app" + + Scenario: Selected conditional action deps become implicit Ninja dependencies + When the manifest file "tests/data/conditional_action_deps.yml" is compiled to IR + Then the graph target "fallback-alpha" has inputs "src/alpha.in" + And the graph target "fallback-alpha" has implicit deps "build/alpha.o, shared/action.cfg" + When the ninja file is generated + Then the ninja file contains "build fallback-alpha: " + And the ninja file contains " src/alpha.in | build/alpha.o shared/action.cfg || order/alpha.stamp" diff --git a/tests/ir_from_manifest_tests.rs b/tests/ir_from_manifest_tests.rs index 3fcc928e6..06c78545d 100644 --- a/tests/ir_from_manifest_tests.rs +++ b/tests/ir_from_manifest_tests.rs @@ -16,6 +16,7 @@ use netsuke::{ }; use rstest::rstest; +/// Generate the expected action and target counts for each manifest fixture. #[rstest] #[case::minimal_manifest("tests/data/minimal.yml", 1, 1)] #[case::duplicate_rules("tests/data/duplicate_rules.yml", 2, 2)] @@ -39,6 +40,7 @@ fn manifest_fixture_generates_expected_ir( Ok(()) } +/// Preserve declaration order while interpolating command-list entries. #[rstest] fn command_list_entries_are_interpolated_in_order() -> Result<()> { let yaml = r#" @@ -70,6 +72,7 @@ fn command_list_entries_are_interpolated_in_order() -> Result<()> { Ok(()) } +/// Report an IR error when a target names no declared rule. #[rstest] fn missing_rule_fails() -> Result<()> { let manifest = manifest::from_path("tests/data/missing_rule.yml")?; @@ -83,6 +86,7 @@ fn missing_rule_fails() -> Result<()> { Ok(()) } +/// Exclude skipped conditional entries from all IR validation and graph output. #[rstest] #[case::skipped_target_duplicate_output( concat!( @@ -146,6 +150,7 @@ fn skipped_manifest_conditions_do_not_contribute_to_ir( Ok(()) } +/// Lower manifest dependencies into the implicit Ninja dependency class. #[rstest] #[case::target_deps( concat!( @@ -212,6 +217,7 @@ fn manifest_deps_populate_implicit_deps( Ok(()) } +/// Exclude manifest dependencies from recipe input interpolation. #[rstest] fn manifest_deps_do_not_contribute_to_recipe_inputs() -> Result<()> { let yaml = concat!( @@ -258,6 +264,111 @@ fn manifest_deps_do_not_contribute_to_recipe_inputs() -> Result<()> { Ok(()) } +/// Preserve distinct explicit, implicit, and order-only dependency classes. +#[rstest] +fn conditional_action_deps_populate_distinct_ir_classes() -> Result<()> { + let manifest = manifest::from_path("tests/data/conditional_action_deps.yml")?; + let graph = BuildGraph::from_manifest(&manifest).context("expected graph generation")?; + + assert_conditional_edge( + &graph, + "fallback-alpha", + &ExpectedEdge { + inputs: &["src/alpha.in"], + implicit_deps: &["build/alpha.o", "shared/action.cfg"], + order_only_deps: &["order/alpha.stamp"], + is_phony: true, + }, + )?; + assert_conditional_edge( + &graph, + "fallback-beta", + &ExpectedEdge { + inputs: &["src/beta.in"], + implicit_deps: &["build/beta.o", "shared/action.cfg"], + order_only_deps: &["order/beta.stamp"], + is_phony: true, + }, + )?; + assert_conditional_edge( + &graph, + "out/fallback", + &ExpectedEdge { + inputs: &["src/target.in"], + implicit_deps: &["include/fallback.h"], + order_only_deps: &["order/target.stamp"], + is_phony: false, + }, + )?; + + let rendered_paths = graph + .targets + .iter() + .flat_map(|(output, edge)| { + std::iter::once(output) + .chain(&edge.inputs) + .chain(&edge.implicit_deps) + .chain(&edge.order_only_deps) + }) + .map(|path| path.as_str()) + .collect::>(); + ensure!( + rendered_paths + .iter() + .all(|path| !path.starts_with("preferred")), + "filtered branches should not contribute paths to the IR: {rendered_paths:?}" + ); + Ok(()) +} + +struct ExpectedEdge<'a> { + inputs: &'a [&'a str], + implicit_deps: &'a [&'a str], + order_only_deps: &'a [&'a str], + is_phony: bool, +} + +/// Assert one conditional action's IR edge and dependency classes. +fn assert_conditional_edge( + graph: &BuildGraph, + output: &str, + expected: &ExpectedEdge<'_>, +) -> Result<()> { + let edge = graph + .targets + .get(&Utf8PathBuf::from(output)) + .with_context(|| format!("expected edge for {output}"))?; + let expected_paths = |paths: &[&str]| { + paths + .iter() + .copied() + .map(Utf8PathBuf::from) + .collect::>() + }; + ensure!( + edge.inputs == expected_paths(expected.inputs), + "unexpected explicit inputs for {output}: {:?}", + edge.inputs + ); + ensure!( + edge.implicit_deps == expected_paths(expected.implicit_deps), + "unexpected implicit deps for {output}: {:?}", + edge.implicit_deps + ); + ensure!( + edge.order_only_deps == expected_paths(expected.order_only_deps), + "unexpected order-only deps for {output}: {:?}", + edge.order_only_deps + ); + ensure!( + edge.phony == expected.is_phony, + "unexpected phony flag for {output}: {}", + edge.phony + ); + Ok(()) +} + +/// Lower dependency-only entries into deduplicated phony actions. #[test] fn dependency_only_entries_lower_to_deduplicated_phony_actions() -> Result<()> { let yaml = concat!( @@ -297,6 +408,7 @@ fn dependency_only_entries_lower_to_deduplicated_phony_actions() -> Result<()> { Ok(()) } +/// Keep a target description separate from its rule's progress text. #[rstest] fn target_descriptions_do_not_replace_rule_progress_text() -> Result<()> { let yaml = concat!( @@ -336,6 +448,7 @@ enum ExpectedError { CircularDependency(Vec), } +/// Map invalid manifest structures to their expected IR generation errors. #[rstest] #[case( "tests/data/duplicate_outputs.yml", @@ -379,9 +492,7 @@ fn manifest_error_cases( ) => { ensure!( outputs == exp_outputs, - "unexpected duplicate outputs: got {:?}, expected {:?}", - outputs, - exp_outputs + "unexpected duplicate outputs: got {outputs:?}, expected {exp_outputs:?}" ); } ( @@ -399,9 +510,7 @@ fn manifest_error_cases( ); ensure!( rules == exp_rules, - "unexpected rules: got {:?}, expected {:?}", - rules, - exp_rules + "unexpected rules: got {rules:?}, expected {exp_rules:?}" ); } (IrGenError::EmptyRule { target_name, .. }, ExpectedError::EmptyRule(exp_target)) => { @@ -435,9 +544,7 @@ fn manifest_error_cases( actual.sort(); ensure!( actual == expected_cycle, - "unexpected dependency cycle: got {:?}, expected {:?}", - actual, - expected_cycle + "unexpected dependency cycle: got {actual:?}, expected {expected_cycle:?}" ); } (other, exp) => bail!("expected {exp:?} but got {other:?}"), diff --git a/tests/locale_resolution_tests.rs b/tests/locale_resolution_tests.rs index 3d0120b18..690fd6048 100644 --- a/tests/locale_resolution_tests.rs +++ b/tests/locale_resolution_tests.rs @@ -27,10 +27,7 @@ fn normalize_locale_tag_handles_common_formats( let normalized = normalize_locale_tag(raw); ensure!( normalized.as_deref() == expected, - "expected {:?} to normalize to {:?}, got {:?}", - raw, - expected, - normalized + "expected {raw:?} to normalize to {expected:?}, got {normalized:?}" ); Ok(()) } diff --git a/tests/ninja_dollar_escaping_tests.rs b/tests/ninja_dollar_escaping_tests.rs index d9496b3c4..4abd2422e 100644 --- a/tests/ninja_dollar_escaping_tests.rs +++ b/tests/ninja_dollar_escaping_tests.rs @@ -14,7 +14,12 @@ use netsuke::{ ninja_gen::generate, }; use rstest::rstest; -use std::{ffi::OsString, process::Command}; +use std::{ + ffi::OsString, + fmt::{self, Debug, Display, Formatter}, + path::Path, + process::Command, +}; use tempfile::TempDir; use test_support::ninja::ninja_integration_workspace; @@ -33,6 +38,7 @@ fn host_path() -> Result { .context("host PATH is required to run Ninja") } +/// Build the action used by a generated single-edge test graph. const fn action(recipe: Recipe) -> Action { Action { recipe, @@ -44,6 +50,7 @@ const fn action(recipe: Recipe) -> Action { } } +/// Build a single-action graph for a command-escaping scenario. fn graph(recipe: Recipe, input: &str, output: &str) -> BuildGraph { let edge = BuildEdge { action_id: "action".into(), @@ -63,10 +70,20 @@ fn graph(recipe: Recipe, input: &str, output: &str) -> BuildGraph { graph } +/// Create an isolated workspace after confirming that Ninja is available. fn required_ninja_workspace() -> Result { ninja_integration_workspace().context("Ninja is required for dollar-escaping tests") } +/// Render a path with its escaped debug representation for invalid UTF-8 diagnostics. +struct DebugPath<'path>(&'path Path); + +impl Display for DebugPath<'_> { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + Debug::fmt(self.0, formatter) + } +} + /// An isolated workspace containing a generated Ninja file and its output. struct NinjaWorkspace { /// Keeps the temporary directory alive while Ninja uses its contents. @@ -81,8 +98,9 @@ impl NinjaWorkspace { /// Creates an isolated workspace and writes the generated Ninja file into it. fn create(ninja_file: &str) -> anyhow::Result { let temporary_directory = required_ninja_workspace()?; - let path = Utf8PathBuf::from_path_buf(temporary_directory.path().to_path_buf()) - .map_err(|non_utf8| anyhow::anyhow!("non-UTF-8 temporary path: {non_utf8:?}"))?; + let path = Utf8PathBuf::from_path_buf(temporary_directory.path().to_path_buf()).map_err( + |non_utf8| anyhow::anyhow!("non-UTF-8 temporary path: {}", DebugPath(&non_utf8)), + )?; let directory = Dir::open_ambient_dir(&path, ambient_authority()) .with_context(|| format!("open Ninja workspace {path}"))?; let workspace = Self { @@ -97,6 +115,7 @@ impl NinjaWorkspace { Ok(workspace) } } +/// Query Ninja's effective commands for a generated target. fn ninja_commands(ninja_file: &str, target: &str) -> Result { let workspace = NinjaWorkspace::create(ninja_file)?; @@ -117,6 +136,7 @@ fn ninja_commands(ninja_file: &str, target: &str) -> Result { String::from_utf8(output.stdout).context("Ninja command output was not UTF-8") } +/// Run a generated Ninja target and read its shell-produced output. #[cfg(unix)] fn ninja_output( ninja_file: &str, @@ -153,6 +173,7 @@ fn ninja_output( .context("read shell output from generated target") } +/// Verify that remaining shell dollars are doubled for Ninja. #[rstest] #[case::shell_variable("echo $NETSUKE_TEST_SENTINEL", "echo $$NETSUKE_TEST_SENTINEL")] #[case::shell_default( @@ -178,6 +199,7 @@ fn backend_doubles_every_residual_shell_dollar(#[case] command: &str, #[case] ex ); } +/// Verify that Netsuke placeholders lower before residual dollars are escaped. #[rstest] #[case::scalar(Recipe::Command { command: "echo $NETSUKE_TEST_SENTINEL".into() })] #[case::command_list(Recipe::Command { @@ -237,6 +259,8 @@ fn command_list_default_reaches_the_child_shell( ); Ok(()) } + +/// Verify that manifest placeholder syntax lowers inside scalar and list recipes. #[rstest] #[case::scalar("command: 'cat $in > $out'")] #[case::command_list("command:\n - 'cat $in > $out'")] @@ -295,6 +319,8 @@ fn script_default_reaches_the_child_shell( ); Ok(()) } + +/// Verify that script placeholders run against their real input and output paths. #[cfg(unix)] #[rstest] fn script_placeholders_execute_against_real_paths() -> Result<()> { @@ -310,6 +336,8 @@ fn script_placeholders_execute_against_real_paths() -> Result<()> { ); Ok(()) } + +/// Verify that placeholder-looking text inside backticks is rejected before escaping. #[rstest] fn placeholders_inside_backticks_are_rejected_before_backend_escaping() -> Result<()> { let manifest = manifest::from_str( @@ -323,6 +351,7 @@ fn placeholders_inside_backticks_are_rejected_before_backend_escaping() -> Resul Ok(()) } +/// Verify that command control characters cannot inject Ninja syntax. #[rstest] #[case::newline("echo safe\nbuild injected: action")] #[case::carriage_return("echo safe\rbuild injected: action")] @@ -340,6 +369,7 @@ fn command_control_characters_are_rejected(#[case] command: &str) { ); } +/// Verify that Ninja-unsafe path characters are rejected during generation. #[rstest] #[case::dollar("input$file")] #[case::space("input file")] @@ -363,6 +393,7 @@ fn unsafe_paths_are_rejected(#[case] input: &str) { ); } +/// Verify that commands without dollars retain their exact text. #[rstest] fn dollar_free_commands_remain_byte_identical() -> Result<()> { let ninja = generate(&graph( diff --git a/tests/ninja_gen_command_list_integration_tests.rs b/tests/ninja_gen_command_list_integration_tests.rs index 7d618ae15..421d85fd4 100644 --- a/tests/ninja_gen_command_list_integration_tests.rs +++ b/tests/ninja_gen_command_list_integration_tests.rs @@ -156,7 +156,7 @@ fn open_temp_workspace(dir: &TempDir) -> Result { fn temp_workspace_path(dir: &TempDir) -> Result { let dir_path = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()) - .map_err(|path| anyhow::anyhow!("temp dir path {path:?} is not UTF-8"))?; + .map_err(|path| anyhow::anyhow!("temp dir path {} is not UTF-8", path.display()))?; Ok(dir_path) } diff --git a/tests/ninja_gen_command_list_process_integration_tests.rs b/tests/ninja_gen_command_list_process_integration_tests.rs index 7cefe992d..862a22baa 100644 --- a/tests/ninja_gen_command_list_process_integration_tests.rs +++ b/tests/ninja_gen_command_list_process_integration_tests.rs @@ -36,7 +36,7 @@ fn run_command_list( return Ok(None); }; let path = Utf8PathBuf::from_path_buf(tempdir.path().to_path_buf()) - .map_err(|path| anyhow::anyhow!("temp dir path {:?} is not UTF-8", path))?; + .map_err(|path| anyhow::anyhow!("temp dir path {} is not UTF-8", path.display()))?; let action = Action { recipe: Recipe::Command { command: StringOrList::List(entries.iter().map(|entry| (*entry).into()).collect()), diff --git a/tests/ninja_gen_integration_tests.rs b/tests/ninja_gen_integration_tests.rs index 222ecee7f..58017481c 100644 --- a/tests/ninja_gen_integration_tests.rs +++ b/tests/ninja_gen_integration_tests.rs @@ -135,7 +135,7 @@ fn ninja_integration_tests( return Ok(()); }; let dir_path = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()) - .map_err(|path| anyhow::anyhow!("temp dir path {:?} is not UTF-8", path))?; + .map_err(|path| anyhow::anyhow!("temp dir path {path:?} is not UTF-8"))?; let NinjaIntegrationCase { action, @@ -153,8 +153,7 @@ fn ninja_integration_tests( if !matches!(&assertion, AssertionType::FileExists) { ensure!( output == target_name, - "expected edge output '{}' to match test target '{target_name}'", - output + "expected edge output '{output}' to match test target '{target_name}'" ); } let mut graph = BuildGraph::default(); @@ -185,8 +184,7 @@ fn ninja_integration_tests( .with_context(|| format!("check existence of {target_name}"))?; ensure!( exists, - "expected {} to exist after ninja invocation", - &target_name + "expected {target_name} to exist after ninja invocation" ); } AssertionType::FileContent(expected) => { diff --git a/tests/ninja_snapshot_tests.rs b/tests/ninja_snapshot_tests.rs index 32824ea52..6acd2b5dc 100644 --- a/tests/ninja_snapshot_tests.rs +++ b/tests/ninja_snapshot_tests.rs @@ -17,6 +17,11 @@ use test_support::ensure_binaries_available; #[cfg(unix)] use test_support::fs; +#[cfg(unix)] +#[path = "ninja_snapshot_tests/conditional_action_deps.rs"] +mod conditional_action_deps; + +/// Run a command and return its UTF-8 standard output when it succeeds. #[cfg(unix)] fn run_ok(cmd: &mut Command) -> Result { let out = cmd.output().context("failed to spawn command")?; @@ -30,6 +35,7 @@ fn run_ok(cmd: &mut Command) -> Result { Ok(stdout) } +/// Validate that generated Ninja executes and then becomes a no-op for a touch rule. #[test] #[cfg(unix)] fn touch_manifest_ninja_validation() -> Result<()> { @@ -95,6 +101,7 @@ fn touch_manifest_ninja_validation() -> Result<()> { Ok(()) } +/// Snapshot only the targets retained after conditional foreach expansion. #[test] fn conditional_manifest_ninja_snapshot() -> Result<()> { let manifest_yaml = r#" @@ -137,6 +144,7 @@ fn conditional_manifest_ninja_snapshot() -> Result<()> { Ok(()) } +/// Snapshot the deterministic fallback selected by an unavailable command. #[test] fn command_available_manifest_ninja_snapshot() -> Result<()> { // Pin the `command_available` resolver to an empty PATH through the @@ -192,6 +200,7 @@ fn command_available_manifest_ninja_snapshot() -> Result<()> { Ok(()) } +/// Snapshot fail-fast lowering for a multi-command manifest rule. #[test] fn multi_command_manifest_ninja_snapshot() -> Result<()> { let fixture_dir = Dir::open_ambient_dir(env!("CARGO_MANIFEST_DIR"), ambient_authority()) @@ -228,6 +237,7 @@ fn multi_command_manifest_ninja_snapshot() -> Result<()> { Ok(()) } +/// Snapshot Ninja's implicit-dependency separator for manifest dependencies. #[test] fn implicit_deps_manifest_ninja_snapshot() -> Result<()> { let fixture_dir = Dir::open_ambient_dir(env!("CARGO_MANIFEST_DIR"), ambient_authority()) @@ -257,6 +267,7 @@ fn implicit_deps_manifest_ninja_snapshot() -> Result<()> { Ok(()) } +/// Snapshot dependency-only actions as native Ninja phony nodes. #[test] fn dependency_only_manifest_ninja_snapshot() -> Result<()> { let manifest_yaml = r#" diff --git a/tests/ninja_snapshot_tests/conditional_action_deps.rs b/tests/ninja_snapshot_tests/conditional_action_deps.rs new file mode 100644 index 000000000..06f90e879 --- /dev/null +++ b/tests/ninja_snapshot_tests/conditional_action_deps.rs @@ -0,0 +1,188 @@ +//! Snapshot and executable-Ninja coverage for conditional action dependencies. +//! +//! Keeps the conditional dependency fixture and its real-Ninja validation +//! isolated from the general snapshot cases while sharing their command runner. + +use super::run_ok; +use anyhow::{Context, Result, ensure}; +use insta::{Settings, assert_snapshot}; +use netsuke::{ir::BuildGraph, manifest, ninja_gen}; +use std::{ + fs as std_fs, + io::Write, + path::{Path, PathBuf}, + process::Command, + time::Duration, +}; +use tempfile::{TempDir, tempdir}; +use test_support::ensure_binaries_available; + +/// Snapshot and execute the selected conditional-action dependency graph. +#[test] +fn conditional_action_deps_ninja_snapshot() -> Result<()> { + let manifest = manifest::from_path("tests/data/conditional_action_deps.yml")?; + let ir = BuildGraph::from_manifest(&manifest)?; + let ninja_content = ninja_gen::generate(&ir)?; + + assert_dependency_classes( + &ninja_content, + &ExpectedNinjaEdge { + output: "fallback-alpha", + input: "src/alpha.in", + implicit_deps: "build/alpha.o shared/action.cfg", + order_only_deps: "order/alpha.stamp", + }, + )?; + assert_dependency_classes( + &ninja_content, + &ExpectedNinjaEdge { + output: "fallback-beta", + input: "src/beta.in", + implicit_deps: "build/beta.o shared/action.cfg", + order_only_deps: "order/beta.stamp", + }, + )?; + assert_dependency_classes( + &ninja_content, + &ExpectedNinjaEdge { + output: "out/fallback", + input: "src/target.in", + implicit_deps: "include/fallback.h", + order_only_deps: "order/target.stamp", + }, + )?; + ensure!( + !ninja_content.contains("preferred"), + "filtered branches should not appear in Ninja output:\n{ninja_content}" + ); + + let mut settings = Settings::new(); + settings.set_snapshot_path(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/snapshots/ninja" + )); + settings.bind(|| { + assert_snapshot!("conditional_action_deps_ninja", ninja_content); + }); + + validate_conditional_ninja(&ninja_content) +} + +/// Describe the expected Ninja dependency classes for a generated output. +struct ExpectedNinjaEdge<'a> { + output: &'a str, + input: &'a str, + implicit_deps: &'a str, + order_only_deps: &'a str, +} + +/// Assert that a generated edge retains explicit, implicit, and order-only inputs. +fn assert_dependency_classes(ninja_content: &str, expected: &ExpectedNinjaEdge<'_>) -> Result<()> { + let build_line = ninja_content + .lines() + .find(|line| line.starts_with(&format!("build {}:", expected.output))) + .with_context(|| format!("expected build line for {}", expected.output))?; + ensure!( + build_line.contains(&format!( + " {} | {} || {}", + expected.input, expected.implicit_deps, expected.order_only_deps + )), + "unexpected dependency classes for {}: {build_line}", + expected.output + ); + Ok(()) +} + +/// Validate the generated conditional Ninja file with a real Ninja installation. +fn validate_conditional_ninja(ninja_content: &str) -> Result<()> { + if let Err(err) = ensure_binaries_available(&[("ninja", &["--version"])]) { + tracing::warn!("skipping real Ninja validation: {}", err); + writeln!( + std::io::stderr().lock(), + "skipping real Ninja validation: {err}" + ) + .context("write real Ninja skip reason")?; + return Ok(()); + } + + let (dir, build_file) = prepare_conditional_ninja_workspace(ninja_content)?; + assert_conditional_ninja_selection(&dir, &build_file)?; + mark_conditional_ninja_output_up_to_date(&dir)?; + assert_conditional_ninja_no_op(&dir, &build_file) +} + +/// Create the files required to run the conditional Ninja fixture. +fn prepare_conditional_ninja_workspace(ninja_content: &str) -> Result<(TempDir, PathBuf)> { + let dir = tempdir().context("create temp dir for conditional Ninja validation")?; + let build_file = dir.path().join("build.ninja"); + std_fs::write(&build_file, ninja_content) + .with_context(|| format!("write Ninja file to {}", build_file.display()))?; + for relative_path in ["src/target.in", "include/fallback.h", "order/target.stamp"] { + let dependency_path = dir.path().join(relative_path); + let parent = dependency_path + .parent() + .context("dependency path should have parent")?; + std_fs::create_dir_all(parent) + .with_context(|| format!("create dependency directory {}", parent.display()))?; + std_fs::write(&dependency_path, "") + .with_context(|| format!("write Ninja dependency {}", dependency_path.display()))?; + } + + Ok((dir, build_file)) +} + +/// Run Ninja with the conditional fixture as its build file. +fn run_conditional_ninja(dir: &TempDir, build_file: &Path, args: &[&str]) -> Result { + let mut cmd = Command::new("ninja"); + cmd.arg("-f").arg(build_file).args(args); + cmd.current_dir(dir.path()); + run_ok(&mut cmd) +} + +/// Confirm that Ninja selects the fallback branch and schedules its command. +fn assert_conditional_ninja_selection(dir: &TempDir, build_file: &Path) -> Result<()> { + run_conditional_ninja(dir, build_file, &["-t", "query", "fallback-alpha"])?; + run_conditional_ninja(dir, build_file, &["-t", "query", "out/fallback"])?; + let first = run_conditional_ninja(dir, build_file, &["-n", "out/fallback"])?; + ensure!( + first.contains("echo fallback"), + "expected dry run to reach selected target, got:\n{first}" + ); + + Ok(()) +} + +/// Mark the fallback output newer than its order-only dependency. +fn mark_conditional_ninja_output_up_to_date(dir: &TempDir) -> Result<()> { + let output = dir.path().join("out/fallback"); + let output_dir = output.parent().context("output path should have parent")?; + std_fs::create_dir_all(output_dir) + .with_context(|| format!("create output directory {}", output_dir.display()))?; + std_fs::write(&output, "") + .with_context(|| format!("write up-to-date output {}", output.display()))?; + let latest_dependency = dir.path().join("order/target.stamp"); + let output_modified = std_fs::metadata(&latest_dependency) + .with_context(|| format!("stat dependency {}", latest_dependency.display()))? + .modified() + .context("read dependency modification time")? + + Duration::from_secs(1); + std_fs::File::options() + .write(true) + .open(&output) + .with_context(|| format!("open output {}", output.display()))? + .set_times(std_fs::FileTimes::new().set_modified(output_modified)) + .with_context(|| format!("set output time {}", output.display()))?; + + Ok(()) +} + +/// Confirm that the prepared conditional output has no pending Ninja work. +fn assert_conditional_ninja_no_op(dir: &TempDir, build_file: &Path) -> Result<()> { + run_conditional_ninja(dir, build_file, &["out/fallback"])?; + let second = run_conditional_ninja(dir, build_file, &["-n", "out/fallback"])?; + ensure!( + second.contains("no work to do"), + "expected no-op second pass, got:\n{second}" + ); + Ok(()) +} diff --git a/tests/snapshots/ninja/ninja_snapshot_tests__conditional_action_deps__conditional_action_deps_ninja.snap b/tests/snapshots/ninja/ninja_snapshot_tests__conditional_action_deps__conditional_action_deps_ninja.snap new file mode 100644 index 000000000..8cfddcabf --- /dev/null +++ b/tests/snapshots/ninja/ninja_snapshot_tests__conditional_action_deps__conditional_action_deps_ninja.snap @@ -0,0 +1,19 @@ +--- +source: tests/ninja_snapshot_tests/conditional_action_deps.rs +assertion_line: 65 +expression: ninja_content +--- +rule dfd2ff5921f146a85d62fcfeb725508df48b315134db4f325585bd6981940de6 + command = echo alpha + +rule e119fafadd9fa5af44c4dc58e047f27d387ed098c0702aeffbce67f90746ca3c + command = echo beta + +rule e8c3bab461990fead7f11b007334dcd68b89db712472c5a294c69609dd7302b5 + command = echo fallback + +build fallback-alpha: dfd2ff5921f146a85d62fcfeb725508df48b315134db4f325585bd6981940de6 src/alpha.in | build/alpha.o shared/action.cfg || order/alpha.stamp + +build fallback-beta: e119fafadd9fa5af44c4dc58e047f27d387ed098c0702aeffbce67f90746ca3c src/beta.in | build/beta.o shared/action.cfg || order/beta.stamp + +build out/fallback: e8c3bab461990fead7f11b007334dcd68b89db712472c5a294c69609dd7302b5 src/target.in | include/fallback.h || order/target.stamp diff --git a/tests/std_filter_tests/command_filters/mod.rs b/tests/std_filter_tests/command_filters/mod.rs index 79de9d242..eade74612 100644 --- a/tests/std_filter_tests/command_filters/mod.rs +++ b/tests/std_filter_tests/command_filters/mod.rs @@ -37,8 +37,12 @@ impl CommandFixture { F: FnOnce(StdlibConfig) -> Result, { let temp = tempdir().context("create command fixture tempdir")?; - let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) - .map_err(|path| anyhow!("command fixture root is not valid UTF-8: {path:?}"))?; + let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()).map_err(|path| { + anyhow!( + "command fixture root is not valid UTF-8: {}", + path.display() + ) + })?; let helper_dir = Dir::open_ambient_dir(&root, ambient_authority()) .context("open command fixture directory")?; let helper = compiler(&helper_dir, &root, binary)?; diff --git a/tests/std_filter_tests/command_filters/windows_filter_tests.rs b/tests/std_filter_tests/command_filters/windows_filter_tests.rs index cb07b62a6..8a44ccf03 100644 --- a/tests/std_filter_tests/command_filters/windows_filter_tests.rs +++ b/tests/std_filter_tests/command_filters/windows_filter_tests.rs @@ -90,7 +90,7 @@ fn windows_command_setup( ) -> Result<(tempfile::TempDir, OsString, Utf8PathBuf)> { let temp = tempdir().context(ctx.tempdir)?; let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) - .map_err(|path| anyhow!("{}: {path:?}", ctx.root))?; + .map_err(|path| anyhow!("{}: {}", ctx.root, path.display()))?; let dir = Dir::open_ambient_dir(&root, ambient_authority()).context(ctx.dir)?; let helper = compile_rust_helper(&dir, &root, helper_name, helper_source).context(ctx.compile)?; diff --git a/tests/std_filter_tests/support.rs b/tests/std_filter_tests/support.rs index a79c1c6b7..9392b3a7d 100644 --- a/tests/std_filter_tests/support.rs +++ b/tests/std_filter_tests/support.rs @@ -70,7 +70,7 @@ pub(crate) mod fallible { pub(crate) fn filter_workspace() -> Result { let temp = tempdir().context("create standard filter workspace")?; let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) - .map_err(|path| anyhow!("workspace path is not valid UTF-8: {path:?}"))?; + .map_err(|path| anyhow!("workspace path is not valid UTF-8: {}", path.display()))?; let dir = Dir::open_ambient_dir(&root, ambient_authority()) .context("open filter workspace directory")?; dir.write("file", b"data") diff --git a/tests/stdlib_expanduser_filter_tests.rs b/tests/stdlib_expanduser_filter_tests.rs index 95b2d9892..bd9f193ce 100644 --- a/tests/stdlib_expanduser_filter_tests.rs +++ b/tests/stdlib_expanduser_filter_tests.rs @@ -24,7 +24,7 @@ struct StdlibWorkspace { fn stdlib_workspace() -> Result { let temp = tempfile::tempdir().context("create temp workspace")?; let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) - .map_err(|path| anyhow!("temp path should be UTF-8: {path:?}"))?; + .map_err(|path| anyhow!("temp path should be UTF-8: {}", path.display()))?; Ok(StdlibWorkspace { _temp: temp, root }) } diff --git a/tests/stdlib_which_pathext_tests.rs b/tests/stdlib_which_pathext_tests.rs index 7dc98fa15..027a08dd9 100644 --- a/tests/stdlib_which_pathext_tests.rs +++ b/tests/stdlib_which_pathext_tests.rs @@ -55,7 +55,7 @@ impl ToolWorkspace { fn tool_workspace() -> Result { let temp = tempfile::tempdir().context("create temp workspace")?; let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) - .map_err(|path| anyhow!("temp path should be UTF-8: {path:?}"))?; + .map_err(|path| anyhow!("temp path should be UTF-8: {}", path.display()))?; let dir = Dir::open_ambient_dir(&root, ambient_authority()) .with_context(|| format!("open workspace {root}"))?; let bin = root.join("bin"); diff --git a/tests/stdlib_which_tests.rs b/tests/stdlib_which_tests.rs index 58b96997e..a30d8ceca 100644 --- a/tests/stdlib_which_tests.rs +++ b/tests/stdlib_which_tests.rs @@ -29,7 +29,7 @@ fn stdlib_env(root: &Utf8Path, path_override: OsString) -> Result Result { let temp = tempfile::tempdir().context("create temp workspace")?; let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) - .map_err(|path| anyhow!("temp path should be UTF-8: {path:?}"))?; + .map_err(|path| anyhow!("temp path should be UTF-8: {}", path.display()))?; Ok(StdlibWorkspace { _temp: temp, root }) } diff --git a/tests/which_diagnostic_snapshot_tests.rs b/tests/which_diagnostic_snapshot_tests.rs index 303a5f784..8ab9dcf6c 100644 --- a/tests/which_diagnostic_snapshot_tests.rs +++ b/tests/which_diagnostic_snapshot_tests.rs @@ -18,7 +18,7 @@ struct SnapshotWorkspace { fn snapshot_workspace() -> Result { let temp = tempfile::tempdir().context("create snapshot workspace")?; let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) - .map_err(|path| anyhow!("temp path should be UTF-8: {path:?}"))?; + .map_err(|path| anyhow!("temp path should be UTF-8: {}", path.display()))?; Ok(SnapshotWorkspace { _temp: temp, root }) } diff --git a/tests/workflow_contracts/ci_lint_test.py b/tests/workflow_contracts/ci_lint_test.py index cb0aae5a1..754e095f8 100644 --- a/tests/workflow_contracts/ci_lint_test.py +++ b/tests/workflow_contracts/ci_lint_test.py @@ -153,6 +153,47 @@ def test_makefile_clippy_flags_stay_workspace_wide() -> None: ) +def test_makefile_check_fmt_runs_markdown_format_checker() -> None: + """Protect Markdown formatting from being silently removed from make check-fmt.""" + makefile_lines = MAKEFILE_PATH.read_text(encoding="utf-8").splitlines() + target_index = next( + ( + index + for index, line in enumerate(makefile_lines) + if line.startswith("check-fmt:") + ), + None, + ) + assert target_index is not None, "the Makefile must define a check-fmt target" + + top_level_target = re.compile(r"^[A-Za-z0-9_.%/-]+:") + recipe_lines = [] + for line in makefile_lines[target_index + 1 :]: + if top_level_target.match(line): + break + if line.startswith("\t"): + recipe_lines.append(line) + recipe = "\n".join(recipe_lines) + expected_pipeline = ( + "@$(MD_FILES_FIND) | xargs -0 -r scripts/check-markdown-format.sh" + ) + + required_fragments = { + "$(MD_FILES_FIND)": "discover Markdown files with $(MD_FILES_FIND)", + "scripts/check-markdown-format.sh": "invoke the Markdown format checker", + "xargs -0 -r": "batch Markdown paths with NUL delimiters and skip empty input", + } + missing_fragments = [ + description + for fragment, description in required_fragments.items() + if fragment not in recipe + ] + assert not missing_fragments, "check-fmt must " + "; ".join(missing_fragments) + assert recipe.count(expected_pipeline) == 1, ( + "check-fmt must contain exactly one Markdown format checker pipeline" + ) + + def test_nextest_version_declared_once_at_workflow_scope() -> None: r"""NEXTEST_VERSION is declared once, at workflow scope. @@ -231,16 +272,22 @@ def test_mdtablefix_installers_require_the_pinned_version() -> None: job_steps(workflow, job_name), "Install mdtablefix", ) - run = step.get("run") - assert isinstance(run, str), f"{job_name} must configure mdtablefix" - assert expected_guard in run, f"{job_name} must pin the expected version" - assert "mdtablefix --version" in run, ( - f"{job_name} must inspect the installed version" - ) - assert "tr -d '\\r'" in run, f"{job_name} must normalise Windows version output" - assert expected_match in run, ( - f"{job_name} must replace a missing or mismatched formatter" - ) + match step.get("run"): + case str() as run: + assert expected_guard in run, ( + f"{job_name} must pin the expected version" + ) + assert "mdtablefix --version" in run, ( + f"{job_name} must inspect the installed version" + ) + assert "tr -d '\\r'" in run, ( + f"{job_name} must normalise Windows version output" + ) + assert expected_match in run, ( + f"{job_name} must replace a missing or mismatched formatter" + ) + case _: + pytest.fail(f"{job_name} must configure mdtablefix") def test_build_job_runs_markdown_formatter_checker_tests() -> None: