Skip to content

Commit f7427db

Browse files
authored
fix(ci): fix stale artifact caching in prebuild step (#1838)
# What does this PR do? `inner_build_artifact` had a simple `if artifact_path.exists() { return }` check that skipped cargo build entirely when the binary file was on disk, even if source code had changed. This caused bin_test tests to run against stale binaries after code edits. **Changes** `rebuild_artifacts` (used by the prebuild step): Always invokes cargo build, letting cargo's own dependency tracking handle staleness. `fetch_built_artifacts` (used by tests): Only looks up the prebuilt artifact path. If it doesn't exist, the test fails with a message telling the developer to add the artifact to `all_prebuild_artifacts()`. No silent fallback building that would hide missing prebuild entries and slow down CI. # Motivation Noticed bin_tests running against stale binaries here: https://gitlab.ddbuild.io/DataDog/apm-reliability/libddprof-build/-/jobs/1559430494 [[PROF-14184] ](https://datadoghq.atlassian.net/jira/software/c/projects/PROF/boards/11?selectedIssue=PROF-14184) # Additional Notes Anything else we should know when reviewing? # How to test the change? Run `cargo nextest run` locally with clean artifacts directory, then making changes to observe rebuild of stale artifacts, then again with no changes to observe cargo handling of non-stale files [in progress]: doing the same combination of runs as the above local verification but in CI Co-authored-by: gyuheon.oh <gyuheon.oh@datadoghq.com>
1 parent ce9506c commit f7427db

5 files changed

Lines changed: 59 additions & 46 deletions

File tree

bin_tests/src/bin/prebuild.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
//!
1010
//! Run with: cargo run -p bin_tests --bin prebuild
1111
12-
use bin_tests::{artifacts, build_artifacts};
12+
use bin_tests::{artifacts, rebuild_artifacts};
1313

1414
fn main() -> anyhow::Result<()> {
1515
println!("Pre-building bin_tests artifacts...");
@@ -20,7 +20,7 @@ fn main() -> anyhow::Result<()> {
2020

2121
// Build all artifacts
2222
let start = std::time::Instant::now();
23-
build_artifacts(&artifact_refs)?;
23+
rebuild_artifacts(&artifact_refs)?;
2424
let elapsed = start.elapsed();
2525

2626
println!(

bin_tests/src/lib.rs

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,10 @@ pub fn compute_artifact_path(c: &ArtifactsBuild) -> anyhow::Result<PathBuf> {
7979
/// This crate implements an abstraction over compilation with cargo with the purpose
8080
/// of testing full binaries or dynamic libraries, instead of just rust static libraries.
8181
///
82-
/// The main entrypoint is `fn build_artifacts` which takes a list of artifacts to build,
83-
/// either executable crates, cdylib, or extra binaries, invokes cargo and return the path
84-
/// of the built artifact.
85-
///
86-
/// Builds are cached between invocations so that multiple tests can use the same artifact
87-
/// without doing expensive work twice.
82+
/// Artifacts are built upfront by the prebuild step via [`rebuild_artifacts`], which
83+
/// invokes `cargo build` and lets cargo handle staleness. Tests then use
84+
/// [`fetch_built_artifacts`] to look up the prebuilt paths, failing if any are missing
85+
/// (indicating the prebuild script needs to be updated).
8886
///
8987
/// It is assumed that functions in this crate are invoked in the context of a cargo #[test]
9088
/// item, or a `cargo run` command to be able to locate artifacts built by cargo from the position
@@ -115,15 +113,9 @@ pub struct ArtifactsBuild {
115113
pub panic_abort: Option<bool>,
116114
}
117115

118-
fn inner_build_artifact(c: &ArtifactsBuild) -> anyhow::Result<PathBuf> {
119-
// Compute the expected artifact path first
116+
fn cargo_build_artifact(c: &ArtifactsBuild) -> anyhow::Result<PathBuf> {
120117
let artifact_path = compute_artifact_path(c)?;
121118

122-
// If the artifact already exists, skip building
123-
if artifact_path.exists() {
124-
return Ok(artifact_path);
125-
}
126-
127119
let mut build_cmd = process::Command::new(env!("CARGO"));
128120
build_cmd.arg("build");
129121

@@ -167,29 +159,49 @@ fn inner_build_artifact(c: &ArtifactsBuild) -> anyhow::Result<PathBuf> {
167159
Ok(artifact_path)
168160
}
169161

170-
/// Caches and returns the path of the artifacts built by cargo
171-
/// This function should only be called from cargo tests
172-
pub fn build_artifacts<'b>(
162+
/// Returns the paths of prebuilt artifacts, failing if any are missing.
163+
/// Tests should use this: if an artifact doesn't exist, it means the prebuild
164+
/// script needs to be updated rather than silently building inline (which would
165+
/// slow down CI).
166+
pub fn fetch_built_artifacts<'b>(
167+
crates: &[&'b ArtifactsBuild],
168+
) -> anyhow::Result<HashMap<&'b ArtifactsBuild, PathBuf>> {
169+
let mut res = HashMap::new();
170+
for &c in crates {
171+
let artifact_path = compute_artifact_path(c)?;
172+
anyhow::ensure!(
173+
artifact_path.exists(),
174+
"Prebuilt artifact not found at {path}. \
175+
Add this artifact to `artifacts::all_prebuild_artifacts()` \
176+
and re-run the prebuild step.",
177+
path = artifact_path.display(),
178+
);
179+
res.insert(c, artifact_path);
180+
}
181+
Ok(res)
182+
}
183+
184+
/// Invokes `cargo build` for each artifact, letting cargo's own dependency
185+
/// tracking decide whether recompilation is needed.
186+
pub fn rebuild_artifacts<'b>(
173187
crates: &[&'b ArtifactsBuild],
174188
) -> anyhow::Result<HashMap<&'b ArtifactsBuild, PathBuf>> {
175189
static ARTIFACTS: OnceCell<Mutex<HashMap<ArtifactsBuild, PathBuf>>> = OnceCell::new();
176190

177191
let mut res = HashMap::new();
178-
179192
let artifacts = ARTIFACTS.get_or_init(|| Mutex::new(HashMap::new()));
180193
for &c in crates {
181194
let mut artifacts = artifacts.lock().unwrap();
182195
let artifacts = artifacts.deref_mut();
183196

184-
if artifacts.contains_key(c) {
185-
res.insert(c, artifacts.get(c).unwrap().clone());
197+
if let Some(p) = artifacts.get(c) {
198+
res.insert(c, p.clone());
186199
} else {
187-
let p = inner_build_artifact(c)?;
200+
let p = cargo_build_artifact(c)?;
188201
res.insert(c, p.clone());
189202
artifacts.insert(c.clone(), p);
190203
}
191204
}
192-
193205
Ok(res)
194206
}
195207

bin_tests/src/test_runner.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
88
use crate::{
99
artifacts::StandardArtifacts,
10-
build_artifacts,
10+
fetch_built_artifacts,
1111
test_types::{CrashType, TestMode},
1212
validation::{read_and_parse_crash_payload, validate_std_outputs, PayloadValidator},
1313
ArtifactsBuild, BuildProfile,
@@ -170,7 +170,7 @@ where
170170
/// # Example
171171
/// ```no_run
172172
/// use bin_tests::test_runner::run_custom_crash_test;
173-
/// use bin_tests::{build_artifacts, ArtifactType, ArtifactsBuild, BuildProfile};
173+
/// use bin_tests::{fetch_built_artifacts, ArtifactType, ArtifactsBuild, BuildProfile};
174174
///
175175
/// # fn main() -> anyhow::Result<()> {
176176
/// let receiver = ArtifactsBuild {
@@ -189,7 +189,7 @@ where
189189
/// ..Default::default()
190190
/// };
191191
///
192-
/// let artifacts_map = build_artifacts(&[&receiver, &crashing_app])?;
192+
/// let artifacts_map = fetch_built_artifacts(&[&receiver, &crashing_app])?;
193193
///
194194
/// run_custom_crash_test(
195195
/// &artifacts_map[&crashing_app],
@@ -247,7 +247,7 @@ where
247247
/// (preload allocation detector). It just runs the binary and waits.
248248
pub fn run_crash_no_op(config: &CrashTestConfig) -> Result<()> {
249249
let artifacts = StandardArtifacts::new(config.profile);
250-
let artifacts_map = build_artifacts(&artifacts.as_slice())?;
250+
let artifacts_map = fetch_built_artifacts(&artifacts.as_slice())?;
251251
let fixtures = TestFixtures::new()?;
252252

253253
let mut cmd = process::Command::new(&artifacts_map[&artifacts.crashtracker_bin]);

bin_tests/tests/crashtracker_bin_test.rs

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use anyhow::Context;
1414
use bin_tests::test_runner::run_crash_no_op;
1515
use bin_tests::{
1616
artifacts::{self, StandardArtifacts},
17-
build_artifacts,
17+
fetch_built_artifacts,
1818
test_runner::{run_crash_test_with_artifacts, CrashTestConfig, ValidatorFn},
1919
test_types::{CrashType, TestMode},
2020
validation::PayloadValidator,
@@ -70,7 +70,7 @@ fn run_standard_crash_test_refactored(
7070
) {
7171
let config = CrashTestConfig::new(profile, mode, crash_type);
7272
let artifacts = StandardArtifacts::new(config.profile);
73-
let artifacts_map = build_artifacts(&artifacts.as_slice()).unwrap();
73+
let artifacts_map = fetch_built_artifacts(&artifacts.as_slice()).unwrap();
7474

7575
let crash_type_str = crash_type.as_str();
7676
let validator: ValidatorFn = Box::new(move |payload, fixtures| {
@@ -107,7 +107,7 @@ fn test_crash_tracking_bin_errno_preservation() {
107107
CrashType::NullDeref,
108108
);
109109
let artifacts = StandardArtifacts::new(config.profile);
110-
let artifacts_map = build_artifacts(&artifacts.as_slice()).unwrap();
110+
let artifacts_map = fetch_built_artifacts(&artifacts.as_slice()).unwrap();
111111

112112
let validator: ValidatorFn = Box::new(|_payload, fixtures| {
113113
let status_path = fixtures.output_dir.join(ERRNO_STATUS_FILENAME);
@@ -132,7 +132,7 @@ fn test_crash_tracking_bin_unhandled_exception() {
132132
CrashType::UnhandledException,
133133
);
134134
let artifacts = StandardArtifacts::new(config.profile);
135-
let artifacts_map = build_artifacts(&artifacts.as_slice()).unwrap();
135+
let artifacts_map = fetch_built_artifacts(&artifacts.as_slice()).unwrap();
136136

137137
let validator: ValidatorFn = Box::new(|payload, fixtures| {
138138
PayloadValidator::new(payload)
@@ -168,7 +168,7 @@ fn test_crash_tracking_bin_runtime_callback_frame() {
168168
CrashType::NullDeref,
169169
);
170170
let artifacts = StandardArtifacts::new(config.profile);
171-
let artifacts_map = build_artifacts(&artifacts.as_slice()).unwrap();
171+
let artifacts_map = fetch_built_artifacts(&artifacts.as_slice()).unwrap();
172172

173173
let validator: ValidatorFn = Box::new(|payload, fixtures| {
174174
PayloadValidator::new(payload).validate_counters()?;
@@ -198,7 +198,7 @@ fn test_crash_tracking_thread_name() {
198198
CrashType::NullDeref,
199199
);
200200
let artifacts = StandardArtifacts::new(config.profile);
201-
let artifacts_map = build_artifacts(&artifacts.as_slice()).unwrap();
201+
let artifacts_map = fetch_built_artifacts(&artifacts.as_slice()).unwrap();
202202

203203
let validator: ValidatorFn = Box::new(|payload, _fixtures| {
204204
let error = &payload["error"];
@@ -231,7 +231,7 @@ fn test_crash_tracking_bin_runtime_callback_string() {
231231
CrashType::NullDeref,
232232
);
233233
let artifacts = StandardArtifacts::new(config.profile);
234-
let artifacts_map = build_artifacts(&artifacts.as_slice()).unwrap();
234+
let artifacts_map = fetch_built_artifacts(&artifacts.as_slice()).unwrap();
235235

236236
let validator: ValidatorFn = Box::new(|payload, fixtures| {
237237
PayloadValidator::new(payload).validate_counters()?;
@@ -260,7 +260,7 @@ fn test_crash_tracking_bin_no_runtime_callback() {
260260
CrashType::NullDeref,
261261
);
262262
let artifacts = StandardArtifacts::new(config.profile);
263-
let artifacts_map = build_artifacts(&artifacts.as_slice()).unwrap();
263+
let artifacts_map = fetch_built_artifacts(&artifacts.as_slice()).unwrap();
264264

265265
let validator: ValidatorFn = Box::new(|payload, fixtures| {
266266
PayloadValidator::new(payload).validate_counters()?;
@@ -338,7 +338,7 @@ fn test_crash_tracking_bin_runtime_callback_frame_invalid_utf8() {
338338
CrashType::NullDeref,
339339
);
340340
let artifacts = StandardArtifacts::new(config.profile);
341-
let artifacts_map = build_artifacts(&artifacts.as_slice()).unwrap();
341+
let artifacts_map = fetch_built_artifacts(&artifacts.as_slice()).unwrap();
342342

343343
let validator: ValidatorFn = Box::new(|payload, fixtures| {
344344
PayloadValidator::new(payload).validate_counters()?;
@@ -380,7 +380,7 @@ fn test_crash_tracking_errors_intake_upload() {
380380
.with_env("DD_CRASHTRACKING_ERRORS_INTAKE_ENABLED", "true");
381381

382382
let artifacts = StandardArtifacts::new(config.profile);
383-
let artifacts_map = build_artifacts(&artifacts.as_slice()).unwrap();
383+
let artifacts_map = fetch_built_artifacts(&artifacts.as_slice()).unwrap();
384384

385385
let validator: ValidatorFn = Box::new(|_payload, fixtures| {
386386
let errors_intake_path = fixtures.crash_profile_path.with_extension("errors");
@@ -413,7 +413,7 @@ fn test_crash_tracking_errors_intake_crash_ping() {
413413
.with_env("DD_CRASHTRACKING_ERRORS_INTAKE_ENABLED", "true");
414414

415415
let artifacts = StandardArtifacts::new(config.profile);
416-
let artifacts_map = build_artifacts(&artifacts.as_slice()).unwrap();
416+
let artifacts_map = fetch_built_artifacts(&artifacts.as_slice()).unwrap();
417417

418418
let validator: ValidatorFn = Box::new(|_payload, fixtures| {
419419
let errors_intake_path = fixtures.crash_profile_path.with_extension("errors");
@@ -444,7 +444,7 @@ fn test_crash_tracking_errors_intake_crash_info_parity() {
444444
.with_env("DD_CRASHTRACKING_ERRORS_INTAKE_ENABLED", "true");
445445

446446
let artifacts = StandardArtifacts::new(config.profile);
447-
let artifacts_map = build_artifacts(&artifacts.as_slice()).unwrap();
447+
let artifacts_map = fetch_built_artifacts(&artifacts.as_slice()).unwrap();
448448

449449
let validator: ValidatorFn = Box::new(|crash_info_payload, fixtures| {
450450
let errors_intake_path = fixtures.crash_profile_path.with_extension("errors");
@@ -638,7 +638,7 @@ fn test_crash_tracking_app(crash_type: &str) {
638638
let crashtracker_receiver = artifacts::crashtracker_receiver(BuildProfile::Release);
639639
let crashing_app = artifacts::crashing_app(BuildProfile::Debug, true);
640640

641-
let artifacts_map = build_artifacts(&[&crashtracker_receiver, &crashing_app]).unwrap();
641+
let artifacts_map = fetch_built_artifacts(&[&crashtracker_receiver, &crashing_app]).unwrap();
642642

643643
// Create validator based on crash type
644644
let crash_type_owned = crash_type.to_owned();
@@ -712,7 +712,8 @@ fn test_panic_hook_mode(mode: &str, expected_category: &str, expected_panic_mess
712712
let crashtracker_receiver = artifacts::crashtracker_receiver(BuildProfile::Release);
713713
let crashtracker_bin_test = artifacts::crashtracker_bin_test(BuildProfile::Debug, true);
714714

715-
let artifacts_map = build_artifacts(&[&crashtracker_receiver, &crashtracker_bin_test]).unwrap();
715+
let artifacts_map =
716+
fetch_built_artifacts(&[&crashtracker_receiver, &crashtracker_bin_test]).unwrap();
716717

717718
let expected_category = expected_category.to_owned();
718719
let expected_panic_message = expected_panic_message.map(|s| s.to_owned());
@@ -785,7 +786,7 @@ fn test_crash_tracking_callstack() {
785786
// compile in debug so we avoid inlining and can check the callchain
786787
let crashing_app = artifacts::crashing_app(BuildProfile::Debug, false);
787788

788-
let artifacts_map = build_artifacts(&[&crashtracker_receiver, &crashing_app]).unwrap();
789+
let artifacts_map = fetch_built_artifacts(&[&crashtracker_receiver, &crashing_app]).unwrap();
789790

790791
// Note: in Release, we do not have the crate and module name prepended to the function name
791792
// Here we compile the crashing app in Debug.
@@ -1532,7 +1533,7 @@ fn test_receiver_emits_debug_logs_on_receiver_issue() -> anyhow::Result<()> {
15321533
use std::time::Duration;
15331534

15341535
let receiver = artifacts::crashtracker_receiver(BuildProfile::Debug);
1535-
let artifacts = build_artifacts(&[&receiver])?;
1536+
let artifacts = fetch_built_artifacts(&[&receiver])?;
15361537
let fixtures = bin_tests::test_runner::TestFixtures::new()?;
15371538

15381539
let missing_file = fixtures.output_dir.join("missing_additional_file.txt");
@@ -1808,7 +1809,7 @@ struct TestFixtures<'a> {
18081809
}
18091810

18101811
fn setup_test_fixtures<'a>(crates: &[&'a ArtifactsBuild]) -> TestFixtures<'a> {
1811-
let artifacts = build_artifacts(crates).unwrap();
1812+
let artifacts = fetch_built_artifacts(crates).unwrap();
18121813

18131814
let tmpdir = tempfile::TempDir::new().unwrap();
18141815
let dirpath = tmpdir.path();

bin_tests/tests/test_the_tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
use std::{fs, process};
77

8-
use bin_tests::{build_artifacts, ArtifactType, ArtifactsBuild, BuildProfile};
8+
use bin_tests::{fetch_built_artifacts, ArtifactType, ArtifactsBuild, BuildProfile};
99

1010
#[test]
1111
#[cfg_attr(miri, ignore)]
@@ -38,7 +38,7 @@ fn test_the_tests_inner(profile: BuildProfile) {
3838
},
3939
&test_the_tests,
4040
];
41-
let artifacts = build_artifacts(crates).unwrap();
41+
let artifacts = fetch_built_artifacts(crates).unwrap();
4242

4343
for c in crates {
4444
assert!(fs::metadata(&artifacts[c]).unwrap().file_type().is_file());

0 commit comments

Comments
 (0)