Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 162 additions & 30 deletions tests/locale_stub_ui_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ fn stub_env_builders_compile_under_the_same_harness(
Ok(())
}

/// The `test_support` rlib and the deps directory holding its dependencies.
/// The `test_support` rlib and the directories holding its dependencies.
struct TestSupportRlib {
rlib: PathBuf,
deps_dir: PathBuf,
deps_dirs: Vec<PathBuf>,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

impl TestSupportRlib {
Expand All @@ -89,12 +89,24 @@ impl TestSupportRlib {
/// with the same Polonius flags as the rest of the suite — the property
/// trybuild could not preserve.
fn build() -> io::Result<Self> {
let output = Command::new(cargo())
Self::build_with(&[])
}

/// Build `test_support` with additional environment variables applied.
///
/// The split-layout regression test uses this to force Cargo's
/// `build.build-dir` into a separate directory.
fn build_with(env: &[(&str, &Path)]) -> io::Result<Self> {
let mut command = Command::new(cargo());
command
.arg("build")
.arg("--manifest-path")
.arg(manifest_dir().join("test_support/Cargo.toml"))
.arg("--message-format=json")
.output()?;
.arg("--message-format=json");
for (key, value) in env {
command.env(key, value);
}
let output = command.output()?;
if !output.status.success() {
return Err(io::Error::other(format!(
"building test_support failed:\n{}",
Expand All @@ -108,18 +120,23 @@ impl TestSupportRlib {
.filter_map(test_support_rlib_in_message)
.next_back()
.ok_or_else(|| io::Error::other("cargo reported no test_support rlib artefact"))?;
// Cargo uplifts the top-level package's rlib out of `deps/` into the
// profile directory, so the rlib's own parent is not where the
// dependency rlibs live.
let parent = rlib
.parent()
.ok_or_else(|| io::Error::other("the rlib path should have a parent"))?;
let deps_dir = if parent.file_name() == Some(std::ffi::OsStr::new("deps")) {
parent.to_path_buf()
} else {
parent.join("deps")
};
Ok(Self { rlib, deps_dir })
// Dependency rlibs do not necessarily sit beside the uplifted
// `test_support` rlib: Cargo's `build.build-dir` setting splits
// intermediate artefacts (where dependencies live) from final ones.
// Every compiler-artifact message names its rlib's real location, so
// collect each artefact's parent directory for `-L dependency=`.
let mut deps_dirs: Vec<PathBuf> = Vec::new();
for parent in stdout.lines().flat_map(rlib_parents_in_message) {
if !deps_dirs.contains(&parent) {
deps_dirs.push(parent);
}
}
if deps_dirs.is_empty() {
return Err(io::Error::other(
"cargo reported no rlib artefacts to derive dependency dirs from",
));
}
Ok(Self { rlib, deps_dirs })
}

/// Type-check `source` against the rlib without linking a binary.
Expand All @@ -135,34 +152,63 @@ impl TestSupportRlib {
.arg(manifest_dir().join(source))
.arg("--extern")
.arg(format!("test_support={}", self.rlib.display()))
.arg("-L")
.arg(format!("dependency={}", self.deps_dir.display()))
.args(self.deps_dirs.iter().flat_map(|dir| {
[
std::ffi::OsString::from("-L"),
format!("dependency={}", dir.display()).into(),
]
}))
.arg("-o")
.arg(output_dir.path().join("stub-env-ui.rmeta"))
.output()
}
}

/// Extract the `test_support` rlib path from one Cargo JSON message, if any.
fn test_support_rlib_in_message(line: &str) -> Option<PathBuf> {
/// Extract a compiler-artifact message's target name and rlib paths.
///
/// Returns `None` for lines that are not valid JSON, not compiler-artifact
/// messages, or that lack a target name; the rlib list may be empty for
/// artefacts that emit no rlib.
fn compiler_artifact_rlibs(line: &str) -> Option<(String, Vec<PathBuf>)> {
let message: serde_json::Value = serde_json::from_str(line).ok()?;
if message.get("reason")? != "compiler-artifact"
|| message.get("target")?.get("name")? != "test_support"
{
if message.get("reason")? != "compiler-artifact" {
return None;
}
message
.get("filenames")?
.as_array()?
.iter()
.filter_map(|filename| filename.as_str())
let name = message.get("target")?.get("name")?.as_str()?.to_owned();
let rlibs = message
.get("filenames")
.and_then(serde_json::Value::as_array)
.into_iter()
.flatten()
.filter_map(serde_json::Value::as_str)
.filter(|filename| {
Path::new(filename)
.extension()
.is_some_and(|extension| extension.eq_ignore_ascii_case("rlib"))
})
.map(PathBuf::from)
.next_back()
.collect();
Some((name, rlibs))
}

/// Extract the parent directories of every rlib in one Cargo JSON message.
fn rlib_parents_in_message(line: &str) -> Vec<PathBuf> {
compiler_artifact_rlibs(line)
.map(|(_name, rlibs)| {
rlibs
.iter()
.filter_map(|rlib| rlib.parent().map(Path::to_path_buf))
.collect()
})
.unwrap_or_default()
}

/// Extract the `test_support` rlib path from one Cargo JSON message, if any.
fn test_support_rlib_in_message(line: &str) -> Option<PathBuf> {
let (name, rlibs) = compiler_artifact_rlibs(line)?;
(name == "test_support")
.then(|| rlibs.into_iter().next_back())
.flatten()
}

fn manifest_dir() -> PathBuf {
Expand All @@ -188,3 +234,89 @@ fn rustc() -> PathBuf {
fn stderr(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).into_owned()
}

/// A synthetic Cargo message with two rlibs in different directories,
/// mirroring a split `build.build-dir` layout.
const SPLIT_LAYOUT_MESSAGE: &str = r#"{"reason":"compiler-artifact","target":{"name":"anyhow"},"filenames":["/build/debug/deps/libanyhow-1.rlib","/target/debug/libanyhow-1.rlib"]}"#;

#[rstest]
fn parser_collects_every_rlib_directory_from_a_message() {
let parents = rlib_parents_in_message(SPLIT_LAYOUT_MESSAGE);
assert_eq!(
parents,
vec![
PathBuf::from("/build/debug/deps"),
PathBuf::from("/target/debug"),
],
"both rlib directories should be collected in message order"
);
}

#[rstest]
#[case::malformed_json("not json at all")]
#[case::other_reason(r#"{"reason":"build-script-executed","target":{"name":"anyhow"}}"#)]
#[case::missing_target(r#"{"reason":"compiler-artifact","filenames":["/a/lib.rlib"]}"#)]
fn parser_ignores_non_artifact_messages(#[case] line: &str) {
assert!(
rlib_parents_in_message(line).is_empty(),
"non-artifact input should yield no directories: {line:?}"
);
assert!(
test_support_rlib_in_message(line).is_none(),
"non-artifact input should yield no test_support rlib: {line:?}"
);
}

#[rstest]
fn parser_selects_the_test_support_rlib_by_target_name() {
let message = r#"{"reason":"compiler-artifact","target":{"name":"test_support"},"filenames":["/deps/libtest_support-1.rlib","/final/libtest_support.rlib"]}"#;
assert_eq!(
test_support_rlib_in_message(message),
Some(PathBuf::from("/final/libtest_support.rlib")),
"the last-listed rlib should win, matching Cargo's uplift ordering"
);
assert!(
test_support_rlib_in_message(SPLIT_LAYOUT_MESSAGE).is_none(),
"other targets' artefacts should not be mistaken for test_support"
);
}

/// Forcing a split `build.build-dir` must still yield a working harness:
/// the dependency rlibs land apart from the uplifted `test_support` rlib, so
/// the collected `-L dependency=` set has to span the split for the control
/// fixture to compile. This pins the regression where a single derived
/// directory missed the dependencies entirely.
#[rstest]
fn harness_compiles_under_a_split_build_dir() -> io::Result<()> {
// Both roots are private to this test: sharing the ambient target dir
// with the concurrently building `#[once]` fixture races on the
// uplifted rlibs and fails with version-skew errors (E0460).
let target_dir = tempfile::tempdir()?;
let build_dir = tempfile::tempdir()?;
let harness = TestSupportRlib::build_with(&[
("CARGO_TARGET_DIR", target_dir.path()),
("CARGO_BUILD_BUILD_DIR", build_dir.path()),
])?;

let spans_split_dir = harness
.deps_dirs
.iter()
.any(|dir| dir.starts_with(build_dir.path()));
if !spans_split_dir {
return Err(io::Error::other(format!(
"the dependency directories should include the split build dir {}; found {:?}",
build_dir.path().display(),
harness.deps_dirs,
)));
}

let output = harness.compile("tests/ui/stub_env_strict_compile_pass.rs")?;
if !output.status.success() {
return Err(io::Error::other(format!(
"the control fixture should compile under a split build dir:
{}",
stderr(&output),
)));
}
Ok(())
}
47 changes: 37 additions & 10 deletions tests/polonius_toolchain_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

#[path = "support/makefile.rs"]
mod makefile;
#[path = "support/shared_actions.rs"]
pub mod shared_actions;

use anyhow::{Context, Result, ensure};
use camino::Utf8Path;
Expand All @@ -22,14 +24,8 @@ use toml::Value as TomlValue;

const POLONIUS_FLAG: &str = "-Zpolonius=next";
const POLONIUS_VAR: &str = "$(POLONIUS_FLAGS)";
const SETUP_RUST_ACTION: &str = concat!(
"leynos/shared-actions/.github/actions/setup-rust@",
"2f90d1041ea108148be0620e3bbcc1fa80ac03e4"
);
const RUST_BUILD_RELEASE_ACTION: &str = concat!(
"leynos/shared-actions/.github/actions/rust-build-release@",
"2f90d1041ea108148be0620e3bbcc1fa80ac03e4"
);
const SETUP_RUST_ACTION: &str = "leynos/shared-actions/.github/actions/setup-rust";
const RUST_BUILD_RELEASE_ACTION: &str = "leynos/shared-actions/.github/actions/rust-build-release";
const WARNINGS_POLONIUS_RUSTFLAGS: &str = "-D warnings -Zpolonius=next";

/// Describes one workflow's shared-action and toolchain contract.
Expand Down Expand Up @@ -70,6 +66,36 @@ const PACKAGING_WORKFLOW: WorkflowExpectation = WorkflowExpectation {
pins_toolchain_env: false,
};

/// Every workflow under the shared-action toolchain contract.
const WORKFLOW_EXPECTATIONS: [WorkflowExpectation; 4] = [
CI_WORKFLOW,
NETSUKEFILE_WORKFLOW,
COVERAGE_WORKFLOW,
PACKAGING_WORKFLOW,
];

/// Returns the single shared-actions commit SHA the checked workflows pin.
///
/// The SHA's value is owned by the workflow files (and the pin-bump process
/// that updates them); this contract derives it rather than restating it, so
/// a complete bump stays green while a partial bump — some workflows moved,
/// others left behind — fails on the disagreement. The broader shape-only
/// sweep across every workflow lives in `workflow_shared_actions_pins`.
fn shared_actions_sha() -> Result<String> {
let mut refs = Vec::new();
for expectation in WORKFLOW_EXPECTATIONS {
let contents = read_repo_file(Utf8Path::new(expectation.path))?;
let extracted = shared_actions::extract_shared_actions_uses(&contents);
ensure!(
!extracted.is_empty(),
"{} should pin at least one shared action",
expectation.path
);
refs.extend(extracted);
}
shared_actions::consistent_pin(&refs)
}

/// Returns the dated nightly channel pinned in `rust-toolchain.toml`.
///
/// The workflow assertions compare against this value so a future pin move
Expand Down Expand Up @@ -147,10 +173,11 @@ fn workflows_pass_polonius_rustflags_to_shared_actions(
let WorkflowExpectation {
path,
job,
action: expected_action,
action,
rustflags: expected_rustflags,
pins_toolchain_env,
} = expectation;
let expected_action = &format!("{action}@{}", shared_actions_sha()?);
let workflow: YamlValue = serde_yaml::from_str(&read_repo_file(Utf8Path::new(path))?)
.with_context(|| format!("parse {path}"))?;
ensure!(
Expand All @@ -165,7 +192,7 @@ fn workflows_pass_polonius_rustflags_to_shared_actions(
.with_context(|| format!("{path} job {job} should declare steps"))?;
let shared_action = steps
.iter()
.find(|step| yaml_str(step, &["uses"]) == Some(expected_action))
.find(|step| yaml_str(step, &["uses"]) == Some(expected_action.as_str()))
.with_context(|| format!("{path} job {job} should use {expected_action}"))?;
let rustflags = yaml_str(shared_action, &["with", "rustflags"])
.with_context(|| format!("{path} {expected_action} should pass rustflags"))?;
Expand Down
Loading
Loading