Skip to main content

Mountain/Environment/
TestProvider.rs

1//! # TestProvider (Environment)
2//!
3//! RESPONSIBILITIES:
4//! - Implements [`TestController`](CommonLibrary::Testing::TestController) for
5//!   [`MountainEnvironment`]
6//! - Manages test discovery, execution, and result reporting
7//! - Handles test controller registration and lifecycle
8//! - Tracks test run progress and aggregates results
9//! - Provides sidecar proxy for extension-provided test frameworks
10//!
11//! ARCHITECTURAL ROLE:
12//! - Environment provider for testing functionality
13//! - Uses controller pattern: each extension can register its own test
14//!   controller
15//! - Controllers identified by unique `ControllerIdentifier` and scoped to
16//!   extensions
17//! - Integrates with [`IPCProvider`](CommonLibrary::IPC::IPCProvider) for RPC
18//!   to test runners
19//! - Stores controller state in `ApplicationState`
20//!
21//! TEST EXECUTION FLOW:
22//! 1. Extension registers test controller via `RegisterTestController`
23//! 2. Mountain calls `ResolveTests` to discover tests (async, emits
24//!    `TestItemAdded`)
25//! 3. Extension returns test tree structure with IDs, labels, children
26//! 4. UI requests to run tests via `RunTest` or `RunTests` with optional
27//!    `RunProfile`
28//! 5. Mountain forwards to extension's controller via RPC
29//! 6. Extension executes tests and reports progress via `TestRunStarted`,
30//!    `TestItemStarted`, `TestItemPassed`, `TestItemFailed`, `TestRunEnded`
31//!    events
32//! 7. Mountain aggregates results and emits to UI
33//!
34//! ERROR HANDLING:
35//! - Uses [`CommonError`](CommonLibrary::Error::CommonError) for all operations
36//! - Validates controller identifiers and run profile names (non-empty)
37//! - Controller state tracked in RwLock for thread-safe mutation
38//! - Unknown controller ID errors return `TestControllerNotFound`
39//! - Duplicate registration returns `InvalidArgument` error
40//!
41//! PERFORMANCE:
42//! - Test discovery is async and can be cancelled (drop sender)
43//! - Test run progress is streamed via events, avoiding blocking
44//! - Controller state uses RwLock for concurrent read access during test runs
45//! - TODO: Consider test result caching for quick re-runs
46//!
47//! VS CODE REFERENCE:
48//! - `vs/workbench/contrib/testing/common/testService.ts` - test service
49//!   architecture
50//! - `vs/workbench/contrib/testing/common/testController.ts` - test controller
51//!   interface
52//! - `vs/workbench/contrib/testing/common/testTypes.ts` - test data models
53//! - `vs/workbench/contrib/testing/browser/testingView.ts` - testing UI panel
54//!
55//! TODO:
56//! - Implement test run cancellation and timeout handling
57//! - Add test result caching and quick re-run support
58//! - Implement test tree filtering and search
59//! - Add test run configuration persistence
60//! - Support test run peeking (inline results in editor)
61//! - Implement test coverage integration
62//! - Add test run duration metrics and profiling
63//! - Support parallel test execution (multiple workers)
64//! - Implement test run retry logic for flaky tests
65//! - Add test run export (JUnit, TAP, custom formats)
66//! - Integrate with CodeLens for in-source test run buttons
67//!
68//! MODULE CONTENTS:
69//! - [`TestController`](CommonLibrary::Testing::TestController) implementation:
70//! - `RegisterTestController` - register extension's controller
71//! - `UnregisterTestController` - remove controller
72//! - `ResolveTests` - discover tests (async with cancellation)
73//! - `RunTest` - run single test by ID
74//! - `RunTests` - run multiple tests (by ID or all in parent)
75//! - `StopTestRun` - cancel ongoing test run
76//! - `DidTestItemDiscoveryStart` - discovery progress event
77//!   - `TestRunStarted`/`TestItemStarted`/`TestItemPassed`/`TestItemFailed`/
78//!     `TestRunEnded` - events
79//! - Data types: `TestControllerState`, `TestItemState`, `TestRunProfile`,
80//!   `TestResultState`
81
82use std::{collections::HashMap, sync::Arc};
83
84use CommonLibrary::{
85	Environment::Requires::Requires,
86	Error::CommonError::CommonError,
87	IPC::{DTO::ProxyTarget::ProxyTarget, IPCProvider::IPCProvider},
88	Testing::TestController::TestController,
89};
90use async_trait::async_trait;
91use serde::{Deserialize, Serialize};
92use serde_json::{Value, json};
93use tauri::Emitter;
94use uuid::Uuid;
95
96use super::MountainEnvironment::MountainEnvironment;
97use crate::dev_log;
98
99/// Represents a test controller's state
100#[derive(Debug, Clone, Serialize, Deserialize)]
101struct TestControllerState {
102	pub ControllerIdentifier:String,
103
104	pub Label:String,
105
106	pub SideCarIdentifier:Option<String>,
107
108	pub IsActive:bool,
109
110	pub SupportedTestTypes:Vec<String>,
111}
112
113/// Represents the status of a test run
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
115enum TestRunStatus {
116	Queued,
117
118	Running,
119
120	Passed,
121
122	Failed,
123
124	Skipped,
125
126	Errored,
127}
128
129/// Represents a test result
130#[derive(Debug, Clone, Serialize, Deserialize)]
131struct TestResult {
132	pub TestIdentifier:String,
133
134	pub FullName:String,
135
136	pub Status:TestRunStatus,
137
138	pub DurationMs:Option<u64>,
139
140	pub ErrorMessage:Option<String>,
141
142	pub StackTrace:Option<String>,
143}
144
145/// Represents an active test run
146#[derive(Debug, Clone)]
147struct TestRun {
148	pub RunIdentifier:String,
149
150	pub ControllerIdentifier:String,
151
152	pub Status:TestRunStatus,
153
154	pub StartedAt:std::time::Instant,
155
156	pub Results:HashMap<String, TestResult>,
157}
158
159/// Stores test provider state
160#[derive(Debug)]
161pub struct TestProviderState {
162	pub Controllers:HashMap<String, TestControllerState>,
163
164	pub ActiveRuns:HashMap<String, TestRun>,
165}
166
167impl TestProviderState {
168	pub fn new() -> Self { Self { Controllers:HashMap::new(), ActiveRuns:HashMap::new() } }
169}
170
171#[async_trait]
172impl TestController for MountainEnvironment {
173	/// Registers a new test controller from an extension (e.g., Cocoon).
174	///
175	/// This method creates a TestControllerState entry and notifies the
176	/// frontend about the available test controller.
177	async fn RegisterTestController(&self, ControllerId:String, Label:String) -> Result<(), CommonError> {
178		dev_log!(
179			"extensions",
180			"[TestProvider] Registering test controller '{}' with label '{}'",
181			ControllerId,
182			Label
183		);
184
185		// For now, assume all extension providers come from the main sidecar
186		let SideCarIdentifier = Some("cocoon-main".to_string());
187
188		let ControllerState = TestControllerState {
189			ControllerIdentifier:ControllerId.clone(),
190
191			Label,
192
193			SideCarIdentifier,
194
195			IsActive:true,
196
197			SupportedTestTypes:vec!["unit".to_string(), "integration".to_string()],
198		};
199
200		// Store the controller state
201		let mut StateGuard = self.ApplicationState.TestProviderState.write().await;
202
203		StateGuard.Controllers.insert(ControllerId.clone(), ControllerState);
204
205		drop(StateGuard);
206
207		// Notify the frontend about the new test controller
208		self.ApplicationHandle
209			.emit("sky://test/registered", json!({ "ControllerIdentifier": ControllerId }))
210			.map_err(|Error| {
211				CommonError::IPCError { Description:format!("Failed to emit test registration event: {}", Error) }
212			})?;
213
214		dev_log!(
215			"extensions",
216			"[TestProvider] Test controller '{}' registered successfully",
217			ControllerId
218		);
219
220		Ok(())
221	}
222
223	/// Runs tests based on the test run request.
224	///
225	/// This implementation supports both native (Rust) and proxied (extension)
226	/// test controllers, with proper test discovery, execution, and result
227	/// reporting.
228	async fn RunTests(&self, ControllerIdentifier:String, TestRunRequest:Value) -> Result<(), CommonError> {
229		dev_log!(
230			"extensions",
231			"[TestProvider] Running tests for controller '{}': {:?}",
232			ControllerIdentifier,
233			TestRunRequest
234		);
235
236		// Get controller state
237		let ControllerState = {
238			let StateGuard = self.ApplicationState.TestProviderState.read().await;
239
240			StateGuard.Controllers.get(&ControllerIdentifier).cloned().ok_or_else(|| {
241				CommonError::TestControllerNotFound { ControllerIdentifier:ControllerIdentifier.clone() }
242			})?
243		};
244
245		// Create a new test run
246		let RunIdentifier = Uuid::new_v4().to_string();
247
248		let TestRun = TestRun {
249			RunIdentifier:RunIdentifier.clone(),
250
251			ControllerIdentifier:ControllerIdentifier.clone(),
252
253			Status:TestRunStatus::Queued,
254
255			StartedAt:std::time::Instant::now(),
256
257			Results:HashMap::new(),
258		};
259
260		{
261			let mut StateGuard = self.ApplicationState.TestProviderState.write().await;
262
263			StateGuard.ActiveRuns.insert(RunIdentifier.clone(), TestRun);
264		}
265
266		// Notify frontend about test run start
267		self.ApplicationHandle
268			.emit(
269				"sky://test/run-started",
270				json!({ "RunIdentifier": RunIdentifier, "ControllerIdentifier": ControllerIdentifier }),
271			)
272			.map_err(|Error| {
273				CommonError::IPCError { Description:format!("Failed to emit test run started event: {}", Error) }
274			})?;
275
276		// Execute tests based on controller type
277		if let Some(SideCarIdentifier) = &ControllerState.SideCarIdentifier {
278			// Proxied extension test controller
279			Self::RunProxiedTests(self, SideCarIdentifier, &RunIdentifier, TestRunRequest).await?;
280		} else {
281			// Native Rust test controller (currently not supported)
282			dev_log!(
283				"extensions",
284				"warn: [TestProvider] Native test controllers not yet implemented for '{}'",
285				ControllerIdentifier
286			);
287
288			Self::UpdateRunStatus(self, &RunIdentifier, TestRunStatus::Skipped).await;
289		}
290
291		Ok(())
292	}
293}
294
295// ============================================================================
296// Private Helper Methods
297// ============================================================================
298
299impl MountainEnvironment {
300	/// Runs tests via a proxied sidecar test controller.
301	async fn RunProxiedTests(
302		&self,
303
304		SideCarIdentifier:&str,
305
306		RunIdentifier:&str,
307
308		TestRunRequest:Value,
309	) -> Result<(), CommonError> {
310		dev_log!(
311			"extensions",
312			"[TestProvider] Running proxied tests for run '{}' on sidecar '{}'",
313			RunIdentifier,
314			SideCarIdentifier
315		);
316
317		// Update test run status to running
318		Self::UpdateRunStatus(self, RunIdentifier, TestRunStatus::Running).await;
319
320		let IPCProvider:Arc<dyn IPCProvider> = self.Require();
321
322		let RPCMethod = format!("{}$runTests", ProxyTarget::ExtHostTesting.GetTargetPrefix());
323
324		let RPCParams = json!({
325			"RunIdentifier": RunIdentifier,
326
327			"TestRunRequest": TestRunRequest,
328
329		});
330
331		match IPCProvider
332			.SendRequestToSideCar(SideCarIdentifier.to_string(), RPCMethod, RPCParams, 300000)
333			.await
334		{
335			Ok(Response) => {
336				// Parse test results from response
337				if let Ok(Results) = serde_json::from_value::<Vec<TestResult>>(Response) {
338					Self::StoreTestResults(self, RunIdentifier, Results).await;
339
340					// Determine final status based on results
341					let FinalStatus = Self::CalculateRunStatus(self, RunIdentifier).await;
342
343					Self::UpdateRunStatus(self, RunIdentifier, FinalStatus).await;
344
345					dev_log!(
346						"extensions",
347						"[TestProvider] Test run '{}' completed with status {:?}",
348						RunIdentifier,
349						FinalStatus
350					);
351				} else {
352					dev_log!(
353						"extensions",
354						"error: [TestProvider] Failed to parse test results for run '{}'",
355						RunIdentifier
356					);
357
358					Self::UpdateRunStatus(self, RunIdentifier, TestRunStatus::Errored).await;
359				}
360				Ok(())
361			},
362
363			Err(Error) => {
364				dev_log!("extensions", "error: [TestProvider] Failed to run tests: {}", Error);
365
366				let _ = Self::UpdateRunStatus(self, RunIdentifier, TestRunStatus::Errored).await;
367
368				Err(Error)
369			},
370		}
371	}
372
373	/// Updates the status of a test run and notifies the frontend.
374	async fn UpdateRunStatus(&self, RunIdentifier:&str, Status:TestRunStatus) -> Result<(), CommonError> {
375		let mut StateGuard = self.ApplicationState.TestProviderState.write().await;
376
377		if let Some(TestRun) = StateGuard.ActiveRuns.get_mut(RunIdentifier) {
378			TestRun.Status = Status;
379
380			drop(StateGuard);
381
382			// Notify frontend about status change
383			self.ApplicationHandle
384				.emit(
385					"sky://test/run-status-changed",
386					json!({
387						"RunIdentifier": RunIdentifier,
388
389						"Status": Status,
390
391					}),
392				)
393				.map_err(|Error| {
394					CommonError::IPCError { Description:format!("Failed to emit test status change event: {}", Error) }
395				})?;
396
397			Ok(())
398		} else {
399			Err(CommonError::TestRunNotFound { RunIdentifier:RunIdentifier.to_string() })
400		}
401	}
402
403	/// Stores test results for a test run.
404	async fn StoreTestResults(&self, RunIdentifier:&str, Results:Vec<TestResult>) -> Result<(), CommonError> {
405		let mut StateGuard = self.ApplicationState.TestProviderState.write().await;
406
407		if let Some(TestRun) = StateGuard.ActiveRuns.get_mut(RunIdentifier) {
408			for Result in Results {
409				TestRun.Results.insert(Result.TestIdentifier.clone(), Result);
410			}
411			Ok(())
412		} else {
413			Err(CommonError::TestRunNotFound { RunIdentifier:RunIdentifier.to_string() })
414		}
415	}
416
417	/// Calculates the final status of a test run based on its results.
418	async fn CalculateRunStatus(&self, RunIdentifier:&str) -> TestRunStatus {
419		let StateGuard = self.ApplicationState.TestProviderState.read().await;
420
421		if let Some(TestRun) = StateGuard.ActiveRuns.get(RunIdentifier) {
422			if TestRun.Results.is_empty() {
423				TestRunStatus::Passed // No tests considered passed
424			} else {
425				let HasFailed = TestRun.Results.values().any(|r| r.Status == TestRunStatus::Failed);
426
427				let HasErrored = TestRun.Results.values().any(|r| r.Status == TestRunStatus::Errored);
428
429				if HasErrored {
430					TestRunStatus::Errored
431				} else if HasFailed {
432					TestRunStatus::Failed
433				} else {
434					TestRunStatus::Passed
435				}
436			}
437		} else {
438			TestRunStatus::Errored
439		}
440	}
441}