Skip to main content

rch_common/e2e/
mod.rs

1//! E2E Test Infrastructure
2//!
3//! This module provides comprehensive infrastructure for end-to-end testing
4//! of the Remote Compilation Helper system.
5//!
6//! # Components
7//!
8//! - **logging**: Structured logging system for capturing test execution details
9//! - **harness**: Test harness for managing processes, files, and assertions
10//! - **fixtures**: Pre-built configurations and sample data for tests
11//! - **verification**: Remote compilation verification for self-testing
12//!
13//! # Usage Example
14//!
15//! ```rust,ignore
16//! use rch_common::e2e::{TestHarnessBuilder, TestLoggerBuilder, HookInputFixture};
17//!
18//! #[test]
19//! fn test_daemon_startup() {
20//!     // Create a test harness
21//!     let harness = TestHarnessBuilder::new("test_daemon_startup")
22//!         .cleanup_on_success(true)
23//!         .build()
24//!         .unwrap();
25//!
26//!     // Create daemon config
27//!     let socket_path = harness.test_dir().join("rch.sock");
28//!     let config = DaemonConfigFixture::minimal(&socket_path);
29//!     harness.create_daemon_config(&config.to_toml()).unwrap();
30//!
31//!     // Start the daemon
32//!     harness.start_daemon(&[]).unwrap();
33//!
34//!     // Wait for socket to be available
35//!     harness.wait_for_socket(&socket_path, Duration::from_secs(5)).unwrap();
36//!
37//!     // Run test assertions
38//!     let result = harness.exec_rch(["status"]).unwrap();
39//!     harness.assert_success(&result, "rch status").unwrap();
40//!
41//!     harness.mark_passed();
42//! }
43//! ```
44//!
45//! # Test Categories
46//!
47//! The E2E test infrastructure supports several categories of tests:
48//!
49//! - **Daemon Lifecycle**: Start/stop, configuration, health checks
50//! - **Worker Connectivity**: SSH connections, capability detection
51//! - **Hook Integration**: Command classification, interception
52//! - **Full Build Pipeline**: End-to-end compilation offloading
53//! - **Fleet Deployment**: Multi-worker scenarios
54
55pub mod fault_injection;
56pub mod fixtures;
57pub mod harness;
58pub mod logging;
59pub mod process_triage;
60pub mod test_workers;
61pub mod verification;
62
63// Re-export commonly used items
64pub use fault_injection::{
65    ELF_MAGIC, FaultFixture, FaultScenario, MACH_O_64_MAGIC, SimulatedDiskStats,
66};
67pub use fixtures::{
68    DEFAULT_MULTI_REPO_ALIAS_ROOT, DEFAULT_MULTI_REPO_CANONICAL_ROOT,
69    DEFAULT_MULTI_REPO_FIXTURE_NAMESPACE, DaemonConfigFixture, FixtureFailureMode, FixtureLayer,
70    FixtureReadiness, HookInputFixture, MultiRepoFixtureConfig, MultiRepoFixtureError,
71    MultiRepoFixtureMetadata, MultiRepoFixtureResult, MultiRepoFixtureSet, RustProjectFixture,
72    TestCaseFixture, WorkerFixture, WorkersFixture, reset_default_multi_repo_fixtures,
73    reset_multi_repo_fixtures,
74};
75pub use harness::{
76    CommandResult, HarnessConfig, HarnessError, HarnessResult, ProcessInfo,
77    ReliabilityCommandRecord, ReliabilityFailureHook, ReliabilityFailureHookFlags,
78    ReliabilityLifecycleCommand, ReliabilityScenarioReport, ReliabilityScenarioSpec,
79    ReliabilityWorkerLifecycleHooks, TestHarness, TestHarnessBuilder, cleanup_stale_test_artifacts,
80};
81pub use logging::{
82    LogEntry, LogLevel, LogSource, LoggerConfig, RELIABILITY_EVENT_SCHEMA_VERSION,
83    ReliabilityContext, ReliabilityEventInput, ReliabilityPhase, ReliabilityPhaseEvent,
84    TestLogSummary, TestLogger, TestLoggerBuilder,
85};
86pub use process_triage::{
87    PROCESS_TRIAGE_CONTRACT_SCHEMA_VERSION, ProcessTriageActionClass, ProcessTriageActionOutcome,
88    ProcessTriageActionRequest, ProcessTriageActionResult, ProcessTriageAdapterCommand,
89    ProcessTriageAuditRecord, ProcessTriageCommandBudget, ProcessTriageContract,
90    ProcessTriageContractError, ProcessTriageEscalationLevel, ProcessTriageFailure,
91    ProcessTriageFailureKind, ProcessTriagePolicyDecision, ProcessTriageRequest,
92    ProcessTriageResponse, ProcessTriageResponseStatus, ProcessTriageRetryPolicy,
93    ProcessTriageSafeActionPolicy, ProcessTriageTimeoutPolicy, ProcessTriageTrigger,
94    evaluate_triage_action, process_triage_request_schema, process_triage_response_schema,
95};
96pub use test_workers::{
97    ENV_SKIP_WORKER_CHECK, ENV_TIMEOUT_SECS, ENV_WORKERS_CONFIG, TestConfigError, TestConfigResult,
98    TestSettings, TestWorkerEntry, TestWorkersConfig, expand_tilde_path, get_config_path,
99    is_mock_ssh_mode, should_skip_worker_check,
100};
101pub use verification::{RemoteCompilationTest, VerificationConfig, VerificationResult};
102
103/// Macro for creating E2E tests with automatic harness setup and cleanup
104///
105/// # Example
106///
107/// ```rust,ignore
108/// e2e_test!(test_basic_hook, |harness| {
109///     let result = harness.exec_rch(["--help"]).unwrap();
110///     harness.assert_success(&result, "rch --help").unwrap();
111/// });
112/// ```
113#[macro_export]
114macro_rules! e2e_test {
115    ($name:ident, $body:expr) => {
116        #[test]
117        fn $name() {
118            use $crate::e2e::{HarnessResult, TestHarnessBuilder};
119
120            fn run_test() -> HarnessResult<()> {
121                let harness = TestHarnessBuilder::new(stringify!($name))
122                    .cleanup_on_success(true)
123                    .cleanup_on_failure(false)
124                    .build()?;
125
126                let body: fn(&$crate::e2e::TestHarness) -> HarnessResult<()> = $body;
127                let result = body(&harness);
128
129                if result.is_ok() {
130                    harness.mark_passed();
131                }
132
133                result
134            }
135
136            if let Err(e) = run_test() {
137                panic!("E2E test failed: {}", e);
138            }
139        }
140    };
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use std::time::Duration;
147
148    #[test]
149    fn test_e2e_infrastructure_integration() {
150        // Create a logger
151        let logger = TestLoggerBuilder::new("integration_test")
152            .print_realtime(false)
153            .build();
154
155        logger.info("Starting integration test");
156
157        // Create a harness
158        let harness = TestHarnessBuilder::new("integration_test")
159            .cleanup_on_success(true)
160            .build()
161            .unwrap();
162
163        // Create some files using fixtures
164        let project = RustProjectFixture::minimal("test-proj");
165        let project_dir = harness.create_dir("project").unwrap();
166        project.create_in(&project_dir).unwrap();
167
168        // Verify the project was created
169        assert!(project_dir.join("Cargo.toml").exists());
170        assert!(project_dir.join("src/main.rs").exists());
171
172        // Create worker config
173        let workers = WorkersFixture::mock_local(1);
174        let workers_toml = workers.to_toml();
175        assert!(workers_toml.contains("worker1"));
176
177        // Create daemon config
178        let socket_path = harness.test_dir().join("rch.sock");
179        let daemon_config = DaemonConfigFixture::minimal(&socket_path);
180        let daemon_toml = daemon_config.to_toml();
181        assert!(daemon_toml.contains("confidence_threshold"));
182
183        // Test hook input fixture
184        let hook_input = HookInputFixture::cargo_build();
185        let json = hook_input.to_json();
186        assert!(json.contains("cargo build"));
187
188        // Test command execution
189        let result = harness.exec("echo", ["hello"]).unwrap();
190        harness.assert_success(&result, "echo").unwrap();
191        harness
192            .assert_stdout_contains(&result, "hello", "echo output")
193            .unwrap();
194
195        logger.info("Integration test completed");
196        harness.mark_passed();
197    }
198
199    #[test]
200    fn test_logger_standalone() {
201        let logger = TestLoggerBuilder::new("logger_test")
202            .print_realtime(false)
203            .min_level(LogLevel::Debug)
204            .max_entries(100)
205            .build();
206
207        logger.debug("Debug message");
208        logger.info("Info message");
209        logger.warn("Warning message");
210        logger.error("Error message");
211
212        let entries = logger.entries();
213        assert_eq!(entries.len(), 4);
214
215        let errors = logger.entries_by_level(LogLevel::Error);
216        assert_eq!(errors.len(), 1);
217
218        let summary = logger.summary();
219        assert_eq!(summary.total_entries, 4);
220        assert_eq!(*summary.counts_by_level.get(&LogLevel::Error).unwrap(), 1);
221    }
222
223    #[test]
224    fn test_harness_wait_for() {
225        let harness = TestHarnessBuilder::new("wait_test")
226            .cleanup_on_success(true)
227            .build()
228            .unwrap();
229
230        // Create a file to wait for
231        let test_file = harness.test_dir().join("marker.txt");
232
233        // Spawn a thread to create the file after a delay
234        let file_path = test_file.clone();
235        std::thread::spawn(move || {
236            std::thread::sleep(Duration::from_millis(100));
237            std::fs::write(&file_path, "created").unwrap();
238        });
239
240        // Wait for the file to exist
241        harness
242            .wait_for_file(&test_file, Duration::from_secs(2))
243            .unwrap();
244
245        assert!(test_file.exists());
246        harness.mark_passed();
247    }
248}