chore: import upstream snapshot with attribution
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// exitNoAffected is the affected verb's CI sentinel: the change touched nothing
|
||||
// the test graph covers, so a selective-test runner can skip the suite. It is
|
||||
// distinct from the generic error exit (1) so a CI script can tell "no tests to
|
||||
// run" apart from "the command failed".
|
||||
const exitNoAffected = 3
|
||||
|
||||
var (
|
||||
affectedStdin bool
|
||||
affectedJSON bool
|
||||
affectedQuiet bool
|
||||
affectedIndex string
|
||||
)
|
||||
|
||||
var affectedCmd = &cobra.Command{
|
||||
Use: "affected [files...]",
|
||||
Short: "List the test files affected by a set of changed files",
|
||||
Long: `Resolve which test files cover the symbols in a set of changed files, by
|
||||
walking the graph's test edges on the daemon that tracks the repo.
|
||||
|
||||
Built for CI / git-hook piping: pass changed paths as args or via --stdin
|
||||
(e.g. ` + "`git diff --name-only | gortex affected --stdin --quiet`" + `), and
|
||||
branch on the exit code:
|
||||
|
||||
exit 0 one or more test files are affected — run them
|
||||
exit 3 nothing the test graph covers changed — skip the suite
|
||||
exit 1 the command failed (no daemon, repo not tracked, …)`,
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: runAffected,
|
||||
}
|
||||
|
||||
func init() {
|
||||
affectedCmd.Flags().BoolVar(&affectedStdin, "stdin", false, "read changed file paths from stdin (whitespace/newline separated)")
|
||||
affectedCmd.Flags().BoolVar(&affectedJSON, "json", false, "emit the affected test files as JSON")
|
||||
affectedCmd.Flags().BoolVarP(&affectedQuiet, "quiet", "q", false, "print nothing; communicate only via the exit code")
|
||||
affectedCmd.Flags().StringVar(&affectedIndex, "index", "", "repository path the daemon tracks (default: current directory)")
|
||||
rootCmd.AddCommand(affectedCmd)
|
||||
}
|
||||
|
||||
func runAffected(cmd *cobra.Command, args []string) error {
|
||||
changed := append([]string(nil), args...)
|
||||
if affectedStdin {
|
||||
changed = append(changed, parseAffectedStdin(cmd.InOrStdin())...)
|
||||
}
|
||||
changed = dedupeNonEmpty(changed)
|
||||
if len(changed) == 0 {
|
||||
return fmt.Errorf("affected: no changed files given (pass paths as args or --stdin)")
|
||||
}
|
||||
|
||||
repoPath := affectedIndex
|
||||
if repoPath == "" {
|
||||
repoPath = "."
|
||||
}
|
||||
|
||||
targets, err := resolveAffectedTests(repoPath, changed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !affectedQuiet {
|
||||
if affectedJSON {
|
||||
_ = json.NewEncoder(cmd.OutOrStdout()).Encode(map[string]any{"affected_tests": targets})
|
||||
} else {
|
||||
for _, t := range targets {
|
||||
fmt.Fprintln(cmd.OutOrStdout(), t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if code := affectedExitCode(len(targets)); code != 0 {
|
||||
return &exitCodeError{code: code}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// affectedExitCode maps an affected-test count to the CI sentinel exit code:
|
||||
// 0 when something is affected (run the tests), exitNoAffected when nothing is.
|
||||
func affectedExitCode(targetCount int) int {
|
||||
if targetCount > 0 {
|
||||
return 0
|
||||
}
|
||||
return exitNoAffected
|
||||
}
|
||||
|
||||
// parseAffectedStdin reads whitespace/newline-separated file paths from r. It
|
||||
// tolerates `git diff --name-only` output (one path per line) and a space-
|
||||
// separated list on a single line equally.
|
||||
func parseAffectedStdin(r io.Reader) []string {
|
||||
var out []string
|
||||
sc := bufio.NewScanner(r)
|
||||
for sc.Scan() {
|
||||
out = append(out, strings.Fields(sc.Text())...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// dedupeNonEmpty trims, drops blanks, and dedupes while preserving order.
|
||||
func dedupeNonEmpty(in []string) []string {
|
||||
seen := make(map[string]bool, len(in))
|
||||
out := make([]string, 0, len(in))
|
||||
for _, s := range in {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" || seen[s] {
|
||||
continue
|
||||
}
|
||||
seen[s] = true
|
||||
out = append(out, s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// resolveAffectedTests maps changed files to the test files that cover them: it
|
||||
// asks the daemon for each changed file's symbol IDs (get_file_summary), then
|
||||
// resolves the covering tests in one get_test_targets call. The returned test
|
||||
// file paths are unique and sorted.
|
||||
func resolveAffectedTests(repoPath string, changedFiles []string) ([]string, error) {
|
||||
idSet := make(map[string]bool)
|
||||
for _, f := range changedFiles {
|
||||
raw, err := requireDaemonTool(repoPath, "get_file_summary", map[string]any{"file_path": f})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, id := range collectSymbolIDs(raw) {
|
||||
idSet[id] = true
|
||||
}
|
||||
}
|
||||
if len(idSet) == 0 {
|
||||
return nil, nil // no indexed symbols in the changed files → nothing affected
|
||||
}
|
||||
ids := make([]string, 0, len(idSet))
|
||||
for id := range idSet {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
sort.Strings(ids)
|
||||
|
||||
raw, err := requireDaemonTool(repoPath, "get_test_targets", map[string]any{"ids": strings.Join(ids, ",")})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseTestTargetFiles(raw), nil
|
||||
}
|
||||
|
||||
// parseTestTargetFiles extracts the unique, sorted test file paths from a
|
||||
// get_test_targets response (its `test_targets[].file` list).
|
||||
func parseTestTargetFiles(raw []byte) []string {
|
||||
var resp struct {
|
||||
TestTargets []struct {
|
||||
File string `json:"file"`
|
||||
} `json:"test_targets"`
|
||||
}
|
||||
if json.Unmarshal(raw, &resp) != nil {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
var files []string
|
||||
for _, t := range resp.TestTargets {
|
||||
if t.File != "" && !seen[t.File] {
|
||||
seen[t.File] = true
|
||||
files = append(files, t.File)
|
||||
}
|
||||
}
|
||||
sort.Strings(files)
|
||||
return files
|
||||
}
|
||||
|
||||
// collectSymbolIDs walks a JSON document and gathers the string values under
|
||||
// any "id" key that have the symbol-id shape (contain "::"), deduped. Tolerant
|
||||
// of the differing get_file_summary shapes across versions.
|
||||
func collectSymbolIDs(raw []byte) []string {
|
||||
var doc any
|
||||
if json.Unmarshal(raw, &doc) != nil {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
var out []string
|
||||
var walk func(any)
|
||||
walk = func(n any) {
|
||||
switch t := n.(type) {
|
||||
case map[string]any:
|
||||
for k, v := range t {
|
||||
if k == "id" {
|
||||
if sv, ok := v.(string); ok && strings.Contains(sv, "::") && !seen[sv] {
|
||||
seen[sv] = true
|
||||
out = append(out, sv)
|
||||
continue
|
||||
}
|
||||
}
|
||||
walk(v)
|
||||
}
|
||||
case []any:
|
||||
for _, e := range t {
|
||||
walk(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(doc)
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user