diff --git a/CHANGELOG.md b/CHANGELOG.md index bddfe30..c9a9d24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and Base versions are tracked in the repo-root `VERSION` file. - Added a repo-owned `bin/base-test` runner and declared it through `base_manifest.yaml` so Base can dogfood `basectl test base`. - Added GitHub CLI authentication diagnostics to developer prerequisite checks. +- Added `basectl test -- ` passthrough for delegated test + command arguments. ### Changed diff --git a/README.md b/README.md index 02b18f6..7a08113 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,14 @@ root, exports `BASE_PROJECT`, `BASE_PROJECT_ROOT`, `BASE_PROJECT_MANIFEST`, and exists, and returns the command's exit status. Use `--dry-run` to inspect the resolved command without running it. +Pass additional arguments to the project's test command after `--`: + +```bash +basectl test example -- -k focused_case +``` + +For `test.mise`, Base passes those arguments after `mise run --`. + Once a project is discoverable, activate it with: ```bash diff --git a/cli/bash/commands/basectl/README.md b/cli/bash/commands/basectl/README.md index 85cb402..0a001ba 100644 --- a/cli/bash/commands/basectl/README.md +++ b/cli/bash/commands/basectl/README.md @@ -70,7 +70,8 @@ that delegate to `basectl`. `docs/basectl-onboard.md`. - `basectl test [project]` runs the project's manifest `test.command` or `test.mise` from the project root with Base project environment variables - exported. + exported. Use `basectl test -- ` to pass extra arguments + to the delegated test command. - `basectl update-profile` creates or refreshes managed sections in Bash and Zsh dotfiles. - `basectl update` updates the Base repository from Git and then runs `basectl setup`. - `basectl projects list` scans a workspace for `base_manifest.yaml` files and prints discovered project names and paths. diff --git a/cli/bash/commands/basectl/subcommands/test.sh b/cli/bash/commands/basectl/subcommands/test.sh index e30d578..437c297 100644 --- a/cli/bash/commands/basectl/subcommands/test.sh +++ b/cli/bash/commands/basectl/subcommands/test.sh @@ -7,7 +7,7 @@ readonly _base_test_subcommand_sourced base_test_subcommand_usage() { cat <<'EOF' Usage: - basectl test [project] [options] + basectl test [project] [options] [-- extra args...] Options: --workspace Workspace directory to scan. Defaults to BASE_HOME's parent. @@ -16,6 +16,7 @@ Options: -h, --help Show this help text. Run a project's declared test command from its project root. +Use -- to pass additional arguments to the declared test command. EOF } @@ -36,13 +37,61 @@ base_test_project_venv_dir() { printf '%s\n' "$HOME/.base.d/$project/.venv" } +base_test_format_extra_args() { + local arg quoted output="" + + for arg in "$@"; do + printf -v quoted '%q' "$arg" + output+=" $quoted" + done + printf '%s\n' "$output" +} + +base_test_command_with_extra_args() { + local command="$1" + shift + + if (($# == 0)); then + printf '%s\n' "$command" + return 0 + fi + + if [[ "$command" == mise\ run\ * ]]; then + printf '%s -- "$@"\n' "$command" + else + printf '%s "$@"\n' "$command" + fi +} + +base_test_display_command() { + local command="$1" + shift + + if (($# == 0)); then + printf '%s\n' "$command" + return 0 + fi + + if [[ "$command" == mise\ run\ * ]]; then + printf '%s --%s\n' "$command" "$(base_test_format_extra_args "$@")" + else + printf '%s%s\n' "$command" "$(base_test_format_extra_args "$@")" + fi +} + base_test_subcommand_main() { local project="" wrapper resolve_output resolved_name project_root manifest_path test_command venv_dir + local command_to_run display_command local dry_run=0 workspace_requested=0 - local args=() + local args=() extra_args=() while (($#)); do case "$1" in + --) + shift + extra_args=("$@") + break + ;; -h|--help|help) base_test_subcommand_usage return 0 @@ -114,11 +163,14 @@ base_test_subcommand_main() { log_warn "Project virtual environment was not found at '$venv_dir'. Run 'basectl setup $resolved_name' first." fi + command_to_run="$(base_test_command_with_extra_args "$test_command" "${extra_args[@]}")" + display_command="$(base_test_display_command "$test_command" "${extra_args[@]}")" + if [[ "$dry_run" == "1" ]]; then - printf '[DRY-RUN] Would run tests for project %q in %q: %s\n' "$resolved_name" "$project_root" "$test_command" + printf '[DRY-RUN] Would run tests for project %q in %q: %s\n' "$resolved_name" "$project_root" "$display_command" return 0 fi - log_info "Running tests for project '$resolved_name': $test_command" - (cd "$project_root" && bash -c "$test_command") + log_info "Running tests for project '$resolved_name': $display_command" + (cd "$project_root" && bash -c "$command_to_run" basectl-test "${extra_args[@]}") } diff --git a/cli/bash/commands/basectl/tests/basectl.bats b/cli/bash/commands/basectl/tests/basectl.bats index 592391e..2e6a5e1 100644 --- a/cli/bash/commands/basectl/tests/basectl.bats +++ b/cli/bash/commands/basectl/tests/basectl.bats @@ -860,6 +860,102 @@ EOF [ ! -e "$state_file" ] } +@test "basectl test passes extra args after separator to command" { + local python_bin="$TEST_HOME/.base.d/base/.venv/bin/python" + local workspace="$TEST_TMPDIR/workspace" + local state_file="$TEST_TMPDIR/test-state" + + mkdir -p "$(dirname "$python_bin")" "$workspace/demo" "$TEST_HOME/.base.d/demo/.venv/bin" + cat > "$python_bin" <<'EOF' +#!/usr/bin/env bash +if [[ "${1:-}" == "-m" && "${2:-}" == "base_projects" && "${3:-}" == "test-command" && "${4:-}" == "demo" ]]; then + printf 'demo\t%s\t%s\t%s\n' "${BASE_TEST_PROJECT_ROOT:?}" "${BASE_TEST_PROJECT_ROOT:?}/base_manifest.yaml" 'fake-test tests/' + exit 0 +fi +printf 'unexpected test python args: %s\n' "$*" >&2 +exit 1 +EOF + cat > "$TEST_HOME/.base.d/demo/.venv/bin/fake-test" <<'EOF' +#!/usr/bin/env bash +printf '%s\n' "$@" > "${BASE_TEST_TEST_STATE:?}" +EOF + chmod +x "$python_bin" "$TEST_HOME/.base.d/demo/.venv/bin/fake-test" + printf 'project:\n name: demo\ntest:\n command: fake-test tests/\nartifacts: []\n' > "$workspace/demo/base_manifest.yaml" + workspace="$(cd "$workspace" && pwd -P)" + + run env \ + HOME="$TEST_HOME" \ + PATH="/usr/bin:/bin:/usr/sbin:/sbin" \ + BASE_TEST_PROJECT_ROOT="$workspace/demo" \ + BASE_TEST_TEST_STATE="$state_file" \ + "$BASE_REPO_ROOT/bin/basectl" test demo -- -k "name with spaces" --verbose + + [ "$status" -eq 0 ] + [ "$(cat "$state_file")" = $'tests/\n-k\nname with spaces\n--verbose' ] +} + +@test "basectl test dry-run shows extra args with shell quoting" { + local python_bin="$TEST_HOME/.base.d/base/.venv/bin/python" + local workspace="$TEST_TMPDIR/workspace" + + mkdir -p "$(dirname "$python_bin")" "$workspace/demo" "$TEST_HOME/.base.d/demo/.venv/bin" + cat > "$python_bin" <<'EOF' +#!/usr/bin/env bash +if [[ "${1:-}" == "-m" && "${2:-}" == "base_projects" && "${3:-}" == "test-command" && "${4:-}" == "demo" ]]; then + printf 'demo\t%s\t%s\t%s\n' "${BASE_TEST_PROJECT_ROOT:?}" "${BASE_TEST_PROJECT_ROOT:?}/base_manifest.yaml" 'pytest tests/' + exit 0 +fi +printf 'unexpected test python args: %s\n' "$*" >&2 +exit 1 +EOF + chmod +x "$python_bin" + printf 'project:\n name: demo\ntest:\n command: pytest tests/\nartifacts: []\n' > "$workspace/demo/base_manifest.yaml" + workspace="$(cd "$workspace" && pwd -P)" + + run env \ + HOME="$TEST_HOME" \ + PATH="/usr/bin:/bin:/usr/sbin:/sbin" \ + BASE_TEST_PROJECT_ROOT="$workspace/demo" \ + "$BASE_REPO_ROOT/bin/basectl" test demo --dry-run -- -k "name with spaces" + + [ "$status" -eq 0 ] + [[ "$output" == *"pytest tests/ -k name\\ with\\ spaces"* ]] +} + +@test "basectl test passes extra args to mise task after separator" { + local python_bin="$TEST_HOME/.base.d/base/.venv/bin/python" + local workspace="$TEST_TMPDIR/workspace" + local state_file="$TEST_TMPDIR/test-state" + + mkdir -p "$(dirname "$python_bin")" "$workspace/demo" "$TEST_HOME/.base.d/demo/.venv/bin" + cat > "$python_bin" <<'EOF' +#!/usr/bin/env bash +if [[ "${1:-}" == "-m" && "${2:-}" == "base_projects" && "${3:-}" == "test-command" && "${4:-}" == "demo" ]]; then + printf 'demo\t%s\t%s\t%s\n' "${BASE_TEST_PROJECT_ROOT:?}" "${BASE_TEST_PROJECT_ROOT:?}/base_manifest.yaml" 'mise run unit' + exit 0 +fi +printf 'unexpected test python args: %s\n' "$*" >&2 +exit 1 +EOF + cat > "$TEST_HOME/.base.d/demo/.venv/bin/mise" <<'EOF' +#!/usr/bin/env bash +printf '%s\n' "$@" > "${BASE_TEST_TEST_STATE:?}" +EOF + chmod +x "$python_bin" "$TEST_HOME/.base.d/demo/.venv/bin/mise" + printf 'project:\n name: demo\ntest:\n mise: unit\nartifacts: []\n' > "$workspace/demo/base_manifest.yaml" + workspace="$(cd "$workspace" && pwd -P)" + + run env \ + HOME="$TEST_HOME" \ + PATH="/usr/bin:/bin:/usr/sbin:/sbin" \ + BASE_TEST_PROJECT_ROOT="$workspace/demo" \ + BASE_TEST_TEST_STATE="$state_file" \ + "$BASE_REPO_ROOT/bin/basectl" test demo -- -k focused + + [ "$status" -eq 0 ] + [ "$(cat "$state_file")" = $'run\nunit\n--\n-k\nfocused' ] +} + @test "basectl test warns when project venv is missing" { local python_bin="$TEST_HOME/.base.d/base/.venv/bin/python" local workspace="$TEST_TMPDIR/workspace" @@ -944,6 +1040,10 @@ EOF run_basectl test --unknown demo [ "$status" -eq 2 ] [[ "$output" == *"ERROR: Unknown test option '--unknown'."* ]] + + run_basectl test demo -- --unknown + [ "$status" -ne 2 ] + [[ "$output" != *"ERROR: Unknown test option '--unknown'."* ]] } @test "basectl clean delegates to the Python cleanup layer" { diff --git a/docs/architecture.md b/docs/architecture.md index bc3c48b..c11b931 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -349,7 +349,8 @@ orchestration actions. The design rule is delegation-first: setup and does not reimplement mise's version management. - Use a project-owned `test` contract for `basectl test ` delegation. Projects can declare either `test.command` for a shell command or `test.mise` - for a `mise run ` delegation. + for a `mise run ` delegation. Extra arguments after `basectl test + --` are passed through to the delegated command. - Let Base own the project virtual environment and Base-aware package reconciliation. - Do not run arbitrary project setup hooks until Base has a clear safety