Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/karva/tests/it/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2233,7 +2233,7 @@ def test_static_env():
assert os.environ["KARVA"] == "1"
assert os.environ["KARVA_WORKER_ID"] == "0"
assert re.fullmatch(
r"[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}",
r"\d+-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}",
os.environ["KARVA_RUN_ID"],
), os.environ["KARVA_RUN_ID"]
assert os.environ["KARVA_WORKSPACE_ROOT"] == os.getcwd()
Expand Down
5 changes: 4 additions & 1 deletion crates/karva/tests/it/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ impl TestContext {
settings.add_filter(r"\[\s*\d+\.\d+s\]", "[TIME]");
settings.add_filter(r"(\s|\()(\d+m )?(\d+\.)?\d+(ms|s)", "$1[TIME]");
settings.add_filter(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}", "[DATETIME]");
settings.add_filter(r"run-\d+", "run-[TIMESTAMP]");
settings.add_filter(
r"run-\d+(-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})?",
"run-[TIMESTAMP]",
);
settings.add_filter(r"[-─]{30,}", "[LONG-LINE]");
settings.add_filter(r"karva \d+\.\d+\.\d+[a-zA-Z0-9._-]*", "karva [VERSION]");
settings.add_filter(r"karva\.exe", "karva");
Expand Down
1 change: 1 addition & 0 deletions crates/karva_cache/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ camino = { workspace = true }
ruff_db = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
uuid = { workspace = true }

[dev-dependencies]
insta = { workspace = true }
Expand Down
9 changes: 5 additions & 4 deletions crates/karva_cache/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,9 +341,10 @@ mod tests {
let cache_dir = Utf8PathBuf::try_from(tmp.path().to_path_buf()).unwrap();

let run_hash = RunHash::from_existing("run-500");
let run_name = run_hash.dir_name();

create_cache_with_stats(tmp.path(), "run-500", 0, r#"{"passed": 3, "failed": 1}"#);
create_cache_with_stats(tmp.path(), "run-500", 1, r#"{"passed": 2, "skipped": 1}"#);
create_cache_with_stats(tmp.path(), &run_name, 0, r#"{"passed": 3, "failed": 1}"#);
create_cache_with_stats(tmp.path(), &run_name, 1, r#"{"passed": 2, "skipped": 1}"#);

let cache = RunCache::new(&cache_dir, &run_hash);
let results = cache.aggregate_results().unwrap();
Expand All @@ -359,7 +360,7 @@ mod tests {
let cache_dir = Utf8PathBuf::try_from(tmp.path().to_path_buf()).unwrap();

let run_hash = RunHash::from_existing("run-600");
let run_dir = tmp.path().join("run-600");
let run_dir = tmp.path().join(run_hash.dir_name());
fs::create_dir_all(&run_dir).unwrap();

let cache = RunCache::new(&cache_dir, &run_hash);
Expand Down Expand Up @@ -524,7 +525,7 @@ mod tests {
let cache_dir = Utf8PathBuf::try_from(tmp.path().to_path_buf()).unwrap();
let run_hash = RunHash::from_existing("run-700");

let run_dir = tmp.path().join("run-700");
let run_dir = tmp.path().join(run_hash.dir_name());
let worker0 = run_dir.join("worker-0");
let worker1 = run_dir.join("worker-1");
fs::create_dir_all(&worker0).unwrap();
Expand Down
88 changes: 62 additions & 26 deletions crates/karva_cache/src/hash.rs
Original file line number Diff line number Diff line change
@@ -1,42 +1,59 @@
use std::fmt;
use std::time::{SystemTime, UNIX_EPOCH};

use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::RUN_PREFIX;

/// A unique identifier for a test run based on a millisecond timestamp.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
/// A unique identifier for a test run.
///
/// Combines a millisecond timestamp (for chronological ordering of cache
/// directories) with a UUID v4 (for uniqueness across dense CI matrices and
/// for correlating logs across worker processes). Serialized as
/// `<ms>-<uuid>`; the cache directory adds the `run-` prefix.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RunHash {
timestamp: u128,
uuid: Uuid,
}

impl RunHash {
/// Creates a new hash from the current system time.
/// Creates a new identifier for the current invocation.
pub fn current_time() -> Self {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("System time is before UNIX epoch")
.as_millis();

Self { timestamp }
Self {
timestamp,
uuid: Uuid::new_v4(),
}
}

/// Parses a hash from an existing run directory name (e.g. `run-1234`).
/// Parses a hash from an existing run directory name (e.g.
/// `run-1234-<uuid>`) or its bare `<ms>-<uuid>` form.
///
/// Falls back to timestamp `0` if the input cannot be parsed.
/// Falls back to a zero timestamp and nil UUID if the input cannot be
/// parsed; this keeps callers from having to handle malformed legacy
/// directories that may exist on disk.
pub fn from_existing(hash: &str) -> Self {
let timestamp = hash
.strip_prefix(RUN_PREFIX)
.unwrap_or(hash)
.parse()
.unwrap_or(0);
Self { timestamp }
let inner = hash.strip_prefix(RUN_PREFIX).unwrap_or(hash);
let (ts_str, uuid_str) = inner.split_once('-').unwrap_or((inner, ""));
let timestamp = ts_str.parse().unwrap_or(0);
let uuid = Uuid::parse_str(uuid_str).unwrap_or(Uuid::nil());
Self { timestamp, uuid }
}

/// Returns the string representation used as a directory name (e.g. `run-1234`).
/// Returns the bare `<ms>-<uuid>` form. This is the value exposed to
/// tests as `KARVA_RUN_ID` and passed between processes.
pub fn inner(&self) -> String {
format!("{RUN_PREFIX}{}", self.timestamp)
format!("{}-{}", self.timestamp, self.uuid)
}

/// Returns the directory name used in the cache (`run-<ms>-<uuid>`).
pub fn dir_name(&self) -> String {
format!("{RUN_PREFIX}{}", self.inner())
}

/// Returns the underlying timestamp, used for ordering runs chronologically.
Expand All @@ -47,7 +64,7 @@ impl RunHash {

impl fmt::Display for RunHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.inner())
write!(f, "{}", self.dir_name())
}
}

Expand All @@ -58,16 +75,23 @@ mod tests {
#[test]
fn current_time_produces_valid_hash() {
let hash = RunHash::current_time();
let inner = hash.inner();
assert!(inner.starts_with("run-"));
let dir = hash.dir_name();
assert!(dir.starts_with("run-"));
assert!(hash.sort_key() > 0);
assert!(dir.contains('-'));
}

#[test]
fn from_existing_roundtrips_with_dir_name() {
let original = RunHash::current_time();
let restored = RunHash::from_existing(&original.dir_name());
assert_eq!(original, restored);
}

#[test]
fn from_existing_roundtrips_with_inner() {
let original = RunHash::current_time();
let inner = original.inner();
let restored = RunHash::from_existing(&inner);
let restored = RunHash::from_existing(&original.inner());
assert_eq!(original, restored);
}

Expand All @@ -83,17 +107,29 @@ mod tests {
assert_eq!(hash.sort_key(), 0);
}

#[test]
fn from_existing_handles_legacy_timestamp_only_dir() {
let hash = RunHash::from_existing("run-42");
assert_eq!(hash.sort_key(), 42);
}

#[test]
fn sort_key_reflects_timestamp_ordering() {
let earlier = RunHash::from_existing("run-100");
let later = RunHash::from_existing("run-200");
let earlier = RunHash::from_existing("run-100-00000000-0000-4000-8000-000000000000");
let later = RunHash::from_existing("run-200-00000000-0000-4000-8000-000000000000");
assert!(earlier.sort_key() < later.sort_key());
}

#[test]
fn display_matches_inner() {
let hash = RunHash::from_existing("run-42");
assert_eq!(hash.to_string(), hash.inner());
assert_eq!(hash.to_string(), "run-42");
fn display_matches_dir_name() {
let hash = RunHash::current_time();
assert_eq!(hash.to_string(), hash.dir_name());
}

#[test]
fn two_invocations_produce_distinct_hashes_even_at_same_ms() {
let a = RunHash::current_time();
let b = RunHash::current_time();
assert_ne!(a, b);
}
}
1 change: 0 additions & 1 deletion crates/karva_runner/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ ctrlc = { workspace = true }
fastrand = { workspace = true }
ignore = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true }
which = { workspace = true }

[lints]
Expand Down
1 change: 0 additions & 1 deletion crates/karva_runner/src/orchestration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,6 @@ pub fn run_parallel_tests(
args,
num_workers,
profile: config.profile.as_deref().unwrap_or("default"),
run_id: &uuid::Uuid::new_v4().to_string(),
worker_binary: &worker_binary,
coverage_enabled: !project.settings().coverage().sources.is_empty(),
};
Expand Down
5 changes: 2 additions & 3 deletions crates/karva_runner/src/worker_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ pub struct WorkerSpawn<'a> {
pub args: &'a SubTestCommand,
pub num_workers: usize,
pub profile: &'a str,
pub run_id: &'a str,
pub worker_binary: &'a Utf8PathBuf,
pub coverage_enabled: bool,
}
Expand All @@ -29,7 +28,7 @@ pub fn worker_command(spawn: &WorkerSpawn, worker_id: usize, partition: &Partiti
let mut cmd = Command::new(spawn.worker_binary);
cmd.arg("--cache-dir")
.arg(spawn.cache_dir)
.arg("--run-hash")
.arg("--run-id")
.arg(spawn.run_hash.inner())
.arg("--worker-id")
.arg(worker_id.to_string())
Expand All @@ -38,7 +37,7 @@ pub fn worker_command(spawn: &WorkerSpawn, worker_id: usize, partition: &Partiti
.env("PYTHONUNBUFFERED", "1")
.env(WorkerEnvVars::KARVA, "1")
.env(WorkerEnvVars::KARVA_WORKER_ID, worker_id.to_string())
.env(WorkerEnvVars::KARVA_RUN_ID, spawn.run_id)
.env(WorkerEnvVars::KARVA_RUN_ID, spawn.run_hash.inner())
.env(
WorkerEnvVars::KARVA_WORKSPACE_ROOT,
spawn.project.cwd().as_str(),
Expand Down
10 changes: 7 additions & 3 deletions crates/karva_static/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,13 @@ env_vars! {
/// parallel workers without coordination.
pub const KARVA_WORKER_ID: &'static str = "KARVA_WORKER_ID";

/// Unique identifier (UUID) for a single `karva test` invocation,
/// shared by every worker. Useful for correlating logs and external
/// artifacts produced across multiple worker processes.
/// Unique identifier for a single `karva test` invocation, shared by
/// every worker. Encodes `<ms>-<uuid>`: a millisecond Unix timestamp
/// followed by a UUID v4. The timestamp prefix sorts chronologically
/// across runs; the UUID makes the id unique even when multiple jobs
/// start in the same millisecond. The same value names the run's
/// cache directory under `.karva_cache/run-<ms>-<uuid>`, which makes
/// correlating logs to cached artifacts straightforward.
pub const KARVA_RUN_ID: &'static str = "KARVA_RUN_ID";

/// Absolute path to the directory Karva resolved as the project root.
Expand Down
5 changes: 3 additions & 2 deletions crates/karva_worker/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ struct Args {
cache_dir: Utf8PathBuf,

/// Unique identifier for this test run, used for cache coordination.
/// Encodes `<ms>-<uuid>`; the cache directory adds the `run-` prefix.
#[arg(long)]
run_hash: String,
run_id: String,

/// Numeric identifier for this worker in a parallel test run.
#[arg(long)]
Expand Down Expand Up @@ -162,7 +163,7 @@ fn run(f: impl FnOnce(Vec<OsString>) -> Vec<OsString>) -> anyhow::Result<ExitSta
settings.set_filter(filter);
settings.set_run_ignored(run_ignored);

let run_hash = RunHash::from_existing(&args.run_hash);
let run_hash = RunHash::from_existing(&args.run_id);

let cache = RunCache::new(&args.cache_dir, &run_hash);

Expand Down
10 changes: 7 additions & 3 deletions docs/reference/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,13 @@ parallel workers without coordination.

### `KARVA_RUN_ID`

Unique identifier (UUID) for a single `karva test` invocation,
shared by every worker. Useful for correlating logs and external
artifacts produced across multiple worker processes.
Unique identifier for a single `karva test` invocation, shared by
every worker. Encodes `<ms>-<uuid>`: a millisecond Unix timestamp
followed by a UUID v4. The timestamp prefix sorts chronologically
across runs; the UUID makes the id unique even when multiple jobs
start in the same millisecond. The same value names the run's
cache directory under `.karva_cache/run-<ms>-<uuid>`, which makes
correlating logs to cached artifacts straightforward.

### `KARVA_WORKSPACE_ROOT`

Expand Down
Loading