From 033006aade63e006f2f6f29f88d9f1a3fc02b7fb Mon Sep 17 00:00:00 2001 From: Adam Babik Date: Tue, 11 Jun 2024 18:22:04 +0200 Subject: [PATCH] Add a Docker image capable of running tests (#608) This change adds a new GHA workflow that runs tests in a Docker container. It appears as a new check but it's not required, for now. Other changes: * GoReleaser update * Docker image release in GoReleaser config * Moving of tests setup from Makefile entirely to Go Closes https://github.com/stateful/runme/issues/605 --- .github/workflows/ci.yml | 13 ++ .github/workflows/release.yml | 25 +++- .goreleaser.yml | 111 ++++++++++-------- CONTRIBUTING.md | 24 +++- Dockerfile.alpine | 10 -- Dockerfile.ubuntu | 8 -- Makefile | 54 +++++---- docker/.dockerignore | 5 + docker/alpine.Dockerfile | 13 ++ docker/runme-test-env.Dockerfile | 41 +++++++ docker/runme-test-env.Dockerfile.dockerignore | 5 + docker/ubuntu.Dockerfile | 13 ++ experimental/runme.yaml | 2 +- go.mod | 1 + go.sum | 4 + internal/command/factory.go | 11 +- internal/project/project_test.go | 67 ++++++----- .../projectservice/project_service_test.go | 32 ++--- internal/project/testdata/doc.go | 3 - internal/project/testdata/testdata.go | 56 --------- internal/project/testdata/testdata_test.go | 24 ---- internal/project/teststub/teststub.go | 76 ++++++++++++ .../runnerv2service/service_execute_test.go | 14 ++- .../runnerv2service/service_sessions_test.go | 7 +- main_test.go | 15 ++- testdata/beta/server.txtar | 2 +- testdata/script/basic.txtar | 16 +-- 27 files changed, 402 insertions(+), 250 deletions(-) delete mode 100644 Dockerfile.alpine delete mode 100644 Dockerfile.ubuntu create mode 100644 docker/.dockerignore create mode 100644 docker/alpine.Dockerfile create mode 100644 docker/runme-test-env.Dockerfile create mode 100644 docker/runme-test-env.Dockerfile.dockerignore create mode 100644 docker/ubuntu.Dockerfile delete mode 100644 internal/project/testdata/doc.go delete mode 100644 internal/project/testdata/testdata.go delete mode 100644 internal/project/testdata/testdata_test.go create mode 100644 internal/project/teststub/teststub.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17399d150..8798a6cb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,6 +81,19 @@ jobs: path: cover.out if-no-files-found: error + test-in-docker: + name: Test in Docker + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Test + run: make test-docker + build-and-robustness-test: name: Test parser against vast amount of READMEs runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2203ed076..d98c559a8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Prepare id: prepare run: | @@ -28,29 +29,41 @@ jobs: if [[ "$ref_name" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then echo "pre_release=false" >> $GITHUB_ENV fi + - name: Set up Go uses: actions/setup-go@v4 with: go-version: "1.22" + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + - name: Auth to GCP uses: google-github-actions/auth@v2 with: credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT_KEY }} create_credentials_file: true export_environment_variables: true - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} + - name: Set up gcloud uses: google-github-actions/setup-gcloud@v2 + - name: Release notes run: | owner="${{ vars.RELEASE_OWNER || github.actor }}" go run ./cmd/release-notes/main.go -version "${GITHUB_REF_NAME}" -owner "$owner" > ${{ runner.temp }}/releasenotes env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Run GoReleaser uses: goreleaser/goreleaser-action@v5 with: @@ -62,9 +75,11 @@ jobs: HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} SCOOP_BUCKET_GITHUB_TOKEN: ${{ secrets.SCOOP_BUCKET_GITHUB_TOKEN }} GS_BUCKET: ${{ secrets.GS_BUCKET }} + - name: Copy to latest if: env.pre_release == 'false' run: gsutil -m cp "gs://${{ secrets.GS_BUCKET }}/${{ env.version }}/*" gs://${{ secrets.GS_BUCKET }}/latest + - name: Bump Homebrew Formula uses: mislav/bump-homebrew-formula-action@v3 # skip prereleases diff --git a/.goreleaser.yml b/.goreleaser.yml index 3a4a15e5b..ba2f0019f 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -82,7 +82,7 @@ blobs: bucket: "{{ .Env.GS_BUCKET }}" ids: - cli - folder: "{{ .Version }}" + directory: "{{ .Version }}" brews: - name: runme @@ -90,14 +90,14 @@ brews: - cli homepage: https://runme.dev description: "Execute your runbooks, docs, and READMEs." - tap: + repository: owner: stateful name: homebrew-tap token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" url_template: "https://download.stateful.com/runme/{{ .Version }}/{{ .ArtifactName }}" dependencies: [] skip_upload: auto - folder: Formula + directory: Formula download_strategy: CurlDownloadStrategy commit_author: name: release-bot @@ -131,70 +131,85 @@ nfpms: - apk - rpm -scoop: - url_template: "https://download.stateful.com/runme/{{ .Version }}/{{ .ArtifactName }}" - bucket: - owner: stateful - name: scoop-bucket - token: "{{ .Env.SCOOP_BUCKET_GITHUB_TOKEN }}" - commit_author: - name: release-bot - email: bot@stateful.com - homepage: https://runme.dev - description: "Execute your runbooks, docs, and READMEs." - skip_upload: auto +scoops: + - url_template: "https://download.stateful.com/runme/{{ .Version }}/{{ .ArtifactName }}" + repository: + owner: stateful + name: scoop-bucket + token: "{{ .Env.SCOOP_BUCKET_GITHUB_TOKEN }}" + commit_author: + name: release-bot + email: bot@stateful.com + homepage: https://runme.dev + description: "Execute your runbooks, docs, and READMEs." + skip_upload: auto dockers: - - goos: linux - goarch: amd64 - image_templates: - - "statefulhq/runme:latest" + - image_templates: - "statefulhq/runme:{{ .Version }}-alpine-amd64" - skip_push: auto - dockerfile: Dockerfile.alpine - - goos: linux + use: buildx + dockerfile: docker/alpine.Dockerfile + goos: linux goarch: amd64 - image_templates: - - "statefulhq/runme:latest" + build_flag_templates: + - "--platform=linux/amd64" + skip_push: auto + - image_templates: - "statefulhq/runme:{{ .Version }}-ubuntu-amd64" + use: buildx + dockerfile: docker/ubuntu.Dockerfile + goos: linux + goarch: amd64 + build_flag_templates: + - "--platform=linux/amd64" skip_push: auto - dockerfile: Dockerfile.ubuntu - - goos: linux + - image_templates: + - "statefulhq/runme:{{ .Version }}-alpine-arm64v8" + use: buildx + dockerfile: docker/alpine.Dockerfile + goos: linux goarch: arm64 - image_templates: - - "statefulhq/runme:{{ .Version }}-alpine-arm64" + build_flag_templates: + - "--platform=linux/arm64/v8" skip_push: auto - dockerfile: Dockerfile.alpine - - goos: linux + - image_templates: + - "statefulhq/runme:{{ .Version }}-ubuntu-arm64v8" + use: buildx + dockerfile: docker/ubuntu.Dockerfile + goos: linux goarch: arm64 - image_templates: - - "statefulhq/runme:{{ .Version }}-ubuntu-arm64" + build_flag_templates: + - "--platform=linux/arm64/v8" skip_push: auto - dockerfile: Dockerfile.ubuntu docker_manifests: - - name_template: 'statefulhq/runme:{{ .Version }}' + - name_template: "statefulhq/runme:latest" + image_templates: + - "statefulhq/runme:{{ .Version }}-alpine-amd64" + - "statefulhq/runme:{{ .Version }}-alpine-arm64v8" skip_push: auto + - name_template: "statefulhq/runme:latest-alpine" image_templates: - - 'statefulhq/runme:{{ .Version }}-alpine-amd64' - - 'statefulhq/runme:{{ .Version }}-alpine-arm64' - - name_template: 'statefulhq/runme:latest-alpine' + - "statefulhq/runme:{{ .Version }}-alpine-amd64" + - "statefulhq/runme:{{ .Version }}-alpine-arm64v8" skip_push: auto + - name_template: "statefulhq/runme:latest-ubuntu" image_templates: - - 'statefulhq/runme:{{ .Version }}-alpine-amd64' - - 'statefulhq/runme:{{ .Version }}-alpine-arm64' - - name_template: 'statefulhq/runme:latest-ubuntu' + - "statefulhq/runme:{{ .Version }}-ubuntu-amd64" + - "statefulhq/runme:{{ .Version }}-ubuntu-arm64v8" skip_push: auto + - name_template: "statefulhq/runme:{{ .Version }}" image_templates: - - 'statefulhq/runme:{{ .Version }}-ubuntu-amd64' - - 'statefulhq/runme:{{ .Version }}-ubuntu-arm64' - - name_template: 'statefulhq/runme:{{ .Version }}-alpine' + - "statefulhq/runme:{{ .Version }}-alpine-amd64" + - "statefulhq/runme:{{ .Version }}-alpine-arm64v8" + skip_push: auto + - name_template: "statefulhq/runme:{{ .Version }}-alpine" skip_push: auto image_templates: - - 'statefulhq/runme:{{ .Version }}-alpine-amd64' - - 'statefulhq/runme:{{ .Version }}-alpine-arm64' - - name_template: 'statefulhq/runme:{{ .Version }}-ubuntu' + - "statefulhq/runme:{{ .Version }}-alpine-amd64" + - "statefulhq/runme:{{ .Version }}-alpine-arm64v8" + - name_template: "statefulhq/runme:{{ .Version }}-ubuntu" skip_push: auto image_templates: - - 'statefulhq/runme:{{ .Version }}-ubuntu-amd64' - - 'statefulhq/runme:{{ .Version }}-ubuntu-arm64' + - "statefulhq/runme:{{ .Version }}-ubuntu-amd64" + - "statefulhq/runme:{{ .Version }}-ubuntu-arm64v8" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f1a949e5..5c74c3e40 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,7 +31,7 @@ To request a new feature you should open an [issue](../../issues/new) and summar This is an outline of what the workflow for code contributions looks like - Check the list of open [issues](../../issues). Either assign an existing issue to yourself, or - create a new one that you would like work on and discuss your ideas and use cases. + create a new one that you would like work on and discuss your ideas and use cases. It is always best to discuss your plans beforehand, to ensure that your contribution is in line with our goals. @@ -51,7 +51,7 @@ This project uses a `Makefile` to manage build scripts. You will need `make` ins You will need to have a `go` installation - ideally compatible with the project's current go version (see [go.mod](/go.mod)). -### OSX +### macOS If you are using [`homebrew`](https://brew.sh/), you can install the required system modules with the following command: @@ -145,19 +145,33 @@ pre-commit run --files */** ## Testing -Tests are run with go's default test runner. So, for example, you can run all tests with: +Tests are run with Go's default test runner wrapped in Makefile targets. So, for example, you can run all tests with: ```sh {"id":"01HF7BT3HEQBTBM9SSTS88ZSCF"} make test ``` -Generate HTML representation of coverage profile +Please notice that our tests include integration tests which depend on additional software like Python or node.js. If you don't want to install them or tests fail because of different versions, you can run all tests in a Docker container: + +```sh +make test-docker +``` + +### Coverage + +In order to generate a coverage report, run tests using + +```sh {"name":"coverage-run"} +make test-coverage +``` + +And then: ```sh {"id":"01HJVHEVPX2AZJ86999P1MY5H0","name":"coverage-html"} make test/coverage/html ``` -Output coverage profile information for each function +Output coverage profile information for each function: ```sh {"id":"01HJVHHNMZRNK0ZGA154A9AJCZ","name":"coverage-func"} make test/coverage/func diff --git a/Dockerfile.alpine b/Dockerfile.alpine deleted file mode 100644 index db5fffc17..000000000 --- a/Dockerfile.alpine +++ /dev/null @@ -1,10 +0,0 @@ -FROM alpine:latest as build -LABEL maintainer="StatefulHQ " - -RUN apk --no-cache add ca-certificates - -RUN mkdir -p /opt/var/runme /opt/bin -COPY runme /opt/bin/runme -WORKDIR /opt/var/runme - -ENTRYPOINT ["/opt/bin/runme"] diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu deleted file mode 100644 index 58adf3ba2..000000000 --- a/Dockerfile.ubuntu +++ /dev/null @@ -1,8 +0,0 @@ -FROM ubuntu:23.10 as build -LABEL maintainer="StatefulHQ " - -RUN mkdir -p /opt/var/runme /opt/bin -COPY runme /opt/bin/runme -WORKDIR /opt/var/runme - -ENTRYPOINT ["/opt/bin/runme"] diff --git a/Makefile b/Makefile index 7625de5a3..00ccc9e3e 100644 --- a/Makefile +++ b/Makefile @@ -17,8 +17,9 @@ RUNME_EXT_BASE := "../vscode-runme" endif .PHONY: build +build: BUILD_OUTPUT ?= runme build: - go build -o runme -ldflags="$(LDFLAGS)" main.go + CGO_ENABLED=0 go build -o $(BUILD_OUTPUT) -ldflags="$(LDFLAGS)" main.go .PHONY: wasm wasm: WASM_OUTPUT ?= examples/web @@ -31,33 +32,46 @@ test/execute: PKGS ?= "./..." test/execute: RUN ?= .* test/execute: RACE ?= false test/execute: TAGS ?= "" # e.g. TAGS="test_with_docker" -test/execute: build test/prep-git-project +# It depends on the build target because the runme binary +# is used for tests, for example, "runme env dump". +test/execute: build TZ=UTC go test -ldflags="$(LDTESTFLAGS)" -run="$(RUN)" -tags="$(TAGS)" -timeout=60s -race=$(RACE) $(PKGS) .PHONY: test/coverage test/coverage: PKGS ?= "./..." test/coverage: RUN ?= .* test/coverage: TAGS ?= "" # e.g. TAGS="test_with_docker" -test/coverage: build test/prep-git-project +# It depends on the build target because the runme binary +# is used for tests, for example, "runme env dump". +test/coverage: build TZ=UTC go test -ldflags="$(LDTESTFLAGS)" -run="$(RUN)" -tags="$(TAGS)" -timeout=90s -covermode=atomic -coverprofile=cover.out -coverpkg=./... $(PKGS) -.PHONY: test/prep-git-project -test/prep-git-project: - @cp -r -f internal/project/testdata/git-project/.git.bkp internal/project/testdata/git-project/.git - @cp -r -f internal/project/testdata/git-project/.gitignore.bkp internal/project/testdata/git-project/.gitignore - @cp -r -f internal/project/testdata/git-project/nested/.gitignore.bkp internal/project/testdata/git-project/nested/.gitignore - -.PHONY: test/clean-git-project -test/clean-git-project: - @rm -r -f internal/project/testdata/git-project/.git - @rm -r -f internal/project/testdata/git-project/.gitignore - @rm -r -f internal/project/testdata/git-project/nested/.gitignore - .PHONY: test -test: test/prep-git-project test/execute test/clean-git-project +test: test/execute .PHONY: test-coverage -test-coverage: test/prep-git-project test/coverage test/clean-git-project +test-coverage: test/coverage + +.PHONY: test-docker +test-docker: test-docker/setup test-docker/run + +.PHONY: test-docker/setup +test-docker/setup: + docker build \ + -t runme-test-env:latest \ + -f ./docker/runme-test-env.Dockerfile . + docker volume create dev.runme.test-env-gocache + +.PHONY: test-docker/cleanup +test-docker/cleanup: + docker volume rm dev.runme.test-env-gocache + +.PHONY: test-docker/run +test-docker/run: + docker run --rm \ + -v $(shell pwd):/workspace \ + -v dev.runme.test-env-gocache:/root/.cache/go-build \ + runme-test-env:latest .PHONY: test/update-snapshots test/update-snapshots: @@ -97,7 +111,7 @@ install/dev: .PHONY: install/goreleaser install/goreleaser: - go install github.com/goreleaser/goreleaser@v1.15.2 + go install github.com/goreleaser/goreleaser@v1.26.2 .PHONY: proto/generate proto/generate: @@ -146,5 +160,5 @@ generate: .PHONY: docker docker: CGO_ENABLED=0 make build - docker build -f Dockerfile.alpine . -t runme:alpine - docker build -f Dockerfile.ubuntu . -t runme:ubuntu + docker build -f docker/Dockerfile.alpine . -t runme:alpine + docker build -f docker/Dockerfile.ubuntu . -t runme:ubuntu diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 000000000..e5d722723 --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1,5 @@ +.github +dist +public +runme +*/**/node_modules diff --git a/docker/alpine.Dockerfile b/docker/alpine.Dockerfile new file mode 100644 index 000000000..ba8e4bd11 --- /dev/null +++ b/docker/alpine.Dockerfile @@ -0,0 +1,13 @@ +FROM --platform=$BUILDPLATFORM alpine:3.20 + +LABEL org.opencontainers.image.authors="StatefulHQ " +LABEL org.opencontainers.image.source="https://github.com/stateful/runme" +LABEL org.opencontainers.image.ref.name="runme" +LABEL org.opencontainers.image.title="Runme" +LABEL org.opencontainers.image.description="An image to run runme in a container." + +WORKDIR /project + +COPY runme /runme + +ENTRYPOINT [ "/runme" ] diff --git a/docker/runme-test-env.Dockerfile b/docker/runme-test-env.Dockerfile new file mode 100644 index 000000000..aca7d3fde --- /dev/null +++ b/docker/runme-test-env.Dockerfile @@ -0,0 +1,41 @@ +FROM golang:1.22-bookworm + +LABEL org.opencontainers.image.authors="StatefulHQ " +LABEL org.opencontainers.image.source="https://github.com/stateful/runme" +LABEL org.opencontainers.image.ref.name="runme-test-env" +LABEL org.opencontainers.image.title="Runme test environment" +LABEL org.opencontainers.image.description="An image to run unit and integration tests for runme." + +RUN apt-get update && apt-get install -y \ + bash \ + curl \ + make \ + python3 \ + unzip + +# Install node.js +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y nodejs + +# Install deno +ENV DENO_INSTALL=$HOME/.deno +RUN curl -fsSL https://deno.land/install.sh | sh \ + && cp $DENO_INSTALL/bin/deno /usr/local/bin/deno + +# Configure workspace +WORKDIR /workspace + +# Handle permissions when mounting a host directory to /workspace +RUN git config --global --add safe.directory /workspace + +# Populate Go cache. We do it in an old way +# because --mount is not supported in CMD. +COPY go.sum go.mod /workspace/ +RUN go mod download -x + +# Set output for the runmbe binary +ENV BUILD_OUTPUT=/usr/local/bin/runme +# Enable testing with race detector +ENV RACE=false + +CMD [ "make", "test" ] diff --git a/docker/runme-test-env.Dockerfile.dockerignore b/docker/runme-test-env.Dockerfile.dockerignore new file mode 100644 index 000000000..fba419753 --- /dev/null +++ b/docker/runme-test-env.Dockerfile.dockerignore @@ -0,0 +1,5 @@ +# Ignore all files and folders starting with a dot +\\.* +dist +public +**/node_modules diff --git a/docker/ubuntu.Dockerfile b/docker/ubuntu.Dockerfile new file mode 100644 index 000000000..0821d3bfa --- /dev/null +++ b/docker/ubuntu.Dockerfile @@ -0,0 +1,13 @@ +FROM --platform=$BUILDPLATFORM ubuntu:24.04 + +LABEL org.opencontainers.image.authors="StatefulHQ " +LABEL org.opencontainers.image.source="https://github.com/stateful/runme" +LABEL org.opencontainers.image.ref.name="runme" +LABEL org.opencontainers.image.title="Runme" +LABEL org.opencontainers.image.description="An image to run runme in a container." + +WORKDIR /project + +COPY runme /runme + +ENTRYPOINT [ "/runme" ] diff --git a/experimental/runme.yaml b/experimental/runme.yaml index 6257f9926..a6da3b1d1 100644 --- a/experimental/runme.yaml +++ b/experimental/runme.yaml @@ -64,5 +64,5 @@ server: log: enabled: true - path: "/var/tmp/runme.log" + path: "/var/log/runme.log" verbose: true diff --git a/go.mod b/go.mod index 905cf7722..d18762708 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/muesli/cancelreader v0.2.2 github.com/oklog/ulid/v2 v2.1.0 github.com/opencontainers/image-spec v1.1.0 + github.com/otiai10/copy v1.14.0 github.com/rogpeppe/go-internal v1.12.0 github.com/rwtodd/Go.Sed v0.0.0-20240405174034-bb8ed5da0fd0 github.com/stateful/godotenv v0.0.0-20240309032207-c7bc0b812915 diff --git a/go.sum b/go.sum index b3191559c..460ff7acf 100644 --- a/go.sum +++ b/go.sum @@ -217,6 +217,10 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= +github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= +github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= +github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= diff --git a/internal/command/factory.go b/internal/command/factory.go index 3e41597f4..46ba58605 100644 --- a/internal/command/factory.go +++ b/internal/command/factory.go @@ -145,15 +145,14 @@ func (f *commandFactory) buildBase(cfg *ProgramConfig, opts CommandOptions) *bas func (f *commandFactory) buildInternal(cfg *ProgramConfig, opts CommandOptions) internalCommand { base := f.buildBase(cfg, opts) - if f.docker != nil { + switch { + case f.docker != nil: return f.buildDocker(base) - } - - if base.Interactive() { + case base.Interactive(): return f.buildVirtual(base, opts) + default: + return f.buildNative(base) } - - return f.buildNative(base) } func (f *commandFactory) buildDocker(base *base) internalCommand { diff --git a/internal/project/project_test.go b/internal/project/project_test.go index 638bd4d9d..f27d97de1 100644 --- a/internal/project/project_test.go +++ b/internal/project/project_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/stateful/runme/v3/internal/project/testdata" + "github.com/stateful/runme/v3/internal/project/teststub" ) func TestExtractDataFromLoadEvent(t *testing.T) { @@ -39,30 +39,22 @@ func TestExtractDataFromLoadEvent(t *testing.T) { }) } -func TestMain(m *testing.M) { - testdata.AssertGitProject() - - code := m.Run() - os.Exit(code) -} - func TestNewDirProject(t *testing.T) { + testData := teststub.Setup(t, t.TempDir()) + t.Run("ProperDirProject", func(t *testing.T) { - projectDir := testdata.DirProjectPath() - _, err := NewDirProject(projectDir) + _, err := NewDirProject(testData.DirProjectPath()) require.NoError(t, err) }) t.Run("ProperGitProject", func(t *testing.T) { // git-based project is also a dir-based project. - gitProjectDir := testdata.GitProjectPath() - _, err := NewDirProject(gitProjectDir) + _, err := NewDirProject(testData.GitProjectPath()) require.NoError(t, err) }) t.Run("UnknownDir", func(t *testing.T) { - testdataDir := testdata.TestdataPath() - unknownDir := filepath.Join(testdataDir, "unknown-project") + unknownDir := testData.Join("unknown-project") _, err := NewDirProject(unknownDir) require.ErrorIs(t, err, os.ErrNotExist) }) @@ -71,7 +63,10 @@ func TestNewDirProject(t *testing.T) { cwd, err := os.Getwd() require.NoError(t, err) - projectDir, err := filepath.Rel(cwd, testdata.DirProjectPath()) + projectDir, err := filepath.Rel( + cwd, + teststub.OriginalPath().DirProjectPath(), + ) require.NoError(t, err) proj, err := NewDirProject(projectDir) @@ -81,8 +76,11 @@ func TestNewDirProject(t *testing.T) { } func TestNewFileProject(t *testing.T) { + temp := t.TempDir() + testData := teststub.Setup(t, temp) + t.Run("UnknownFile", func(t *testing.T) { - fileProject := filepath.Join(testdata.TestdataPath(), "unknown-file.md") + fileProject := testData.Join("unknown-file.md") _, err := NewFileProject(fileProject) require.ErrorIs(t, err, os.ErrNotExist) }) @@ -96,7 +94,10 @@ func TestNewFileProject(t *testing.T) { cwd, err := os.Getwd() require.NoError(t, err) - fileProject, err := filepath.Rel(cwd, testdata.ProjectFilePath()) + fileProject, err := filepath.Rel( + cwd, + teststub.OriginalPath().ProjectFilePath(), + ) require.NoError(t, err) proj, err := NewFileProject(fileProject) @@ -105,15 +106,17 @@ func TestNewFileProject(t *testing.T) { }) t.Run("ProperFileProject", func(t *testing.T) { - fileProject := testdata.ProjectFilePath() - _, err := NewFileProject(fileProject) + _, err := NewFileProject(testData.ProjectFilePath()) require.NoError(t, err) }) } func TestProjectRoot(t *testing.T) { + temp := t.TempDir() + testData := teststub.Setup(t, temp) + t.Run("GitProject", func(t *testing.T) { - gitProjectDir := testdata.GitProjectPath() + gitProjectDir := testData.GitProjectPath() p, err := NewDirProject(gitProjectDir) require.NoError(t, err) assert.Equal(t, gitProjectDir, p.Root()) @@ -121,16 +124,18 @@ func TestProjectRoot(t *testing.T) { }) t.Run("FileProject", func(t *testing.T) { - fileProject := testdata.ProjectFilePath() + fileProject := testData.ProjectFilePath() p, err := NewFileProject(fileProject) require.NoError(t, err) - assert.Equal(t, testdata.TestdataPath(), p.Root()) + assert.Equal(t, testData.Root(), p.Root()) assert.True(t, filepath.IsAbs(p.Root()), "project root is not absolute: %s", p.Root()) }) } func TestProjectLoad(t *testing.T) { - gitProjectDir := testdata.GitProjectPath() + temp := t.TempDir() + testData := teststub.Setup(t, temp) + gitProjectDir := testData.GitProjectPath() t.Run("GitProject", func(t *testing.T) { p, err := NewDirProject( @@ -265,7 +270,7 @@ func TestProjectLoad(t *testing.T) { } }) - gitProjectNestedDir := testdata.GitProjectNestedPath() + gitProjectNestedDir := testData.GitProjectNestedPath() t.Run("GitProjectWithNested", func(t *testing.T) { pRoot1, err := NewDirProject( @@ -337,7 +342,7 @@ func TestProjectLoad(t *testing.T) { ) }) - projectDir := testdata.DirProjectPath() + projectDir := testData.DirProjectPath() t.Run("DirProject", func(t *testing.T) { p, err := NewDirProject(projectDir) @@ -422,7 +427,7 @@ func TestProjectLoad(t *testing.T) { ) }) - fileProject := testdata.ProjectFilePath() + fileProject := testData.ProjectFilePath() t.Run("FileProject", func(t *testing.T) { p, err := NewFileProject(fileProject) @@ -473,7 +478,10 @@ func TestProjectLoad(t *testing.T) { } func TestLoadTasks(t *testing.T) { - gitProjectDir := testdata.GitProjectPath() + temp := t.TempDir() + testData := teststub.Setup(t, temp) + + gitProjectDir := testData.GitProjectPath() p, err := NewDirProject(gitProjectDir, WithIgnoreFilePatterns(".*.bkp")) require.NoError(t, err) @@ -483,7 +491,10 @@ func TestLoadTasks(t *testing.T) { } func TestLoadEnv(t *testing.T) { - gitProjectDir := testdata.GitProjectPath() + temp := t.TempDir() + testData := teststub.Setup(t, temp) + + gitProjectDir := testData.GitProjectPath() p, err := NewDirProject(gitProjectDir, WithIgnoreFilePatterns(".*.bkp"), WithEnvFilesReadOrder([]string{".env"})) require.NoError(t, err) diff --git a/internal/project/projectservice/project_service_test.go b/internal/project/projectservice/project_service_test.go index 600dc7a67..9a8edeb37 100644 --- a/internal/project/projectservice/project_service_test.go +++ b/internal/project/projectservice/project_service_test.go @@ -4,14 +4,9 @@ import ( "context" "io" "net" - "os" "testing" "github.com/pkg/errors" - "github.com/stateful/runme/v3/internal/project/projectservice" - "github.com/stateful/runme/v3/internal/project/testdata" - "github.com/stateful/runme/v3/internal/project/testutils" - projectv1 "github.com/stateful/runme/v3/pkg/api/gen/proto/go/runme/project/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" @@ -20,14 +15,12 @@ import ( "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" "google.golang.org/grpc/test/bufconn" -) -func TestMain(m *testing.M) { - testdata.AssertGitProject() - - code := m.Run() - os.Exit(code) -} + "github.com/stateful/runme/v3/internal/project/projectservice" + "github.com/stateful/runme/v3/internal/project/teststub" + "github.com/stateful/runme/v3/internal/project/testutils" + projectv1 "github.com/stateful/runme/v3/pkg/api/gen/proto/go/runme/project/v1" +) func TestProjectServiceServer_Load(t *testing.T) { t.Parallel() @@ -39,10 +32,13 @@ func TestProjectServiceServer_Load(t *testing.T) { t.Run("GitProject", func(t *testing.T) { t.Parallel() + temp := t.TempDir() + testData := teststub.Setup(t, temp) + req := &projectv1.LoadRequest{ Kind: &projectv1.LoadRequest_Directory{ Directory: &projectv1.DirectoryProjectOptions{ - Path: testdata.GitProjectPath(), + Path: testData.GitProjectPath(), SkipGitignore: false, IgnoreFilePatterns: testutils.IgnoreFilePatternsWithDefaults("ignored.md"), SkipRepoLookupUpward: false, @@ -61,10 +57,13 @@ func TestProjectServiceServer_Load(t *testing.T) { t.Run("FileProject", func(t *testing.T) { t.Parallel() + temp := t.TempDir() + testData := teststub.Setup(t, temp) + req := &projectv1.LoadRequest{ Kind: &projectv1.LoadRequest_File{ File: &projectv1.FileProjectOptions{ - Path: testdata.ProjectFilePath(), + Path: testData.ProjectFilePath(), }, }, } @@ -81,6 +80,9 @@ func TestProjectServiceServer_Load(t *testing.T) { func TestProjectServiceServer_Load_ErrorWhileSending(t *testing.T) { t.Parallel() + temp := t.TempDir() + testData := teststub.Setup(t, temp) + lis, stop := testStartProjectServiceServer(t) t.Cleanup(stop) clientConn, client := testCreateProjectServiceClient(t, lis) @@ -88,7 +90,7 @@ func TestProjectServiceServer_Load_ErrorWhileSending(t *testing.T) { req := &projectv1.LoadRequest{ Kind: &projectv1.LoadRequest_File{ File: &projectv1.FileProjectOptions{ - Path: testdata.ProjectFilePath(), + Path: testData.ProjectFilePath(), }, }, } diff --git a/internal/project/testdata/doc.go b/internal/project/testdata/doc.go deleted file mode 100644 index 7c10201e4..000000000 --- a/internal/project/testdata/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// testdata exports functions and variables to manage and interact -// with the fixtures located in its folder. -package testdata diff --git a/internal/project/testdata/testdata.go b/internal/project/testdata/testdata.go deleted file mode 100644 index 8eef69187..000000000 --- a/internal/project/testdata/testdata.go +++ /dev/null @@ -1,56 +0,0 @@ -package testdata - -import ( - "log" - "os" - "path/filepath" - "runtime" -) - -func TestdataPath() string { - return testdataDir() -} - -func DirProjectPath() string { - return filepath.Join(testdataDir(), "dir-project") -} - -func GitProjectPath() string { - return filepath.Join(testdataDir(), "git-project") -} - -func GitProjectNestedPath() string { - return filepath.Join(testdataDir(), "git-project", "nested") -} - -func ProjectFilePath() string { - return filepath.Join(testdataDir(), "file-project.md") -} - -// TODO(adamb): a better approach is to store "testdata" during build time. -func testdataDir() string { - _, b, _, _ := runtime.Caller(0) - return filepath.Dir(b) -} - -func AssertGitProject() { - assertGitProject() -} - -// assertGitProject checks that ./testdata/git-project is a valid git project. -// If it's not it will fail with a call to action to run the right make targets. -func assertGitProject() { - dir := GitProjectPath() - - gitProjectDestFiles := []string{ - filepath.Join(dir, ".git"), - filepath.Join(dir, ".gitignore"), - filepath.Join(dir, "nested", ".gitignore"), - } - - for _, dest := range gitProjectDestFiles { - if _, err := os.Stat(dest); err != nil { - log.Fatalf("failed to assert %s: %v; please run maket target 'test/prepare-git-project'.", dest, err) - } - } -} diff --git a/internal/project/testdata/testdata_test.go b/internal/project/testdata/testdata_test.go deleted file mode 100644 index e96e28b6d..000000000 --- a/internal/project/testdata/testdata_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package testdata - -import ( - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestDirProjectPath(t *testing.T) { - path := DirProjectPath() - require.True(t, strings.HasSuffix(path, filepath.Join("testdata", "dir-project"))) -} - -func TestGitProjectPath(t *testing.T) { - path := GitProjectPath() - require.True(t, strings.HasSuffix(path, filepath.Join("testdata", "git-project"))) -} - -func TestProjectFilePath(t *testing.T) { - path := ProjectFilePath() - require.True(t, strings.HasSuffix(path, filepath.Join("testdata", "file-project.md"))) -} diff --git a/internal/project/teststub/teststub.go b/internal/project/teststub/teststub.go new file mode 100644 index 000000000..6becb355f --- /dev/null +++ b/internal/project/teststub/teststub.go @@ -0,0 +1,76 @@ +package teststub + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/otiai10/copy" + "github.com/stretchr/testify/require" +) + +func Setup(t *testing.T, temp string) Path { + t.Helper() + + testDataSrc := originalTestDataPath() + require.NoError(t, copy.Copy(testDataSrc, temp)) + + err := os.Rename( + filepath.Join(temp, "git-project", ".git.bkp"), + filepath.Join(temp, "git-project", ".git"), + ) + require.NoError(t, err) + + err = os.Rename( + filepath.Join(temp, "git-project", ".gitignore.bkp"), + filepath.Join(temp, "git-project", ".gitignore"), + ) + require.NoError(t, err) + + err = os.Rename( + filepath.Join(temp, "git-project", "nested", ".gitignore.bkp"), + filepath.Join(temp, "git-project", "nested", ".gitignore"), + ) + require.NoError(t, err) + + return Path{root: temp} +} + +func OriginalPath() Path { + return Path{root: originalTestDataPath()} +} + +type Path struct { + root string +} + +func (p Path) Root() string { + return p.root +} + +func (p Path) Join(elems ...string) string { + elems = append([]string{p.root}, elems...) + return filepath.Join(elems...) +} + +func (p Path) DirProjectPath() string { + return p.Join("dir-project") +} + +func (p Path) GitProjectPath() string { + return p.Join("git-project") +} + +func (p Path) GitProjectNestedPath() string { + return p.Join("git-project", "nested") +} + +func (p Path) ProjectFilePath() string { + return p.Join("file-project.md") +} + +func originalTestDataPath() string { + _, b, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(b), "..", "testdata") +} diff --git a/internal/runnerv2service/service_execute_test.go b/internal/runnerv2service/service_execute_test.go index 03342cf87..f8b18d60a 100644 --- a/internal/runnerv2service/service_execute_test.go +++ b/internal/runnerv2service/service_execute_test.go @@ -10,6 +10,7 @@ import ( "testing" "testing/fstest" "time" + "unicode" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -674,12 +675,15 @@ func TestRunnerServiceServerExecute_WithInput(t *testing.T) { result := <-execResult assert.NoError(t, result.Err) - expected := "a\r\nb\r\nc\r\nd\r\nA\r\nB\r\nC\r\nD\r\n" - // On macOS, the ctrl+d (EOT) character followed by two backspaces is collected. - // On Linux, in the CI, this does not occur. - got := string(bytes.ReplaceAll(result.Stdout, []byte("^D\b\b"), nil)) - assert.Equal(t, expected, got) assert.EqualValues(t, 0, result.ExitCode) + // Validate the output by asserting that lowercase letters precede uppercase letters. + for _, c := range "abcd" { + idxLower := bytes.IndexRune(result.Stdout, c) + idxUpper := bytes.IndexRune(result.Stdout, unicode.ToUpper(c)) + assert.Greater(t, idxLower, -1) + assert.Greater(t, idxUpper, -1) + assert.True(t, idxUpper > idxLower) + } }) t.Run("SimulateCtrlC", func(t *testing.T) { diff --git a/internal/runnerv2service/service_sessions_test.go b/internal/runnerv2service/service_sessions_test.go index 41c699170..db299b5c3 100644 --- a/internal/runnerv2service/service_sessions_test.go +++ b/internal/runnerv2service/service_sessions_test.go @@ -9,12 +9,15 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/stateful/runme/v3/internal/project/testdata" + "github.com/stateful/runme/v3/internal/project/teststub" runnerv2alpha1 "github.com/stateful/runme/v3/pkg/api/gen/proto/go/runme/runner/v2alpha1" ) // TODO(adamb): add a test case with project. func TestRunnerServiceSessions(t *testing.T) { + temp := t.TempDir() + testData := teststub.Setup(t, temp) + lis, stop := testStartRunnerServiceServer(t) t.Cleanup(stop) _, client := testCreateRunnerServiceClient(t, lis) @@ -49,7 +52,7 @@ func TestRunnerServiceSessions(t *testing.T) { }) t.Run("WithProject", func(t *testing.T) { - projectPath := testdata.GitProjectPath() + projectPath := testData.GitProjectPath() createResp, err := client.CreateSession( context.Background(), &runnerv2alpha1.CreateSessionRequest{Project: &runnerv2alpha1.Project{Root: projectPath, EnvLoadOrder: []string{".env"}}}, diff --git a/main_test.go b/main_test.go index 663348169..1bfa8089d 100644 --- a/main_test.go +++ b/main_test.go @@ -28,31 +28,36 @@ func TestMain(m *testing.M) { // https://bitfieldconsulting.com/golang/test-scripts func TestRunme(t *testing.T) { testscript.Run(t, testscript.Params{ - Dir: "testdata/script", + Dir: "testdata/script", + ContinueOnError: true, }) } func TestRunmeFlags(t *testing.T) { testscript.Run(t, testscript.Params{ - Dir: "testdata/flags", + Dir: "testdata/flags", + ContinueOnError: true, }) } func TestRunmeCategories(t *testing.T) { testscript.Run(t, testscript.Params{ - Dir: "testdata/categories", + Dir: "testdata/categories", + ContinueOnError: true, }) } func TestRunmeRunAll(t *testing.T) { testscript.Run(t, testscript.Params{ - Dir: "testdata/runall", + Dir: "testdata/runall", + ContinueOnError: true, }) } func TestRunmeBeta(t *testing.T) { testscript.Run(t, testscript.Params{ - Dir: "testdata/beta", + Dir: "testdata/beta", + ContinueOnError: true, }) } diff --git a/testdata/beta/server.txtar b/testdata/beta/server.txtar index bcb72b26e..3b2457e4f 100644 --- a/testdata/beta/server.txtar +++ b/testdata/beta/server.txtar @@ -1,6 +1,6 @@ ! exec runme beta server start & # wait for the server to generate certs and start up -exec sleep 10 +exec sleep 8 exec runme beta server stop wait ! stdout . diff --git a/testdata/script/basic.txtar b/testdata/script/basic.txtar index c35623e99..d7fa39410 100644 --- a/testdata/script/basic.txtar +++ b/testdata/script/basic.txtar @@ -49,8 +49,8 @@ stdout 'Hello, runme, from javascript!' ! stderr . env PATH=/opt/homebrew/bin:$PATH -exec runme run hello-js-cat -stdout 'console\.log\(\"Hello, runme, from javascript!\"\)' +exec runme run hello-cat +stdout 'Hello runme' ! stderr . env PATH=/opt/homebrew/bin:$PATH @@ -133,8 +133,8 @@ console.log("Hello, runme, from javascript!") And it can even run a cell with a custom interpreter: -```js {"interpreter":"cat","name":"hello-js-cat"} -console.log("Hello, runme, from javascript!") +```js {"interpreter":"cat","name":"hello-cat"} +Hello runme ``` It works with `cd`, `pushd`, and similar because all lines are executed as a single script: @@ -188,7 +188,7 @@ $ echo "Runs as shell script" NAME FILE FIRST COMMAND DESCRIPTION NAMED echo README.md echo "Hello, runme!" With {"name":"hello"} you can annotate it and give it a nice name. Yes hello-js README.md console.log("Hello, runme, from javascript!") It can even run scripting languages. Yes -hello-js-cat README.md console.log("Hello, runme, from javascript!") And it can even run a cell with a custom interpreter. Yes +hello-cat README.md Hello runme And it can even run a cell with a custom interpreter. Yes hello-python README.md def say_hi(): Yes run-shellscript shellscript.md echo "Runs as shell script" This is a basic snippet with shell command. Yes -- golden-list-allow-unnamed.txt -- @@ -200,7 +200,7 @@ echo README.md echo "Hello, runme!" With {"name":"hello"} you can annotate it an echo-1 README.md echo "1" It can contain multiple lines too. No echo-hello-3 README.md echo "Hello, runme! Again!" Also, the dollar sign is not needed. No hello-js README.md console.log("Hello, runme, from javascript!") It can even run scripting languages. Yes -hello-js-cat README.md console.log("Hello, runme, from javascript!") And it can even run a cell with a custom interpreter. Yes +hello-cat README.md Hello runme And it can even run a cell with a custom interpreter. Yes tempdir README.md temp_dir=$(mktemp -d -t "runme-XXXXXXX") It works with cd, pushd, and similar because all lines are executed as a single script. No package-main README.md package main It can also execute a snippet of Go code. No hello-python README.md def say_hi(): Yes @@ -222,9 +222,9 @@ run-shellscript shellscript.md echo "Runs as shell script" This is a basic snipp "named": true }, { - "name": "hello-js-cat", + "name": "hello-cat", "file": "README.md", - "first_command": "console.log(\"Hello, runme, from javascript!\")", + "first_command": "Hello runme", "description": "And it can even run a cell with a custom interpreter.", "named": true },