diff --git a/README.md b/README.md index add9590..e4cde22 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,29 @@ ANSIBLE_COLLECTIONS_PATH="$PWD/.ansible/collections" \ ../.venv/bin/ansible-lint . ``` +## Development and integration validation + +Create the workspace virtual environment described by the control repository +and run the complete local suite from this repository with: + +```sh +tests/integration +``` + +The driver installs the pinned collection into the ignored local `.ansible` +directory, runs static checks, and then mutates the local host using privilege +escalation. It manages `/usr/local/src/seriatim`, +`/usr/local/src/seriatim.ansible-build-state`, and +`/usr/local/bin/seriatim`; do not run it where those paths contain data that +must be preserved. It verifies convergence, idempotence, platform guards, +authoritative source cleanup, compiler-version invalidation, and preservation +of a working installation after a failed build. + +The driver uses temporary Go wrappers and removes them on exit. It attempts to +restore the default role state after a failure. Recovery consists only of +rerunning `tests/integration` or the default `tests/converge.yml`; no manual +deletion of managed paths is required. + ## License BSD-3-Clause diff --git a/tests/integration b/tests/integration new file mode 100755 index 0000000..37e6c8b --- /dev/null +++ b/tests/integration @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +set -euo pipefail + +repository_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +workspace_venv=$(cd "$repository_root/../.venv" && pwd) +test_tmp=$(mktemp -d) + +export ANSIBLE_COLLECTIONS_PATH="$repository_root/.ansible/collections" +export ANSIBLE_ROLES_PATH="$(dirname "$repository_root")" + +playbook="$workspace_venv/bin/ansible-playbook" +inventory="$repository_root/tests/inventory" +converge="$repository_root/tests/converge.yml" + +run_converge() { + "$playbook" -i "$inventory" "$converge" "$@" +} + +restore_default() { + run_converge >/dev/null 2>&1 +} + +cleanup() { + exit_status=$? + restore_default || true + rm -rf -- "$test_tmp" + exit "$exit_status" +} +trap cleanup EXIT + +require_clean_recap() { + recap_file=$1 + grep -Eq 'changed=0 .*failed=0' "$recap_file" +} + +require_changed_recap() { + recap_file=$1 + grep -Eq 'changed=[1-9][0-9]* .*failed=0' "$recap_file" +} + +binary_digest() { + sha256sum /usr/local/bin/seriatim | awk '{print $1}' +} + +state_digest() { + sudo sha256sum /usr/local/src/seriatim.ansible-build-state/* | + sha256sum | awk '{print $1}' +} + +cd "$repository_root" +"$workspace_venv/bin/ansible-galaxy" collection install \ + -p .ansible/collections -r collections/requirements.yml +"$playbook" -i "$inventory" "$converge" --syntax-check +"$workspace_venv/bin/ansible-lint" . + +run_converge +"$playbook" -i "$inventory" tests/verify.yml +run_converge | tee "$test_tmp/default-repeat.log" +require_clean_recap "$test_tmp/default-repeat.log" + +baseline_binary=$(binary_digest) +baseline_state=$(state_digest) +for scenario in \ + '{"test_distribution":"Debian","test_distribution_version":"26.04","test_architecture":"x86_64"}' \ + '{"test_distribution":"Ubuntu","test_distribution_version":"24.04","test_architecture":"x86_64"}' \ + '{"test_distribution":"Ubuntu","test_distribution_version":"26.04","test_architecture":"riscv64"}' +do + if "$playbook" -i "$inventory" tests/unsupported.yml -e "$scenario"; then + echo "unsupported platform scenario unexpectedly succeeded: $scenario" >&2 + exit 1 + fi + test "$(binary_digest)" = "$baseline_binary" + test "$(state_digest)" = "$baseline_state" +done + +sudo touch /usr/local/src/seriatim/ansible-untracked-test +sudo sed -i '1s/^/ansible-test-change\n/' /usr/local/src/seriatim/go.mod +run_converge +"$playbook" -i "$inventory" tests/verify.yml + +compiler_wrapper="$test_tmp/go-version-wrapper" +sed 's|@GO@|/snap/bin/go|' >"$compiler_wrapper" <<'WRAPPER' +#!/bin/sh +if [ "${1:-}" = version ]; then + echo 'go version go1.26.99 linux/amd64' + exit 0 +fi +exec @GO@ "$@" +WRAPPER +chmod 0755 "$compiler_wrapper" + +run_converge -e "seriatim_go_binary=$compiler_wrapper" | + tee "$test_tmp/compiler-change.log" +require_changed_recap "$test_tmp/compiler-change.log" +run_converge -e "seriatim_go_binary=$compiler_wrapper" | + tee "$test_tmp/compiler-repeat.log" +require_clean_recap "$test_tmp/compiler-repeat.log" +run_converge | tee "$test_tmp/compiler-restore.log" +require_changed_recap "$test_tmp/compiler-restore.log" +run_converge | tee "$test_tmp/compiler-restored-repeat.log" +require_clean_recap "$test_tmp/compiler-restored-repeat.log" + +failure_wrapper="$test_tmp/go-failure-wrapper" +sed 's|@GO@|/snap/bin/go|' >"$failure_wrapper" <<'WRAPPER' +#!/bin/sh +if [ "${1:-}" = version ]; then + exec @GO@ version +fi +echo 'deliberate integration-test build failure' >&2 +exit 42 +WRAPPER +chmod 0755 "$failure_wrapper" + +baseline_binary=$(binary_digest) +baseline_state=$(state_digest) +if run_converge -e "seriatim_go_binary=$failure_wrapper"; then + echo 'deliberate build failure unexpectedly succeeded' >&2 + exit 1 +fi +test "$(binary_digest)" = "$baseline_binary" +test "$(state_digest)" = "$baseline_state" +test "$(/usr/local/bin/seriatim --version)" = 'seriatim version v1.5.0' + +run_converge | tee "$test_tmp/final-restore.log" +require_clean_recap "$test_tmp/final-restore.log" +"$playbook" -i "$inventory" tests/verify.yml + +trap - EXIT +rm -rf -- "$test_tmp" diff --git a/tests/unsupported.yml b/tests/unsupported.yml new file mode 100644 index 0000000..9f39b42 --- /dev/null +++ b/tests/unsupported.yml @@ -0,0 +1,12 @@ +--- +- name: Exercise Seriatim platform mutation guard + hosts: seriatim + gather_facts: false + become: true + vars: + ansible_facts: + distribution: "{{ test_distribution }}" + distribution_version: "{{ test_distribution_version }}" + architecture: "{{ test_architecture }}" + roles: + - role: ansible-role-seriatim diff --git a/tests/verify.yml b/tests/verify.yml new file mode 100644 index 0000000..b471c11 --- /dev/null +++ b/tests/verify.yml @@ -0,0 +1,97 @@ +--- +- name: Verify Seriatim installation + hosts: seriatim + gather_facts: true + become: true + vars: + seriatim_expected_commit: 0fc92f364316c874d366b00cdc884a33e1900b25 + seriatim_expected_version: v1.5.0 + seriatim_verify_source_dir: /usr/local/src/seriatim + seriatim_verify_binary_path: /usr/local/bin/seriatim + seriatim_verify_state_dir: /usr/local/src/seriatim.ansible-build-state + tasks: + - name: Inspect Seriatim source directory + ansible.builtin.stat: + path: "{{ seriatim_verify_source_dir }}" + register: seriatim_verify_source_stat + + - name: Inspect installed Seriatim binary + ansible.builtin.stat: + path: "{{ seriatim_verify_binary_path }}" + checksum_algorithm: sha256 + register: seriatim_verify_binary_stat + + - name: Inspect Seriatim build state directory + ansible.builtin.stat: + path: "{{ seriatim_verify_state_dir }}" + register: seriatim_verify_state_stat + + - name: Resolve installed Seriatim source commit + ansible.builtin.command: + argv: + - /usr/bin/git + - -C + - "{{ seriatim_verify_source_dir }}" + - rev-parse + - HEAD + register: seriatim_verify_commit + changed_when: false + + - name: Inspect installed Seriatim source cleanliness + ansible.builtin.command: + argv: + - /usr/bin/git + - -C + - "{{ seriatim_verify_source_dir }}" + - status + - --porcelain + - --untracked-files=all + - --ignored + register: seriatim_verify_source_status + changed_when: false + + - name: Read installed Seriatim version + ansible.builtin.command: + argv: + - "{{ seriatim_verify_binary_path }}" + - --version + register: seriatim_verify_version + changed_when: false + + - name: Read stored Seriatim build inputs + ansible.builtin.slurp: + src: "{{ seriatim_verify_state_dir }}/build-inputs.json" + register: seriatim_verify_inputs + + - name: Read stored Seriatim fingerprint + ansible.builtin.slurp: + src: "{{ seriatim_verify_state_dir }}/fingerprint.sha256" + register: seriatim_verify_fingerprint + + - name: Read stored Seriatim checksum + ansible.builtin.slurp: + src: "{{ seriatim_verify_state_dir }}/binary.sha256" + register: seriatim_verify_checksum + + - name: Require valid Seriatim installation state + ansible.builtin.assert: + that: + - ansible_facts.distribution == 'Ubuntu' + - ansible_facts.distribution_version is version('26.04', '==') + - ansible_facts.architecture in ['x86_64', 'aarch64'] + - seriatim_verify_source_stat.stat.isdir + - seriatim_verify_source_stat.stat.pw_name == 'root' + - seriatim_verify_source_stat.stat.gr_name == 'root' + - seriatim_verify_binary_stat.stat.isreg + - seriatim_verify_binary_stat.stat.pw_name == 'root' + - seriatim_verify_binary_stat.stat.gr_name == 'root' + - seriatim_verify_binary_stat.stat.mode == '0755' + - seriatim_verify_state_stat.stat.isdir + - seriatim_verify_state_stat.stat.pw_name == 'root' + - seriatim_verify_state_stat.stat.gr_name == 'root' + - seriatim_verify_commit.stdout == seriatim_expected_commit + - seriatim_verify_source_status.stdout == '' + - seriatim_verify_version.stdout == 'seriatim version ' ~ seriatim_expected_version + - (seriatim_verify_inputs.content | b64decode | trim | hash('sha256')) == (seriatim_verify_fingerprint.content | b64decode | trim) + - seriatim_verify_binary_stat.stat.checksum == (seriatim_verify_checksum.content | b64decode | trim) + fail_msg: Installed Seriatim source, binary, or build state is inconsistent.