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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <project> -- <args...>` passthrough for delegated test
command arguments.

### Changed

Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <task> --`.

Once a project is discoverable, activate it with:

```bash
Expand Down
3 changes: 2 additions & 1 deletion cli/bash/commands/basectl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <project> -- <args...>` 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.
Expand Down
62 changes: 57 additions & 5 deletions cli/bash/commands/basectl/subcommands/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path> Workspace directory to scan. Defaults to BASE_HOME's parent.
Expand All @@ -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
}

Expand All @@ -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
Expand Down Expand Up @@ -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[@]}")
}
100 changes: 100 additions & 0 deletions cli/bash/commands/basectl/tests/basectl.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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" {
Expand Down
3 changes: 2 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <project>` delegation.
Projects can declare either `test.command` for a shell command or `test.mise`
for a `mise run <task>` delegation.
for a `mise run <task>` delegation. Extra arguments after `basectl test
<project> --` 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
Expand Down
Loading