Add lab restart command and 1BRC validators#15
Conversation
Implements two validators for performance challenges: - output_match: runs command, compares stdout to expected file - benchmark: validates output correctness and execution time Both validators support workspace detection from active lab state.
Adds factory methods and RuntimeValidator enum variants: - output_match:string(cmd),string(expected_file) - benchmark:string(cmd),string(expected_file),int(max_ms) Includes unit tests for DSL parsing.
There was a problem hiding this comment.
Pull request overview
This PR adds a lab restart command and introduces benchmark validators for performance-focused challenges like the 1 Billion Row Challenge (1BRC). The restart functionality allows users to reset their progress on a lab while maintaining their workspace, and the new validators enable correctness checking and performance benchmarking of command outputs.
Changes:
- Added
luxctl lab restartcommand with API integration - Implemented
OutputMatchValidatorandBenchmarkValidatorfor comparing command outputs against expected files - Added comprehensive E2E test suite covering authentication, lab lifecycle, and task operations
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/commands/lab.rs | Implements restart command handling with server synchronization |
| src/api/client.rs | Adds restart_lab API endpoint |
| src/api/types.rs | Defines RestartLabResponse and RestartLabData types |
| src/state.rs | Adds clear_progress method to reset task status |
| src/main.rs | Adds Restart action to CLI |
| src/validators/benchmark.rs | New module with output matching and benchmarking validators |
| src/validators/factory.rs | Registers new validators and factory functions |
| src/validators/mod.rs | Exports new benchmark validators |
| tests/e2e.rs | New E2E test suite for local API testing |
| src/lib.rs | Version bump to 0.7.0 |
| Cargo.toml | Version bump to 0.7.0 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let out = stdout(&output); | ||
| // should show user email or name | ||
| assert!( | ||
| out.contains("@") || out.len() > 0, |
There was a problem hiding this comment.
The condition out.len() > 0 is redundant and always true when combined with ||. Use !out.is_empty() as the second condition, or if any non-empty output is acceptable, simplify to just that check.
| out.contains("@") || out.len() > 0, | |
| !out.is_empty(), |
| fn get_workspace() -> PathBuf { | ||
| let config = match Config::load() { | ||
| Ok(c) => c, | ||
| Err(_) => return std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), | ||
| }; | ||
| if !config.has_auth_token() { | ||
| return std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); | ||
| } | ||
| let state = match LabState::load(config.expose_token()) { | ||
| Ok(s) => s, | ||
| Err(_) => return std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), | ||
| }; | ||
| match state.get_active() { | ||
| Some(lab) => PathBuf::from(&lab.workspace), | ||
| None => std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), |
There was a problem hiding this comment.
The fallback std::env::current_dir().unwrap_or_else(|_| PathBuf::from(\".\")) is duplicated in lines 17, 20, 24, and 28. Extract this into a helper function or constant to improve maintainability.
| fn get_workspace() -> PathBuf { | |
| let config = match Config::load() { | |
| Ok(c) => c, | |
| Err(_) => return std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), | |
| }; | |
| if !config.has_auth_token() { | |
| return std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); | |
| } | |
| let state = match LabState::load(config.expose_token()) { | |
| Ok(s) => s, | |
| Err(_) => return std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), | |
| }; | |
| match state.get_active() { | |
| Some(lab) => PathBuf::from(&lab.workspace), | |
| None => std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), | |
| fn current_dir_or_dot() -> PathBuf { | |
| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) | |
| } | |
| fn get_workspace() -> PathBuf { | |
| let config = match Config::load() { | |
| Ok(c) => c, | |
| Err(_) => return current_dir_or_dot(), | |
| }; | |
| if !config.has_auth_token() { | |
| return current_dir_or_dot(); | |
| } | |
| let state = match LabState::load(config.expose_token()) { | |
| Ok(s) => s, | |
| Err(_) => return current_dir_or_dot(), | |
| }; | |
| match state.get_active() { | |
| Some(lab) => PathBuf::from(&lab.workspace), | |
| None => current_dir_or_dot(), |
| let parts: Vec<&str> = cmd_str.split_whitespace().collect(); | ||
| if parts.is_empty() { | ||
| return Err("empty command".to_string()); | ||
| } | ||
|
|
||
| let program = parts[0]; | ||
| let args = &parts[1..]; |
There was a problem hiding this comment.
Using split_whitespace() for command parsing doesn't handle quoted arguments (e.g., paths with spaces). Consider using a proper shell-like parser or document this limitation, especially since this is used for benchmark commands that may reference file paths.
| let expected_lines: Vec<&str> = expected.lines().collect(); | ||
|
|
||
| let mut diff_msg = String::new(); | ||
| for (i, (a, e)) in actual_lines.iter().zip(expected_lines.iter()).enumerate() { |
There was a problem hiding this comment.
The diff generation logic in lines 120-130 and 187-196 is duplicated between OutputMatchValidator and BenchmarkValidator. Extract this into a shared helper function to reduce code duplication.
Replace verbose email-checking logic with simple empty check since any user info indicates success.
Extract current_dir_fallback() to reduce duplication in get_workspace(). Extract diff_preview() to consolidate duplicate diff generation logic. Add comment documenting split_whitespace limitation in run_command().
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated 29 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const API_BASE_URL: &str = "http://0.0.0.0:8000"; | ||
|
|
||
| fn get_test_token() -> Option<String> { | ||
| // try dev_token file first (gitignored, local dev) |
There was a problem hiding this comment.
Corrected capitalization and improved comment clarity for 'try dev_token file first' to 'Try dev_token file first'.
| // try dev_token file first (gitignored, local dev) | |
| // Try dev_token file first (gitignored, local dev) |
| } | ||
| } | ||
|
|
||
| // fallback to env var |
There was a problem hiding this comment.
Corrected capitalization for 'fallback to env var' to 'Fallback to env var'.
| // fallback to env var | |
| // Fallback to env var |
| } | ||
|
|
||
| fn luxctl_with_token(args: &[&str], token: &str) -> Output { | ||
| // first authenticate, then run command |
There was a problem hiding this comment.
Corrected capitalization for 'first authenticate, then run command' to 'First authenticate, then run command'.
| // first authenticate, then run command | |
| // First authenticate, then run command |
| String::from_utf8_lossy(&output.stderr).to_string() | ||
| } | ||
|
|
||
| // helper to check if local API is running |
There was a problem hiding this comment.
Corrected capitalization for 'helper to check if local API is running' to 'Helper to check if local API is running'.
| // helper to check if local API is running | |
| // Helper to check if local API is running |
|
|
||
| let output = luxctl(&["auth", "--token", "invalid-token-12345"]); | ||
|
|
||
| // should fail gracefully |
There was a problem hiding this comment.
Corrected capitalization for 'should fail gracefully' to 'Should fail gracefully'.
| // should fail gracefully | |
| // Should fail gracefully |
| String::new() | ||
| } | ||
|
|
||
| /// truncate string for display |
There was a problem hiding this comment.
Corrected capitalization for 'truncate string for display' to 'Truncate string for display'.
| /// truncate string for display | |
| /// Truncate string for display |
| let actual = normalize(&stdout); | ||
| let expected = normalize(&expected); | ||
|
|
||
| // first check correctness |
There was a problem hiding this comment.
Corrected capitalization for 'first check correctness' to 'First check correctness'.
| // first check correctness | |
| // First check correctness |
| }); | ||
| } | ||
|
|
||
| // then check timing |
There was a problem hiding this comment.
Corrected capitalization for 'then check timing' to 'Then check timing'.
| // then check timing | |
| // Then check timing |
|
|
||
| match client.restart_lab(&lab.slug).await { | ||
| Ok(response) => { | ||
| // refresh tasks from server |
There was a problem hiding this comment.
Corrected capitalization for 'refresh tasks from server' to 'Refresh tasks from server'.
| // refresh tasks from server | |
| // Refresh tasks from server |
| state.save(config.expose_token())?; | ||
| } | ||
| Err(_) => { | ||
| // if refresh fails, just clear local progress |
There was a problem hiding this comment.
Corrected capitalization for 'if refresh fails, just clear local progress' to 'If refresh fails, just clear local progress'.
| // if refresh fails, just clear local progress | |
| // If refresh fails, just clear local progress |
No description provided.