diff --git a/.github/workflows/go-basic-tests.yaml b/.github/workflows/go-basic-tests.yaml index a159cf1..aaf8761 100644 --- a/.github/workflows/go-basic-tests.yaml +++ b/.github/workflows/go-basic-tests.yaml @@ -21,6 +21,11 @@ on: required: false type: string default: "./..." + TEST_TIMEOUT_MINUTES: + description: Per-package Go test timeout in minutes; must be a positive integer + required: false + type: number + default: 10 TEST_PARALLELISM: description: Maximum parallel Go tests per package; 0 keeps the Go default required: false @@ -103,6 +108,7 @@ jobs: shell: bash env: UNIT_TESTS_PATH: ${{ inputs.UNIT_TESTS_PATH }} + TEST_TIMEOUT_MINUTES: ${{ inputs.TEST_TIMEOUT_MINUTES }} TEST_PARALLELISM: ${{ inputs.TEST_PARALLELISM || 0 }} TEST_PACKAGE_PARALLELISM: ${{ inputs.TEST_PACKAGE_PARALLELISM || 0 }} run: | @@ -110,7 +116,11 @@ jobs: results_dir=$(mktemp -d "$RUNNER_TEMP/go-tests-${GITHUB_JOB}.XXXXXX") printf 'results_dir=%s\nartifact_name=%s\n' "$results_dir" "${results_dir##*/}" >> "$GITHUB_OUTPUT" - args=(-json -count=1) + if [[ ! $TEST_TIMEOUT_MINUTES =~ ^[1-9][0-9]*$ ]]; then + printf 'TEST_TIMEOUT_MINUTES must be a positive integer\n' | tee "$results_dir/setup.stderr" >&2 + exit 1 + fi + args=(-json -count=1 -timeout "${TEST_TIMEOUT_MINUTES}m") for setting in TEST_PARALLELISM TEST_PACKAGE_PARALLELISM; do value=${!setting} if [[ ! $value =~ ^(0|[1-9][0-9]*)$ ]]; then diff --git a/.github/workflows/incluster-comp-pr-created.yaml b/.github/workflows/incluster-comp-pr-created.yaml index 907d938..703f924 100644 --- a/.github/workflows/incluster-comp-pr-created.yaml +++ b/.github/workflows/incluster-comp-pr-created.yaml @@ -29,6 +29,11 @@ on: required: false type: string default: "./..." + TEST_TIMEOUT_MINUTES: + description: Per-package Go test timeout in minutes; must be a positive integer + required: false + type: number + default: 10 TEST_PARALLELISM: description: Maximum parallel Go tests per package; 0 keeps the Go default required: false @@ -61,6 +66,7 @@ jobs: CGO_ENABLED: ${{ inputs.CGO_ENABLED }} UNIT_TESTS_PATH: ${{ inputs.UNIT_TESTS_PATH }} BUILD_PATH: ${{ inputs.BUILD_PATH }} + TEST_TIMEOUT_MINUTES: ${{ inputs.TEST_TIMEOUT_MINUTES }} TEST_PARALLELISM: ${{ inputs.TEST_PARALLELISM || 0 }} TEST_PACKAGE_PARALLELISM: ${{ inputs.TEST_PACKAGE_PARALLELISM || 0 }} # TEST_MULTI_ENVIRONMENTS: ${{ inputs.TEST_MULTI_ENVIRONMENTS }} diff --git a/README.md b/README.md index 3bf44be..4354176 100644 --- a/README.md +++ b/README.md @@ -123,3 +123,9 @@ strict latency checks need a separate performance test with controlled load. The reporting regression harness uses Python's standard library and local Go fixtures: `python3 -m unittest discover -s tests -v`. + +`TEST_TIMEOUT_MINUTES` sets Go's per-package test timeout for both race and +coverage runs. It defaults to `10`, preserving Go's default budget; callers +with large migration or integration suites can set `30`. It must be a positive +integer. This limits test execution, not compilation or the whole workflow, +and does not change race detection or assertions inside tests. diff --git a/tests/test_go_ci.py b/tests/test_go_ci.py index 6bd2905..498e554 100644 --- a/tests/test_go_ci.py +++ b/tests/test_go_ci.py @@ -50,7 +50,7 @@ def setUp(self): GITHUB_STEP_SUMMARY=str(self.root / "summary"), UNIT_TESTS_PATH="./...", TEST_PARALLELISM="0", TEST_PACKAGE_PARALLELISM="0", - TEST_COVERAGE="false", GOPROXY="off", GOSUMDB="off", + TEST_TIMEOUT_MINUTES="10", TEST_COVERAGE="false", GOPROXY="off", GOSUMDB="off", GOTOOLCHAIN="local", GOFLAGS="", GOWORK="off") def shell(self, anchor): @@ -141,7 +141,7 @@ def test_flags_and_package_filtering(self): calls = [json.loads(line) for line in (self.root / "calls").read_text().splitlines()] self.assertEqual(calls[-2], ["list", "./pkg/...", "./internal/...", "./literal-$(touch-INJECTED)/*"]) - expected = ["test", "-json", "-count=1"] + expected = ["test", "-json", "-count=1", "-timeout", "10m"] if parallel != "0": expected += ["-parallel", parallel, "-p", package_parallel] if coverage == "true": @@ -150,6 +150,32 @@ def test_flags_and_package_filtering(self): expected += ["-race"] self.assertEqual(calls[-1], expected + ["example.com/ci"]) + def test_timeout_wrapper_preserves_input_for_validation(self): + wrapper = WORKFLOW.with_name("incluster-comp-pr-created.yaml").read_text() + self.assertIn("TEST_TIMEOUT_MINUTES: ${{ inputs.TEST_TIMEOUT_MINUTES }}", wrapper) + self.assertNotIn("inputs.TEST_TIMEOUT_MINUTES ||", wrapper) + + def test_custom_timeout_in_both_modes(self): + self.fake_go() + for coverage in ("false", "true"): + with self.subTest(coverage=coverage): + self.env.update(TEST_TIMEOUT_MINUTES="30", TEST_COVERAGE=coverage) + result, _ = self.execute() + self.assertEqual(result.returncode, 0, result.stderr) + calls = [json.loads(line) for line in (self.root / "calls").read_text().splitlines()] + self.assertEqual(calls[-1][3:5], ["-timeout", "30m"]) + + def test_invalid_timeout_fails_before_go(self): + self.fake_go() + for value in ("0", "-1", "1.5", "abc", "", "$(touch INJECTED)"): + with self.subTest(value=value): + self.env["TEST_TIMEOUT_MINUTES"] = value + result, summary = self.execute() + self.assertNotEqual(result.returncode, 0) + self.assertIn("TEST_TIMEOUT_MINUTES must be a positive integer", summary) + self.assertFalse((self.root / "calls").exists()) + self.assertFalse((self.module / "INJECTED").exists()) + def test_invalid_parallelism_fails_before_go(self): self.fake_go() for setting in ("TEST_PARALLELISM", "TEST_PACKAGE_PARALLELISM"):