mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
fix(test): synchronize plugin diagnostics deadline coverage (#2069)
A diagnostics test assumed a cold plugin actor would enter its provider within 10 ms, which failed on slower CI. The fixture is prewarmed and uses synchronous entry gates, so the tests tell a skipped request from a provider that started and hold the provider past the unchanged deadline; adds a test that cancelling a running provider doesn't affect another window's command. Co-authored-by: arnaud-ritti <[email protected]>
This commit is contained in:
1 parent
ccb53af564
commit
3ee5471ca2
4 files changed
+105
-23
No files matched your search
@@ -1,11 +1,11 @@
|
||||
//! Real Wasm provider dispatch and the document-edit path used locally and over RPC.
|
||||
use ide_core::{CancellationToken, ChangeSet, Diagnostic, IdeEvent, Pos, Range, TextChange};
|
||||
use ide_host::plugins::{Consent, HostWorkspace, PluginHost, set_editor_providers};
|
||||
use ide_host::plugins::{Consent, HostWorkspace, PluginEvent, PluginHost, set_editor_providers};
|
||||
use ide_host::{Host, HostConfig, ops};
|
||||
use ide_plugin::{Invocation, Limits, ProviderDocument};
|
||||
use parking_lot::Mutex;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, mpsc};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const ID: &str = "test.diagnostics";
|
||||
@@ -86,27 +86,110 @@ fn diagnostics_is_an_api_1_3_addition() {
|
||||
assert!(host.install_from_dir(&package, &Consent::all()).unwrap_err().message.contains("new in plugin API 1.3"));
|
||||
}
|
||||
|
||||
// The fixture signals from inside a host import, so the actor cannot finish
|
||||
// until this test releases it. A dropped guard also releases a failing test.
|
||||
enum FixtureEvent {
|
||||
Entered,
|
||||
Returned,
|
||||
}
|
||||
|
||||
struct FixtureGate {
|
||||
events: mpsc::Receiver<FixtureEvent>,
|
||||
finished: mpsc::Sender<FixtureEvent>,
|
||||
release: mpsc::Sender<()>,
|
||||
}
|
||||
|
||||
impl FixtureGate {
|
||||
fn new(dir: &Path, limits: Limits) -> (Arc<PluginHost>, Self) {
|
||||
let (started, events) = mpsc::channel();
|
||||
let finished = started.clone();
|
||||
let (release, released) = mpsc::channel();
|
||||
let released = Mutex::new(released);
|
||||
let sink = Arc::new(move |_: Option<&str>, event| {
|
||||
if matches!(event, PluginEvent::ContextChanged { ref plugin_id, ref key, value: Some(_), .. } if plugin_id == ID && key == "fixture-gate")
|
||||
{
|
||||
started.send(FixtureEvent::Entered).unwrap();
|
||||
released.lock().recv_timeout(Duration::from_secs(5)).unwrap();
|
||||
}
|
||||
});
|
||||
let host = Arc::new(PluginHost::new(dir.join("plugins"), sink, limits).unwrap());
|
||||
host.install_from_dir(&fixture(), &Consent::all()).unwrap();
|
||||
assert_eq!(host.run_command(ID, "calls", None, Invocation::default()).unwrap().as_deref(), Some("0"));
|
||||
(host, Self { events, finished, release })
|
||||
}
|
||||
|
||||
fn entered(&self) {
|
||||
assert!(matches!(self.events.recv_timeout(Duration::from_secs(5)).unwrap(), FixtureEvent::Entered));
|
||||
}
|
||||
|
||||
fn provider(
|
||||
&self,
|
||||
host: &Arc<PluginHost>,
|
||||
document: ProviderDocument,
|
||||
cancel: &CancellationToken,
|
||||
) -> std::thread::JoinHandle<Vec<(String, Vec<Diagnostic>)>> {
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
let (host, document, cancel, finished) =
|
||||
(host.clone(), document.clone(), cancel.clone(), self.finished.clone());
|
||||
let call = std::thread::spawn(move || {
|
||||
let result = host.provide_diagnostics(&document, &cancel);
|
||||
let _ = finished.send(FixtureEvent::Returned);
|
||||
result
|
||||
});
|
||||
if matches!(self.events.recv_timeout(Duration::from_secs(5)).unwrap(), FixtureEvent::Entered) {
|
||||
return call;
|
||||
}
|
||||
// Expiry before entry is valid, but cannot exercise a late answer.
|
||||
assert!(call.join().unwrap().is_empty());
|
||||
assert!(start.elapsed() < Duration::from_secs(5), "provider never entered the fixture");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FixtureGate {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.release.send(());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn late_and_cancelled_answers_are_dropped_without_disabling_the_plugin() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let host = plugins(tmp.path(), Limits { edit_soft: Duration::from_millis(10), ..generous() });
|
||||
assert!(host.provide_diagnostics(&doc(tmp.path(), "text", "slow BAD"), &CancellationToken::new()).is_empty());
|
||||
let soft = Duration::from_millis(10);
|
||||
let (host, gate) = FixtureGate::new(tmp.path(), Limits { edit_soft: soft, ..generous() });
|
||||
let call = gate.provider(&host, doc(tmp.path(), "text", "gate BAD"), &CancellationToken::new());
|
||||
// Entry was observed: hold this actual provider beyond its soft deadline.
|
||||
assert!(matches!(gate.events.recv_timeout(soft), Err(mpsc::RecvTimeoutError::Timeout)));
|
||||
drop(gate);
|
||||
assert!(call.join().unwrap().is_empty());
|
||||
let cancel = CancellationToken::new();
|
||||
cancel.cancel();
|
||||
assert!(host.provide_diagnostics(&doc(tmp.path(), "text", "BAD"), &cancel).is_empty());
|
||||
assert_eq!(host.run_command(ID, "calls", None, Invocation::default()).unwrap().as_deref(), Some("1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancelling_a_started_provider_drops_its_answer_without_disabling_the_plugin() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let (host, gate) = FixtureGate::new(tmp.path(), generous());
|
||||
let cancel = CancellationToken::new();
|
||||
let call = gate.provider(&host, doc(tmp.path(), "text", "gate BAD"), &cancel);
|
||||
cancel.cancel();
|
||||
drop(gate);
|
||||
assert!(call.join().unwrap().is_empty());
|
||||
assert_eq!(host.run_command(ID, "calls", None, Invocation::default()).unwrap().as_deref(), Some("1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancelling_a_queued_provider_does_not_interrupt_another_windows_command() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let host = plugins(tmp.path(), generous());
|
||||
host.run_command(ID, "calls", None, Invocation::default()).unwrap();
|
||||
let (host, gate) = FixtureGate::new(tmp.path(), generous());
|
||||
let waiter = {
|
||||
let host = host.clone();
|
||||
std::thread::spawn(move || host.run_command(ID, "wait", None, Invocation::default()))
|
||||
};
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
gate.entered();
|
||||
let cancel = CancellationToken::new();
|
||||
let canceller = {
|
||||
let cancel = cancel.clone();
|
||||
@@ -117,6 +200,7 @@ fn cancelling_a_queued_provider_does_not_interrupt_another_windows_command() {
|
||||
};
|
||||
assert!(host.provide_diagnostics(&doc(tmp.path(), "text", "BAD"), &cancel).is_empty());
|
||||
canceller.join().unwrap();
|
||||
drop(gate);
|
||||
assert_eq!(waiter.join().unwrap().unwrap().as_deref(), Some("finished"));
|
||||
assert_eq!(host.run_command(ID, "calls", None, Invocation::default()).unwrap().as_deref(), Some("0"));
|
||||
}
|
||||
@@ -124,14 +208,14 @@ fn cancelling_a_queued_provider_does_not_interrupt_another_windows_command() {
|
||||
#[test]
|
||||
fn editor_requests_expire_in_the_queue_without_running_after_a_command() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let host = plugins(tmp.path(), Limits { edit_soft: Duration::from_millis(10), ..generous() });
|
||||
host.run_command(ID, "calls", None, Invocation::default()).unwrap();
|
||||
let (host, gate) = FixtureGate::new(tmp.path(), Limits { edit_soft: Duration::from_millis(10), ..generous() });
|
||||
let waiter = {
|
||||
let host = host.clone();
|
||||
std::thread::spawn(move || host.run_command(ID, "wait", None, Invocation::default()))
|
||||
};
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
gate.entered();
|
||||
assert!(host.provide_diagnostics(&doc(tmp.path(), "text", "BAD"), &CancellationToken::new()).is_empty());
|
||||
drop(gate);
|
||||
assert_eq!(waiter.join().unwrap().unwrap().as_deref(), Some("finished"));
|
||||
assert_eq!(host.run_command(ID, "calls", None, Invocation::default()).unwrap().as_deref(), Some("0"));
|
||||
assert_eq!(host.info_of(ID).unwrap().stats.slow_calls, 1);
|
||||
@@ -140,20 +224,14 @@ fn editor_requests_expire_in_the_queue_without_running_after_a_command() {
|
||||
#[test]
|
||||
fn typing_on_cancels_a_running_provider_before_its_hard_deadline() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let host = plugins(tmp.path(), Limits { fuel: u64::MAX, ..generous() });
|
||||
host.run_command(ID, "calls", None, Invocation::default()).unwrap();
|
||||
let (host, gate) = FixtureGate::new(tmp.path(), Limits { fuel: u64::MAX, ..generous() });
|
||||
let cancel = CancellationToken::new();
|
||||
let canceller = {
|
||||
let cancel = cancel.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(Duration::from_millis(30));
|
||||
cancel.cancel();
|
||||
})
|
||||
};
|
||||
let call = gate.provider(&host, doc(tmp.path(), "text", "gate spin BAD"), &cancel);
|
||||
let started = Instant::now();
|
||||
assert!(host.provide_diagnostics(&doc(tmp.path(), "text", "spin BAD"), &cancel).is_empty());
|
||||
cancel.cancel();
|
||||
drop(gate);
|
||||
assert!(call.join().unwrap().is_empty());
|
||||
assert!(started.elapsed() < Duration::from_secs(2));
|
||||
canceller.join().unwrap();
|
||||
}
|
||||
|
||||
// This executable has one edit-path test because editor providers are installed once per process.
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
93247e39cda6a6174fa44dc27ea4b919efb563c5540b77937b740d2de94c7cbb
|
||||
3fe751d024341ab4f62aa30916c51708d371e52c8b9e7b7518332ed93b24395a
|
||||
+5
-1
@@ -11,6 +11,7 @@ impl Plugin for Fixture {
|
||||
fn run_command(&mut self, id: &str, _: &Invocation, arg: Option<&str>) -> Result<Option<String>, Error> {
|
||||
match id {
|
||||
"wait" => {
|
||||
host::set_context("fixture-gate", "wait").unwrap();
|
||||
std::thread::sleep(Duration::from_millis(400));
|
||||
Ok(Some("finished".into()))
|
||||
}
|
||||
@@ -28,6 +29,9 @@ impl Plugin for Fixture {
|
||||
impl DiagnosticsProvider for Fixture {
|
||||
fn diagnostics(&mut self, doc: &Document) -> Vec<Diagnostic> {
|
||||
self.calls += 1;
|
||||
if doc.text.starts_with("gate") {
|
||||
host::set_context("fixture-gate", self.calls.to_string()).unwrap();
|
||||
}
|
||||
if doc.text == "limits" {
|
||||
let mut diagnostic = Diagnostic::new(Range::default(), Severity::Hint, "m".repeat(5000));
|
||||
diagnostic.source = Some("s".repeat(200));
|
||||
@@ -39,7 +43,7 @@ impl DiagnosticsProvider for Fixture {
|
||||
if doc.text.starts_with("slow") {
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
}
|
||||
if doc.text.starts_with("spin") {
|
||||
if doc.text.starts_with("spin") || doc.text.starts_with("gate spin") {
|
||||
loop {
|
||||
std::hint::spin_loop();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user