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
26 changes: 26 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,32 @@ jobs:
lib/bash/version/tests/lib_version.bats \
tests/install.bats

integration:
name: Integration tests
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Install dependencies
run: |
python -m venv .integration-venv
.integration-venv/bin/python -m pip install --upgrade pip
.integration-venv/bin/python -m pip install -r requirements-dev.txt
sudo apt-get update
sudo apt-get install -y bats

- name: Run integration tests
env:
BASE_INTEGRATION_PYTHON: ${{ github.workspace }}/.integration-venv/bin/python
run: |
bats tests/integration/base_workflows.bats

security:
name: Security scanners
runs-on: ubuntu-latest
Expand Down
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ basectl test base
git diff --check
```

Use the integration suite when a change affects cross-command workflows,
workspace discovery, setup/check/doctor behavior, shell profile wiring, or
installation layout assumptions. See [Testing](docs/testing.md) for the testing
layers and integration-test boundaries.

Use `basectl setup --dev` to install developer prerequisites such as BATS and
the GitHub CLI. Use `basectl check --dev` or `basectl doctor --dev` to diagnose
missing developer tools.
Expand Down
3 changes: 2 additions & 1 deletion bin/base-test
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ bats \
lib/bash/runtime/tests/runtime_bashrc.bats \
lib/bash/std/tests/lib_std.bats \
lib/bash/version/tests/lib_version.bats \
tests/install.bats
tests/install.bats \
tests/integration/base_workflows.bats
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ reference. The filename should answer "what is this about?"
dispatch order, public launchers, and runtime shell behavior.
- [Linux Support](linux-support.md) defines the first Ubuntu/Debian runtime
support plan and bootstrap boundaries.
- [Testing](testing.md) explains Base's Python, BATS, and hermetic integration
test layers.
- [Tool Boundaries](tool-boundaries.md) records ecosystem decisions for tools
such as `mise`, `direnv`, Homebrew, IDEs, Docker, and dotfile managers.

Expand Down
56 changes: 56 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Testing

Base uses three test layers. Prefer the narrowest layer that proves the behavior,
then broaden when a change crosses command or runtime boundaries.

## Python Unit Tests

Python engine and helper behavior lives under `cli/python/**/tests/` and
`lib/python/**/tests/`. These tests should cover parsing, manifest merging,
artifact decisions, JSON output, and error handling without launching public
shell commands.

Run them with:

```bash
PYTHONPATH=lib/python:cli/python python -m pytest
```

## Bash Command And Library Tests

BATS tests next to Bash commands and libraries cover shell parsing, dispatch,
small command contracts, and failure messages. Keep these focused on one command
or library at a time.

Run the full command/library suite through:

```bash
basectl test base
```

## Integration Tests

Integration tests live under `tests/integration/`. They run real `basectl`
launchers against a temporary `HOME`, temporary workspace, copied Base runtime,
and fake project repositories. External platform tools such as `brew` and
`xcode-select` are stubbed so the suite stays deterministic, network-free, and
safe for local machines and CI.

Add integration coverage when a change affects:

- workspace discovery across Base and project repositories
- `basectl setup`, `check`, `doctor`, or `test` working together
- shell profile update behavior
- installation layout assumptions, including Homebrew-style Base homes
- public command behavior that cannot be proven by a single command unit test

The default integration suite should not install real Homebrew packages, edit
real shell startup files, depend on network access, or mutate repositories
outside the temporary BATS workspace.

Run only integration tests with:

```bash
BASE_INTEGRATION_PYTHON="$HOME/.base.d/base/.venv/bin/python" \
bats tests/integration/base_workflows.bats
```
244 changes: 244 additions & 0 deletions tests/integration/base_workflows.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
#!/usr/bin/env bats

load ../../lib/bash/tests/test_helper.sh
bats_require_minimum_version 1.5.0

setup() {
setup_test_tmpdir
TEST_HOME="$TEST_TMPDIR/home"
TEST_WORKSPACE="$TEST_TMPDIR/workspace"
TEST_STATE_DIR="$TEST_TMPDIR/state"
TEST_MOCKBIN="$TEST_TMPDIR/mockbin"
TEST_XCODE_DIR="$TEST_TMPDIR/CommandLineTools"
TEST_INTEGRATION_PYTHON="${BASE_INTEGRATION_PYTHON:-$HOME/.base.d/base/.venv/bin/python}"

[[ -x "$TEST_INTEGRATION_PYTHON" ]] || skip "set BASE_INTEGRATION_PYTHON to a Python with Base test dependencies"

mkdir -p "$TEST_HOME" "$TEST_WORKSPACE" "$TEST_STATE_DIR" "$TEST_MOCKBIN" "$TEST_XCODE_DIR"
TEST_HOME="$(cd "$TEST_HOME" && pwd -P)"
TEST_WORKSPACE="$(cd "$TEST_WORKSPACE" && pwd -P)"
TEST_STATE_DIR="$(cd "$TEST_STATE_DIR" && pwd -P)"
TEST_MOCKBIN="$(cd "$TEST_MOCKBIN" && pwd -P)"
TEST_XCODE_DIR="$(cd "$TEST_XCODE_DIR" && pwd -P)"
TEST_BASE_HOME="$TEST_WORKSPACE/base"
TEST_PROJECT_ROOT="$TEST_WORKSPACE/demo"

mkdir -p "$TEST_XCODE_DIR/usr/bin"
touch "$TEST_XCODE_DIR/usr/bin/clang"

create_base_runtime "$TEST_BASE_HOME"
create_fake_platform_tools
create_python_venv "$TEST_HOME/.base.d/base/.venv"
create_demo_project "$TEST_PROJECT_ROOT"
create_python_venv "$TEST_HOME/.base.d/demo/.venv"
create_fake_project_test_command "$TEST_HOME/.base.d/demo/.venv/bin/fake-test"
}

create_base_runtime() {
local base_home="$1"

mkdir -p "$base_home"
cp -R "$BASE_REPO_ROOT/bin" "$base_home/bin"
cp -R "$BASE_REPO_ROOT/cli" "$base_home/cli"
cp -R "$BASE_REPO_ROOT/lib" "$base_home/lib"
cp "$BASE_REPO_ROOT/base_init.sh" "$base_home/base_init.sh"
cp "$BASE_REPO_ROOT/base_manifest.yaml" "$base_home/base_manifest.yaml"
cp "$BASE_REPO_ROOT/VERSION" "$base_home/VERSION"
}

create_fake_platform_tools() {
cat > "$TEST_MOCKBIN/brew" <<'EOF'
#!/usr/bin/env bash
case "${1:-}" in
--prefix)
printf '%s\n' "${BASE_INTEGRATION_BREW_PREFIX:?}"
exit 0
;;
list)
case "${2:-}" in
python@3.13) exit 0 ;;
esac
exit 1
;;
bundle)
[[ "${2:-}" == "check" ]] && exit 0
;;
esac
printf 'unexpected brew args: %s\n' "$*" >&2
exit 1
EOF
chmod +x "$TEST_MOCKBIN/brew"

cat > "$TEST_MOCKBIN/xcode-select" <<'EOF'
#!/usr/bin/env bash
if [[ "${1:-}" == "-p" ]]; then
printf '%s\n' "${BASE_SETUP_XCODE_COMMAND_LINE_TOOLS_DIR:?}"
exit 0
fi
printf 'unexpected xcode-select args: %s\n' "$*" >&2
exit 1
EOF
chmod +x "$TEST_MOCKBIN/xcode-select"
}

create_python_venv() {
local venv_dir="$1"

mkdir -p "$venv_dir/bin"
cat > "$venv_dir/bin/python" <<EOF
#!/usr/bin/env bash
exec "$TEST_INTEGRATION_PYTHON" "\$@"
EOF
chmod +x "$venv_dir/bin/python"
printf 'python-home = integration-test\n' > "$venv_dir/pyvenv.cfg"
printf '#!/usr/bin/env bash\n' > "$venv_dir/bin/activate"
}

create_demo_project() {
local project_root="$1"

mkdir -p "$project_root"
cat > "$project_root/base_manifest.yaml" <<'EOF'
project:
name: demo
test:
command: fake-test tests/
artifacts: []
EOF
}

create_fake_project_test_command() {
local command_path="$1"

cat > "$command_path" <<'EOF'
#!/usr/bin/env bash
{
printf 'project=%s\n' "${BASE_PROJECT:-}"
printf 'root=%s\n' "${BASE_PROJECT_ROOT:-}"
printf 'manifest=%s\n' "${BASE_PROJECT_MANIFEST:-}"
printf 'venv=%s\n' "${BASE_PROJECT_VENV_DIR:-}"
printf 'pwd=%s\n' "$PWD"
printf 'args='
printf '<%s>' "$@"
printf '\n'
} > "${BASE_INTEGRATION_STATE_DIR:?}/fake-test.out"
EOF
chmod +x "$command_path"
}

run_basectl() {
run env \
HOME="$TEST_HOME" \
PATH="$TEST_MOCKBIN:/usr/bin:/bin:/usr/sbin:/sbin" \
OSTYPE=darwin24 \
BASE_INTEGRATION_BREW_PREFIX="$TEST_TMPDIR/homebrew-prefix" \
BASE_INTEGRATION_STATE_DIR="$TEST_STATE_DIR" \
BASE_SETUP_BREW_BIN="$TEST_MOCKBIN/brew" \
BASE_SETUP_NOTIFY=false \
BASE_SETUP_XCODE_COMMAND_LINE_TOOLS_DIR="$TEST_XCODE_DIR" \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
"$TEST_BASE_HOME/bin/basectl" "$@"
}

run_basectl_separate_stderr() {
run --separate-stderr env \
HOME="$TEST_HOME" \
PATH="$TEST_MOCKBIN:/usr/bin:/bin:/usr/sbin:/sbin" \
OSTYPE=darwin24 \
BASE_INTEGRATION_BREW_PREFIX="$TEST_TMPDIR/homebrew-prefix" \
BASE_INTEGRATION_STATE_DIR="$TEST_STATE_DIR" \
BASE_SETUP_BREW_BIN="$TEST_MOCKBIN/brew" \
BASE_SETUP_NOTIFY=false \
BASE_SETUP_XCODE_COMMAND_LINE_TOOLS_DIR="$TEST_XCODE_DIR" \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
"$TEST_BASE_HOME/bin/basectl" "$@"
}

@test "basectl resolves version and discovers sibling workspace projects" {
local expected_version

expected_version="$(cat "$BASE_REPO_ROOT/VERSION")"

run_basectl --version
[ "$status" -eq 0 ]
[ "$output" = "basectl $expected_version" ]

run_basectl projects list
[ "$status" -eq 0 ]
[[ "$output" == *$'base\t'"$TEST_BASE_HOME"* ]]
[[ "$output" == *$'demo\t'"$TEST_PROJECT_ROOT"* ]]
}

@test "basectl setup, check, and doctor run against an isolated project" {
run_basectl setup demo
[ "$status" -eq 0 ]
[[ "$output" == *"Resolved project 'demo' at '$TEST_PROJECT_ROOT'."* ]]
[[ "$output" == *"Project 'demo' setup is complete."* ]]
[[ "$output" == *"Base CLI setup is complete."* ]]

run_basectl check demo
[ "$status" -eq 0 ]
[[ "$output" == *"Base CLI environment and project 'demo' check passed."* ]]

run_basectl doctor demo
[ "$status" -eq 0 ]
[[ "$output" == *"Base doctor for project 'demo'"* ]]
[[ "$output" == *"Base doctor found no blocking issues for project 'demo'."* ]]
}

@test "basectl check and doctor emit structured project JSON" {
run_basectl_separate_stderr check demo --format json
[ "$status" -eq 0 ]
[ "${stderr:-}" = "" ]
[[ "$output" == *'"ok": true'* ]]
[[ "$output" == *'"project": "demo"'* ]]
[[ "$output" == *'"project_checks":'* ]]
[[ "$output" == *'"name": "click"'* ]]

run_basectl_separate_stderr doctor demo --format json
[ "$status" -eq 0 ]
[ "${stderr:-}" = "" ]
[[ "$output" == *'"ok": true'* ]]
[[ "$output" == *'"project": "demo"'* ]]
[[ "$output" == *'"project_findings":'* ]]
[[ "$output" == *'"status": "ok"'* ]]
}

@test "basectl test delegates from the project root with project environment" {
run_basectl test demo -- -k "focused case"
[ "$status" -eq 0 ]

grep -Fqx "project=demo" "$TEST_STATE_DIR/fake-test.out"
grep -Fqx "root=$TEST_PROJECT_ROOT" "$TEST_STATE_DIR/fake-test.out"
grep -Fqx "manifest=$TEST_PROJECT_ROOT/base_manifest.yaml" "$TEST_STATE_DIR/fake-test.out"
grep -Fqx "venv=$TEST_HOME/.base.d/demo/.venv" "$TEST_STATE_DIR/fake-test.out"
grep -Fqx "pwd=$TEST_PROJECT_ROOT" "$TEST_STATE_DIR/fake-test.out"
grep -Fqx "args=<tests/><-k><focused case>" "$TEST_STATE_DIR/fake-test.out"
}

@test "basectl update-profile dry-run does not write real shell startup files" {
run_basectl update-profile --dry-run
[ "$status" -eq 0 ]
[[ "$output" == *"[DRY-RUN] Would update '$TEST_HOME/.base.d/profile.conf'."* ]]
[[ "$output" == *"[DRY-RUN] Would update '$TEST_HOME/.bashrc' with section 'bashrc'."* ]]

[ ! -e "$TEST_HOME/.base.d/profile.conf" ]
[ ! -e "$TEST_HOME/.bashrc" ]
[ ! -e "$TEST_HOME/.zshrc" ]
}

@test "brew-like Base homes can discover projects through explicit workspace override" {
local brew_base_home="$TEST_TMPDIR/homebrew/opt/base/libexec"

create_base_runtime "$brew_base_home"

run env \
HOME="$TEST_HOME" \
PATH="$TEST_MOCKBIN:/usr/bin:/bin:/usr/sbin:/sbin" \
BASE_INTEGRATION_BREW_PREFIX="$TEST_TMPDIR/homebrew-prefix" \
"$brew_base_home/bin/basectl" projects list --workspace "$TEST_WORKSPACE"

[ "$status" -eq 0 ]
[[ "$output" == *$'base\t'"$TEST_BASE_HOME"* ]]
[[ "$output" == *$'demo\t'"$TEST_PROJECT_ROOT"* ]]
}
Loading