Compare commits

...
30 Commits
Author SHA1 Message Date
Noah Campbell 2a2b0e0f30 reverted regression tests to how they were before
This was turning into something worthy of its own PR so I put it back to how it was before to work on next/separately
2025-10-14 11:30:05 -05:00
Noah Campbell 1013f2a160 does not recalculate affected e2e tests 2025-10-14 10:24:27 -05:00
Noah Campbell e73e1375cc uses unique namespaces for different preflight regression tests 2025-10-14 10:14:33 -05:00
Noah Campbell 9278d50252 use kubeconfig to properly set up preflight regression tests 2025-10-14 10:00:35 -05:00
Noah Campbell e084c0a46a regression tests run in matrix 2025-10-14 09:53:58 -05:00
Noah Campbell f7a941c220 regression tests run separate from relevant test detection 2025-10-14 08:11:07 -05:00
Noah Campbell 8920a9d09a Update Makefile 2025-10-13 14:35:25 -05:00
Noah Campbell a677646b17 reduced redundant building 2025-10-13 14:31:14 -05:00
Noah Campbell c84ea20b88 e2e test make target build the binaries first 2025-10-13 14:19:27 -05:00
Noah Campbell 409a02a125 Update affected-tests.yml 2025-10-13 14:02:20 -05:00
Noah Campbell 78af9208cf runs e2e preflight and support bundle tests as matrix 2025-10-13 13:53:09 -05:00
Noah Campbell fb0983480c Update affected-tests.yml 2025-10-13 13:40:06 -05:00
Noah Campbell 1aa74db81d Update affected-tests.yml 2025-10-13 13:24:35 -05:00
Noah Campbell 8f7cf26cc9 Update affected-tests.yml 2025-10-13 13:13:19 -05:00
Noah Campbell a6d3e3e423 Update affected-tests.yml 2025-10-13 13:02:45 -05:00
Noah Campbell 6ea8cbcb09 creates clusters for tests via replicated actions 2025-10-13 13:00:59 -05:00
Noah Campbell bf1dfa0318 removed references to nonexistent tests 2025-10-13 12:45:59 -05:00
Noah Campbell 45ee61aa4d creates cluster for tests and removed non testable files from being flagged 2025-10-13 12:01:58 -05:00
Noah Campbell e3458ad297 can run unit tests via makefile 2025-10-13 11:29:58 -05:00
Noah Campbell cb32216df8 runs all tests when test change is detected 2025-10-13 10:54:59 -05:00
Noah Campbell 2b82454ef8 Update affected-tests.yml 2025-10-13 10:18:43 -05:00
Noah Campbell e05962bcf3 affected test workflow runs on PR 2025-10-13 10:11:46 -05:00
Noah Campbell 0f747f55dc removed main requirement on affected tests 2025-10-13 10:07:26 -05:00
Noah Campbell 1c234e432c shows affected test summary 2025-10-13 10:00:49 -05:00
Noah Campbell d325804359 readded build binary action for tests 2025-10-13 09:52:47 -05:00
Noah Campbell 2d664b8736 separated build and test actions for relevant tests 2025-10-13 09:44:39 -05:00
Noah Campbell 8762bd1515 changed tests always are flagged as affected tests 2025-10-11 11:31:43 -07:00
Noah Campbell a2a424b201 added test and makes new relevant tests run tests 2025-10-11 11:17:26 -07:00
Noah Campbell 3f069125a2 added flag to specify files changed for testing 2025-10-11 10:49:54 -07:00
Noah Campbell 37292e6ba1 finds and runs relevant code change tests 2025-10-10 07:50:07 -07:00
9 changed files with 1134 additions and 71 deletions
+192
View File
@@ -0,0 +1,192 @@
name: Affected Go Tests
on:
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
permissions:
contents: read
jobs:
test-affected:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 30
outputs:
unit_has_changes: ${{ steps.affected.outputs.has_changes }}
e2e_has_changes: ${{ steps.affected_e2e.outputs.has_changes }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Go Mod Download
run: go mod download
- name: Compute base ref
id: pr-info
run: |
echo "BASE_REF=origin/${{ github.base_ref }}" >> "$GITHUB_OUTPUT"
echo "Base ref: origin/${{ github.base_ref }}"
# 2) Detect relevant unit packages and e2e tests
- name: Compute affected packages
id: affected
run: |
set -euo pipefail
echo "Base: ${{ steps.pr-info.outputs.BASE_REF }}"
# Generate affected package list to a file for reuse in subsequent steps
go run ./scripts/affected-packages.go -base "${{ steps.pr-info.outputs.BASE_REF }}" > /tmp/affected.txt
echo "Affected packages:" || true
if [ -s /tmp/affected.txt ]; then
cat /tmp/affected.txt
else
echo "(none)"
fi
# Expose whether we have any packages to test
if [ -s /tmp/affected.txt ]; then
echo "has_changes=true" >> "$GITHUB_OUTPUT"
else
echo "has_changes=false" >> "$GITHUB_OUTPUT"
fi
- name: Compute affected e2e tests
id: affected_e2e
run: |
set -euo pipefail
go run ./scripts/affected-packages.go -mode=suites -base "${{ steps.pr-info.outputs.BASE_REF }}" > /tmp/affected-e2e.txt
awk -F: '$1=="preflight"{print $2}' /tmp/affected-e2e.txt > /tmp/preflight-tests.txt
awk -F: '$1=="support-bundle"{print $2}' /tmp/affected-e2e.txt > /tmp/support-tests.txt
if [ -s /tmp/preflight-tests.txt ] || [ -s /tmp/support-tests.txt ]; then
echo "has_changes=true" >> "$GITHUB_OUTPUT"
else
echo "has_changes=false" >> "$GITHUB_OUTPUT"
fi
- name: Publish affected summary
if: always()
run: |
{
echo "### Affected unit packages";
if [ -s /tmp/affected.txt ]; then
sed 's/^/- /' /tmp/affected.txt;
else
echo "- (none)";
fi;
echo;
echo "### Affected e2e tests";
if [ -s /tmp/affected-e2e.txt ]; then
sed 's/^/- /' /tmp/affected-e2e.txt;
else
echo "- (none)";
fi;
} | tee -a "$GITHUB_STEP_SUMMARY"
- name: Upload affected unit packages
uses: actions/upload-artifact@v4
with:
name: affected-unit
path: /tmp/affected.txt
if-no-files-found: warn
- name: Upload affected e2e artifacts
uses: actions/upload-artifact@v4
with:
name: affected-e2e
path: |
/tmp/affected-e2e.txt
/tmp/preflight-tests.txt
/tmp/support-tests.txt
if-no-files-found: warn
- name: No affected packages — skip tests
if: steps.affected.outputs.has_changes != 'true'
run: echo "No Go packages affected by this PR; skipping tests."
e2e-affected:
needs: test-affected
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
suite: [unit, preflight, support-bundle]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Go Mod Download
run: go mod download
- name: Download affected unit packages
uses: actions/download-artifact@v4
with:
name: affected-unit
path: /tmp
- name: Download affected e2e artifacts
uses: actions/download-artifact@v4
with:
name: affected-e2e
path: /tmp
- name: Run unit tests (filtered)
if: matrix.suite == 'unit' && needs.test-affected.outputs.unit_has_changes == 'true'
run: |
set -euo pipefail
if grep -qx "./..." /tmp/affected.txt; then
echo "Module files changed; running all unit tests"
make test
else
echo "Running unit tests for affected packages"
pkgs=$(tr '\n' ' ' < /tmp/affected.txt)
PACKAGES="$pkgs" make test-packages
fi
- name: Run e2e (filtered) - ${{ matrix.suite }}
if: matrix.suite != 'unit' && needs.test-affected.outputs.e2e_has_changes == 'true'
run: |
set -euo pipefail
docker rm -f kind-cluster-control-plane 2>/dev/null || true
if [ "${{ matrix.suite }}" = "preflight" ]; then
file=/tmp/preflight-tests.txt
path=./test/e2e/preflight
else
file=/tmp/support-tests.txt
path=./test/e2e/support-bundle
fi
if [ -s "$file" ]; then
regex="$(grep -v '^$' "$file" | tr '\n' '|' | sed 's/|$//')"
if [ -n "$regex" ]; then
if [ "${{ matrix.suite }}" = "preflight" ]; then
E2EPATHS="$path" RUN="^(${regex})$" make preflight-e2e-go-test
else
E2EPATHS="$path" RUN="^(${regex})$" make support-bundle-e2e-go-test
fi
else
echo "No valid ${{ matrix.suite }} tests matched after filtering"
fi
else
echo "No ${{ matrix.suite }} e2e changes"
fi
+9 -4
View File
@@ -37,6 +37,7 @@ jobs:
- run: make tidy-diff
test-integration:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
@@ -65,6 +66,7 @@ jobs:
path: bin/preflight
validate-preflight-e2e:
if: github.event_name == 'push'
runs-on: ubuntu-latest
needs: compile-preflight
steps:
@@ -95,6 +97,7 @@ jobs:
path: bin/support-bundle
validate-supportbundle-e2e:
if: github.event_name == 'push'
runs-on: ubuntu-latest
needs: compile-supportbundle
steps:
@@ -113,9 +116,10 @@ jobs:
# Additional e2e tests for support bundle that run in Go, these create a Kind cluster
validate-supportbundle-e2e-go:
runs-on: ubuntu-latest
needs: compile-supportbundle
steps:
if: github.event_name == 'push'
runs-on: ubuntu-latest
needs: compile-supportbundle
steps:
- uses: actions/checkout@v5
- name: Download support bundle binary
uses: actions/download-artifact@v5
@@ -133,6 +137,7 @@ jobs:
# summary jobs, these jobs will only run if all the other jobs have succeeded
validate-pr-tests:
if: github.event_name == 'push'
runs-on: ubuntu-latest
needs:
- tidy-check
@@ -147,10 +152,10 @@ jobs:
# this job will validate that the validation did not fail and that all pr-tests succeed
# it is used for the github branch protection rule
validate-success:
if: ${{ always() && github.event_name == 'push' }}
runs-on: ubuntu-latest
needs:
- validate-pr-tests
if: always()
steps:
# https://docs.github.com/en/actions/learn-github-actions/contexts#needs-context
# if the validate-pr-tests job was not successful, this job will fail
@@ -1,4 +1,4 @@
name: build-test
name: build
on:
pull_request:
@@ -47,6 +47,19 @@ jobs:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup-go
- name: Cache Go build and modules
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Go mod download
run: go mod download
- name: Check go mod tidy
run: |
go mod tidy
@@ -65,22 +78,7 @@ jobs:
make vet
# Unit and integration tests
test:
if: needs.changes.outputs.go-files == 'true'
needs: [changes, lint]
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup-go
- name: Setup K3s
uses: replicatedhq/action-k3s@main
with:
version: v1.31.2-k3s1
- name: Run tests
run: make test-integration
# (moved to push-full-tests.yml)
# Build binaries
build:
@@ -91,6 +89,19 @@ jobs:
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup-go
- name: Cache Go build and modules
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Go mod download
run: go mod download
- run: make build
- uses: actions/upload-artifact@v4
with:
@@ -98,64 +109,26 @@ jobs:
path: bin/
retention-days: 1
# E2E tests
e2e:
if: needs.changes.outputs.go-files == 'true' || github.event_name == 'push'
needs: [changes, build]
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- name: preflight
target: preflight-e2e-test
needs-k3s: true
- name: support-bundle-shell
target: support-bundle-e2e-test
needs-k3s: true
- name: support-bundle-go
target: support-bundle-e2e-go-test
needs-k3s: false
steps:
- uses: actions/checkout@v5
- name: Setup K3s
if: matrix.needs-k3s
uses: replicatedhq/action-k3s@main
with:
version: v1.31.2-k3s1
- uses: actions/download-artifact@v4
with:
name: binaries
path: bin/
- run: chmod +x bin/*
- run: make ${{ matrix.target }}
# (moved to push-full-tests.yml)
# Success summary
success:
if: always()
needs: [lint, test, build, e2e]
needs: [lint, build]
runs-on: ubuntu-latest
steps:
- name: Check results
run: |
# Check if any required jobs failed
if [[ "${{ needs.lint.result }}" == "failure" ]] || \
[[ "${{ needs.test.result }}" == "failure" ]] || \
[[ "${{ needs.build.result }}" == "failure" ]] || \
[[ "${{ needs.e2e.result }}" == "failure" ]]; then
[[ "${{ needs.build.result }}" == "failure" ]]; then
echo "::error::Some jobs failed or were cancelled"
exit 1
fi
# Check if any required jobs were cancelled
if [[ "${{ needs.lint.result }}" == "cancelled" ]] || \
[[ "${{ needs.test.result }}" == "cancelled" ]] || \
[[ "${{ needs.build.result }}" == "cancelled" ]] || \
[[ "${{ needs.e2e.result }}" == "cancelled" ]]; then
[[ "${{ needs.build.result }}" == "cancelled" ]]; then
echo "::error::Some jobs failed or were cancelled"
exit 1
fi
+77
View File
@@ -0,0 +1,77 @@
name: push-full-tests
on:
push:
branches: [main]
jobs:
unit-integration:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup-go
- name: Setup K3s
uses: replicatedhq/action-k3s@main
with:
version: v1.31.2-k3s1
- name: Run tests
run: make test-integration
build-binaries:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup-go
- name: Cache Go build and modules
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Go mod download
run: go mod download
- name: Build binaries
run: make build
- uses: actions/upload-artifact@v4
with:
name: binaries
path: bin/
retention-days: 1
e2e:
needs: [unit-integration, build-binaries]
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- name: preflight
target: preflight-e2e-test
needs-k3s: true
- name: support-bundle-shell
target: support-bundle-e2e-test
needs-k3s: true
- name: support-bundle-go
target: support-bundle-e2e-go-test
needs-k3s: false
steps:
- uses: actions/checkout@v5
- uses: actions/download-artifact@v4
with:
name: binaries
path: bin/
- run: chmod +x bin/*
- name: Setup K3s
if: matrix.needs-k3s
uses: replicatedhq/action-k3s@main
with:
version: v1.31.2-k3s1
- run: make ${{ matrix.target }}
+2 -2
View File
@@ -2,7 +2,7 @@ name: Regression Test Suite
on:
push:
branches: [main, v1beta3]
branches: [main]
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:
@@ -289,4 +289,4 @@ jobs:
continue-on-error: true
with:
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
cluster-id: ${{ steps.create-cluster.outputs.cluster-id }}
cluster-id: ${{ steps.create-cluster.outputs.cluster-id }}
+25 -6
View File
@@ -37,7 +37,7 @@ endef
BUILDTAGS = "netgo containers_image_ostree_stub exclude_graphdriver_devicemapper exclude_graphdriver_btrfs containers_image_openpgp"
BUILDFLAGS = -tags ${BUILDTAGS} -installsuffix netgo
BUILDPATHS = ./pkg/... ./cmd/... ./internal/...
E2EPATHS = ./test/e2e/...
E2EPATHS ?= ./test/e2e/...
TESTFLAGS ?= -v -coverprofile cover.out
.DEFAULT_GOAL := all
@@ -49,12 +49,23 @@ ffi: fmt vet
.PHONY: test
test: generate fmt vet
if [ -n $(RUN) ]; then \
go test ${BUILDFLAGS} ${BUILDPATHS} ${TESTFLAGS} -run $(RUN); \
if [ -n "$(RUN)" ]; then \
go test ${BUILDFLAGS} ${BUILDPATHS} ${TESTFLAGS} -run "$(RUN)"; \
else \
go test ${BUILDFLAGS} ${BUILDPATHS} ${TESTFLAGS}; \
fi
# Run unit tests only for a provided list of packages.
# Usage: make test-packages PACKAGES="pkg/a pkg/b cmd/foo"
.PHONY: test-packages
test-packages:
@if [ -z "$(PACKAGES)" ]; then \
echo "No PACKAGES provided; nothing to test."; \
exit 0; \
fi
@echo "Running unit tests for packages: $(PACKAGES)"
go test ${BUILDFLAGS} $(PACKAGES) ${TESTFLAGS}
# Go tests that require a K8s instance
# TODOLATER: merge with test, so we get unified coverage reports? it'll add 21~sec to the test job though...
.PHONY: test-integration
@@ -73,10 +84,18 @@ run-examples:
support-bundle-e2e-test:
./test/validate-support-bundle-e2e.sh
.PHONY: preflight-e2e-go-test
preflight-e2e-go-test: bin/preflight
if [ -n "$(RUN)" ]; then \
go test ${BUILDFLAGS} ${E2EPATHS} -v -run "$(RUN)"; \
else \
go test ${BUILDFLAGS} ${E2EPATHS} -v; \
fi
.PHONY: support-bundle-e2e-go-test
support-bundle-e2e-go-test:
if [ -n $(RUN) ]; then \
go test ${BUILDFLAGS} ${E2EPATHS} -v -run $(RUN); \
support-bundle-e2e-go-test: bin/support-bundle
if [ -n "$(RUN)" ]; then \
go test ${BUILDFLAGS} ${E2EPATHS} -v -run "$(RUN)"; \
else \
go test ${BUILDFLAGS} ${E2EPATHS} -v; \
fi
+549
View File
@@ -0,0 +1,549 @@
package main
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
)
// goListPackageJSON models the subset of fields we need from `go list -json` output.
// The JSON can be quite large; we intentionally only decode what we use to keep memory reasonable.
type goListPackageJSON struct {
ImportPath string `json:"ImportPath"`
Deps []string `json:"Deps"`
}
// runCommand executes a command and returns stdout as bytes with trimmed trailing newline.
func runCommand(name string, args ...string) ([]byte, error) {
cmd := exec.Command(name, args...)
cmd.Stderr = os.Stderr
out, err := cmd.Output()
if err != nil {
return nil, err
}
return bytes.TrimRight(out, "\n"), nil
}
// listPackageWithDeps returns the full transitive dependency set for a package import path,
// including the package itself.
func listPackageWithDeps(importPath string) (map[string]struct{}, error) {
cmd := exec.Command("go", "list", "-json", "-deps", importPath)
cmd.Stderr = os.Stderr
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("go list -json -deps %s failed: %w", importPath, err)
}
deps := make(map[string]struct{})
dec := json.NewDecoder(bytes.NewReader(out))
for {
var pkg goListPackageJSON
if err := dec.Decode(&pkg); err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("decode go list json: %w", err)
}
if pkg.ImportPath != "" {
deps[pkg.ImportPath] = struct{}{}
}
for _, d := range pkg.Deps {
deps[d] = struct{}{}
}
}
return deps, nil
}
// changedFiles returns a slice of file paths changed between baseRef and HEAD.
func changedFiles(baseRef string) ([]string, error) {
// Use triple-dot to include merge base with baseRef, typical for PR diffs.
out, err := runCommand("git", "diff", "--name-only", baseRef+"...HEAD")
if err != nil {
return nil, fmt.Errorf("git diff failed: %w", err)
}
var files []string
scanner := bufio.NewScanner(bytes.NewReader(out))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
files = append(files, line)
}
if err := scanner.Err(); err != nil {
return nil, err
}
return files, nil
}
// mapFilesToPackages resolves a set of Go package import paths that directly contain the changed Go files.
func mapFilesToPackages(files []string) (map[string]struct{}, error) {
packages := make(map[string]struct{})
// Collect unique directories that contain changed Go files.
dirSet := make(map[string]struct{})
for _, f := range files {
if strings.HasPrefix(f, "vendor/") {
continue
}
if filepath.Ext(f) != ".go" {
continue
}
d := filepath.Dir(f)
if d == "." {
d = "."
}
dirSet[d] = struct{}{}
}
if len(dirSet) == 0 {
return packages, nil
}
// Convert to a stable-ordered slice of directories to avoid nondeterminism.
var dirs []string
for d := range dirSet {
// Ensure relative paths are treated as packages; prepend ./ for clarity.
if strings.HasPrefix(d, "./") || d == "." {
dirs = append(dirs, d)
} else {
dirs = append(dirs, "./"+d)
}
}
sort.Strings(dirs)
// `go list` accepts directories and returns their package import paths.
args := append([]string{"list", "-f", "{{.ImportPath}}"}, dirs...)
out, err := runCommand("go", args...)
if err != nil {
return nil, fmt.Errorf("go list for files failed: %w", err)
}
scanner := bufio.NewScanner(bytes.NewReader(out))
for scanner.Scan() {
pkg := strings.TrimSpace(scanner.Text())
if pkg != "" {
packages[pkg] = struct{}{}
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return packages, nil
}
// computeAffectedPackages expands directly changed packages to include reverse dependencies across the module.
// We query all packages with test dependencies (-test) to ensure test-only imports are considered.
func computeAffectedPackages(directPkgs map[string]struct{}) (map[string]struct{}, error) {
affected := make(map[string]struct{})
for p := range directPkgs {
affected[p] = struct{}{}
}
if len(directPkgs) == 0 {
return affected, nil
}
// Enumerate all packages in the module with their deps.
// We stream decode concatenated JSON objects produced by `go list -json`.
cmd := exec.Command("go", "list", "-json", "-deps", "-test", "./...")
cmd.Stderr = os.Stderr
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("go list -json failed: %w", err)
}
dec := json.NewDecoder(bytes.NewReader(out))
for {
var pkg goListPackageJSON
if err := dec.Decode(&pkg); err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("decode go list json: %w", err)
}
// If this package is directly changed, it's already included.
// If it depends (directly or transitively) on any changed package, include it.
for changed := range directPkgs {
if pkg.ImportPath == changed {
affected[pkg.ImportPath] = struct{}{}
break
}
// Linear scan over deps is acceptable given typical package counts.
for _, dep := range pkg.Deps {
if dep == changed {
affected[pkg.ImportPath] = struct{}{}
break
}
}
}
}
return affected, nil
}
// listTestFunctions scans a directory for Go test files and returns names of functions
// that match the pattern `func TestXxx(t *testing.T)`.
func listTestFunctions(dir string) ([]string, error) {
var tests []string
// Regex to capture test function names. This is a simple heuristic suitable for our codebase.
testFuncRe := regexp.MustCompile(`^func\s+(Test[\w\d_]+)\s*\(`)
walkFn := func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if !strings.HasSuffix(d.Name(), "_test.go") {
return nil
}
b, err := os.ReadFile(path)
if err != nil {
return err
}
scanner := bufio.NewScanner(bytes.NewReader(b))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if m := testFuncRe.FindStringSubmatch(line); m != nil {
tests = append(tests, m[1])
}
}
return scanner.Err()
}
if err := filepath.WalkDir(dir, walkFn); err != nil {
return nil, err
}
sort.Strings(tests)
return tests, nil
}
func main() {
baseRef := flag.String("base", "origin/main", "Git base ref to diff against (e.g., origin/main)")
printAllOnChanges := flag.Bool("all-on-mod-change", true, "Run all tests if go.mod or go.sum changed")
verbose := flag.Bool("v", false, "Enable verbose diagnostics to stderr")
mode := flag.String("mode", "packages", "Output mode: 'packages' to print import paths; 'suites' to print e2e suite names")
changedFilesCSV := flag.String("changed-files", "", "Comma-separated paths to treat as changed (bypass git)")
changedFilesFile := flag.String("changed-files-file", "", "File with newline-separated paths to treat as changed")
flag.Parse()
// Determine the set of changed files: explicit list if provided, otherwise via git diff.
var files []string
if *changedFilesCSV != "" || *changedFilesFile != "" {
if *changedFilesCSV != "" {
parts := strings.Split(*changedFilesCSV, ",")
for _, p := range parts {
if s := strings.TrimSpace(p); s != "" {
files = append(files, s)
}
}
}
if *changedFilesFile != "" {
b, err := os.ReadFile(*changedFilesFile)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
scanner := bufio.NewScanner(bytes.NewReader(b))
for scanner.Scan() {
if s := strings.TrimSpace(scanner.Text()); s != "" {
files = append(files, s)
}
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
}
} else {
var err error
files, err = changedFiles(*baseRef)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
}
if *verbose {
fmt.Fprintln(os.Stderr, "Changed files vs base:")
if len(files) == 0 {
fmt.Fprintln(os.Stderr, " (none)")
} else {
for _, f := range files {
fmt.Fprintln(os.Stderr, " ", f)
}
}
}
// Track module change and CI configuration changes to drive conservative behavior.
moduleChanged := false
ciChanged := false
if *printAllOnChanges {
for _, f := range files {
if f == "go.mod" || f == "go.sum" {
moduleChanged = true
}
if strings.HasPrefix(f, "scripts/") || strings.HasPrefix(f, ".github/workflows/") {
ciChanged = true
}
}
if (moduleChanged || ciChanged) && *mode == "packages" {
if *verbose {
if moduleChanged {
fmt.Fprintln(os.Stderr, "Detected module file change (go.mod/go.sum); selecting all packages ./...")
}
if ciChanged {
fmt.Fprintln(os.Stderr, "Detected CI/detector change (scripts/ or .github/workflows/); selecting all packages ./...")
}
}
fmt.Println("./...")
return
}
}
directPkgs, err := mapFilesToPackages(files)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
if *verbose {
// Stable dump of direct packages
var dirs []string
for p := range directPkgs {
dirs = append(dirs, p)
}
sort.Strings(dirs)
fmt.Fprintln(os.Stderr, "Directly changed packages:")
if len(dirs) == 0 {
fmt.Fprintln(os.Stderr, " (none)")
} else {
for _, p := range dirs {
fmt.Fprintln(os.Stderr, " ", p)
}
}
}
switch *mode {
case "packages":
affected, err := computeAffectedPackages(directPkgs)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
if *verbose {
var dbg []string
for p := range affected {
dbg = append(dbg, p)
}
sort.Strings(dbg)
fmt.Fprintln(os.Stderr, "Final affected packages:")
if len(dbg) == 0 {
fmt.Fprintln(os.Stderr, " (none)")
} else {
for _, p := range dbg {
fmt.Fprintln(os.Stderr, " ", p)
}
}
}
// Normalize and filter import paths:
// - Strip test variant suffixes like "pkg [pkg.test]"
// - Exclude e2e test packages (./test/e2e/...)
normalized := make(map[string]struct{})
for p := range affected {
// Trim Go test variant decorations that appear in `go list -test`
if idx := strings.Index(p, " ["); idx != -1 {
p = p[:idx]
}
// Exclude synthetic test packages like github.com/org/repo/pkg.name.test
if strings.HasSuffix(p, ".test") {
continue
}
if strings.Contains(p, "/test/e2e/") {
continue
}
if p != "" {
normalized[p] = struct{}{}
}
}
var list []string
for p := range normalized {
list = append(list, p)
}
sort.Strings(list)
for _, p := range list {
fmt.Println(p)
}
case "suites":
// Determine impacted suites by dependency mapping and direct e2e test changes,
// then print exact test names for those suites.
preflightRoot := "github.com/replicatedhq/troubleshoot/cmd/preflight"
supportRoot := "github.com/replicatedhq/troubleshoot/cmd/troubleshoot"
preflightDeps, err := listPackageWithDeps(preflightRoot)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
supportDeps, err := listPackageWithDeps(supportRoot)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
preflightHit := false
supportHit := false
// Track whether e2e test files were directly changed per suite and collect specific test names
changedPreflightTests := make(map[string]struct{})
changedSupportTests := make(map[string]struct{})
preflightE2EChangedNonGo := false
supportE2EChangedNonGo := false
for _, f := range files {
if strings.HasPrefix(f, "test/e2e/preflight/") {
if strings.HasSuffix(f, "_test.go") {
// Extract test names from just this file
b, err := os.ReadFile(f)
if err == nil { // ignore read errors; they will be caught later if needed
scanner := bufio.NewScanner(bytes.NewReader(b))
re := regexp.MustCompile(`^func\s+(Test[\w\d_]+)\s*\(`)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if m := re.FindStringSubmatch(line); m != nil {
changedPreflightTests[m[1]] = struct{}{}
}
}
}
preflightHit = true
} else {
// Non-go change under preflight e2e; run whole suite
preflightE2EChangedNonGo = true
preflightHit = true
}
}
if strings.HasPrefix(f, "test/e2e/support-bundle/") {
if strings.HasSuffix(f, "_test.go") {
b, err := os.ReadFile(f)
if err == nil {
scanner := bufio.NewScanner(bytes.NewReader(b))
re := regexp.MustCompile(`^func\s+(Test[\w\d_]+)\s*\(`)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if m := re.FindStringSubmatch(line); m != nil {
changedSupportTests[m[1]] = struct{}{}
}
}
}
supportHit = true
} else {
supportE2EChangedNonGo = true
supportHit = true
}
}
}
for changed := range directPkgs {
if !preflightHit {
if _, ok := preflightDeps[changed]; ok {
preflightHit = true
}
}
if !supportHit {
if _, ok := supportDeps[changed]; ok {
supportHit = true
}
}
if preflightHit && supportHit {
break
}
}
if *verbose {
fmt.Fprintln(os.Stderr, "E2E suite impact:")
fmt.Fprintf(os.Stderr, " preflight: %v\n", preflightHit)
fmt.Fprintf(os.Stderr, " support-bundle: %v\n", supportHit)
}
// If module files or CI/detector changed, conservatively select all tests for both suites.
if moduleChanged || ciChanged {
preTests, err := listTestFunctions("test/e2e/preflight")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
for _, tname := range preTests {
fmt.Printf("preflight:%s\n", tname)
}
sbTests, err := listTestFunctions("test/e2e/support-bundle")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
for _, tname := range sbTests {
fmt.Printf("support-bundle:%s\n", tname)
}
return
}
// Collect tests for impacted suites and print as `<suite>:<TestName>`
if preflightHit || supportHit {
if preflightHit {
toPrint := make(map[string]struct{})
if preflightE2EChangedNonGo || len(changedPreflightTests) == 0 {
// Run full suite if e2e non-go assets changed or no specific test names collected
preTests, err := listTestFunctions("test/e2e/preflight")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
for _, t := range preTests {
toPrint[t] = struct{}{}
}
} else {
for t := range changedPreflightTests {
toPrint[t] = struct{}{}
}
}
var list []string
for t := range toPrint {
list = append(list, t)
}
sort.Strings(list)
for _, tname := range list {
fmt.Printf("preflight:%s\n", tname)
}
}
if supportHit {
toPrint := make(map[string]struct{})
if supportE2EChangedNonGo || len(changedSupportTests) == 0 {
sbTests, err := listTestFunctions("test/e2e/support-bundle")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
for _, t := range sbTests {
toPrint[t] = struct{}{}
}
} else {
for t := range changedSupportTests {
toPrint[t] = struct{}{}
}
}
var list []string
for t := range toPrint {
list = append(list, t)
}
sort.Strings(list)
for _, tname := range list {
fmt.Printf("support-bundle:%s\n", tname)
}
}
}
default:
fmt.Fprintln(os.Stderr, "unknown mode; use 'packages' or 'suites'")
os.Exit(2)
}
}
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
set -euo pipefail
# 0) Preconditions (one-time)
export PATH="$(go env GOPATH)/bin:$PATH"
go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.19.0 >/dev/null
go install k8s.io/code-generator/cmd/client-gen@v0.34.0 >/dev/null
git fetch origin main --depth=1 || true
# 1) Determine changed files source: explicit args or git base diff
if [ "$#" -gt 0 ]; then
# Treat provided paths as changed files
CHANGED_CSV=$(printf "%s," "$@" | sed 's/,$//')
echo "Simulating changes in: $CHANGED_CSV"
PKGS="$(go run ./scripts/affected-packages.go -changed-files "${CHANGED_CSV}")"
E2E_OUT="$(go run ./scripts/affected-packages.go -mode=suites -changed-files "${CHANGED_CSV}")"
else
# Compute base (robust to unrelated histories)
BASE="$(git merge-base HEAD origin/main 2>/dev/null || true)"
if [ -z "${BASE}" ]; then
echo "No merge-base with origin/main → running full set"
PKGS="./..."
E2E_OUT="$(go run ./scripts/affected-packages.go -mode=suites -changed-files go.mod || true)"
else
PKGS="$(go run ./scripts/affected-packages.go -base "${BASE}")"
E2E_OUT="$(go run ./scripts/affected-packages.go -mode=suites -base "${BASE}")"
fi
fi
# 2) Print what will run
echo "=== Affected unit packages ==="
if [ -n "${PKGS}" ]; then echo "${PKGS}"; else echo "(none)"; fi
echo
echo "=== Affected e2e tests ==="
if [ -n "${E2E_OUT}" ]; then echo "${E2E_OUT}"; else echo "(none)"; fi
echo
# 3) Unit tests via Makefile (inherits required build tags)
if [ "${PKGS}" = "./..." ]; then
echo "Running: make test (all)"
make test
elif [ -n "${PKGS}" ]; then
echo "Running: make test-packages for affected pkgs"
PACKAGES="$(echo "${PKGS}" | xargs)" make test-packages
else
echo "No affected unit packages"
fi
# 4) E2E tests via Makefile (filtered by regex)
PRE="$(echo "${E2E_OUT}" | awk -F: '$1=="preflight"{print $2}' | paste -sd'|' -)"
SB="$( echo "${E2E_OUT}" | awk -F: '$1=="support-bundle"{print $2}' | paste -sd'|' -)"
# Use direct go test with the same build tags as the Makefile to avoid RUN quoting issues locally
BUILD_TAGS='netgo containers_image_ostree_stub exclude_graphdriver_devicemapper exclude_graphdriver_btrfs containers_image_openpgp'
overall=0
if [ -n "${PRE}" ]; then
echo "Running preflight e2e: ${PRE}"
go test -tags "${BUILD_TAGS}" -installsuffix netgo -v -count=1 ./test/e2e/preflight -run "^(${PRE})$" || overall=1
fi
if [ -n "${SB}" ]; then
echo "Running support-bundle e2e: ${SB}"
go test -tags "${BUILD_TAGS}" -installsuffix netgo -v -count=1 ./test/e2e/support-bundle -run "^(${SB})$" || overall=1
fi
exit $overall
+181
View File
@@ -0,0 +1,181 @@
#!/bin/bash
# Comprehensive test for affected test detection
# Tests various code change scenarios to ensure correct suite detection
set -e
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
TESTS_PASSED=0
TESTS_FAILED=0
echo "========================================"
echo "Affected Test Detection Validation"
echo "========================================"
echo ""
# Helper function to run test
run_test() {
local test_name="$1"
local test_file="$2"
local expected_suites="$3"
echo -e "${BLUE}Test: $test_name${NC}"
echo "File: $test_file"
echo "Expected: $expected_suites"
# Get affected tests from explicit changed files (no git required); detector prints <suite>:<TestName>
local detector_output=$(go run ./scripts/affected-packages.go -mode=suites -changed-files "$test_file" 2>/dev/null)
# Derive suites from prefixes for comparison
local actual_suites=$(echo "$detector_output" | cut -d':' -f1 | grep -v '^$' | sort | uniq | tr '\n' ' ' | xargs)
# Compare results
if [ "$actual_suites" = "$expected_suites" ]; then
echo -e "${GREEN}✓ PASS${NC} - Got: $actual_suites"
if [ -n "$detector_output" ]; then
echo "Tests:" && echo "$detector_output" | sed 's/^/ - /'
fi
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo -e "${RED}✗ FAIL${NC} - Got: '$actual_suites', Expected: '$expected_suites'"
if [ -n "$detector_output" ]; then
echo "Tests:" && echo "$detector_output" | sed 's/^/ - /'
fi
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
echo ""
}
# Test 1: Preflight-only package (should only trigger preflight)
run_test "Preflight-only package change" \
"pkg/preflight/run.go" \
"preflight"
# Test 2: Support-bundle-only package
run_test "Support-bundle-only package change" \
"pkg/supportbundle/supportbundle.go" \
"support-bundle"
# Test 3: Shared package - collect
run_test "Shared package (collect) change" \
"pkg/collect/run.go" \
"preflight support-bundle"
# Test 4: Shared package - analyze
run_test "Shared package (analyze) change" \
"pkg/analyze/analyzer.go" \
"preflight support-bundle"
# Test 5: Shared package - k8sutil
run_test "Shared package (k8sutil) change" \
"pkg/k8sutil/config.go" \
"preflight support-bundle"
# Test 6: Shared package - convert
run_test "Shared package (convert) change" \
"pkg/convert/output.go" \
"preflight support-bundle"
# Test 7: Shared package - redact (another shared one)
run_test "Shared package (redact) change" \
"pkg/redact/redact.go" \
"preflight support-bundle"
# Test 8: Preflight command (should only trigger preflight)
run_test "Preflight command change" \
"cmd/preflight/main.go" \
"preflight"
# Test 9: Support-bundle types (support-bundle only package)
run_test "Support-bundle types change" \
"pkg/supportbundle/types/types.go" \
"support-bundle"
# Test 10: Workflow file (should not trigger e2e)
echo -e "${BLUE}Test: Workflow file change (should trigger nothing)${NC}"
echo "File: .github/workflows/affected-tests.yml"
echo "Expected: (no suites)"
detector_output=$(go run ./scripts/affected-packages.go -mode=suites -changed-files ".github/workflows/affected-tests.yml" 2>/dev/null)
actual_suites=$(echo "$detector_output" | cut -d':' -f1 | grep -v '^$' | sort | uniq | tr '\n' ' ' | xargs)
if [ -z "$actual_suites" ]; then
echo -e "${GREEN}✓ PASS${NC} - No suites affected (as expected)"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo -e "${RED}✗ FAIL${NC} - Got: '$actual_suites', Expected: (empty)"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
echo ""
# Test 11: go.mod change (should trigger all)
echo -e "${BLUE}Test: go.mod change (should trigger all suites)${NC}"
echo "File: go.mod"
echo "Expected: preflight support-bundle"
detector_output=$(go run ./scripts/affected-packages.go -mode=suites -changed-files "go.mod" 2>/dev/null)
actual_suites=$(echo "$detector_output" | cut -d':' -f1 | grep -v '^$' | sort | uniq | tr '\n' ' ' | xargs)
if [ "$actual_suites" = "preflight support-bundle" ]; then
echo -e "${GREEN}✓ PASS${NC} - Got: $actual_suites"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo -e "${RED}✗ FAIL${NC} - Got: '$actual_suites', Expected: 'preflight support-bundle'"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
echo ""
# Test 12: Multiple files across different areas
echo -e "${BLUE}Test: Multiple file changes (support-bundle + shared)${NC}"
echo "Files: pkg/supportbundle/supportbundle.go + pkg/collect/run.go"
echo "Expected: preflight support-bundle"
detector_output=$(go run ./scripts/affected-packages.go -mode=suites -changed-files "pkg/supportbundle/supportbundle.go,pkg/collect/run.go" 2>/dev/null)
actual_suites=$(echo "$detector_output" | cut -d':' -f1 | grep -v '^$' | sort | uniq | tr '\n' ' ' | xargs)
if [ "$actual_suites" = "preflight support-bundle" ]; then
echo -e "${GREEN}✓ PASS${NC} - Got: $actual_suites"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo -e "${RED}✗ FAIL${NC} - Got: '$actual_suites', Expected: 'preflight support-bundle'"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
echo ""
# Test 13: README change (should not trigger e2e)
echo -e "${BLUE}Test: Documentation change (should trigger nothing)${NC}"
echo "File: README.md"
echo "Expected: (no suites)"
detector_output=$(go run ./scripts/affected-packages.go -mode=suites -changed-files "README.md" 2>/dev/null)
actual_suites=$(echo "$detector_output" | cut -d':' -f1 | grep -v '^$' | sort | uniq | tr '\n' ' ' | xargs)
if [ -z "$actual_suites" ]; then
echo -e "${GREEN}✓ PASS${NC} - No suites affected (as expected)"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo -e "${RED}✗ FAIL${NC} - Got: '$actual_suites', Expected: (empty)"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
echo ""
# Summary
echo "========================================"
echo -e "${GREEN}Tests Passed: $TESTS_PASSED${NC}"
echo -e "${RED}Tests Failed: $TESTS_FAILED${NC}"
echo "========================================"
if [ $TESTS_FAILED -eq 0 ]; then
echo -e "${GREEN}✓ All tests passed!${NC}"
exit 0
else
echo -e "${RED}✗ Some tests failed${NC}"
exit 1
fi