Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(tests): Reduce reliance on CARGO_IS_TEST #659

Merged
merged 5 commits into from
Mar 8, 2022

Conversation

epage
Copy link
Collaborator

@epage epage commented Mar 8, 2022

Problems with CARGO_IS_TEST:

  • Reduces the fidelity of our tests
  • In switching to cargo's code, we'd need to make it work with the new registry code which was getting messy

To allow this programamtic fixture creation, I switched from trycmd to its lower level snapbox mixed with cargo-test-support.

@epage
Copy link
Collaborator Author

epage commented Mar 8, 2022

Switching to snapbox was done by:
setup.sh:

#!/usr/bin/env bash

python3 -m venv .venv
.venv/bin/pip install -r requirements.txt

requirements.txt

tomli

convert.py:

#!./.venv/bin/python3

import tomli
import pathlib


cases = []
for case_path in pathlib.Path("tests/snapshots/add").glob("*.toml"):
    case_data = tomli.loads(case_path.read_text())
    case_data["name"] = case_path.stem
    case_data["path"] = case_path
    cases.append(case_data)


test = """#![warn(rust_2018_idioms)]
#![allow(clippy::all)]
#![warn(clippy::needless_borrow)]
#![warn(clippy::redundant_clone)]

#[macro_use]
extern crate cargo_test_macro;

pub fn cargo_exe() -> &'static std::path::Path {
    snapbox::cmd::cargo_bin!("cargo-add")
}

pub fn cargo_command() -> snapbox::cmd::Command {
    snapbox::cmd::Command::new(cargo_exe())
        .with_assert(assert())
}

pub fn project_from_template(template_path: impl AsRef<std::path::Path>) -> std::path::PathBuf {
    let root = cargo_test_support::paths::root();
    let project_root = root.join("case");
    snapbox::path::copy_template(template_path.as_ref(), &project_root).unwrap();
    project_root
}

pub fn assert() -> snapbox::Assert {
    let root = cargo_test_support::paths::root().display().to_string();

    let mut subs = snapbox::Substitutions::new();
    subs.extend([
        ("[EXE]", std::borrow::Cow::Borrowed(std::env::consts::EXE_SUFFIX)),
        ("[ROOT]", std::borrow::Cow::Owned(root.into())),
    ]).unwrap();
    snapbox::Assert::new()
        .action_env(snapbox::DEFAULT_ACTION_ENV)
        .substitutions(subs)
}

"""

for case in sorted(cases, key=lambda c: c["name"]):
    remaining_args = case['args'][1:]

    assert "stdin" not in case

    stdout_path = case["path"].with_suffix(".stdout")
    stdout_path.write_text(case["stdout"])
    stderr_path = case["path"].with_suffix(".stderr")
    stderr_path.write_text(case["stderr"])

    if isinstance(case["status"], dict):
        code = case["status"]["code"]
        status_func = f"code({code})"
    elif case["status"] == "success":
        status_func = "success()"
    elif case["status"] == "failure":
        status_func = "failure()"
    else:
        raise NotImplementedError(case["status"])

    in_path = case["path"].with_suffix(".in")
    out_path = case["path"].with_suffix(".out")

    toml_cwd = case.get("fs", {}).get("cwd", None)
    if toml_cwd:
        cwd = "/".join(pathlib.Path(toml_cwd).parts[1:])
        init_cwd = f'let cwd = project_root.join("{cwd}");'
        cwd_ref = "&cwd"
    else:
        init_cwd = f"let cwd = &project_root;"
        cwd_ref = "cwd"

    name = case["name"]
    test += f'#[cargo_test]\n'
    if not case.get("env"):
        test += f'#[cfg(feature = "test-external-apis")]\n'
    test += f'fn {name}() {{\n'
    test += f'    let project_root = project_from_template("{in_path}");\n'
    test += f'    {init_cwd}\n'
    test += f'\n'
    test += f'    cargo_command()\n'
    test += f'        .arg("add")\n'
    if remaining_args:
        args = f"{remaining_args!r}".replace("'", '"')
        test += f'        .args({args})\n'
    if case.get("env"):
        test += f'        .env("CARGO_IS_TEST", "1")\n'
    test += f'        .current_dir({cwd_ref})\n'
    test += f'        .assert()\n'
    test += f'        .{status_func}\n'
    test += f'        .stdout_matches_path("{stdout_path}")\n'
    test += f'        .stderr_matches_path("{stderr_path}");\n'
    test += f'\n'
    test += f'    assert()\n'
    test += f'        .subset_matches("{out_path}", &project_root);\n'
    test += f'}}\n'
    test += f'\n'

pathlib.Path("tests/cargo-add.rs").write_text(test)

@epage epage force-pushed the snap branch 2 times, most recently from b884793 to b882236 Compare March 8, 2022 18:54
epage added 4 commits March 8, 2022 12:58
My hope is this will give us the flexibility we need to still get most
of the benefits of trycmd while allowing to use `cargo-test-support`s
registry logic so we can remove CARGO_IS_TEST.
This reduces the need for `CARGO_IS_TEST`
- Increases fidelity
- Allows us to more easily switch to cargo's logic
@epage

This comment was marked as outdated.

@epage epage changed the title Snap refactor(tests): Reduce reliance on CARGO_IS_TEST Mar 8, 2022
@epage epage marked this pull request as ready for review March 8, 2022 21:14
@epage epage merged commit 7c844b8 into killercup:merge-add Mar 8, 2022
@epage epage deleted the snap branch March 8, 2022 21:17
@epage epage mentioned this pull request Apr 20, 2022
14 tasks
@epage
Copy link
Collaborator Author

epage commented Jul 27, 2022

Updated version of convert.py

#!./.venv/bin/python3

import shutil
import tomli
import pathlib

TOOL = "upgrade"
TOOL_ROOT = pathlib.Path(f"tests/cargo-{TOOL}")
TOOL_ROOT.mkdir(parents=True, exist_ok=True)


cases = []
for case_path in pathlib.Path(f"tests/cmd/{TOOL}").glob("*.toml"):
    case_data = tomli.loads(case_path.read_text())
    case_data["name"] = case_path.stem
    case_data["path"] = case_path
    cases.append(case_data)


main_text = """
#![allow(clippy::all)]
#![warn(clippy::needless_borrow)]
#![warn(clippy::redundant_clone)]

#[macro_use]
extern crate cargo_test_macro;

"""
for case in sorted(cases, key=lambda c: c["name"]):
    name = case["name"]
    main_text += f"mod {name};\n"
main_text += """

fn init_registry() {
    cargo_test_support::registry::init();
    add_registry_packages(false);
}

fn init_alt_registry() {
    cargo_test_support::registry::alt_init();
    add_registry_packages(true);
}

fn add_registry_packages(alt: bool) {
    for name in [
        "my-package",
        "my-package1",
        "my-package2",
        "my-dev-package1",
        "my-dev-package2",
        "my-build-package1",
        "my-build-package2",
        "toml",
        "versioned-package",
        "cargo-list-test-fixture-dependency",
        "unrelateed-crate",
    ] {
        cargo_test_support::registry::Package::new(name, "0.1.1+my-package")
            .alternative(alt)
            .publish();
        cargo_test_support::registry::Package::new(name, "0.2.0+my-package")
            .alternative(alt)
            .publish();
        cargo_test_support::registry::Package::new(name, "0.2.3+my-package")
            .alternative(alt)
            .publish();
        cargo_test_support::registry::Package::new(name, "0.4.1+my-package")
            .alternative(alt)
            .publish();
        cargo_test_support::registry::Package::new(name, "20.0.0+my-package")
            .alternative(alt)
            .publish();
        cargo_test_support::registry::Package::new(name, "99999.0.0+my-package")
            .alternative(alt)
            .publish();
        cargo_test_support::registry::Package::new(name, "99999.0.0-alpha.1+my-package")
            .alternative(alt)
            .publish();
    }

    cargo_test_support::registry::Package::new("prerelease_only", "0.2.0-alpha.1")
        .alternative(alt)
        .publish();
    cargo_test_support::registry::Package::new("test_breaking", "0.2.0")
        .alternative(alt)
        .publish();
    cargo_test_support::registry::Package::new("test_nonbreaking", "0.1.1")
        .alternative(alt)
        .publish();

    // Normalization
    cargo_test_support::registry::Package::new("linked-hash-map", "0.5.4")
        .alternative(alt)
        .feature("clippy", &[])
        .feature("heapsize", &[])
        .feature("heapsize_impl", &[])
        .feature("nightly", &[])
        .feature("serde", &[])
        .feature("serde_impl", &[])
        .feature("serde_test", &[])
        .publish();
    cargo_test_support::registry::Package::new("inflector", "0.11.4")
        .alternative(alt)
        .feature("default", &["heavyweight", "lazy_static", "regex"])
        .feature("heavyweight", &[])
        .feature("lazy_static", &[])
        .feature("regex", &[])
        .feature("unstable", &[])
        .publish();

    cargo_test_support::registry::Package::new("your-face", "99999.0.0+my-package")
        .alternative(alt)
        .feature("nose", &[])
        .feature("mouth", &[])
        .feature("eyes", &[])
        .feature("ears", &[])
        .publish();
}

pub fn cargo_exe() -> std::path::PathBuf {
    snapbox::cmd::cargo_bin("cargo-upgrade")
}

/// Test the cargo command
pub trait CargoCommand {
    fn cargo_ui() -> Self;
}

impl CargoCommand for snapbox::cmd::Command {
    fn cargo_ui() -> Self {
        use cargo_test_support::TestEnv;
        Self::new(cargo_exe())
            .with_assert(cargo_test_support::compare::assert_ui())
            .test_env()
    }
}
"""
(TOOL_ROOT / "main.rs").write_text(main_text)
if pathlib.Path(f"tests/cargo-{TOOL}.rs").exists():
    pathlib.Path(f"tests/cargo-{TOOL}.rs").unlink()


for case in sorted(cases, key=lambda c: c["name"]):
    remaining_args = case['args'][1:]

    assert "stdin" not in case

    name = case["name"]
    case_root = TOOL_ROOT / name
    case_root.mkdir(parents=True, exist_ok=True)
    stdout_path = case_root / "stdout.log"
    stdout_path.write_text(case["stdout"])
    stderr_path = case_root / "stderr.log"
    stderr_path.write_text(case["stderr"])

    if isinstance(case["status"], dict):
        code = case["status"]["code"]
        status_func = f"code({code})"
    elif case["status"] == "success":
        status_func = "success()"
    elif case["status"] == "failure":
        status_func = "failure()"
    elif case["status"] == "failed":
        status_func = "failure()"
    else:
        raise NotImplementedError(case["status"])

    old_in_path = case["path"].with_suffix(".in")
    in_path = case_root / "in"
    shutil.copytree(old_in_path, in_path)
    old_out_path = case["path"].with_suffix(".out")
    out_path = case_root / "out"
    shutil.copytree(old_out_path, out_path)

    toml_cwd = case.get("fs", {}).get("cwd", None)
    if toml_cwd:
        cwd = "/".join(pathlib.Path(toml_cwd).parts[1:])
        init_cwd = f'let cwd = project_root.join("{cwd}");'
        cwd_ref = "&cwd"
    else:
        init_cwd = f"let cwd = &project_root;"
        cwd_ref = "cwd"

    test = """use cargo_test_support::compare::assert_ui;
use cargo_test_support::Project;

use crate::init_registry;
use crate::CargoCommand;
use cargo_test_support::curr_dir;

#[cargo_test]
fn case() {
    init_registry();
    let project = Project::from_template(curr_dir!().join("in"));
    let project_root = project.root();
"""
    test += f'    {init_cwd}\n'
    test += f'\n'
    test += f'    snapbox::cmd::Command::cargo_ui()\n'
    test += f'        .arg("upgrade")\n'
    if remaining_args:
        args = f"{remaining_args!r}".replace("'", '"')
        test += f'        .args({args})\n'
    test += f'        .current_dir({cwd_ref})\n'
    test += f'        .assert()\n'
    test += f'        .{status_func}\n'
    test += f'        .stdout_matches_path(curr_dir!().join("stdout.log"))\n'
    test += f'        .stderr_matches_path(curr_dir!().join("stderr.log"));\n'
    test += f'\n'
    test += f'    assert_ui()\n'
    test += f'        .subset_matches(curr_dir!().join("out"), &project_root);\n'
    test += f'}}\n'
    test += f'\n'

    (case_root / "mod.rs").write_text(test)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant