Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
67 changes: 51 additions & 16 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 Down Expand Up @@ -108,18 +108,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,14 +140,44 @@ 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 parent directories of every rlib in one Cargo JSON message.
fn rlib_parents_in_message(line: &str) -> Vec<PathBuf> {
let Ok(message) = serde_json::from_str::<serde_json::Value>(line) else {
return Vec::new();
};
if message
.get("reason")
.is_none_or(|reason| reason != "compiler-artifact")
{
return Vec::new();
}
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"))
})
.filter_map(|filename| Path::new(filename).parent().map(Path::to_path_buf))
.collect()
}

/// Extract the `test_support` rlib path from one Cargo JSON message, if any.
fn test_support_rlib_in_message(line: &str) -> Option<PathBuf> {
let message: serde_json::Value = serde_json::from_str(line).ok()?;
Expand Down
74 changes: 64 additions & 10 deletions tests/polonius_toolchain_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,10 @@ 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";
const SHARED_ACTIONS_MARKER: &str = "leynos/shared-actions/";

/// Describes one workflow's shared-action and toolchain contract.
struct WorkflowExpectation {
Expand Down Expand Up @@ -70,6 +65,64 @@ 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,
];

/// Extracts every shared-actions commit pin from workflow `contents`.
fn shared_action_pins(contents: &str) -> Vec<String> {
contents
.split(SHARED_ACTIONS_MARKER)
.skip(1)
.filter_map(|piece| piece.split_once('@'))
.map(|(_action, rest)| {
rest.chars()
.take_while(char::is_ascii_hexdigit)
.collect::<String>()
})
.collect()
}

/// 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.
fn shared_actions_sha() -> Result<String> {
let mut shas = std::collections::BTreeSet::new();
for expectation in WORKFLOW_EXPECTATIONS {
let contents = read_repo_file(Utf8Path::new(expectation.path))?;
let pins = shared_action_pins(&contents);
ensure!(
!pins.is_empty(),
"{} should pin at least one shared action",
expectation.path
);
shas.extend(pins);
}
ensure!(
shas.len() == 1,
"the checked workflows disagree on the shared-actions pin: {shas:?}"
);
let sha = shas
.into_iter()
.next()
.context("one shared-actions pin should remain")?;
ensure!(
sha.len() == 40
&& sha
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
"the shared-actions pin should be a 40-character lowercase commit SHA, found {sha:?}"
);
Ok(sha)
}

/// 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 +200,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 +219,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