Skip to content

feat: add isolated bats test suite for main.sh - #37

Open
hllvc wants to merge 8 commits into
mainfrom
test/main-sh-suite
Open

feat: add isolated bats test suite for main.sh#37
hllvc wants to merge 8 commits into
mainfrom
test/main-sh-suite

Conversation

@hllvc

@hllvc hllvc commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds an isolated, hermetic bats test suite (unit + smoke) for main.sh, the minimal seams in main.sh that make it testable, a lint setup that enforces modern Bash idioms, a Makefile, and a GitHub Actions workflow that runs on pull requests against main. The suite runs without root, network, Docker, or a real host, so it can gate every change.

Motivation & Context

main.sh is the private-runner registration entrypoint, yet it had no automated tests. Its most fragile surface — the proxy/runner-id injection guards, required-argument enforcement, and HTTP-status parsing (including the "a proxy inserts its own 200" case) — could regress silently. This introduces fast, deterministic tests that pin that behavior, mocking every host-affecting command so nothing touches the machine running them.

Changes Made

  • main.sh testability seams (behavior-preserving). Wrapped main "$@" in a [[ "${BASH_SOURCE[0]}" == "${0}" ]] guard so the script can be sourced without executing; made the hardcoded write paths (LOG_FILE, SG_DIAGNOSTIC_DIR, ECS_CONFIG_DIR, ECS_LOG_DIR, ECS_DATA_DIR, REGISTRATION_DIR) env-overridable with unchanged production defaults; added an SG_SKIP_ROOT_CHECK bypass in is_root() (off by default).
  • Modernized Bash idioms. Converted the remaining single-bracket tests to [[ ]] in main.sh and the mock stubs.
  • Vendored the test framework. bats-core, bats-support, bats-assert as git submodules under test/lib/; added a !test/lib/ negation to .gitignore (the existing lib/ rule was swallowing them).
  • Harness. test/helpers/load.bash (single entry point: redirects all paths to a per-test temp dir, prepends the mock PATH, sources main.sh on demand), test/helpers/mocks.bash assertions, and test/mocks/bin/* stubs for every external command (systemctl, docker, curl, iptables, crontab, ...).
  • Unit tier (test/unit/, 104 tests). Arg parsing/validation, api_call status/response parsing, patch_json, update_diagnostic, predicates, iptables_ensure idempotency, configure_local_data ecs.config rendering, and cgroupsv2.
  • Smoke tier (test/smoke/, 11 tests). Drives main.sh as a subprocess to assert the CLI contract (help output, command validation, required-argument enforcement) — the surface that exits before preflight(), so it needs no systemd, Docker, or root.
  • Lint / conventions. .shellcheckrc (enable=require-double-brackets, enable=deprecate-which, disable=SC2034) plus a make lint idiom gate — shellcheck --include=SC2292,SC2006 over every shell source — enforcing [[ ]] over [ ] and $(...) over backticks.
  • Tooling. Makefile (test, test-unit, test-smoke, lint) and .github/workflows/test.yml (lint + unit-smoke jobs, actions/checkout@v7 with submodules: recursive), triggered on pull requests against main.
  • Docs. Refreshed README.md (fixed the table-of-contents anchors, corrected the stale /tmp diagnostic/log paths to /var/lib/sg-runner and /var/log, fixed a cgroupsv2 typo, added a Development section) and added test/README.md documenting the harness.

Testing

  • make test — 115 tests, 0 failures, 1 intentional documenting skip.
  • make lintshellcheck correctness plus the modern-idiom gate, clean across main.sh, the mock stubs, and the helpers.

Risks & Edge Cases

  • main.sh is modified. The seams are behavior-preserving: overridable paths default to the original values, the source guard only affects source-ing, and the root check is unchanged unless SG_SKIP_ROOT_CHECK=true. No runtime behavior changes when the script is executed normally.
  • Submodules. bats is vendored as submodules, so any checkout (CI included) must use submodules: recursive; a plain clone yields no test runner.
  • .bats files are not shellchecked — their @test syntax isn't valid standalone Bash, so the idiom gate covers .sh/.bash sources only.
  • Platform boundary. On macOS preflight() requires /run/systemd/system, so subcommands that reach preflight aren't exercised end to end by this host-only suite (documented as a skip).

Deployment Notes

  • CI triggers only on pull_request targeting main; it pins actions/checkout@v7 and installs shellcheck on the runner. No AWS accounts, secrets, or prod wiring involved.
  • Anyone cloning the repo for tests must initialize submodules (git submodule update --init --recursive).

@hllvc hllvc self-assigned this Jul 20, 2026
@sonarqubecloud

Copy link
Copy Markdown

@hllvc
hllvc requested a review from arunim2405 August 3, 2026 14:05
@hllvc
hllvc marked this pull request as ready for review August 3, 2026 14:05
@arunim2405
arunim2405 requested a review from Copilot August 3, 2026 14:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a hermetic bats-core test harness (unit + smoke) for main.sh, plus small “seams” in main.sh to enable safe sourcing and path/root overrides, and adds lint + CI plumbing so changes to the runner entrypoint can be gated automatically.

Changes:

  • Add an isolated bats test harness with unit and smoke tiers, fixtures, and command mocks.
  • Make main.sh safely sourceable and allow key filesystem paths (and root check) to be overridden for tests.
  • Add make lint/make test* targets and a PR workflow to run lint + tests (with submodules).

Reviewed changes

Copilot reviewed 38 out of 55 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
main.sh Adds source guard, env-overridable filesystem paths, and root-check bypass for testability.
Makefile Adds test/lint targets to run bats tiers and shellcheck gates.
.github/workflows/test.yml Runs lint and unit/smoke tiers on PRs to main (with recursive submodules).
.shellcheckrc Centralizes shellcheck configuration and modern-idiom enforcement.
.gitmodules Vendors bats-core/support/assert as git submodules under test/lib/.
.gitignore Un-ignores test/lib/ so vendored bats submodules aren’t swallowed by lib/ ignore.
README.md Updates docs (anchors/paths/typos) and adds development/test instructions.
test/README.md Documents the test harness contract, mocks, tiers, and caveats.
test/helpers/load.bash Harness entrypoint: temp dirs, PATH mocking, and load_main sourcing.
test/helpers/mocks.bash Mock call-log assertion helpers (assert_called, refute_called, etc.).
test/mocks/bin/curl Stubbed curl used by tests and parsing fixtures.
test/mocks/bin/crontab Stubbed crontab with list/install behavior.
test/mocks/bin/df Stubbed df output shaped for existing parsing.
test/mocks/bin/docker Stubbed docker subcommands used by main.sh.
test/mocks/bin/free Stubbed free -h output for parsing.
test/mocks/bin/grubby Stubbed grubby for cgroupsv2 path.
test/mocks/bin/ip Stubbed ip route output for node IP parsing.
test/mocks/bin/iptables Stubbed iptables with controllable -C exit behavior.
test/mocks/bin/nproc Stubbed CPU count.
test/mocks/bin/reboot Stubbed reboot for safe cgroupsv2 testing.
test/mocks/bin/systemctl Stubbed systemctl queries and no-op mutating operations.
test/mocks/bin/sysctl Stubbed sysctl writes.
test/mocks/bin/top Stubbed top output for CPU parsing pipeline.
test/mocks/bin/uptime Stubbed uptime output including load averages.
test/unit/api_call.bats Unit tests for HTTP status/body parsing and proxy double-status case.
test/unit/cgroupsv2.bats Unit tests for cgroupsv2 enable/disable flows using mocks.
test/unit/check_args.bats Unit tests for argument/value guard helpers.
test/unit/configure_local_data.bats Unit tests for deterministic ecs.config rendering and proxy blocks.
test/unit/harness.bats Canary tests validating harness + mocks behavior.
test/unit/init_args_are_valid.bats Unit tests for subcommand/required-flag validation gate.
test/unit/iptables_ensure.bats Unit tests for idempotent iptables rule ensure helper.
test/unit/parse_arguments.bats Unit tests for option parsing side effects and failure paths.
test/unit/patch_json.bats Unit tests for JSON deep-merge patch helper.
test/unit/predicates.bats Unit tests for boolean predicate helpers.
test/unit/update_diagnostic.bats Unit tests for diagnostic file init/update behavior.
test/unit/validate_proxy_format.bats Unit tests for proxy format validation.
test/unit/validate_runner_id.bats Unit tests for runner ID injection guard.
test/smoke/command_validation.bats Smoke tests for CLI command validation as a subprocess.
test/smoke/help.bats Smoke tests for help/no-args short-circuit behavior.
test/smoke/required_args.bats Smoke tests for required-flag enforcement boundary (preflight not reached).
test/smoke/.gitkeep Keeps smoke directory present even if empty in some contexts.
test/fixtures/api/* HTTP response fixtures for api_call parsing tests.
test/fixtures/ecs/ecs.config.v4.golden Golden ecs.config for deterministic diff-based assertions.
test/fixtures/.gitkeep Keeps fixtures directory present.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread main.sh
Comment on lines 624 to +626
configure_local_data() { #{{{
mkdir -p /var/log/ecs /etc/ecs /var/lib/ecs/data /var/log/registration/
rm -rf /etc/ecs/ecs.config >/dev/null
mkdir -p "$ECS_LOG_DIR" "$ECS_CONFIG_DIR" "$ECS_DATA_DIR" "$REGISTRATION_DIR"
rm -rf "$ECS_CONFIG_DIR/ecs.config" >/dev/null
Comment on lines +98 to +99
refute [ -n "$(grep '^HTTP_PROXY=' "$f")" ]
refute [ -n "$(grep '^HTTPS_PROXY=' "$f")" ]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants