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
151 changes: 142 additions & 9 deletions .github/workflows/go-basic-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ on:
required: false
type: string
default: "./..."
TEST_PARALLELISM:
description: Maximum parallel Go tests per package; 0 keeps the Go default
required: false
type: number
default: 0
TEST_PACKAGE_PARALLELISM:
description: Maximum concurrent Go test/build packages; 0 keeps the Go default
required: false
type: number
default: 0
FAILEDTHRESHOLD:
required: false
type: number
Expand Down Expand Up @@ -88,13 +98,135 @@ jobs:
with:
go-version: '${{ inputs.GO_VERSION }}'

- name: Test race conditions
if: ${{ env.CGO_ENABLED == 1 }}
run: go test -v -race $(go list ${{ inputs.UNIT_TESTS_PATH }} | grep -v /e2e)
- &go_tests
name: Test Go packages
id: unit-test
shell: bash
env:
UNIT_TESTS_PATH: ${{ inputs.UNIT_TESTS_PATH }}
TEST_PARALLELISM: ${{ inputs.TEST_PARALLELISM || 0 }}
TEST_PACKAGE_PARALLELISM: ${{ inputs.TEST_PACKAGE_PARALLELISM || 0 }}
run: |
set -euo pipefail
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)
for setting in TEST_PARALLELISM TEST_PACKAGE_PARALLELISM; do
value=${!setting}
if [[ ! $value =~ ^(0|[1-9][0-9]*)$ ]]; then
printf '%s must be a non-negative integer\n' "$setting" | tee "$results_dir/setup.stderr" >&2
exit 1
fi
done
if [[ $TEST_PARALLELISM != 0 ]]; then
args+=(-parallel "$TEST_PARALLELISM")
fi
if [[ $TEST_PACKAGE_PARALLELISM != 0 ]]; then
args+=(-p "$TEST_PACKAGE_PARALLELISM")
fi
if [[ ${TEST_COVERAGE:-false} == true ]]; then
args+=(-covermode=count -coverprofile=coverage.out)
elif [[ $CGO_ENABLED == 1 ]]; then
args+=(-race)
fi

- name: Test without race conditions
if: ${{ env.CGO_ENABLED != 1 }}
run: go test -v $(go list ${{ inputs.UNIT_TESTS_PATH }} | grep -v /e2e)
read -r -a patterns <<< "${UNIT_TESTS_PATH//$'\n'/ }"
go list "${patterns[@]}" > "$results_dir/packages.txt" 2> "$results_dir/list.stderr"
mapfile -t packages < <(grep -v /e2e "$results_dir/packages.txt")
if (( ${#packages[@]} == 0 )); then
printf 'No unit-test packages selected\n' | tee "$results_dir/setup.stderr" >&2
exit 1
fi
# pipefail retains the test exit status even when tee succeeds.
go test "${args[@]}" "${packages[@]}" 2> "$results_dir/test.stderr" | tee "$results_dir/tests.jsonl"

- &go_test_summary
name: Summarize Go test results
if: ${{ always() && steps.unit-test.outputs.results_dir != '' }}
shell: bash
env:
TEST_RESULTS_DIR: ${{ steps.unit-test.outputs.results_dir }}
TEST_OUTCOME: ${{ steps.unit-test.outcome }}
run: |
python3 - <<'PYTHON'
import collections
import html
import json
import os
from pathlib import Path

results = Path(os.environ["TEST_RESULTS_DIR"])
failed = set()
output = collections.defaultdict(lambda: collections.deque(maxlen=40))
malformed = 0
with (results / "tests.log").open("w") as readable:
json_log = results / "tests.jsonl"
if json_log.exists():
with json_log.open(errors="replace") as events:
for line in events:
try:
event = json.loads(line)
except json.JSONDecodeError:
malformed += 1
readable.write(line)
continue
# Go interleaves BuildEvents (ImportPath)
# with TestEvents (Package) in the JSON stream.
package = event.get("Package") or event.get("ImportPath", "unknown package")
key = (package, event.get("Test", ""))
if event.get("Action") in ("fail", "build-fail"):
failed.add(key)
text = event.get("Output", "")
readable.write(text)
output[key].extend(text.splitlines())

status = os.environ["TEST_OUTCOME"]
parts = [f"## Go tests: {html.escape(status)}\n",
"Full JSON, readable output, and stderr are attached as a job artifact.\n"]
if failed:
parts.append("### Failed tests and packages\n")
for package, test in sorted(failed):
label = package + (" / " + test if test else " (package)")
parts.append(f"- {html.escape(label)}\n")
# Named failures already include package context. Show package-level
# output when a build, panic, or TestMain failure has no named test.
named_packages = {package for package, test in failed if test}
for key in sorted(failed):
package, test = key
if not test and package in named_packages:
continue
label = package + (" / " + test if test else " (package)")
excerpt = "\n".join(output[key])[-6000:]
parts.append(f"\n<details><summary>{html.escape(label)}</summary>\n\n"
f"<pre>{html.escape(excerpt)}</pre>\n</details>\n")
elif status != "success":
parts.append("No named test failure was recorded. Check setup/build stderr and the artifact.\n")
if malformed:
parts.append(f"\n{malformed} non-JSON or incomplete lines retained in tests.log.\n")
for name in ("setup.stderr", "list.stderr", "test.stderr"):
path = results / name
if path.exists() and path.stat().st_size:
with path.open(errors="replace") as stream:
tail = "".join(collections.deque(stream, maxlen=40))[-6000:]
parts.append(f"\n### {name}\n<pre>{html.escape(tail)}</pre>\n")
# Leave ample room below GitHub's 1 MiB per-step summary limit.
summary = "".join(parts)
if len(summary) > 60000:
summary = summary[:60000] + "\n\nSummary truncated; consult the complete artifact.\n"
with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as stream:
stream.write(summary)
PYTHON

- &go_test_artifact
name: Upload Go test results
if: ${{ always() && steps.unit-test.outputs.results_dir != '' }}
uses: actions/upload-artifact@v4
with:
name: ${{ steps.unit-test.outputs.artifact_name }}
path: ${{ steps.unit-test.outputs.results_dir }}
retention-days: 7
if-no-files-found: error

- name: Initialize CodeQL
continue-on-error: true
Expand All @@ -115,6 +247,7 @@ jobs:

Basic-Test:
env:
TEST_COVERAGE: true
GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }}
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
Expand Down Expand Up @@ -167,9 +300,9 @@ jobs:
args: --timeout 10m
only-new-issues: true

- name: Test coverage
id: unit-test
run: go test -v $(go list ${{ inputs.UNIT_TESTS_PATH }} | grep -v /e2e) -covermode=count -coverprofile=coverage.out
- *go_tests
- *go_test_summary
- *go_test_artifact

- name: Comment results to PR
uses: peter-evans/create-or-update-comment@v4
Expand Down
14 changes: 13 additions & 1 deletion .github/workflows/incluster-comp-pr-created.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ on:
required: false
type: string
default: "./..."
TEST_PARALLELISM:
description: Maximum parallel Go tests per package; 0 keeps the Go default
required: false
type: number
default: 0
TEST_PACKAGE_PARALLELISM:
description: Maximum concurrent Go test/build packages; 0 keeps the Go default
required: false
type: number
default: 0
# TEST_MULTI_ENVIRONMENTS:
# required: false
# type: boolean
Expand All @@ -51,5 +61,7 @@ jobs:
CGO_ENABLED: ${{ inputs.CGO_ENABLED }}
UNIT_TESTS_PATH: ${{ inputs.UNIT_TESTS_PATH }}
BUILD_PATH: ${{ inputs.BUILD_PATH }}
TEST_PARALLELISM: ${{ inputs.TEST_PARALLELISM || 0 }}
TEST_PACKAGE_PARALLELISM: ${{ inputs.TEST_PACKAGE_PARALLELISM || 0 }}
# TEST_MULTI_ENVIRONMENTS: ${{ inputs.TEST_MULTI_ENVIRONMENTS }}
secrets: inherit
secrets: inherit
12 changes: 12 additions & 0 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ on:
- '**.md' ### Ignore running when .md files change

jobs:
validate-go-test-reporting:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.25'
- name: Exercise test failure reporting
run: python3 -m unittest discover -s tests -v

validate-go-basic-tests-file:
permissions:
pull-requests: write
Expand Down
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,49 @@ Here's the list of reusable workflows available from this repository:
| **Name** | **Variables** | **Description** |
| ------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| `sanity-check.yaml` | - GH_RUNNER: define the runner to use on the workflow. Default: `ubuntu-latest`</br>- `SYSTEM_TESTS_BRANCH`: define alternative branch to clone for [system-tests](github.com/armosec/system-tests) repository. Default: `master`</br>- `BINARY_TESTS`: specify which tests are going to be executed by the workflow. | This workflow allow you to run **system-tests** in the **production** environment |

## Go test diagnostics and concurrency

`go-basic-tests.yaml` captures each unit-test and coverage run as a separate
`go-tests-<job>-<unique suffix>` artifact, retained for seven days. It contains:

- `tests.jsonl`: the complete `go test -json` event stream.
- `tests.log`: readable output reconstructed from those events.
- `test.stderr`, `list.stderr`, and `packages.txt`: compiler/tool diagnostics
and the selected package list. Setup errors also produce `setup.stderr`.

The job summary lists failed tests and packages with bounded output excerpts.
Summary and artifact steps run after test failure. A nonzero `go test` or
`go list` exit still fails the job; there are no automatic retries. Tests use
`-count=1` so every run executes them. Existing race and coverage modes are
preserved, as is the exclusion of `/e2e` packages.

Both `go-basic-tests.yaml` and `incluster-comp-pr-created.yaml` accept two
optional numeric inputs:

| Input | Meaning | Default |
| --- | --- | --- |
| `TEST_PARALLELISM` | Go `-parallel`: simultaneous parallel tests within each package | `0` (Go default) |
| `TEST_PACKAGE_PARALLELISM` | Go `-p`: concurrent package test/build processes | `0` (Go default) |

For example, a caller can reduce competition for CPU and disk:

```yaml
jobs:
pr-created:
uses: kubescape/workflows/.github/workflows/incluster-comp-pr-created.yaml@main
with:
GO_VERSION: "1.25"
CGO_ENABLED: 0
TEST_PARALLELISM: 4
TEST_PACKAGE_PARALLELISM: 2
secrets: inherit
```

Zero preserves Go's default; negative and fractional values fail validation.
These limits do not make wall-clock assertions deterministic. Tests of
concurrency correctness should synchronize on the events they need to observe;
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`.
Loading
Loading