flub / testdir

Semi-persistent, scoped test directories
Apache License 2.0
8 stars 3 forks source link

Support for cargo-nextest #4

Closed flub closed 11 months ago

flub commented 1 year ago

When using cargo-nextest the detection for being in the same testrun is probably broken.

hellertime commented 1 year ago

Just leaving a 👍 for this. Really enjoying using testdir, but nexttest definitely causes some issues with paths disappearing unexpectedly!

hellertime commented 1 year ago

I ended up copying a bunch of the logic out of testdir into my work, to fast track a nextest compatible version of testdir.

This is mainly to serve as an example, and would require a different approach for integrating the changes into testdir proper.

At its core, there is a second reusefn needed to support nextest, and a test against the NEXTEST env var to decide when to use it:

pub(crate) mod nextestdir {
    use std::path::Path;

    const NEXTEST_RUN_ID_FILE_NAME: &str = "nextest-run-id";

    pub(crate) fn reuse_nextest(dir: &Path) -> bool {
        let file_name = dir.join(NEXTEST_RUN_ID_FILE_NAME);
        if let Ok(content) = std::fs::read_to_string(file_name) {
            if let Ok(read_nextest_run_id) = content.parse::<String>() {
                if let Ok(nextest_run_id) = std::env::var("NEXTEST_RUN_ID") {
                    return read_nextest_run_id == nextest_run_id;
                }
            }
        }
        false
    }

    pub(crate) fn create_nextest_run_id_file(dir: &Path) {
        if let Ok(nextest_run_id) = std::env::var("NEXTEST_RUN_ID") {
            let file_name = dir.join(NEXTEST_RUN_ID_FILE_NAME);
            if !file_name.exists() {
                std::fs::write(&file_name, nextest_run_id.to_string())
                    .expect("Failed to write nextest run ID");
            }
        }
    }

    pub(crate) fn with_nextesdir<F, R>(func: F) -> R
    where
        F: FnOnce(&testdir::NumberedDir) -> R,
    {
        let test_dir = testdir::TESTDIR.get_or_init(|| {
            let mut builder =
                testdir::NumberedDirBuilder::new(String::from("init_testdir-not-called"));
            builder.reusefn(reuse_nextest);
            let testdir = builder.create().expect("Failed to create testdir");
            create_nextest_run_id_file(testdir.path());
            testdir
        });
        func(test_dir)
    }
}

/// Specialized version of testdir::init_testdir!() that will work with nextest
macro_rules! init_nextestdir {
    () => {{
        testdir::TESTDIR.get_or_init(move || {
            let parent = match testdir::private::cargo_metadata::MetadataCommand::new().exec() {
                Ok(metadata) => metadata.target_directory.into(),
                Err(_) => {
                    // In some environments cargo-metadata is not available,
                    // e.g. cargo-dinghy.  Use the directory of test executable.
                    let current_exe = ::std::env::current_exe().expect("no current exe");
                    current_exe
                        .parent()
                        .expect("no parent dir for current exe")
                        .into()
                }
            };
            let pkg_name = "testdir";
            let mut builder = testdir::NumberedDirBuilder::new(pkg_name.to_string());
            builder.set_parent(parent);
            builder.reusefn(crate::nextestdir::reuse_nextest);
            let testdir = builder.create().expect("Failed to create testdir");
            crate::nextestdir::create_nextest_run_id_file(testdir.path());
            testdir
        })
    }};
}

/// Specialized version of testdir::testdir!() that will work with nextest
macro_rules! nextestdir {
    () => {{
        init_nextestdir!();
        let module_path = ::std::module_path!();
        let test_name = testdir::private::extract_test_name(&module_path);
        let subdir_path = ::std::path::Path::new(&module_path.replace("::", "/")).join(&test_name);
        crate::nextestdir::with_nextesdir(move |tdir| {
            tdir.create_subdir(subdir_path)
                .expect("Failed to create test-scope sub-directory")
        })
    }};
}

/// Compatible layer to give us either testdir!() or nextestdir!()
macro_rules! testdir_compat {
  () => {{
      let is_nextest = if let Ok(nextest) = std::env::var("NEXTEST") {
          if let Ok(read_nextest) = nextest.parse::<i32>() {
              read_nextest == 1
           } else {
               false
           }
       } else {
            false
       };
       if is_nextest {
            nextestdir!()
        } else {
            testdir::testdir!()
        }
    }};
}
flub commented 1 year ago

awesome! thanks for these pointers, I still haven't managed to find time to work on this but this certainly will speed things up!

(if you feel like making a PR out of this that'd also be cool ;) but no worries if not)

hellertime commented 1 year ago

I had originally planned implement this in a fork of testdir, before I realized everything I needed was public and could be re-implemented.

If you'd be OK with the approach that tries to detect if its running under nextest and falls back to the default test runner approach if not, I'd be OK to work this into a PR.

flub commented 11 months ago

@hellertime

Apologies for taking so long to come back to this. I've found some time to wrap my head around testdir again and look at this issue.

To my surprise I ended up making just a tiny change: https://github.com/flub/testdir/pull/7

I haven't tested it that widely yet, so maybe I missed something. I would be grateful if you could check out that approach and see if it works for your situation.