gix-testtools-0.19.0/.cargo_vcs_info.json0000644000000001511046102023000137670ustar { "git": { "sha1": "8eefc3153366671deaf1b70b43346b9587b89dd1" }, "path_in_vcs": "tests/tools" }gix-testtools-0.19.0/Cargo.lock0000644000001114321046102023000117470ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anyhow" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "bitflags" version = "2.11.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", ] [[package]] name = "block-buffer" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" dependencies = [ "hybrid-array", ] [[package]] name = "bstr" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", "serde", ] [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ "libc", ] [[package]] name = "crc" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] [[package]] name = "crc-catalog" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crypto-common" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", ] [[package]] name = "crypto-common" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" dependencies = [ "hybrid-array", ] [[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.7", ] [[package]] name = "digest" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" dependencies = [ "block-buffer 0.12.0", "crypto-common 0.2.1", ] [[package]] name = "document-features" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] [[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys", ] [[package]] name = "faster-hex" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" dependencies = [ "heapless", "serde", ] [[package]] name = "fastrand" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "filetime" version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", "libredox", ] [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "foldhash" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "fs_extra" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", "wasip3", ] [[package]] name = "gix-actor" version = "0.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bc998b8f746dda8565450d08a63b792ced9165d8c27a1ed3f02799ec6a7820f" dependencies = [ "bstr", "gix-date", "gix-error", ] [[package]] name = "gix-attributes" version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d43f12e246d3bf7ec624c8fc15ac4a4b62b7c4c6f586cb82be6c90bf84c9d02" dependencies = [ "bstr", "gix-glob", "gix-path", "gix-quote", "gix-trace", "kstring", "smallvec", "thiserror", "unicode-bom", ] [[package]] name = "gix-bitmap" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ebef0c26ad305747649e727bbcd56a7b7910754eb7cea88f6dff6f93c51283" dependencies = [ "gix-error", ] [[package]] name = "gix-chunk" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9faee47943b638e58ddd5e275a4906ad3e4b6c8584f1d41bd18ab9032ec52afb" dependencies = [ "gix-error", ] [[package]] name = "gix-commitgraph" version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f675d0df484a7f6a47e64bd6f311af489d947c0323b0564f36d14f3d7762abb" dependencies = [ "bstr", "gix-chunk", "gix-error", "gix-hash", "memmap2", "nonempty", ] [[package]] name = "gix-date" version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3ecab64a98bbac9f8e02990a9ea5e3c974a7d49b95f2bd70ad94ad22fa6b48c" dependencies = [ "bstr", "gix-error", "itoa", "jiff", ] [[package]] name = "gix-discover" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77bacdd12b7879d2178a80c58c2f319995e4654e1a7a23e3181e5c8a12b824f7" dependencies = [ "bstr", "dunce", "gix-fs", "gix-path", "gix-ref", "gix-sec", "thiserror", ] [[package]] name = "gix-error" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e57831e199be480af90dcd7e459abed8a174c09ec9a6e2cc8f7ca6c54598b06b" dependencies = [ "bstr", ] [[package]] name = "gix-features" version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1849ae154d38bc403185be14fa871e38e3c93ee606875d94e207fdb9fba52dbc" dependencies = [ "gix-path", "gix-trace", "gix-utils", "libc", "prodash", "walkdir", ] [[package]] name = "gix-fs" version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6cdff46db8798e47e2f727d84b9379aac5add3dd3d9d0b07bb4d7d5d640771fe" dependencies = [ "bstr", "fastrand", "gix-features", "gix-path", "gix-utils", "thiserror", ] [[package]] name = "gix-glob" version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1fcb8ef5b16bcf874abe9b68d8abb3c0493c876d367ab824151f30a0f3f3756" dependencies = [ "bitflags", "bstr", "gix-features", "gix-path", ] [[package]] name = "gix-hash" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0926d3819c837750b4e03c7754901e73f68b8c9b690753a6372a1bed4eedce" dependencies = [ "faster-hex", "gix-features", "sha1-checked", "sha2", "thiserror", ] [[package]] name = "gix-hashtable" version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0e30b93eea8718baf7d8153fcb938e2926175bbf18097c09f1c01b6f0be0563" dependencies = [ "gix-hash", "hashbrown 0.17.0", "parking_lot", ] [[package]] name = "gix-ignore" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d491bab9bf2c9f341dc754f425c31d5d3f63aca615312167b82e1deeaca97d8d" dependencies = [ "bstr", "gix-glob", "gix-path", "gix-trace", "unicode-bom", ] [[package]] name = "gix-index" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e6b28cc592dc753adb58302bb14a64e412ee591a3bec77aa4df87bff74fa80d" dependencies = [ "bitflags", "bstr", "filetime", "fnv", "gix-bitmap", "gix-features", "gix-fs", "gix-hash", "gix-lock", "gix-object", "gix-traverse", "gix-utils", "gix-validate", "hashbrown 0.17.0", "itoa", "libc", "memmap2", "rustix", "smallvec", "thiserror", ] [[package]] name = "gix-lock" version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65c9dedd9e90b0d47624d2ed241d394e09294118364e87b9b7e5f1fe755f3c2c" dependencies = [ "gix-tempfile", "gix-utils", "thiserror", ] [[package]] name = "gix-object" version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5cd857e29429c7213bdef3f5aef83f8cc124774fe8ae0d27b1607d218d6d525" dependencies = [ "bstr", "gix-actor", "gix-date", "gix-features", "gix-hash", "gix-hashtable", "gix-utils", "gix-validate", "itoa", "smallvec", "thiserror", ] [[package]] name = "gix-path" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afa6ac14cd14939ea94a496ce7460daa6511c09f5b84757e9cfc6f9c8d0f93a6" dependencies = [ "bstr", "gix-trace", "gix-validate", "thiserror", ] [[package]] name = "gix-quote" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6e541fc33cc2b783b7979040d445a0c86a2eca747c8faea4ca84230d06ae6ef" dependencies = [ "bstr", "gix-error", "gix-utils", ] [[package]] name = "gix-ref" version = "0.64.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c04f64c37eb7e6feb73c7060f8dc6f381cc5de5d53249bfd450bc48a86b2e8b" dependencies = [ "gix-actor", "gix-features", "gix-fs", "gix-hash", "gix-lock", "gix-object", "gix-path", "gix-tempfile", "gix-utils", "gix-validate", "memmap2", "thiserror", ] [[package]] name = "gix-revwalk" version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85f5756abffe0917827aac683b13684ed99875bc398fa1f9b8f479b0681ef9e6" dependencies = [ "gix-commitgraph", "gix-date", "gix-error", "gix-hash", "gix-hashtable", "gix-object", "smallvec", "thiserror", ] [[package]] name = "gix-sec" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab8519976e4c7e486270740a5400369f37940779b80bd1377d94cfa1125d01b3" dependencies = [ "bitflags", "gix-path", "libc", "windows-sys", ] [[package]] name = "gix-tempfile" version = "23.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27850097e1ff9515f46a0dad0f5f9c9d020e972727772dabab9450690c4adb22" dependencies = [ "gix-fs", "libc", "parking_lot", "signal-hook", "signal-hook-registry", "tempfile", ] [[package]] name = "gix-testtools" version = "0.19.0" dependencies = [ "bstr", "crc", "document-features", "fastrand", "fs_extra", "gix-discover", "gix-fs", "gix-hash", "gix-lock", "gix-tempfile", "gix-worktree", "io-close", "is_ci", "parking_lot", "serial_test", "tar", "tempfile", "xz2", ] [[package]] name = "gix-trace" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44dc45eae785c0eb14173e0f152e6e224dcf4d45b6a6999a3aed22af541ad678" [[package]] name = "gix-traverse" version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8de590ecc86a3b2870665f2288324fa9f7f8672c7fc2d4e020fdd81cd1f7aed" dependencies = [ "bitflags", "gix-commitgraph", "gix-date", "gix-hash", "gix-hashtable", "gix-object", "gix-revwalk", "smallvec", "thiserror", ] [[package]] name = "gix-utils" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66c50966184123caf580ffa64e28031a878597f1c7fceb8fe19566c38eb1b771" dependencies = [ "fastrand", "unicode-normalization", ] [[package]] name = "gix-validate" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bc6fc771c4063ba7cd2f47b91fb6076251c6a823b64b7fe7b8874b0fe4afae3" dependencies = [ "bstr", ] [[package]] name = "gix-worktree" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cef414ed275e8407cd5d53d301e83be19700b0dd3f859d2434417b58f454a2d1" dependencies = [ "bstr", "gix-attributes", "gix-fs", "gix-glob", "gix-hash", "gix-ignore", "gix-index", "gix-object", "gix-path", "gix-validate", ] [[package]] name = "hash32" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" dependencies = [ "byteorder", ] [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash 0.1.5", ] [[package]] name = "hashbrown" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" dependencies = [ "allocator-api2", "equivalent", "foldhash 0.2.0", ] [[package]] name = "heapless" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" dependencies = [ "hash32", "stable_deref_trait", ] [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hybrid-array" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" dependencies = [ "typenum", ] [[package]] name = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "indexmap" version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.0", "serde", "serde_core", ] [[package]] name = "io-close" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc" dependencies = [ "libc", "winapi", ] [[package]] name = "is_ci" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", "serde_core", "windows-sys", ] [[package]] name = "jiff-static" version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "jiff-tzdb" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" [[package]] name = "jiff-tzdb-platform" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" dependencies = [ "jiff-tzdb", ] [[package]] name = "kstring" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" dependencies = [ "static_assertions", ] [[package]] name = "leb128fmt" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libredox" version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ "bitflags", "libc", "plain", "redox_syscall 0.7.4", ] [[package]] name = "linux-raw-sys" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litrs" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ "scopeguard", ] [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lzma-sys" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" dependencies = [ "cc", "libc", "pkg-config", ] [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmap2" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", ] [[package]] name = "nonempty" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall 0.5.18", "smallvec", "windows-link", ] [[package]] name = "pkg-config" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "prettyplease" version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", ] [[package]] name = "proc-macro2" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "prodash" version = "31.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c" dependencies = [ "parking_lot", ] [[package]] name = "quote" version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "redox_syscall" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ "bitflags", ] [[package]] name = "regex-automata" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys", ] [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "scc" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" dependencies = [ "sdd", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sdd" version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" [[package]] name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", "serde", "serde_core", "zmij", ] [[package]] name = "serial_test" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" dependencies = [ "once_cell", "parking_lot", "scc", "serial_test_derive", ] [[package]] name = "serial_test_derive" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures 0.2.17", "digest 0.10.7", ] [[package]] name = "sha1-checked" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" dependencies = [ "digest 0.10.7", "sha1", ] [[package]] name = "sha2" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures 0.3.0", "digest 0.11.2", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" dependencies = [ "libc", "signal-hook-registry", ] [[package]] name = "signal-hook-registry" version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ "errno", "libc", ] [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tar" version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", ] [[package]] name = "tempfile" version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom", "once_cell", "rustix", "windows-sys", ] [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tinyvec" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "typenum" version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-bom" version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasip2" version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ "wit-bindgen 0.57.1", ] [[package]] name = "wasip3" version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ "wit-bindgen 0.51.0", ] [[package]] name = "wasm-encoder" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ "leb128fmt", "wasmparser", ] [[package]] name = "wasm-metadata" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", "indexmap", "wasm-encoder", "wasmparser", ] [[package]] name = "wasmparser" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", "indexmap", "semver", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ "windows-sys", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ "wit-bindgen-rust-macro", ] [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "wit-bindgen-core" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", "heck", "wit-parser", ] [[package]] name = "wit-bindgen-rust" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", "indexmap", "prettyplease", "syn", "wasm-metadata", "wit-bindgen-core", "wit-component", ] [[package]] name = "wit-bindgen-rust-macro" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" dependencies = [ "anyhow", "prettyplease", "proc-macro2", "quote", "syn", "wit-bindgen-core", "wit-bindgen-rust", ] [[package]] name = "wit-component" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", "indexmap", "log", "serde", "serde_derive", "serde_json", "wasm-encoder", "wasm-metadata", "wasmparser", "wit-parser", ] [[package]] name = "wit-parser" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", "indexmap", "log", "semver", "serde", "serde_derive", "serde_json", "unicode-xid", "wasmparser", ] [[package]] name = "xz2" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" dependencies = [ "lzma-sys", ] [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" gix-testtools-0.19.0/Cargo.toml0000644000000105661046102023000120000ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2024" rust-version = "1.85" name = "gix-testtools" version = "0.19.0" authors = ["Sebastian Thiel "] build = false include = [ "/src/**/*", "/LICENSE-*", ] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Shared code for gitoxide crates to facilitate testing" readme = false license = "MIT OR Apache-2.0" resolver = "2" [package.metadata.docs.rs] all-features = true features = ["document-features"] [features] default = ["worktree-exclusions"] worktree-exclusions = [ "dep:gix-discover", "dep:gix-worktree", "dep:gix-fs", ] xz = ["dep:xz2"] [lib] name = "gix_testtools" path = "src/lib.rs" doctest = true [[bin]] name = "jtt" path = "src/main.rs" [dependencies.bstr] version = "1.12.0" default-features = false [dependencies.crc] version = "3.4.0" [dependencies.document-features] version = "0.2.1" optional = true [dependencies.fastrand] version = "2.0.0" [dependencies.fs_extra] version = "1.2.0" [dependencies.gix-discover] version = "^0.52.0" optional = true [dependencies.gix-fs] version = "^0.21.2" optional = true [dependencies.gix-hash] version = "^0.25.1" features = [ "sha1", "sha256", ] [dependencies.gix-lock] version = "^23.0.0" [dependencies.gix-tempfile] version = "^23.0.0" features = ["signals"] default-features = false [dependencies.gix-worktree] version = "^0.53.0" optional = true [dependencies.io-close] version = "0.3.7" [dependencies.is_ci] version = "1.1.1" [dependencies.parking_lot] version = "0.12.4" [dependencies.tar] version = "0.4.38" default-features = false [dependencies.tempfile] version = "3.26.0" [dependencies.xz2] version = "0.1.6" optional = true [dev-dependencies.serial_test] version = "3.4.0" default-features = false [lints.clippy] bool_to_int_with_if = "allow" borrow_as_ptr = "allow" cast_lossless = "allow" cast_possible_truncation = "allow" cast_possible_wrap = "allow" cast_precision_loss = "allow" cast_sign_loss = "allow" checked_conversions = "allow" copy_iterator = "allow" default_trait_access = "allow" doc_markdown = "allow" empty_docs = "allow" enum_glob_use = "allow" explicit_deref_methods = "allow" explicit_into_iter_loop = "allow" explicit_iter_loop = "allow" filter_map_next = "allow" fn_params_excessive_bools = "allow" from_iter_instead_of_collect = "allow" if_not_else = "allow" ignored_unit_patterns = "allow" implicit_clone = "allow" inconsistent_struct_constructor = "allow" inefficient_to_string = "allow" inline_always = "allow" items_after_statements = "allow" iter_not_returning_iterator = "allow" iter_without_into_iter = "allow" large_enum_variant = "allow" large_stack_arrays = "allow" manual_assert = "allow" manual_is_variant_and = "allow" manual_let_else = "allow" manual_string_new = "allow" many_single_char_names = "allow" match_bool = "allow" match_same_arms = "allow" match_wild_err_arm = "allow" match_wildcard_for_single_variants = "allow" missing_errors_doc = "allow" missing_panics_doc = "allow" module_name_repetitions = "allow" must_use_candidate = "allow" mut_mut = "allow" naive_bytecount = "allow" needless_continue = "allow" needless_for_each = "allow" needless_pass_by_value = "allow" needless_raw_string_hashes = "allow" no_effect_underscore_binding = "allow" option_option = "allow" range_plus_one = "allow" redundant_else = "allow" result_large_err = "allow" return_self_not_must_use = "allow" should_panic_without_expect = "allow" similar_names = "allow" single_match_else = "allow" stable_sort_primitive = "allow" struct_excessive_bools = "allow" struct_field_names = "allow" too_long_first_doc_paragraph = "allow" too_many_lines = "allow" transmute_ptr_to_ptr = "allow" trivially_copy_pass_by_ref = "allow" unnecessary_join = "allow" unnecessary_wraps = "allow" unreadable_literal = "allow" unused_self = "allow" used_underscore_binding = "allow" wildcard_imports = "allow" [lints.clippy.pedantic] level = "warn" priority = -1 [lints.rust] gix-testtools-0.19.0/Cargo.toml.orig000064400000000000000000000036021046102023000154300ustar 00000000000000lints.workspace = true [package] name = "gix-testtools" description = "Shared code for gitoxide crates to facilitate testing" version = "0.19.0" authors = ["Sebastian Thiel "] edition = "2024" license = "MIT OR Apache-2.0" include = ["/src/**/*", "/LICENSE-*"] rust-version = "1.85" [[bin]] name = "jtt" path = "src/main.rs" [lib] doctest = true [features] default = ["worktree-exclusions"] ## Use the current repository's ignore rules to decide if generated fixture archives should be written. worktree-exclusions = ["dep:gix-discover", "dep:gix-worktree", "dep:gix-fs"] ## Use instead of plain `tar` files, compress these to produce `tar.xz` files instead. ## This is useful if archives are uploaded into `git-lfs`, which doesn't have built-in compression ## and metering counts towards uncompressed bytes transferred. xz = ["dep:xz2"] [dependencies] gix-hash = { version = "^0.25.1", path = "../../gix-hash", features = ["sha1", "sha256"] } gix-lock = { version = "^23.0.0", path = "../../gix-lock" } gix-discover = { version = "^0.52.0", path = "../../gix-discover", optional = true } gix-worktree = { version = "^0.53.0", path = "../../gix-worktree", optional = true } gix-fs = { version = "^0.21.2", path = "../../gix-fs", optional = true } gix-tempfile = { version = "^23.0.0", path = "../../gix-tempfile", default-features = false, features = ["signals"] } fastrand = "2.0.0" bstr = { version = "1.12.0", default-features = false } crc = "3.4.0" tempfile = "3.26.0" fs_extra = "1.2.0" parking_lot = { version = "0.12.4" } is_ci = "1.1.1" io-close = "0.3.7" tar = { version = "0.4.38", default-features = false } xz2 = { version = "0.1.6", optional = true } document-features = { version = "0.2.1", optional = true } [dev-dependencies] serial_test = { version = "3.4.0", default-features = false } [package.metadata.docs.rs] all-features = true features = ["document-features"] gix-testtools-0.19.0/LICENSE-APACHE000064400000000000000000000236761046102023000145020ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS gix-testtools-0.19.0/LICENSE-MIT000064400000000000000000000017771046102023000142100ustar 00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gix-testtools-0.19.0/src/lib.rs000064400000000000000000002224331046102023000144510ustar 00000000000000//! Utilities for testing `gitoxide` crates, many of which might be useful for testing programs that use `git` in general. //! //! ## Environment Variables //! //! ### `GIX_TEST_FIXTURE_HASH` //! //! Set this variable to control which hash function is used when creating or loading test fixtures. //! Valid values are the names of hash functions supported by `gix_hash::Kind` (e.g., `sha1`, `sha256`). //! If not set, the default hash function via `gix_hash::Kind::default()` is used. //! //! ## Feature Flags #![cfg_attr( all(doc, feature = "document-features"), doc = ::document_features::document_features!() )] #![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))] #![deny(missing_docs)] use std::{ collections::{BTreeMap, HashMap}, env, ffi::{OsStr, OsString}, io::Read, path::{Path, PathBuf}, str::FromStr, time::Duration, }; pub use bstr; use bstr::ByteSlice; use io_close::Close; pub use is_ci; use parking_lot::Mutex; use std::sync::LazyLock; pub use tempfile; const ARCHIVE_DIR_NAME: &str = "generated-archives"; /// A result type to allow using the try operator `?` in unit tests. /// /// Use it like so: /// /// ```no_run /// use gix_testtools::Result; /// /// #[test] /// fn this() -> Result { /// let x: usize = "42".parse()?; /// Ok(()) /// /// } /// ``` pub type Result = std::result::Result>; /// A result type for post-processing closures in `*_with_post` fixture functions. /// /// The closure can return any value `T`, which will be returned alongside the fixture path. /// This is useful for computing values based on the fixture contents. pub type PostResult = std::result::Result>; /// Build `example` from `package` and copy the executable to this test process' temporary target directory. /// /// The returned executable path is stable for the lifetime of the test process and avoids races with other /// concurrently running tests that may cause Cargo to update the shared example binary in `target/debug/examples`. pub fn build_example_for_test(package: &str, example: &str, target_tmpdir: impl Into) -> PathBuf { let mut cargo = std::process::Command::new(env::var_os("CARGO").unwrap_or_else(|| OsString::from(env!("CARGO")))); let res = cargo .args(["build", "-p", package, "--example", example]) .status() .expect("cargo should run fine"); assert!(res.success(), "cargo invocation should be successful"); let target_tmpdir = target_tmpdir.into(); let shared_path = target_tmpdir .ancestors() .nth(1) .expect("first parent in target dir") .join("debug") .join("examples") .join(format!("{example}{}", std::env::consts::EXE_SUFFIX)); let stable_path = target_tmpdir.join(format!( "{example}-{}{}", std::process::id(), std::env::consts::EXE_SUFFIX )); let mut last_err = None; for _ in 0..10 { match std::fs::copy(&shared_path, &stable_path) { Ok(_) => return stable_path, Err(err) => { last_err = Some(err); std::thread::sleep(Duration::from_millis(50)); } } } panic!( "driver at {} could be copied for stable test execution: {last_err:?}", shared_path.display() ); } /// Indicates the state of a fixture when a closure is called. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FixtureState<'a> { /// The fixture is newly created and needs post-processing. /// /// The closure should perform any necessary modifications to the fixture /// directory and compute its return value. Uninitialized(&'a Path), /// The fixture was already created (cached) and only needs to produce a return value. /// /// The closure should NOT modify the fixture directory, but only compute /// and return a value based on the existing contents. Fresh(&'a Path), } impl FixtureState<'_> { /// Returns the path of the fixture, which is always a directory. pub fn path(&self) -> &Path { match self { FixtureState::Uninitialized(path) | FixtureState::Fresh(path) => path, } } /// Returns true if the fixture is uninitialized and needs to be modified. pub fn is_uninitialized(&self) -> bool { matches!(self, FixtureState::Uninitialized(_)) } } /// Determines whether fixture generation should skip creating, updating, or overwriting a cached fixture archive. /// /// In this module, an archive is the tar file under [`ARCHIVE_DIR_NAME`] that stores the output of a fixture /// script or Rust fixture closure. The read-only fixture helpers unpack that archive when it already exists, and /// [`create_archive_if_we_should()`] consults this trait before writing a new archive from the generated fixture /// directory. trait IsExcluded { /// Return true if `archive` matches the configured exclusion source. fn is_excluded(&self, archive: &Path) -> bool; } /// Checks whether `archive` matches `.gitignore`-style lines read by [`GitignoreExclusions`]. /// /// This is the fallback used when the `worktree-exclusions` feature is disabled, so the full `gix-worktree` /// exclusion stack is not available. In that configuration, [`GitignoreExclusions::is_excluded()`] reads the /// `.gitignore` next to the generated archive and delegates the line matching to this function. #[cfg(not(feature = "worktree-exclusions"))] fn is_excluded_by_lines(lines: &str, archive: &Path) -> bool { let archive = archive.to_string_lossy().replace('\\', "/"); let filename = archive.rsplit('/').next().unwrap_or(&archive); lines.lines().any(|line| { let pattern = line.trim(); if pattern.is_empty() || pattern.starts_with('#') { return false; } let pattern = pattern.trim_start_matches('/'); let candidate = if pattern.contains('/') { archive.as_str() } else { filename }; wildcard_match(pattern, candidate) }) } /// Matches `text` against the fallback exclusion pattern syntax. /// /// The matcher understands literal characters and `*`, where `*` matches any byte sequence, including path /// separators. Patterns are anchored to the full `text`; other `.gitignore` features such as `?`, character /// classes, directory-only matches, and negation are not supported here. /// /// For example, `*.tar` matches `fixture.tar`, `generated-archives/*.tar` matches /// `generated-archives/fixture.tar`, and `generated-*/*.tar` matches `generated-archives/fixture.tar`. #[cfg(not(feature = "worktree-exclusions"))] fn wildcard_match(pattern: &str, text: &str) -> bool { if !pattern.contains('*') { return pattern == text; } let mut remainder = text; let mut parts = pattern.split('*').peekable(); let first = parts.next().expect("split yields at least one item"); if !first.is_empty() { let Some(stripped) = remainder.strip_prefix(first) else { return false; }; remainder = stripped; } while let Some(part) = parts.next() { if part.is_empty() { continue; } let Some(pos) = remainder.find(part) else { return false; }; remainder = &remainder[pos + part.len()..]; if parts.peek().is_none() && !pattern.ends_with('*') { return remainder.is_empty(); } } pattern.ends_with('*') || remainder.is_empty() } /// A wrapper for a running git-daemon which is stopped automatically on drop. /// /// Note that we will swallow any errors, assuming that the test would have failed if the daemon crashed. pub struct GitDaemon { process: GitDaemonProcess, /// The base url under which all repositories are hosted, typically `git://127.0.0.1:port`. pub url: String, } enum GitDaemonProcess { #[cfg(not(unix))] Child(std::process::Child), #[cfg(unix)] Inetd { shutdown: std::sync::Arc, server_addr: std::net::SocketAddr, listener_thread: Option>, }, } impl Drop for GitDaemon { fn drop(&mut self) { match &mut self.process { #[cfg(not(unix))] GitDaemonProcess::Child(child) => { child.kill().ok(); } #[cfg(unix)] GitDaemonProcess::Inetd { shutdown, server_addr, listener_thread, } => { shutdown.store(true, std::sync::atomic::Ordering::SeqCst); std::net::TcpStream::connect(*server_addr).ok(); if let Some(listener_thread) = listener_thread.take() { listener_thread.join().ok(); } } } } } static SCRIPT_IDENTITY: LazyLock>> = LazyLock::new(|| Mutex::new(BTreeMap::new())); #[cfg(feature = "worktree-exclusions")] static EXCLUDE_LUT: LazyLock>> = LazyLock::new(|| { let cache = (|| { let (repo_path, _) = gix_discover::upwards(Path::new(".")).ok()?; let (gix_dir, work_tree) = repo_path.into_repository_and_work_tree_directories(); let work_tree = work_tree?.canonicalize().ok()?; let mut buf = Vec::with_capacity(512); let case = if gix_fs::Capabilities::probe(&work_tree).ignore_case { gix_worktree::ignore::glob::pattern::Case::Fold } else { Default::default() }; let state = gix_worktree::stack::State::IgnoreStack(gix_worktree::stack::state::Ignore::new( Default::default(), gix_worktree::ignore::Search::from_git_dir( &gix_dir, None, &mut buf, gix_worktree::stack::state::ignore::ParseIgnore { support_precious: false, }, ) .ok()?, None, gix_worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped, Default::default(), )); Some(gix_worktree::Stack::new( work_tree, state, case, buf, Default::default(), )) })(); Mutex::new(cache) }); #[cfg(feature = "worktree-exclusions")] struct WorktreeExclusions; #[cfg(feature = "worktree-exclusions")] impl IsExcluded for WorktreeExclusions { fn is_excluded(&self, archive: &Path) -> bool { let mut lut = EXCLUDE_LUT.lock(); lut.as_mut() .and_then(|cache| { let archive = env::current_dir().ok()?.join(archive); let relative_path = archive.strip_prefix(cache.base()).ok()?; cache .at_path( relative_path, Some(gix_worktree::index::entry::Mode::FILE), &gix_worktree::object::find::Never, ) .ok()? .is_excluded() .into() }) .unwrap_or(false) } } #[cfg(feature = "worktree-exclusions")] fn default_excludes() -> &'static dyn IsExcluded { static WORKTREE_EXCLUSIONS: WorktreeExclusions = WorktreeExclusions; &WORKTREE_EXCLUSIONS } #[cfg(not(feature = "worktree-exclusions"))] struct GitignoreExclusions; #[cfg(not(feature = "worktree-exclusions"))] impl IsExcluded for GitignoreExclusions { fn is_excluded(&self, archive: &Path) -> bool { let Some(parent) = archive.parent() else { return false; }; std::fs::read_to_string(parent.join(".gitignore")).is_ok_and(|lines| is_excluded_by_lines(&lines, archive)) } } #[cfg(not(feature = "worktree-exclusions"))] fn default_excludes() -> &'static dyn IsExcluded { static GITIGNORE_EXCLUSIONS: GitignoreExclusions = GitignoreExclusions; &GITIGNORE_EXCLUSIONS } #[cfg(windows)] const GIT_PROGRAM: &str = "git.exe"; #[cfg(not(windows))] const GIT_PROGRAM: &str = "git"; const DISABLE_AUTO_MAINTENANCE_CONFIG: &[(&str, &str)] = &[("maintenance.auto", "false"), ("gc.auto", "0")]; const ISOLATED_GIT_CONFIG: &[(&str, &str)] = &[ ("commit.gpgsign", "false"), ("tag.gpgsign", "false"), ("init.defaultBranch", "main"), ("protocol.file.allow", "always"), ("maintenance.auto", "false"), ("gc.auto", "0"), ]; static GIT_CORE_DIR: LazyLock = LazyLock::new(|| { let output = std::process::Command::new(GIT_PROGRAM) .arg("--exec-path") .output() .expect("can execute `git --exec-path`"); assert!(output.status.success(), "`git --exec-path` failed"); output .stdout .strip_suffix(b"\n") .expect("`git --exec-path` output to be well-formed") .to_os_str() .expect("no invalid UTF-8 in `--exec-path` except as OS allows") .into() }); /// The major, minor and patch level of the git version on the system. pub static GIT_VERSION: LazyLock<(u8, u8, u8)> = LazyLock::new(|| parse_git_version().expect("git version to be parsable")); /// Define how [`scripted_fixture_writable_with_args()`] and [`rust_fixture_writable()`] /// produces the writable copy. pub enum Creation { /// Run the code once and copy the data from its output to the writable location. /// This is fast but won't work if absolute paths are produced by the script. /// /// ### Limitation /// /// Cannot handle symlinks currently. Waiting for [this PR](https://github.com/webdesus/fs_extra/pull/70). CopyFromReadOnly, /// Run the code in the writable location. That way, absolute paths match the location. Execute, } /// Returns true if the given `major`, `minor` and `patch` is smaller than the actual git version on the system /// to facilitate skipping a test on the caller. /// Will never return true on CI which is expected to have a recent enough git version. /// /// # Panics /// /// If `git` cannot be executed or if its version output cannot be parsed. pub fn should_skip_as_git_version_is_smaller_than(major: u8, minor: u8, patch: u8) -> bool { if is_ci::cached() { return false; // CI should be made to use a recent git version, it should run there. } *GIT_VERSION < (major, minor, patch) } fn parse_git_version() -> Result<(u8, u8, u8)> { let output = std::process::Command::new(GIT_PROGRAM).arg("--version").output()?; git_version_from_bytes(&output.stdout) } fn git_version_from_bytes(bytes: &[u8]) -> Result<(u8, u8, u8)> { let mut numbers = bytes .split(|b| *b == b' ' || *b == b'\n') .nth(2) .expect("git version ") .split(|b| *b == b'.') .take(3) .map(|n| std::str::from_utf8(n).expect("valid utf8 in version number")) .map(u8::from_str); Ok((|| -> Result<_> { Ok(( numbers.next().expect("major")?, numbers.next().expect("minor")?, numbers.next().expect("patch")?, )) })() .map_err(|err| { format!( "Could not parse version from output of 'git --version' ({:?}) with error: {}", bytes.to_str_lossy(), err ) })?) } /// Set the current working dir to `new_cwd` and return a type that returns to the previous working dir on drop. pub fn set_current_dir(new_cwd: impl AsRef) -> std::io::Result { let cwd = env::current_dir()?; env::set_current_dir(new_cwd)?; Ok(AutoRevertToPreviousCWD(cwd)) } /// A utility to set the current working dir to the given value, on drop. /// /// # Panics /// /// Note that this will panic if the CWD cannot be set on drop. #[derive(Debug)] #[must_use] pub struct AutoRevertToPreviousCWD(PathBuf); impl Drop for AutoRevertToPreviousCWD { fn drop(&mut self) { env::set_current_dir(&self.0).unwrap(); } } /// Run `git` in `working_dir` with all provided `args`. pub fn run_git(working_dir: &Path, args: &[&str]) -> std::io::Result { let mut cmd = std::process::Command::new(GIT_PROGRAM); apply_git_config_by_environment(&mut cmd, DISABLE_AUTO_MAINTENANCE_CONFIG) .current_dir(working_dir) .args(args) .status() } /// Run `script` with [`bash_program()`] in `cwd`. /// /// Standard input is disconnected while standard output and error stay attached to the inherited /// handles. /// /// # Panics /// /// This function expects the script to succeed and will panic otherwise. pub fn invoke_bash(cwd: impl AsRef, script: &str) { let mut cmd = std::process::Command::new(bash_program()); let status = apply_git_config_by_environment(&mut cmd, DISABLE_AUTO_MAINTENANCE_CONFIG) .current_dir(cwd) .arg("-c") .arg(script) .stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::inherit()) .stderr(std::process::Stdio::inherit()) .status() .expect("can run bash script"); assert!(status.success(), "bash script failed with {status}"); } /// Spawn a git daemon to host all repositories at or below `working_dir`. /// /// It runs in the background until the [`GitDaemon`] is dropped. pub fn spawn_git_daemon(working_dir: impl AsRef) -> std::io::Result { #[cfg(unix)] { spawn_git_daemon_inetd(working_dir) } #[cfg(not(unix))] { spawn_git_daemon_process(working_dir) } } #[cfg(not(unix))] fn spawn_git_daemon_process(working_dir: impl AsRef) -> std::io::Result { let mut ports: Vec<_> = (9419u16..9419 + 100).collect(); fastrand::shuffle(&mut ports); let addr_at = |port| std::net::SocketAddr::from(([127, 0, 0, 1], port)); let free_port = { let listener = std::net::TcpListener::bind(ports.into_iter().map(addr_at).collect::>().as_slice())?; listener.local_addr().expect("listener address is available").port() }; let child = { let mut cmd = std::process::Command::new(GIT_CORE_DIR.join(if cfg!(windows) { "git-daemon.exe" } else { "git-daemon" })); apply_git_config_by_environment(&mut cmd, DISABLE_AUTO_MAINTENANCE_CONFIG) .current_dir(working_dir) .args(["--verbose", "--base-path=.", "--export-all", "--user-path"]) .arg(format!("--port={free_port}")) .spawn()? }; let server_addr = addr_at(free_port); for time in gix_lock::backoff::Quadratic::default_with_random() { std::thread::sleep(time); if std::net::TcpStream::connect(server_addr).is_ok() { break; } } Ok(GitDaemon { process: GitDaemonProcess::Child(child), url: format!("git://{server_addr}"), }) } #[cfg(unix)] fn spawn_git_daemon_inetd(working_dir: impl AsRef) -> std::io::Result { use std::{ net::{TcpListener, TcpStream}, os::fd::{FromRawFd, IntoRawFd}, process::Stdio, sync::{ Arc, atomic::{AtomicBool, Ordering}, }, }; fn stream_to_stdio(stream: TcpStream) -> Stdio { // SAFETY: `into_raw_fd()` transfers ownership of the socket fd, and `Stdio` // takes over closing it in the spawned child. unsafe { Stdio::from_raw_fd(stream.into_raw_fd()) } } let working_dir = working_dir.as_ref().to_owned(); let listener = TcpListener::bind(("127.0.0.1", 0))?; let server_addr = listener.local_addr()?; let shutdown = Arc::new(AtomicBool::new(false)); let listener_thread = std::thread::spawn({ let shutdown = shutdown.clone(); move || { for incoming in listener.incoming() { let stream = match incoming { Ok(stream) => stream, Err(_) => break, }; if shutdown.load(Ordering::SeqCst) { break; } let peer_addr = stream.peer_addr().ok(); let stdin = match stream.try_clone() { Ok(stream) => stream_to_stdio(stream), Err(_) => continue, }; let stdout = stream_to_stdio(stream); let mut cmd = std::process::Command::new(GIT_PROGRAM); let Ok(mut child) = apply_git_config_by_environment(&mut cmd, DISABLE_AUTO_MAINTENANCE_CONFIG) .args([ "-c", "uploadpack.allowrefinwant", "daemon", "--inetd", "--verbose", "--base-path=.", "--export-all", "--user-path", ]) .current_dir(&working_dir) .stdin(stdin) .stdout(stdout) .stderr(Stdio::null()) .envs(remote_env(peer_addr)) .spawn() else { continue; }; std::thread::spawn(move || { let _ = child.wait(); }); } } }); Ok(GitDaemon { process: GitDaemonProcess::Inetd { shutdown, server_addr, listener_thread: Some(listener_thread), }, url: format!("git://{server_addr}"), }) } #[cfg(unix)] fn remote_env(peer_addr: Option) -> Vec<(&'static str, String)> { peer_addr .map(|addr| { vec![ ("REMOTE_ADDR", addr.ip().to_string()), ("REMOTE_PORT", addr.port().to_string()), ] }) .unwrap_or_default() } /// Don't add a suffix to the archive name as `args` are platform dependent, non-deterministic, /// or otherwise don't influence the content of the archive. /// Note that this also means that `args` won't be used to control the hash of the archive itself. #[derive(Copy, Clone)] enum ArgsInHash { Yes, No, } /// Return the path to the `/tests/fixtures/` directory. pub fn fixture_path(path: impl AsRef) -> PathBuf { fixture_base().join(path.as_ref()) } fn fixture_base() -> PathBuf { PathBuf::from("tests").join("fixtures") } /// Load the fixture from `/tests/fixtures/` and return its data, or _panic_. pub fn fixture_bytes(path: impl AsRef) -> Vec { match std::fs::read(fixture_path(path.as_ref())) { Ok(res) => res, Err(_) => panic!("File at '{}' not found", path.as_ref().display()), } } /// Run the executable at `script_name`, like `make_repo.sh` or `my_setup.py` to produce a read-only directory to which /// the path is returned. /// /// Note that it persists and the script at `script_name` will only be executed once if it ran without error. /// /// ### Automatic Archive Creation /// /// In order to speed up CI and even local runs should the cache get purged, the result of each script run /// is automatically placed into a compressed _tar_ archive. /// If a script result doesn't exist, these will be checked first and extracted if present, which they are by default. /// This behaviour can be prohibited by setting the `GIX_TEST_IGNORE_ARCHIVES` to any value. /// /// To speed CI up, one can add these archives to the repository. Since LFS is not currently being used, it is /// important to check their size first, though in most cases generated archives will not be very large. /// /// #### Disable Archive Creation /// /// If archives aren't useful, they can be disabled by using `.gitignore` specifications. /// That way it's trivial to prevent creation of all archives with `generated-archives/*.tar{.xz}` in the root /// or more specific `.gitignore` configurations in lower levels of the work tree. /// /// The latter is useful if the script's output is platform specific. pub fn scripted_fixture_read_only(script_name: impl AsRef) -> Result { scripted_fixture_read_only_with_args(script_name, None::) } /// Like [`scripted_fixture_read_only()`], but uses a matching existing archive even if /// `GIX_TEST_IGNORE_ARCHIVES` is set. /// /// Use this only for fixtures whose generated contents are not stable across /// platforms or filesystems and must therefore be frozen by the checked-in /// archive. /// /// CI normally sets `GIX_TEST_IGNORE_ARCHIVES` so fixture scripts are rerun and /// tracked archives are proven reproducible. This helper is the opt-out for /// fixtures where rerunning the producer can legitimately change /// without changing semantics, for example when Git writes entries in filesystem /// traversal order. pub fn scripted_fixture_read_only_needs_archive(script_name: impl AsRef) -> Result { scripted_fixture_read_only_with_args_inner::) -> PostResult, ()>( script_name, None::, None, ArgsInHash::Yes, default_excludes(), None::<(u32, _)>, true, ) .map(|(dir, _)| dir) } /// Run the executable at `script_name`, like `make_repo.sh` to produce a writable directory to which /// the tempdir is returned. It will be removed automatically, courtesy of [`tempfile::TempDir`]. /// /// Note that `script_name` is only executed once, so the data can be copied from its read-only location. pub fn scripted_fixture_writable(script_name: impl AsRef) -> Result { scripted_fixture_writable_with_args(script_name, None::, Creation::CopyFromReadOnly) } /// Like [`scripted_fixture_writable()`], but passes `args` to `script_name` while providing control over /// the way files are created with `mode`. pub fn scripted_fixture_writable_with_args( script_name: impl AsRef, args: impl IntoIterator>, mode: Creation, ) -> Result { scripted_fixture_writable_with_args_inner::) -> PostResult, ()>( script_name, args, mode, ArgsInHash::Yes, default_excludes(), None::<(u32, _)>, ) .map(|(dir, _)| dir) } /// Like [`scripted_fixture_writable()`], but passes `args` to `script_name` while providing control over /// the way files are created with `mode`. /// /// See [`scripted_fixture_read_only_with_args_single_archive()`] for important details on what `single_archive` means. pub fn scripted_fixture_writable_with_args_single_archive( script_name: impl AsRef, args: impl IntoIterator>, mode: Creation, ) -> Result { scripted_fixture_writable_with_args_inner::) -> PostResult, ()>( script_name, args, mode, ArgsInHash::No, default_excludes(), None::<(u32, _)>, ) .map(|(dir, _)| dir) } fn scripted_fixture_writable_with_args_inner( script_name: impl AsRef, args: impl IntoIterator>, mode: Creation, args_in_hash: ArgsInHash, excludes: &dyn IsExcluded, mut post_process: Option<(u32, F)>, ) -> Result<(tempfile::TempDir, Option)> where F: FnMut(FixtureState<'_>) -> PostResult, { let dst = tempfile::TempDir::new()?; Ok(match mode { Creation::CopyFromReadOnly => { // Create the read-only fixture with post_process (modifications are cached) let (ro_dir, _res_ignored) = scripted_fixture_read_only_with_args_inner( script_name, args, None, args_in_hash, excludes, post_process.as_mut().map(|(v, f)| (*v, f)), false, )?; copy_recursively_into_existing_dir(ro_dir, dst.path())?; (dst, _res_ignored) } Creation::Execute => { // Execute directly in the temp dir with post_process let (_, post_result) = scripted_fixture_read_only_with_args_inner( script_name, args, dst.path().into(), args_in_hash, excludes, post_process.as_mut().map(|(v, f)| (*v, f)), false, )?; (dst, post_result) } }) } /// A utility to copy the entire contents of `src_dir` into `dst_dir`. pub fn copy_recursively_into_existing_dir(src_dir: impl AsRef, dst_dir: impl AsRef) -> std::io::Result<()> { fs_extra::copy_items( &std::fs::read_dir(src_dir)? .map(|e| e.map(|e| e.path())) .collect::, _>>()?, dst_dir, &fs_extra::dir::CopyOptions { overwrite: false, skip_exist: false, copy_inside: false, content_only: false, ..Default::default() }, ) .map_err(std::io::Error::other)?; Ok(()) } /// Like [`scripted_fixture_read_only()`], but passes `args` to `script_name`. pub fn scripted_fixture_read_only_with_args( script_name: impl AsRef, args: impl IntoIterator>, ) -> Result { scripted_fixture_read_only_with_args_inner::) -> PostResult, ()>( script_name, args, None, ArgsInHash::Yes, default_excludes(), None::<(u32, _)>, false, ) .map(|(dir, _)| dir) } /// Like `scripted_fixture_read_only()`], but passes `args` to `script_name`. /// /// Also, don't add a suffix to the archive name as `args` are platform dependent, none-deterministic, /// or otherwise don't influence the content of the archive. /// Note that this also means that `args` won't be used to control the hash of the archive itself. /// /// Sometimes, this should be combined with adding the archive name to `.gitignore` to prevent its creation /// in the first place. /// /// Note that suffixing archives by default helps to learn what calls are made, and forces the author to /// think about what should be done to get it right. pub fn scripted_fixture_read_only_with_args_single_archive( script_name: impl AsRef, args: impl IntoIterator>, ) -> Result { scripted_fixture_read_only_with_args_inner::) -> PostResult, ()>( script_name, args, None, ArgsInHash::No, default_excludes(), None::<(u32, _)>, false, ) .map(|(dir, _)| dir) } /// Like [`scripted_fixture_read_only`], but runs a Rust closure after the script completes. /// /// - `version` should be incremented when the closure's behavior changes to invalidate the cache. /// - The closure receives a [`FixtureState`] enum indicating whether the fixture is newly created /// or was loaded from cache. /// - For uninitialized fixtures, the closure can modify the directory and compute values. /// - For fresh fixtures, the closure should only compute values without modifications. /// - The closure always runs, ensuring the returned value is always available. pub fn scripted_fixture_read_only_with_post( script_name: impl AsRef, version: u32, post_process: impl FnMut(FixtureState<'_>) -> PostResult, ) -> Result<(PathBuf, T)> { scripted_fixture_read_only_with_args_inner( script_name, None::, None, ArgsInHash::Yes, default_excludes(), Some((version, post_process)), false, ) .map(|(path, opt)| (path, opt.expect("post_process was provided"))) } /// Like [`scripted_fixture_read_only_with_args`], but runs a Rust closure after the script completes. /// /// See [`scripted_fixture_read_only_with_post`] for details on the closure behavior. pub fn scripted_fixture_read_only_with_args_with_post( script_name: impl AsRef, args: impl IntoIterator>, version: u32, post_process: impl FnMut(FixtureState<'_>) -> PostResult, ) -> Result<(PathBuf, T)> { scripted_fixture_read_only_with_args_inner( script_name, args, None, ArgsInHash::Yes, default_excludes(), Some((version, post_process)), false, ) .map(|(path, opt)| (path, opt.expect("post_process was provided"))) } /// Like [`scripted_fixture_read_only_with_args_single_archive`], but runs a Rust closure after the script completes. /// /// See [`scripted_fixture_read_only_with_post`] for details on the closure behavior. pub fn scripted_fixture_read_only_with_args_single_archive_with_post( script_name: impl AsRef, args: impl IntoIterator>, version: u32, post_process: impl FnMut(FixtureState<'_>) -> PostResult, ) -> Result<(PathBuf, T)> { scripted_fixture_read_only_with_args_inner( script_name, args, None, ArgsInHash::No, default_excludes(), Some((version, post_process)), false, ) .map(|(path, opt)| (path, opt.expect("post_process was provided"))) } /// Like [`scripted_fixture_writable`], but runs a Rust closure after the script completes. /// /// - `version` should be incremented when the closure's behavior changes to invalidate the cache. /// - The closure receives a [`FixtureState`] enum indicating whether the fixture is newly created /// (`Fresh`) or was loaded from cache (`Cached`). Both variants carry the fixture directory path. /// - For `Fresh` fixtures, the closure can modify the directory and compute values. /// - For `Cached` fixtures, the closure should only compute values without modifications. /// - The closure always runs, ensuring the returned value is always available. pub fn scripted_fixture_writable_with_post( script_name: impl AsRef, version: u32, post_process: impl FnMut(FixtureState<'_>) -> PostResult, ) -> Result<(tempfile::TempDir, T)> { scripted_fixture_writable_with_args_inner( script_name, None::, Creation::CopyFromReadOnly, ArgsInHash::Yes, default_excludes(), Some((version, post_process)), ) .map(|(tmp, opt)| (tmp, opt.expect("post_process was provided"))) } /// Like [`scripted_fixture_writable_with_args`], but runs a Rust closure after the script completes. /// /// See [`scripted_fixture_writable_with_post`] for details on the closure behavior. pub fn scripted_fixture_writable_with_args_with_post( script_name: impl AsRef, args: impl IntoIterator>, mode: Creation, version: u32, post_process: impl FnMut(FixtureState<'_>) -> PostResult, ) -> Result<(tempfile::TempDir, T)> { scripted_fixture_writable_with_args_inner( script_name, args, mode, ArgsInHash::Yes, default_excludes(), Some((version, post_process)), ) .map(|(tmp, opt)| (tmp, opt.expect("post_process was provided"))) } /// Like [`scripted_fixture_writable_with_args_single_archive`], but runs a Rust closure after the script completes. /// /// See [`scripted_fixture_writable_with_post`] for details on the closure behavior. pub fn scripted_fixture_writable_with_args_single_archive_with_post( script_name: impl AsRef, args: impl IntoIterator>, mode: Creation, version: u32, post_process: impl FnMut(FixtureState<'_>) -> PostResult, ) -> Result<(tempfile::TempDir, T)> { scripted_fixture_writable_with_args_inner( script_name, args, mode, ArgsInHash::No, default_excludes(), Some((version, post_process)), ) .map(|(tmp, opt)| (tmp, opt.expect("post_process was provided"))) } /// Execute a Rust closure in a directory, returning a read-only fixture path. /// /// - `version` should be incremented when the closure's behavior changes to invalidate the cache. /// - `name` is used to identify this fixture for caching purposes and should be unique within the crate. /// - `make_fixture(fixture_state)` is the closure that creates the fixture, with the `fixture_state`, /// indicating whether or not the fixture should be written to. /// /// This is an alternative to script-based fixtures that allows creating fixtures in pure Rust, /// while still benefiting from the caching system. /// /// ### Archive Creation /// /// Just like script-based fixtures, the result is cached and compressed archives can be created. /// Increment the `version` number whenever the closure's behavior changes to force recreation. /// /// #### Disable Archive Creation /// /// Archives can be disabled by using `.gitignore` specifications, /// for example `generated-archives/rust-*.tar` or `generated-archives/rust-*.tar.xz` /// in the `tests/fixtures` directory. /// /// ### Example /// /// ```no_run /// use gix_testtools::{Result, FixtureState}; /// /// #[test] /// fn test_with_rust_fixture() -> Result { /// let (dir, _) = gix_testtools::rust_fixture_read_only("my_fixture", 1, |state| { /// if let FixtureState::Uninitialized(path) = state { /// std::fs::write(path.join("file.txt"), "content")?; /// } /// Ok(()) /// })?; /// assert!(dir.join("file.txt").exists()); /// Ok(()) /// } /// ``` pub fn rust_fixture_read_only(name: &str, version: u32, make_fixture: F) -> Result<(PathBuf, T)> where F: FnOnce(FixtureState<'_>) -> PostResult, { rust_fixture_read_only_inner(name, version, None, make_fixture, None, default_excludes()) } /// Execute a Rust closure in a directory, returning a writable temporary directory. /// /// The closure is used to create a fixture in the given directory. /// The resulting directory is writable and will be automatically cleaned up when the returned /// [`tempfile::TempDir`] is dropped. /// It may be called multiple times, and the returned `T` will be primed on the final, writable location. /// /// `version` should be incremented when the closure's behavior changes to invalidate the cache. /// `name` is used to identify this fixture for caching purposes and should be unique within the crate. /// /// ### Example /// /// ```no_run /// use gix_testtools::{Result, Creation, FixtureState}; /// /// #[test] /// fn test_with_writable_rust_fixture() -> Result { /// let (dir, ()) = gix_testtools::rust_fixture_writable("my_fixture", 1, Creation::CopyFromReadOnly, |state| { /// if let FixtureState::Uninitialized(path) = state { /// std::fs::write(path.join("file.txt"), "content")?; /// } /// Ok(()) /// })?; /// // Can modify files in dir /// std::fs::write(dir.path().join("new_file.txt"), "new content")?; /// Ok(()) /// } /// ``` pub fn rust_fixture_writable( name: &str, version: u32, mode: Creation, make_fixture: F, ) -> Result<(tempfile::TempDir, T)> where F: FnMut(FixtureState<'_>) -> PostResult, { rust_fixture_writable_inner(name, version, None, make_fixture, mode, default_excludes()) } fn rust_fixture_writable_inner( name: &str, version: u32, object_hash: Option, mut make_fixture: F, mode: Creation, excludes: &dyn IsExcluded, ) -> Result<(tempfile::TempDir, T)> where F: FnMut(FixtureState<'_>) -> PostResult, { let dst = tempfile::TempDir::new()?; let res = match mode { Creation::CopyFromReadOnly => { let (ro_dir, _res_ignored) = rust_fixture_read_only_inner(name, version, object_hash, &mut make_fixture, None, excludes)?; copy_recursively_into_existing_dir(ro_dir, dst.path())?; make_fixture(FixtureState::Fresh(dst.path()))? } Creation::Execute => { let (_, res) = rust_fixture_read_only_inner(name, version, object_hash, make_fixture, Some(dst.path()), excludes)?; res } }; Ok((dst, res)) } fn rust_fixture_read_only_inner( name: &str, version: u32, object_hash: Option, make_fixture: F, destination_dir: Option<&Path>, excludes: &dyn IsExcluded, ) -> Result<(PathBuf, T)> where F: FnOnce(FixtureState<'_>) -> PostResult, { // Assure tempfiles get removed when aborting the test. gix_tempfile::signal::setup( gix_tempfile::signal::handler::Mode::DeleteTempfilesOnTerminationAndRestoreDefaultBehaviour, ); // For Rust fixtures, the identity is simply the provided version number. // Users must increment this manually when the closure behavior changes. let script_identity = version; let archive_name = format!("rust-{name}"); let fixture_base = fixture_base(); let archive_file_path = fixture_base .join(ARCHIVE_DIR_NAME) .join(format!("{archive_name}.{}", tar_extension())); let (force_run, script_result_directory) = force_and_dir( destination_dir, &fixture_base, &archive_name, object_hash, &script_identity, None, ); let _marker = marker_if_needed(destination_dir, archive_name)?; run_fixture_generator_with_marker_handling( &archive_file_path, &script_result_directory, script_identity, force_run, false, excludes, &format!("using Rust closure '{name}'"), make_fixture, ) .map(|res| (script_result_directory, res)) } // We may assume that destination_dir is already unique (i.e. temp-dir) if present - thus there is no need for a lock, // and we can execute closures in parallel. Otherwise, we need to acquire a lock to ensure that only one closure is running at a time. fn marker_if_needed( destination_dir: Option<&Path>, archive_name: impl AsRef, ) -> Result> { Ok(destination_dir .is_none() .then(|| { gix_lock::Marker::acquire_to_hold_resource( archive_name, gix_lock::acquire::Fail::AfterDurationWithBackoff(Duration::from_secs(6 * 60)), None, ) }) .transpose()?) } fn force_and_dir( destination_dir: Option<&Path>, fixture_base: &Path, archive_name: impl AsRef, object_hash: Option, script_identity: &dyn std::fmt::Display, cache_variant: Option<&str>, ) -> (bool, PathBuf) { destination_dir.map_or_else( || { let mut dir = fixture_base.join( Path::new("generated-do-not-edit") .join(archive_name) .join(object_hash.unwrap_or_else(self::object_hash).to_string()), ); if let Some(cache_variant) = cache_variant { dir = dir.join(cache_variant); } let dir = dir.join(format!("{}-{}", script_identity, family_name())); (false, dir) }, |d| (true, d.to_owned()), ) } #[expect(clippy::too_many_arguments)] fn run_fixture_generator_with_marker_handling( archive_file_path: &Path, script_result_directory: &Path, script_identity: u32, force_run: bool, needs_archive: bool, excludes: &dyn IsExcluded, description: &str, make_fixture: F, ) -> Result where F: FnOnce(FixtureState<'_>) -> PostResult, { let failure_marker = script_result_directory.join("_invalid_state_due_to_script_failure_"); if force_run || !script_result_directory.is_dir() || failure_marker.is_file() { if failure_marker.is_file() { std::fs::remove_dir_all(script_result_directory).map_err(|err| { format!( "Failed to remove '{script_result_directory}', please try to do that by hand. Original error: {err}", script_result_directory = script_result_directory.display() ) })?; } std::fs::create_dir_all(script_result_directory)?; match extract_archive( archive_file_path, script_result_directory, script_identity, needs_archive, ) { Ok((archive_id, platform)) => { eprintln!( "Extracted fixture from archive '{}' ({}, {:?})", archive_file_path.display(), archive_id, platform ); make_fixture(FixtureState::Fresh(script_result_directory)) } Err(err) => { if err.kind() != std::io::ErrorKind::NotFound { eprintln!("failed to extract '{}': {}", archive_file_path.display(), err); std::fs::remove_dir_all(script_result_directory).map_err(|err| { format!( "Failed to remove '{script_result_directory}', please try to do that by hand. Original error: {err}", script_result_directory = script_result_directory.display() ) })?; std::fs::create_dir_all(script_result_directory)?; } else if !excludes.is_excluded(archive_file_path) { eprintln!( "Archive at '{}' not found, creating fixture {}", archive_file_path.display(), description ); } let res = match make_fixture(FixtureState::Uninitialized(script_result_directory)) { Ok(value) => value, Err(err) => { write_failure_marker(&failure_marker); return Err(err); } }; create_archive_if_we_should(script_result_directory, archive_file_path, script_identity, excludes) .inspect_err(|_err| { write_failure_marker(&failure_marker); })?; Ok(res) } } } else { make_fixture(FixtureState::Fresh(script_result_directory)) } } fn scripted_fixture_read_only_with_args_inner( script_name: impl AsRef, args: impl IntoIterator>, destination_dir: Option<&Path>, args_in_hash: ArgsInHash, excludes: &dyn IsExcluded, post_process: Option<(u32, F)>, needs_archive: bool, ) -> Result<(PathBuf, Option)> where F: FnMut(FixtureState<'_>) -> PostResult, { // Assure tempfiles get removed when aborting the test. gix_tempfile::signal::setup( gix_tempfile::signal::handler::Mode::DeleteTempfilesOnTerminationAndRestoreDefaultBehaviour, ); let object_hash = object_hash(); let script_location = script_name.as_ref(); let fixture_base = fixture_base(); let script_path = fixture_path(script_location); // keep this lock to assure we don't return unfinished directories for threaded callers let args: Vec = args.into_iter().map(Into::into).collect(); let post_version = post_process.as_ref().map(|(v, _)| *v); let script_identity = { let mut map = SCRIPT_IDENTITY.lock(); let init = if object_hash == gix_hash::Kind::Sha1 { script_path.clone() } else { script_path.clone().join(object_hash.to_string()) }; let key = args.iter().fold(init, |p, a| p.join(a)); // Include post_version in the key if present let key = if let Some(v) = post_version { key.join(format!("post-v{v}")) } else { key }; map.entry(key) .or_insert_with(|| { let crc_value = crc::Crc::::new(&crc::CRC_32_CKSUM); let mut crc_digest = crc_value.digest(); crc_digest.update(&std::fs::read(&script_path).unwrap_or_else(|err| { panic!( "file {script_path} in CWD '{cwd}' could not be read: {err}", cwd = env::current_dir().expect("valid cwd").display(), script_path = script_path.display(), ) })); for arg in &args { crc_digest.update(arg.as_bytes()); } // Hash the post_process version if present if let Some(v) = post_version { crc_digest.update(&v.to_le_bytes()); } crc_digest.finalize() }) .to_owned() }; let script_basename = script_location.file_stem().unwrap_or(script_location.as_os_str()); let archive_file_path = fixture_base.join(ARCHIVE_DIR_NAME).join({ let suffix = match args_in_hash { ArgsInHash::Yes => { let mut suffix = args.join("_"); if !suffix.is_empty() { suffix.insert(0, '_'); } suffix.replace(['\\', '/', ' ', '.'], "_") } ArgsInHash::No => "".into(), }; let potential_hash_suffix = if object_hash == gix_hash::Kind::Sha1 { "".into() } else { format!("_{object_hash}") }; format!( "{}{suffix}{potential_hash_suffix}.{}", script_basename.to_str().expect("valid UTF-8"), tar_extension() ) }); let (force_run, script_result_directory) = force_and_dir( destination_dir, &fixture_base, script_basename, Some(object_hash), &script_identity, needs_archive.then_some("archive"), ); let _marker = marker_if_needed(destination_dir, script_basename)?; let script_identity_for_archive = match args_in_hash { ArgsInHash::Yes => script_identity, ArgsInHash::No => 0, }; let script_absolute_path = env::current_dir()?.join(&script_path); let post_process_closure = post_process.map(|(_, f)| f); let res = run_fixture_generator_with_marker_handling( &archive_file_path, &script_result_directory, script_identity_for_archive, force_run, needs_archive, excludes, &format!("using script '{}'", script_location.display()), |fixture_state| { if let FixtureState::Uninitialized(dir) = fixture_state { let mut cmd = std::process::Command::new(&script_absolute_path); let output = match configure_command(&mut cmd, object_hash, &args, dir).output() { Ok(out) => out, Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied || err.raw_os_error() == Some(193) /* windows */ => { cmd = std::process::Command::new(bash_program()); configure_command(cmd.arg(&script_absolute_path), object_hash, &args, dir).output()? } Err(err) => return Err(err.into()), }; if !output.status.success() { eprintln!("stdout: {}", output.stdout.as_bstr()); eprintln!("stderr: {}", output.stderr.as_bstr()); return Err(format!("fixture script of {cmd:?} failed").into()); } } if let Some(mut f) = post_process_closure { f(fixture_state).map(Some) } else { Ok(None) } }, )?; Ok((script_result_directory, res)) } /// Returns the hash function that is used when creating or loading test fixtures. /// /// The value returned is derived from the environment variable `GIX_TEST_FIXTURE_HASH`. /// Use this, e. g., when you need to run different assertions depending on the hash /// function used in a specific fixture. /// /// Returns `None` if the environment variable isn't set. /// /// # Panics /// /// If the value set in `GIX_TEST_FIXTURE_HASH` is not valid. pub fn object_hash_from_env() -> Option { static FIXTURE_HASH: LazyLock> = LazyLock::new(|| { env::var_os("GIX_TEST_FIXTURE_HASH").and_then(|value| value.into_string().ok()).map(|object_kind| { gix_hash::Kind::from_str(&object_kind).unwrap_or_else(|_| { panic!( "GIX_TEST_FIXTURE_HASH was set to {object_kind} which is an invalid value. Valid values are {}. Exiting.", gix_hash::Kind::all().iter().map(std::string::ToString::to_string).collect::>().join(", ") ) }) }) }); *FIXTURE_HASH } /// Like [`object_hash_from_env()`], but returns the default hash if `GIX_TEST_FIXTURE_HASH` is not set. pub fn object_hash() -> gix_hash::Kind { object_hash_from_env().unwrap_or_default() } /// Run `git` in `current_dir` with shell-like whitespace-separated `arguments`, returning stdout as UTF-8. /// /// Note that Git is run as isolated as possible, just like scripts. /// /// Arguments may be split across multiple lines. Single and double quotes can be used to keep whitespace /// within an argument, for example `commit -m 'a message with spaces'`. pub fn git(current_dir: impl AsRef, arguments: &str) -> Result { let args = split_git_arguments(arguments)?; let cwd = current_dir.as_ref(); let mut cmd = std::process::Command::new(GIT_PROGRAM); let output = configure_command(&mut cmd, object_hash(), args.iter().map(String::as_str), cwd) .current_dir(cwd) .output()?; if !output.status.success() { return Err(format!( "{cmd:?} failed with status {}\nstdout: {}\nstderr: {}", output.status, output.stdout.as_bstr(), output.stderr.as_bstr() ) .into()); } Ok(String::from_utf8(output.stdout)?) } fn split_git_arguments(input: &str) -> Result> { let mut args = Vec::new(); let mut arg = String::new(); let mut quote = None; let mut has_arg = false; let mut chars = input.chars(); while let Some(ch) = chars.next() { match quote { Some('\'') => { if ch == '\'' { quote = None; } else { arg.push(ch); } } Some('"') => { if ch == '"' { quote = None; } else if ch == '\\' { if let Some(next) = chars.next() { arg.push(next); } } else { arg.push(ch); } } Some(_) => unreachable!("only single and double quotes are set"), None => { if ch.is_whitespace() { if has_arg { args.push(std::mem::take(&mut arg)); has_arg = false; } } else if matches!(ch, '\'' | '"') { quote = Some(ch); has_arg = true; } else if ch == '\\' { if let Some(next) = chars.next() { arg.push(next); } has_arg = true; } else { arg.push(ch); has_arg = true; } } } } if let Some(quote) = quote { return Err(format!("unterminated {quote:?} quote in git arguments").into()); } if has_arg { args.push(arg); } Ok(args) } /// Normalize debug-formatted `value` so one snapshot can be reused for SHA-1 and SHA-256 fixtures. /// /// The helper rewrites 40- and 64-character hexadecimal object IDs to stable `Oid()` /// placeholders in first-seen order while leaving the surrounding pretty-debug formatting untouched. /// Debug wrappers like `Sha1()` and `Sha256()` are collapsed to the same placeholder. /// It also returns the replaced object IDs in first-seen order, so `Oid(n)` can be looked up as /// `result.1[n - 1]`. pub fn normalize_debug_snapshot(value: &dyn std::fmt::Debug) -> (String, Vec) { normalize_hashes(&format!("{value:#?}")) } /// Normalize 40- and 64-character hexadecimal object IDs in `input`. /// /// This is like [`normalize_debug_snapshot()`], but operates on already-formatted text. pub fn normalize_hashes(input: &str) -> (String, Vec) { let mut out = String::with_capacity(input.len()); let mut seen = HashMap::::new(); let mut removed = Vec::::new(); let mut chars = input.chars().peekable(); let mut hex = String::new(); while let Some(ch) = chars.next() { if ch.is_ascii_hexdigit() { hex.clear(); hex.push(ch); while let Some(ch) = chars.next_if(char::is_ascii_hexdigit) { hex.push(ch); } if let Some(oid) = raw_object_id(&hex) { strip_debug_hash_wrapper(&mut out, &mut chars); push_normalized_oid(oid, &mut seen, &mut removed, &mut out); } else { out.push_str(&hex); } } else { out.push(ch); } } (out, removed) } fn raw_object_id(input: &str) -> Option { if !matches!(input.len(), 40 | 64) { return None; } gix_hash::ObjectId::from_hex(input.as_bytes()).ok() } fn strip_debug_hash_wrapper(out: &mut String, chars: &mut std::iter::Peekable>) { if !matches!(chars.peek(), Some(')')) { return; } // The `Sha1(` / `Sha256(` prefix was already copied before the hex run was // recognized as an object ID. Remove it, then consume the matching `)`. let consume_closing_parenthesis = if out.ends_with("Sha1(") { out.truncate(out.len() - "Sha1(".len()); true } else if out.ends_with("Sha256(") { out.truncate(out.len() - "Sha256(".len()); true } else { false }; if consume_closing_parenthesis { chars.next(); } } fn push_normalized_oid( oid: gix_hash::ObjectId, seen: &mut HashMap, removed: &mut Vec, out: &mut String, ) { let normalized = *seen.entry(oid).or_insert_with(|| { let current = removed.len(); removed.push(oid); current }); out.push_str("Oid("); out.push_str(&(normalized + 1).to_string()); out.push(')'); } #[cfg(windows)] const NULL_DEVICE: &str = "nul"; // See `gix_path::env::git::NULL_DEVICE` on why this form is used. #[cfg(not(windows))] const NULL_DEVICE: &str = "/dev/null"; fn configure_command<'a, I: IntoIterator, S: AsRef>( cmd: &'a mut std::process::Command, object_hash: gix_hash::Kind, args: I, script_result_directory: &Path, ) -> &'a mut std::process::Command { // For simplicity, we extend the `MSYS` variable from our own environment. This disregards // state from any prior `cmd.env("MSYS")` or `cmd.env_remove("MSYS")` calls. Such calls should // either be avoided, or made after this function returns (but before spawning the command). let mut msys_for_git_bash_on_windows = env::var_os("MSYS").unwrap_or_default(); msys_for_git_bash_on_windows.push(" winsymlinks:nativestrict"); cmd.args(args) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .current_dir(script_result_directory) .env_remove("GIT_DIR") .env_remove("GIT_INDEX_FILE") .env_remove("GIT_OBJECT_DIRECTORY") .env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES") .env_remove("GIT_WORK_TREE") .env_remove("GIT_COMMON_DIR") .env_remove("GIT_ASKPASS") .env_remove("SSH_ASKPASS") .env("MSYS", msys_for_git_bash_on_windows) .env( "XDG_CONFIG_HOME", script_result_directory.join(".gix-testtools-xdg-config"), ) .env("GIT_CONFIG_NOSYSTEM", "1") .env("GIT_CONFIG_GLOBAL", NULL_DEVICE) .env("GIT_TERMINAL_PROMPT", "false") .env("GIT_AUTHOR_DATE", "2000-01-01 00:00:00 +0000") .env("GIT_AUTHOR_EMAIL", "author@example.com") .env("GIT_AUTHOR_NAME", "author") .env("GIT_COMMITTER_DATE", "2000-01-02 00:00:00 +0000") .env("GIT_COMMITTER_EMAIL", "committer@example.com") .env("GIT_COMMITTER_NAME", "committer") .env("GIT_DEFAULT_HASH", object_hash.to_string()); apply_git_config_by_environment(cmd, ISOLATED_GIT_CONFIG) } /// Apply command-scoped Git `config` to `cmd`, and return it. /// /// This sets `GIT_CONFIG_COUNT` and matching `GIT_CONFIG_KEY_` / /// `GIT_CONFIG_VALUE_` environment variables, which Git treats like /// command-line `-c =` entries for the spawned process. Existing /// values for these variables on `cmd` are overwritten for the configured /// indices. pub fn apply_git_config_by_environment<'a>( cmd: &'a mut std::process::Command, config: &[(&str, &str)], ) -> &'a mut std::process::Command { cmd.env("GIT_CONFIG_COUNT", config.len().to_string()); for (idx, (key, value)) in config.iter().enumerate() { cmd.env(format!("GIT_CONFIG_KEY_{idx}"), key); cmd.env(format!("GIT_CONFIG_VALUE_{idx}"), value); } cmd } /// Get the path attempted as a `bash` interpreter, for fixture scripts having no `#!` we can use. /// /// This is rarely called on Unix-like systems, provided that fixture scripts have usable shebang /// (`#!`) lines and are marked executable. However, Windows does not recognize `#!` when executing /// a file. If all fixture scripts that cannot be directly executed are `bash` scripts or can be /// treated as such, fixture generation still works on Windows, as long as this function manages to /// find or guess a suitable `bash` interpreter. /// /// ### Search order /// /// This function is used internally. It is public to facilitate diagnostic use. The following /// details are subject to change without warning, and changes are treated as non-breaking. /// /// The `bash.exe` found in a path search is not always suitable on Windows. This is mainly because /// `bash.exe` in `System32`, which is associated with WSL, would often be found first. But even /// where that is not the case, the best `bash.exe` to use to run fixture scripts to set up Git /// repositories for testing is usually one associated with Git for Windows, even if some other /// `bash.exe` would be found in a path search. Currently, the search order we use is as follows: /// /// 1. The shim `bash.exe`, which sets environment variables when run and is, on some systems, /// needed to find the POSIX utilities that scripts need (or correct versions of them). /// /// 2. The non-shim `bash.exe`, which is sometimes available even when the shim is not available. /// This is mainly because the Git for Windows SDK does not come with a `bash.exe` shim. /// /// 3. As a fallback, the simple name `bash.exe`, which triggers a path search when run. /// /// On non-Windows systems, the simple name `bash` is used, which triggers a path search when run. pub fn bash_program() -> &'static Path { // TODO(deps): Unify with `gix_path::env::shell()` by having both call a more general function // in `gix-path`. See https://github.com/GitoxideLabs/gitoxide/issues/1886. static GIT_BASH: LazyLock = LazyLock::new(|| { if cfg!(windows) { GIT_CORE_DIR .ancestors() .nth(3) .map(OsStr::new) .iter() .flat_map(|prefix| { // Go down to places `bash.exe` usually is. Keep using `/` separators, not `\`. ["/bin/bash.exe", "/usr/bin/bash.exe"].into_iter().map(|suffix| { let mut raw_path = (*prefix).to_owned(); raw_path.push(suffix); raw_path }) }) .map(PathBuf::from) .find(|bash| bash.is_file()) .unwrap_or_else(|| "bash.exe".into()) } else { "bash".into() } }); GIT_BASH.as_ref() } fn write_failure_marker(failure_marker: &Path) { std::fs::write(failure_marker, []).ok(); } fn should_skip_all_archive_creation() -> bool { // On Windows, we fail to remove the meta_dir and can't do anything about it, which means tests will see more // in the directory than they should which makes them fail. It's probably a bad idea to generate archives on Windows // anyway. Either Unix is portable OR no archive is created anywhere. This also means that Windows users can't create // archives, but that's not a deal-breaker. cfg!(windows) || (is_ci::cached() && env::var_os("GIX_TEST_CREATE_ARCHIVES_EVEN_ON_CI").is_none()) } fn is_lfs_pointer_file(path: &Path) -> bool { const PREFIX: &[u8] = b"version https://git-lfs"; let mut buf = [0_u8; PREFIX.len()]; std::fs::OpenOptions::new() .read(true) .open(path) .is_ok_and(|mut f| f.read_exact(&mut buf).is_ok_and(|_| buf.starts_with(PREFIX))) } /// The `script_identity` will be baked into the soon to be created `archive` as it identifies the script /// that created the contents of `source_dir`. fn create_archive_if_we_should( source_dir: &Path, archive: &Path, script_identity: u32, excludes: &dyn IsExcluded, ) -> std::io::Result<()> { if should_skip_all_archive_creation() || excludes.is_excluded(archive) { return Ok(()); } if is_lfs_pointer_file(archive) { eprintln!( "Refusing to overwrite `gix-lfs` pointer file at \"{}\" - git lfs might not be properly installed.", archive.display() ); return Ok(()); } std::fs::create_dir_all(archive.parent().expect("archive is a file"))?; let meta_dir = populate_meta_dir(source_dir, script_identity)?; let res = (move || { let mut buf = Vec::::new(); { let mut ar = tar::Builder::new(&mut buf); ar.mode(tar::HeaderMode::Deterministic); ar.follow_symlinks(false); ar.append_dir_all(".", source_dir)?; ar.finish()?; } #[cfg_attr(feature = "xz", allow(unused_mut))] let mut archive = std::fs::OpenOptions::new() .write(true) .create(true) .truncate(true) .open(archive)?; #[cfg(feature = "xz")] { let mut xz_write = xz2::write::XzEncoder::new(archive, 3); std::io::copy(&mut &*buf, &mut xz_write)?; xz_write.finish()?.close() } #[cfg(not(feature = "xz"))] { use std::io::Write; archive.write_all(&buf)?; archive.close() } })(); #[cfg(not(windows))] std::fs::remove_dir_all(meta_dir)?; #[cfg(windows)] std::fs::remove_dir_all(meta_dir).ok(); // it really can't delete these directories for some reason (even after 10 seconds) res } const META_DIR_NAME: &str = "__gitoxide_meta__"; const META_IDENTITY: &str = "identity"; const META_GIT_VERSION: &str = "git-version"; fn populate_meta_dir(destination_dir: &Path, script_identity: u32) -> std::io::Result { let meta_dir = destination_dir.join(META_DIR_NAME); std::fs::create_dir_all(&meta_dir)?; std::fs::write( meta_dir.join(META_IDENTITY), format!("{}-{}", script_identity, family_name()).as_bytes(), )?; std::fs::write( meta_dir.join(META_GIT_VERSION), std::process::Command::new(GIT_PROGRAM) .arg("--version") .output()? .stdout, )?; Ok(meta_dir) } /// `required_script_identity` is the identity of the script that generated the state that is contained in `archive`. /// If this is not the case, the arvhive will be ignored. fn extract_archive( archive: &Path, destination_dir: &Path, required_script_identity: u32, needs_archive: bool, ) -> std::io::Result<(u32, Option)> { let archive_buf: Vec = { let mut buf = Vec::new(); #[cfg_attr(feature = "xz", allow(unused_mut))] let mut input_archive = std::fs::File::open(archive)?; if !needs_archive && env::var_os("GIX_TEST_IGNORE_ARCHIVES").is_some() { return Err(std::io::Error::other(format!( "Ignoring archive at '{}' as GIX_TEST_IGNORE_ARCHIVES is set.", archive.display() ))); } #[cfg(feature = "xz")] { let mut decoder = xz2::bufread::XzDecoder::new(std::io::BufReader::new(input_archive)); std::io::copy(&mut decoder, &mut buf)?; } #[cfg(not(feature = "xz"))] { input_archive.read_to_end(&mut buf)?; } buf }; let mut entry_buf = Vec::::new(); let (archive_identity, platform): (u32, _) = tar::Archive::new(std::io::Cursor::new(&mut &*archive_buf)) .entries_with_seek()? .filter_map(std::result::Result::ok) .find_map(|mut e: tar::Entry<'_, _>| { let path = e.path().ok()?; if path.parent()?.file_name()? == META_DIR_NAME && path.file_name()? == META_IDENTITY { entry_buf.clear(); e.read_to_end(&mut entry_buf).ok()?; let mut tokens = entry_buf.to_str().ok()?.trim().splitn(2, '-'); match (tokens.next(), tokens.next()) { (Some(id), platform) => Some((id.parse().ok()?, platform.map(ToOwned::to_owned))), _ => None, } } else { None } }) .ok_or_else(|| std::io::Error::other("BUG: Could not find meta directory in our own archive")) .map_err(|err| { std::io::Error::other(format!( "Could not extract archive at '{archive}': {err}", archive = archive.display() )) })?; if archive_identity != required_script_identity { eprintln!( "Ignoring archive at '{}' as its generating script changed", archive.display() ); return Err(std::io::ErrorKind::NotFound.into()); } for entry in tar::Archive::new(&mut &*archive_buf).entries()? { let mut entry = entry?; let path = entry.path()?; if path.to_str() == Some(META_DIR_NAME) || path.parent().and_then(Path::to_str) == Some(META_DIR_NAME) { continue; } entry.unpack_in(destination_dir)?; } Ok((archive_identity, platform)) } fn family_name() -> &'static str { if cfg!(windows) { "windows" } else { "unix" } } /// A utility to set and unset environment variables, while restoring or removing them on drop. #[derive(Default)] pub struct Env<'a> { altered_vars: Vec<(&'a str, Option)>, } fn set_var(var: &str, value: impl AsRef) { // SAFETY: Tests using this helper are responsible for serializing access to // process-wide environment variables they mutate. unsafe { env::set_var(var, value) }; } fn remove_var(var: &str) { // SAFETY: Tests using this helper are responsible for serializing access to // process-wide environment variables they mutate. unsafe { env::remove_var(var) }; } impl<'a> Env<'a> { /// Create a new instance. pub fn new() -> Self { Env { altered_vars: Vec::new(), } } /// Set `var` to `value`. pub fn set(mut self, var: &'a str, value: impl Into) -> Self { let prev = env::var_os(var); set_var(var, value.into()); self.altered_vars.push((var, prev)); self } /// Unset `var`. pub fn unset(mut self, var: &'a str) -> Self { let prev = env::var_os(var); remove_var(var); self.altered_vars.push((var, prev)); self } } impl Drop for Env<'_> { fn drop(&mut self) { for (var, prev_value) in self.altered_vars.iter().rev() { match prev_value { Some(value) => set_var(var, value), None => remove_var(var), } } } } /// Check data structure size, comparing strictly on 64-bit targets. /// /// - On 32-bit targets, checks if `actual_size` is at most `expected_64_bit_size`. /// - On 64-bit targets, checks if `actual_size` is exactly `expected_64_bit_size`. /// /// This is for assertions about the size of data structures, when the goal is to keep them from /// growing too large even across breaking changes. Such assertions must always fail when data /// structures grow larger than they have ever been, for which `<=` is enough. But it also helps to /// know when they have shrunk unexpectedly. They may shrink, other changes may rely on the smaller /// size for acceptable performance, and then they may grow again to their earlier size. /// /// The problem with `==` is that data structures are often smaller on 32-bit targets. This could /// be addressed by asserting separate exact 64-bit and 32-bit sizes. But sizes may also differ /// across 32-bit targets, due to ABI and layout/packing details. That can happen across 64-bit /// targets too, but it seems less common. /// /// For those reasons, this function does a `==` on 64-bit targets, but a `<=` on 32-bit targets. pub fn size_ok(actual_size: usize, expected_64_bit_size: usize) -> bool { #[cfg(target_pointer_width = "64")] return actual_size == expected_64_bit_size; #[cfg(target_pointer_width = "32")] return actual_size <= expected_64_bit_size; } /// Get the umask in a way that is safe, but may be too slow for use outside of tests. #[cfg(unix)] pub fn umask() -> u32 { let output = std::process::Command::new("/bin/sh") .args(["-c", "umask"]) .output() .expect("can execute `sh -c umask`"); assert!(output.status.success(), "`sh -c umask` failed"); assert_eq!(output.stderr.as_bstr(), "", "`sh -c umask` unexpected message"); let text = output.stdout.to_str().expect("valid Unicode").trim(); u32::from_str_radix(text, 8).expect("parses as octal number") } fn tar_extension() -> &'static str { if cfg!(feature = "xz") { "tar.xz" } else { "tar" } } #[cfg(test)] mod tests; gix-testtools-0.19.0/src/main.rs000064400000000000000000000042051046102023000146220ustar 00000000000000use std::{fs, io, io::prelude::*, path::PathBuf}; fn bash_program() -> io::Result<()> { use std::io::IsTerminal; if !std::io::stdout().is_terminal() { eprintln!("warning: `bash-program` subcommand not meant for scripting, format may change"); } println!("{}", gix_testtools::bash_program().display()); Ok(()) } fn mess_in_the_middle(path: PathBuf) -> io::Result<()> { let mut file = fs::OpenOptions::new().read(false).write(true).open(path)?; file.seek(io::SeekFrom::Start(file.metadata()?.len() / 2))?; file.write_all(b"hello")?; Ok(()) } #[cfg(unix)] fn umask() -> io::Result<()> { println!("{:04o}", gix_testtools::umask()); Ok(()) } /// Run a Git protocol test daemon on an OS-assigned loopback port. /// This function blocks and the process needs to be killed. /// /// Journey tests use this instead of `git daemon --port=` because Git /// treats `--port=0` as "use the default port", so it can't bind an /// ephemeral port and report it back. This wrapper owns the listening socket, /// writes the resulting `git://127.0.0.1:/` URL to `url_file`, and then /// hands every accepted connection to `git daemon --inetd`. #[cfg(unix)] fn git_daemon(url_file: PathBuf) -> io::Result<()> { let daemon = gix_testtools::spawn_git_daemon(".")?; fs::write(url_file, format!("{}/\n", daemon.url))?; loop { std::thread::park(); } } #[cfg(not(unix))] fn git_daemon(_url_file: PathBuf) -> io::Result<()> { Err(io::Error::new( io::ErrorKind::Unsupported, "`jtt git-daemon` is only supported on Unix", )) } fn main() -> Result<(), Box> { let mut args = std::env::args().skip(1); let scmd = args.next().expect("sub command"); match &*scmd { "bash-program" | "bp" => bash_program()?, "git-daemon" => git_daemon(PathBuf::from(args.next().expect("path to write the git:// URL to")))?, "mess-in-the-middle" => mess_in_the_middle(PathBuf::from(args.next().expect("path to file to mess with")))?, #[cfg(unix)] "umask" => umask()?, _ => unreachable!("Unknown subcommand: {}", scmd), } Ok(()) } gix-testtools-0.19.0/src/tests.rs000064400000000000000000000271231046102023000150440ustar 00000000000000use super::*; #[test] fn parse_version() { assert_eq!(git_version_from_bytes(b"git version 2.37.2").unwrap(), (2, 37, 2)); assert_eq!( git_version_from_bytes(b"git version 2.32.1 (Apple Git-133)").unwrap(), (2, 32, 1) ); } #[test] fn parse_version_with_trailing_newline() { assert_eq!(git_version_from_bytes(b"git version 2.37.2\n").unwrap(), (2, 37, 2)); } const SCOPE_ENV_VALUE: &str = "gitconfig"; fn populate_ad_hoc_config_files(dir: &Path) { const CONFIG_DATA: &[u8] = b"[foo]\n\tbar = baz\n"; let paths: &[PathBuf] = if cfg!(windows) { let unc_literal_nul = dir.canonicalize().expect("directory exists").join("nul"); &[dir.join(SCOPE_ENV_VALUE), dir.join("-"), unc_literal_nul] } else { &[dir.join(SCOPE_ENV_VALUE), dir.join("-"), dir.join(":")] }; // Create the files. for path in paths { std::fs::write(path, CONFIG_DATA).expect("can write contents"); } // Verify the files. This is mostly to show we really made a `\\?\...\nul` on Windows. for path in paths { let buf = std::fs::read(path).expect("the file really exists"); assert_eq!(buf, CONFIG_DATA, "{path:?} should be a config file"); } } #[test] fn configure_command_clears_external_config() { let temp = tempfile::TempDir::new().expect("can create temp dir"); populate_ad_hoc_config_files(temp.path()); let mut cmd = std::process::Command::new(GIT_PROGRAM); cmd.env("GIT_CONFIG_SYSTEM", SCOPE_ENV_VALUE); cmd.env("GIT_CONFIG_GLOBAL", SCOPE_ENV_VALUE); configure_command( &mut cmd, gix_hash::Kind::default(), ["config", "-l", "--show-origin"], temp.path(), ); let output = cmd.output().expect("can run git"); let lines: Vec<_> = output .stdout .to_str() .expect("valid UTF-8") .lines() .filter(|line| !line.starts_with("command line:\t")) .collect(); let status = output.status.code().expect("terminated normally"); assert_eq!(lines, Vec::<&str>::new(), "should be no config variables from files"); assert_eq!(status, 0, "reading the config should succeed"); } #[test] fn configure_command_overrides_xdg_config_home() { let temp = tempfile::TempDir::new().expect("can create temp dir"); let mut cmd = std::process::Command::new(GIT_PROGRAM); cmd.env("XDG_CONFIG_HOME", temp.path().join("external-config")); configure_command(&mut cmd, gix_hash::Kind::default(), ["--version"], temp.path()); let xdg_config_home = cmd .get_envs() .find_map(|(key, value)| (key == "XDG_CONFIG_HOME").then_some(value)) .flatten(); assert_eq!( xdg_config_home, Some(temp.path().join(".gix-testtools-xdg-config").as_os_str()) ); } #[test] #[cfg(windows)] fn bash_program_ok_for_platform() { let path = bash_program(); assert!(path.is_absolute()); let for_version = std::process::Command::new(path) .arg("--version") .output() .expect("can pass it `--version`"); assert!(for_version.status.success(), "passing `--version` succeeds"); for_version .stdout .lines() .nth(0) .expect("`--version` output has first line"); let for_uname_os = std::process::Command::new(path) .args(["-c", "uname -o"]) .output() .expect("can tell it to run `uname -o`"); assert!(for_uname_os.status.success(), "telling it to run `uname -o` succeeds"); assert_eq!( for_uname_os.stdout.trim_end(), b"Msys", "it runs commands in an MSYS environment" ); } #[test] #[cfg(not(windows))] fn bash_program_ok_for_platform() { assert_eq!(bash_program(), Path::new("bash")); } #[test] fn bash_program_unix_path() { let path = bash_program() .to_str() .expect("This test depends on the bash path being valid Unicode"); assert!( !path.contains('\\'), "The path to bash should have no backslashes, barring very unusual environments" ); } fn is_rooted_relative(path: impl AsRef) -> bool { let p = path.as_ref(); p.is_relative() && p.has_root() } #[test] #[cfg(windows)] fn unix_style_absolute_is_rooted_relative() { assert!(is_rooted_relative("/bin/bash"), "can detect paths like /bin/bash"); } #[test] fn bash_program_absolute_or_unrooted() { let bash = bash_program(); assert!(!is_rooted_relative(bash), "{bash:?}"); } #[test] fn invoke_bash_runs_in_given_working_directory() { let dir = tempfile::TempDir::new().expect("can create temp dir"); invoke_bash(dir.path(), "printf '%s' hello > out"); assert_eq!( std::fs::read(dir.path().join("out")).expect("script wrote output"), b"hello" ); } #[test] fn invoke_bash_disables_auto_maintenance_for_git_commands() { let dir = tempfile::TempDir::new().expect("can create temp dir"); invoke_bash( dir.path(), "git config --get maintenance.auto > out && git config --get gc.auto >> out", ); assert_eq!( std::fs::read_to_string(dir.path().join("out")).expect("script wrote output"), "false\n0\n", "Git commands run from the shell should not run automatic maintenance" ); } #[test] fn run_git_disables_auto_maintenance() -> Result { let dir = tempfile::TempDir::new().expect("can create temp dir"); let status = run_git(dir.path(), &["config", "--get", "maintenance.auto"])?; assert!(status.success(), "command-scope maintenance.auto should be visible"); let status = run_git(dir.path(), &["config", "--get", "gc.auto"])?; assert!(status.success(), "command-scope gc.auto should be visible"); Ok(()) } #[test] fn git_helper_disables_auto_maintenance() -> Result { let dir = tempfile::TempDir::new().expect("can create temp dir"); assert_eq!( git(dir.path(), "config --get maintenance.auto")?, "false\n", "Git commands run through gix-testtools should not run automatic maintenance" ); assert_eq!( git(dir.path(), "config --get gc.auto")?, "0\n", "Auto-gc should be disabled for Git commands run through gix-testtools" ); Ok(()) } #[test] fn split_git_arguments_handles_multiline_whitespace() { assert_eq!( split_git_arguments( "log --graph --oneline", ) .expect("valid arguments"), ["log", "--graph", "--oneline"] ); } #[test] fn split_git_arguments_handles_quoted_arguments() { assert_eq!( split_git_arguments( "commit -m 'subject with spaces' --author=\"A U Thor \"", ) .expect("valid arguments"), [ "commit", "-m", "subject with spaces", "--author=A U Thor " ] ); } #[test] fn split_git_arguments_handles_empty_quoted_arguments() { assert_eq!( split_git_arguments("diff -- pathspec:''").expect("valid arguments"), ["diff", "--", "pathspec:"] ); assert_eq!( split_git_arguments("diff -- ''").expect("valid arguments"), ["diff", "--", ""] ); } #[test] fn split_git_arguments_handles_escaped_whitespace() { assert_eq!( split_git_arguments(r"add path\ with\ spaces").expect("valid arguments"), ["add", "path with spaces"] ); } #[test] fn split_git_arguments_concatenates_quoted_and_unquoted_parts() { assert_eq!( split_git_arguments(r#"commit -m prefix" quoted "suffix"#).expect("valid arguments"), ["commit", "-m", "prefix quoted suffix"] ); } #[test] fn split_git_arguments_rejects_unterminated_quotes() { assert!(split_git_arguments("commit -m 'unterminated").is_err()); assert!(split_git_arguments("commit -m \"unterminated").is_err()); } #[test] fn normalize_debug_snapshot_returns_replaced_ids_by_placeholder_index() { let first = gix_hash::ObjectId::from_hex(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").expect("valid SHA1"); let second = gix_hash::ObjectId::from_hex(b"496d6428b9cf92981dc9495211e6e1120fb6f2ba").expect("valid SHA1"); let (snapshot, ids) = normalize_debug_snapshot(&vec![first, first, second, first]); assert_eq!(ids, vec![first, second]); assert_eq!( snapshot, r#"[ Oid(1), Oid(1), Oid(2), Oid(1), ]"# ); } #[test] fn normalize_hashes_replaces_raw_object_ids() { let sha1 = gix_hash::ObjectId::from_hex(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").expect("valid SHA1"); let sha256 = gix_hash::ObjectId::from_hex(b"473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813") .expect("valid SHA256"); let (snapshot, ids) = normalize_hashes( "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 \ 473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813 \ e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", ); assert_eq!(ids, vec![sha1, sha256]); assert_eq!(snapshot, "Oid(1) Oid(2) Oid(1)"); } #[test] #[cfg(not(feature = "worktree-exclusions"))] fn gitignore_fallback_matches_archive_basename_patterns() { let lines = "\n# generated fixture archives\nrust-*.tar\n"; assert!(is_excluded_by_lines( lines, Path::new("tests/fixtures/generated-archives/rust-basic.tar") )); assert!(!is_excluded_by_lines( lines, Path::new("tests/fixtures/generated-archives/script-basic.tar") )); } #[test] #[cfg(not(feature = "worktree-exclusions"))] fn gitignore_fallback_matches_paths_relative_to_fixture_base() { let lines = "generated-archives/rust-*.tar\n"; assert!(is_excluded_by_lines( lines, Path::new("generated-archives/rust-basic.tar") )); assert!(!is_excluded_by_lines( lines, Path::new("other-generated-archives/rust-basic.tar") )); } #[test] #[cfg(not(feature = "worktree-exclusions"))] fn gitignore_fallback_treats_leading_slash_as_rooted_pattern() { let lines = "/generated-archives/rust-*.tar\n"; assert!(is_excluded_by_lines( lines, Path::new("generated-archives/rust-basic.tar") )); } #[test] #[cfg(not(feature = "worktree-exclusions"))] fn gitignore_fallback_ignores_blank_lines_and_comments() { let lines = "\n \n# generated-archives/rust-*.tar\ngenerated-archives/script-*.tar\n"; assert!(is_excluded_by_lines( lines, Path::new("generated-archives/script-basic.tar") )); assert!(!is_excluded_by_lines( lines, Path::new("generated-archives/rust-basic.tar") )); } #[test] #[cfg(not(feature = "worktree-exclusions"))] fn gitignore_fallback_normalizes_windows_path_separators() { let lines = "generated-archives/rust-*.tar\n"; assert!(is_excluded_by_lines( lines, Path::new(r"generated-archives\rust-basic.tar") )); } #[test] fn archive_required_fixtures_use_a_separate_cache_directory() { // Archive-required fixtures must not share the normal generated fixture // cache. Otherwise, a previous script run can leave platform-specific // output behind and make a later archive-required request skip extraction. // Using different paths makes sure they are actually from the archive if they exist. let fixture_base = Path::new("tests").join("fixtures"); let (_, generated_dir) = force_and_dir(None, &fixture_base, "scripted", Some(gix_hash::Kind::Sha1), &1234, None); let (_, archived_dir) = force_and_dir( None, &fixture_base, "scripted", Some(gix_hash::Kind::Sha1), &1234, Some("archive"), ); assert_ne!(generated_dir, archived_dir); assert!( archived_dir .components() .any(|component| component.as_os_str() == "archive") ); }