|
| 1 | +package cmd |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/json" |
| 5 | + "fmt" |
| 6 | + "log" |
| 7 | + |
| 8 | + "github.com/smartcontractkit/chainlink-testing-framework/tools/flakeguard/git" |
| 9 | + "github.com/smartcontractkit/chainlink-testing-framework/tools/flakeguard/golang" |
| 10 | + "github.com/smartcontractkit/chainlink-testing-framework/tools/flakeguard/utils" |
| 11 | + "github.com/spf13/cobra" |
| 12 | +) |
| 13 | + |
| 14 | +var FindTestsCmd = &cobra.Command{ |
| 15 | + Use: "find", |
| 16 | + Long: "Analyzes Golang project repository for changed files against a specified base reference and determines the test packages that are potentially impacted", |
| 17 | + Short: "Find test packages that may be affected by changes", |
| 18 | + Run: func(cmd *cobra.Command, args []string) { |
| 19 | + projectPath, _ := cmd.Flags().GetString("project-path") |
| 20 | + verbose, _ := cmd.Flags().GetBool("verbose") |
| 21 | + jsonOutput, _ := cmd.Flags().GetBool("json") |
| 22 | + filterEmptyTests, _ := cmd.Flags().GetBool("filter-empty-tests") |
| 23 | + baseRef, _ := cmd.Flags().GetString("base-ref") |
| 24 | + excludes, _ := cmd.Flags().GetStringSlice("excludes") |
| 25 | + levels, _ := cmd.Flags().GetInt("levels") |
| 26 | + findByTestFilesDiff, _ := cmd.Flags().GetBool("find-by-test-files-diff") |
| 27 | + findByAffected, _ := cmd.Flags().GetBool("find-by-affected-packages") |
| 28 | + onlyShowChangedTestFiles, _ := cmd.Flags().GetBool("only-show-changed-test-files") |
| 29 | + |
| 30 | + // Find all changes in test files and get their package names |
| 31 | + var changedTestPkgs []string |
| 32 | + if findByTestFilesDiff { |
| 33 | + changedTestFiles, err := git.FindChangedFiles(baseRef, "grep '_test\\.go$'", excludes) |
| 34 | + if err != nil { |
| 35 | + log.Fatalf("Error finding changed test files: %v", err) |
| 36 | + } |
| 37 | + if onlyShowChangedTestFiles { |
| 38 | + outputResults(changedTestFiles, jsonOutput) |
| 39 | + return |
| 40 | + } |
| 41 | + if verbose { |
| 42 | + fmt.Println("Changed test files:", changedTestFiles) |
| 43 | + } |
| 44 | + changedTestPkgs, err = golang.GetFilePackages(changedTestFiles) |
| 45 | + if err != nil { |
| 46 | + log.Fatalf("Error getting package names for test files: %v", err) |
| 47 | + } |
| 48 | + } |
| 49 | + |
| 50 | + // Find all affected test packages |
| 51 | + var affectedTestPkgs []string |
| 52 | + if findByAffected { |
| 53 | + if verbose { |
| 54 | + fmt.Println("Finding affected packages...") |
| 55 | + } |
| 56 | + affectedTestPkgs = findAffectedPackages(baseRef, projectPath, excludes, levels) |
| 57 | + } |
| 58 | + |
| 59 | + // Combine and deduplicate test package names |
| 60 | + testPkgs := append(changedTestPkgs, affectedTestPkgs...) |
| 61 | + testPkgs = utils.Deduplicate(testPkgs) |
| 62 | + |
| 63 | + // Filter out packages that do not have tests |
| 64 | + if filterEmptyTests { |
| 65 | + if verbose { |
| 66 | + fmt.Println("Filtering packages without tests...") |
| 67 | + } |
| 68 | + testPkgs = golang.FilterPackagesWithTests(testPkgs) |
| 69 | + } |
| 70 | + |
| 71 | + outputResults(testPkgs, jsonOutput) |
| 72 | + }, |
| 73 | +} |
| 74 | + |
| 75 | +func init() { |
| 76 | + FindTestsCmd.Flags().StringP("project-path", "r", ".", "The path to the Go project. Default is the current directory. Useful for subprojects.") |
| 77 | + FindTestsCmd.Flags().String("base-ref", "", "Git base reference (branch, tag, commit) for comparing changes. Required.") |
| 78 | + FindTestsCmd.Flags().BoolP("verbose", "v", false, "Enable verbose mode") |
| 79 | + FindTestsCmd.Flags().Bool("json", false, "Output the results in JSON format") |
| 80 | + FindTestsCmd.Flags().Bool("filter-empty-tests", false, "Filter out test packages with no actual test functions. Can be very slow for large projects.") |
| 81 | + FindTestsCmd.Flags().StringSlice("excludes", []string{}, "List of paths to exclude. Useful for repositories with multiple Go projects within.") |
| 82 | + FindTestsCmd.Flags().IntP("levels", "l", 2, "The number of levels of recursion to search for affected packages. Default is 2. 0 is unlimited.") |
| 83 | + FindTestsCmd.Flags().Bool("find-by-test-files-diff", true, "Enable the mode to find test packages by changes in test files.") |
| 84 | + FindTestsCmd.Flags().Bool("find-by-affected-packages", true, "Enable the mode to find test packages that may be affected by changes in any of the project packages.") |
| 85 | + FindTestsCmd.Flags().Bool("only-show-changed-test-files", false, "Only show the changed test files and exit") |
| 86 | + |
| 87 | + if err := FindTestsCmd.MarkFlagRequired("base-ref"); err != nil { |
| 88 | + fmt.Println("Error marking base-ref as required:", err) |
| 89 | + } |
| 90 | +} |
| 91 | + |
| 92 | +func findAffectedPackages(baseRef, projectPath string, excludes []string, levels int) []string { |
| 93 | + goList, err := golang.GoList() |
| 94 | + if err != nil { |
| 95 | + log.Fatalf("Error getting go list: %v\nStdErr: %s", err, goList.Stderr.String()) |
| 96 | + } |
| 97 | + gitDiff, err := git.Diff(baseRef) |
| 98 | + if err != nil { |
| 99 | + log.Fatalf("Error getting the git diff: %v\nStdErr: %s", err, gitDiff.Stderr.String()) |
| 100 | + } |
| 101 | + gitModDiff, err := git.ModDiff(baseRef, projectPath) |
| 102 | + if err != nil { |
| 103 | + log.Fatalf("Error getting the git mod diff: %v\nStdErr: %s", err, gitModDiff.Stderr.String()) |
| 104 | + } |
| 105 | + |
| 106 | + packages, err := golang.ParsePackages(goList.Stdout) |
| 107 | + if err != nil { |
| 108 | + log.Fatalf("Error parsing packages: %v", err) |
| 109 | + } |
| 110 | + |
| 111 | + fileMap := golang.GetGoFileMap(packages, true) |
| 112 | + |
| 113 | + var changedPackages []string |
| 114 | + changedPackages, err = git.GetChangedGoPackagesFromDiff(gitDiff.Stdout, projectPath, excludes, fileMap) |
| 115 | + if err != nil { |
| 116 | + log.Fatalf("Error getting changed packages: %v", err) |
| 117 | + } |
| 118 | + |
| 119 | + changedModPackages, err := git.GetGoModChangesFromDiff(gitModDiff.Stdout) |
| 120 | + if err != nil { |
| 121 | + log.Fatalf("Error getting go.mod changes: %v", err) |
| 122 | + } |
| 123 | + |
| 124 | + depMap := golang.GetGoDepMap(packages) |
| 125 | + |
| 126 | + // Find affected packages |
| 127 | + // use map to make handling duplicates simpler |
| 128 | + affectedPkgs := map[string]bool{} |
| 129 | + |
| 130 | + // loop through packages changed via file changes |
| 131 | + for _, pkg := range changedPackages { |
| 132 | + p := golang.FindAffectedPackages(pkg, depMap, false, levels) |
| 133 | + for _, p := range p { |
| 134 | + affectedPkgs[p] = true |
| 135 | + } |
| 136 | + } |
| 137 | + |
| 138 | + // loop through packages changed via go.mod changes |
| 139 | + for _, pkg := range changedModPackages { |
| 140 | + p := golang.FindAffectedPackages(pkg, depMap, true, levels) |
| 141 | + for _, p := range p { |
| 142 | + affectedPkgs[p] = true |
| 143 | + } |
| 144 | + } |
| 145 | + |
| 146 | + // convert map to array |
| 147 | + pkgs := []string{} |
| 148 | + for k := range affectedPkgs { |
| 149 | + pkgs = append(pkgs, k) |
| 150 | + } |
| 151 | + |
| 152 | + return pkgs |
| 153 | +} |
| 154 | + |
| 155 | +func outputResults(packages []string, jsonOutput bool) { |
| 156 | + if jsonOutput { |
| 157 | + if packages == nil { |
| 158 | + packages = make([]string, 0) // Ensure the slice is initialized to an empty array |
| 159 | + } |
| 160 | + data, err := json.Marshal(packages) |
| 161 | + if err != nil { |
| 162 | + log.Fatalf("Error marshaling test files to JSON: %v", err) |
| 163 | + } |
| 164 | + fmt.Println(string(data)) |
| 165 | + } else { |
| 166 | + for _, pkg := range packages { |
| 167 | + fmt.Print(pkg, " ") |
| 168 | + } |
| 169 | + } |
| 170 | +} |
0 commit comments