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 fixtures;
56pub mod harness;
57pub mod logging;
58pub mod process_triage;
59pub mod test_workers;
60pub mod verification;
61
62// Re-export commonly used items
63pub use fixtures::{
64    DEFAULT_MULTI_REPO_ALIAS_ROOT, DEFAULT_MULTI_REPO_CANONICAL_ROOT,
65    DEFAULT_MULTI_REPO_FIXTURE_NAMESPACE, DaemonConfigFixture, FixtureFailureMode, FixtureLayer,
66    FixtureReadiness, HookInputFixture, MultiRepoFixtureConfig, MultiRepoFixtureError,
67    MultiRepoFixtureMetadata, MultiRepoFixtureResult, MultiRepoFixtureSet, RustProjectFixture,
68    TestCaseFixture, WorkerFixture, WorkersFixture, reset_default_multi_repo_fixtures,
69    reset_multi_repo_fixtures,
70};
71pub use harness::{
72    CommandResult, HarnessConfig, HarnessError, HarnessResult, ProcessInfo,
73    ReliabilityCommandRecord, ReliabilityFailureHook, ReliabilityFailureHookFlags,
74    ReliabilityLifecycleCommand, ReliabilityScenarioReport, ReliabilityScenarioSpec,
75    ReliabilityWorkerLifecycleHooks, TestHarness, TestHarnessBuilder, cleanup_stale_test_artifacts,
76};
77pub use logging::{
78    LogEntry, LogLevel, LogSource, LoggerConfig, RELIABILITY_EVENT_SCHEMA_VERSION,
79    ReliabilityContext, ReliabilityEventInput, ReliabilityPhase, ReliabilityPhaseEvent,
80    TestLogSummary, TestLogger, TestLoggerBuilder,
81};
82pub use process_triage::{
83    PROCESS_TRIAGE_CONTRACT_SCHEMA_VERSION, ProcessTriageActionClass, ProcessTriageActionOutcome,
84    ProcessTriageActionRequest, ProcessTriageActionResult, ProcessTriageAdapterCommand,
85    ProcessTriageAuditRecord, ProcessTriageCommandBudget, ProcessTriageContract,
86    ProcessTriageContractError, ProcessTriageEscalationLevel, ProcessTriageFailure,
87    ProcessTriageFailureKind, ProcessTriagePolicyDecision, ProcessTriageRequest,
88    ProcessTriageResponse, ProcessTriageResponseStatus, ProcessTriageRetryPolicy,
89    ProcessTriageSafeActionPolicy, ProcessTriageTimeoutPolicy, ProcessTriageTrigger,
90    evaluate_triage_action, process_triage_request_schema, process_triage_response_schema,
91};
92pub use test_workers::{
93    ENV_SKIP_WORKER_CHECK, ENV_TIMEOUT_SECS, ENV_WORKERS_CONFIG, TestConfigError, TestConfigResult,
94    TestSettings, TestWorkerEntry, TestWorkersConfig, expand_tilde_path, get_config_path,
95    is_mock_ssh_mode, should_skip_worker_check,
96};
97pub use verification::{RemoteCompilationTest, VerificationConfig, VerificationResult};
98
99/// Macro for creating E2E tests with automatic harness setup and cleanup
100///
101/// # Example
102///
103/// ```rust,ignore
104/// e2e_test!(test_basic_hook, |harness| {
105///     let result = harness.exec_rch(["--help"]).unwrap();
106///     harness.assert_success(&result, "rch --help").unwrap();
107/// });
108/// ```
109#[macro_export]
110macro_rules! e2e_test {
111    ($name:ident, $body:expr) => {
112        #[test]
113        fn $name() {
114            use $crate::e2e::{HarnessResult, TestHarnessBuilder};
115
116            fn run_test() -> HarnessResult<()> {
117                let harness = TestHarnessBuilder::new(stringify!($name))
118                    .cleanup_on_success(true)
119                    .cleanup_on_failure(false)
120                    .build()?;
121
122                let body: fn(&$crate::e2e::TestHarness) -> HarnessResult<()> = $body;
123                let result = body(&harness);
124
125                if result.is_ok() {
126                    harness.mark_passed();
127                }
128
129                result
130            }
131
132            if let Err(e) = run_test() {
133                panic!("E2E test failed: {}", e);
134            }
135        }
136    };
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use std::time::Duration;
143
144    #[test]
145    fn test_e2e_infrastructure_integration() {
146        // Create a logger
147        let logger = TestLoggerBuilder::new("integration_test")
148            .print_realtime(false)
149            .build();
150
151        logger.info("Starting integration test");
152
153        // Create a harness
154        let harness = TestHarnessBuilder::new("integration_test")
155            .cleanup_on_success(true)
156            .build()
157            .unwrap();
158
159        // Create some files using fixtures
160        let project = RustProjectFixture::minimal("test-proj");
161        let project_dir = harness.create_dir("project").unwrap();
162        project.create_in(&project_dir).unwrap();
163
164        // Verify the project was created
165        assert!(project_dir.join("Cargo.toml").exists());
166        assert!(project_dir.join("src/main.rs").exists());
167
168        // Create worker config
169        let workers = WorkersFixture::mock_local(1);
170        let workers_toml = workers.to_toml();
171        assert!(workers_toml.contains("worker1"));
172
173        // Create daemon config
174        let socket_path = harness.test_dir().join("rch.sock");
175        let daemon_config = DaemonConfigFixture::minimal(&socket_path);
176        let daemon_toml = daemon_config.to_toml();
177        assert!(daemon_toml.contains("confidence_threshold"));
178
179        // Test hook input fixture
180        let hook_input = HookInputFixture::cargo_build();
181        let json = hook_input.to_json();
182        assert!(json.contains("cargo build"));
183
184        // Test command execution
185        let result = harness.exec("echo", ["hello"]).unwrap();
186        harness.assert_success(&result, "echo").unwrap();
187        harness
188            .assert_stdout_contains(&result, "hello", "echo output")
189            .unwrap();
190
191        logger.info("Integration test completed");
192        harness.mark_passed();
193    }
194
195    #[test]
196    fn test_logger_standalone() {
197        let logger = TestLoggerBuilder::new("logger_test")
198            .print_realtime(false)
199            .min_level(LogLevel::Debug)
200            .max_entries(100)
201            .build();
202
203        logger.debug("Debug message");
204        logger.info("Info message");
205        logger.warn("Warning message");
206        logger.error("Error message");
207
208        let entries = logger.entries();
209        assert_eq!(entries.len(), 4);
210
211        let errors = logger.entries_by_level(LogLevel::Error);
212        assert_eq!(errors.len(), 1);
213
214        let summary = logger.summary();
215        assert_eq!(summary.total_entries, 4);
216        assert_eq!(*summary.counts_by_level.get(&LogLevel::Error).unwrap(), 1);
217    }
218
219    #[test]
220    fn test_harness_wait_for() {
221        let harness = TestHarnessBuilder::new("wait_test")
222            .cleanup_on_success(true)
223            .build()
224            .unwrap();
225
226        // Create a file to wait for
227        let test_file = harness.test_dir().join("marker.txt");
228
229        // Spawn a thread to create the file after a delay
230        let file_path = test_file.clone();
231        std::thread::spawn(move || {
232            std::thread::sleep(Duration::from_millis(100));
233            std::fs::write(&file_path, "created").unwrap();
234        });
235
236        // Wait for the file to exist
237        harness
238            .wait_for_file(&test_file, Duration::from_secs(2))
239            .unwrap();
240
241        assert!(test_file.exists());
242        harness.mark_passed();
243    }
244}