cargo/tests/testsuite/features2.rs
2021-07-30 15:40:37 -07:00

2400 lines
63 KiB
Rust

//! Tests for the new feature resolver.
use cargo_test_support::cross_compile::{self, alternate};
use cargo_test_support::install::cargo_home;
use cargo_test_support::paths::CargoPathExt;
use cargo_test_support::publish::validate_crate_contents;
use cargo_test_support::registry::{Dependency, Package};
use cargo_test_support::{basic_manifest, cargo_process, project, rustc_host, Project};
use std::fs::File;
/// Switches Cargo.toml to use `resolver = "2"`.
pub fn switch_to_resolver_2(p: &Project) {
let mut manifest = p.read_file("Cargo.toml");
if manifest.contains("resolver =") {
panic!("did not expect manifest to already contain a resolver setting");
}
if let Some(index) = manifest.find("[workspace]\n") {
manifest.insert_str(index + 12, "resolver = \"2\"\n");
} else if let Some(index) = manifest.find("[package]\n") {
manifest.insert_str(index + 10, "resolver = \"2\"\n");
} else {
panic!("expected [package] or [workspace] in manifest");
}
p.change_file("Cargo.toml", &manifest);
}
#[cargo_test]
fn inactivate_targets() {
// Basic test of `itarget`. A shared dependency where an inactive [target]
// changes the features.
Package::new("common", "1.0.0")
.feature("f1", &[])
.file(
"src/lib.rs",
r#"
#[cfg(feature = "f1")]
compile_error!("f1 should not activate");
"#,
)
.publish();
Package::new("bar", "1.0.0")
.add_dep(
Dependency::new("common", "1.0")
.target("cfg(whatever)")
.enable_features(&["f1"]),
)
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
common = "1.0"
bar = "1.0"
"#,
)
.file("src/lib.rs", "")
.build();
p.cargo("check")
.with_status(101)
.with_stderr_contains("[..]f1 should not activate[..]")
.run();
switch_to_resolver_2(&p);
p.cargo("check").run();
}
#[cargo_test]
fn inactive_target_optional() {
// Activating optional [target] dependencies for inactivate target.
Package::new("common", "1.0.0")
.feature("f1", &[])
.feature("f2", &[])
.feature("f3", &[])
.feature("f4", &[])
.file(
"src/lib.rs",
r#"
pub fn f() {
if cfg!(feature="f1") { println!("f1"); }
if cfg!(feature="f2") { println!("f2"); }
if cfg!(feature="f3") { println!("f3"); }
if cfg!(feature="f4") { println!("f4"); }
}
"#,
)
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
edition = "2018"
[dependencies]
common = "1.0"
[target.'cfg(whatever)'.dependencies]
dep1 = {path='dep1', optional=true}
dep2 = {path='dep2', optional=true, features=["f3"]}
common = {version="1.0", optional=true, features=["f4"]}
[features]
foo1 = ["dep1/f2"]
foo2 = ["dep2"]
"#,
)
.file(
"src/main.rs",
r#"
fn main() {
if cfg!(feature="foo1") { println!("foo1"); }
if cfg!(feature="foo2") { println!("foo2"); }
if cfg!(feature="dep1") { println!("dep1"); }
if cfg!(feature="dep2") { println!("dep2"); }
if cfg!(feature="common") { println!("common"); }
common::f();
}
"#,
)
.file(
"dep1/Cargo.toml",
r#"
[package]
name = "dep1"
version = "0.1.0"
[dependencies]
common = {version="1.0", features=["f1"]}
[features]
f2 = ["common/f2"]
"#,
)
.file(
"dep1/src/lib.rs",
r#"compile_error!("dep1 should not build");"#,
)
.file(
"dep2/Cargo.toml",
r#"
[package]
name = "dep2"
version = "0.1.0"
[dependencies]
common = "1.0"
[features]
f3 = ["common/f3"]
"#,
)
.file(
"dep2/src/lib.rs",
r#"compile_error!("dep2 should not build");"#,
)
.build();
p.cargo("run --all-features")
.with_stdout("foo1\nfoo2\ndep1\ndep2\ncommon\nf1\nf2\nf3\nf4\n")
.run();
p.cargo("run --features dep1")
.with_stdout("dep1\nf1\n")
.run();
p.cargo("run --features foo1")
.with_stdout("foo1\ndep1\nf1\nf2\n")
.run();
p.cargo("run --features dep2")
.with_stdout("dep2\nf3\n")
.run();
p.cargo("run --features common")
.with_stdout("common\nf4\n")
.run();
switch_to_resolver_2(&p);
p.cargo("run --all-features")
.with_stdout("foo1\nfoo2\ndep1\ndep2\ncommon")
.run();
p.cargo("run --features dep1").with_stdout("dep1\n").run();
p.cargo("run --features foo1").with_stdout("foo1\n").run();
p.cargo("run --features dep2").with_stdout("dep2\n").run();
p.cargo("run --features common").with_stdout("common").run();
}
#[cargo_test]
fn itarget_proc_macro() {
// itarget inside a proc-macro while cross-compiling
if cross_compile::disabled() {
return;
}
Package::new("hostdep", "1.0.0").publish();
Package::new("pm", "1.0.0")
.proc_macro(true)
.target_dep("hostdep", "1.0", rustc_host())
.file("src/lib.rs", "extern crate hostdep;")
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
pm = "1.0"
"#,
)
.file("src/lib.rs", "")
.build();
// Old behavior
p.cargo("check").run();
p.cargo("check --target").arg(alternate()).run();
// New behavior
switch_to_resolver_2(&p);
p.cargo("check").run();
p.cargo("check --target").arg(alternate()).run();
// For good measure, just make sure things don't break.
p.cargo("check --target").arg(alternate()).run();
}
#[cargo_test]
fn decouple_host_deps() {
// Basic test for `host_dep` decouple.
Package::new("common", "1.0.0")
.feature("f1", &[])
.file(
"src/lib.rs",
r#"
#[cfg(feature = "f1")]
pub fn foo() {}
#[cfg(not(feature = "f1"))]
pub fn bar() {}
"#,
)
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
edition = "2018"
[build-dependencies]
common = {version="1.0", features=["f1"]}
[dependencies]
common = "1.0"
"#,
)
.file(
"build.rs",
r#"
use common::foo;
fn main() {}
"#,
)
.file("src/lib.rs", "use common::bar;")
.build();
p.cargo("check")
.with_status(101)
.with_stderr_contains("[..]unresolved import `common::bar`[..]")
.run();
switch_to_resolver_2(&p);
p.cargo("check").run();
}
#[cargo_test]
fn decouple_host_deps_nested() {
// `host_dep` decouple of transitive dependencies.
Package::new("common", "1.0.0")
.feature("f1", &[])
.file(
"src/lib.rs",
r#"
#[cfg(feature = "f1")]
pub fn foo() {}
#[cfg(not(feature = "f1"))]
pub fn bar() {}
"#,
)
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
edition = "2018"
[build-dependencies]
bdep = {path="bdep"}
[dependencies]
common = "1.0"
"#,
)
.file(
"build.rs",
r#"
use bdep::foo;
fn main() {}
"#,
)
.file("src/lib.rs", "use common::bar;")
.file(
"bdep/Cargo.toml",
r#"
[package]
name = "bdep"
version = "0.1.0"
edition = "2018"
[dependencies]
common = {version="1.0", features=["f1"]}
"#,
)
.file("bdep/src/lib.rs", "pub use common::foo;")
.build();
p.cargo("check")
.with_status(101)
.with_stderr_contains("[..]unresolved import `common::bar`[..]")
.run();
switch_to_resolver_2(&p);
p.cargo("check").run();
}
#[cargo_test]
fn decouple_dev_deps() {
// Basic test for `dev_dep` decouple.
Package::new("common", "1.0.0")
.feature("f1", &[])
.feature("f2", &[])
.file(
"src/lib.rs",
r#"
// const ensures it uses the correct dependency at *build time*
// compared to *link time*.
#[cfg(all(feature="f1", not(feature="f2")))]
pub const X: u32 = 1;
#[cfg(all(feature="f1", feature="f2"))]
pub const X: u32 = 3;
pub fn foo() -> u32 {
let mut res = 0;
if cfg!(feature = "f1") {
res |= 1;
}
if cfg!(feature = "f2") {
res |= 2;
}
res
}
"#,
)
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
edition = "2018"
[dependencies]
common = {version="1.0", features=["f1"]}
[dev-dependencies]
common = {version="1.0", features=["f2"]}
"#,
)
.file(
"src/main.rs",
r#"
fn main() {
let expected: u32 = std::env::args().skip(1).next().unwrap().parse().unwrap();
assert_eq!(foo::foo(), expected);
assert_eq!(foo::build_time(), expected);
assert_eq!(common::foo(), expected);
assert_eq!(common::X, expected);
}
#[test]
fn test_bin() {
assert_eq!(foo::foo(), 3);
assert_eq!(common::foo(), 3);
assert_eq!(common::X, 3);
assert_eq!(foo::build_time(), 3);
}
"#,
)
.file(
"src/lib.rs",
r#"
pub fn foo() -> u32 {
common::foo()
}
pub fn build_time() -> u32 {
common::X
}
#[test]
fn test_lib() {
assert_eq!(foo(), 3);
assert_eq!(common::foo(), 3);
assert_eq!(common::X, 3);
}
"#,
)
.file(
"tests/t1.rs",
r#"
#[test]
fn test_t1() {
assert_eq!(foo::foo(), 3);
assert_eq!(common::foo(), 3);
assert_eq!(common::X, 3);
assert_eq!(foo::build_time(), 3);
}
#[test]
fn test_main() {
// Features are unified for main when run with `cargo test`,
// even with the new resolver.
let s = std::process::Command::new("target/debug/foo")
.arg("3")
.status().unwrap();
assert!(s.success());
}
"#,
)
.build();
// Old behavior
p.cargo("run 3").run();
p.cargo("test").run();
// New behavior
switch_to_resolver_2(&p);
p.cargo("run 1").run();
p.cargo("test").run();
}
#[cargo_test]
fn build_script_runtime_features() {
// Check that the CARGO_FEATURE_* environment variable is set correctly.
//
// This has a common dependency between build/normal/dev-deps, and it
// queries which features it was built with in different circumstances.
Package::new("common", "1.0.0")
.feature("normal", &[])
.feature("dev", &[])
.feature("build", &[])
.file(
"build.rs",
r#"
fn is_set(name: &str) -> bool {
std::env::var(name) == Ok("1".to_string())
}
fn main() {
let mut res = 0;
if is_set("CARGO_FEATURE_NORMAL") {
res |= 1;
}
if is_set("CARGO_FEATURE_DEV") {
res |= 2;
}
if is_set("CARGO_FEATURE_BUILD") {
res |= 4;
}
println!("cargo:rustc-cfg=RunCustomBuild=\"{}\"", res);
let mut res = 0;
if cfg!(feature = "normal") {
res |= 1;
}
if cfg!(feature = "dev") {
res |= 2;
}
if cfg!(feature = "build") {
res |= 4;
}
println!("cargo:rustc-cfg=CustomBuild=\"{}\"", res);
}
"#,
)
.file(
"src/lib.rs",
r#"
pub fn foo() -> u32 {
let mut res = 0;
if cfg!(feature = "normal") {
res |= 1;
}
if cfg!(feature = "dev") {
res |= 2;
}
if cfg!(feature = "build") {
res |= 4;
}
res
}
pub fn build_time() -> u32 {
#[cfg(RunCustomBuild="1")] return 1;
#[cfg(RunCustomBuild="3")] return 3;
#[cfg(RunCustomBuild="4")] return 4;
#[cfg(RunCustomBuild="5")] return 5;
#[cfg(RunCustomBuild="7")] return 7;
}
"#,
)
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
edition = "2018"
[build-dependencies]
common = {version="1.0", features=["build"]}
[dependencies]
common = {version="1.0", features=["normal"]}
[dev-dependencies]
common = {version="1.0", features=["dev"]}
"#,
)
.file(
"build.rs",
r#"
fn main() {
assert_eq!(common::foo(), common::build_time());
println!("cargo:rustc-cfg=from_build=\"{}\"", common::foo());
}
"#,
)
.file(
"src/lib.rs",
r#"
pub fn foo() -> u32 {
common::foo()
}
pub fn build_time() -> u32 {
common::build_time()
}
#[test]
fn test_lib() {
assert_eq!(common::foo(), common::build_time());
assert_eq!(common::foo(),
std::env::var("CARGO_FEATURE_EXPECT").unwrap().parse().unwrap());
}
"#,
)
.file(
"src/main.rs",
r#"
fn main() {
assert_eq!(common::foo(), common::build_time());
assert_eq!(common::foo(),
std::env::var("CARGO_FEATURE_EXPECT").unwrap().parse().unwrap());
}
#[test]
fn test_bin() {
assert_eq!(common::foo(), common::build_time());
assert_eq!(common::foo(),
std::env::var("CARGO_FEATURE_EXPECT").unwrap().parse().unwrap());
}
"#,
)
.file(
"tests/t1.rs",
r#"
#[test]
fn test_t1() {
assert_eq!(common::foo(), common::build_time());
assert_eq!(common::foo(),
std::env::var("CARGO_FEATURE_EXPECT").unwrap().parse().unwrap());
}
#[test]
fn test_main() {
// Features are unified for main when run with `cargo test`,
// even with the new resolver.
let s = std::process::Command::new("target/debug/foo")
.status().unwrap();
assert!(s.success());
}
"#,
)
.build();
// Old way, unifies all 3.
p.cargo("run").env("CARGO_FEATURE_EXPECT", "7").run();
p.cargo("test").env("CARGO_FEATURE_EXPECT", "7").run();
// New behavior.
switch_to_resolver_2(&p);
// normal + build unify
p.cargo("run").env("CARGO_FEATURE_EXPECT", "1").run();
// dev_deps are still unified with `cargo test`
p.cargo("test").env("CARGO_FEATURE_EXPECT", "3").run();
}
#[cargo_test]
fn cyclical_dev_dep() {
// Check how a cyclical dev-dependency will work.
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
edition = "2018"
[features]
dev = []
[dev-dependencies]
foo = { path = '.', features = ["dev"] }
"#,
)
.file(
"src/lib.rs",
r#"
pub fn assert_dev(enabled: bool) {
assert_eq!(enabled, cfg!(feature="dev"));
}
#[test]
fn test_in_lib() {
assert_dev(true);
}
"#,
)
.file(
"src/main.rs",
r#"
fn main() {
let expected: bool = std::env::args().skip(1).next().unwrap().parse().unwrap();
foo::assert_dev(expected);
}
"#,
)
.file(
"tests/t1.rs",
r#"
#[test]
fn integration_links() {
foo::assert_dev(true);
// The lib linked with main.rs will also be unified.
let s = std::process::Command::new("target/debug/foo")
.arg("true")
.status().unwrap();
assert!(s.success());
}
"#,
)
.build();
// Old way unifies features.
p.cargo("run true").run();
// dev feature should always be enabled in tests.
p.cargo("test").run();
// New behavior.
switch_to_resolver_2(&p);
// Should decouple main.
p.cargo("run false").run();
// And this should be no different.
p.cargo("test").run();
}
#[cargo_test]
fn all_feature_opts() {
// All feature options at once.
Package::new("common", "1.0.0")
.feature("normal", &[])
.feature("build", &[])
.feature("dev", &[])
.feature("itarget", &[])
.file(
"src/lib.rs",
r#"
pub fn feats() -> u32 {
let mut res = 0;
if cfg!(feature="normal") { res |= 1; }
if cfg!(feature="build") { res |= 2; }
if cfg!(feature="dev") { res |= 4; }
if cfg!(feature="itarget") { res |= 8; }
res
}
"#,
)
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
edition = "2018"
[dependencies]
common = {version = "1.0", features=["normal"]}
[dev-dependencies]
common = {version = "1.0", features=["dev"]}
[build-dependencies]
common = {version = "1.0", features=["build"]}
[target.'cfg(whatever)'.dependencies]
common = {version = "1.0", features=["itarget"]}
"#,
)
.file(
"src/main.rs",
r#"
fn main() {
expect();
}
fn expect() {
let expected: u32 = std::env::var("EXPECTED_FEATS").unwrap().parse().unwrap();
assert_eq!(expected, common::feats());
}
#[test]
fn from_test() {
expect();
}
"#,
)
.build();
p.cargo("run").env("EXPECTED_FEATS", "15").run();
p.cargo("test").env("EXPECTED_FEATS", "15").run();
// New behavior.
switch_to_resolver_2(&p);
// Only normal feature.
p.cargo("run").env("EXPECTED_FEATS", "1").run();
// only normal+dev
p.cargo("test").env("EXPECTED_FEATS", "5").run();
}
#[cargo_test]
fn required_features_host_dep() {
// Check that required-features handles build-dependencies correctly.
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
edition = "2018"
[[bin]]
name = "x"
required-features = ["bdep/f1"]
[build-dependencies]
bdep = {path="bdep"}
"#,
)
.file("build.rs", "fn main() {}")
.file(
"src/bin/x.rs",
r#"
fn main() {}
"#,
)
.file(
"bdep/Cargo.toml",
r#"
[package]
name = "bdep"
version = "0.1.0"
[features]
f1 = []
"#,
)
.file("bdep/src/lib.rs", "")
.build();
p.cargo("run")
.with_status(101)
.with_stderr(
"\
[ERROR] target `x` in package `foo` requires the features: `bdep/f1`
Consider enabling them by passing, e.g., `--features=\"bdep/f1\"`
",
)
.run();
// New behavior.
switch_to_resolver_2(&p);
p.cargo("run --features bdep/f1").run();
}
#[cargo_test]
fn disabled_shared_host_dep() {
// Check for situation where an optional dep of a shared dep is enabled in
// a normal dependency, but disabled in an optional one. The unit tree is:
// foo
// ├── foo build.rs
// | └── common (BUILD dependency, NO FEATURES)
// └── common (Normal dependency, default features)
// └── somedep
Package::new("somedep", "1.0.0")
.file(
"src/lib.rs",
r#"
pub fn f() { println!("hello from somedep"); }
"#,
)
.publish();
Package::new("common", "1.0.0")
.feature("default", &["somedep"])
.add_dep(Dependency::new("somedep", "1.0").optional(true))
.file(
"src/lib.rs",
r#"
pub fn check_somedep() -> bool {
#[cfg(feature="somedep")]
{
extern crate somedep;
somedep::f();
true
}
#[cfg(not(feature="somedep"))]
{
println!("no somedep");
false
}
}
"#,
)
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "1.0.0"
edition = "2018"
resolver = "2"
[dependencies]
common = "1.0"
[build-dependencies]
common = {version = "1.0", default-features = false}
"#,
)
.file(
"src/main.rs",
"fn main() { assert!(common::check_somedep()); }",
)
.file(
"build.rs",
"fn main() { assert!(!common::check_somedep()); }",
)
.build();
p.cargo("run -v").with_stdout("hello from somedep").run();
}
#[cargo_test]
fn required_features_inactive_dep() {
// required-features with an inactivated dep.
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
resolver = "2"
[target.'cfg(whatever)'.dependencies]
bar = {path="bar"}
[[bin]]
name = "foo"
required-features = ["feat1"]
[features]
feat1 = []
"#,
)
.file("src/main.rs", "fn main() {}")
.file("bar/Cargo.toml", &basic_manifest("bar", "0.1.0"))
.file("bar/src/lib.rs", "")
.build();
p.cargo("check").with_stderr("[FINISHED] [..]").run();
p.cargo("check --features=feat1")
.with_stderr("[CHECKING] foo[..]\n[FINISHED] [..]")
.run();
}
#[cargo_test]
fn decouple_proc_macro() {
// proc macro features are not shared
Package::new("common", "1.0.0")
.feature("somefeat", &[])
.file(
"src/lib.rs",
r#"
pub const fn foo() -> bool { cfg!(feature="somefeat") }
#[cfg(feature="somefeat")]
pub const FEAT_ONLY_CONST: bool = true;
"#,
)
.publish();
Package::new("pm", "1.0.0")
.proc_macro(true)
.feature_dep("common", "1.0", &["somefeat"])
.file(
"src/lib.rs",
r#"
extern crate proc_macro;
extern crate common;
#[proc_macro]
pub fn foo(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
assert!(common::foo());
"".parse().unwrap()
}
"#,
)
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "1.0.0"
edition = "2018"
[dependencies]
pm = "1.0"
common = "1.0"
"#,
)
.file(
"src/lib.rs",
r#"
//! Test with docs.
//!
//! ```rust
//! pm::foo!{}
//! fn main() {
//! let expected = std::env::var_os("TEST_EXPECTS_ENABLED").is_some();
//! assert_eq!(expected, common::foo(), "common is wrong");
//! }
//! ```
"#,
)
.file(
"src/main.rs",
r#"
pm::foo!{}
fn main() {
println!("it is {}", common::foo());
}
"#,
)
.build();
p.cargo("run")
.env("TEST_EXPECTS_ENABLED", "1")
.with_stdout("it is true")
.run();
// Make sure the test is fallible.
p.cargo("test --doc")
.with_status(101)
.with_stdout_contains("[..]common is wrong[..]")
.run();
p.cargo("test --doc").env("TEST_EXPECTS_ENABLED", "1").run();
p.cargo("doc").run();
assert!(p
.build_dir()
.join("doc/common/constant.FEAT_ONLY_CONST.html")
.exists());
// cargo doc should clean in-between runs, but it doesn't, and leaves stale files.
// https://github.com/rust-lang/cargo/issues/6783 (same for removed items)
p.build_dir().join("doc").rm_rf();
// New behavior.
switch_to_resolver_2(&p);
p.cargo("run").with_stdout("it is false").run();
p.cargo("test --doc").run();
p.cargo("doc").run();
assert!(!p
.build_dir()
.join("doc/common/constant.FEAT_ONLY_CONST.html")
.exists());
}
#[cargo_test]
fn proc_macro_ws() {
// Checks for bug with proc-macro in a workspace with dependency (shouldn't panic).
let p = project()
.file(
"Cargo.toml",
r#"
[workspace]
members = ["foo", "pm"]
resolver = "2"
"#,
)
.file(
"foo/Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[features]
feat1 = []
"#,
)
.file("foo/src/lib.rs", "")
.file(
"pm/Cargo.toml",
r#"
[package]
name = "pm"
version = "0.1.0"
[lib]
proc-macro = true
[dependencies]
foo = { path = "../foo", features=["feat1"] }
"#,
)
.file("pm/src/lib.rs", "")
.build();
p.cargo("check -p pm -v")
.with_stderr_contains("[RUNNING] `rustc --crate-name foo [..]--cfg[..]feat1[..]")
.run();
// This may be surprising that `foo` doesn't get built separately. It is
// because pm might have other units (binaries, tests, etc.), and so the
// feature resolver must assume that normal deps get unified with it. This
// is related to the bigger issue where the features selected in a
// workspace depend on which packages are selected.
p.cargo("check --workspace -v")
.with_stderr(
"\
[FRESH] foo v0.1.0 [..]
[FRESH] pm v0.1.0 [..]
[FINISHED] dev [..]
",
)
.run();
// Selecting just foo will build without unification.
p.cargo("check -p foo -v")
// Make sure `foo` is built without feat1
.with_stderr_line_without(&["[RUNNING] `rustc --crate-name foo"], &["--cfg[..]feat1"])
.run();
}
#[cargo_test]
fn has_dev_dep_for_test() {
// Check for a bug where the decision on whether or not "dev dependencies"
// should be used did not consider `check --profile=test`.
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dev-dependencies]
dep = { path = 'dep', features = ['f1'] }
"#,
)
.file(
"src/lib.rs",
r#"
#[test]
fn t1() {
dep::f();
}
"#,
)
.file(
"dep/Cargo.toml",
r#"
[package]
name = "dep"
version = "0.1.0"
[features]
f1 = []
"#,
)
.file(
"dep/src/lib.rs",
r#"
#[cfg(feature = "f1")]
pub fn f() {}
"#,
)
.build();
p.cargo("check -v")
.with_stderr(
"\
[CHECKING] foo v0.1.0 [..]
[RUNNING] `rustc --crate-name foo [..]
[FINISHED] [..]
",
)
.run();
p.cargo("check -v --profile=test")
.with_stderr(
"\
[CHECKING] dep v0.1.0 [..]
[RUNNING] `rustc --crate-name dep [..]
[CHECKING] foo v0.1.0 [..]
[RUNNING] `rustc --crate-name foo [..]
[FINISHED] [..]
",
)
.run();
// New resolver should not be any different.
switch_to_resolver_2(&p);
p.cargo("check -v --profile=test")
.with_stderr(
"\
[FRESH] dep [..]
[FRESH] foo [..]
[FINISHED] [..]
",
)
.run();
}
#[cargo_test]
fn build_dep_activated() {
// Build dependencies always match the host for [target.*.build-dependencies].
if cross_compile::disabled() {
return;
}
Package::new("somedep", "1.0.0")
.file("src/lib.rs", "")
.publish();
Package::new("targetdep", "1.0.0").publish();
Package::new("hostdep", "1.0.0")
// Check that "for_host" is sticky.
.target_dep("somedep", "1.0", rustc_host())
.feature("feat1", &[])
.file(
"src/lib.rs",
r#"
extern crate somedep;
#[cfg(not(feature="feat1"))]
compile_error!{"feat1 missing"}
"#,
)
.publish();
let p = project()
.file(
"Cargo.toml",
&format!(
r#"
[package]
name = "foo"
version = "0.1.0"
# This should never be selected.
[target.'{}'.build-dependencies]
targetdep = "1.0"
[target.'{}'.build-dependencies]
hostdep = {{version="1.0", features=["feat1"]}}
"#,
alternate(),
rustc_host()
),
)
.file("src/lib.rs", "")
.file("build.rs", "fn main() {}")
.build();
p.cargo("check").run();
p.cargo("check --target").arg(alternate()).run();
// New behavior.
switch_to_resolver_2(&p);
p.cargo("check").run();
p.cargo("check --target").arg(alternate()).run();
}
#[cargo_test]
fn resolver_bad_setting() {
// Unknown setting in `resolver`
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
resolver = "foo"
"#,
)
.file("src/lib.rs", "")
.build();
p.cargo("build")
.with_status(101)
.with_stderr(
"\
error: failed to parse manifest at `[..]/foo/Cargo.toml`
Caused by:
`resolver` setting `foo` is not valid, valid options are \"1\" or \"2\"
",
)
.run();
}
#[cargo_test]
fn resolver_original() {
// resolver="1" uses old unification behavior.
Package::new("common", "1.0.0")
.feature("f1", &[])
.file(
"src/lib.rs",
r#"
#[cfg(feature = "f1")]
compile_error!("f1 should not activate");
"#,
)
.publish();
Package::new("bar", "1.0.0")
.add_dep(
Dependency::new("common", "1.0")
.target("cfg(whatever)")
.enable_features(&["f1"]),
)
.publish();
let manifest = |resolver| {
format!(
r#"
[package]
name = "foo"
version = "0.1.0"
resolver = "{}"
[dependencies]
common = "1.0"
bar = "1.0"
"#,
resolver
)
};
let p = project()
.file("Cargo.toml", &manifest("1"))
.file("src/lib.rs", "")
.build();
p.cargo("check")
.with_status(101)
.with_stderr_contains("[..]f1 should not activate[..]")
.run();
p.change_file("Cargo.toml", &manifest("2"));
p.cargo("check").run();
}
#[cargo_test]
fn resolver_not_both() {
// Can't specify resolver in both workspace and package.
let p = project()
.file(
"Cargo.toml",
r#"
[workspace]
resolver = "2"
[package]
name = "foo"
version = "0.1.0"
resolver = "2"
"#,
)
.file("src/lib.rs", "")
.build();
p.cargo("build")
.with_status(101)
.with_stderr(
"\
error: failed to parse manifest at `[..]/foo/Cargo.toml`
Caused by:
cannot specify `resolver` field in both `[workspace]` and `[package]`
",
)
.run();
}
#[cargo_test]
fn resolver_ws_member() {
// Can't specify `resolver` in a ws member.
let p = project()
.file(
"Cargo.toml",
r#"
[workspace]
members = ["a"]
"#,
)
.file(
"a/Cargo.toml",
r#"
[package]
name = "a"
version = "0.1.0"
resolver = "2"
"#,
)
.file("a/src/lib.rs", "")
.build();
p.cargo("check")
.with_stderr(
"\
warning: resolver for the non root package will be ignored, specify resolver at the workspace root:
package: [..]/foo/a/Cargo.toml
workspace: [..]/foo/Cargo.toml
[CHECKING] a v0.1.0 [..]
[FINISHED] [..]
",
)
.run();
}
#[cargo_test]
fn resolver_ws_root_and_member() {
// Check when specified in both ws root and member.
let p = project()
.file(
"Cargo.toml",
r#"
[workspace]
members = ["a"]
resolver = "2"
"#,
)
.file(
"a/Cargo.toml",
r#"
[package]
name = "a"
version = "0.1.0"
resolver = "2"
"#,
)
.file("a/src/lib.rs", "")
.build();
// Ignores if they are the same.
p.cargo("check")
.with_stderr(
"\
[CHECKING] a v0.1.0 [..]
[FINISHED] [..]
",
)
.run();
}
#[cargo_test]
fn resolver_enables_new_features() {
// resolver="2" enables all the things.
Package::new("common", "1.0.0")
.feature("normal", &[])
.feature("build", &[])
.feature("dev", &[])
.feature("itarget", &[])
.file(
"src/lib.rs",
r#"
pub fn feats() -> u32 {
let mut res = 0;
if cfg!(feature="normal") { res |= 1; }
if cfg!(feature="build") { res |= 2; }
if cfg!(feature="dev") { res |= 4; }
if cfg!(feature="itarget") { res |= 8; }
res
}
"#,
)
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[workspace]
members = ["a", "b"]
resolver = "2"
"#,
)
.file(
"a/Cargo.toml",
r#"
[package]
name = "a"
version = "0.1.0"
edition = "2018"
[dependencies]
common = {version = "1.0", features=["normal"]}
[dev-dependencies]
common = {version = "1.0", features=["dev"]}
[build-dependencies]
common = {version = "1.0", features=["build"]}
[target.'cfg(whatever)'.dependencies]
common = {version = "1.0", features=["itarget"]}
"#,
)
.file(
"a/src/main.rs",
r#"
fn main() {
expect();
}
fn expect() {
let expected: u32 = std::env::var("EXPECTED_FEATS").unwrap().parse().unwrap();
assert_eq!(expected, common::feats());
}
#[test]
fn from_test() {
expect();
}
"#,
)
.file(
"b/Cargo.toml",
r#"
[package]
name = "b"
version = "0.1.0"
[features]
ping = []
"#,
)
.file(
"b/src/main.rs",
r#"
fn main() {
if cfg!(feature="ping") {
println!("pong");
}
}
"#,
)
.build();
// Only normal.
p.cargo("run --bin a")
.env("EXPECTED_FEATS", "1")
.with_stderr(
"\
[UPDATING] [..]
[DOWNLOADING] crates ...
[DOWNLOADED] common [..]
[COMPILING] common v1.0.0
[COMPILING] a v0.1.0 [..]
[FINISHED] [..]
[RUNNING] `target/debug/a[EXE]`
",
)
.run();
// only normal+dev
p.cargo("test").cwd("a").env("EXPECTED_FEATS", "5").run();
// Can specify features of packages from a different directory.
p.cargo("run -p b --features=ping")
.cwd("a")
.with_stdout("pong")
.run();
}
#[cargo_test]
fn install_resolve_behavior() {
// install honors the resolver behavior.
Package::new("common", "1.0.0")
.feature("f1", &[])
.file(
"src/lib.rs",
r#"
#[cfg(feature = "f1")]
compile_error!("f1 should not activate");
"#,
)
.publish();
Package::new("bar", "1.0.0").dep("common", "1.0").publish();
Package::new("foo", "1.0.0")
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "1.0.0"
resolver = "2"
[target.'cfg(whatever)'.dependencies]
common = {version="1.0", features=["f1"]}
[dependencies]
bar = "1.0"
"#,
)
.file("src/main.rs", "fn main() {}")
.publish();
cargo_process("install foo").run();
}
#[cargo_test]
fn package_includes_resolve_behavior() {
// `cargo package` will inherit the correct resolve behavior.
let p = project()
.file(
"Cargo.toml",
r#"
[workspace]
members = ["a"]
resolver = "2"
"#,
)
.file(
"a/Cargo.toml",
r#"
[package]
name = "a"
version = "0.1.0"
authors = ["Zzz"]
description = "foo"
license = "MIT"
homepage = "https://example.com/"
"#,
)
.file("a/src/lib.rs", "")
.build();
p.cargo("package").cwd("a").run();
let rewritten_toml = format!(
r#"{}
[package]
name = "a"
version = "0.1.0"
authors = ["Zzz"]
description = "foo"
homepage = "https://example.com/"
license = "MIT"
resolver = "2"
"#,
cargo::core::package::MANIFEST_PREAMBLE
);
let f = File::open(&p.root().join("target/package/a-0.1.0.crate")).unwrap();
validate_crate_contents(
f,
"a-0.1.0.crate",
&["Cargo.toml", "Cargo.toml.orig", "src/lib.rs"],
&[("Cargo.toml", &rewritten_toml)],
);
}
#[cargo_test]
fn tree_all() {
// `cargo tree` with the new feature resolver.
Package::new("log", "0.4.8").feature("serde", &[]).publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
resolver = "2"
[target.'cfg(whatever)'.dependencies]
log = {version="*", features=["serde"]}
"#,
)
.file("src/lib.rs", "")
.build();
p.cargo("tree --target=all")
.with_stdout(
"\
foo v0.1.0 ([..]/foo)
└── log v0.4.8
",
)
.run();
}
#[cargo_test]
fn shared_dep_same_but_dependencies() {
// Checks for a bug of nondeterminism. This scenario creates a shared
// dependency `dep` which needs to be built twice (once as normal, and
// once as a build dep). However, in both cases the flags to `dep` are the
// same, the only difference is what it links to. The normal dependency
// should link to `subdep` with the feature disabled, and the build
// dependency should link to it with it enabled. Crucially, the `--target`
// flag should not be specified, otherwise Unit.kind would be different
// and avoid the collision, and this bug won't manifest.
let p = project()
.file(
"Cargo.toml",
r#"
[workspace]
members = ["bin1", "bin2"]
resolver = "2"
"#,
)
.file(
"bin1/Cargo.toml",
r#"
[package]
name = "bin1"
version = "0.1.0"
[dependencies]
dep = { path = "../dep" }
"#,
)
.file("bin1/src/main.rs", "fn main() { dep::feat_func(); }")
.file(
"bin2/Cargo.toml",
r#"
[package]
name = "bin2"
version = "0.1.0"
[build-dependencies]
dep = { path = "../dep" }
subdep = { path = "../subdep", features = ["feat"] }
"#,
)
.file("bin2/build.rs", "fn main() { dep::feat_func(); }")
.file("bin2/src/main.rs", "fn main() {}")
.file(
"dep/Cargo.toml",
r#"
[package]
name = "dep"
version = "0.1.0"
[dependencies]
subdep = { path = "../subdep" }
"#,
)
.file(
"dep/src/lib.rs",
"pub fn feat_func() { subdep::feat_func(); }",
)
.file(
"subdep/Cargo.toml",
r#"
[package]
name = "subdep"
version = "0.1.0"
[features]
feat = []
"#,
)
.file(
"subdep/src/lib.rs",
r#"
pub fn feat_func() {
#[cfg(feature = "feat")] println!("cargo:warning=feat: enabled");
#[cfg(not(feature = "feat"))] println!("cargo:warning=feat: not enabled");
}
"#,
)
.build();
p.cargo("build --bin bin1 --bin bin2")
// unordered because bin1 and bin2 build at the same time
.with_stderr_unordered(
"\
[COMPILING] subdep [..]
[COMPILING] dep [..]
[COMPILING] bin2 [..]
[COMPILING] bin1 [..]
warning: feat: enabled
[FINISHED] [..]
",
)
.run();
p.process(p.bin("bin1"))
.with_stdout("cargo:warning=feat: not enabled")
.run();
// Make sure everything stays cached.
p.cargo("build -v --bin bin1 --bin bin2")
.with_stderr_unordered(
"\
[FRESH] subdep [..]
[FRESH] dep [..]
[FRESH] bin1 [..]
warning: feat: enabled
[FRESH] bin2 [..]
[FINISHED] [..]
",
)
.run();
}
#[cargo_test]
fn test_proc_macro() {
// Running `cargo test` on a proc-macro, with a shared dependency that has
// different features.
//
// There was a bug where `shared` was built twice (once with feature "B"
// and once without), and both copies linked into the unit test. This
// would cause a type failure when used in an intermediate dependency
// (the-macro-support).
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "runtime"
version = "0.1.0"
resolver = "2"
[dependencies]
the-macro = { path = "the-macro", features = ['a'] }
[build-dependencies]
shared = { path = "shared", features = ['b'] }
"#,
)
.file("src/lib.rs", "")
.file(
"the-macro/Cargo.toml",
r#"
[package]
name = "the-macro"
version = "0.1.0"
[lib]
proc-macro = true
test = false
[dependencies]
the-macro-support = { path = "../the-macro-support" }
shared = { path = "../shared" }
[dev-dependencies]
runtime = { path = ".." }
[features]
a = []
"#,
)
.file(
"the-macro/src/lib.rs",
"
fn _test() {
the_macro_support::foo(shared::Foo);
}
",
)
.file(
"the-macro-support/Cargo.toml",
r#"
[package]
name = "the-macro-support"
version = "0.1.0"
[dependencies]
shared = { path = "../shared" }
"#,
)
.file(
"the-macro-support/src/lib.rs",
"
pub fn foo(_: shared::Foo) {}
",
)
.file(
"shared/Cargo.toml",
r#"
[package]
name = "shared"
version = "0.1.0"
[features]
b = []
"#,
)
.file("shared/src/lib.rs", "pub struct Foo;")
.build();
p.cargo("test --manifest-path the-macro/Cargo.toml").run();
}
#[cargo_test]
fn doc_optional() {
// Checks for a bug where `cargo doc` was failing with an inactive target
// that enables a shared optional dependency.
Package::new("spin", "1.0.0").publish();
Package::new("bar", "1.0.0")
.add_dep(Dependency::new("spin", "1.0").optional(true))
.publish();
// The enabler package enables the `spin` feature, which we don't want.
Package::new("enabler", "1.0.0")
.feature_dep("bar", "1.0", &["spin"])
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
resolver = "2"
[target.'cfg(whatever)'.dependencies]
enabler = "1.0"
[dependencies]
bar = "1.0"
"#,
)
.file("src/lib.rs", "")
.build();
p.cargo("doc")
.with_stderr_unordered(
"\
[UPDATING] [..]
[DOWNLOADING] crates ...
[DOWNLOADED] spin v1.0.0 [..]
[DOWNLOADED] bar v1.0.0 [..]
[DOCUMENTING] bar v1.0.0
[CHECKING] bar v1.0.0
[DOCUMENTING] foo v0.1.0 [..]
[FINISHED] [..]
",
)
.run();
}
#[cargo_test]
fn minimal_download() {
// Various checks that it only downloads the minimum set of dependencies
// needed in various situations.
//
// This checks several permutations of the different
// host_dep/dev_dep/itarget settings. These 3 are planned to be stabilized
// together, so there isn't much need to be concerned about how the behave
// independently. However, there are some cases where they do behave
// independently. Specifically:
//
// * `cargo test` forces dev_dep decoupling to be disabled.
// * `cargo tree --target=all` forces ignore_inactive_targets off and decouple_dev_deps off.
// * `cargo tree --target=all -e normal` forces ignore_inactive_targets off.
//
// However, `cargo tree` is a little weird because it downloads everything
// anyways.
//
// So to summarize the different permutations:
//
// dev_dep | host_dep | itarget | Notes
// --------|----------|---------|----------------------------
// | | | -Zfeatures=compare (new resolver should behave same as old)
// | | ✓ | This scenario should not happen.
// | ✓ | | `cargo tree --target=all -Zfeatures=all`†
// | ✓ | ✓ | `cargo test`
// ✓ | | | This scenario should not happen.
// ✓ | | ✓ | This scenario should not happen.
// ✓ | ✓ | | `cargo tree --target=all -e normal -Z features=all`†
// ✓ | ✓ | ✓ | A normal build.
//
// † — However, `cargo tree` downloads everything.
Package::new("normal", "1.0.0").publish();
Package::new("normal_pm", "1.0.0").publish();
Package::new("normal_opt", "1.0.0").publish();
Package::new("dev_dep", "1.0.0").publish();
Package::new("dev_dep_pm", "1.0.0").publish();
Package::new("build_dep", "1.0.0").publish();
Package::new("build_dep_pm", "1.0.0").publish();
Package::new("build_dep_opt", "1.0.0").publish();
Package::new("itarget_normal", "1.0.0").publish();
Package::new("itarget_normal_pm", "1.0.0").publish();
Package::new("itarget_dev_dep", "1.0.0").publish();
Package::new("itarget_dev_dep_pm", "1.0.0").publish();
Package::new("itarget_build_dep", "1.0.0").publish();
Package::new("itarget_build_dep_pm", "1.0.0").publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
normal = "1.0"
normal_pm = "1.0"
normal_opt = { version = "1.0", optional = true }
[dev-dependencies]
dev_dep = "1.0"
dev_dep_pm = "1.0"
[build-dependencies]
build_dep = "1.0"
build_dep_pm = "1.0"
build_dep_opt = { version = "1.0", optional = true }
[target.'cfg(whatever)'.dependencies]
itarget_normal = "1.0"
itarget_normal_pm = "1.0"
[target.'cfg(whatever)'.dev-dependencies]
itarget_dev_dep = "1.0"
itarget_dev_dep_pm = "1.0"
[target.'cfg(whatever)'.build-dependencies]
itarget_build_dep = "1.0"
itarget_build_dep_pm = "1.0"
"#,
)
.file("src/lib.rs", "")
.file("build.rs", "fn main() {}")
.build();
let clear = || {
cargo_home().join("registry/cache").rm_rf();
cargo_home().join("registry/src").rm_rf();
p.build_dir().rm_rf();
};
// none
// Should be the same as `-Zfeatures=all`
p.cargo("check -Zfeatures=compare")
.masquerade_as_nightly_cargo()
.with_stderr_unordered(
"\
[UPDATING] [..]
[DOWNLOADING] crates ...
[DOWNLOADED] normal_pm v1.0.0 [..]
[DOWNLOADED] normal v1.0.0 [..]
[DOWNLOADED] build_dep_pm v1.0.0 [..]
[DOWNLOADED] build_dep v1.0.0 [..]
[COMPILING] build_dep v1.0.0
[COMPILING] build_dep_pm v1.0.0
[CHECKING] normal_pm v1.0.0
[CHECKING] normal v1.0.0
[COMPILING] foo v0.1.0 [..]
[FINISHED] [..]
",
)
.run();
clear();
// New behavior
switch_to_resolver_2(&p);
// all
p.cargo("check")
.with_stderr_unordered(
"\
[DOWNLOADING] crates ...
[DOWNLOADED] normal_pm v1.0.0 [..]
[DOWNLOADED] normal v1.0.0 [..]
[DOWNLOADED] build_dep_pm v1.0.0 [..]
[DOWNLOADED] build_dep v1.0.0 [..]
[COMPILING] build_dep v1.0.0
[COMPILING] build_dep_pm v1.0.0
[CHECKING] normal v1.0.0
[CHECKING] normal_pm v1.0.0
[COMPILING] foo v0.1.0 [..]
[FINISHED] [..]
",
)
.run();
clear();
// This disables decouple_dev_deps.
p.cargo("test --no-run")
.with_stderr_unordered(
"\
[DOWNLOADING] crates ...
[DOWNLOADED] normal_pm v1.0.0 [..]
[DOWNLOADED] normal v1.0.0 [..]
[DOWNLOADED] dev_dep_pm v1.0.0 [..]
[DOWNLOADED] dev_dep v1.0.0 [..]
[DOWNLOADED] build_dep_pm v1.0.0 [..]
[DOWNLOADED] build_dep v1.0.0 [..]
[COMPILING] build_dep v1.0.0
[COMPILING] build_dep_pm v1.0.0
[COMPILING] normal_pm v1.0.0
[COMPILING] normal v1.0.0
[COMPILING] dev_dep_pm v1.0.0
[COMPILING] dev_dep v1.0.0
[COMPILING] foo v0.1.0 [..]
[FINISHED] [..]
",
)
.run();
clear();
// This disables itarget, but leaves decouple_dev_deps enabled.
p.cargo("tree -e normal --target=all")
.with_stderr_unordered(
"\
[DOWNLOADING] crates ...
[DOWNLOADED] normal v1.0.0 [..]
[DOWNLOADED] normal_pm v1.0.0 [..]
[DOWNLOADED] build_dep v1.0.0 [..]
[DOWNLOADED] build_dep_pm v1.0.0 [..]
[DOWNLOADED] itarget_normal v1.0.0 [..]
[DOWNLOADED] itarget_normal_pm v1.0.0 [..]
[DOWNLOADED] itarget_build_dep v1.0.0 [..]
[DOWNLOADED] itarget_build_dep_pm v1.0.0 [..]
",
)
.with_stdout(
"\
foo v0.1.0 ([ROOT]/foo)
├── itarget_normal v1.0.0
├── itarget_normal_pm v1.0.0
├── normal v1.0.0
└── normal_pm v1.0.0
",
)
.run();
clear();
// This disables itarget and decouple_dev_deps.
p.cargo("tree --target=all")
.with_stderr_unordered(
"\
[DOWNLOADING] crates ...
[DOWNLOADED] normal_pm v1.0.0 [..]
[DOWNLOADED] normal v1.0.0 [..]
[DOWNLOADED] itarget_normal_pm v1.0.0 [..]
[DOWNLOADED] itarget_normal v1.0.0 [..]
[DOWNLOADED] itarget_dev_dep_pm v1.0.0 [..]
[DOWNLOADED] itarget_dev_dep v1.0.0 [..]
[DOWNLOADED] itarget_build_dep_pm v1.0.0 [..]
[DOWNLOADED] itarget_build_dep v1.0.0 [..]
[DOWNLOADED] dev_dep_pm v1.0.0 [..]
[DOWNLOADED] dev_dep v1.0.0 [..]
[DOWNLOADED] build_dep_pm v1.0.0 [..]
[DOWNLOADED] build_dep v1.0.0 [..]
",
)
.with_stdout(
"\
foo v0.1.0 ([ROOT]/foo)
├── itarget_normal v1.0.0
├── itarget_normal_pm v1.0.0
├── normal v1.0.0
└── normal_pm v1.0.0
[build-dependencies]
├── build_dep v1.0.0
├── build_dep_pm v1.0.0
├── itarget_build_dep v1.0.0
└── itarget_build_dep_pm v1.0.0
[dev-dependencies]
├── dev_dep v1.0.0
├── dev_dep_pm v1.0.0
├── itarget_dev_dep v1.0.0
└── itarget_dev_dep_pm v1.0.0
",
)
.run();
clear();
}
#[cargo_test]
fn pm_with_int_shared() {
// This is a somewhat complex scenario of a proc-macro in a workspace with
// an integration test where the proc-macro is used for other things, and
// *everything* is built at once (`--workspace --all-targets
// --all-features`). There was a bug where the UnitFor settings were being
// incorrectly computed based on the order that the graph was traversed.
//
// There are some uncertainties about exactly how proc-macros should behave
// with `--workspace`, see https://github.com/rust-lang/cargo/issues/8312.
//
// This uses a const-eval hack to do compile-time feature checking.
let p = project()
.file(
"Cargo.toml",
r#"
[workspace]
members = ["foo", "pm", "shared"]
resolver = "2"
"#,
)
.file(
"foo/Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
edition = "2018"
[dependencies]
pm = { path = "../pm" }
shared = { path = "../shared", features = ["norm-feat"] }
"#,
)
.file(
"foo/src/lib.rs",
r#"
// foo->shared always has both features set
const _CHECK: [(); 0] = [(); 0-!(shared::FEATS==3) as usize];
"#,
)
.file(
"pm/Cargo.toml",
r#"
[package]
name = "pm"
version = "0.1.0"
[lib]
proc-macro = true
[dependencies]
shared = { path = "../shared", features = ["host-feat"] }
"#,
)
.file(
"pm/src/lib.rs",
r#"
// pm->shared always has just host
const _CHECK: [(); 0] = [(); 0-!(shared::FEATS==1) as usize];
"#,
)
.file(
"pm/tests/pm_test.rs",
r#"
// integration test gets both set
const _CHECK: [(); 0] = [(); 0-!(shared::FEATS==3) as usize];
"#,
)
.file(
"shared/Cargo.toml",
r#"
[package]
name = "shared"
version = "0.1.0"
[features]
norm-feat = []
host-feat = []
"#,
)
.file(
"shared/src/lib.rs",
r#"
pub const FEATS: u32 = {
if cfg!(feature="norm-feat") && cfg!(feature="host-feat") {
3
} else if cfg!(feature="norm-feat") {
2
} else if cfg!(feature="host-feat") {
1
} else {
0
}
};
"#,
)
.build();
p.cargo("build --workspace --all-targets --all-features -v")
.with_stderr_unordered(
"\
[COMPILING] shared [..]
[RUNNING] `rustc --crate-name shared [..]--crate-type lib [..]
[RUNNING] `rustc --crate-name shared [..]--crate-type lib [..]
[RUNNING] `rustc --crate-name shared [..]--test[..]
[COMPILING] pm [..]
[RUNNING] `rustc --crate-name pm [..]--crate-type proc-macro[..]
[RUNNING] `rustc --crate-name pm [..]--test[..]
[COMPILING] foo [..]
[RUNNING] `rustc --crate-name foo [..]--test[..]
[RUNNING] `rustc --crate-name pm_test [..]--test[..]
[RUNNING] `rustc --crate-name foo [..]--crate-type lib[..]
[FINISHED] [..]
",
)
.run();
// And again, should stay fresh.
p.cargo("build --workspace --all-targets --all-features -v")
.with_stderr_unordered(
"\
[FRESH] shared [..]
[FRESH] pm [..]
[FRESH] foo [..]
[FINISHED] [..]",
)
.run();
}
#[cargo_test]
fn doc_proc_macro() {
// Checks for a bug when documenting a proc-macro with a dependency. The
// doc unit builder was not carrying the "for host" setting through the
// dependencies, and the `pm-dep` dependency was causing a panic because
// it was looking for target features instead of host features.
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
resolver = "2"
[dependencies]
pm = { path = "pm" }
"#,
)
.file("src/lib.rs", "")
.file(
"pm/Cargo.toml",
r#"
[package]
name = "pm"
version = "0.1.0"
[lib]
proc-macro = true
[dependencies]
pm-dep = { path = "../pm-dep" }
"#,
)
.file("pm/src/lib.rs", "")
.file("pm-dep/Cargo.toml", &basic_manifest("pm-dep", "0.1.0"))
.file("pm-dep/src/lib.rs", "")
.build();
// Unfortunately this cannot check the output because what it prints is
// nondeterministic. Sometimes it says "Compiling pm-dep" and sometimes
// "Checking pm-dep". This is because it is both building it and checking
// it in parallel (building so it can build the proc-macro, and checking
// so rustdoc can load it).
p.cargo("doc").run();
}
#[cargo_test]
fn edition_2021_default_2() {
// edition = 2021 defaults to v2 resolver.
Package::new("common", "1.0.0")
.feature("f1", &[])
.file("src/lib.rs", "")
.publish();
Package::new("bar", "1.0.0")
.add_dep(
Dependency::new("common", "1.0")
.target("cfg(whatever)")
.enable_features(&["f1"]),
)
.publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
common = "1.0"
bar = "1.0"
"#,
)
.file("src/lib.rs", "")
.build();
// First without edition.
p.cargo("tree -f")
.arg("{p} feats:{f}")
.with_stdout(
"\
foo v0.1.0 [..]
├── bar v1.0.0 feats:
└── common v1.0.0 feats:f1
",
)
.run();
p.change_file(
"Cargo.toml",
r#"
cargo-features = ["edition2021"]
[package]
name = "foo"
version = "0.1.0"
edition = "2021"
[dependencies]
common = "1.0"
bar = "1.0"
"#,
);
// Importantly, this does not include `f1` on `common`.
p.cargo("tree -f")
.arg("{p} feats:{f}")
.masquerade_as_nightly_cargo()
.with_stdout(
"\
foo v0.1.0 [..]
├── bar v1.0.0 feats:
└── common v1.0.0 feats:
",
)
.run();
}