From c5cd962837e254a693548951104ef5e4f4e975d7 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Thu, 8 Jun 2023 16:42:15 +0400 Subject: [PATCH 1/8] Shard integration tests --- .github/codecov.yml | 1 - .github/workflows/go.yml | 10 ++- Taskfile.yml | 25 ++++-- cmd/envtool/envtool.go | 24 ++--- cmd/envtool/envtool_test.go | 22 +++-- cmd/envtool/tests.go | 158 ++++++++++++++------------------ cmd/envtool/tests_test.go | 175 ++++++------------------------------ 7 files changed, 142 insertions(+), 273 deletions(-) diff --git a/.github/codecov.yml b/.github/codecov.yml index 8ed2e21631e2..94c04fcccfc0 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -3,7 +3,6 @@ codecov: require_ci_to_pass: true notify: - after_n_builds: 2 # 1 for unit tests + at least 1 for integration tests wait_for_ci: true # https://docs.codecov.com/docs/coverage-configuration diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 03e0ad3b6d0a..15384581989d 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -158,9 +158,11 @@ jobs: fail-fast: false matrix: include: - - { name: "MongoDB", task: "mongodb" } - - { name: "PostgreSQL", task: "pg" } - - { name: "SQLite", task: "sqlite" } + - { name: "MongoDB", task: "mongodb", shard_index: 0, shard_total: 1 } + - { name: "PostgreSQL", task: "pg", shard_index: 0, shard_total: 3 } + - { name: "PostgreSQL", task: "pg", shard_index: 1, shard_total: 3 } + - { name: "PostgreSQL", task: "pg", shard_index: 2, shard_total: 3 } + - { name: "SQLite", task: "sqlite", shard_index: 0, shard_total: 1 } steps: - name: Checkout code @@ -188,7 +190,7 @@ jobs: run: bin/task env-setup - name: Run ${{ matrix.task }} tests - run: bin/task test-integration-${{ matrix.task }} + run: bin/task test-integration-${{ matrix.task }} SHARD_INDEX=${{ matrix.shard_index }} SHARD_TOTAL=${{ matrix.shard_total }} # The token is not required but should make uploads more stable. # If secrets are unavailable (for example, for a pull request from a fork), it fallbacks to the tokenless uploads. diff --git a/Taskfile.yml b/Taskfile.yml index 3c1599d8b78d..644e2fea1b02 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -189,58 +189,73 @@ tasks: dir: integration cmds: - > - go test -count=1 -timeout={{.INTEGRATIONTIME}} {{.RACEFLAG}} -tags={{.BUILDTAGS}} -shuffle=on -coverpkg=../... + go test -count=1 -run='{{.RUN}}' -timeout={{.INTEGRATIONTIME}} {{.RACEFLAG}} -tags={{.BUILDTAGS}} -shuffle=on -coverpkg=../... -coverprofile=integration-pg.txt . -target-backend=ferretdb-pg -target-tls -postgresql-url=postgres://username@127.0.0.1:5432/ferretdb -compat-url='mongodb://username:password@127.0.0.1:47018/?tls=true&tlsCertificateKeyFile=../build/certs/client.pem&tlsCaFile=../build/certs/rootCA-cert.pem' + vars: + RUN: + sh: go run -C .. ./cmd/envtool tests shard --index={{.SHARD_INDEX | default 0}} --total={{.SHARD_TOTAL | default 1}} test-integration-sqlite: desc: "Run integration tests for `sqlite` handler" dir: integration cmds: - > - go test -count=1 -timeout={{.INTEGRATIONTIME}} {{.RACEFLAG}} -tags={{.BUILDTAGS}} -shuffle=on -coverpkg=../... + go test -count=1 -run='{{.RUN}}' -timeout={{.INTEGRATIONTIME}} {{.RACEFLAG}} -tags={{.BUILDTAGS}} -shuffle=on -coverpkg=../... -coverprofile=integration-sqlite.txt . -target-backend=ferretdb-sqlite -target-tls -compat-url='mongodb://username:password@127.0.0.1:47018/?tls=true&tlsCertificateKeyFile=../build/certs/client.pem&tlsCaFile=../build/certs/rootCA-cert.pem' -disable-filter-pushdown + vars: + RUN: + sh: go run -C .. ./cmd/envtool tests shard --index={{.SHARD_INDEX | default 0}} --total={{.SHARD_TOTAL | default 1}} test-integration-tigris: desc: "Run integration tests for `tigris` handler" dir: integration cmds: - > - go test -count=1 -timeout={{.INTEGRATIONTIME}} {{.RACEFLAG}} -tags={{.BUILDTAGS}} -shuffle=on -coverpkg=../... + go test -count=1 -run='{{.RUN}}' -timeout={{.INTEGRATIONTIME}} {{.RACEFLAG}} -tags={{.BUILDTAGS}} -shuffle=on -coverpkg=../... -coverprofile=integration-tigris.txt . -target-backend=ferretdb-tigris {{.UNIXSOCKETFLAG}} -tigris-urls=127.0.0.1:8081,127.0.0.1:8091,127.0.0.1:8092,127.0.0.1:8093,127.0.0.1:8094 -compat-url=mongodb://127.0.0.1:47017/ + vars: + RUN: + sh: go run -C .. ./cmd/envtool tests shard --index={{.SHARD_INDEX | default 0}} --total={{.SHARD_TOTAL | default 1}} test-integration-hana: desc: "Run integration tests for `hana` handler" dir: integration cmds: - > - go test -count=1 -timeout={{.INTEGRATIONTIME}} {{.RACEFLAG}} -tags={{.BUILDTAGS}} -shuffle=on -coverpkg=../... + go test -count=1 -run='{{.RUN}}' -timeout={{.INTEGRATIONTIME}} {{.RACEFLAG}} -tags={{.BUILDTAGS}} -shuffle=on -coverpkg=../... -coverprofile=integration-hana.txt . -target-backend=ferretdb-hana {{.UNIXSOCKETFLAG}} -hana-url=$FERRETDB_HANA_URL -compat-url=mongodb://127.0.0.1:47017/ + vars: + RUN: + sh: go run -C .. ./cmd/envtool tests shard --index={{.SHARD_INDEX | default 0}} --total={{.SHARD_TOTAL | default 1}} test-integration-mongodb: desc: "Run integration tests for MongoDB" dir: integration cmds: - > - go test -count=1 -timeout={{.INTEGRATIONTIME}} {{.RACEFLAG}} -tags={{.BUILDTAGS}} -shuffle=on -coverpkg=../... + go test -count=1 -run='{{.RUN}}' -timeout={{.INTEGRATIONTIME}} {{.RACEFLAG}} -tags={{.BUILDTAGS}} -shuffle=on -coverpkg=../... -coverprofile=integration-mongodb.txt . -target-url=mongodb://127.0.0.1:47017/ -target-backend=mongodb + vars: + RUN: + sh: go run -C .. ./cmd/envtool tests shard --index={{.SHARD_INDEX | default 0}} --total={{.SHARD_TOTAL | default 1}} bench-unit-short: desc: "Benchmark internal package for about 25 seconds (with default BENCHTIME)" diff --git a/cmd/envtool/envtool.go b/cmd/envtool/envtool.go index 5b81793f19e4..0d8e08c20ab5 100644 --- a/cmd/envtool/envtool.go +++ b/cmd/envtool/envtool.go @@ -329,8 +329,8 @@ func printDiagnosticData(setupError error, logger *zap.SugaredLogger) { }) } -// mkdir creates all directories from given paths. -func mkdir(paths ...string) error { +// shellMkDir creates all directories from given paths. +func shellMkDir(paths ...string) error { var errs error for _, path := range paths { @@ -342,8 +342,8 @@ func mkdir(paths ...string) error { return errs } -// rmdir removes all directories from given paths. -func rmdir(paths ...string) error { +// shellRmDir removes all directories from given paths. +func shellRmDir(paths ...string) error { var errs error for _, path := range paths { @@ -355,8 +355,8 @@ func rmdir(paths ...string) error { return errs } -// read will show the content of a file. -func read(w io.Writer, paths ...string) error { +// shellRead will show the content of a file. +func shellRead(w io.Writer, paths ...string) error { for _, path := range paths { b, err := os.ReadFile(path) if err != nil { @@ -403,8 +403,8 @@ var cli struct { PackageVersion struct{} `cmd:"" help:"Print package version"` Tests struct { Shard struct { - Index uint `required:""` - Total uint `help:"Total number of shards" required:""` + Index uint `help:"Shard index, starting from 0" required:""` + Total uint `help:"Total number of shards" required:""` } `cmd:"" help:"Print sharded integration tests"` } `cmd:""` } @@ -434,15 +434,15 @@ func main() { case "setup": err = setup(ctx, logger) case "shell mkdir ": - err = mkdir(cli.Shell.Mkdir.Paths...) + err = shellMkDir(cli.Shell.Mkdir.Paths...) case "shell rmdir ": - err = rmdir(cli.Shell.Rmdir.Paths...) + err = shellRmDir(cli.Shell.Rmdir.Paths...) case "shell read ": - err = read(os.Stdout, cli.Shell.Read.Paths...) + err = shellRead(os.Stdout, cli.Shell.Read.Paths...) case "package-version": err = packageVersion(os.Stdout, versionFile) case "tests shard": - err = shardIntegrationTests(os.Stdout, cli.Tests.Shard.Index, cli.Tests.Shard.Total) + err = testsShard(os.Stdout, cli.Tests.Shard.Index, cli.Tests.Shard.Total) default: err = fmt.Errorf("unknown command: %s", cmd) } diff --git a/cmd/envtool/envtool_test.go b/cmd/envtool/envtool_test.go index dca293f3edcd..6473c15c57fd 100644 --- a/cmd/envtool/envtool_test.go +++ b/cmd/envtool/envtool_test.go @@ -33,26 +33,24 @@ func TestPrintDiagnosticData(t *testing.T) { }) } -func TestRmdirAbsentDir(t *testing.T) { +func TestShellMkDirRmDir(t *testing.T) { t.Parallel() - err := rmdir("absent") - assert.NoError(t, err) -} - -func TestMkdirAndRmdir(t *testing.T) { - t.Parallel() + t.Run("Absent", func(t *testing.T) { + err := shellRmDir("absent") + assert.NoError(t, err) + }) paths := []string{"ab/c", "ab"} - err := mkdir(paths...) + err := shellMkDir(paths...) assert.NoError(t, err) for _, path := range paths { assert.DirExists(t, path) } - err = rmdir(paths...) + err = shellRmDir(paths...) assert.NoError(t, err) for _, path := range paths { @@ -60,7 +58,7 @@ func TestMkdirAndRmdir(t *testing.T) { } } -func TestRead(t *testing.T) { +func TestShellRead(t *testing.T) { t.Parallel() f, err := os.CreateTemp("", "test_read") @@ -71,12 +69,12 @@ func TestRead(t *testing.T) { assert.NoError(t, err) var output bytes.Buffer - err = read(&output, f.Name()) + err = shellRead(&output, f.Name()) assert.NoError(t, err) assert.Equal(t, s, output.String()) } -func TestPrintVersion(t *testing.T) { +func TestPackageVersion(t *testing.T) { t.Parallel() f, err := os.CreateTemp("", "test_print_version") diff --git a/cmd/envtool/tests.go b/cmd/envtool/tests.go index 5068d574fcf3..baa841d48bff 100644 --- a/cmd/envtool/tests.go +++ b/cmd/envtool/tests.go @@ -15,136 +15,110 @@ package main import ( + "bufio" + "bytes" "fmt" "io" - "os" "os/exec" - "path" "sort" "strings" -) -// shardIntegrationTests shards integration test names from the specified directory. -func shardIntegrationTests(w io.Writer, index, total uint) error { - var output strings.Builder + "golang.org/x/exp/maps" +) - tests, err := getAllTestNames("integration") +// testsShard shards integration test names. +func testsShard(w io.Writer, index, total uint) error { + all, err := getAllTestNames("integration") if err != nil { return err } - shardedTests, err := shardTests(index, total, tests...) + sharded, err := shardTests(index, total, all) if err != nil { return err } - output.WriteString("^(") - output.WriteString(strings.Join(shardedTests, "|")) - output.WriteString(")$") - - if _, err = w.Write([]byte(output.String())); err != nil { - return err + fmt.Fprint(w, "^(") + for i, t := range sharded { + fmt.Fprint(w, t) + if i != len(sharded)-1 { + fmt.Fprint(w, "|") + } } + fmt.Fprint(w, ")$") return nil } -// shardTests shards gotten test names. -func shardTests(index, total uint, tests ...string) ([]string, error) { - if index >= total { - return nil, fmt.Errorf("Cannot shard when Index is greater or equal to Total (%d >= %d)", index, total) - } - - testsLen := uint(len(tests)) - if total > testsLen { - return nil, fmt.Errorf("Cannot shard when Total is greater than amount of tests (%d > %d)", total, testsLen) - } - - sharder := testsLen / total - start := sharder * index - end := sharder*index + sharder - - if index == total-1 { - modulo := testsLen % total - end += modulo - } - - return tests[start:end], nil -} - -// getAllTestNames gets all test names from the specified directory. +// getAllTestNames returns a sorted slice of all tests in the specified directory and subdirectories. func getAllTestNames(dir string) ([]string, error) { - newDir, err := getNewWorkingDir(dir) - if err != nil { - return nil, err - } + cmd := exec.Command("go", "test", "-list=.", "./...") + cmd.Dir = dir - cmd := exec.Command("go", "test", "-list=.") - cmd.Dir = newDir - - outputBytes, err := cmd.Output() - if err != nil { - return nil, err - } - - tests, err := prepareTestNamesOutput(string(outputBytes)) + b, err := cmd.Output() if err != nil { return nil, err } - return tests, nil -} - -// prepareTestNamesOutput prepares test names output. -func prepareTestNamesOutput(output string) ([]string, error) { - tests := strings.FieldsFunc(output, func(s rune) bool { - return s == '\n' - }) - - status := tests[len(tests)-1] - if !strings.Contains(status, "ok") { - return nil, fmt.Errorf("Could not read test names: %s", status) + tests := make(map[string]struct{}) + + s := bufio.NewScanner(bytes.NewReader(b)) + for s.Scan() { + l := s.Text() + switch { + case strings.HasPrefix(l, "Test"): + case strings.HasPrefix(l, "Benchmark"): + case strings.HasPrefix(l, "Example"): + case strings.HasPrefix(l, "Fuzz"): + case strings.HasPrefix(l, "? "): + continue + case strings.HasPrefix(l, "ok "): + continue + default: + return nil, fmt.Errorf("can't parse line %q", l) + } + + if _, dup := tests[l]; dup { + return nil, fmt.Errorf("duplicate test name %q", l) + } + + tests[l] = struct{}{} } - if strings.Contains(status, "github.com/FerretDB/FerretDB/integration") { - tests = tests[:len(tests)-1] + if err := s.Err(); err != nil { + return nil, err } - sort.Strings(tests) + res := maps.Keys(tests) + sort.Strings(res) - return tests, nil + return res, nil } -// getNewWorkingDir gets the new working directory based on the repo root directory and passed destination. -func getNewWorkingDir(dest string) (string, error) { - var rootDir, newWorkingDir string - - rootDir, err := getRootDir() - if err != nil { - return rootDir, err +// shardTests shards given test names. +func shardTests(index, total uint, tests []string) ([]string, error) { + if index >= total { + return nil, fmt.Errorf("cannot shard when index is greater or equal to total (%d >= %d)", index, total) } - newWorkingDir = path.Join(rootDir, dest) - if _, err := os.Stat(newWorkingDir); err != nil { - return newWorkingDir, err + testsLen := uint(len(tests)) + if total > testsLen { + return nil, fmt.Errorf("cannot shard when Total is greater than amount of tests (%d > %d)", total, testsLen) } - return newWorkingDir, nil -} + // use different shards for tests with similar names for better load balancing + res := make([]string, 0, testsLen/total) + var test, shard uint + for { + if test == testsLen { + return res, nil + } -// getRootDir gets the repository's root directory. -func getRootDir() (string, error) { - var rootDir string + if index == shard { + res = append(res, tests[test]) + } - workingDir, err := os.Getwd() - if err != nil { - return rootDir, err - } - - if strings.Contains(workingDir, "envtool") { - rootDir = path.Join(workingDir, "..", "..") - return rootDir, nil + test++ + shard = (shard + 1) % total } - - return workingDir, nil } diff --git a/cmd/envtool/tests_test.go b/cmd/envtool/tests_test.go index 933375ba1854..4d0ec704678f 100644 --- a/cmd/envtool/tests_test.go +++ b/cmd/envtool/tests_test.go @@ -15,168 +15,49 @@ package main import ( - "bytes" - "os" - "strconv" - "strings" + "path/filepath" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestShardIntegrationTests(t *testing.T) { +func TestTestsShard(t *testing.T) { t.Parallel() - t.Run("Successfully Shard Integration tests", func(t *testing.T) { - var w bytes.Buffer - err := shardIntegrationTests(&w, 0, 10) - assert.NoError(t, err) - assert.NotNil(t, w.Bytes()) - s := w.String() - assert.Regexp(t, "^\\^.*\\$$", s) - }) - - t.Run("Fail Shard Integration tests - too big index", func(t *testing.T) { - var w bytes.Buffer - err := shardIntegrationTests(&w, 12, 10) - assert.EqualError(t, err, "Cannot shard when Index is greater or equal to Total (12 >= 10)") - assert.Nil(t, w.Bytes()) - }) - - t.Run("Failp Shard Integration tests - too big index", func(t *testing.T) { - var w bytes.Buffer - err := shardIntegrationTests(&w, 12, 100000000000000) - assert.ErrorContains(t, err, "Cannot shard when Total is greater than amount of tests") - assert.Nil(t, w.Bytes()) - }) -} - -func TestShardTests(t *testing.T) { - t.Parallel() - - t.Run("Shard odd amount of tests into 2 parts", func(t *testing.T) { - tests := []string{} - for i := 0; i < 10; i++ { - tests = append(tests, strconv.Itoa(i)) - } - - testNames, err := shardTests(0, 2, tests...) - assert.NoError(t, err) - assert.Len(t, testNames, 5) - assert.Equal(t, tests[0:5], testNames) + tests, err := getAllTestNames(filepath.Join("..", "..", "integration")) + require.NoError(t, err) + assert.Contains(t, tests, "TestQueryCompatLimit") + assert.Contains(t, tests, "TestInit") // shareddata's - testNames, err = shardTests(1, 2, tests...) - assert.NoError(t, err) - assert.Len(t, testNames, 5) - assert.Equal(t, tests[5:], testNames) - }) - - t.Run("Shard even amount of tests into 2 parts", func(t *testing.T) { - tests := []string{} - for i := 0; i < 11; i++ { - tests = append(tests, strconv.Itoa(i)) - } - - testNames, err := shardTests(0, 2, tests...) - assert.NoError(t, err) - assert.Len(t, testNames, 5) - assert.Equal(t, tests[0:5], testNames) - - testNames, err = shardTests(1, 2, tests...) - assert.NoError(t, err) - assert.Len(t, testNames, 6) - assert.Equal(t, tests[5:], testNames) - }) + t.Run("ShardTestsInvalidIndex", func(t *testing.T) { + t.Parallel() - t.Run("Shard one test when total is 2", func(t *testing.T) { - tests := []string{"1"} - - testNames, err := shardTests(0, 2, tests...) - assert.EqualError(t, err, "Cannot shard when Total is greater than amount of tests (2 > 1)") - assert.Nil(t, testNames) + _, err := shardTests(3, 3, tests) + assert.EqualError(t, err, "cannot shard when index is greater or equal to total (3 >= 3)") }) - t.Run("Shard empty tests", func(t *testing.T) { - tests := []string{} + t.Run("ShardTestsInvalidTotal", func(t *testing.T) { + t.Parallel() - testNames, err := shardTests(0, 2, tests...) - assert.EqualError(t, err, "Cannot shard when Total is greater than amount of tests (2 > 0)") - assert.Nil(t, testNames) + _, err := shardTests(3, 1000, tests[:42]) + assert.EqualError(t, err, "cannot shard when Total is greater than amount of tests (1000 > 42)") }) - t.Run("Index is greater than Total", func(t *testing.T) { - tests := []string{} - - testNames, err := shardTests(2, 0, tests...) - assert.EqualError(t, err, "Cannot shard when Index is greater or equal to Total (2 >= 0)") - assert.Nil(t, testNames) - }) + t.Run("ShardTestsValid", func(t *testing.T) { + t.Parallel() - t.Run("Index is equal to Total", func(t *testing.T) { - tests := []string{} + res, err := shardTests(0, 3, tests) + require.NoError(t, err) + assert.Equal(t, tests[0], res[0]) + assert.NotEqual(t, tests[1], res[1]) + assert.NotEqual(t, tests[2], res[1]) + assert.Equal(t, tests[3], res[1]) - testNames, err := shardTests(0, 0, tests...) - assert.EqualError(t, err, "Cannot shard when Index is greater or equal to Total (0 >= 0)") - assert.Nil(t, testNames) + res, err = shardTests(2, 3, tests) + require.NoError(t, err) + assert.NotEqual(t, tests[0], res[0]) + assert.NotEqual(t, tests[1], res[0]) + assert.Equal(t, tests[2], res[0]) }) } - -func TestGetAllTestNames(t *testing.T) { - t.Parallel() - - testNames, err := getAllTestNames("integration") - assert.NoError(t, err) - assert.NotEmpty(t, testNames) -} - -func TestPrepareTestNamesOutput(t *testing.T) { - t.Parallel() - - t.Run("Fails when the status is not OK", func(t *testing.T) { - output := `TestAggregateCommandCompat -fail github.com/FerretDB/FerretDB/integration 0.023s` - - tests, err := prepareTestNamesOutput(output) - assert.ErrorContains(t, err, "Could not read test names:") - assert.Nil(t, tests) - }) - - t.Run("Passes when the status is OK", func(t *testing.T) { - output := `TestAggregateCommandCompat -TestAggregateCompatStages -BenchmarkInsertMany -ok github.com/FerretDB/FerretDB/integration 0.023s` - expectedOutput := []string{"BenchmarkInsertMany", "TestAggregateCommandCompat", "TestAggregateCompatStages"} - - tests, err := prepareTestNamesOutput(output) - assert.NoError(t, err) - assert.Equal(t, tests, expectedOutput) - }) -} - -func TestGetNewWorkingDir(t *testing.T) { - t.Parallel() - - // while running the tests the current working location is where the test is - oldWorkingDir, err := os.Getwd() - assert.NoError(t, err) - dir, err := getNewWorkingDir("integration") - assert.NoError(t, err) - assert.NotEqual(t, oldWorkingDir, dir) - assert.Contains(t, dir, "integration") - assert.NotContains(t, dir, "envtool") - assert.True(t, strings.HasSuffix(dir, "/FerretDB/integration")) -} - -func TestGetRootDir(t *testing.T) { - t.Parallel() - - oldWorkingDir, err := os.Getwd() - assert.NoError(t, err) - rootDir, err := getRootDir() - assert.NoError(t, err) - assert.NotEqual(t, oldWorkingDir, rootDir) - assert.NotContains(t, rootDir, "integration") - assert.NotContains(t, rootDir, "envtool") - assert.True(t, strings.HasSuffix(rootDir, "/FerretDB")) -} From 1d8379de5ac60eb5f2aedf51ddf32a764d2b7ec3 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Thu, 8 Jun 2023 16:56:11 +0400 Subject: [PATCH 2/8] Fix CI concurrency --- .github/workflows/go.yml | 12 ++++++------ Taskfile.yml | 10 +++++----- cmd/envtool/tests.go | 17 ++++++++++++----- cmd/envtool/tests_test.go | 16 +++++++++++----- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 15384581989d..698c23c580b6 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -141,15 +141,15 @@ jobs: git diff --exit-code integration: - name: Integration ${{ matrix.name }} + name: Integration ${{ matrix.name }} ${{ matrix.shard_index }}/${{ matrix.shard_total }} runs-on: group: paid - timeout-minutes: 45 + timeout-minutes: 15 # Do not run this job in parallel for any PR change or branch/tag push # to save some resources. concurrency: - group: ${{ github.workflow }}-${{ matrix.name }}-${{ github.head_ref || github.ref_name }} + group: ${{ github.workflow }}-${{ matrix.name }}-${{ matrix.shard_index }}-${{ matrix.shard_total }}-${{ github.head_ref || github.ref_name }} cancel-in-progress: true if: github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'not ready') @@ -158,11 +158,11 @@ jobs: fail-fast: false matrix: include: - - { name: "MongoDB", task: "mongodb", shard_index: 0, shard_total: 1 } - - { name: "PostgreSQL", task: "pg", shard_index: 0, shard_total: 3 } + - { name: "MongoDB", task: "mongodb", shard_index: 1, shard_total: 1 } - { name: "PostgreSQL", task: "pg", shard_index: 1, shard_total: 3 } - { name: "PostgreSQL", task: "pg", shard_index: 2, shard_total: 3 } - - { name: "SQLite", task: "sqlite", shard_index: 0, shard_total: 1 } + - { name: "PostgreSQL", task: "pg", shard_index: 3, shard_total: 3 } + - { name: "SQLite", task: "sqlite", shard_index: 1, shard_total: 1 } steps: - name: Checkout code diff --git a/Taskfile.yml b/Taskfile.yml index 644e2fea1b02..2bdc0a42b9ba 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -197,7 +197,7 @@ tasks: -compat-url='mongodb://username:password@127.0.0.1:47018/?tls=true&tlsCertificateKeyFile=../build/certs/client.pem&tlsCaFile=../build/certs/rootCA-cert.pem' vars: RUN: - sh: go run -C .. ./cmd/envtool tests shard --index={{.SHARD_INDEX | default 0}} --total={{.SHARD_TOTAL | default 1}} + sh: go run -C .. ./cmd/envtool tests shard --index={{.SHARD_INDEX | default 1}} --total={{.SHARD_TOTAL | default 1}} test-integration-sqlite: desc: "Run integration tests for `sqlite` handler" @@ -212,7 +212,7 @@ tasks: -disable-filter-pushdown vars: RUN: - sh: go run -C .. ./cmd/envtool tests shard --index={{.SHARD_INDEX | default 0}} --total={{.SHARD_TOTAL | default 1}} + sh: go run -C .. ./cmd/envtool tests shard --index={{.SHARD_INDEX | default 1}} --total={{.SHARD_TOTAL | default 1}} test-integration-tigris: desc: "Run integration tests for `tigris` handler" @@ -227,7 +227,7 @@ tasks: -compat-url=mongodb://127.0.0.1:47017/ vars: RUN: - sh: go run -C .. ./cmd/envtool tests shard --index={{.SHARD_INDEX | default 0}} --total={{.SHARD_TOTAL | default 1}} + sh: go run -C .. ./cmd/envtool tests shard --index={{.SHARD_INDEX | default 1}} --total={{.SHARD_TOTAL | default 1}} test-integration-hana: desc: "Run integration tests for `hana` handler" @@ -242,7 +242,7 @@ tasks: -compat-url=mongodb://127.0.0.1:47017/ vars: RUN: - sh: go run -C .. ./cmd/envtool tests shard --index={{.SHARD_INDEX | default 0}} --total={{.SHARD_TOTAL | default 1}} + sh: go run -C .. ./cmd/envtool tests shard --index={{.SHARD_INDEX | default 1}} --total={{.SHARD_TOTAL | default 1}} test-integration-mongodb: desc: "Run integration tests for MongoDB" @@ -255,7 +255,7 @@ tasks: -target-backend=mongodb vars: RUN: - sh: go run -C .. ./cmd/envtool tests shard --index={{.SHARD_INDEX | default 0}} --total={{.SHARD_TOTAL | default 1}} + sh: go run -C .. ./cmd/envtool tests shard --index={{.SHARD_INDEX | default 1}} --total={{.SHARD_TOTAL | default 1}} bench-unit-short: desc: "Benchmark internal package for about 25 seconds (with default BENCHTIME)" diff --git a/cmd/envtool/tests.go b/cmd/envtool/tests.go index baa841d48bff..498d0aa07a6e 100644 --- a/cmd/envtool/tests.go +++ b/cmd/envtool/tests.go @@ -97,18 +97,25 @@ func getAllTestNames(dir string) ([]string, error) { // shardTests shards given test names. func shardTests(index, total uint, tests []string) ([]string, error) { - if index >= total { - return nil, fmt.Errorf("cannot shard when index is greater or equal to total (%d >= %d)", index, total) + if index == 0 { + return nil, fmt.Errorf("index must be greater than 0") + } + if total == 0 { + return nil, fmt.Errorf("total must be greater than 0") + } + if index > total { + return nil, fmt.Errorf("cannot shard when index is greater to total (%d > %d)", index, total) } testsLen := uint(len(tests)) if total > testsLen { - return nil, fmt.Errorf("cannot shard when Total is greater than amount of tests (%d > %d)", total, testsLen) + return nil, fmt.Errorf("cannot shard when total is greater than amount of tests (%d > %d)", total, testsLen) } // use different shards for tests with similar names for better load balancing res := make([]string, 0, testsLen/total) - var test, shard uint + var test uint + var shard uint = 1 for { if test == testsLen { return res, nil @@ -119,6 +126,6 @@ func shardTests(index, total uint, tests []string) ([]string, error) { } test++ - shard = (shard + 1) % total + shard = shard%total + 1 } } diff --git a/cmd/envtool/tests_test.go b/cmd/envtool/tests_test.go index 4d0ec704678f..6db16e024767 100644 --- a/cmd/envtool/tests_test.go +++ b/cmd/envtool/tests_test.go @@ -33,28 +33,34 @@ func TestTestsShard(t *testing.T) { t.Run("ShardTestsInvalidIndex", func(t *testing.T) { t.Parallel() - _, err := shardTests(3, 3, tests) - assert.EqualError(t, err, "cannot shard when index is greater or equal to total (3 >= 3)") + _, err := shardTests(0, 3, tests) + assert.EqualError(t, err, "index must be greater than 0") + + _, err = shardTests(3, 3, tests) + assert.NoError(t, err) + + _, err = shardTests(4, 3, tests) + assert.EqualError(t, err, "cannot shard when index is greater to total (4 > 3)") }) t.Run("ShardTestsInvalidTotal", func(t *testing.T) { t.Parallel() _, err := shardTests(3, 1000, tests[:42]) - assert.EqualError(t, err, "cannot shard when Total is greater than amount of tests (1000 > 42)") + assert.EqualError(t, err, "cannot shard when total is greater than amount of tests (1000 > 42)") }) t.Run("ShardTestsValid", func(t *testing.T) { t.Parallel() - res, err := shardTests(0, 3, tests) + res, err := shardTests(1, 3, tests) require.NoError(t, err) assert.Equal(t, tests[0], res[0]) assert.NotEqual(t, tests[1], res[1]) assert.NotEqual(t, tests[2], res[1]) assert.Equal(t, tests[3], res[1]) - res, err = shardTests(2, 3, tests) + res, err = shardTests(3, 3, tests) require.NoError(t, err) assert.NotEqual(t, tests[0], res[0]) assert.NotEqual(t, tests[1], res[0]) From c90287e9ac07ae48a4f614f3816bd7e28daeb718 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Thu, 8 Jun 2023 17:06:39 +0400 Subject: [PATCH 3/8] Lint --- cmd/envtool/tests.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/cmd/envtool/tests.go b/cmd/envtool/tests.go index 498d0aa07a6e..9ec695de5ebc 100644 --- a/cmd/envtool/tests.go +++ b/cmd/envtool/tests.go @@ -39,12 +39,15 @@ func testsShard(w io.Writer, index, total uint) error { } fmt.Fprint(w, "^(") + for i, t := range sharded { fmt.Fprint(w, t) + if i != len(sharded)-1 { fmt.Fprint(w, "|") } } + fmt.Fprint(w, ")$") return nil @@ -60,11 +63,12 @@ func getAllTestNames(dir string) ([]string, error) { return nil, err } - tests := make(map[string]struct{}) + tests := make(map[string]struct{}, 200) s := bufio.NewScanner(bytes.NewReader(b)) for s.Scan() { l := s.Text() + switch { case strings.HasPrefix(l, "Test"): case strings.HasPrefix(l, "Benchmark"): @@ -100,9 +104,11 @@ func shardTests(index, total uint, tests []string) ([]string, error) { if index == 0 { return nil, fmt.Errorf("index must be greater than 0") } + if total == 0 { return nil, fmt.Errorf("total must be greater than 0") } + if index > total { return nil, fmt.Errorf("cannot shard when index is greater to total (%d > %d)", index, total) } @@ -112,10 +118,11 @@ func shardTests(index, total uint, tests []string) ([]string, error) { return nil, fmt.Errorf("cannot shard when total is greater than amount of tests (%d > %d)", total, testsLen) } - // use different shards for tests with similar names for better load balancing res := make([]string, 0, testsLen/total) var test uint - var shard uint = 1 + shard := uint(1) + + // use different shards for tests with similar names for better load balancing for { if test == testsLen { return res, nil From f6ee263be2d535886a53c3ca8430896670d3a4e7 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Thu, 8 Jun 2023 17:08:02 +0400 Subject: [PATCH 4/8] Simplify --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 698c23c580b6..acdd8058a666 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -149,7 +149,7 @@ jobs: # Do not run this job in parallel for any PR change or branch/tag push # to save some resources. concurrency: - group: ${{ github.workflow }}-${{ matrix.name }}-${{ matrix.shard_index }}-${{ matrix.shard_total }}-${{ github.head_ref || github.ref_name }} + group: ${{ github.workflow }}-${{ matrix.name }}-${{ matrix.shard_index }}-${{ github.head_ref || github.ref_name }} cancel-in-progress: true if: github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'not ready') From daf24e5a6e44ed79d436870694b9b639e4c3e5bb Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Thu, 8 Jun 2023 17:11:41 +0400 Subject: [PATCH 5/8] Fix flags --- .github/workflows/go.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index acdd8058a666..f23197b580ba 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -204,7 +204,7 @@ jobs: with: token: 22159d7c-856d-4fe9-8fdb-5d9ecff35514 files: ./integration/integration-${{ matrix.task }}.txt - flags: integration,${{ matrix.task }} + flags: integration,${{ matrix.task }},shard-${{ matrix.shard_index }} fail_ci_if_error: true verbose: true @@ -212,7 +212,7 @@ jobs: uses: coverallsapp/github-action@v2 with: file: ./integration/integration-${{ matrix.task }}.txt - flag-name: integration-${{ matrix.task }} + flag-name: integration-${{ matrix.task }}-${{ matrix.shard_index }} parallel: true # we don't want them on CI From bf2fa1da442e89cbe1c70e5b3b526838f03b17bb Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Thu, 8 Jun 2023 17:31:44 +0400 Subject: [PATCH 6/8] Minor tweaks --- .github/workflows/go.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index f23197b580ba..ca0836c3960b 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -144,7 +144,7 @@ jobs: name: Integration ${{ matrix.name }} ${{ matrix.shard_index }}/${{ matrix.shard_total }} runs-on: group: paid - timeout-minutes: 15 + timeout-minutes: 20 # Do not run this job in parallel for any PR change or branch/tag push # to save some resources. @@ -189,7 +189,7 @@ jobs: - name: Wait for and setup environment run: bin/task env-setup - - name: Run ${{ matrix.task }} tests + - name: Run ${{ matrix.name }} tests (${{ matrix.shard_index }}/${{ matrix.shard_total }}) run: bin/task test-integration-${{ matrix.task }} SHARD_INDEX=${{ matrix.shard_index }} SHARD_TOTAL=${{ matrix.shard_total }} # The token is not required but should make uploads more stable. @@ -231,7 +231,7 @@ jobs: runs-on: ubuntu-22.04 needs: [test, integration] - if: github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'not ready') + if: always() && (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'not ready')) steps: - name: Submit coveralls From 7b1f7f68d427e19d6b7db88f58d891e0576078bd Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Thu, 8 Jun 2023 17:51:00 +0400 Subject: [PATCH 7/8] Fix help --- cmd/envtool/envtool.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/envtool/envtool.go b/cmd/envtool/envtool.go index 0d8e08c20ab5..33ec5d4f1e09 100644 --- a/cmd/envtool/envtool.go +++ b/cmd/envtool/envtool.go @@ -403,7 +403,7 @@ var cli struct { PackageVersion struct{} `cmd:"" help:"Print package version"` Tests struct { Shard struct { - Index uint `help:"Shard index, starting from 0" required:""` + Index uint `help:"Shard index, starting from 1" required:""` Total uint `help:"Total number of shards" required:""` } `cmd:"" help:"Print sharded integration tests"` } `cmd:""` From ec829e29d0289916a76841a0f90b3fb9dffb3e09 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Thu, 8 Jun 2023 18:08:35 +0400 Subject: [PATCH 8/8] Shard SQLite tests too --- .github/workflows/go.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index ca0836c3960b..6542f6bbf5aa 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -162,7 +162,9 @@ jobs: - { name: "PostgreSQL", task: "pg", shard_index: 1, shard_total: 3 } - { name: "PostgreSQL", task: "pg", shard_index: 2, shard_total: 3 } - { name: "PostgreSQL", task: "pg", shard_index: 3, shard_total: 3 } - - { name: "SQLite", task: "sqlite", shard_index: 1, shard_total: 1 } + - { name: "SQLite", task: "sqlite", shard_index: 1, shard_total: 3 } + - { name: "SQLite", task: "sqlite", shard_index: 2, shard_total: 3 } + - { name: "SQLite", task: "sqlite", shard_index: 3, shard_total: 3 } steps: - name: Checkout code