Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"targeted_mechanism": "none",
"kind": "harness",
"slice_class": "non_ranking",
"fixtures": [
"harness-goldens"
]
}
25 changes: 19 additions & 6 deletions crates/aft/src/lsp/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7208,11 +7208,12 @@ pub fn walk_applicable_area(
})
}

/// Test hook: stretch the applicability walk so a test can prove the walk
/// does not block other users of the language-server manager. Debug builds
/// only. `AFT_TEST_APPLICABILITY_WALK_DELAY_MS` is the delay in milliseconds;
/// when `AFT_TEST_APPLICABILITY_WALK_SIGNAL` names a file, it is written as the
/// delay starts so the test knows the walk is under way.
/// Test hook: hold the applicability walk so a test can prove that configure
/// does not wait for it. Debug builds only. `AFT_TEST_APPLICABILITY_WALK_DELAY_MS`
/// caps the hold in milliseconds. When `AFT_TEST_APPLICABILITY_WALK_SIGNAL`
/// names a file, it is written as the hold starts; when
/// `AFT_TEST_APPLICABILITY_WALK_RELEASE` names a file, the hook waits for it to
/// appear or until the cap expires.
fn delay_applicability_walk_for_test() {
#[cfg(debug_assertions)]
if let Some(delay_ms) = std::env::var("AFT_TEST_APPLICABILITY_WALK_DELAY_MS")
Expand All @@ -7222,7 +7223,19 @@ fn delay_applicability_walk_for_test() {
if let Some(signal) = std::env::var_os("AFT_TEST_APPLICABILITY_WALK_SIGNAL") {
let _ = std::fs::write(signal, b"walking");
}
std::thread::sleep(Duration::from_millis(delay_ms));
if let Some(release) = std::env::var_os("AFT_TEST_APPLICABILITY_WALK_RELEASE") {
let release = PathBuf::from(release);
let deadline = Instant::now() + Duration::from_millis(delay_ms);
while !release.exists() {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
break;
}
std::thread::sleep(remaining.min(Duration::from_millis(20)));
}
} else {
std::thread::sleep(Duration::from_millis(delay_ms));
}
}
}

Expand Down
68 changes: 32 additions & 36 deletions crates/aft/tests/integration/configure_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1047,58 +1047,54 @@ fn configure_rejects_malformed_lsp_paths_extra() {
assert!(shutdown.success());
}

/// Configure used to walk the whole project on its bind path to decide which
/// language servers apply, so a large or busy checkout held the configure
/// reply, and with it a route bind's deadline, for as long as the walk took.
/// The walk now runs after the reply, on its own thread.
/// Configure's project applicability walk runs on its own thread. Holding the
/// walk at entry while waiting for configure's reply proves that the reply does
/// not depend on the walk completing.
#[test]
fn configure_answers_before_the_language_server_applicability_walk() {
let temp_dir = tempfile::tempdir().expect("create fixture");
let project = temp_dir.path().join("project");
fs::create_dir_all(project.join("src")).expect("create project");
fs::write(project.join("src/main.rs"), "fn main() {}\n").expect("write source");
let signal = temp_dir.path().join("walk-started");
let release = temp_dir.path().join("release-walk");
let mut aft = AftProcess::spawn_with_env(&[
(
"AFT_TEST_APPLICABILITY_WALK_DELAY_MS",
std::ffi::OsStr::new("10000"),
std::ffi::OsStr::new("120000"),
),
("AFT_TEST_APPLICABILITY_WALK_SIGNAL", signal.as_os_str()),
("AFT_TEST_APPLICABILITY_WALK_RELEASE", release.as_os_str()),
]);

let started = Instant::now();
let configure = aft.send_with_timeout(
&json!({
"id": "cfg-applicability-walk",
"command": "configure",
"harness": "opencode",
"project_root": project,
"config": user_config(json!({
"indexes": { "trigram": false, "semantic": false, "callgraph": false }
})),
})
.to_string(),
Duration::from_secs(30),
);
let elapsed = started.elapsed();
let request = json!({
"id": "cfg-applicability-walk",
"command": "configure",
"harness": "opencode",
"project_root": project,
"config": user_config(json!({
"indexes": { "trigram": false, "semantic": false, "callgraph": false }
})),
})
.to_string();
thread::scope(|scope| {
let configure = scope.spawn(|| aft.send_with_timeout(&request, Duration::from_secs(30)));
let deadline = Instant::now() + Duration::from_secs(30);
while !signal.exists() {
assert!(
Instant::now() < deadline,
"the applicability walk never started after configure"
);
thread::sleep(Duration::from_millis(20));
}

assert_eq!(
configure["success"], true,
"configure failed: {configure:#}"
);
assert!(
elapsed < Duration::from_secs(1),
"configure waited {elapsed:?} for a 10 s applicability walk"
);
// The walk still runs, after the reply.
let deadline = Instant::now() + Duration::from_secs(10);
while !signal.exists() {
assert!(
Instant::now() < deadline,
"the applicability walk never started after configure"
let configure = configure.join().expect("configure request thread panicked");
assert_eq!(
configure["success"], true,
"configure failed: {configure:#}"
);
thread::sleep(Duration::from_millis(20));
}
fs::write(&release, b"release").expect("release applicability walk");
});
let shutdown = aft.shutdown();
assert!(shutdown.success());
}
Loading