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: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,5 @@ __marimo__/
demo/*.gif
demo/*.webm
demo/*.mp4

.claude/worktrees
40 changes: 40 additions & 0 deletions crates/tryke/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ pub struct Cli {
/// this flag in CI or in terminals that mis-render the sequence.
#[arg(long = "no-progress", global = true)]
pub no_progress: bool,

/// Directory for tryke's persistent discovery cache.
///
/// Overrides `[tool.tryke] cache_dir` in `pyproject.toml`. Defaults to
/// `<project-root>/.tryke/cache`.
#[arg(long = "cache-dir", global = true)]
pub cache_dir: Option<PathBuf>,
}

/// Reporter format used to render test results.
Expand Down Expand Up @@ -367,3 +374,36 @@ impl Commands {
}
}
}

#[cfg(test)]
mod tests {
use std::path::Path;

use clap::Parser;

use super::*;

#[test]
fn parses_global_cache_dir_before_subcommand() {
let cli = Cli::parse_from(["tryke", "--cache-dir", "/tmp/tryke-cache", "test"]);
assert_eq!(
cli.cache_dir.as_deref(),
Some(Path::new("/tmp/tryke-cache"))
);
}

#[test]
fn parses_global_cache_dir_after_subcommand() {
let cli = Cli::parse_from(["tryke", "test", "--cache-dir", "/tmp/tryke-cache"]);
assert_eq!(
cli.cache_dir.as_deref(),
Some(Path::new("/tmp/tryke-cache"))
);
}

#[test]
fn cache_dir_defaults_to_none() {
let cli = Cli::parse_from(["tryke", "test"]);
assert_eq!(cli.cache_dir, None);
}
}
42 changes: 26 additions & 16 deletions crates/tryke/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,16 @@ pub fn resolved_excludes(
}

/// Discover tests, optionally restricting to changed files.
///
/// `cache_dir` optionally overrides the discovery cache directory.
pub fn discover_tests(
root: &Path,
changed: bool,
base_branch: Option<&str>,
excludes: &[String],
cache_dir: Option<&Path>,
) -> DiscoverySelection {
let mut discoverer = Discoverer::new_with_excludes(root, excludes);
let mut discoverer = Discoverer::new_with_excludes_and_cache_dir(root, excludes, cache_dir);
discoverer.rediscover();
let warnings = all_discovery_warnings(&discoverer);
let hooks = discoverer.hooks();
Expand Down Expand Up @@ -143,20 +146,23 @@ pub fn discover_tests(
/// if any spec resolves to a nonexistent file or escapes the project
/// root — the existing post-filter (`TestFilter::apply`) still runs in
/// `main` and handles suffix-match semantics in that case.
///
/// `cache_dir` optionally overrides the discovery cache directory.
pub fn discover_tests_for_paths(
root: &Path,
path_specs: &[PathSpec],
excludes: &[String],
cache_dir: Option<&Path>,
) -> DiscoverySelection {
let walk_roots = match resolve_walk_roots(root, path_specs) {
Some(roots) => roots,
None => {
debug!("discover_tests_for_paths: falling back to full discovery");
return discover_tests(root, false, None, excludes);
return discover_tests(root, false, None, excludes, cache_dir);
}
};

let mut discoverer = Discoverer::new_with_excludes(root, excludes);
let mut discoverer = Discoverer::new_with_excludes_and_cache_dir(root, excludes, cache_dir);
let tests = discoverer.rediscover_restricted(&walk_roots);
let warnings = all_discovery_warnings(&discoverer);
let hooks = discoverer.hooks();
Expand Down Expand Up @@ -218,12 +224,15 @@ fn resolve_walk_roots(root: &Path, path_specs: &[PathSpec]) -> Option<Vec<PathBu
}

/// Discover all tests but place changed tests first in the returned list.
///
/// `cache_dir` optionally overrides the discovery cache directory.
pub fn discover_tests_changed_first(
root: &Path,
base_branch: Option<&str>,
excludes: &[String],
cache_dir: Option<&Path>,
) -> DiscoverySelection {
let mut discoverer = Discoverer::new_with_excludes(root, excludes);
let mut discoverer = Discoverer::new_with_excludes_and_cache_dir(root, excludes, cache_dir);
discoverer.rediscover();
let warnings = all_discovery_warnings(&discoverer);
let hooks = discoverer.hooks();
Expand Down Expand Up @@ -339,7 +348,7 @@ mod tests {
git_run(dir.path(), &["add", "test_feature.py"]);
git_run(dir.path(), &["commit", "-m", "add feature test"]);

let discovered = discover_tests(dir.path(), true, Some("main"), &[]);
let discovered = discover_tests(dir.path(), true, Some("main"), &[], None);
assert!(
discovered.tests.iter().any(|t| t.name == "test_feature"),
"should find the branch's test: {:?}",
Expand Down Expand Up @@ -373,7 +382,7 @@ mod tests {
)
.expect("write");

let discovered = discover_tests_changed_first(dir.path(), None, &[]);
let discovered = discover_tests_changed_first(dir.path(), None, &[], None);
let names: Vec<&str> = discovered.tests.iter().map(|t| t.name.as_str()).collect();

assert!(
Expand Down Expand Up @@ -406,7 +415,7 @@ mod tests {
)],
);

let discovered = discover_tests_changed_first(dir.path(), None, &[]);
let discovered = discover_tests_changed_first(dir.path(), None, &[], None);
assert!(
discovered.changed_prefix_len.is_none(),
"changed_prefix_len should be None when no changes"
Expand Down Expand Up @@ -443,7 +452,7 @@ mod tests {
git_run(dir.path(), &["add", "test_c.py"]);
git_run(dir.path(), &["commit", "-m", "add test_c"]);

let discovered = discover_tests_changed_first(dir.path(), Some("main"), &[]);
let discovered = discover_tests_changed_first(dir.path(), Some("main"), &[], None);
let names: Vec<&str> = discovered.tests.iter().map(|t| t.name.as_str()).collect();

assert!(
Expand Down Expand Up @@ -471,7 +480,7 @@ mod tests {
)
.expect("write test_dyn.py");

let discovered = discover_tests(dir.path(), false, None, &[]);
let discovered = discover_tests(dir.path(), false, None, &[], None);
assert!(
!discovered.warnings.is_empty(),
"should have at least one dynamic import warning"
Expand Down Expand Up @@ -520,7 +529,7 @@ mod tests {
),
]);
let specs = vec![pathspec_file("test_a.py")];
let discovered = discover_tests_for_paths(dir.path(), &specs, &[]);
let discovered = discover_tests_for_paths(dir.path(), &specs, &[], None);
let names: Vec<&str> = discovered.tests.iter().map(|t| t.name.as_str()).collect();
assert_eq!(names, vec!["test_a"], "got: {names:?}");
}
Expand All @@ -542,7 +551,7 @@ mod tests {
),
]);
let specs = vec![pathspec_file("tests")];
let discovered = discover_tests_for_paths(dir.path(), &specs, &[]);
let discovered = discover_tests_for_paths(dir.path(), &specs, &[], None);
let mut names: Vec<&str> = discovered.tests.iter().map(|t| t.name.as_str()).collect();
names.sort_unstable();
assert_eq!(names, vec!["test_a", "test_b"], "got: {names:?}");
Expand All @@ -563,7 +572,7 @@ mod tests {
// Dir + a contained file should dedupe to just the dir; both
// tests should be discovered (not just test_a).
let specs = vec![pathspec_file("tests"), pathspec_file("tests/test_a.py")];
let discovered = discover_tests_for_paths(dir.path(), &specs, &[]);
let discovered = discover_tests_for_paths(dir.path(), &specs, &[], None);
let mut names: Vec<&str> = discovered.tests.iter().map(|t| t.name.as_str()).collect();
names.sort_unstable();
assert_eq!(names, vec!["test_a", "test_b"], "got: {names:?}");
Expand All @@ -576,7 +585,7 @@ mod tests {
"from tryke import test\n@test\ndef test_real(): pass\n",
)]);
let specs = vec![pathspec_file("does_not_exist.py")];
let discovered = discover_tests_for_paths(dir.path(), &specs, &[]);
let discovered = discover_tests_for_paths(dir.path(), &specs, &[], None);
// Fallback runs full discovery; the post-filter (applied in
// main, not here) is what would narrow the set. So we expect
// every test in the project here.
Expand All @@ -600,7 +609,7 @@ mod tests {
),
]);
let specs = vec![PathSpec::FileLine(PathBuf::from("test_a.py"), 2)];
let discovered = discover_tests_for_paths(dir.path(), &specs, &[]);
let discovered = discover_tests_for_paths(dir.path(), &specs, &[], None);
let names: Vec<&str> = discovered.tests.iter().map(|t| t.name.as_str()).collect();
// The walk is restricted to test_a.py — test_b should not appear
// even before the post-filter narrows by line.
Expand All @@ -620,7 +629,8 @@ mod tests {
),
]);
let specs = vec![pathspec_file("tests")];
let discovered = discover_tests_for_paths(dir.path(), &specs, &["tests/skip".to_string()]);
let discovered =
discover_tests_for_paths(dir.path(), &specs, &["tests/skip".to_string()], None);
let names: Vec<&str> = discovered.tests.iter().map(|t| t.name.as_str()).collect();
assert_eq!(names, vec!["test_a"], "got: {names:?}");
}
Expand All @@ -635,7 +645,7 @@ mod tests {
let outside_file = outside.path().join("stray.py");
std::fs::write(&outside_file, "x = 1\n").expect("write stray");
let specs = vec![PathSpec::File(outside_file)];
let discovered = discover_tests_for_paths(dir.path(), &specs, &[]);
let discovered = discover_tests_for_paths(dir.path(), &specs, &[], None);
let names: Vec<&str> = discovered.tests.iter().map(|t| t.name.as_str()).collect();
// Out-of-root spec falls back to full discovery rather than
// attempting to walk outside the project.
Expand Down
10 changes: 5 additions & 5 deletions crates/tryke/src/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ mod tests {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("pyproject.toml"), "").expect("write pyproject.toml");
let excludes = resolved_excludes(dir.path(), &[], &[]);
let tests = discover_tests(dir.path(), false, None, &excludes).tests;
let tests = discover_tests(dir.path(), false, None, &excludes, None).tests;
let _ = run_tests(
reporter,
dir.path(),
Expand Down Expand Up @@ -353,7 +353,7 @@ mod tests {
std::fs::write(dir.path().join("pyproject.toml"), "").expect("write pyproject.toml");
let mut reporter = TextReporter::new();
// Non-git directory → git_changed_files returns None → discover_tests runs all (0 here)
let tests = discover_tests(dir.path(), true, None, &[]).tests;
let tests = discover_tests(dir.path(), true, None, &[], None).tests;
assert!(
run_tests(
&mut reporter,
Expand Down Expand Up @@ -399,7 +399,7 @@ def test_failing():
)
.expect("write test file");

let tests = discover_tests(dir.path(), false, None, &[]).tests;
let tests = discover_tests(dir.path(), false, None, &[], None).tests;
assert_eq!(tests.len(), 2);

let pool = WorkerPool::with_python_path(
Expand Down Expand Up @@ -449,7 +449,7 @@ def test_failing():
"from tryke import test, expect\n\n@test\ndef test_ok():\n expect(1 + 1).to_equal(2)\n",
)
.expect("write test file");
let tests = discover_tests(dir.path(), false, None, &[]).tests;
let tests = discover_tests(dir.path(), false, None, &[], None).tests;
let mut reporter = TextReporter::with_writer(Vec::new());
let pool = WorkerPool::with_python_path(
1,
Expand Down Expand Up @@ -488,7 +488,7 @@ def test_failing():
"from tryke import test, expect\n\n@test\ndef test_bad():\n expect(1 + 1).to_equal(3)\n",
)
.expect("write test file");
let tests = discover_tests(dir.path(), false, None, &[]).tests;
let tests = discover_tests(dir.path(), false, None, &[], None).tests;
let mut reporter = TextReporter::with_writer(Vec::new());
let pool = WorkerPool::with_python_path(
1,
Expand Down
21 changes: 14 additions & 7 deletions crates/tryke/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ pub fn run_graph(
connected_only: bool,
changed: bool,
base_branch: Option<&str>,
cache_dir: Option<&Path>,
) -> Result<()> {
let cwd = std::env::current_dir()?;
let root_path = root.unwrap_or(&cwd);
let mut discoverer = Discoverer::new_with_excludes(root_path, excludes);
let mut discoverer =
Discoverer::new_with_excludes_and_cache_dir(root_path, excludes, cache_dir);
discoverer.rediscover();

let changed_files = if changed {
Expand Down Expand Up @@ -101,10 +103,15 @@ pub fn run_graph(
/// dependency names (references to hooks that don't exist in any
/// discovered module) are printed with a `?` suffix so users can spot
/// typos or missing fixtures without reading through test output.
pub fn run_fixture_graph(root: Option<&Path>, excludes: &[String]) -> Result<()> {
pub fn run_fixture_graph(
root: Option<&Path>,
excludes: &[String],
cache_dir: Option<&Path>,
) -> Result<()> {
let cwd = std::env::current_dir()?;
let root_path = root.unwrap_or(&cwd);
let mut discoverer = Discoverer::new_with_excludes(root_path, excludes);
let mut discoverer =
Discoverer::new_with_excludes_and_cache_dir(root_path, excludes, cache_dir);
discoverer.rediscover();

let hooks = discoverer.hooks();
Expand Down Expand Up @@ -201,7 +208,7 @@ mod tests {
"from utils import helper\n@test\ndef test_foo(): pass\n",
)
.expect("write");
assert!(run_graph(Some(dir.path()), &[], false, false, None).is_ok());
assert!(run_graph(Some(dir.path()), &[], false, false, None, None).is_ok());
}

#[test]
Expand All @@ -219,7 +226,7 @@ mod tests {
"@test\ndef test_isolated(): pass\n",
)
.expect("write");
assert!(run_graph(Some(dir.path()), &[], true, false, None).is_ok());
assert!(run_graph(Some(dir.path()), &[], true, false, None, None).is_ok());
}

#[test]
Expand All @@ -237,7 +244,7 @@ mod tests {
def test_it(s=Depends(session)):\n pass\n",
)
.expect("write");
assert!(run_fixture_graph(Some(dir.path()), &[]).is_ok());
assert!(run_fixture_graph(Some(dir.path()), &[], None).is_ok());
}

#[test]
Expand All @@ -249,6 +256,6 @@ mod tests {
"@test\ndef test_it(): pass\n",
)
.expect("write");
assert!(run_fixture_graph(Some(dir.path()), &[]).is_ok());
assert!(run_fixture_graph(Some(dir.path()), &[], None).is_ok());
}
}
Loading
Loading