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