From d6475fd7879a78f408415ffa7fc78289205b9a32 Mon Sep 17 00:00:00 2001 From: Zoo Sky <127824+zoosky@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:51:02 +0200 Subject: [PATCH 1/2] refactor: split engine into a library target (src/lib.rs) driller was a binary-only crate, which blocked integration tests, benchmarks, and sibling tools from using the engine directly -- they could only shell out to the binary. Introduce a clean engine-in-library / CLI-in-binary boundary: - src/lib.rs is the new crate root. It re-exports the engine modules (actions, benchmark, checker, config, expandable, reader, tags) and exposes a typed entry point, `run(&RunOptions) -> BenchmarkResult`. interpolator and writer stay crate-private (no public interface). - src/main.rs keeps the CLI: clap structs, version strings, argv -> RunOptions mapping, the stats/compare presentation, and process exit codes. It now drives the engine via `driller::run`. - Cargo.toml declares explicit [lib] and [[bin]] targets. Behaviour-preserving: identical CLI output and exit codes; fmt, clippy, and the full test suite (lib + bin unit tests, integration tests, the new lib doctest) pass unchanged. Member-level visibility is untouched -- only module-level reachability changed. --- Cargo.toml | 12 ++++++++++++ src/lib.rs | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 17 ++++------------- 3 files changed, 70 insertions(+), 13 deletions(-) create mode 100644 src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 960df7b..5b273e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,18 @@ exclude = [ "example/server/", ] +# Combined library + binary package. The library (`src/lib.rs`) is the reusable +# load-testing engine; the binary (`src/main.rs`) is the CLI shell that drives +# it. Targets are declared explicitly so the boundary and binary name are +# unambiguous and so integration tests / benchmarks can `use driller::`. +[lib] +name = "driller" +path = "src/lib.rs" + +[[bin]] +name = "driller" +path = "src/main.rs" + [dependencies] clap = { version = "4", features = ["derive"] } colored = "3" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..93699ea --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,54 @@ +//! driller -- HTTP load-testing engine with Ansible-style YAML plans. +//! +//! This crate is the reusable engine behind the `driller` binary. The binary +//! (`src/main.rs`) owns the command-line interface -- argument parsing, version +//! strings, the stats/compare presentation, and process exit codes -- while +//! everything that actually loads a plan and runs a benchmark lives here, so +//! integration tests, benchmarks, and sibling tools can drive the engine +//! directly without shelling out to the binary. +//! +//! # Entry point +//! +//! [`run`] is the single typed entry point: build a [`RunOptions`] and call it. +//! +//! ```no_run +//! use driller::{run, RunOptions}; +//! +//! # fn build_options() -> RunOptions { unimplemented!() } +//! let options: RunOptions = build_options(); +//! let result = run(&options); +//! println!("{} assertion(s) failed", result.assertion_failures); +//! ``` +//! +//! # Module surface +//! +//! The engine modules are exposed at module level for in-repo consumers +//! (tests, benchmarks, the `harness/` crate); their members keep their original +//! visibility. `interpolator` and `writer` stay crate-private as they appear in +//! no public interface. + +pub mod actions; +pub mod benchmark; +pub mod checker; +pub mod config; +pub mod expandable; +pub mod reader; +pub mod tags; + +mod interpolator; +mod writer; + +pub use benchmark::{BenchmarkResult, RunOptions}; + +/// Runs a benchmark to completion and returns its aggregated result. +/// +/// This is the library's primary entry point. The `driller` binary constructs +/// a [`RunOptions`] from parsed command-line arguments and calls this; tests and +/// benchmarks can do the same without going through the CLI layer. +/// +/// The call builds its own tokio runtime (current-thread by default, or +/// multi-thread when `worker_threads >= 2`) and blocks until the run finishes, +/// so it is safe to call from a synchronous context. +pub fn run(options: &RunOptions) -> BenchmarkResult { + benchmark::execute(options) +} diff --git a/src/main.rs b/src/main.rs index fb5f94e..60ff91d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,8 @@ -mod actions; -mod benchmark; -mod checker; -mod config; -mod expandable; -mod interpolator; -mod reader; -mod tags; -mod writer; - -use crate::actions::Report; -use crate::benchmark::RunOptions; use clap::{Args, Parser, Subcommand}; use colored::*; +use driller::actions::Report; +use driller::tags; +use driller::{RunOptions, checker}; use hdrhistogram::Histogram; use linked_hash_map::LinkedHashMap; use std::collections::BTreeMap; @@ -298,7 +289,7 @@ fn main() { } }; - let benchmark_result = benchmark::execute(&options); + let benchmark_result = driller::run(&options); let assertion_failures = benchmark_result.assertion_failures; let list_reports = benchmark_result.reports; let duration = benchmark_result.duration; From e052ad3ef30a5b1a1c58619b3a1152ed42d82738 Mon Sep 17 00:00:00 2001 From: Zoo Sky <127824+zoosky@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:52:49 +0200 Subject: [PATCH 2/2] docs: address review of the lib/bin split Follow-up to the code review on the library-target split: - Drop the reference to the private-workbench-only `harness/` crate from the crate-level doc comment (it does not exist in this repo and would surface in cargo doc); use a generic "sibling crates" phrasing. - Document BenchmarkResult and its reports/duration fields, now that it is part of the public API. - Narrow benchmark::execute to pub(crate) so driller::run is genuinely the single public entry point for executing a run. - Document on run() and checker::compare() that some fatal-input paths terminate the process via process::exit rather than returning, so library callers can pre-validate. The die() -> Result migration remains a tracked follow-up. --- src/benchmark.rs | 13 ++++++++++++- src/checker.rs | 9 ++++++--- src/lib.rs | 23 ++++++++++++++++------- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/benchmark.rs b/src/benchmark.rs index fb51650..f617803 100644 --- a/src/benchmark.rs +++ b/src/benchmark.rs @@ -46,8 +46,16 @@ pub struct RunOptions { pub tags: Tags, } +/// Aggregated outcome of a benchmark run, returned by [`crate::run`]. +/// +/// Bundles the per-iteration reports, the total wall-clock time, and the +/// assertion-failure tally so a caller (the CLI, a test, or a benchmark) can +/// render stats and decide an exit status without re-deriving them. pub struct BenchmarkResult { + /// Per-iteration results: one inner `Vec` per completed iteration, + /// each holding one [`Report`] per executed step. pub reports: Vec, + /// Total wall-clock duration of the run, in seconds. pub duration: f64, /// Number of `assert` checks that failed during the run. Non-zero drives a /// non-zero process exit code in `main`, so a failed assertion is detectable @@ -94,7 +102,10 @@ fn build_synthetic_plan(path: &str) -> Benchmark { } /// Executes a benchmark run using the provided options. -pub fn execute(options: &RunOptions) -> BenchmarkResult { +/// +/// Crate-internal: external callers go through [`crate::run`], which is the +/// library's single public entry point for executing a run. +pub(crate) fn execute(options: &RunOptions) -> BenchmarkResult { let config = Arc::new(Config::new(options)); // Held outside the runtime so the failure tally survives after `config` is diff --git a/src/checker.rs b/src/checker.rs index de001b5..67cbfbe 100644 --- a/src/checker.rs +++ b/src/checker.rs @@ -18,9 +18,12 @@ use crate::reader; /// /// Returns `Ok(())` when every named request stays within `threshold` /// milliseconds of its baseline mean, or `Err(n)` where `n` is the number of -/// names that regressed. Exits with a clean error if the baseline file is empty -/// or is not a list of records; records missing a `name` or numeric `duration` -/// are skipped rather than panicking. +/// names that regressed. Records missing a `name` or numeric `duration` are +/// skipped rather than panicking. +/// +/// Note for library callers: a missing/empty/malformed baseline file is treated +/// as fatal and **terminates the process** via `std::process::exit(1)` rather +/// than returning an `Err` -- pre-validate the file if that is not acceptable. pub fn compare(list_reports: &[Vec], filepath: &str, threshold: f64) -> Result<(), i32> { let docs = reader::read_file_as_yml(filepath); let items = match docs.first().and_then(|doc| doc.as_sequence()) { diff --git a/src/lib.rs b/src/lib.rs index 93699ea..cc4e401 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,7 @@ //! # Module surface //! //! The engine modules are exposed at module level for in-repo consumers -//! (tests, benchmarks, the `harness/` crate); their members keep their original +//! (tests, benchmarks, and sibling crates); their members keep their original //! visibility. `interpolator` and `writer` stay crate-private as they appear in //! no public interface. @@ -42,13 +42,22 @@ pub use benchmark::{BenchmarkResult, RunOptions}; /// Runs a benchmark to completion and returns its aggregated result. /// -/// This is the library's primary entry point. The `driller` binary constructs -/// a [`RunOptions`] from parsed command-line arguments and calls this; tests and -/// benchmarks can do the same without going through the CLI layer. +/// This is the library's single entry point for executing a run. The `driller` +/// binary constructs a [`RunOptions`] from parsed command-line arguments and +/// calls this; tests and benchmarks can do the same without going through the +/// CLI layer. /// -/// The call builds its own tokio runtime (current-thread by default, or -/// multi-thread when `worker_threads >= 2`) and blocks until the run finishes, -/// so it is safe to call from a synchronous context. +/// The call builds its own tokio runtime (current-thread when `worker_threads` +/// is `None`/`1`, multi-thread when it is `2` or more) and blocks until the run +/// finishes, so it is safe to call from a synchronous context. +/// +/// # Process exit on fatal input +/// +/// Some invalid inputs are currently treated as fatal and terminate the +/// process via `std::process::exit(1)` rather than returning -- an empty plan, +/// and the unreadable/malformed-file paths in [`reader`]. A caller embedding +/// the engine should pre-validate its [`RunOptions`] accordingly. Migrating +/// these paths to returned errors is tracked as a follow-up. pub fn run(options: &RunOptions) -> BenchmarkResult { benchmark::execute(options) }