Skip to content

Commit 583f84e

Browse files
davidcavazosNimJayiennae
authored
feat: testing isolation (#3872)
* add samples testing tooling * edit to trigger tests * update reusable workflow path * make relative path * fix config * inline workflow * install before lint * install before lint on Makefile * install gts * install repo wide deps * add test action * add project id * add auth credentials * add id-token permissions * use service account * cleanup * modify ignored file * revert change * make change to trigger tests * include e2e-test * check whether to e2e-test * add e2e-test configuration * add e2e-test as extra config * also check for substring as match * pass entire path to match * mark as experimental * add experimental emoji * test on node 18 and 22 * reorder matrix * use wip directly without service account * use kokoro service account * trigger all tests on root changes * do not fail fast * make lint a global job * separate nodejs versions * add checkout to reusable workflow * add contents read permission * skip testing everything on root changes * give contents read permissions * call reusable workflow correctly * reusable workflow for nodejs test * change names * more explicit names * only show version number * test all * use node 20 * create service account file * adjust token lifetime * lower node version * do not test all * use node 16 * use node 20 * restore language test * add install and more comments * assign and print variables in the same line * change to trigger tests * test all * exclude special tests * more packages to exclude * support exclude-packages and json comments * use jsonc extension * use flag package * remove unused actions for config * restore changes * use correct config * better error messages * improve error messages * use io/fs.WalkDir * rename variable * experiment with nightly tests * remove e2e-test for now * use ternary operator * disable pr tests temporarily * use diffs file * print git diff outputs as well * remove git utils * initial prototype for nightlies * update workflow command * only sprintf when wildcard is there * print on success too * improve error messages * disable parallelism * improve error reporting * clean before each test * include parallel flag * re-enable tests and disable nightlies experiment * disable fail-fast * revert clean commands * add files to ignore * ignore speech package * ignore translate package * add license headers and support multi-line comments * revert and add build to lint * move tools to single command * update workflow to new command * Add comment for cron schedule Co-authored-by: Nim Jayawardena <[email protected]> * run nightlies each on their own job * fix typo on path * better messages * exclude talent package * document affected * reorganize files * use script to run tests * run all tests * fix syntax error * do not fail on npm install * print outputs immediately * increment failures atomically * disable nightly batch job * install repo root package * use log.Fatalf * add unit tests and testing instructions * move tools directory * fix tests pkg path * run tests on action * add go linting * use double star on paths * use variadic argument prints * remove everything related to run-all * add tool summary * use pointer for methods * use table based testing * return nil if error * use manual test checks * moved tests to source directory * update test command * comment schedule trigger --------- Co-authored-by: Nim Jayawardena <[email protected]> Co-authored-by: Jennifer Davis <[email protected]>
1 parent f9b1fd4 commit 583f84e

File tree

17 files changed

+925
-1
lines changed

17 files changed

+925
-1
lines changed

Diff for: .github/cloud-samples-tools/.gitignore

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# If you prefer the allow list template instead of the deny list, see community template:
2+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3+
#
4+
# Binaries for programs and plugins
5+
*.exe
6+
*.exe~
7+
*.dll
8+
*.so
9+
*.dylib
10+
11+
# Test binary, built with `go test -c`
12+
*.test
13+
14+
# Output of the go coverage tool, specifically when used with LiteIDE
15+
*.out
16+
17+
# Dependency directories (remove the comment below to include it)
18+
# vendor/
19+
20+
# Go workspace file
21+
go.work
22+
go.work.sum
23+
24+
# env file
25+
.env

Diff for: .github/cloud-samples-tools/README.md

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Cloud Samples tools
2+
3+
This is a collection of tools used for Cloud Samples maintenance and infrastructure.
4+
5+
This tool has one function:
6+
7+
- `affected` finds the affected packages given a list of diffs.
8+
9+
## Config files
10+
11+
For this tools, we refer to a **package** as an isolated directory, which contains a "package file".
12+
For example, `package.json` in Node.js, `requirements.txt` in Python, `go.mod` in Go, or `pom.xml` in Java.
13+
14+
Each language has different configurations.
15+
We define them in config files in the repository, this way the tooling keeps language agnostic and each repository can have different configurations.
16+
17+
The config file can be a `.json` file, or a `.jsonc` (JSON with comments) file.
18+
For `.jsonc` files, it supports both `// single line comments` and `/* multi-line comments */`.
19+
20+
For example:
21+
22+
```jsonc
23+
{
24+
// The package file where the tests should be run (required).
25+
"package-file": "package.json",
26+
27+
// Match diffs only on .js and .ts files
28+
// Defaults to match all files.
29+
"match": ["*.js", "*.ts"],
30+
31+
// Ignore diffs on the README, text files, and anything under node_modules/.
32+
// Defaults to not ignore anything.
33+
"ignore": ["README.md", "*.txt", "node_modules/"],
34+
35+
// Skip these packages, these could be handled by a different config.
36+
// Defaults to not exclude anything.
37+
"exclude-packages": ["path/to/slow-to-test", "special-config-package"],
38+
}
39+
```
40+
41+
For more information, see [`pkg/utils/config.go`](pkg/utils/config.go).
42+
43+
## Building
44+
45+
To build the tools, we must change to the directory where the tools package is defined.
46+
We can run it in a subshell using parentheses to keep our working directory from changing.
47+
48+
```sh
49+
(cd .github/workflows/samples-tools && go build -o /tmp/tools ./cmd/*)
50+
```
51+
52+
## Running the tools unit tests
53+
54+
To the tools tests, we must change to the directory where the tools package is defined.
55+
We can run it in a subshell using parentheses to keep our working directory from changing.
56+
57+
```sh
58+
(cd .github/workflows/samples-tools && go test ./...)
59+
```
60+
61+
## Finding affected packages
62+
63+
> This must run at the repository root directory.
64+
65+
First, generate a file with all the diffs.
66+
This file should be one file per line.
67+
68+
You can use `git diff` to test on files that have changed in your branch.
69+
You can also create the file manually if you want to test something without commiting changes to your branch.
70+
71+
```sh
72+
git --no-pager diff --name-only HEAD origin/main | tee /tmp/diffs.txt
73+
```
74+
75+
Now we can check which packages have been affected.
76+
We pass the config file and the diffs file as positional arguments.
77+
78+
```sh
79+
/tmp/tools affected .github/config/nodejs.jsonc /tmp/diffs.txt
80+
```

Diff for: .github/cloud-samples-tools/cmd/main.go

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
Copyright 2024 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
c "cloud-samples-tools/pkg/config"
21+
"encoding/json"
22+
"flag"
23+
"fmt"
24+
"log"
25+
"os"
26+
"strings"
27+
)
28+
29+
var usage = `usage: tools <command> ...
30+
31+
commands:
32+
affected path/to/config.jsonc path/to/diffs.txt
33+
run-all path/to/config.jsonc path/to/script.sh
34+
`
35+
36+
// Entry point to validate command line arguments.
37+
func main() {
38+
flag.Parse()
39+
40+
command := flag.Arg(0)
41+
if command == "" {
42+
log.Fatalln("❌ no command specified\n", usage)
43+
}
44+
45+
switch command {
46+
case "affected":
47+
configFile := flag.Arg(1)
48+
if configFile == "" {
49+
log.Fatalln("❌ no config file specified\n", usage)
50+
}
51+
52+
diffsFile := flag.Arg(2)
53+
if diffsFile == "" {
54+
log.Fatalln("❌ no diffs file specified\n", usage)
55+
}
56+
57+
affectedCmd(configFile, diffsFile)
58+
59+
default:
60+
log.Fatalln("❌ unknown command: ", command, "\n", usage)
61+
}
62+
}
63+
64+
// affected command entry point to validate inputs.
65+
func affectedCmd(configFile string, diffsFile string) {
66+
config, err := c.LoadConfig(configFile)
67+
if err != nil {
68+
log.Fatalln("❌ error loading the config file: ", configFile, "\n", err)
69+
}
70+
71+
diffsBytes, err := os.ReadFile(diffsFile)
72+
if err != nil {
73+
log.Fatalln("❌ error getting the diffs: ", diffsFile, "\n", err)
74+
}
75+
diffs := strings.Split(string(diffsBytes), "\n")
76+
77+
packages, err := config.Affected(diffs)
78+
if err != nil {
79+
log.Fatalln("❌ error finding the affected packages.\n", err)
80+
}
81+
if len(packages) > 256 {
82+
log.Fatalln(
83+
"❌ Error: GitHub Actions only supports up to 256 packages, got ",
84+
len(packages),
85+
" packages, for more details see:\n",
86+
"https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow",
87+
)
88+
}
89+
90+
packagesJson, err := json.Marshal(packages)
91+
if err != nil {
92+
log.Fatalln("❌ error marshaling packages to JSON.\n", err)
93+
}
94+
95+
fmt.Println(string(packagesJson))
96+
}

Diff for: .github/cloud-samples-tools/go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module cloud-samples-tools
2+
3+
go 1.22.0

Diff for: .github/cloud-samples-tools/pkg/config/config.go

+190
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
Copyright 2024 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package config
18+
19+
import (
20+
"encoding/json"
21+
"errors"
22+
"io/fs"
23+
"os"
24+
"path/filepath"
25+
"regexp"
26+
"slices"
27+
"strings"
28+
)
29+
30+
type Config struct {
31+
// Filename to look for the root of a package.
32+
PackageFile []string `json:"package-file"`
33+
34+
// Pattern to match filenames or directories.
35+
Match []string `json:"match"`
36+
37+
// Pattern to ignore filenames or directories.
38+
Ignore []string `json:"ignore"`
39+
40+
// Packages to always exclude.
41+
ExcludePackages []string `json:"exclude-packages"`
42+
}
43+
44+
var multiLineCommentsRegex = regexp.MustCompile(`(?s)\s*/\*.*?\*/`)
45+
var singleLineCommentsRegex = regexp.MustCompile(`\s*//.*\s*`)
46+
47+
// Saves the config to the given file.
48+
func (c *Config) Save(file *os.File) error {
49+
bytes, err := json.MarshalIndent(c, "", " ")
50+
if err != nil {
51+
return err
52+
}
53+
_, err = file.Write(bytes)
54+
if err != nil {
55+
return err
56+
}
57+
return nil
58+
}
59+
60+
// LoadConfig loads the config from the given path.
61+
func LoadConfig(path string) (*Config, error) {
62+
// Read the JSONC file.
63+
sourceJsonc, err := os.ReadFile(path)
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
// Strip the comments and load the JSON.
69+
sourceJson := multiLineCommentsRegex.ReplaceAll(sourceJsonc, []byte{})
70+
sourceJson = singleLineCommentsRegex.ReplaceAll(sourceJson, []byte{})
71+
72+
var config Config
73+
err = json.Unmarshal(sourceJson, &config)
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
// Set default values if they are not set.
79+
if config.PackageFile == nil {
80+
return nil, errors.New("package-file is required")
81+
}
82+
if config.Match == nil {
83+
config.Match = []string{"*"}
84+
}
85+
86+
return &config, nil
87+
}
88+
89+
// Match returns true if the path matches any of the patterns.
90+
func Match(patterns []string, path string) bool {
91+
filename := filepath.Base(path)
92+
for _, pattern := range patterns {
93+
if match, _ := filepath.Match(pattern, filename); match {
94+
return true
95+
}
96+
if strings.Contains(path, pattern) {
97+
return true
98+
}
99+
}
100+
return false
101+
}
102+
103+
// Matches returns true if the path matches the config.
104+
func (c *Config) Matches(path string) bool {
105+
return Match(c.Match, path) && !Match(c.Ignore, path)
106+
}
107+
108+
// IsPackageDir returns true if the path is a package directory.
109+
func (c *Config) IsPackageDir(dir string) bool {
110+
for _, filename := range c.PackageFile {
111+
packageFile := filepath.Join(dir, filename)
112+
if fileExists(packageFile) {
113+
return true
114+
}
115+
}
116+
return false
117+
}
118+
119+
// FindPackage returns the package name for the given path.
120+
func (c *Config) FindPackage(path string) string {
121+
dir := filepath.Dir(path)
122+
if dir == "." || c.IsPackageDir(dir) {
123+
return dir
124+
}
125+
return c.FindPackage(dir)
126+
}
127+
128+
// FindAllPackages finds all the packages in the given root directory.
129+
func (c *Config) FindAllPackages(root string) ([]string, error) {
130+
var packages []string
131+
err := fs.WalkDir(os.DirFS(root), ".",
132+
func(path string, d os.DirEntry, err error) error {
133+
if err != nil {
134+
return err
135+
}
136+
if path == "." {
137+
return nil
138+
}
139+
if slices.Contains(c.ExcludePackages, path) {
140+
return nil
141+
}
142+
if d.IsDir() && c.Matches(path) && c.IsPackageDir(path) {
143+
packages = append(packages, path)
144+
return nil
145+
}
146+
return nil
147+
})
148+
if err != nil {
149+
return []string{}, err
150+
}
151+
return packages, nil
152+
}
153+
154+
// Affected returns the packages that have been affected from diffs.
155+
// If there are diffs on at leat one global file affecting all packages,
156+
// then this returns all packages matched by the config.
157+
func (c *Config) Affected(diffs []string) ([]string, error) {
158+
changed := c.Changed(diffs)
159+
if slices.Contains(changed, ".") {
160+
return c.FindAllPackages(".")
161+
}
162+
return changed, nil
163+
}
164+
165+
// Changed returns the packages that have changed.
166+
// It only returns packages that are matched by the config,
167+
// and are not excluded by the config.
168+
func (c *Config) Changed(diffs []string) []string {
169+
changedUnique := make(map[string]bool)
170+
for _, diff := range diffs {
171+
if !c.Matches(diff) {
172+
continue
173+
}
174+
pkg := c.FindPackage(diff)
175+
if slices.Contains(c.ExcludePackages, pkg) {
176+
continue
177+
}
178+
changedUnique[pkg] = true
179+
}
180+
181+
if len(changedUnique) == 0 {
182+
return []string{"."}
183+
}
184+
185+
changed := make([]string, 0, len(changedUnique))
186+
for pkg := range changedUnique {
187+
changed = append(changed, pkg)
188+
}
189+
return changed
190+
}

0 commit comments

Comments
 (0)