Skip to content
Open
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
8 changes: 8 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/aspect-cli/src/builtins/aspect/MODULE.aspect
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use_task("github.axl", "token")
use_task("run.axl", "run")
use_task("test.axl", "test")
use_task("axl_add.axl", "add")
use_task("axl_test.axl", "test")
use_task("delivery.axl", "delivery")
use_task("lint.axl", "lint")
use_task("format.axl", "format")
Expand Down
32 changes: 32 additions & 0 deletions crates/aspect-cli/src/builtins/aspect/axl_test.axl
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""`aspect axl test` — run AXL `*.test.axl` unit tests.

Discovers `*.test.axl` files under the given paths (files or directories,
defaulting to the Aspect workspace root), loads each through the normal AXL
load path — so a test file's own `load(...)`s resolve exactly as they would in
any other module — and runs each file's top-level `def test_*(t)` functions,
reporting pass/fail per test. Discovery does not descend into hidden
directories (e.g. `.git`) or follow directory symlinks (so Bazel output trees
are skipped).

The runner is implemented natively and intercepts this command before the
ordinary task phases run; this task exists to define the command's CLI surface
(its `paths` argument and help text).
"""

def _impl(ctx: TaskContext) -> int:
# `aspect axl test` is dispatched to the native runner before a task
# implementation would execute. Reaching here means that interception did
# not fire — surface it loudly rather than silently reporting no tests.
print("internal error: `aspect axl test` was not dispatched to the native test runner")
return 1

test = task(
group = ["axl"],
summary = "Run AXL *.test.axl unit tests.",
implementation = _impl,
args = {
"paths": args.positional(
description = "Files or directories to search for `*.test.axl` tests. Defaults to the Aspect workspace root.",
),
},
)
11 changes: 11 additions & 0 deletions crates/aspect-cli/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,17 @@ impl Dispatch {
merge_args(task.args(), task.overrides(), leaf, heap, Scope::Task)
}

/// Values of a positional / string-list arg on the selected leaf command,
/// read straight from the parsed matches. Used by the native `aspect axl
/// test` path, which needs the `paths` positional without building a full
/// `Arguments` on a heap.
pub fn string_list(&self, key: &str) -> Vec<String> {
let leaf = deepest_subcommand(&self.matches).expect("dispatch built from valid matches");
leaf.get_many::<String>(key)
.map(|it| it.cloned().collect())
.unwrap_or_default()
}

/// Build the merged `Arguments` for a feature implementation invocation.
pub fn feature_args<'v>(&self, feat: &dyn FeatureLike<'v>, heap: Heap<'v>) -> Arguments<'v> {
let leaf = deepest_subcommand(&self.matches).expect("dispatch built from valid matches");
Expand Down
17 changes: 17 additions & 0 deletions crates/aspect-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ async fn run() -> Result<ExitCode, anyhow::Error> {
aspect_root.clone(),
bazel_root.clone(),
git_root.clone(),
&root_mod,
&modules,
);
let mut mpe = MultiPhaseEval::new(env, &loader);
Expand Down Expand Up @@ -162,6 +163,22 @@ async fn run() -> Result<ExitCode, anyhow::Error> {

let dispatch = cmd.dispatch(matches)?;

// `aspect axl test` is served by the native test runner. It borrows
// the live loader to load every `*.test.axl` file through the normal
// load path — so a test file's own `load(...)`s resolve — then runs
// its `test_*` functions. This bypasses the ordinary feature/task
// phases, which a self-contained test run does not need.
{
let tasks = mpe.tasks();
if let Some(task) = tasks.get(dispatch.task_id) {
if task.name() == "test" && task.group().as_slice() == ["axl"] {
let paths = dispatch.string_list("paths");
let code = axl_runtime::engine::testing::run_tests(&loader, &paths)?;
return Ok(ExitCode::from(code as u8));
}
}
}

// Print the "Running <task>" header before feature
// implementations run so any diagnostic output from feature
// initialization (auth WARNINGs, tip blocks, etc.) is
Expand Down
1 change: 1 addition & 0 deletions crates/axl-proto/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ rust_library(
visibility = [
"//crates/axl-runtime:__pkg__",
"//crates/basil:__pkg__",
"//crates/basil-core:__pkg__",
"//crates/build-event-stream:__pkg__",
],
deps = [
Expand Down
1 change: 1 addition & 0 deletions crates/axl-runtime/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ rust_library(
"//crates/aspect-telemetry",
"//crates/axl-proto",
"//crates/axl-types",
"//crates/basil-core",
"//crates/bazelrc",
"//crates/build-event-stream",
"//crates/galvanize",
Expand Down
1 change: 1 addition & 0 deletions crates/axl-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ bazelrc = { path = "../bazelrc" }
axl-types = { path = "../axl-types" }
axl-proto = { path = "../axl-proto" }
starbuf-derive = { path = "../starbuf-derive" }
basil-core = { path = "../basil-core" }
build-event-stream = { path = "../build-event-stream" }
galvanize = { path = "../galvanize" }

Expand Down
5 changes: 4 additions & 1 deletion crates/axl-runtime/src/docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ pub fn documentation() -> anyhow::Result<Documentation> {
// short-circuit before any caller-context lookups in `FileLoader::load`),
// so no seeding is required.
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
let loader = Loader::new("docgen".to_string(), cwd.clone(), cwd, None, &[]);
// Docgen only evaluates leaf `@std`/`@bazel` modules, which never consult
// the root scope — an empty placeholder module satisfies the signature.
let root_mod = crate::module::Mod::default();
let loader = Loader::new("docgen".to_string(), cwd.clone(), cwd, None, &root_mod, &[]);

let mut builtins = Vec::new();
let modules = [
Expand Down
Loading