Publish harness and TUI open-source
initial sync from the monorepo
This commit is contained in:
commit
c68e39f604
2734 changed files with 1437016 additions and 0 deletions
21
crates/common/xai-test-utils/Cargo.toml
Normal file
21
crates/common/xai-test-utils/Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
license = "Apache-2.0"
|
||||
name = "xai-test-utils"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
description = "Shared test utilities: hermetic git, optional runfiles helpers"
|
||||
|
||||
[features]
|
||||
# Enable Bazel runfiles support when building under that toolchain. Under
|
||||
# plain `cargo` the feature is off and crate_root! falls back to
|
||||
# CARGO_MANIFEST_DIR.
|
||||
default-bazel = ["bazel"]
|
||||
bazel = ["dep:runfiles"]
|
||||
|
||||
[dependencies]
|
||||
runfiles = { workspace = true, optional = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
11
crates/common/xai-test-utils/src/env.rs
Normal file
11
crates/common/xai-test-utils/src/env.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
//! Environment-variable test knobs.
|
||||
|
||||
/// Parse a `usize` env knob, falling back to `default` when unset or
|
||||
/// unparseable. The perf-repro convention for sizing `#[ignore]` benches
|
||||
/// (e.g. `GROK_PERF_GIT_FILES`).
|
||||
pub fn env_usize(key: &str, default: usize) -> usize {
|
||||
std::env::var(key)
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
176
crates/common/xai-test-utils/src/git.rs
Normal file
176
crates/common/xai-test-utils/src/git.rs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
//! Hermetic git helpers for tests.
|
||||
//!
|
||||
//! When running under `bazel test`, the `GIT_BIN_PATH` environment variable
|
||||
//! points to a statically-linked git binary provided by Bazel. The helpers
|
||||
//! in this module prepend that binary's directory to `PATH` so that
|
||||
//! `Command::new("git")` resolves to it instead of relying on a
|
||||
//! system-installed git.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Once;
|
||||
|
||||
static HERMETIC_GIT_INIT: Once = Once::new();
|
||||
|
||||
/// Prepend the hermetic git binary directory to `PATH` so that
|
||||
/// `Command::new("git")` resolves to the Bazel-provided static binary
|
||||
/// instead of relying on a system-installed git.
|
||||
///
|
||||
/// Safe to call multiple times — only the first call mutates `PATH`.
|
||||
pub fn ensure_hermetic_git_on_path() {
|
||||
HERMETIC_GIT_INIT.call_once(|| {
|
||||
if let Ok(git_bin) = std::env::var("GIT_BIN_PATH") {
|
||||
let git_path = PathBuf::from(&git_bin);
|
||||
let git_path = if git_path.is_relative() {
|
||||
std::env::current_dir().unwrap().join(&git_path)
|
||||
} else {
|
||||
git_path
|
||||
};
|
||||
if let Some(bin_dir) = git_path.parent() {
|
||||
let current_path = std::env::var("PATH").unwrap_or_default();
|
||||
// SAFETY: called once via `Once` before any child processes are spawned.
|
||||
unsafe {
|
||||
std::env::set_var("PATH", format!("{}:{}", bin_dir.display(), current_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Ensure the hermetic git binary is on `PATH` before running tests that
|
||||
/// need git. Call at the top of any `#[test]` that spawns `git` commands.
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[test]
|
||||
/// fn my_git_test() {
|
||||
/// xai_test_utils::require_git!();
|
||||
/// // ... git commands work here ...
|
||||
/// }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! require_git {
|
||||
() => {
|
||||
$crate::git::ensure_hermetic_git_on_path();
|
||||
};
|
||||
}
|
||||
|
||||
/// Initialise a fresh git repository at `path` with a dummy user config.
|
||||
///
|
||||
/// Calls [`ensure_hermetic_git_on_path`] first so the hermetic binary is used.
|
||||
pub fn init_git_repo(path: &Path) {
|
||||
ensure_hermetic_git_on_path();
|
||||
std::process::Command::new("git")
|
||||
.current_dir(path)
|
||||
.args(["init"])
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
std::process::Command::new("git")
|
||||
.current_dir(path)
|
||||
.args(["config", "user.email", "test@test.com"])
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
std::process::Command::new("git")
|
||||
.current_dir(path)
|
||||
.args(["config", "user.name", "Test"])
|
||||
.output()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Stage all files and create a commit.
|
||||
///
|
||||
/// Calls [`ensure_hermetic_git_on_path`] first so the hermetic binary is used.
|
||||
pub fn git_commit_all(path: &Path, message: &str) {
|
||||
ensure_hermetic_git_on_path();
|
||||
std::process::Command::new("git")
|
||||
.current_dir(path)
|
||||
.args(["add", "."])
|
||||
.output()
|
||||
.unwrap();
|
||||
std::process::Command::new("git")
|
||||
.current_dir(path)
|
||||
.args(["commit", "-m", message])
|
||||
.output()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Run a git command in `dir` with a deterministic author/committer, assert
|
||||
/// success, and return trimmed stdout.
|
||||
///
|
||||
/// Calls [`ensure_hermetic_git_on_path`] first so the hermetic binary is used.
|
||||
pub fn run_git(dir: &Path, args: &[&str]) -> String {
|
||||
run_git_with_env(dir, args, &[])
|
||||
}
|
||||
|
||||
/// Like [`run_git`], with extra environment variables (e.g.
|
||||
/// `GIT_SEQUENCE_EDITOR`). Hermetic beyond the binary and author identity:
|
||||
/// the developer's global/system git config is masked (a local
|
||||
/// `commit.gpgsign`/`core.hooksPath`/`rebase.autoSquash` must not change
|
||||
/// test behavior) and credential prompts are disabled. `envs` is applied
|
||||
/// last, so callers can override any of this.
|
||||
pub fn run_git_with_env(dir: &Path, args: &[&str], envs: &[(&str, &str)]) -> String {
|
||||
ensure_hermetic_git_on_path();
|
||||
let mut cmd = std::process::Command::new("git");
|
||||
cmd.args(args)
|
||||
.current_dir(dir)
|
||||
.env("GIT_AUTHOR_NAME", "Test User")
|
||||
.env("GIT_AUTHOR_EMAIL", "test@test.com")
|
||||
.env("GIT_COMMITTER_NAME", "Test User")
|
||||
.env("GIT_COMMITTER_EMAIL", "test@test.com")
|
||||
.env(
|
||||
"GIT_CONFIG_GLOBAL",
|
||||
if cfg!(windows) { "NUL" } else { "/dev/null" },
|
||||
)
|
||||
.env("GIT_CONFIG_NOSYSTEM", "1")
|
||||
.env("GIT_TERMINAL_PROMPT", "0");
|
||||
for (key, value) in envs {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
let output = cmd
|
||||
.output()
|
||||
.unwrap_or_else(|e| panic!("git {args:?} failed to spawn: {e}"));
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"git {:?} failed: {}",
|
||||
args,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
String::from_utf8_lossy(&output.stdout).trim().to_string()
|
||||
}
|
||||
|
||||
/// Write a grouped fan-out tree of ~`files` files (`files_per_dir` per
|
||||
/// directory, directories bucketed 100 per group) under `dir`. No git
|
||||
/// operations — callers stage/commit as needed.
|
||||
pub fn write_fanout_tree(dir: &Path, files: usize, files_per_dir: usize) {
|
||||
for d in 0..files.div_ceil(files_per_dir) {
|
||||
let sub = dir.join(format!("g{}", d / 100)).join(format!("d{d}"));
|
||||
std::fs::create_dir_all(&sub).expect("create populated dir");
|
||||
for f in 0..files_per_dir {
|
||||
std::fs::write(
|
||||
sub.join(format!("file_{f}.txt")),
|
||||
format!("content {d} {f}\n"),
|
||||
)
|
||||
.expect("write populated file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a `feature` branch with `picks` one-file commits off the current
|
||||
/// HEAD, advance the base branch by one commit (so a rebase has work), and
|
||||
/// leave `feature` checked out. Returns the base branch name.
|
||||
pub fn make_feature_branch(dir: &Path, picks: usize) -> String {
|
||||
let base = run_git(dir, &["rev-parse", "--abbrev-ref", "HEAD"]);
|
||||
run_git(dir, &["checkout", "-b", "feature"]);
|
||||
for k in 0..picks {
|
||||
let name = format!("pick_{k}.txt");
|
||||
std::fs::write(dir.join(&name), format!("pick {k}\n")).expect("write pick file");
|
||||
run_git(dir, &["add", &name]);
|
||||
run_git(dir, &["commit", "-m", &format!("pick {k}")]);
|
||||
}
|
||||
run_git(dir, &["checkout", &base]);
|
||||
std::fs::write(dir.join("base_advance.txt"), "advance\n").expect("write base advance file");
|
||||
run_git(dir, &["add", "base_advance.txt"]);
|
||||
run_git(dir, &["commit", "-m", "advance base"]);
|
||||
run_git(dir, &["checkout", "feature"]);
|
||||
base
|
||||
}
|
||||
13
crates/common/xai-test-utils/src/image.rs
Normal file
13
crates/common/xai-test-utils/src/image.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
//! Synthetic image fixtures shared across crates' test suites.
|
||||
|
||||
/// Wrap a PNG into a minimal single-frame ICO. `width`/`height` are the
|
||||
/// ICONDIRENTRY bytes (`0` means 256); the PNG carries the real dimensions.
|
||||
pub fn ico_with_png_frame(png: &[u8], width: u8, height: u8) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(22 + png.len());
|
||||
buf.extend_from_slice(&[0, 0, 1, 0, 1, 0]); // ICONDIR
|
||||
buf.extend_from_slice(&[width, height, 0, 0, 1, 0, 32, 0]); // ICONDIRENTRY
|
||||
buf.extend_from_slice(&(png.len() as u32).to_le_bytes()); // bytes in resource
|
||||
buf.extend_from_slice(&22u32.to_le_bytes()); // offset to the PNG payload
|
||||
buf.extend_from_slice(png);
|
||||
buf
|
||||
}
|
||||
25
crates/common/xai-test-utils/src/lib.rs
Normal file
25
crates/common/xai-test-utils/src/lib.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
//! Shared test utilities for xAI crates.
|
||||
//!
|
||||
//! Provides common helpers that are needed by many crates' test suites:
|
||||
//!
|
||||
//! - **Hermetic git**: [`git::ensure_hermetic_git_on_path`] prepends the Bazel-provided
|
||||
//! static `git` binary to `PATH` so that tests don't depend on a system-installed git.
|
||||
//! The [`require_git!`] macro is a convenient shorthand.
|
||||
//!
|
||||
//! - **Git repo helpers**: [`git::init_git_repo`] and [`git::git_commit_all`] for
|
||||
//! setting up throwaway git repos in tests.
|
||||
//!
|
||||
//! - **Bazel runfiles**: [`crate_root!`] resolves the crate root directory via
|
||||
//! Bazel runfiles (for `bazel test`) or `CARGO_MANIFEST_DIR` (for `cargo test`).
|
||||
//!
|
||||
//! - **Tracing capture**: [`tracing_capture::MessagePrefixCounter`] counts
|
||||
//! log lines by message prefix (thread-scoped or global install) for tests
|
||||
//! that assert on how often an instrumented code path ran.
|
||||
//!
|
||||
//! - **Env knobs**: [`env::env_usize`] for perf-repro test sizing.
|
||||
|
||||
pub mod env;
|
||||
pub mod git;
|
||||
pub mod image;
|
||||
pub mod runfiles_util;
|
||||
pub mod tracing_capture;
|
||||
46
crates/common/xai-test-utils/src/runfiles_util.rs
Normal file
46
crates/common/xai-test-utils/src/runfiles_util.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
//! Bazel runfiles helpers for locating test data.
|
||||
//!
|
||||
//! Under `bazel test`, source files and test data are accessed via the
|
||||
//! *runfiles* tree. Under `cargo test`, `CARGO_MANIFEST_DIR` provides
|
||||
//! the crate root. The [`crate_root!`] macro abstracts over both.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Try to resolve a runfiles path to an absolute directory.
|
||||
///
|
||||
/// Returns `Some(path)` when running under Bazel (with the `bazel` feature
|
||||
/// enabled) and the runfiles entry exists, `None` otherwise.
|
||||
pub fn try_resolve_runfiles(_path: &str) -> Option<PathBuf> {
|
||||
#[cfg(feature = "bazel")]
|
||||
{
|
||||
let r = runfiles::Runfiles::create().ok()?;
|
||||
runfiles::rlocation!(r, _path)
|
||||
}
|
||||
#[cfg(not(feature = "bazel"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the crate root directory, working under both `bazel test` and
|
||||
/// `cargo test`.
|
||||
///
|
||||
/// Under Bazel the path is resolved via runfiles; under Cargo it falls back
|
||||
/// to `CARGO_MANIFEST_DIR`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// use xai_test_utils::crate_root;
|
||||
///
|
||||
/// fn test_data_dir() -> std::path::PathBuf {
|
||||
/// crate_root!("_main/crates/common/xai-test-utils").join("testdata")
|
||||
/// }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! crate_root {
|
||||
($runfiles_path:expr) => {
|
||||
$crate::runfiles_util::try_resolve_runfiles($runfiles_path)
|
||||
.unwrap_or_else(|| ::std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")))
|
||||
};
|
||||
}
|
||||
101
crates/common/xai-test-utils/src/tracing_capture.rs
Normal file
101
crates/common/xai-test-utils/src/tracing_capture.rs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
//! Test-only tracing capture: count events whose `message` starts with a
|
||||
//! known prefix.
|
||||
//!
|
||||
//! Producers should export the exact log-line prefixes as `pub const`s next
|
||||
//! to the `tracing::debug!` call sites (e.g. `xai_hunk_tracker`'s
|
||||
//! `REFRESH_SCAN_LOG_PREFIX`) so tests never duplicate the strings.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
/// Extracts the formatted `message` field of one event.
|
||||
#[derive(Default)]
|
||||
struct MessageVisitor(String);
|
||||
|
||||
impl tracing::field::Visit for MessageVisitor {
|
||||
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
|
||||
if field.name() == "message" {
|
||||
self.0 = format!("{value:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `tracing_subscriber::Layer` counting, per registered prefix, the events
|
||||
/// whose `message` starts with it. Clones share the counts.
|
||||
#[derive(Clone)]
|
||||
pub struct MessagePrefixCounter {
|
||||
counters: Arc<Vec<(&'static str, AtomicUsize)>>,
|
||||
}
|
||||
|
||||
impl MessagePrefixCounter {
|
||||
pub fn new(prefixes: &[&'static str]) -> Self {
|
||||
Self {
|
||||
counters: Arc::new(prefixes.iter().map(|p| (*p, AtomicUsize::new(0))).collect()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Events counted so far for `prefix`. Panics on a prefix that was never
|
||||
/// registered — that is a bug in the test, not a zero count.
|
||||
pub fn count(&self, prefix: &str) -> usize {
|
||||
self.counters
|
||||
.iter()
|
||||
.find(|(p, _)| *p == prefix)
|
||||
.unwrap_or_else(|| panic!("prefix not registered with this counter: {prefix:?}"))
|
||||
.1
|
||||
.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for MessagePrefixCounter {
|
||||
fn on_event(
|
||||
&self,
|
||||
event: &tracing::Event<'_>,
|
||||
_ctx: tracing_subscriber::layer::Context<'_, S>,
|
||||
) {
|
||||
let mut visitor = MessageVisitor::default();
|
||||
event.record(&mut visitor);
|
||||
for (prefix, count) in self.counters.iter() {
|
||||
if visitor.0.starts_with(prefix) {
|
||||
count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Install a **thread-scoped** default subscriber counting `prefixes`; hold
|
||||
/// the guard for the test's lifetime. Only observes events emitted on the
|
||||
/// current thread — tasks under test must run on a current-thread runtime.
|
||||
pub fn install_prefix_counter_thread(
|
||||
prefixes: &[&'static str],
|
||||
) -> (tracing::subscriber::DefaultGuard, MessagePrefixCounter) {
|
||||
use tracing_subscriber::layer::SubscriberExt as _;
|
||||
let counter = MessagePrefixCounter::new(prefixes);
|
||||
let subscriber = tracing_subscriber::registry().with(counter.clone());
|
||||
(tracing::subscriber::set_default(subscriber), counter)
|
||||
}
|
||||
|
||||
/// Install the **process-global** subscriber counting `prefixes` — for tests
|
||||
/// whose subject spawns its own threads/runtimes. Panics if a global
|
||||
/// subscriber already exists: the test binary must own it.
|
||||
///
|
||||
/// `stderr_env_filter` additionally tees formatted logs matching the given
|
||||
/// `EnvFilter` directive to stderr (local debugging).
|
||||
pub fn install_prefix_counter_global(
|
||||
prefixes: &[&'static str],
|
||||
stderr_env_filter: Option<&str>,
|
||||
) -> MessagePrefixCounter {
|
||||
use tracing_subscriber::layer::{Layer as _, SubscriberExt as _};
|
||||
use tracing_subscriber::util::SubscriberInitExt as _;
|
||||
let counter = MessagePrefixCounter::new(prefixes);
|
||||
let fmt = stderr_env_filter.map(|filter| {
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_writer(std::io::stderr)
|
||||
.with_filter(tracing_subscriber::EnvFilter::new(filter))
|
||||
});
|
||||
tracing_subscriber::registry()
|
||||
.with(counter.clone())
|
||||
.with(fmt)
|
||||
.try_init()
|
||||
.expect("this test binary must own the global subscriber");
|
||||
counter
|
||||
}
|
||||
Loading…
Reference in a new issue