diff --git a/.github/workflows/dance.yml b/.github/workflows/dance.yml index 14fde31ec..8dadfc65d 100644 --- a/.github/workflows/dance.yml +++ b/.github/workflows/dance.yml @@ -23,7 +23,7 @@ env: jobs: dance: - name: dance + name: ${{ matrix.db }} ${{ matrix.project }} # https://www.ubicloud.com/docs/github-actions-integration/price-performance#usage-pricing # https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#per-minute-rates diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 000000000..7c7767320 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,97 @@ +--- +name: Go +on: + pull_request: + types: + - unlabeled # if GitHub Actions stuck, add and remove "not ready" label to force rebuild + - opened + - reopened + - synchronize + push: + branches: + - main + schedule: + - cron: "12 3 * * *" + +env: + GOPATH: /home/runner/go + GOCACHE: /home/runner/go/cache + GOLANGCI_LINT_CACHE: /home/runner/go/cache/lint + GOMODCACHE: /home/runner/go/mod + GOPROXY: https://proxy.golang.org + GOTOOLCHAIN: local + +jobs: + tests: + name: Tests + runs-on: ubuntu-22.04 + timeout-minutes: 10 + + # Do not run this job in parallel for any PR change or branch push. + concurrency: + group: ${{ github.workflow }}-tests-${{ 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') + + steps: + # TODO https://github.com/FerretDB/github-actions/issues/211 + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go + uses: FerretDB/github-actions/setup-go@main + with: + cache-key: tests + + - name: Install Task + run: go generate -x + working-directory: tools + + - name: Run init + run: bin/task init + + - name: Run tests + run: bin/task test + env: + GOFLAGS: ${{ runner.debug == '1' && '-v' || '' }} + + # we don't want them on CI + - name: Clean test and fuzz caches + if: always() + run: go clean -testcache -fuzzcache + + - name: Check dirty + run: | + git status + git diff --exit-code + + linters: + name: Linters + runs-on: ubuntu-22.04 + timeout-minutes: 5 + + # Do not run this job in parallel for any PR change or branch push. + concurrency: + group: ${{ github.workflow }}-linters-${{ 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') + + steps: + # TODO https://github.com/FerretDB/github-actions/issues/211 + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # for `golangci-lint run --new` to work + + - name: Setup Go + uses: FerretDB/github-actions/setup-go@main + with: + cache-key: linters + + - name: Run linters + uses: FerretDB/github-actions/linters@main + + - name: Format and lint documentation + run: bin/task docs-fmt diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml deleted file mode 100644 index cf54a056b..000000000 --- a/.github/workflows/linters.yml +++ /dev/null @@ -1,52 +0,0 @@ ---- -name: Linters -on: - push: - branches: - - main - pull_request: - types: - - unlabeled # if GitHub Actions stuck, add and remove "not ready" label to force rebuild - - opened - - reopened - - synchronize - schedule: - - cron: "12 3 * * *" - -# Do not run this job in parallel for any PR change or branch push. -concurrency: - group: ${{ github.workflow }}-golangci-lint-${{ github.head_ref || github.ref_name }} - cancel-in-progress: true - -env: - GOPATH: /home/runner/go - GOCACHE: /home/runner/go/cache - GOLANGCI_LINT_CACHE: /home/runner/go/cache/lint - GOMODCACHE: /home/runner/go/mod - GOPROXY: https://proxy.golang.org - GOTOOLCHAIN: local - -jobs: - linters: - name: linters - runs-on: ubuntu-22.04 - timeout-minutes: 5 - - if: github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'not ready') - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Go - uses: FerretDB/github-actions/setup-go@main - with: - cache-key: lint - - - name: Run linters - uses: FerretDB/github-actions/linters@main - - - name: Format and lint documentation - run: bin/task docs-fmt diff --git a/Taskfile.yaml b/Taskfile.yaml index 846325c24..0a695db63 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -4,6 +4,7 @@ version: "3" vars: DB: "" CONFIG: "" + RACE_FLAG: -race={{and (ne OS "windows") (ne ARCH "arm") (ne ARCH "riscv64")}} tasks: init-tools: @@ -64,6 +65,11 @@ tasks: cmds: - go build -v -o bin/ ./cmd/dance/ + test: + desc: "Run unit tests (with caching)" + cmds: + - go test {{.RACE_FLAG}} -shuffle=on ./internal/... + dance: desc: "Dance!" deps: [build] diff --git a/cmd/dance/main.go b/cmd/dance/main.go index 74fbbf91d..83e7d7d0e 100644 --- a/cmd/dance/main.go +++ b/cmd/dance/main.go @@ -169,7 +169,7 @@ func main() { switch c.Runner { case config.RunnerTypeCommand: - runner, err = command.New(c.Params.(*config.RunnerParamsCommand), rl) + runner, err = command.New(c.Params.(*config.RunnerParamsCommand), rl, cli.Verbose) case config.RunnerTypeGoTest: fallthrough case config.RunnerTypeJSTest: diff --git a/internal/runner/command/command.go b/internal/runner/command/command.go index 46b96ca64..080ffc687 100644 --- a/internal/runner/command/command.go +++ b/internal/runner/command/command.go @@ -18,6 +18,7 @@ package command import ( "context" "fmt" + "io" "log/slog" "os" "os/exec" @@ -29,21 +30,23 @@ import ( // command represents a generic test runner. type command struct { - p *config.RunnerParamsCommand - l *slog.Logger + p *config.RunnerParamsCommand + l *slog.Logger + verbose bool } // New creates a new `command` runner with given parameters. -func New(params *config.RunnerParamsCommand, l *slog.Logger) (runner.Runner, error) { +func New(params *config.RunnerParamsCommand, l *slog.Logger, verbose bool) (runner.Runner, error) { return &command{ - p: params, - l: l, + p: params, + l: l, + verbose: verbose, }, nil } // execScripts stores the given shell script content in dir/file-XXX.sh and executes it. // It returns the combined output of the script execution. -func execScript(ctx context.Context, dir, file, content string) ([]byte, error) { +func execScript(ctx context.Context, dir, file, content string, verbose bool) ([]byte, error) { f, err := os.CreateTemp(dir, file+"-*.sh") if err != nil { return nil, err @@ -70,7 +73,21 @@ func execScript(ctx context.Context, dir, file, content string) ([]byte, error) cmd := exec.CommandContext(ctx, "./"+filepath.Base(f.Name())) cmd.Dir = dir - return cmd.CombinedOutput() + var b runner.LockedBuffer + + if verbose { + cmd.Stdout = io.MultiWriter(&b, os.Stdout) + cmd.Stderr = io.MultiWriter(&b, os.Stderr) + } else { + cmd.Stdout = &b + cmd.Stderr = &b + } + + if err = cmd.Run(); err != nil { + return nil, err + } + + return b.Bytes(), nil } // Run implements [runner.Runner] interface. @@ -78,7 +95,7 @@ func (c *command) Run(ctx context.Context) (map[string]config.TestResult, error) if c.p.Setup != "" { c.l.InfoContext(ctx, "Running setup") - b, err := execScript(ctx, c.p.Dir, "setup", c.p.Setup) + b, err := execScript(ctx, c.p.Dir, "setup", c.p.Setup, c.verbose) if err != nil { return nil, fmt.Errorf("%s\n%w", b, err) } @@ -89,7 +106,7 @@ func (c *command) Run(ctx context.Context) (map[string]config.TestResult, error) for _, t := range c.p.Tests { c.l.InfoContext(ctx, "Running test", slog.String("test", t.Name)) - b, err := execScript(ctx, c.p.Dir, t.Name, t.Cmd) + b, err := execScript(ctx, c.p.Dir, t.Name, t.Cmd, c.verbose) tc := config.TestResult{ Status: config.Pass, diff --git a/internal/runner/command/command_test.go b/internal/runner/command/command_test.go index ca4340a0c..5d0fef1d8 100644 --- a/internal/runner/command/command_test.go +++ b/internal/runner/command/command_test.go @@ -31,7 +31,7 @@ func TestCommand(t *testing.T) { p := &config.RunnerParamsCommand{ Setup: "exit 1", } - c, err := New(p, slog.Default()) + c, err := New(p, slog.Default(), true) require.NoError(t, err) ctx := context.Background() diff --git a/internal/runner/locked_buffer.go b/internal/runner/locked_buffer.go new file mode 100644 index 000000000..081874779 --- /dev/null +++ b/internal/runner/locked_buffer.go @@ -0,0 +1,42 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runner + +import ( + "bytes" + "sync" +) + +// LockedBuffer is a thread-safe [bytes.Buffer]. +type LockedBuffer struct { + b bytes.Buffer + m sync.Mutex +} + +// Bytes calls [bytes.Buffer.Write] in a thread-safe way. +func (lb *LockedBuffer) Write(p []byte) (int, error) { + lb.m.Lock() + defer lb.m.Unlock() + + return lb.b.Write(p) +} + +// Bytes calls [bytes.Buffer.Bytes] in a thread-safe way. +func (lb *LockedBuffer) Bytes() []byte { + lb.m.Lock() + defer lb.m.Unlock() + + return lb.b.Bytes() +}