//! Tests for checking behavior of old cargos.
//!
//! These tests are ignored because it is intended to be run on a developer
//! system with a bunch of toolchains installed. This requires `rustup` to be
//! installed. It will iterate over installed toolchains, and run some tests
//! over each one, producing a report at the end. As of this writing, I have
//! tested 1.0 to 1.51. Run this with:
//!
//! ```console
//! cargo test --test testsuite -- old_cargos --nocapture --ignored
//! ```

use cargo::CargoResult;
use cargo_test_support::paths::CargoPathExt;
use cargo_test_support::registry::{self, Dependency, Package};
use cargo_test_support::{cargo_exe, execs, paths, process, project, rustc_host};
use cargo_util::{ProcessBuilder, ProcessError};
use semver::Version;
use std::fs;

fn tc_process(cmd: &str, toolchain: &str) -> ProcessBuilder {
    let mut p = if toolchain == "this" {
        if cmd == "cargo" {
            process(&cargo_exe())
        } else {
            process(cmd)
        }
    } else {
        let mut cmd = process(cmd);
        cmd.arg(format!("+{}", toolchain));
        cmd
    };
    // Reset PATH since `process` modifies it to remove rustup.
    p.env("PATH", std::env::var_os("PATH").unwrap());
    p
}

/// Returns a sorted list of all toolchains.
///
/// The returned value includes the parsed version, and the rustup toolchain
/// name as a string.
fn collect_all_toolchains() -> Vec<(Version, String)> {
    let rustc_version = |tc| {
        let mut cmd = tc_process("rustc", tc);
        cmd.arg("-V");
        let output = cmd.exec_with_output().expect("rustc installed");
        let version = std::str::from_utf8(&output.stdout).unwrap();
        let parts: Vec<_> = version.split_whitespace().collect();
        assert_eq!(parts[0], "rustc");
        assert!(parts[1].starts_with("1."));
        Version::parse(parts[1]).expect("valid version")
    };

    // Provide a way to override the list.
    if let Ok(tcs) = std::env::var("OLD_CARGO") {
        return tcs
            .split(',')
            .map(|tc| (rustc_version(tc), tc.to_string()))
            .collect();
    }

    let host = rustc_host();
    // I tend to have lots of toolchains installed, but I don't want to test
    // all of them (like dated nightlies, or toolchains for non-host targets).
    let valid_names = &[
        format!("stable-{}", host),
        format!("beta-{}", host),
        format!("nightly-{}", host),
    ];

    let output = ProcessBuilder::new("rustup")
        .args(&["toolchain", "list"])
        .exec_with_output()
        .expect("rustup should be installed");
    let stdout = std::str::from_utf8(&output.stdout).unwrap();
    let mut toolchains: Vec<_> = stdout
        .lines()
        .map(|line| {
            // Some lines say things like (default), just get the version.
            line.split_whitespace().next().expect("non-empty line")
        })
        .filter(|line| {
            line.ends_with(&host)
                && (line.starts_with("1.") || valid_names.iter().any(|name| name == line))
        })
        .map(|line| (rustc_version(line), line.to_string()))
        .collect();

    // Also include *this* cargo.
    toolchains.push((rustc_version("this"), "this".to_string()));
    toolchains.sort_by(|a, b| a.0.cmp(&b.0));
    toolchains
}

// This is a test for exercising the behavior of older versions of cargo with
// the new feature syntax.
//
// The test involves a few dependencies with different feature requirements:
//
// * `bar` 1.0.0 is the base version that does not use the new syntax.
// * `bar` 1.0.1 has a feature with the new syntax, but the feature is unused.
//   The optional dependency `new-baz-dep` should not be activated.
// * `bar` 1.0.2 has a dependency on `baz` that *requires* the new feature
//   syntax.
#[ignore]
#[cargo_test]
fn new_features() {
    if std::process::Command::new("rustup").output().is_err() {
        panic!("old_cargos requires rustup to be installed");
    }
    Package::new("new-baz-dep", "1.0.0").publish();

    Package::new("baz", "1.0.0").publish();
    let baz101_cksum = Package::new("baz", "1.0.1")
        .add_dep(Dependency::new("new-baz-dep", "1.0").optional(true))
        .feature("new-feat", &["dep:new-baz-dep"])
        .publish();

    let bar100_cksum = Package::new("bar", "1.0.0")
        .add_dep(Dependency::new("baz", "1.0").optional(true))
        .feature("feat", &["baz"])
        .publish();
    let bar101_cksum = Package::new("bar", "1.0.1")
        .add_dep(Dependency::new("baz", "1.0").optional(true))
        .feature("feat", &["dep:baz"])
        .publish();
    let bar102_cksum = Package::new("bar", "1.0.2")
        .add_dep(Dependency::new("baz", "1.0").enable_features(&["new-feat"]))
        .publish();

    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                version = "0.1.0"

                [dependencies]
                bar = "1.0"
            "#,
        )
        .file("src/lib.rs", "")
        .build();

    let lock_bar_to = |toolchain_version: &Version, bar_version| {
        let lock = if toolchain_version < &Version::new(1, 12, 0) {
            let url = registry::registry_url();
            match bar_version {
                100 => format!(
                    r#"
                        [root]
                        name = "foo"
                        version = "0.1.0"
                        dependencies = [
                         "bar 1.0.0 (registry+{url})",
                        ]

                        [[package]]
                        name = "bar"
                        version = "1.0.0"
                        source = "registry+{url}"
                    "#,
                    url = url
                ),
                101 => format!(
                    r#"
                        [root]
                        name = "foo"
                        version = "0.1.0"
                        dependencies = [
                         "bar 1.0.1 (registry+{url})",
                        ]

                        [[package]]
                        name = "bar"
                        version = "1.0.1"
                        source = "registry+{url}"
                    "#,
                    url = url
                ),
                102 => format!(
                    r#"
                        [root]
                        name = "foo"
                        version = "0.1.0"
                        dependencies = [
                         "bar 1.0.2 (registry+{url})",
                        ]

                        [[package]]
                        name = "bar"
                        version = "1.0.2"
                        source = "registry+{url}"
                        dependencies = [
                         "baz 1.0.1 (registry+{url})",
                        ]

                        [[package]]
                        name = "baz"
                        version = "1.0.1"
                        source = "registry+{url}"
                    "#,
                    url = url
                ),
                _ => panic!("unexpected version"),
            }
        } else {
            match bar_version {
                100 => format!(
                    r#"
                        [root]
                        name = "foo"
                        version = "0.1.0"
                        dependencies = [
                         "bar 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
                        ]

                        [[package]]
                        name = "bar"
                        version = "1.0.0"
                        source = "registry+https://github.com/rust-lang/crates.io-index"

                        [metadata]
                        "checksum bar 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "{}"
                    "#,
                    bar100_cksum
                ),
                101 => format!(
                    r#"
                        [root]
                        name = "foo"
                        version = "0.1.0"
                        dependencies = [
                         "bar 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
                        ]

                        [[package]]
                        name = "bar"
                        version = "1.0.1"
                        source = "registry+https://github.com/rust-lang/crates.io-index"

                        [metadata]
                        "checksum bar 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "{}"
                    "#,
                    bar101_cksum
                ),
                102 => format!(
                    r#"
                        [root]
                        name = "foo"
                        version = "0.1.0"
                        dependencies = [
                         "bar 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
                        ]

                        [[package]]
                        name = "bar"
                        version = "1.0.2"
                        source = "registry+https://github.com/rust-lang/crates.io-index"
                        dependencies = [
                         "baz 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
                        ]

                        [[package]]
                        name = "baz"
                        version = "1.0.1"
                        source = "registry+https://github.com/rust-lang/crates.io-index"

                        [metadata]
                        "checksum bar 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "{bar102_cksum}"
                        "checksum baz 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "{baz101_cksum}"
                    "#,
                    bar102_cksum = bar102_cksum,
                    baz101_cksum = baz101_cksum
                ),
                _ => panic!("unexpected version"),
            }
        };
        p.change_file("Cargo.lock", &lock);
    };

    let toolchains = collect_all_toolchains();

    let config_path = paths::home().join(".cargo/config");
    let lock_path = p.root().join("Cargo.lock");

    struct ToolchainBehavior {
        bar: Option<Version>,
        baz: Option<Version>,
        new_baz_dep: Option<Version>,
    }

    // Collect errors to print at the end. One entry per toolchain, a list of
    // strings to print.
    let mut unexpected_results: Vec<Vec<String>> = Vec::new();

    for (version, toolchain) in &toolchains {
        let mut tc_result = Vec::new();
        // Write a config appropriate for this version.
        if version < &Version::new(1, 12, 0) {
            fs::write(
                &config_path,
                format!(
                    r#"
                        [registry]
                        index = "{}"
                    "#,
                    registry::registry_url()
                ),
            )
            .unwrap();
        } else {
            fs::write(
                &config_path,
                format!(
                    "
                        [source.crates-io]
                        registry = 'https://wut'  # only needed by 1.12
                        replace-with = 'dummy-registry'

                        [source.dummy-registry]
                        registry = '{}'
                    ",
                    registry::registry_url()
                ),
            )
            .unwrap();
        }

        // Fetches the version of a package in the lock file.
        let pkg_version = |pkg| -> Option<Version> {
            let output = tc_process("cargo", toolchain)
                .args(&["pkgid", pkg])
                .cwd(p.root())
                .exec_with_output()
                .ok()?;
            let stdout = std::str::from_utf8(&output.stdout).unwrap();
            let version = stdout
                .trim()
                .rsplitn(2, ':')
                .next()
                .expect("version after colon");
            Some(Version::parse(version).expect("parseable version"))
        };

        // Runs `cargo build` and returns the versions selected in the lock.
        let run_cargo = || -> CargoResult<ToolchainBehavior> {
            match tc_process("cargo", toolchain)
                .args(&["build", "--verbose"])
                .cwd(p.root())
                .exec_with_output()
            {
                Ok(_output) => {
                    eprintln!("{} ok", toolchain);
                    let bar = pkg_version("bar");
                    let baz = pkg_version("baz");
                    let new_baz_dep = pkg_version("new-baz-dep");
                    Ok(ToolchainBehavior {
                        bar,
                        baz,
                        new_baz_dep,
                    })
                }
                Err(e) => {
                    eprintln!("{} err {}", toolchain, e);
                    Err(e)
                }
            }
        };

        macro_rules! check_lock {
            ($tc_result:ident, $pkg:expr, $which:expr, $actual:expr, None) => {
                check_lock!(= $tc_result, $pkg, $which, $actual, None);
            };
            ($tc_result:ident, $pkg:expr, $which:expr, $actual:expr, $expected:expr) => {
                check_lock!(= $tc_result, $pkg, $which, $actual, Some(Version::parse($expected).unwrap()));
            };
            (= $tc_result:ident, $pkg:expr, $which:expr, $actual:expr, $expected:expr) => {
                let exp: Option<Version> = $expected;
                if $actual != $expected {
                    $tc_result.push(format!(
                        "{} for {} saw {:?} but expected {:?}",
                        $which, $pkg, $actual, exp
                    ));
                }
            };
        }

        let check_err_contains = |tc_result: &mut Vec<_>, err: anyhow::Error, contents| {
            if let Some(ProcessError {
                stderr: Some(stderr),
                ..
            }) = err.downcast_ref::<ProcessError>()
            {
                let stderr = std::str::from_utf8(stderr).unwrap();
                if !stderr.contains(contents) {
                    tc_result.push(format!(
                        "{} expected to see error contents:\n{}\nbut saw:\n{}",
                        toolchain, contents, stderr
                    ));
                }
            } else {
                panic!("{} unexpected error {}", toolchain, err);
            }
        };

        // Unlocked behavior.
        let which = "unlocked";
        lock_path.rm_rf();
        p.build_dir().rm_rf();
        match run_cargo() {
            Ok(behavior) => {
                // TODO: Switch to 51 after backport.
                if version < &Version::new(1, 52, 0) && toolchain != "this" {
                    check_lock!(tc_result, "bar", which, behavior.bar, "1.0.2");
                    check_lock!(tc_result, "baz", which, behavior.baz, "1.0.1");
                    check_lock!(tc_result, "new-baz-dep", which, behavior.new_baz_dep, None);
                } else {
                    check_lock!(tc_result, "bar", which, behavior.bar, "1.0.0");
                    check_lock!(tc_result, "baz", which, behavior.baz, None);
                    check_lock!(tc_result, "new-baz-dep", which, behavior.new_baz_dep, None);
                }
            }
            Err(e) => {
                tc_result.push(format!("unlocked build failed: {}", e));
            }
        }

        let which = "locked bar 1.0.0";
        lock_bar_to(version, 100);
        match run_cargo() {
            Ok(behavior) => {
                check_lock!(tc_result, "bar", which, behavior.bar, "1.0.0");
                check_lock!(tc_result, "baz", which, behavior.baz, None);
                check_lock!(tc_result, "new-baz-dep", which, behavior.new_baz_dep, None);
            }
            Err(e) => {
                tc_result.push(format!("bar 1.0.0 locked build failed: {}", e));
            }
        }

        let which = "locked bar 1.0.1";
        lock_bar_to(version, 101);
        match run_cargo() {
            Ok(behavior) => {
                check_lock!(tc_result, "bar", which, behavior.bar, "1.0.1");
                check_lock!(tc_result, "baz", which, behavior.baz, None);
                check_lock!(tc_result, "new-baz-dep", which, behavior.new_baz_dep, None);
            }
            Err(e) => {
                if toolchain == "this" {
                    // 1.0.1 can't be used without -Znamespaced-features
                    // It gets filtered out of the index.
                    check_err_contains(&mut tc_result, e,
                        "error: failed to select a version for the requirement `bar = \"=1.0.1\"`\n\
                        candidate versions found which didn't match: 1.0.2, 1.0.0"
                    );
                } else {
                    tc_result.push(format!("bar 1.0.1 locked build failed: {}", e));
                }
            }
        }

        let which = "locked bar 1.0.2";
        lock_bar_to(version, 102);
        match run_cargo() {
            Ok(behavior) => {
                check_lock!(tc_result, "bar", which, behavior.bar, "1.0.2");
                check_lock!(tc_result, "baz", which, behavior.baz, "1.0.1");
                check_lock!(tc_result, "new-baz-dep", which, behavior.new_baz_dep, None);
            }
            Err(e) => {
                if toolchain == "this" {
                    // baz can't lock to 1.0.1, it requires -Znamespaced-features
                    check_err_contains(&mut tc_result, e,
                        "error: failed to select a version for the requirement `baz = \"=1.0.1\"`\n\
                        candidate versions found which didn't match: 1.0.0"
                    );
                } else {
                    tc_result.push(format!("bar 1.0.2 locked build failed: {}", e));
                }
            }
        }

        unexpected_results.push(tc_result);
    }

    // Generate a report.
    let mut has_err = false;
    for ((tc_vers, tc_name), errs) in toolchains.iter().zip(unexpected_results) {
        if errs.is_empty() {
            continue;
        }
        eprintln!("error: toolchain {} (version {}):", tc_name, tc_vers);
        for err in errs {
            eprintln!("  {}", err);
        }
        has_err = true;
    }
    if has_err {
        panic!("at least one toolchain did not run as expected");
    }
}

#[cargo_test]
#[ignore]
fn index_cache_rebuild() {
    // Checks that the index cache gets rebuilt.
    //
    // 1.48 will not cache entries with features with the same name as a
    // dependency. If the cache does not get rebuilt, then running with
    // `-Znamespaced-features` would prevent the new cargo from seeing those
    // entries. The index cache version was changed to prevent this from
    // happening, and switching between versions should work correctly
    // (although it will thrash the cash, that's better than not working
    // correctly.
    Package::new("baz", "1.0.0").publish();
    Package::new("bar", "1.0.0").publish();
    Package::new("bar", "1.0.1")
        .add_dep(Dependency::new("baz", "1.0").optional(true))
        .feature("baz", &["dep:baz"])
        .publish();

    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                version = "0.1.0"

                [dependencies]
                bar = "1.0"
            "#,
        )
        .file("src/lib.rs", "")
        .build();

    // This version of Cargo errors on index entries that have overlapping
    // feature names, so 1.0.1 will be missing.
    execs()
        .with_process_builder(tc_process("cargo", "1.48.0"))
        .arg("check")
        .cwd(p.root())
        .with_stderr(
            "\
[UPDATING] [..]
[DOWNLOADING] crates ...
[DOWNLOADED] bar v1.0.0 [..]
[CHECKING] bar v1.0.0
[CHECKING] foo v0.1.0 [..]
[FINISHED] [..]
",
        )
        .run();

    fs::remove_file(p.root().join("Cargo.lock")).unwrap();

    // This should rebuild the cache and use 1.0.1.
    p.cargo("check -Znamespaced-features")
        .masquerade_as_nightly_cargo()
        .with_stderr(
            "\
[UPDATING] [..]
[DOWNLOADING] crates ...
[DOWNLOADED] bar v1.0.1 [..]
[CHECKING] bar v1.0.1
[CHECKING] foo v0.1.0 [..]
[FINISHED] [..]
",
        )
        .run();

    fs::remove_file(p.root().join("Cargo.lock")).unwrap();

    // Verify 1.48 can still resolve, and is at 1.0.0.
    execs()
        .with_process_builder(tc_process("cargo", "1.48.0"))
        .arg("tree")
        .cwd(p.root())
        .with_stdout(
            "\
foo v0.1.0 [..]
└── bar v1.0.0
",
        )
        .run();
}

#[cargo_test]
#[ignore]
fn avoids_split_debuginfo_collision() {
    // Checks for a bug where .o files were being incorrectly shared between
    // different toolchains using incremental and split-debuginfo on macOS.
    let p = project()
        .file(
            "Cargo.toml",
            r#"
                [package]
                name = "foo"
                version = "0.1.0"

                [profile.dev]
                split-debuginfo = "unpacked"
            "#,
        )
        .file("src/main.rs", "fn main() {}")
        .build();

    execs()
        .with_process_builder(tc_process("cargo", "stable"))
        .arg("build")
        .env("CARGO_INCREMENTAL", "1")
        .cwd(p.root())
        .with_stderr(
            "\
[COMPILING] foo v0.1.0 [..]
[FINISHED] [..]
",
        )
        .run();

    p.cargo("build")
        .env("CARGO_INCREMENTAL", "1")
        .with_stderr(
            "\
[COMPILING] foo v0.1.0 [..]
[FINISHED] [..]
",
        )
        .run();

    execs()
        .with_process_builder(tc_process("cargo", "stable"))
        .arg("build")
        .env("CARGO_INCREMENTAL", "1")
        .cwd(p.root())
        .with_stderr(
            "\
[COMPILING] foo v0.1.0 [..]
[FINISHED] [..]
",
        )
        .run();
}
