Repository: jubako/arx (language: python)

Issue:
Get rid of `arx_test_dir` crate
`arx_test_dir` (https://github.com/jubako/arx_test_dir) is used only for generate source directory for testing arx (used in https://github.com/jubako/arx/blob/main/arx/tests/create.rs#L8-L16).

We should generate a test tree ourselves instead of using arx_test_dir.


Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 7e69bca..2efa0fa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -139,7 +139,6 @@ name = "arx"
 version = "0.3.2"
 dependencies = [
  "anyhow",
- "arx_test_dir",
  "clap",
  "clap_complete",
  "clap_mangen",
@@ -147,26 +146,17 @@ dependencies = [
  "const_format",
  "daemonize",
  "env_logger",
+ "format-bytes",
  "git-version",
  "human-panic",
  "indicatif",
  "jubako",
  "libarx",
  "libc",
- "log",
- "tempfile",
-]
-
-[[package]]
-name = "arx_test_dir"
-version = "0.1.0"
-source = "git+https://github.com/jubako/arx_test_dir.git#b818e3c70401edf731748577da376fccb6b2058c"
-dependencies = [
- "clap",
- "fuser 0.13.0",
- "libc",
  "lipsum",
+ "log",
  "rand",
+ "tempfile",
 ]
 
 [[package]]
@@ -365,7 +355,7 @@ dependencies = [
  "heck",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.89",
 ]
 
 [[package]]
@@ -402,7 +392,7 @@ dependencies = [
  "nom",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.89",
 ]
 
 [[package]]
@@ -565,7 +555,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.89",
 ]
 
 [[package]]
@@ -587,7 +577,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.89",
 ]
 
 [[package]]
@@ -703,18 +693,23 @@ dependencies = [
 ]
 
 [[package]]
-name = "fuser"
-version = "0.13.0"
+name = "format-bytes"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21370f84640642c8ea36dfb2a6bfc4c55941f476fcf431f6fef25a5ddcf0169b"
+checksum = "48942366ef93975da38e175ac9e10068c6fc08ca9e85930d4f098f4d5b14c2fd"
 dependencies = [
- "libc",
- "log",
- "memchr",
- "page_size 0.5.0",
- "pkg-config",
- "smallvec",
- "zerocopy 0.6.6",
+ "format-bytes-macros",
+]
+
+[[package]]
+name = "format-bytes-macros"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "203aadebefcc73d12038296c228eabf830f99cba991b0032adf20e9fa6ce7e4f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -727,7 +722,7 @@ dependencies = [
  "log",
  "memchr",
  "nix",
- "page_size 0.6.0",
+ "page_size",
  "pkg-config",
  "smallvec",
  "zerocopy 0.8.10",
@@ -786,7 +781,7 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.89",
 ]
 
 [[package]]
@@ -975,7 +970,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.89",
 ]
 
 [[package]]
@@ -1101,7 +1096,7 @@ dependencies = [
  "bstr",
  "clap",
  "epochs",
- "fuser 0.15.0",
+ "fuser",
  "fxhash",
  "jubako",
  "libc",
@@ -1336,16 +1331,6 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
-[[package]]
-name = "page_size"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b7663cbd190cfd818d08efa8497f6cd383076688c49a391ef7c0d03cd12b561"
-dependencies = [
- "libc",
- "winapi",
-]
-
 [[package]]
 name = "page_size"
 version = "0.6.0"
@@ -1461,7 +1446,7 @@ dependencies = [
  "proc-macro2",
  "pyo3-macros-backend",
  "quote",
- "syn",
+ "syn 2.0.89",
 ]
 
 [[package]]
@@ -1474,7 +1459,7 @@ dependencies = [
  "proc-macro2",
  "pyo3-build-config",
  "quote",
- "syn",
+ "syn 2.0.89",
 ]
 
 [[package]]
@@ -1687,7 +1672,7 @@ checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.89",
 ]
 
 [[package]]
@@ -1758,6 +1743,17 @@ version = "2.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
 
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
 [[package]]
 name = "syn"
 version = "2.0.89"
@@ -1777,7 +1773,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.89",
 ]
 
 [[package]]
@@ -1856,7 +1852,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.89",
 ]
 
 [[package]]
@@ -1867,7 +1863,7 @@ checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.89",
 ]
 
 [[package]]
@@ -2072,7 +2068,7 @@ dependencies = [
  "once_cell",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.89",
  "wasm-bindgen-shared",
 ]
 
@@ -2094,7 +2090,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.89",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -2298,20 +2294,10 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.89",
  "synstructure",
 ]
 
-[[package]]
-name = "zerocopy"
-version = "0.6.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6"
-dependencies = [
- "byteorder",
- "zerocopy-derive 0.6.6",
-]
-
 [[package]]
 name = "zerocopy"
 version = "0.7.35"
@@ -2331,17 +2317,6 @@ dependencies = [
  "zerocopy-derive 0.8.10",
 ]
 
-[[package]]
-name = "zerocopy-derive"
-version = "0.6.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
 [[package]]
 name = "zerocopy-derive"
 version = "0.7.35"
@@ -2350,7 +2325,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.89",
 ]
 
 [[package]]
@@ -2361,7 +2336,7 @@ checksum = "593e7c96176495043fcb9e87cf7659f4d18679b5bab6b92bdef359c76a7795dd"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.89",
 ]
 
 [[package]]
@@ -2381,7 +2356,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.89",
  "synstructure",
 ]
 
@@ -2402,7 +2377,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.89",
 ]
 
 [[package]]
@@ -2424,7 +2399,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.89",
 ]
 
 [[package]]
diff --git a/arx/Cargo.toml b/arx/Cargo.toml
index 722f492..efe81b7 100644
--- a/arx/Cargo.toml
+++ b/arx/Cargo.toml
@@ -33,9 +33,11 @@ color-print = "0.3.7"
 [target.'cfg(unix)'.dependencies]
 daemonize = "0.5.0"
 
-[target.'cfg(not(windows))'.dev-dependencies]
-arx_test_dir = { git = "https://github.com/jubako/arx_test_dir.git", features = ["fuse"] }
+[dev-dependencies]
+rand = { version = "0.8.5", features = ["small_rng"]}
+lipsum = "0.9.0"
 tempfile = "3.8.0"
+format-bytes = "0.3.0"
 
 [features]
 default = ["zstd", "fuse"]


Test patch:
diff --git a/arx/tests/create.rs b/arx/tests/create.rs
index 97387af..63d4190 100644
--- a/arx/tests/create.rs
+++ b/arx/tests/create.rs
@@ -1,400 +1,96 @@
-use std::io::Read;
+mod utils;
 
-#[cfg(all(unix, not(feature = "in_ci")))]
-mod inner {
-    pub use std::path::Path;
-
-    // Generate a fake directory with fake content.
-    pub fn spawn_mount() -> std::io::Result<(arx_test_dir::BackgroundSession, tempfile::TempDir)> {
-        let mount_path =
-            tempfile::TempDir::with_prefix_in("source_", env!("CARGO_TARGET_TMPDIR")).unwrap();
-        let builder = arx_test_dir::ContextBuilder::new();
-        let context = builder.create();
-        let dir = arx_test_dir::DirEntry::new_root(context);
-        let mount_dir = arx_test_dir::TreeFs::new(dir);
-        Ok((mount_dir.spawn(mount_path.path())?, mount_path))
-    }
-}
-
-macro_rules! cmd {
-    ("{cmd}", $command:ident, $arg:expr) => {{
-        $command.arg($arg);
-        $command.output().expect("Launching arx command should work.")
-    }};
-    ("{cmd}", $command:ident, $arg:expr, $($args:expr),+) => {{
-        $command.arg($arg);
-        cmd!("{cmd}", $command, $($args),+)
-    }};
-    ("arx", $sub_command:literal, $($args:expr),*) => {{
-        let arx_bin = env!("CARGO_BIN_EXE_arx");
-        let mut command = std::process::Command::new(&arx_bin);
-        command.env("NO_COLOR", "1");
-        cmd!("{cmd}", command, $sub_command, $($args),*)
-    }};
-    ($prog:literal, $($args:expr),*) => {{
-        let mut command = std::process::Command::new($prog);
-        cmd!("{cmd}", command, $($args),*)
-    }};
-}
+use format_bytes::format_bytes;
+use std::{io::Read, path::Path};
+use utils::*;
 
 #[test]
-fn test_crate_non_existant_input() {
-    use std::path::Path;
-
-    let arx_tmp_dir = tempfile::tempdir_in(Path::new(env!("CARGO_TARGET_TMPDIR")))
-        .expect("Creating tempdir should work");
-    let arx_file = arx_tmp_dir.path().join("test.arx");
-    let output = cmd!("arx", "create", "--outfile", &arx_file, "non_existant_dir");
-    let stdout = String::from_utf8(output.stdout).unwrap();
-    let stderr = String::from_utf8(output.stderr).unwrap();
-    println!("Out : {}", stdout);
-    println!("Err : {}", stderr);
-    assert_eq!("", stdout);
-    assert_eq!(
-        "[ERROR arx] Error : Input non_existant_dir path doesn't exist or cannot be accessed\n",
-        stderr
+fn test_crate_non_existant_input() -> Result {
+    temp_arx!(arx_file);
+    cmd!("arx", "create", "--outfile", &arx_file, "non_existant_dir").check_fail(
+        b"",
+        b"[ERROR arx] Error : Input non_existant_dir path doesn't exist or cannot be accessed\n",
     );
-    assert!(!output.status.success());
     assert!(!arx_file.exists());
+    Ok(())
 }
 
-#[cfg(all(unix, not(feature = "in_ci")))]
 #[test]
-fn test_crate_non_existant_output_directory() {
-    use inner::*;
-    use std::path::Path;
-
-    let (_source_mount_handle, source_mount_point) = spawn_mount().unwrap();
-    let source_mount_point = source_mount_point.path();
-    let arx_tmp_dir = tempfile::tempdir_in(Path::new(env!("CARGO_TARGET_TMPDIR")))
-        .expect("Creating tempdir should work");
-    let arx_file = arx_tmp_dir
-        .path()
-        .join("non_existant_directory")
-        .join("test.arx");
-    let output = cmd!(
+fn test_crate_non_existant_output_directory() -> Result {
+    let tmp_source_dir = SHARED_TEST_DIR.path();
+    temp_arx!(arx_file, "non_existant_directory/test.arx");
+    cmd!(
         "arx",
         "create",
         "--outfile",
         &arx_file,
         "-C",
-        source_mount_point.parent().unwrap(),
+        tmp_source_dir.parent().unwrap(),
         "--strip-prefix",
-        source_mount_point.file_name().unwrap(),
-        source_mount_point.file_name().unwrap()
-    );
-    let stdout = String::from_utf8(output.stdout).unwrap();
-    let stderr = String::from_utf8(output.stderr).unwrap();
-    println!("Out : {}", stdout);
-    println!("Err : {}", stderr);
-    assert_eq!("", stdout);
-    assert_eq!(
-        format!(
-            "[ERROR arx] Error : Directory {} doesn't exist\n",
-            arx_file.parent().unwrap().display()
+        tmp_source_dir.file_name().unwrap(),
+        tmp_source_dir.file_name().unwrap()
+    )
+    .check_fail(
+        b"",
+        &format_bytes!(
+            b"[ERROR arx] Error : Directory {} doesn't exist\n",
+            arx_file.parent().unwrap().as_os_str().as_encoded_bytes()
         ),
-        stderr
     );
-    assert!(!output.status.success());
     assert!(!arx_file.exists());
+    Ok(())
 }
 
-#[cfg(all(unix, not(feature = "in_ci")))]
 #[test]
-fn test_crate_existant_output() {
-    use inner::*;
-    use std::path::Path;
-
-    let (_source_mount_handle, source_mount_point) = spawn_mount().unwrap();
-    let source_mount_point = source_mount_point.path();
-    let arx_tmp_dir = tempfile::tempdir_in(Path::new(env!("CARGO_TARGET_TMPDIR")))
-        .expect("Creating tempdir should work");
-    let arx_file = arx_tmp_dir.path().join("test.arx");
+fn test_crate_existant_output() -> Result {
+    let tmp_source_dir = SHARED_TEST_DIR.path();
+    temp_arx!(arx_file);
     {
         use std::io::Write;
-        let mut f = std::fs::File::create(&arx_file).unwrap();
-        f.write_all(b"Some dummy content").unwrap();
+        let mut f = std::fs::File::create(&arx_file)?;
+        f.write_all(b"Some dummy content")?;
     }
 
     // Try to write without --force
-    let output = cmd!(
+    cmd!(
         "arx",
         "create",
         "--outfile",
         &arx_file,
         "-C",
-        source_mount_point.parent().unwrap(),
+        tmp_source_dir.parent().unwrap(),
         "--strip-prefix",
-        source_mount_point.file_name().unwrap(),
-        source_mount_point.file_name().unwrap()
-    );
-    let stdout = String::from_utf8(output.stdout).unwrap();
-    let stderr = String::from_utf8(output.stderr).unwrap();
-    println!("Out : {}", stdout);
-    println!("Err : {}", stderr);
-    assert_eq!("", stdout);
-    assert_eq!(
-        format!(
-            "[ERROR arx] Error : File {} already exists. Use option --force to overwrite it.\n",
-            arx_file.display()
+        tmp_source_dir.file_name().unwrap(),
+        tmp_source_dir.file_name().unwrap()
+    )
+    .check_fail(
+        b"",
+        &format_bytes!(
+            b"[ERROR arx] Error : File {} already exists. Use option --force to overwrite it.\n",
+            arx_file.as_os_str().as_encoded_bytes()
         ),
-        stderr
     );
-    assert!(!output.status.success());
-    assert_eq!(std::fs::read(&arx_file).unwrap(), b"Some dummy content");
+    assert_eq!(std::fs::read(&arx_file)?, b"Some dummy content");
 
     // Try to write without --force
-    let output = cmd!(
+    cmd!(
         "arx",
         "create",
         "--outfile",
         &arx_file,
         "-C",
-        source_mount_point.parent().unwrap(),
+        tmp_source_dir.parent().unwrap(),
         "--strip-prefix",
-        source_mount_point.file_name().unwrap(),
-        source_mount_point.file_name().unwrap(),
+        tmp_source_dir.file_name().unwrap(),
+        tmp_source_dir.file_name().unwrap(),
         "--force"
-    );
-    let stdout = String::from_utf8(output.stdout).unwrap();
-    let stderr = String::from_utf8(output.stderr).unwrap();
-    println!("Out : {}", stdout);
-    println!("Err : {}", stderr);
-    assert_eq!("", stdout);
-    assert_eq!("", stderr);
-    assert!(output.status.success());
+    )
+    .check_output(b"", b"");
     {
-        let mut f = std::fs::File::open(&arx_file).unwrap();
+        let mut f = std::fs::File::open(&arx_file)?;
         let mut buf = [0; 10];
-        f.read_exact(&mut buf).unwrap();
+        f.read_exact(&mut buf)?;
         assert_eq!(&buf, b"jbkC\x00\x00\x00\x00\x00\x02");
     }
-}
-
-#[cfg(all(unix, not(feature = "in_ci")))]
-#[test]
-fn test_create_and_mount() {
-    use inner::*;
-
-    let (_source_mount_handle, source_mount_point) = spawn_mount().unwrap();
-    let source_mount_point = source_mount_point.path();
-    let arx_tmp_dir = tempfile::tempdir_in(Path::new(env!("CARGO_TARGET_TMPDIR")))
-        .expect("Creating tempdir should work");
-    let arx_file = arx_tmp_dir.path().join("test.arx");
-    let output = cmd!(
-        "arx",
-        "create",
-        "--outfile",
-        &arx_file,
-        "-C",
-        source_mount_point.parent().unwrap(),
-        "--strip-prefix",
-        source_mount_point.file_name().unwrap(),
-        source_mount_point.file_name().unwrap()
-    );
-    println!("Out : {}", String::from_utf8(output.stdout).unwrap());
-    println!("Err : {}", String::from_utf8(output.stderr).unwrap());
-    assert!(output.status.success());
-    assert!(arx_file.is_file());
-
-    let mount_point = tempfile::TempDir::new_in(env!("CARGO_TARGET_TMPDIR")).unwrap();
-    let arx = arx::Arx::new(arx_file).unwrap();
-    let arxfs = arx::ArxFs::new(arx).unwrap();
-    let _mount_handle = arxfs
-        .spawn_mount("Test mounted arx".into(), mount_point.path())
-        .unwrap();
-    let output = cmd!("diff", "-r", source_mount_point, mount_point.path());
-    println!("Out : {}", String::from_utf8(output.stdout).unwrap());
-    println!("Err: {}", String::from_utf8(output.stderr).unwrap());
-    assert!(output.status.success());
-}
-
-#[cfg(all(unix, not(feature = "in_ci")))]
-#[test]
-fn test_create_and_extract() {
-    use inner::*;
-
-    let (_source_mount_handle, source_mount_point) = spawn_mount().unwrap();
-    let source_mount_point = source_mount_point.path();
-    let arx_tmp_dir = tempfile::tempdir_in(Path::new(env!("CARGO_TARGET_TMPDIR")))
-        .expect("Creating tempdir should work");
-    let arx_file = arx_tmp_dir.path().join("test.arx");
-    let output = cmd!(
-        "arx",
-        "create",
-        "--outfile",
-        &arx_file,
-        "-C",
-        source_mount_point.parent().unwrap(),
-        "--strip-prefix",
-        source_mount_point.file_name().unwrap(),
-        source_mount_point.file_name().unwrap()
-    );
-    println!("Out : {}", String::from_utf8(output.stdout).unwrap());
-    println!("Err : {}", String::from_utf8(output.stderr).unwrap());
-    assert!(output.status.success());
-    assert!(arx_file.is_file());
-
-    let extract_dir = tempfile::TempDir::new_in(env!("CARGO_TARGET_TMPDIR")).unwrap();
-    arx::extract(
-        &arx_file,
-        extract_dir.path(),
-        Default::default(),
-        true,
-        false,
-    )
-    .unwrap();
-    let output = cmd!("diff", "-r", source_mount_point, extract_dir.path());
-    println!("Out : {}", String::from_utf8(output.stdout).unwrap());
-    println!("Err: {}", String::from_utf8(output.stderr).unwrap());
-    assert!(output.status.success());
-}
-
-#[cfg(all(unix, not(feature = "in_ci")))]
-#[test]
-fn test_create_and_extract_filter() {
-    use inner::*;
-
-    let (_source_mount_handle, source_mount_point) = spawn_mount().unwrap();
-    let source_mount_point = source_mount_point.path();
-    let arx_tmp_dir = tempfile::tempdir_in(Path::new(env!("CARGO_TARGET_TMPDIR")))
-        .expect("Creating tempdir should work");
-    let arx_file = arx_tmp_dir.path().join("test.arx");
-    let output = cmd!(
-        "arx",
-        "create",
-        "--outfile",
-        &arx_file,
-        "-C",
-        source_mount_point.parent().unwrap(),
-        "--strip-prefix",
-        source_mount_point.file_name().unwrap(),
-        source_mount_point.file_name().unwrap()
-    );
-    println!("Out : {}", String::from_utf8(output.stdout).unwrap());
-    println!("Err : {}", String::from_utf8(output.stderr).unwrap());
-    assert!(output.status.success());
-    assert!(arx_file.is_file());
-
-    let extract_dir =
-        tempfile::TempDir::with_prefix_in("extract_", env!("CARGO_TARGET_TMPDIR")).unwrap();
-    arx::extract(
-        &arx_file,
-        extract_dir.path(),
-        ["OrcBlIw".into()].into(),
-        true,
-        true,
-    )
-    .unwrap();
-
-    let mut source_sub_dir = source_mount_point.to_path_buf();
-    source_sub_dir.push("OrcBlIw");
-    let mut extract_sub_dir = extract_dir.path().to_path_buf();
-    extract_sub_dir.push("OrcBlIw");
-
-    println!(
-        "Diff {} and {}",
-        source_sub_dir.display(),
-        extract_sub_dir.display()
-    );
-    let output = cmd!("diff", "-r", &source_sub_dir, &extract_sub_dir);
-    println!("Out : {}", String::from_utf8(output.stdout).unwrap());
-    println!("Err: {}", String::from_utf8(output.stderr).unwrap());
-    assert!(output.status.success());
-}
-
-#[cfg(all(unix, not(feature = "in_ci")))]
-#[test]
-fn test_create_and_extract_subdir() {
-    use inner::*;
-
-    let (_source_mount_handle, source_mount_point) = spawn_mount().unwrap();
-    let source_mount_point = source_mount_point.path();
-    let arx_tmp_dir = tempfile::tempdir_in(Path::new(env!("CARGO_TARGET_TMPDIR")))
-        .expect("Creating tempdir should work");
-    let arx_file = arx_tmp_dir.path().join("test.arx");
-    let output = cmd!(
-        "arx",
-        "create",
-        "--outfile",
-        &arx_file,
-        "-C",
-        source_mount_point.parent().unwrap(),
-        "--strip-prefix",
-        source_mount_point.file_name().unwrap(),
-        source_mount_point.file_name().unwrap()
-    );
-    println!("Out : {}", String::from_utf8(output.stdout).unwrap());
-    println!("Err : {}", String::from_utf8(output.stderr).unwrap());
-    assert!(output.status.success());
-    assert!(arx_file.is_file());
-
-    let extract_dir =
-        tempfile::TempDir::with_prefix_in("extract_", env!("CARGO_TARGET_TMPDIR")).unwrap();
-
-    let output = cmd!(
-        "arx",
-        "extract",
-        &arx_file,
-        "--root-dir",
-        "OrcBlIw/tuyuMO7",
-        "-C",
-        extract_dir.path()
-    );
-    assert!(output.status.success());
-
-    let mut source_sub_dir = source_mount_point.to_path_buf();
-    source_sub_dir.push("OrcBlIw");
-    source_sub_dir.push("tuyuMO7");
-
-    println!(
-        "Diff {} and {}",
-        source_sub_dir.display(),
-        extract_dir.path().display()
-    );
-    let output = cmd!("diff", "-r", &source_sub_dir, extract_dir.path());
-    println!("Out: {}", String::from_utf8(output.stdout).unwrap());
-    println!("Err: {}", String::from_utf8(output.stderr).unwrap());
-    assert!(output.status.success());
-}
-
-#[cfg(all(unix, not(feature = "in_ci")))]
-#[test]
-fn test_create_and_extract_subfile() {
-    use inner::*;
-
-    let (_source_mount_handle, source_mount_point) = spawn_mount().unwrap();
-    let source_mount_point = source_mount_point.path();
-    let arx_tmp_dir = tempfile::tempdir_in(Path::new(env!("CARGO_TARGET_TMPDIR")))
-        .expect("Creating tempdir should work");
-    let arx_file = arx_tmp_dir.path().join("test.arx");
-    let output = cmd!(
-        "arx",
-        "create",
-        "--outfile",
-        &arx_file,
-        "-C",
-        source_mount_point.parent().unwrap(),
-        "--strip-prefix",
-        source_mount_point.file_name().unwrap(),
-        source_mount_point.file_name().unwrap()
-    );
-    println!("Out : {}", String::from_utf8(output.stdout).unwrap());
-    println!("Err : {}", String::from_utf8(output.stderr).unwrap());
-    assert!(output.status.success());
-    assert!(arx_file.is_file());
-
-    let extract_dir =
-        tempfile::TempDir::with_prefix_in("extract_", env!("CARGO_TARGET_TMPDIR")).unwrap();
-
-    let output = cmd!(
-        "arx",
-        "extract",
-        &arx_file,
-        "--root-dir",
-        "OrcBlIw/8w5EKLr.text",
-        "-C",
-        extract_dir.path()
-    );
-    assert!(!output.status.success());
+    Ok(())
 }
diff --git a/arx/tests/extract_mount.rs b/arx/tests/extract_mount.rs
new file mode 100644
index 0000000..b47dbe3
--- /dev/null
+++ b/arx/tests/extract_mount.rs
@@ -0,0 +1,143 @@
+mod utils;
+
+use std::{
+    path::{Path, PathBuf},
+    sync::LazyLock,
+};
+use utils::*;
+
+pub struct TmpArx {
+    _tmp: tempfile::TempDir,
+    path: PathBuf,
+}
+
+impl TmpArx {
+    pub(self) fn new(tmp_dir: tempfile::TempDir, path: PathBuf) -> Self {
+        Self {
+            _tmp: tmp_dir,
+            path,
+        }
+    }
+
+    pub fn path(&self) -> &Path {
+        &self.path
+    }
+}
+
+pub static BASE_ARX_FILE: LazyLock<TmpArx> = LazyLock::new(|| {
+    let source_dir = SHARED_TEST_DIR.path();
+    let tmp_arx_dir = tempfile::tempdir_in(Path::new(env!("CARGO_TARGET_TMPDIR")))
+        .expect("Creating tmpdir should work");
+    let tmp_arx = tmp_arx_dir.path().join("test.arx");
+    cmd!(
+        "arx",
+        "create",
+        "--outfile",
+        &tmp_arx,
+        "-C",
+        source_dir.parent().unwrap(),
+        "--strip-prefix",
+        source_dir.file_name().unwrap(),
+        source_dir.file_name().unwrap()
+    )
+    .check();
+    TmpArx::new(tmp_arx_dir, tmp_arx)
+});
+
+#[cfg(all(unix, not(feature = "in_ci")))]
+#[test]
+fn test_mount() -> Result {
+    let tmp_source_dir = SHARED_TEST_DIR.path();
+    let arx_file = BASE_ARX_FILE.path();
+
+    let mount_point = tempfile::TempDir::new_in(env!("CARGO_TARGET_TMPDIR"))?;
+    let arx = arx::Arx::new(arx_file)?;
+    let arxfs = arx::ArxFs::new(arx)?;
+    let _mount_handle = arxfs.spawn_mount("Test mounted arx".into(), mount_point.path())?;
+    assert!(tree_equal(tmp_source_dir, mount_point)?);
+    Ok(())
+}
+
+#[test]
+fn test_extract() -> Result {
+    let tmp_source_dir = SHARED_TEST_DIR.path();
+    let arx_file = BASE_ARX_FILE.path();
+
+    let extract_dir = tempfile::TempDir::new_in(env!("CARGO_TARGET_TMPDIR"))?;
+    arx::extract(
+        &arx_file,
+        extract_dir.path(),
+        Default::default(),
+        true,
+        false,
+    )?;
+    assert!(tree_equal(tmp_source_dir, extract_dir)?);
+    Ok(())
+}
+
+#[test]
+fn test_extract_filter() -> Result {
+    let tmp_source_dir = SHARED_TEST_DIR.path();
+    let arx_file = BASE_ARX_FILE.path();
+
+    let extract_dir = tempfile::TempDir::with_prefix_in("extract_", env!("CARGO_TARGET_TMPDIR"))?;
+    arx::extract(
+        &arx_file,
+        extract_dir.path(),
+        ["sub_dir_a".into()].into(),
+        true,
+        true,
+    )?;
+
+    let source_sub_dir = join!(tmp_source_dir / "sub_dir_a");
+    let extract_sub_dir = join!((extract_dir.path()) / "sub_dir_a");
+
+    assert!(tree_equal(source_sub_dir, extract_sub_dir)?);
+    Ok(())
+}
+
+#[test]
+fn test_extract_subdir() -> Result {
+    let tmp_source_dir = SHARED_TEST_DIR.path();
+    let arx_file = BASE_ARX_FILE.path();
+
+    let extract_dir = tempfile::TempDir::with_prefix_in("extract_", env!("CARGO_TARGET_TMPDIR"))?;
+
+    cmd!(
+        "arx",
+        "extract",
+        &arx_file,
+        "--root-dir",
+        "sub_dir_a",
+        "-C",
+        extract_dir.path()
+    )
+    .check_output(b"", b"");
+
+    let source_sub_dir = join!(tmp_source_dir / "sub_dir_a");
+
+    assert!(tree_equal(source_sub_dir, extract_dir)?);
+    Ok(())
+}
+
+#[test]
+fn test_extract_subfile() -> Result {
+    let arx_file = BASE_ARX_FILE.path();
+
+    let extract_dir = tempfile::TempDir::with_prefix_in("extract_", env!("CARGO_TARGET_TMPDIR"))?;
+
+    cmd!(
+        "arx",
+        "extract",
+        &arx_file,
+        "--root-dir",
+        "sub_dir_a/file1.txt",
+        "-C",
+        extract_dir.path()
+    )
+    .check_fail(
+        b"",
+        b"[ERROR arx] Error : sub_dir_a/file1.txt must be a directory\n",
+    );
+    Ok(())
+}
diff --git a/arx/tests/utils/mod.rs b/arx/tests/utils/mod.rs
new file mode 100644
index 0000000..2fdd0ab
--- /dev/null
+++ b/arx/tests/utils/mod.rs
@@ -0,0 +1,403 @@
+use std::{
+    fs::{create_dir, write},
+    io::Read,
+    path::Path,
+    process::Command,
+    sync::LazyLock,
+};
+
+use rand::prelude::*;
+pub type Result = anyhow::Result<()>;
+
+#[cfg(unix)]
+fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(path: P, target: Q) -> std::io::Result<()> {
+    std::os::unix::fs::symlink(target, path)
+}
+#[cfg(unix)]
+fn symlink_dir<P: AsRef<Path>, Q: AsRef<Path>>(path: P, target: Q) -> std::io::Result<()> {
+    std::os::unix::fs::symlink(target, path)
+}
+
+#[cfg(windows)]
+fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(path: P, target: Q) -> std::io::Result<()> {
+    std::os::windows::fs::symlink_file(target, path)
+}
+#[cfg(windows)]
+fn symlink_dir<P: AsRef<Path>, Q: AsRef<Path>>(path: P, target: Q) -> std::io::Result<()> {
+    std::os::windows::fs::symlink_dir(target, path)
+}
+
+struct BinRead<'a>(pub &'a mut SmallRng);
+
+impl Read for BinRead<'_> {
+    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+        let to_read_len = std::cmp::min(buf.len(), 1024);
+        self.0.fill_bytes(&mut buf[..to_read_len]);
+        Ok(to_read_len)
+    }
+}
+
+/// Generate a tree of directory/file/link
+///
+/// This macro is intended to be used as a function.
+/// The first argument is a integer used as seed to generate random numbers/content
+/// The second argument is a description of the tree to create.
+/// It format is `{ instruction, instruction, ... }`
+/// Each instruction can be :
+/// - `dir "<name>" { inner_instructions }`:
+///   This will create a subdir `<name>` with the content `inner_instructions`
+/// - `text "<name>" <len>`: Generate a text file `<name>` of size `<len>`
+/// - `bin "<name>" <len>`: Generate a binary file `<name>` of size `<len>`
+/// - `link` "<name>" -> "<target>": Generate a symlink `<name>` to file `<target>`
+/// - `link_din` "<name>" -> "<target>": Generate a symlink `<name>` to a directory `<target>`
+/// - `loop <count> { instructions }`: Repeat `instuctions` `<count>` times
+///
+/// All numbers (count/len) can be a simple literal (`42`) or a range (`1..42`).
+/// If it is a range, the actual number is a random number in this range.
+///
+/// `loop` instruction generate a context (the current increment of the loop) which MUST
+/// be used in "<name>". By default, the context is name `ctx`.
+/// So in a loop, all "<name>" or "<target>" must be in the form of "foo_{ctx}_bar".
+/// It is possible to give a explicit name to the context with `loop my_name=42 { instructions }`.
+/// Loop can be neested. As all contexts MUST be used, you MUST explicit name your contexts
+/// to avoid conflict.
+#[macro_export]
+macro_rules! temp_tree {
+    // This macro is implemented using:
+    // - tt_muncher (https://veykril.github.io/tlborm/decl-macros/patterns/tt-muncher.html)
+    //   to parse the input structure
+    // - internal_rules (https://veykril.github.io/tlborm/decl-macros/patterns/internal-rules.html)
+    //   to handle instructions and "sub routine"
+
+
+    // -----------
+    // Entry point
+    // -----------
+    ($seed:literal, { $($what:tt)* }) => {
+        {
+            let temp_path =
+            tempfile::TempDir::with_prefix_in("source_", env!("CARGO_TARGET_TMPDIR"))?;
+            let mut rng = SmallRng::seed_from_u64($seed);
+            temp_tree!(@instr, temp_path.path(), rng, [], $($what)*);
+            Ok(temp_path)
+        }
+    };
+
+    // -----------------------
+    // Parsing of instructions
+    // -----------------------
+    // End of instruction
+    (@instr, $path:expr, $rng:ident, $context:tt,) => {};
+
+    // Handle dir instruction
+    (@instr, $path:expr, $rng:ident, $context:tt, dir $sub_path:tt $what:tt ) => {
+        temp_tree!(@dir, $path, $rng, $context, $sub_path, $what);
+    };
+    (@instr, $path:expr, $rng:ident, $context:tt, dir $sub_path:tt $what:tt, $($left:tt)* ) => {
+        temp_tree!(@dir, $path, $rng, $context, $sub_path, $what);
+        temp_tree!(@instr, $path, $rng, $context, $($left)*)
+    };
+
+    // Handle text instruction
+    (@instr, $path:expr, $rng:ident, $context:tt, text $sub_path:tt $what:tt ) => {
+        temp_tree!(@text, $path, $rng, $context, $sub_path, $what);
+    };
+    (@instr, $path:expr, $rng:ident, $context:tt, text $sub_path:tt $what:tt, $($left:tt)* ) => {
+        temp_tree!(@text, $path, $rng, $context, $sub_path, $what);
+        temp_tree!(@instr, $path, $rng, $context, $($left)*)
+    };
+
+    // Handle binary instruction
+    (@instr, $path:expr, $rng:ident, $context:tt, bin $sub_path:tt $what:tt ) => {
+        temp_tree!(@bin, $path, $rng, $context, $sub_path, $what);
+    };
+
+    (@instr, $path:expr, $rng:ident, $context:tt, bin $sub_path:tt $what:tt, $($left:tt)* ) => {
+        temp_tree!(@bin, $path, $rng, $context, $sub_path, $what);
+        temp_tree!(@instr, $path, $rng, $context, $($left)*)
+    };
+
+    // Handle symlink instruction
+    (@instr, $path:expr, $rng:ident, $context:tt, link $sub_path:tt -> $what:tt ) => {
+        temp_tree!(@link, $path, $rng, $context, $sub_path, $what);
+    };
+
+    (@instr, $path:expr, $rng:ident, $context:tt, link $sub_path:tt -> $what:tt, $($left:tt)* ) => {
+        temp_tree!(@link, $path, $rng, $context, $sub_path, $what);
+        temp_tree!(@instr, $path, $rng, $context, $($left)*)
+    };
+
+    // Handle symlink_dir instruction
+    (@instr, $path:expr, $rng:ident, $context:tt, link_dir $sub_path:tt -> $what:tt ) => {
+        temp_tree!(@link_dir, $path, $rng, $context, $sub_path, $what);
+    };
+    (@instr, $path:expr, $rng:ident, $context:tt, link_dir $sub_path:tt -> $what:tt, $($left:tt)* ) => {
+        temp_tree!(@link_dir, $path, $rng, $context, $sub_path, $what);
+        temp_tree!(@instr, $path, $rng, $context, $($left)*)
+    };
+
+    // Handle loop instruction
+    (@instr, $path:expr, $rng:ident, $context:tt, loop $nb:tt $what:tt ) => {
+        temp_tree!(@loop, $path, $rng, $context, ctx, $nb, $what);
+    };
+    (@instr, $path:expr, $rng:ident, $context:tt, loop $nb:tt $what:tt, $($left:tt)* ) => {
+        temp_tree!(@loop, $path, $rng, $context, ctx, $nb, $what);
+        temp_tree!(@instr, $path, $rng, $context, $($left)*)
+    };
+
+    // Handle named context gen instruction
+    (@instr, $path:expr, $rng:ident, $context:tt, loop $ctx_name:ident=$nb:tt $what:tt ) => {
+        temp_tree!(@loop, $path, $rng, $context, $ctx_name, $nb, $what);
+    };
+    (@instr, $path:expr, $rng:ident, $context:tt, loop $ctx_name:ident=$nb:tt $what:tt, $($left:tt)* ) => {
+        temp_tree!(@loop, $path, $rng, $context, $ctx_name, $nb, $what);
+        temp_tree!(@instr, $path, $rng, $context, $($left)*)
+    };
+
+    // ------------------------
+    // Handling of instructions
+    // ------------------------
+
+    // Empty dir
+    (@dir, $path:expr, $rng:ident, $context:tt, $sub_path:tt, { }) => {
+        create_dir($path.join(&temp_tree!(@ctx, $sub_path, $context)))?;
+    };
+    // Dir with content
+    (@dir, $path:expr, $rng:ident, $context:tt, $sub_path:tt, { $($what:tt)+ }) => {
+        {
+            let new_path = $path.join(&temp_tree!(@ctx, $sub_path, $context));
+            create_dir(&new_path)?;
+            temp_tree!(@instr, new_path, $rng, $context, $($what)+) ;
+        }
+    };
+
+    // Text file
+    (@text, $path:expr, $rng:ident, $context:tt, $sub_path:tt, $len:tt) => {
+        let len = temp_tree!(@num, $rng, $len);
+        let data = lipsum::lipsum_words_with_rng(&mut $rng, len);
+        write($path.join(&temp_tree!(@ctx, $sub_path, $context)), data.as_bytes())?;
+    };
+
+
+    // Binary file
+    (@bin, $path:expr, $rng:ident, $context:tt, $sub_path:tt, $len:tt) => {
+        let len = temp_tree!(@num, $rng, $len);
+        let data = BinRead(&mut $rng);
+        let mut file = std::fs::File::create($path.join(&temp_tree!(@ctx, $sub_path, $context)))?;
+        std::io::copy(&mut data.take(len), &mut file)?;
+    };
+
+    // Symlink to file
+    (@link, $path:expr, $rng:ident, $context:tt, $sub_path:tt, $target:expr) => {
+        {
+            let sub_path = $path.join(temp_tree!(@ctx, $sub_path, $context));
+            let target = temp_tree!(@ctx, $target, $context);
+            symlink(sub_path, target)?;
+        }
+    };
+
+    // Symlink to directory
+    (@link_dir, $path:expr, $rng:ident, $context:tt, $sub_path:tt, $target:expr) => {
+        {
+            let sub_path = $path.join(temp_tree!(@ctx, $sub_path, $context));
+            let target = temp_tree!(@ctx, $target, $context);
+            symlink_dir(sub_path, target)?;
+        }
+    };
+
+    // Loop without upper context
+    (@loop, $path:expr, $rng:ident, [], $ctx_name:ident, $nb:tt, { $($what:tt)+ }) => {
+        for $ctx_name in 0..temp_tree!(@num, $rng, $nb) {
+            temp_tree!(@instr, $path, $rng, [ $ctx_name ], $($what)+);
+        }
+    };
+    // Loop with upper context
+    (@loop, $path:expr, $rng:ident, [$($context:tt)|+], $ctx_name:ident, $nb:tt, { $($what:tt)+ }) => {
+        for $ctx_name in 0..temp_tree!(@num, $rng, $nb) {
+            temp_tree!(@instr, $path, $rng, [ $($context)|+ | $ctx_name ], $($what)+);
+        }
+    };
+
+    // -------
+    // Helpers
+    // -------
+
+    // Generate a number
+    (@num, $rng:ident, ($start:tt..$end:tt)) => {
+        $rng.gen_range($start..$end)
+    };
+    (@num, $rng:ident, $what:expr) => {
+        $what
+    };
+    (@ctx, $path:tt, []) => {
+        $path
+    };
+    (@ctx, $path:tt, [$($c:tt)|+] ) => {
+        format!($path, $($c=$c),+)
+    };
+}
+
+pub static SHARED_TEST_DIR: LazyLock<tempfile::TempDir> = LazyLock::new(|| {
+    (|| -> std::io::Result<tempfile::TempDir> {
+        temp_tree!(1, {
+            dir "sub_dir_a" {
+                text "file_2.txt" (500..1000),
+                loop  (10..50) { text "file{ctx}.txt" (500..1000) }
+            },
+            dir "sub_dir_b" {
+                loop 10 { bin "file{ctx}.bin" (5000..10000) },
+                loop 10 { link "link_to_file{ctx}" -> "file{ctx}.bin" },
+            },
+            link_dir "sub_dir_link" -> "sub_dir_b",
+            dir "empty_sub_dir" {},
+            loop dir_ctx=(1..5) {
+                dir "gen_sub_dir_{dir_ctx}" {
+                    loop (1..10) { text "gen_sub_file_{dir_ctx}_{ctx}" (500..1000)},
+                    loop (1..2) { dir "gen_sub_empty_dir_{ctx}{dir_ctx:0}" {} }
+                }
+            }
+        })
+    })()
+    .unwrap()
+});
+
+#[macro_export]
+macro_rules! cmd {
+    ("{cmd}", $command:ident, $arg:expr) => {{
+        $command.arg($arg);
+        $command
+    }};
+    ("{cmd}", $command:ident, $arg:expr, $($args:expr),+) => {{
+        $command.arg($arg);
+        cmd!("{cmd}", $command, $($args),+)
+    }};
+    ("arx", $sub_command:literal, $($args:expr),*) => {{
+        let arx_bin = env!("CARGO_BIN_EXE_arx");
+        let mut command = std::process::Command::new(&arx_bin);
+        command.env("NO_COLOR", "1");
+        cmd!("{cmd}", command, $sub_command, $($args),*)
+    }};
+    ($prog:literal, $($args:expr),*) => {{
+        let mut command = std::process::Command::new($prog);
+        cmd!("{cmd}", command, $($args),*)
+    }};
+}
+
+#[macro_export]
+macro_rules! run {
+    (status, $prog:tt, $($args:expr),+) => {
+        {
+            let mut command = cmd!($prog, $($args),+);
+            command.status()?
+        }
+    };
+    (output, $prog:tt, $($args:expr),+) => {
+        {
+            let mut command = cmd!($prog, $($args),+);
+            command.output()?
+        }
+    };
+    (spawn, $prog:tt, $($args:expr),+) => {
+        {
+            let mut command = cmd!($prog, $($args),+);
+            command.spawn()?
+        }
+    };
+}
+
+#[macro_export]
+macro_rules! temp_arx {
+    ($name:ident) => {
+        temp_arx!($name, "test.arx")
+    };
+    ($name:ident, $filename:literal) => {
+        let tmp_arx_dir = tempfile::tempdir_in(Path::new(env!("CARGO_TARGET_TMPDIR")))
+            .expect("Creating tmpdir should work");
+        let $name = tmp_arx_dir.path().join($filename);
+    };
+}
+
+#[allow(dead_code)]
+pub fn tree_equal<P: AsRef<Path>, Q: AsRef<Path>>(a: P, b: Q) -> anyhow::Result<bool> {
+    let a = a.as_ref();
+    let b = b.as_ref();
+    let output = run!(output, "diff", "-r", a, b);
+    if output.status.success() {
+        Ok(true)
+    } else {
+        println!(
+            "Diff failed between {a} and {b}:\n{err}",
+            a = a.display(),
+            b = b.display(),
+            err = String::from_utf8_lossy(&output.stderr)
+        );
+        Ok(false)
+    }
+}
+
+#[allow(dead_code)]
+pub trait CheckCommand {
+    fn check_fail(&mut self, stdout: &[u8], stderr: &[u8]);
+    fn check_output(&mut self, stdout: &[u8], stderr: &[u8]);
+    fn check(&mut self);
+}
+
+impl CheckCommand for Command {
+    fn check_output(&mut self, stdout: &[u8], stderr: &[u8]) {
+        println!("Running command {self:?}");
+        let output = self.output().expect("Running command should work.");
+        assert_eq!(
+            output.stdout,
+            stdout,
+            "Output is {}",
+            String::from_utf8_lossy(&output.stdout)
+        );
+        assert_eq!(
+            output.stderr,
+            stderr,
+            "Err is {}",
+            String::from_utf8_lossy(&output.stderr)
+        );
+        assert!(output.status.success());
+    }
+    fn check_fail(&mut self, stdout: &[u8], stderr: &[u8]) {
+        println!("Running command {self:?}");
+        let output = self.output().expect("Running command should work.");
+        assert_eq!(
+            output.stdout,
+            stdout,
+            "Output is {}",
+            String::from_utf8_lossy(&output.stdout)
+        );
+        assert_eq!(
+            output.stderr,
+            stderr,
+            "Err is {}",
+            String::from_utf8_lossy(&output.stderr)
+        );
+        assert!(!output.status.success());
+    }
+    fn check(&mut self) {
+        println!("Running command {self:?}");
+        let status = self.status().expect("Running command should work.");
+        assert!(status.success());
+    }
+}
+
+#[macro_export]
+macro_rules! join {
+    ($first:tt / $($args:tt)/+) => {
+        {
+            let mut path = $first.to_path_buf();
+            join!(@append, path, $($args),+);
+            path
+        }
+    };
+    (@append, $path:ident, $args:expr) => {
+        $path.push($args);
+    };
+    (@append, $path:ident, $args:expr, $($left:expr),+) => {
+        $path.push($args);
+        join!(@append, $path, $($left),+)
+    };
+}