This file provides guidance to agentic coding agents working in the Server Runner codebase.
Server Runner is a Rust CLI tool that starts multiple web servers, waits for them to be ready (HTTP 200 responses), then executes a command when all servers are running. It's designed for automated testing workflows.
cargo build # Debug build
cargo build --release # Release build
cargo check # Fast compilation check without code generationcargo clippy # Run Clippy linter
cargo clippy -- -D warnings # Treat warnings as errors
cargo fmt -- --check # Check formatting without modifying files
cargo fmt # Format all Rust codecargo test # Run all tests
cargo test -- --nocapture # Run tests with stdout output
cargo test <test_name> # Run a single test by name
cargo test --test cli # Run all tests in tests/cli.rs
cargo test runs # Run single test function: tests/cli.rs::runsExamples of running specific tests:
cargo test fails_on_missing_config_file # Single integration test
cargo test fails_on_timeout # Matches multiple tests with "timeout"
cargo test -- --test-threads=1 # Run tests seriallycargo run # Run with default config (servers.yaml)
cargo run -- -c config.yaml # Run with custom config
cargo run -- -v # Run with verbose output
cargo run -- -a 5 # Run with custom max attempts (default: 10)cargo install --path . # Install locally from source- Group imports in this order: external crates, std library, local modules
- Use explicit imports rather than glob imports (avoid
use foo::*;) - Example from
src/main.rs:1-12:use anyhow::{bail, Context}; use clap::Parser; use log::info; use std::collections::HashMap; use std::ops::AddAssign; #[cfg(windows)] use std::os::windows::process::CommandExt; use std::process::{Child, Command};
- Use
rustfmtwith default settings (no custom config file) - 4-space indentation
- Line length: Follow rustfmt defaults (~100 chars)
- Trailing commas in multi-line structures
- Use
cargo fmtbefore committing
- Use explicit types for struct fields:
name: String,timeout: u64 - Leverage type inference in local variables when clear from context
- Use newtype pattern for domain concepts:
struct Attempts(u8),struct ServerName(String) - Implement trait bounds explicitly:
impl AddAssign<u8> for Attempts - Use
anyhow::Result<T>for error handling in functions that can fail - Prefer owned types (
String,Vec<T>) in structs; use references (&str,&[T]) in function parameters
- Types: PascalCase (
ServerProcess,ServerStatus) - Functions: snake_case (
run_command,check_server) - Variables: snake_case (
server_processes,max_attempts) - Constants: SCREAMING_SNAKE_CASE (if needed)
- Enums: PascalCase for type and variants (
ServerStatus::Waiting) - CLI args: kebab-case in help text, snake_case in struct fields
- Use
anyhowfor error handling and context propagation - Use
?operator for error propagation:let config = get_config(&args.config)?; - Add context to errors:
.context(format!("Could not find config file {}", filename))? - Use
bail!macro for early returns with errors:bail!("Configuration must include at least one server") - Pattern match on specific error types when needed (e.g.,
error.is_connect()insrc/main.rs:280) - Return
anyhow::Result<T>from fallible functions - Exit with descriptive error messages in
main()viaexit_with_error()
- Use
#[derive(serde::Deserialize)]for config structs - Provide default values with
#[serde(default = "function_name")] - Example:
#[serde(default = "default_timeout")]ontimeoutfield - Validate deserialized config immediately after parsing (see
get_config())
- Use
logcrate withsimplelogimplementation - Use appropriate levels:
info!()for progress,eprintln!()for errors - Include context in log messages:
info!("Starting server {}", s.name) - Respect verbose flag for log level control
- Use
#[cfg(windows)]for Windows-specific code - Example:
cmd.creation_flags(0x08000000);for Windows process creation - Test on both Unix and Windows when making process-related changes
- Use
Arc<Mutex<T>>for shared mutable state across threads - Clone
Arcbefore moving into closures:let clone = Arc::clone(&original); - Always call
.lock()on Mutex before accessing inner data - Handle
LockResultproperly (seestop_servers()function)
- Implement common traits for custom types:
Display,PartialEq,AddAssign, etc. - Use operator overloading sparingly and only when semantically clear
- Example:
AttemptsimplementsAddAssign<u8>andPartialEq<u8>for ergonomic use
- Use
assert_cmdfor CLI integration tests - Use
predicatesfor assertion matching - Test both success and failure cases
- Test edge cases: empty configs, timeouts, invalid input
- Place test fixtures in
tests/directory (YAML configs) - Test naming: descriptive snake_case describing what fails/succeeds
- Parse CLI args and load YAML configuration
- Start all server processes concurrently
- Poll each server URL until HTTP 200 or max attempts reached
- Execute final command when all servers are ready
- Clean up all processes on completion or failure
Config: YAML config withserverslist andcommandto runServer: Individual server config (name, URL, command, timeout)ServerProcess: Running process wrapperServerStatus: Enum for Waiting/Running statesAttempts: Newtype for attempt counting
- Use
std::process::Commandto spawn processes - Implement Ctrl+C handler for graceful shutdown via
ctrlccrate - Kill all child processes on error or completion
- Use
shlexfor safe command parsing
servers: # List of servers to start
- name: "Server Name"
url: "http://localhost:8080"
command: "node server.js"
timeout: 5 # Optional, defaults to 5 seconds
command: "npm test" # Command to run when all servers ready- Add field to
Argsstruct with#[arg(...)]attribute - Pass argument through to relevant function
- Add test case in
tests/cli.rs
- Add field to
ServerorConfigstruct with#[serde(...)] - Provide default value function if optional
- Update validation logic if needed
- Add test YAML fixture in
tests/
- Create test function in
tests/cli.rswith#[test] - Use
Command::cargo_bin("server-runner")to get binary - Add args with
.arg()method - Assert with
.assert().success()or.failure() - Check stderr/stdout with
predicate::str::contains()
Key dependencies and their purposes:
anyhow: Error handling and contextclap: CLI argument parsing (derive API)config: YAML configuration file parsingctrlc: Signal handling for graceful shutdownlog+simplelog: Logging infrastructurereqwest(blocking): HTTP client for health checksserde: Serialization/deserializationshlex: Shell-like command parsing
- Edition: 2024
- Current version: 1.6.0
- MSRV: Not explicitly specified (uses 2024 edition features)