From dd2eebf13a692f36cce773841ce60dbc6e3fa974 Mon Sep 17 00:00:00 2001 From: Michael McGuinness Date: Sat, 3 May 2025 01:16:53 +0000 Subject: [PATCH] Merged in feature/testingopts (pull request #129) Testing Options and Clean Up * bitsandbobs * lint * healthchecks * imgintests * singlegenerate * testclean * network * networkclean * cmds --- build/Dockerfile | 2 +- deployments/compose.aws.yaml | 10 +- deployments/compose.generate.yaml | 10 +- deployments/compose.local.yaml | 14 +- deployments/compose.test.yaml | 6 +- go.mod | 2 + go.sum | 2 + internal/database/migrations_test.go | 2 +- internal/database/repository/clean_test.go | 2 +- internal/database/repository/client_test.go | 2 +- .../database/repository/collector_test.go | 2 +- internal/database/repository/document_test.go | 2 +- internal/database/repository/query_test.go | 8 +- internal/database/repository/result_test.go | 6 +- internal/database/repository/sync_test.go | 4 +- internal/database/repository/text_test.go | 6 +- internal/server/api/listener_test.go | 2 +- internal/server/runner/listener_test.go | 2 +- internal/server/server_test.go | 2 +- internal/serviceconfig/database/pool_test.go | 2 +- .../serviceconfig/objectstore/config_test.go | 2 +- internal/test/api_test.go | 2 +- internal/test/aws_test.go | 2 +- internal/test/container.go | 50 ++- internal/test/container_test.go | 28 +- internal/test/database_test.go | 4 +- internal/test/ecosystem_test.go | 2 +- internal/test/network.go | 60 +-- internal/test/objectstore_test.go | 4 +- internal/test/queue_test.go | 8 +- internal/test/runner_test.go | 2 +- scripts/Taskfile.yml | 47 +- scripts/database.yml | 20 +- scripts/local-deployments.yml | 77 ++-- scripts/tests.yml | 70 +-- .../github.com/gruntwork-io/terratest/LICENSE | 202 +++++++++ .../github.com/gruntwork-io/terratest/NOTICE | 4 + .../modules/collections/collections.go | 2 + .../terratest/modules/collections/errors.go | 17 + .../terratest/modules/collections/lists.go | 40 ++ .../modules/collections/stringslicevalue.go | 27 ++ .../terratest/modules/docker/build.go | 222 +++++++++ .../terratest/modules/docker/docker.go | 2 + .../modules/docker/docker_compose.go | 99 ++++ .../terratest/modules/docker/host.go | 40 ++ .../terratest/modules/docker/images.go | 112 +++++ .../terratest/modules/docker/inspect.go | 270 +++++++++++ .../terratest/modules/docker/push.go | 25 ++ .../terratest/modules/docker/run.go | 166 +++++++ .../terratest/modules/docker/stop.go | 59 +++ .../terratest/modules/logger/logger.go | 178 ++++++++ .../terratest/modules/shell/command.go | 252 +++++++++++ .../terratest/modules/shell/output.go | 92 ++++ .../terratest/modules/shell/shell.go | 2 + .../terratest/modules/testing/types.go | 28 ++ vendor/gotest.tools/v3/LICENSE | 13 + vendor/gotest.tools/v3/assert/assert.go | 313 +++++++++++++ vendor/gotest.tools/v3/assert/cmp/compare.go | 404 +++++++++++++++++ vendor/gotest.tools/v3/assert/cmp/result.go | 110 +++++ vendor/gotest.tools/v3/icmd/command.go | 295 ++++++++++++ vendor/gotest.tools/v3/icmd/exitcode.go | 23 + vendor/gotest.tools/v3/icmd/ops.go | 60 +++ .../gotest.tools/v3/internal/assert/assert.go | 161 +++++++ .../gotest.tools/v3/internal/assert/result.go | 146 ++++++ .../gotest.tools/v3/internal/difflib/LICENSE | 27 ++ .../v3/internal/difflib/difflib.go | 423 ++++++++++++++++++ .../gotest.tools/v3/internal/format/diff.go | 162 +++++++ .../gotest.tools/v3/internal/format/format.go | 27 ++ .../gotest.tools/v3/internal/source/defers.go | 52 +++ .../gotest.tools/v3/internal/source/source.go | 148 ++++++ .../gotest.tools/v3/internal/source/update.go | 171 +++++++ .../v3/internal/source/version.go | 35 ++ vendor/modules.txt | 16 + 73 files changed, 4657 insertions(+), 234 deletions(-) create mode 100644 vendor/github.com/gruntwork-io/terratest/LICENSE create mode 100644 vendor/github.com/gruntwork-io/terratest/NOTICE create mode 100644 vendor/github.com/gruntwork-io/terratest/modules/collections/collections.go create mode 100644 vendor/github.com/gruntwork-io/terratest/modules/collections/errors.go create mode 100644 vendor/github.com/gruntwork-io/terratest/modules/collections/lists.go create mode 100644 vendor/github.com/gruntwork-io/terratest/modules/collections/stringslicevalue.go create mode 100644 vendor/github.com/gruntwork-io/terratest/modules/docker/build.go create mode 100644 vendor/github.com/gruntwork-io/terratest/modules/docker/docker.go create mode 100644 vendor/github.com/gruntwork-io/terratest/modules/docker/docker_compose.go create mode 100644 vendor/github.com/gruntwork-io/terratest/modules/docker/host.go create mode 100644 vendor/github.com/gruntwork-io/terratest/modules/docker/images.go create mode 100644 vendor/github.com/gruntwork-io/terratest/modules/docker/inspect.go create mode 100644 vendor/github.com/gruntwork-io/terratest/modules/docker/push.go create mode 100644 vendor/github.com/gruntwork-io/terratest/modules/docker/run.go create mode 100644 vendor/github.com/gruntwork-io/terratest/modules/docker/stop.go create mode 100644 vendor/github.com/gruntwork-io/terratest/modules/logger/logger.go create mode 100644 vendor/github.com/gruntwork-io/terratest/modules/shell/command.go create mode 100644 vendor/github.com/gruntwork-io/terratest/modules/shell/output.go create mode 100644 vendor/github.com/gruntwork-io/terratest/modules/shell/shell.go create mode 100644 vendor/github.com/gruntwork-io/terratest/modules/testing/types.go create mode 100644 vendor/gotest.tools/v3/LICENSE create mode 100644 vendor/gotest.tools/v3/assert/assert.go create mode 100644 vendor/gotest.tools/v3/assert/cmp/compare.go create mode 100644 vendor/gotest.tools/v3/assert/cmp/result.go create mode 100644 vendor/gotest.tools/v3/icmd/command.go create mode 100644 vendor/gotest.tools/v3/icmd/exitcode.go create mode 100644 vendor/gotest.tools/v3/icmd/ops.go create mode 100644 vendor/gotest.tools/v3/internal/assert/assert.go create mode 100644 vendor/gotest.tools/v3/internal/assert/result.go create mode 100644 vendor/gotest.tools/v3/internal/difflib/LICENSE create mode 100644 vendor/gotest.tools/v3/internal/difflib/difflib.go create mode 100644 vendor/gotest.tools/v3/internal/format/diff.go create mode 100644 vendor/gotest.tools/v3/internal/format/format.go create mode 100644 vendor/gotest.tools/v3/internal/source/defers.go create mode 100644 vendor/gotest.tools/v3/internal/source/source.go create mode 100644 vendor/gotest.tools/v3/internal/source/update.go create mode 100644 vendor/gotest.tools/v3/internal/source/version.go diff --git a/build/Dockerfile b/build/Dockerfile index e4afa9e6..0eb9c626 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -21,7 +21,7 @@ COPY cmd/ cmd/ COPY api/ api/ COPY internal/ internal/ -COPY .git/ .git/ +COPY --link .git/ .git/ ARG TARGETARCH diff --git a/deployments/compose.aws.yaml b/deployments/compose.aws.yaml index ed4ff130..9e32dbb1 100644 --- a/deployments/compose.aws.yaml +++ b/deployments/compose.aws.yaml @@ -394,10 +394,10 @@ services: volumes: - aws-db-data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${PGUSER}"] - interval: 10s + test: ["CMD", "sh", "-c", "pg_isready -U ${PGUSER} && psql -U ${PGUSER} -c 'SELECT 1'"] + interval: 500ms timeout: 5s - retries: 5 + retries: 50 networks: - aws-server-network @@ -419,9 +419,9 @@ services: - 4566 healthcheck: test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"] - interval: 5s + interval: 500ms timeout: 5s - retries: 10 + retries: 50 networks: - aws-server-network diff --git a/deployments/compose.generate.yaml b/deployments/compose.generate.yaml index 7c70005f..a7d95b33 100644 --- a/deployments/compose.generate.yaml +++ b/deployments/compose.generate.yaml @@ -10,16 +10,12 @@ services: POSTGRES_USER: ${PGUSER} expose: - 5431 - volumes: - - generate-db-data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${PGUSER}"] - interval: 10s + test: ["CMD", "sh", "-c", "pg_isready -U ${PGUSER} && psql -U ${PGUSER} -c 'SELECT 1'"] + interval: 500ms timeout: 5s - retries: 5 + retries: 50 networks: - generate-server-network -volumes: - generate-db-data: networks: generate-server-network: diff --git a/deployments/compose.local.yaml b/deployments/compose.local.yaml index d6403fea..41254314 100644 --- a/deployments/compose.local.yaml +++ b/deployments/compose.local.yaml @@ -286,10 +286,6 @@ services: COGNITO_CLIENT_SECRET: "fillin" COGNITO_DOMAIN: "fillin" COGNITO_CLIENT_ID: "fillin" - # COGNITO_USER_POOL_ID: ${COGNITO_USER_POOL_ID} - # COGNITO_CLIENT_SECRET: ${COGNITO_CLIENT_SECRET} - # COGNITO_DOMAIN: ${COGNITO_DOMAIN} - # COGNITO_CLIENT_ID: ${COGNITO_CLIENT_ID} networks: - server-network @@ -393,10 +389,10 @@ services: volumes: - db-data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${PGUSER}"] - interval: 10s + test: ["CMD", "sh", "-c", "pg_isready -U ${PGUSER} && psql -U ${PGUSER} -c 'SELECT 1'"] + interval: 500ms timeout: 5s - retries: 5 + retries: 50 networks: - server-network @@ -418,9 +414,9 @@ services: - 4566 healthcheck: test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"] - interval: 5s + interval: 500ms timeout: 5s - retries: 10 + retries: 50 networks: - server-network diff --git a/deployments/compose.test.yaml b/deployments/compose.test.yaml index c19874c2..59195b88 100644 --- a/deployments/compose.test.yaml +++ b/deployments/compose.test.yaml @@ -13,10 +13,10 @@ services: volumes: - test-db-data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${PGUSER}"] - interval: 10s + test: ["CMD", "sh", "-c", "pg_isready -U ${PGUSER} && psql -U ${PGUSER} -c 'SELECT 1'"] + interval: 500ms timeout: 5s - retries: 5 + retries: 50 networks: - test-server-network diff --git a/go.mod b/go.mod index 3a982405..50761561 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/go-playground/validator/v10 v10.25.0 github.com/golang-migrate/migrate/v4 v4.18.2 github.com/google/go-cmp v0.7.0 + github.com/gruntwork-io/terratest v0.48.2 github.com/jackc/pgx/v5 v5.7.2 github.com/joho/godotenv v1.5.1 github.com/labstack/echo-contrib v0.17.2 @@ -133,6 +134,7 @@ require ( google.golang.org/protobuf v1.36.5 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gotest.tools/v3 v3.5.1 // indirect ) require ( diff --git a/go.sum b/go.sum index 765cf585..86e93544 100644 --- a/go.sum +++ b/go.sum @@ -149,6 +149,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/gruntwork-io/terratest v0.48.2 h1:+VwfODchq8jxZZWD+s8gBlhD1z6/C4bFLNrhpm9ONrs= +github.com/gruntwork-io/terratest v0.48.2/go.mod h1:Y5ETyD4ZQ2MZhasPno272fWuCpKwvTPYDi8Y0tIMqTE= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/internal/database/migrations_test.go b/internal/database/migrations_test.go index 51566088..5df85864 100644 --- a/internal/database/migrations_test.go +++ b/internal/database/migrations_test.go @@ -13,7 +13,7 @@ import ( func TestRunMigrations(t *testing.T) { if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := t.Context() cfg := &serviceconfig.BaseConfig{} diff --git a/internal/database/repository/clean_test.go b/internal/database/repository/clean_test.go index 62f11805..4fb8f5bd 100644 --- a/internal/database/repository/clean_test.go +++ b/internal/database/repository/clean_test.go @@ -15,7 +15,7 @@ import ( func TestClean(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() diff --git a/internal/database/repository/client_test.go b/internal/database/repository/client_test.go index faed2bf2..e4428df0 100644 --- a/internal/database/repository/client_test.go +++ b/internal/database/repository/client_test.go @@ -15,7 +15,7 @@ import ( func TestClient(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() diff --git a/internal/database/repository/collector_test.go b/internal/database/repository/collector_test.go index bd64df83..e1012b2c 100644 --- a/internal/database/repository/collector_test.go +++ b/internal/database/repository/collector_test.go @@ -17,7 +17,7 @@ import ( func TestCollector(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() diff --git a/internal/database/repository/document_test.go b/internal/database/repository/document_test.go index 1da88a7f..d919d335 100644 --- a/internal/database/repository/document_test.go +++ b/internal/database/repository/document_test.go @@ -15,7 +15,7 @@ import ( func TestDocument(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() diff --git a/internal/database/repository/query_test.go b/internal/database/repository/query_test.go index 9f9ff4e9..9542d87c 100644 --- a/internal/database/repository/query_test.go +++ b/internal/database/repository/query_test.go @@ -16,7 +16,7 @@ import ( func TestQueries(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() @@ -226,7 +226,7 @@ func TestQueries(t *testing.T) { func TestQueryDependencyTree(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() @@ -445,7 +445,7 @@ func TestQueryDependencyTree(t *testing.T) { func TestQueriesList(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() @@ -501,7 +501,7 @@ func TestQueriesList(t *testing.T) { func TestListQueryClients(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() diff --git a/internal/database/repository/result_test.go b/internal/database/repository/result_test.go index cfdcedb6..aa138277 100644 --- a/internal/database/repository/result_test.go +++ b/internal/database/repository/result_test.go @@ -18,7 +18,7 @@ import ( func TestResults(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() @@ -157,7 +157,7 @@ func TestResults(t *testing.T) { func TestResultValues(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() @@ -371,7 +371,7 @@ func TestResultValues(t *testing.T) { func TestUnsyncedNoDepsQueries(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() diff --git a/internal/database/repository/sync_test.go b/internal/database/repository/sync_test.go index 456e824f..4f2a5013 100644 --- a/internal/database/repository/sync_test.go +++ b/internal/database/repository/sync_test.go @@ -18,7 +18,7 @@ import ( func TestListClientDocumentIDs(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() @@ -168,7 +168,7 @@ func createClientWithCollector(t testing.TB, ctx context.Context, queries *repos func TestClientSync(t *testing.T) { if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } t.Run("document fail clean", func(t *testing.T) { diff --git a/internal/database/repository/text_test.go b/internal/database/repository/text_test.go index fc1eed97..4efb4697 100644 --- a/internal/database/repository/text_test.go +++ b/internal/database/repository/text_test.go @@ -18,7 +18,7 @@ import ( func TestTextExtraction(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() @@ -126,7 +126,7 @@ func TestTextExtraction(t *testing.T) { func TestTextTextractPart(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() @@ -256,7 +256,7 @@ func TestTextTextractPart(t *testing.T) { func TestTextOutPart(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() diff --git a/internal/server/api/listener_test.go b/internal/server/api/listener_test.go index 2eee84b6..162d9ba1 100644 --- a/internal/server/api/listener_test.go +++ b/internal/server/api/listener_test.go @@ -22,7 +22,7 @@ import ( func TestNewAPI(t *testing.T) { if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() diff --git a/internal/server/runner/listener_test.go b/internal/server/runner/listener_test.go index ce0c3bb3..1dcd1acd 100644 --- a/internal/server/runner/listener_test.go +++ b/internal/server/runner/listener_test.go @@ -24,7 +24,7 @@ type TestConfig struct { func TestNewRunner(t *testing.T) { if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() cfg := &TestConfig{} diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 54a50fe6..ae9d9f81 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -16,7 +16,7 @@ import ( func TestNewServer(t *testing.T) { if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() diff --git a/internal/serviceconfig/database/pool_test.go b/internal/serviceconfig/database/pool_test.go index e6f98d71..9b82a124 100644 --- a/internal/serviceconfig/database/pool_test.go +++ b/internal/serviceconfig/database/pool_test.go @@ -16,7 +16,7 @@ import ( func TestSetDBPool(t *testing.T) { if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() diff --git a/internal/serviceconfig/objectstore/config_test.go b/internal/serviceconfig/objectstore/config_test.go index 3621e152..faa1f7bf 100644 --- a/internal/serviceconfig/objectstore/config_test.go +++ b/internal/serviceconfig/objectstore/config_test.go @@ -88,7 +88,7 @@ func TestSetStoreClientAndPingByName(t *testing.T) { func TestCalculateAndCompareETag(t *testing.T) { if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() diff --git a/internal/test/api_test.go b/internal/test/api_test.go index f5652c7c..8295add6 100644 --- a/internal/test/api_test.go +++ b/internal/test/api_test.go @@ -12,7 +12,7 @@ import ( func TestCreateAPI(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := t.Context() diff --git a/internal/test/aws_test.go b/internal/test/aws_test.go index 24f62faa..3d5634d3 100644 --- a/internal/test/aws_test.go +++ b/internal/test/aws_test.go @@ -18,7 +18,7 @@ type TestAWSConfig struct { func TestCreateQueueContainer(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() cfg := &TestAWSConfig{} diff --git a/internal/test/container.go b/internal/test/container.go index 47b4d939..dd5dba2d 100644 --- a/internal/test/container.go +++ b/internal/test/container.go @@ -5,12 +5,16 @@ import ( "context" "fmt" "log/slog" + "os" + "path/filepath" "strconv" + "sync" "testing" "queryorchestration/internal/serviceconfig" "github.com/docker/go-connections/nat" + "github.com/gruntwork-io/terratest/modules/shell" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" @@ -31,6 +35,48 @@ type containerConfig struct { MockHTTP string } +const ( + imageTag = "queryorchestration:latest" +) + +var ( + imageBuild sync.Once +) + +func buildImage(t testing.TB) { + imageBuild.Do(func() { + dockerContextPath, err := os.Getwd() + require.NoError(t, err, "Failed to get current directory") + + dockerfilePath := "build/Dockerfile" + + for { + candidatePath := filepath.Join(dockerContextPath, dockerfilePath) + + t.Logf("Checking for Dockerfile at: %s", dockerContextPath) + if _, err := os.Stat(candidatePath); err == nil { + dockerfilePath = candidatePath + t.Logf("Found Dockerfile at: %s", candidatePath) + break + } + + dockerContextPath = filepath.Join(dockerContextPath, "..") + } + + dockerBuildCmd := shell.Command{ + Command: "docker", + Args: []string{ + "build", + "--file", dockerfilePath, + "--tag", imageTag, + dockerContextPath, + }, + } + err = shell.RunCommandE(t, dockerBuildCmd) + require.NoError(t, err) + }) +} + func createContainer(t testing.TB, ctx context.Context, network string, cfg *containerConfig) (testcontainers.Container, func()) { env := map[string]string{ "PGUSER": cfg.Cfg.GetDBUser(), @@ -65,8 +111,10 @@ func createContainer(t testing.TB, ctx context.Context, network string, cfg *con env[string(GetRunnerEnvFromName(e))] = GetQueueURL(t, cfg.Cfg, e) } + buildImage(t) + req := testcontainers.ContainerRequest{ - Image: "queryorchestration:latest", + Image: imageTag, Env: env, WaitingFor: wait.ForLog(cfg.WaitForMsg), Entrypoint: []string{fmt.Sprintf("./%s", cfg.Name)}, diff --git a/internal/test/container_test.go b/internal/test/container_test.go index d83db289..39bd6976 100644 --- a/internal/test/container_test.go +++ b/internal/test/container_test.go @@ -5,23 +5,39 @@ import ( "testing" "queryorchestration/internal/serviceconfig" + "queryorchestration/internal/serviceconfig/objectstore" "github.com/docker/go-connections/nat" + "github.com/gruntwork-io/terratest/modules/docker" "github.com/stretchr/testify/assert" ) +func TestBuildImage(t *testing.T) { + if testing.Short() { + t.SkipNow() + } + buildImage(t) + + exists := docker.DoesImageExist(t, imageTag, nil) + assert.True(t, exists) +} + +type Cfg struct { + serviceconfig.BaseConfig + objectstore.ObjectStoreConfig +} + func TestCreateContainer(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() - cfg := &serviceconfig.BaseConfig{} + cfg := &Cfg{} - net := DepNetwork.Get(t, ctx) - - _ = CreateDB(t, ctx, cfg, net, &CreateDatabaseConfig{NoMigrations: true}) + deps, clean := CreateFullDependencies(t, ctx, cfg) + defer clean() ccfg := &containerConfig{ Name: string(QueryAPIName), @@ -32,7 +48,7 @@ func TestCreateContainer(t *testing.T) { }, } - container, cleanup := createContainer(t, ctx, net, ccfg) + container, cleanup := createContainer(t, ctx, deps.Network, ccfg) assert.NotNil(t, container) assert.NotNil(t, cleanup) diff --git a/internal/test/database_test.go b/internal/test/database_test.go index 206df8f2..264aa27b 100644 --- a/internal/test/database_test.go +++ b/internal/test/database_test.go @@ -13,7 +13,7 @@ import ( func TestCreateDB(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() @@ -29,7 +29,7 @@ func TestCreateDB(t *testing.T) { func TestCreateDBWithMigrations(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() diff --git a/internal/test/ecosystem_test.go b/internal/test/ecosystem_test.go index aa7d6d34..68cb6b4e 100644 --- a/internal/test/ecosystem_test.go +++ b/internal/test/ecosystem_test.go @@ -54,7 +54,7 @@ func TestCreateDetectDocumentTextExpectation(t *testing.T) { func TestWaitForMockEndpoint(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() diff --git a/internal/test/network.go b/internal/test/network.go index 50282a94..f50ecdf1 100644 --- a/internal/test/network.go +++ b/internal/test/network.go @@ -2,6 +2,7 @@ package test import ( "context" + "fmt" "log/slog" "sync" "testing" @@ -29,34 +30,41 @@ func (nm *NetworkManager) Get(t testing.TB, ctx context.Context) string { nm.mutex.Lock() defer nm.mutex.Unlock() - if !nm.initialized { - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - require.NoError(t, err) - - nm.networkName = networkName - - current, err := cli.NetworkList(ctx, network.ListOptions{}) - require.NoError(t, err) - - found := false - for _, c := range current { - if c.Name == nm.networkName { - found = true - continue - } - } - - if !found { - _, err = cli.NetworkCreate(ctx, nm.networkName, network.CreateOptions{ - Driver: "bridge", - }) - require.NoError(t, err) - } - - nm.initialized = true + if nm.initialized { + return nm.networkName } - slog.Info("get network", "name", nm.networkName) + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + require.NoError(t, err) + + nm.networkName = networkName + + current, err := cli.NetworkList(ctx, network.ListOptions{}) + require.NoError(t, err) + + found := false + for _, c := range current { + if c.Name == nm.networkName { + found = true + continue + } + } + + if found { + return nm.networkName + } + + _, err = cli.NetworkCreate(ctx, nm.networkName, network.CreateOptions{ + Driver: "bridge", + }) + conflictErrorMsg := fmt.Sprintf("Error response from daemon: network with name %s already exists", nm.networkName) + if err != nil && err.Error() != conflictErrorMsg { + require.NoError(t, err) + } + + slog.Info("created network", "name", nm.networkName) + + nm.initialized = true return nm.networkName } diff --git a/internal/test/objectstore_test.go b/internal/test/objectstore_test.go index a2555696..8fb22e41 100644 --- a/internal/test/objectstore_test.go +++ b/internal/test/objectstore_test.go @@ -24,7 +24,7 @@ type StoreConfig struct { func TestCreateBucket(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() @@ -40,7 +40,7 @@ func TestCreateBucket(t *testing.T) { func TestCreateStoreClient(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() diff --git a/internal/test/queue_test.go b/internal/test/queue_test.go index f25d77fa..05856115 100644 --- a/internal/test/queue_test.go +++ b/internal/test/queue_test.go @@ -25,7 +25,7 @@ type TestConfig struct { func TestCreateQueue(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() @@ -44,7 +44,7 @@ func TestCreateQueue(t *testing.T) { func TestAssertMessageWait(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() @@ -71,7 +71,7 @@ func TestAssertMessageWait(t *testing.T) { func TestAssertMessageBodyWait(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() @@ -95,7 +95,7 @@ func TestAssertMessageBodyWait(t *testing.T) { func TestAssertMessageAttrWait(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() diff --git a/internal/test/runner_test.go b/internal/test/runner_test.go index 79153809..22fa4914 100644 --- a/internal/test/runner_test.go +++ b/internal/test/runner_test.go @@ -10,7 +10,7 @@ import ( func TestCreateRunner(t *testing.T) { t.Parallel() if testing.Short() { - t.Skip("Skipping long test in short mode") + t.SkipNow() } ctx := context.Background() diff --git a/scripts/Taskfile.yml b/scripts/Taskfile.yml index bad1182c..756da28e 100644 --- a/scripts/Taskfile.yml +++ b/scripts/Taskfile.yml @@ -18,7 +18,7 @@ includes: dir: "{{.CONTEXT}}" taskfile: openapi-scripts.yml compose: - dir: "{{.CONTEXT}}" + dir: "." taskfile: local-deployments.yml db: dir: "{{.CONTEXT}}" @@ -27,42 +27,27 @@ includes: tasks: fullsuite: - task: build:deps - - task: fullsuite:build - - task: test:all - fullsuite:build: + - task: fullsuite:test + fullsuite:test: internal: true deps: - - task: build:cmd - - task: lint:cmd + - task: test:functional + - task: lint precommit: - - task: lint:short + - task: lint deps:tidy: - go mod tidy - go mod vendor - - go mod verify build: deps: - build:deps cmd: - task: build:cmd + task: docker:build build:deps: internal: true cmds: - task: deps:tidy - - task: generate:clean - build:cmd: - internal: true - cmds: - - task: docker:build - generate:clean: - run: once - deps: - - db:generate:clean - - openapi:generate - - test:mocks:generate - - docs:generate - cmds: - - task: go:generate + - task: generate generate: run: once deps: @@ -75,27 +60,13 @@ tasks: go:generate: go generate ./... lint: deps: - - generate:clean - cmds: - - task: lint:cmd - lint:cmd: - internal: true - deps: + - task: db:lint - task: go:lint - task: yaml:lint - task: json:lint - task: docker:lint - task: openapi:lint - - task: db:lint - task: compose:lint - lint:short: - deps: - - go:lint - - yaml:lint - - json:lint - - docker:lint - - openapi:lint - - compose:lint:short go:lint: golangci-lint run --fix {{.CLI_ARGS}} yaml:lint: yamllint . -s {{.CLI_ARGS}} json:lint: jsonlint devbox.json -q -s -i {{.CLI_ARGS}} diff --git a/scripts/database.yml b/scripts/database.yml index b6dc350a..a662a0c1 100644 --- a/scripts/database.yml +++ b/scripts/database.yml @@ -6,20 +6,22 @@ version: "3" vars: MIGRATIONS: "internal/database/migrations" +includes: + compose: + dir: "{{.CONTEXT}}" + taskfile: local-deployments.yml + internal: true + tasks: generate: run: once - deps: - - mig cmds: + - task: compose:up:generate + - migrate -path {{.MIGRATIONS}} -database {{.DB_URI_GENERATE}} up - sqlc generate --file sqlc.yml - generate:clean: - cmds: - - task compose:clean:generate - - task: generate lint: cmds: - - task compose:up:generate + - task: compose:up:generate - sqlc vet --file sqlc.yml - | exit 0 @@ -34,7 +36,3 @@ tasks: read -p "Migration Name: " name test -z "$name" && echo "Error: Name is required." && exit 1 migrate create -ext sql -dir {{.MIGRATIONS}} $name - mig: - cmds: - - task compose:up:generate - - migrate -path {{.MIGRATIONS}} -database {{.DB_URI_GENERATE}} up diff --git a/scripts/local-deployments.yml b/scripts/local-deployments.yml index cfaae72f..a3765495 100644 --- a/scripts/local-deployments.yml +++ b/scripts/local-deployments.yml @@ -4,10 +4,10 @@ version: "3" vars: - LOCAL_COMPOSE_FILE: "deployments/compose.local.yaml" - TEST_COMPOSE_FILE: "deployments/compose.test.yaml" - GENERATE_COMPOSE_FILE: "deployments/compose.generate.yaml" - AWS_COMPOSE_FILE: "deployments/compose.aws.yaml" + LOCAL_COMPOSE_FILE: "{{.CONTEXT}}/deployments/compose.local.yaml" + TEST_COMPOSE_FILE: "{{.CONTEXT}}/deployments/compose.test.yaml" + GENERATE_COMPOSE_FILE: "{{.CONTEXT}}/deployments/compose.generate.yaml" + AWS_COMPOSE_FILE: "{{.CONTEXT}}/deployments/compose.aws.yaml" tasks: build:test: @@ -38,10 +38,9 @@ tasks: COMPOSE_FILE: "{{.TEST_COMPOSE_FILE}}" up:generate: run: once - cmds: - - task: up:cmd - vars: - COMPOSE_FILE: "{{.GENERATE_COMPOSE_FILE}}" + cmd: | + docker compose -f {{.GENERATE_COMPOSE_FILE}} up \ + --force-recreate --renew-anon-volumes --wait -d up:aws: cmds: - task deps:tidy @@ -67,6 +66,7 @@ tasks: - task: up:cmd - task: init init: + internal: true cmds: - aws s3 mb s3://$BUCKET_IN - aws sqs create-queue --queue-name $QNAME_STORE_EVENT @@ -93,11 +93,6 @@ tasks: - task: down vars: COMPOSE_FILE: "{{.TEST_COMPOSE_FILE}}" - down:generate: - cmds: - - task: down - vars: - COMPOSE_FILE: "{{.GENERATE_COMPOSE_FILE}}" down:aws: cmds: - task: down @@ -108,37 +103,39 @@ tasks: COMPOSE_FILE: "{{.COMPOSE_FILE | default .LOCAL_COMPOSE_FILE}}" cmds: - docker compose -f {{.COMPOSE_FILE}} down + clean: + vars: + COMPOSE_FILE: "{{.COMPOSE_FILE | default .LOCAL_COMPOSE_FILE}}" + cmds: + - docker compose -f {{.COMPOSE_FILE}} down -v clean:test: cmds: - - task: down:test - - docker volume rm -f deployments_test-db-data - clean:generate: - run: once - cmds: - - task: down:generate - - docker volume rm -f deployments_generate-db-data + - task: clean + vars: + COMPOSE_FILE: "{{.TEST_COMPOSE_FILE}}" clean:aws: run: once cmds: - - task: down:aws - - docker volume rm -f deployments_aws-db-data - clean: + - task: clean + vars: + COMPOSE_FILE: "{{.AWS_COMPOSE_FILE}}" + lint:cmd: + internal: true + vars: + COMPOSE_FILE: "{{.COMPOSE_FILE | default .LOCAL_COMPOSE_FILE}}" cmds: - - task: down - - docker volume rm -f deployments_db-data + - docker compose -f {{.COMPOSE_FILE}} config -q lint: - cmds: - - docker compose -f {{.LOCAL_COMPOSE_FILE}} config -q - - | - docker compose -f {{.TEST_COMPOSE_FILE}} config -q \ - --resolve-image-digests - - | - docker compose -f {{.GENERATE_COMPOSE_FILE}} config -q \ - --resolve-image-digests - - docker compose -f {{.AWS_COMPOSE_FILE}} config -q - lint:short: - cmds: - - docker compose -f {{.LOCAL_COMPOSE_FILE}} config -q - - docker compose -f {{.TEST_COMPOSE_FILE}} config -q - - docker compose -f {{.GENERATE_COMPOSE_FILE}} config -q - - docker compose -f {{.AWS_COMPOSE_FILE}} config -q + deps: + - task: lint:cmd + vars: + COMPOSE_FILE: "{{.AWS_COMPOSE_FILE}}" + - task: lint:cmd + vars: + COMPOSE_FILE: "{{.LOCAL_COMPOSE_FILE}}" + - task: lint:cmd + vars: + COMPOSE_FILE: "{{.GENERATE_COMPOSE_FILE}}" + - task: lint:cmd + vars: + COMPOSE_FILE: "{{.TEST_COMPOSE_FILE}}" diff --git a/scripts/tests.yml b/scripts/tests.yml index 44b7fd27..c5dfd1aa 100644 --- a/scripts/tests.yml +++ b/scripts/tests.yml @@ -10,62 +10,42 @@ vars: API: "./api/..." PKG: "./pkg/..." CMD: "./cmd/..." - UNIT_PARALLEL: 32 - E2E_PARALLEL: 1 + TEST_PARALLEL: 32 # yamllint disable-line rule:line-length EXCLUDED_FILES: ".gen.go|internal/serviceconfig/observability/prometheus/generator/main.go|internal/cognitoauth/middleware.go|internal/cognitoauth/token.go|internal/cognitoauth/auth.go|internal/cognitoauth/handler.go|internal/cognitoauth/models.go|api/queryAPI/authHandlers.go|api/queryAPI/homehandler.go" -includes: - docker: - dir: "{{.CONTEXT}}" - taskfile: docker.yml - internal: true - tasks: mocks:generate: run: once cmds: - rm -rf mocks/* - go tool mockery - unit:cmd: - internal: true - vars: - SKIP_GENERATE: false + functional: cmds: - - task: mocks:generate - mkdir -p {{.OUT_DIR}} - | - go test -parallel {{.UNIT_PARALLEL}} \ - {{.INTERNAL}} {{.API}} {{.CLI_ARGS}} - unit:short: - cmds: - - task: unit:cmd - vars: - CLI_ARGS: -short - SKIP_GENERATE: true - unit:aws: - cmds: - - | - find ./internal -name "*.aws_test.go" -exec dirname {} \; | sort -u \ - | xargs go test -short -tags aws - unit: + go test -count=1 -parallel {{.TEST_PARALLEL}} \ + -coverpkg={{.INTERNAL}},{{.API}},{{.PKG}} \ + -coverprofile={{.COVERAGE_FILE}} \ + ./... + - task: coverage + bench: vars: TMP_FILE: "{{.OUT_DIR}}/coverage.tmp" FILTER_COVERAGE_FILE: "{{.OUT_DIR}}/filtered_coverage.out" cmds: - - task: unit:cmd - vars: - CLI_ARGS: | - -coverpkg={{.INTERNAL}},{{.API}} \ - -coverprofile={{.COVERAGE_FILE}} - - task: coverage - silent: true - endtoend: + - go test -run ^Benchmark -bench=. ./... > {{.BENCH_FILE}} + - benchstat {{.BENCH_FILE}} + unit:short: deps: - - docker:build + - mocks:generate cmds: - - go test -count=1 -parallel {{.E2E_PARALLEL}} ./test/... + - mkdir -p {{.OUT_DIR}} + - | + go test -parallel {{.TEST_PARALLEL}} -short \ + {{.INTERNAL}} {{.API}} {{.CLI_ARGS}} coverage: + internal: true vars: TMP_FILE: "{{.OUT_DIR}}/coverage.tmp" FILTER_COVERAGE_FILE: "{{.OUT_DIR}}/filtered_coverage.out" @@ -107,19 +87,3 @@ tasks: fi echo "✅ All coverage checks passed!" - all: - cmds: - - mkdir -p {{.OUT_DIR}} - - | - go test -count=1 -parallel {{.UNIT_PARALLEL}} \ - -coverpkg={{.INTERNAL}},{{.API}},{{.PKG}} \ - -coverprofile={{.COVERAGE_FILE}} \ - ./... - - task: coverage - bench: - vars: - TMP_FILE: "{{.OUT_DIR}}/coverage.tmp" - FILTER_COVERAGE_FILE: "{{.OUT_DIR}}/filtered_coverage.out" - cmds: - - go test -run ^Benchmark -bench=. ./... > {{.BENCH_FILE}} - - benchstat {{.BENCH_FILE}} diff --git a/vendor/github.com/gruntwork-io/terratest/LICENSE b/vendor/github.com/gruntwork-io/terratest/LICENSE new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. \ No newline at end of file diff --git a/vendor/github.com/gruntwork-io/terratest/NOTICE b/vendor/github.com/gruntwork-io/terratest/NOTICE new file mode 100644 index 00000000..88b1d1a6 --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/NOTICE @@ -0,0 +1,4 @@ +terratest +Copyright 2018 Gruntwork, Inc. + +This product includes software developed at Gruntwork (https://www.gruntwork.io/). \ No newline at end of file diff --git a/vendor/github.com/gruntwork-io/terratest/modules/collections/collections.go b/vendor/github.com/gruntwork-io/terratest/modules/collections/collections.go new file mode 100644 index 00000000..6967d2f6 --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/modules/collections/collections.go @@ -0,0 +1,2 @@ +// Package collections allows to interact with lists of things. +package collections diff --git a/vendor/github.com/gruntwork-io/terratest/modules/collections/errors.go b/vendor/github.com/gruntwork-io/terratest/modules/collections/errors.go new file mode 100644 index 00000000..56afccea --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/modules/collections/errors.go @@ -0,0 +1,17 @@ +package collections + +import "fmt" + +// SliceValueNotFoundError is returned when a provided values file input is not found on the host path. +type SliceValueNotFoundError struct { + sourceString string +} + +func (err SliceValueNotFoundError) Error() string { + return fmt.Sprintf("Could not resolve requested slice value from string %s", err.sourceString) +} + +// NewSliceValueNotFoundError creates a new slice found error +func NewSliceValueNotFoundError(sourceString string) SliceValueNotFoundError { + return SliceValueNotFoundError{sourceString} +} diff --git a/vendor/github.com/gruntwork-io/terratest/modules/collections/lists.go b/vendor/github.com/gruntwork-io/terratest/modules/collections/lists.go new file mode 100644 index 00000000..ec468d38 --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/modules/collections/lists.go @@ -0,0 +1,40 @@ +package collections + +// ListIntersection returns all the items in both list1 and list2. Note that this will dedup the items so that the +// output is more predictable. Otherwise, the end list depends on which list was used as the base. +func ListIntersection(list1 []string, list2 []string) []string { + out := []string{} + + // Only need to iterate list1, because we want items in both lists, not union. + for _, item := range list1 { + if ListContains(list2, item) && !ListContains(out, item) { + out = append(out, item) + } + } + + return out +} + +// ListSubtract removes all the items in list2 from list1. +func ListSubtract(list1 []string, list2 []string) []string { + out := []string{} + + for _, item := range list1 { + if !ListContains(list2, item) { + out = append(out, item) + } + } + + return out +} + +// ListContains returns true if the given list of strings (haystack) contains the given string (needle). +func ListContains(haystack []string, needle string) bool { + for _, str := range haystack { + if needle == str { + return true + } + } + + return false +} diff --git a/vendor/github.com/gruntwork-io/terratest/modules/collections/stringslicevalue.go b/vendor/github.com/gruntwork-io/terratest/modules/collections/stringslicevalue.go new file mode 100644 index 00000000..0fede5e4 --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/modules/collections/stringslicevalue.go @@ -0,0 +1,27 @@ +package collections + +import ( + "strings" +) + +// GetSliceLastValueE will take a source string and returns the last value when split by the separator char. +func GetSliceLastValueE(source string, separator string) (string, error) { + if len(source) > 0 && len(separator) > 0 && strings.Contains(source, separator) { + tmp := strings.Split(source, separator) + return tmp[len(tmp)-1], nil + } + return "", NewSliceValueNotFoundError(source) +} + +// GetSliceIndexValueE will take a source string and returns the value at the given index when split by +// the separator char. +func GetSliceIndexValueE(source string, separator string, index int) (string, error) { + if len(source) > 0 && len(separator) > 0 && strings.Contains(source, separator) && index >= 0 { + tmp := strings.Split(source, separator) + if index > len(tmp) { + return "", NewSliceValueNotFoundError(source) + } + return tmp[index], nil + } + return "", NewSliceValueNotFoundError(source) +} diff --git a/vendor/github.com/gruntwork-io/terratest/modules/docker/build.go b/vendor/github.com/gruntwork-io/terratest/modules/docker/build.go new file mode 100644 index 00000000..82c29508 --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/modules/docker/build.go @@ -0,0 +1,222 @@ +package docker + +import ( + "os" + "path/filepath" + "strings" + + "github.com/gruntwork-io/terratest/modules/logger" + "github.com/gruntwork-io/terratest/modules/shell" + "github.com/gruntwork-io/terratest/modules/testing" + "github.com/hashicorp/go-multierror" + "github.com/stretchr/testify/require" +) + +// BuildOptions defines options that can be passed to the 'docker build' command. +type BuildOptions struct { + // Tags for the Docker image + Tags []string + + // Build args to pass the 'docker build' command + BuildArgs []string + + // Target build arg to pass to the 'docker build' command + Target string + + // All architectures to target in a multiarch build. Configuring this variable will cause terratest to use docker + // buildx to construct multiarch images. + // You can read more about multiarch docker builds in the official documentation for buildx: + // https://docs.docker.com/buildx/working-with-buildx/ + // NOTE: This list does not automatically include the current platform. For example, if you are building images on + // an Apple Silicon based MacBook, and you configure this variable to []string{"linux/amd64"} to build an amd64 + // image, the buildx command will not automatically include linux/arm64 - you must include that explicitly. + Architectures []string + + // Whether or not to push images directly to the registry on build. Note that for multiarch images (Architectures is + // not empty), this must be true to ensure availability of all architectures - only the image for the current + // platform will be loaded into the daemon (due to a limitation of the docker daemon), so you won't be able to run a + // `docker push` command later to push the multiarch image. + // See https://github.com/moby/moby/pull/38738 for more info on the limitation of multiarch images in docker daemon. + Push bool + + // Whether or not to load the image into the docker daemon at the end of a multiarch build so that it can be used + // locally. Note that this is only used when Architectures is set, and assumes the current architecture is already + // included in the Architectures list. + Load bool + + // Custom CLI options that will be passed as-is to the 'docker build' command. This is an "escape hatch" that allows + // Terratest to not have to support every single command-line option offered by the 'docker build' command, and + // solely focus on the most important ones. + OtherOptions []string + + // Whether ot not to enable buildkit. You can find more information about buildkit here https://docs.docker.com/build/buildkit/#getting-started. + EnableBuildKit bool + + // Additional environment variables to pass in when running docker build command. + Env map[string]string + + // Set a logger that should be used. See the logger package for more info. + Logger *logger.Logger +} + +// Build runs the 'docker build' command at the given path with the given options and fails the test if there are any +// errors. +func Build(t testing.TestingT, path string, options *BuildOptions) { + require.NoError(t, BuildE(t, path, options)) +} + +// BuildE runs the 'docker build' command at the given path with the given options and returns any errors. +func BuildE(t testing.TestingT, path string, options *BuildOptions) error { + options.Logger.Logf(t, "Running 'docker build' in %s", path) + + env := make(map[string]string) + if options.Env != nil { + env = options.Env + } + + if options.EnableBuildKit { + env["DOCKER_BUILDKIT"] = "1" + } + + cmd := shell.Command{ + Command: "docker", + Args: formatDockerBuildArgs(path, options), + Logger: options.Logger, + Env: env, + } + + if err := shell.RunCommandE(t, cmd); err != nil { + return err + } + + // For non multiarch images, we need to call docker push for each tag since build does not have a push option like + // buildx. + if len(options.Architectures) == 0 && options.Push { + var errorsOccurred = new(multierror.Error) + for _, tag := range options.Tags { + if err := PushE(t, options.Logger, tag); err != nil { + options.Logger.Logf(t, "ERROR: error pushing tag %s", tag) + errorsOccurred = multierror.Append(err) + } + } + return errorsOccurred.ErrorOrNil() + } + + // For multiarch images, if a load is requested call the load command to export the built image into the daemon. + if len(options.Architectures) > 0 && options.Load { + loadCmd := shell.Command{ + Command: "docker", + Args: formatDockerBuildxLoadArgs(path, options), + Logger: options.Logger, + } + return shell.RunCommandE(t, loadCmd) + } + + return nil +} + +// GitCloneAndBuild builds a new Docker image from a given Git repo. This function will clone the given repo at the +// specified ref, and call the docker build command on the cloned repo from the given relative path (relative to repo +// root). This will fail the test if there are any errors. +func GitCloneAndBuild( + t testing.TestingT, + repo string, + ref string, + path string, + dockerBuildOpts *BuildOptions, +) { + require.NoError(t, GitCloneAndBuildE(t, repo, ref, path, dockerBuildOpts)) +} + +// GitCloneAndBuildE builds a new Docker image from a given Git repo. This function will clone the given repo at the +// specified ref, and call the docker build command on the cloned repo from the given relative path (relative to repo +// root). +func GitCloneAndBuildE( + t testing.TestingT, + repo string, + ref string, + path string, + dockerBuildOpts *BuildOptions, +) error { + workingDir, err := os.MkdirTemp("", "") + if err != nil { + return err + } + defer os.RemoveAll(workingDir) + + cloneCmd := shell.Command{ + Command: "git", + Args: []string{"clone", repo, workingDir}, + } + if err := shell.RunCommandE(t, cloneCmd); err != nil { + return err + } + + checkoutCmd := shell.Command{ + Command: "git", + Args: []string{"checkout", ref}, + WorkingDir: workingDir, + } + if err := shell.RunCommandE(t, checkoutCmd); err != nil { + return err + } + + contextPath := filepath.Join(workingDir, path) + if err := BuildE(t, contextPath, dockerBuildOpts); err != nil { + return err + } + return nil +} + +// formatDockerBuildArgs formats the arguments for the 'docker build' command. +func formatDockerBuildArgs(path string, options *BuildOptions) []string { + args := []string{} + + if len(options.Architectures) > 0 { + args = append( + args, + "buildx", + "build", + "--platform", + strings.Join(options.Architectures, ","), + ) + if options.Push { + args = append(args, "--push") + } + } else { + args = append(args, "build") + } + + return append(args, formatDockerBuildBaseArgs(path, options)...) +} + +// formatDockerBuildxLoadArgs formats the arguments for calling load on the 'docker buildx' command. +func formatDockerBuildxLoadArgs(path string, options *BuildOptions) []string { + args := []string{ + "buildx", + "build", + "--load", + } + return append(args, formatDockerBuildBaseArgs(path, options)...) +} + +// formatDockerBuildBaseArgs formats the common args for the build command, both for `build` and `buildx`. +func formatDockerBuildBaseArgs(path string, options *BuildOptions) []string { + args := []string{} + for _, tag := range options.Tags { + args = append(args, "--tag", tag) + } + + for _, arg := range options.BuildArgs { + args = append(args, "--build-arg", arg) + } + + if len(options.Target) > 0 { + args = append(args, "--target", options.Target) + } + + args = append(args, options.OtherOptions...) + + args = append(args, path) + return args +} diff --git a/vendor/github.com/gruntwork-io/terratest/modules/docker/docker.go b/vendor/github.com/gruntwork-io/terratest/modules/docker/docker.go new file mode 100644 index 00000000..28d9c965 --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/modules/docker/docker.go @@ -0,0 +1,2 @@ +// Package docker allows to interact with Docker and docker compose resources. +package docker diff --git a/vendor/github.com/gruntwork-io/terratest/modules/docker/docker_compose.go b/vendor/github.com/gruntwork-io/terratest/modules/docker/docker_compose.go new file mode 100644 index 00000000..330309d1 --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/modules/docker/docker_compose.go @@ -0,0 +1,99 @@ +package docker + +import ( + "regexp" + "strings" + + "github.com/gruntwork-io/terratest/modules/logger" + "github.com/gruntwork-io/terratest/modules/shell" + "github.com/gruntwork-io/terratest/modules/testing" + "github.com/stretchr/testify/require" + "gotest.tools/v3/icmd" +) + +// Options are Docker options. +type Options struct { + WorkingDir string + EnvVars map[string]string + + // Whether ot not to enable buildkit. You can find more information about buildkit here https://docs.docker.com/build/buildkit/#getting-started. + EnableBuildKit bool + + // Set a logger that should be used. See the logger package for more info. + Logger *logger.Logger + ProjectName string +} + +// RunDockerCompose runs docker compose with the given arguments and options and return stdout/stderr. +func RunDockerCompose(t testing.TestingT, options *Options, args ...string) string { + out, err := runDockerComposeE(t, false, options, args...) + if err != nil { + t.Fatal(err) + } + return out +} + +// RunDockerComposeAndGetStdout runs docker compose with the given arguments and options and returns only stdout. +func RunDockerComposeAndGetStdOut(t testing.TestingT, options *Options, args ...string) string { + out, err := runDockerComposeE(t, true, options, args...) + require.NoError(t, err) + return out +} + +// RunDockerComposeE runs docker compose with the given arguments and options and return stdout/stderr. +func RunDockerComposeE(t testing.TestingT, options *Options, args ...string) (string, error) { + return runDockerComposeE(t, false, options, args...) +} + +func runDockerComposeE(t testing.TestingT, stdout bool, options *Options, args ...string) (string, error) { + var cmd shell.Command + + projectName := options.ProjectName + if len(projectName) <= 0 { + projectName = strings.ToLower(t.Name()) + } + + dockerComposeVersionCmd := icmd.Command("docker", "compose", "version") + result := icmd.RunCmd(dockerComposeVersionCmd) + + if options.EnableBuildKit { + if options.EnvVars == nil { + options.EnvVars = make(map[string]string) + } + + options.EnvVars["DOCKER_BUILDKIT"] = "1" + options.EnvVars["COMPOSE_DOCKER_CLI_BUILD"] = "1" + } + + if result.ExitCode == 0 { + cmd = shell.Command{ + Command: "docker", + Args: append([]string{"compose", "--project-name", generateValidDockerComposeProjectName(projectName)}, args...), + WorkingDir: options.WorkingDir, + Env: options.EnvVars, + Logger: options.Logger, + } + } else { + cmd = shell.Command{ + Command: "docker-compose", + // We append --project-name to ensure containers from multiple different tests using Docker Compose don't end + // up in the same project and end up conflicting with each other. + Args: append([]string{"--project-name", generateValidDockerComposeProjectName(projectName)}, args...), + WorkingDir: options.WorkingDir, + Env: options.EnvVars, + Logger: options.Logger, + } + } + + if stdout { + return shell.RunCommandAndGetStdOut(t, cmd), nil + } + + return shell.RunCommandAndGetOutputE(t, cmd) +} + +// Note: docker-compose command doesn't like lower case or special characters, other than -. +func generateValidDockerComposeProjectName(str string) string { + lower_str := strings.ToLower(str) + return regexp.MustCompile(`[^a-zA-Z0-9 ]+`).ReplaceAllString(lower_str, "-") +} diff --git a/vendor/github.com/gruntwork-io/terratest/modules/docker/host.go b/vendor/github.com/gruntwork-io/terratest/modules/docker/host.go new file mode 100644 index 00000000..d8142b12 --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/modules/docker/host.go @@ -0,0 +1,40 @@ +package docker + +import ( + "os" + "strings" +) + +// GetDockerHost returns the name or address of the host on which the Docker engine is running. +func GetDockerHost() string { + return getDockerHostFromEnv(os.Environ()) +} + +func getDockerHostFromEnv(env []string) string { + // Parses the DOCKER_HOST environment variable to find the address + // + // For valid formats see: + // https://github.com/docker/cli/blob/6916b427a0b07e8581d121967633235ced6db9a1/opts/hosts.go#L69 + var dockerUrl []string + + for _, item := range env { + envVar := strings.Split(item, "=") + if len(envVar) == 2 && envVar[0] == "DOCKER_HOST" { + dockerUrl = strings.Split(envVar[1], ":") + break + } + } + + if len(dockerUrl) < 2 { + // DOCKER_HOST was empty, not present or not a valid URL + return "localhost" + } + + switch dockerUrl[0] { + case "tcp", "ssh", "fd": + return strings.TrimPrefix(dockerUrl[1], "//") + default: + // if DOCKER_HOST is not in one of the formats listed above, return default + return "localhost" + } +} diff --git a/vendor/github.com/gruntwork-io/terratest/modules/docker/images.go b/vendor/github.com/gruntwork-io/terratest/modules/docker/images.go new file mode 100644 index 00000000..a4f63940 --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/modules/docker/images.go @@ -0,0 +1,112 @@ +package docker + +import ( + "bufio" + "encoding/json" + "fmt" + "strings" + + "github.com/gruntwork-io/terratest/modules/collections" + "github.com/gruntwork-io/terratest/modules/logger" + "github.com/gruntwork-io/terratest/modules/shell" + "github.com/gruntwork-io/terratest/modules/testing" + "github.com/stretchr/testify/require" +) + +// Image represents a docker image, and exports all the fields that the docker images command returns for the +// image. +type Image struct { + // ID is the image ID in docker, and can be used to identify the image in place of the repo and tag. + ID string + + // Repository is the image repository. + Repository string + + // Tag is the image tag wichin the repository. + Tag string + + // CreatedAt represents a timestamp for when the image was created. + CreatedAt string + + // CreatedSince is a diff between when the image was created to now. + CreatedSince string + + // SharedSize is the amount of space that an image shares with another one (i.e. their common data). + SharedSize string + + // UniqueSize is the amount of space that is only used by a given image. + UniqueSize string + + // VirtualSize is the total size of the image, combining SharedSize and UniqueSize. + VirtualSize string + + // Containers represents the list of containers that are using the image. + Containers string + + // Digest is the hash digest of the image, if requested. + Digest string +} + +func (image Image) String() string { + return fmt.Sprintf("%s:%s", image.Repository, image.Tag) +} + +// DeleteImage removes a docker image using the Docker CLI. This will fail the test if there is an error. +func DeleteImage(t testing.TestingT, img string, logger *logger.Logger) { + require.NoError(t, DeleteImageE(t, img, logger)) +} + +// DeleteImageE removes a docker image using the Docker CLI. +func DeleteImageE(t testing.TestingT, img string, logger *logger.Logger) error { + cmd := shell.Command{ + Command: "docker", + Args: []string{"rmi", img}, + Logger: logger, + } + return shell.RunCommandE(t, cmd) +} + +// ListImages calls docker images using the Docker CLI to list the available images on the local docker daemon. +func ListImages(t testing.TestingT, logger *logger.Logger) []Image { + out, err := ListImagesE(t, logger) + require.NoError(t, err) + return out +} + +// ListImagesE calls docker images using the Docker CLI to list the available images on the local docker daemon. +func ListImagesE(t testing.TestingT, logger *logger.Logger) ([]Image, error) { + cmd := shell.Command{ + Command: "docker", + Args: []string{"images", "--format", "{{ json . }}"}, + Logger: logger, + } + out, err := shell.RunCommandAndGetOutputE(t, cmd) + if err != nil { + return nil, err + } + + // Parse and return the list of image objects. + images := []Image{} + scanner := bufio.NewScanner(strings.NewReader(out)) + for scanner.Scan() { + line := scanner.Text() + var image Image + err := json.Unmarshal([]byte(line), &image) + if err != nil { + return nil, err + } + images = append(images, image) + } + return images, nil +} + +// DoesImageExist lists the images in the docker daemon and returns true if the given image label (repo:tag) exists. +// This will fail the test if there is an error. +func DoesImageExist(t testing.TestingT, imgLabel string, logger *logger.Logger) bool { + images := ListImages(t, logger) + imageTags := []string{} + for _, image := range images { + imageTags = append(imageTags, image.String()) + } + return collections.ListContains(imageTags, imgLabel) +} diff --git a/vendor/github.com/gruntwork-io/terratest/modules/docker/inspect.go b/vendor/github.com/gruntwork-io/terratest/modules/docker/inspect.go new file mode 100644 index 00000000..6d058d1b --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/modules/docker/inspect.go @@ -0,0 +1,270 @@ +package docker + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + "testing" + "time" + + "github.com/gruntwork-io/terratest/modules/logger" + "github.com/gruntwork-io/terratest/modules/shell" + "github.com/stretchr/testify/require" +) + +// ContainerInspect defines the output of the Inspect method, with the options returned by 'docker inspect' +// converted into a more friendly and testable interface +type ContainerInspect struct { + // ID of the inspected container + ID string + + // Name of the inspected container + Name string + + // time.Time that the container was created + Created time.Time + + // String representing the container's status + Status string + + // Whether the container is currently running or not + Running bool + + // Container's exit code + ExitCode uint8 + + // String with the container's error message, if there is any + Error string + + // Ports exposed by the container + Ports []Port + + // Volume bindings made to the container + Binds []VolumeBind + + // Health check + Health HealthCheck +} + +// Port represents a single port mapping exported by the container +type Port struct { + HostPort uint16 + ContainerPort uint16 + Protocol string +} + +// VolumeBind represents a single volume binding made to the container +type VolumeBind struct { + Source string + Destination string +} + +// HealthCheck represents the current health history of the container +type HealthCheck struct { + // Health check status + Status string + + // Current count of failing health checks + FailingStreak uint8 + + // Log of failures + Log []HealthLog +} + +// HealthLog represents the output of a single Health check of the container +type HealthLog struct { + // Start time of health check + Start string + + // End time of health check + End string + + // Exit code of health check + ExitCode uint8 + + // Output of health check + Output string +} + +// inspectOutput defines options that will be returned by 'docker inspect', in JSON format. +// Not all options are included here, only the ones that we might need +type inspectOutput struct { + Id string + Created string + Name string + State struct { + Health HealthCheck + Status string + Running bool + ExitCode uint8 + Error string + } + NetworkSettings struct { + Ports map[string][]struct { + HostIp string + HostPort string + } + } + HostConfig struct { + Binds []string + } +} + +// Inspect runs the 'docker inspect {container id}' command and returns a ContainerInspect +// struct, converted from the output JSON, along with any errors +func Inspect(t *testing.T, id string) *ContainerInspect { + out, err := InspectE(t, id) + require.NoError(t, err) + + return out +} + +// InspectE runs the 'docker inspect {container id}' command and returns a ContainerInspect +// struct, converted from the output JSON, along with any errors +func InspectE(t *testing.T, id string) (*ContainerInspect, error) { + cmd := shell.Command{ + Command: "docker", + Args: []string{"container", "inspect", id}, + // inspect is a short-running command, don't print the output. + Logger: logger.Discard, + } + + out, err := shell.RunCommandAndGetStdOutE(t, cmd) + if err != nil { + return nil, err + } + + var containers []inspectOutput + err = json.Unmarshal([]byte(out), &containers) + if err != nil { + return nil, err + } + + if len(containers) == 0 { + return nil, fmt.Errorf("no container found with ID %s", id) + } + + container := containers[0] + + return transformContainer(t, container) +} + +// transformContainerPorts converts 'docker inspect' output JSON into a more friendly and testable format +func transformContainer(t *testing.T, container inspectOutput) (*ContainerInspect, error) { + name := strings.TrimLeft(container.Name, "/") + + ports, err := transformContainerPorts(container) + if err != nil { + return nil, err + } + + volumes := transformContainerVolumes(container) + + created, err := time.Parse(time.RFC3339Nano, container.Created) + if err != nil { + return nil, err + } + + inspect := ContainerInspect{ + ID: container.Id, + Name: name, + Created: created, + Status: container.State.Status, + Running: container.State.Running, + ExitCode: container.State.ExitCode, + Error: container.State.Error, + Ports: ports, + Binds: volumes, + Health: HealthCheck{ + Status: container.State.Health.Status, + FailingStreak: container.State.Health.FailingStreak, + Log: container.State.Health.Log, + }, + } + + return &inspect, nil +} + +// transformContainerPorts converts Docker's ports from the following json into a more testable format +// +// { +// "80/tcp": [ +// { +// "HostIp": "" +// "HostPort": "8080" +// } +// ] +// } +func transformContainerPorts(container inspectOutput) ([]Port, error) { + var ports []Port + + cPorts := container.NetworkSettings.Ports + + for key, portBinding := range cPorts { + split := strings.Split(key, "/") + + containerPort, err := strconv.ParseUint(split[0], 10, 16) + if err != nil { + return nil, err + } + + var protocol string + if len(split) > 1 { + protocol = split[1] + } + + for _, port := range portBinding { + hostPort, err := strconv.ParseUint(port.HostPort, 10, 16) + if err != nil { + return nil, err + } + + ports = append(ports, Port{ + HostPort: uint16(hostPort), + ContainerPort: uint16(containerPort), + Protocol: protocol, + }) + } + } + + return ports, nil +} + +// GetExposedHostPort returns an exposed host port according to requested container port. Returns 0 if the requested port is not exposed. +func (inspectOutput ContainerInspect) GetExposedHostPort(containerPort uint16) uint16 { + for _, port := range inspectOutput.Ports { + if port.ContainerPort == containerPort { + return port.HostPort + } + } + return uint16(0) +} + +// transformContainerVolumes converts Docker's volume bindings from the +// format "/foo/bar:/foo/baz" into a more testable one +func transformContainerVolumes(container inspectOutput) []VolumeBind { + binds := container.HostConfig.Binds + volumes := make([]VolumeBind, 0, len(binds)) + + for _, bind := range binds { + var source, dest string + + split := strings.Split(bind, ":") + + // Considering it as an unbound volume + dest = split[0] + + if len(split) == 2 { + source = split[0] + dest = split[1] + } + + volumes = append(volumes, VolumeBind{ + Source: source, + Destination: dest, + }) + } + + return volumes +} diff --git a/vendor/github.com/gruntwork-io/terratest/modules/docker/push.go b/vendor/github.com/gruntwork-io/terratest/modules/docker/push.go new file mode 100644 index 00000000..af0659b5 --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/modules/docker/push.go @@ -0,0 +1,25 @@ +package docker + +import ( + "github.com/gruntwork-io/terratest/modules/logger" + "github.com/gruntwork-io/terratest/modules/shell" + "github.com/gruntwork-io/terratest/modules/testing" + "github.com/stretchr/testify/require" +) + +// Push runs the 'docker push' command to push the given tag. This will fail the test if there are any errors. +func Push(t testing.TestingT, logger *logger.Logger, tag string) { + require.NoError(t, PushE(t, logger, tag)) +} + +// PushE runs the 'docker push' command to push the given tag. +func PushE(t testing.TestingT, logger *logger.Logger, tag string) error { + logger.Logf(t, "Running 'docker push' for tag %s", tag) + + cmd := shell.Command{ + Command: "docker", + Args: []string{"push", tag}, + Logger: logger, + } + return shell.RunCommandE(t, cmd) +} diff --git a/vendor/github.com/gruntwork-io/terratest/modules/docker/run.go b/vendor/github.com/gruntwork-io/terratest/modules/docker/run.go new file mode 100644 index 00000000..d57a4bb0 --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/modules/docker/run.go @@ -0,0 +1,166 @@ +package docker + +import ( + "github.com/gruntwork-io/terratest/modules/logger" + "github.com/gruntwork-io/terratest/modules/shell" + "github.com/gruntwork-io/terratest/modules/testing" + "github.com/stretchr/testify/require" +) + +// RunOptions defines options that can be passed to the 'docker run' command. +type RunOptions struct { + // Override the default COMMAND of the Docker image + Command []string + + // If set to true, pass the --detach flag to 'docker run' to run the container in the background + Detach bool + + // Override the default ENTRYPOINT of the Docker image + Entrypoint string + + // Set environment variables + EnvironmentVariables []string + + // If set to true, pass the --init flag to 'docker run' to run an init inside the container that forwards signals + // and reaps processes + Init bool + + // Assign a name to the container + Name string + + // Set platform + Platform string + + // If set to true, pass the --privileged flag to 'docker run' to give extended privileges to the container + Privileged bool + + // If set to true, pass the --rm flag to 'docker run' to automatically remove the container when it exits + Remove bool + + // If set to true, pass the -tty flag to 'docker run' to allocate a pseudo-TTY + Tty bool + + // Username or UID + User string + + // Bind mount these volume(s) when running the container + Volumes []string + + // Custom CLI options that will be passed as-is to the 'docker run' command. This is an "escape hatch" that allows + // Terratest to not have to support every single command-line option offered by the 'docker run' command, and + // solely focus on the most important ones. + OtherOptions []string + + // Set a logger that should be used. See the logger package for more info. + Logger *logger.Logger +} + +// Run runs the 'docker run' command on the given image with the given options and return stdout/stderr. This method +// fails the test if there are any errors. +func Run(t testing.TestingT, image string, options *RunOptions) string { + out, err := RunE(t, image, options) + require.NoError(t, err) + return out +} + +// RunE runs the 'docker run' command on the given image with the given options and return stdout/stderr, or any error. +func RunE(t testing.TestingT, image string, options *RunOptions) (string, error) { + options.Logger.Logf(t, "Running 'docker run' on image '%s'", image) + + args, err := formatDockerRunArgs(image, options) + if err != nil { + return "", err + } + + cmd := shell.Command{ + Command: "docker", + Args: args, + Logger: options.Logger, + } + + return shell.RunCommandAndGetOutputE(t, cmd) +} + +// RunAndGetID runs the 'docker run' command on the given image with the given options and returns the container ID +// that is returned in stdout. This method fails the test if there are any errors. +func RunAndGetID(t testing.TestingT, image string, options *RunOptions) string { + out, err := RunAndGetIDE(t, image, options) + require.NoError(t, err) + return out +} + +// RunAndGetIDE runs the 'docker run' command on the given image with the given options and returns the container ID +// that is returned in stdout, or any error. +func RunAndGetIDE(t testing.TestingT, image string, options *RunOptions) (string, error) { + options.Logger.Logf(t, "Running 'docker run' on image '%s', returning stdout", image) + + args, err := formatDockerRunArgs(image, options) + if err != nil { + return "", err + } + + cmd := shell.Command{ + Command: "docker", + Args: args, + Logger: options.Logger, + } + + return shell.RunCommandAndGetStdOutE(t, cmd) +} + +// formatDockerRunArgs formats the arguments for the 'docker run' command. +func formatDockerRunArgs(image string, options *RunOptions) ([]string, error) { + args := []string{"run"} + + if options.Detach { + args = append(args, "--detach") + } + + if options.Entrypoint != "" { + args = append(args, "--entrypoint", options.Entrypoint) + } + + for _, envVar := range options.EnvironmentVariables { + args = append(args, "--env", envVar) + } + + if options.Init { + args = append(args, "--init") + } + + if options.Name != "" { + args = append(args, "--name", options.Name) + } + + if options.Platform != "" { + args = append(args, "--platform", options.Platform) + } + + if options.Privileged { + args = append(args, "--privileged") + } + + if options.Remove { + args = append(args, "--rm") + } + + if options.Tty { + args = append(args, "--tty") + } + + if options.User != "" { + args = append(args, "--user", options.User) + } + + for _, volume := range options.Volumes { + args = append(args, "--volume", volume) + } + + args = append(args, options.OtherOptions...) + + args = append(args, image) + + args = append(args, options.Command...) + + return args, nil +} diff --git a/vendor/github.com/gruntwork-io/terratest/modules/docker/stop.go b/vendor/github.com/gruntwork-io/terratest/modules/docker/stop.go new file mode 100644 index 00000000..e5a43c60 --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/modules/docker/stop.go @@ -0,0 +1,59 @@ +package docker + +import ( + "strconv" + + "github.com/gruntwork-io/terratest/modules/logger" + "github.com/gruntwork-io/terratest/modules/shell" + "github.com/gruntwork-io/terratest/modules/testing" + "github.com/stretchr/testify/require" +) + +// StopOptions defines the options that can be passed to the 'docker stop' command +type StopOptions struct { + // Seconds to wait for stop before killing the container (default 10) + Time int + + // Set a logger that should be used. See the logger package for more info. + Logger *logger.Logger +} + +// Stop runs the 'docker stop' command for the given containers and return the stdout/stderr. This method fails +// the test if there are any errors +func Stop(t testing.TestingT, containers []string, options *StopOptions) string { + out, err := StopE(t, containers, options) + require.NoError(t, err) + return out +} + +// StopE runs the 'docker stop' command for the given containers and returns any errors. +func StopE(t testing.TestingT, containers []string, options *StopOptions) (string, error) { + options.Logger.Logf(t, "Running 'docker stop' on containers '%s'", containers) + + args, err := formatDockerStopArgs(containers, options) + if err != nil { + return "", err + } + + cmd := shell.Command{ + Command: "docker", + Args: args, + Logger: options.Logger, + } + + return shell.RunCommandAndGetOutputE(t, cmd) + +} + +// formatDockerStopArgs formats the arguments for the 'docker stop' command +func formatDockerStopArgs(containers []string, options *StopOptions) ([]string, error) { + args := []string{"stop"} + + if options.Time != 0 { + args = append(args, "--time", strconv.Itoa(options.Time)) + } + + args = append(args, containers...) + + return args, nil +} diff --git a/vendor/github.com/gruntwork-io/terratest/modules/logger/logger.go b/vendor/github.com/gruntwork-io/terratest/modules/logger/logger.go new file mode 100644 index 00000000..abc8c14b --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/modules/logger/logger.go @@ -0,0 +1,178 @@ +// Package logger contains different methods to log. +package logger + +import ( + "fmt" + "io" + "os" + "runtime" + "strings" + "sync" + gotesting "testing" + "time" + + "github.com/gruntwork-io/terratest/modules/testing" +) + +var ( + // Default is the default logger that is used for the Logf function, if no one is provided. It uses the + // TerratestLogger to log messages. This can be overwritten to change the logging globally. + Default = New(terratestLogger{}) + // Discard discards all logging. + Discard = New(discardLogger{}) + // Terratest logs the given format and arguments, formatted using fmt.Sprintf, to stdout, along with a timestamp and + // information about what test and file is doing the logging. Before Go 1.14, this is an alternative to t.Logf as it + // logs to stdout immediately, rather than buffering all log output and only displaying it at the very end of the test. + // This is useful because: + // + // 1. It allows you to iterate faster locally, as you get feedback on whether your code changes are working as expected + // right away, rather than at the very end of the test run. + // + // 2. If you have a bug in your code that causes a test to never complete or if the test code crashes, t.Logf would + // show you no log output whatsoever, making debugging very hard, where as this method will show you all the log + // output available. + // + // 3. If you have a test that takes a long time to complete, some CI systems will kill the test suite prematurely + // because there is no log output with t.Logf (e.g., CircleCI kills tests after 10 minutes of no log output). With + // this log method, you get log output continuously. + // + Terratest = New(terratestLogger{}) + // TestingT can be used to use Go's testing.T to log. If this is used, but no testing.T is provided, it will fallback + // to Default. + TestingT = New(testingT{}) +) + +type TestLogger interface { + Logf(t testing.TestingT, format string, args ...interface{}) +} + +type Logger struct { + l TestLogger +} + +func New(l TestLogger) *Logger { + return &Logger{ + l, + } +} + +func (l *Logger) Logf(t testing.TestingT, format string, args ...interface{}) { + if tt, ok := t.(helper); ok { + tt.Helper() + } + + // methods can be called on (typed) nil pointers. In this case, use the Default function to log. This enables the + // caller to do `var l *Logger` and then use the logger already. + if l == nil || l.l == nil { + Default.Logf(t, format, args...) + return + } + + l.l.Logf(t, format, args...) +} + +// helper is used to mark this library as a "helper", and thus not appearing in the line numbers. testing.T implements +// this interface, for example. +type helper interface { + Helper() +} + +type discardLogger struct{} + +func (_ discardLogger) Logf(_ testing.TestingT, format string, args ...interface{}) {} + +type testingT struct{} + +func (_ testingT) Logf(t testing.TestingT, format string, args ...interface{}) { + // this should never fail + tt, ok := t.(*gotesting.T) + if !ok { + // fallback + DoLog(t, 2, os.Stdout, fmt.Sprintf(format, args...)) + return + } + + tt.Helper() + tt.Logf(format, args...) + return +} + +type terratestLogger struct{} + +func (_ terratestLogger) Logf(t testing.TestingT, format string, args ...interface{}) { + DoLog(t, 3, os.Stdout, fmt.Sprintf(format, args...)) +} + +// Deprecated: use Logger instead, as it provides more flexibility on logging. +// Logf logs the given format and arguments, formatted using fmt.Sprintf, to stdout, along with a timestamp and information +// about what test and file is doing the logging. Before Go 1.14, this is an alternative to t.Logf as it logs to stdout +// immediately, rather than buffering all log output and only displaying it at the very end of the test. This is useful +// because: +// +// 1. It allows you to iterate faster locally, as you get feedback on whether your code changes are working as expected +// right away, rather than at the very end of the test run. +// +// 2. If you have a bug in your code that causes a test to never complete or if the test code crashes, t.Logf would +// show you no log output whatsoever, making debugging very hard, where as this method will show you all the log +// output available. +// +// 3. If you have a test that takes a long time to complete, some CI systems will kill the test suite prematurely +// because there is no log output with t.Logf (e.g., CircleCI kills tests after 10 minutes of no log output). With +// this log method, you get log output continuously. +// +// Although t.Logf now supports streaming output since Go 1.14, this is kept for compatibility purposes. +func Logf(t testing.TestingT, format string, args ...interface{}) { + if tt, ok := t.(helper); ok { + tt.Helper() + } + + mutexStdout.Lock() + defer mutexStdout.Unlock() + DoLog(t, 2, os.Stdout, fmt.Sprintf(format, args...)) +} + +// Log logs the given arguments to stdout, along with a timestamp and information about what test and file is doing the +// logging. This is an alternative to t.Logf that logs to stdout immediately, rather than buffering all log output and +// only displaying it at the very end of the test. See the Logf method for more info. +func Log(t testing.TestingT, args ...interface{}) { + if tt, ok := t.(helper); ok { + tt.Helper() + } + + mutexStdout.Lock() + defer mutexStdout.Unlock() + DoLog(t, 2, os.Stdout, args...) +} + +var mutexStdout sync.Mutex + +// DoLog logs the given arguments to the given writer, along with a timestamp and information about what test and file is +// doing the logging. +func DoLog(t testing.TestingT, callDepth int, writer io.Writer, args ...interface{}) { + date := time.Now() + prefix := fmt.Sprintf("%s %s %s:", t.Name(), date.Format(time.RFC3339), CallerPrefix(callDepth+1)) + allArgs := append([]interface{}{prefix}, args...) + fmt.Fprintln(writer, allArgs...) +} + +// CallerPrefix returns the file and line number information about the methods that called this method, based on the current +// goroutine's stack. The argument callDepth is the number of stack frames to ascend, with 0 identifying the method +// that called CallerPrefix, 1 identifying the method that called that method, and so on. +// +// This code is adapted from testing.go, where it is in a private method called decorate. +func CallerPrefix(callDepth int) string { + _, file, line, ok := runtime.Caller(callDepth) + if ok { + // Truncate file name at last file name separator. + if index := strings.LastIndex(file, "/"); index >= 0 { + file = file[index+1:] + } else if index = strings.LastIndex(file, "\\"); index >= 0 { + file = file[index+1:] + } + } else { + file = "???" + line = 1 + } + + return fmt.Sprintf("%s:%d", file, line) +} diff --git a/vendor/github.com/gruntwork-io/terratest/modules/shell/command.go b/vendor/github.com/gruntwork-io/terratest/modules/shell/command.go new file mode 100644 index 00000000..db964069 --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/modules/shell/command.go @@ -0,0 +1,252 @@ +package shell + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + "os/exec" + "strings" + "sync" + "syscall" + + "github.com/gruntwork-io/terratest/modules/logger" + "github.com/gruntwork-io/terratest/modules/testing" + "github.com/stretchr/testify/require" +) + +// Command is a simpler struct for defining commands than Go's built-in Cmd. +type Command struct { + Command string // The command to run + Args []string // The args to pass to the command + WorkingDir string // The working directory + Env map[string]string // Additional environment variables to set + // Use the specified logger for the command's output. Use logger.Discard to not print the output while executing the command. + Logger *logger.Logger +} + +// RunCommand runs a shell command and redirects its stdout and stderr to the stdout of the atomic script itself. If +// there are any errors, fail the test. +func RunCommand(t testing.TestingT, command Command) { + err := RunCommandE(t, command) + require.NoError(t, err) +} + +// RunCommandE runs a shell command and redirects its stdout and stderr to the stdout of the atomic script itself. Any +// returned error will be of type ErrWithCmdOutput, containing the output streams and the underlying error. +func RunCommandE(t testing.TestingT, command Command) error { + output, err := runCommand(t, command) + if err != nil { + return &ErrWithCmdOutput{err, output} + } + return nil +} + +// RunCommandAndGetOutput runs a shell command and returns its stdout and stderr as a string. The stdout and stderr of +// that command will also be logged with Command.Log to make debugging easier. If there are any errors, fail the test. +func RunCommandAndGetOutput(t testing.TestingT, command Command) string { + out, err := RunCommandAndGetOutputE(t, command) + require.NoError(t, err) + return out +} + +// RunCommandAndGetOutputE runs a shell command and returns its stdout and stderr as a string. The stdout and stderr of +// that command will also be logged with Command.Log to make debugging easier. Any returned error will be of type +// ErrWithCmdOutput, containing the output streams and the underlying error. +func RunCommandAndGetOutputE(t testing.TestingT, command Command) (string, error) { + output, err := runCommand(t, command) + if err != nil { + return output.Combined(), &ErrWithCmdOutput{err, output} + } + + return output.Combined(), nil +} + +// RunCommandAndGetStdOut runs a shell command and returns solely its stdout (but not stderr) as a string. The stdout and +// stderr of that command will also be logged with Command.Log to make debugging easier. If there are any errors, fail +// the test. +func RunCommandAndGetStdOut(t testing.TestingT, command Command) string { + output, err := RunCommandAndGetStdOutE(t, command) + require.NoError(t, err) + return output +} + +// RunCommandAndGetStdOutE runs a shell command and returns solely its stdout (but not stderr) as a string. The stdout +// and stderr of that command will also be printed to the stdout and stderr of this Go program to make debugging easier. +// Any returned error will be of type ErrWithCmdOutput, containing the output streams and the underlying error. +func RunCommandAndGetStdOutE(t testing.TestingT, command Command) (string, error) { + output, err := runCommand(t, command) + if err != nil { + return output.Stdout(), &ErrWithCmdOutput{err, output} + } + + return output.Stdout(), nil +} + +// RunCommandAndGetStdOutErr runs a shell command and returns solely its stdout and stderr as a string. The stdout and +// stderr of that command will also be logged with Command.Log to make debugging easier. If there are any errors, fail +// the test. +func RunCommandAndGetStdOutErr(t testing.TestingT, command Command) (stdout string, stderr string) { + stdout, stderr, err := RunCommandAndGetStdOutErrE(t, command) + require.NoError(t, err) + return stdout, stderr +} + +// RunCommandAndGetStdOutErrE runs a shell command and returns solely its stdout and stderr as a string. The stdout +// and stderr of that command will also be printed to the stdout and stderr of this Go program to make debugging easier. +// Any returned error will be of type ErrWithCmdOutput, containing the output streams and the underlying error. +func RunCommandAndGetStdOutErrE(t testing.TestingT, command Command) (stdout string, stderr string, err error) { + output, err := runCommand(t, command) + if err != nil { + return output.Stdout(), output.Stderr(), &ErrWithCmdOutput{err, output} + } + + return output.Stdout(), output.Stderr(), nil +} + +type ErrWithCmdOutput struct { + Underlying error + Output *output +} + +func (e *ErrWithCmdOutput) Error() string { + return fmt.Sprintf("error while running command: %v; %s", e.Underlying, e.Output.Stderr()) +} + +// runCommand runs a shell command and stores each line from stdout and stderr in Output. Depending on the logger, the +// stdout and stderr of that command will also be printed to the stdout and stderr of this Go program to make debugging +// easier. +func runCommand(t testing.TestingT, command Command) (*output, error) { + command.Logger.Logf(t, "Running command %s with args %s", command.Command, command.Args) + + cmd := exec.Command(command.Command, command.Args...) + cmd.Dir = command.WorkingDir + cmd.Stdin = os.Stdin + cmd.Env = formatEnvVars(command) + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + + stderr, err := cmd.StderrPipe() + if err != nil { + return nil, err + } + + err = cmd.Start() + if err != nil { + return nil, err + } + + output, err := readStdoutAndStderr(t, command.Logger, stdout, stderr) + if err != nil { + return output, err + } + + return output, cmd.Wait() +} + +// This function captures stdout and stderr into the given variables while still printing it to the stdout and stderr +// of this Go program +func readStdoutAndStderr(t testing.TestingT, log *logger.Logger, stdout, stderr io.ReadCloser) (*output, error) { + out := newOutput() + stdoutReader := bufio.NewReader(stdout) + stderrReader := bufio.NewReader(stderr) + + wg := &sync.WaitGroup{} + + wg.Add(2) + var stdoutErr, stderrErr error + go func() { + defer wg.Done() + stdoutErr = readData(t, log, stdoutReader, out.stdout) + }() + go func() { + defer wg.Done() + stderrErr = readData(t, log, stderrReader, out.stderr) + }() + wg.Wait() + + if stdoutErr != nil { + return out, stdoutErr + } + if stderrErr != nil { + return out, stderrErr + } + + return out, nil +} + +func readData(t testing.TestingT, log *logger.Logger, reader *bufio.Reader, writer io.StringWriter) error { + var line string + var readErr error + for { + line, readErr = reader.ReadString('\n') + + // remove newline, our output is in a slice, + // one element per line. + line = strings.TrimSuffix(line, "\n") + + // only return early if the line does not have + // any contents. We could have a line that does + // not not have a newline before io.EOF, we still + // need to add it to the output. + if len(line) == 0 && readErr == io.EOF { + break + } + + // logger.Logger has a Logf method, but not a Log method. + // We have to use the format string indirection to avoid + // interpreting any possible formatting characters in + // the line. + // + // See https://github.com/gruntwork-io/terratest/issues/982. + log.Logf(t, "%s", line) + + if _, err := writer.WriteString(line); err != nil { + return err + } + + if readErr != nil { + break + } + } + if readErr != io.EOF { + return readErr + } + return nil +} + +// GetExitCodeForRunCommandError tries to read the exit code for the error object returned from running a shell command. This is a bit tricky to do +// in a way that works across platforms. +func GetExitCodeForRunCommandError(err error) (int, error) { + if errWithOutput, ok := err.(*ErrWithCmdOutput); ok { + err = errWithOutput.Underlying + } + + // http://stackoverflow.com/a/10385867/483528 + if exitErr, ok := err.(*exec.ExitError); ok { + // The program has exited with an exit code != 0 + + // This works on both Unix and Windows. Although package + // syscall is generally platform dependent, WaitStatus is + // defined for both Unix and Windows and in both cases has + // an ExitStatus() method with the same signature. + if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { + return status.ExitStatus(), nil + } + return 1, errors.New("could not determine exit code") + } + + return 0, nil +} + +func formatEnvVars(command Command) []string { + env := os.Environ() + for key, value := range command.Env { + env = append(env, fmt.Sprintf("%s=%s", key, value)) + } + return env +} diff --git a/vendor/github.com/gruntwork-io/terratest/modules/shell/output.go b/vendor/github.com/gruntwork-io/terratest/modules/shell/output.go new file mode 100644 index 00000000..616f02a5 --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/modules/shell/output.go @@ -0,0 +1,92 @@ +package shell + +import ( + "strings" + "sync" +) + +// output contains the output after runnig a command. +type output struct { + stdout *outputStream + stderr *outputStream + // merged contains stdout and stderr merged into one stream. + merged *merged +} + +func newOutput() *output { + m := new(merged) + return &output{ + merged: m, + stdout: &outputStream{ + merged: m, + }, + stderr: &outputStream{ + merged: m, + }, + } +} + +func (o *output) Stdout() string { + if o == nil { + return "" + } + + return o.stdout.String() +} + +func (o *output) Stderr() string { + if o == nil { + return "" + } + + return o.stderr.String() +} + +func (o *output) Combined() string { + if o == nil { + return "" + } + + return o.merged.String() +} + +type outputStream struct { + Lines []string + *merged +} + +func (st *outputStream) WriteString(s string) (n int, err error) { + st.Lines = append(st.Lines, string(s)) + return st.merged.WriteString(s) +} + +func (st *outputStream) String() string { + if st == nil { + return "" + } + + return strings.Join(st.Lines, "\n") +} + +type merged struct { + // ensure that there are no parallel writes + sync.Mutex + Lines []string +} + +func (m *merged) String() string { + if m == nil { + return "" + } + + return strings.Join(m.Lines, "\n") +} + +func (m *merged) WriteString(s string) (n int, err error) { + m.Lock() + defer m.Unlock() + + m.Lines = append(m.Lines, string(s)) + + return len(s), nil +} diff --git a/vendor/github.com/gruntwork-io/terratest/modules/shell/shell.go b/vendor/github.com/gruntwork-io/terratest/modules/shell/shell.go new file mode 100644 index 00000000..5bbccbc8 --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/modules/shell/shell.go @@ -0,0 +1,2 @@ +// Package shell allows to run commands in a shell. +package shell diff --git a/vendor/github.com/gruntwork-io/terratest/modules/testing/types.go b/vendor/github.com/gruntwork-io/terratest/modules/testing/types.go new file mode 100644 index 00000000..13c5d902 --- /dev/null +++ b/vendor/github.com/gruntwork-io/terratest/modules/testing/types.go @@ -0,0 +1,28 @@ +package testing + +// TestingT is an interface that describes the implementation of the testing object +// that the majority of Terratest functions accept as first argument. +// Using an interface that describes testing.T instead of the actual implementation +// makes terratest usable in a wider variety of contexts (e.g. use with ginkgo : https://godoc.org/github.com/onsi/ginkgo#GinkgoT) +type TestingT interface { + //Fail marks the function as having failed but continues execution. + Fail() + // FailNow marks the function as having failed and stops its execution + // by calling runtime.Goexit (which then runs all deferred calls in the + // current goroutine). + // Execution will continue at the next test or benchmark. + // FailNow must be called from the goroutine running the + // test or benchmark function, not from other goroutines + // created during the test. Calling FailNow does not stop + // those other goroutines. + FailNow() + Fatal(args ...interface{}) + // Fatalf is equivalent to Logf followed by FailNow. + Fatalf(format string, args ...interface{}) + // Error is equivalent to Log followed by Fail. + Error(args ...interface{}) + // Errorf is equivalent to Logf followed by Fail. + Errorf(format string, args ...interface{}) + // Name returns the name of the running test or benchmark. + Name() string +} diff --git a/vendor/gotest.tools/v3/LICENSE b/vendor/gotest.tools/v3/LICENSE new file mode 100644 index 00000000..aeaa2fac --- /dev/null +++ b/vendor/gotest.tools/v3/LICENSE @@ -0,0 +1,13 @@ +Copyright 2018 gotest.tools authors + +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. diff --git a/vendor/gotest.tools/v3/assert/assert.go b/vendor/gotest.tools/v3/assert/assert.go new file mode 100644 index 00000000..c418bd07 --- /dev/null +++ b/vendor/gotest.tools/v3/assert/assert.go @@ -0,0 +1,313 @@ +/* +Package assert provides assertions for comparing expected values to actual +values in tests. When an assertion fails a helpful error message is printed. + +# Example usage + +All the assertions in this package use [testing.T.Helper] to mark themselves as +test helpers. This allows the testing package to print the filename and line +number of the file function that failed. + + assert.NilError(t, err) + // filename_test.go:212: assertion failed: error is not nil: file not found + +If any assertion is called from a helper function, make sure to call t.Helper +from the helper function so that the filename and line number remain correct. + +The examples below show assert used with some common types and the failure +messages it produces. The filename and line number portion of the failure +message is omitted from these examples for brevity. + + // booleans + + assert.Assert(t, ok) + // assertion failed: ok is false + assert.Assert(t, !missing) + // assertion failed: missing is true + + // primitives + + assert.Equal(t, count, 1) + // assertion failed: 0 (count int) != 1 (int) + assert.Equal(t, msg, "the message") + // assertion failed: my message (msg string) != the message (string) + assert.Assert(t, total != 10) // use Assert for NotEqual + // assertion failed: total is 10 + assert.Assert(t, count > 20, "count=%v", count) + // assertion failed: count is <= 20: count=1 + + // errors + + assert.NilError(t, closer.Close()) + // assertion failed: error is not nil: close /file: errno 11 + assert.Error(t, err, "the exact error message") + // assertion failed: expected error "the exact error message", got "oops" + assert.ErrorContains(t, err, "includes this") + // assertion failed: expected error to contain "includes this", got "oops" + assert.ErrorIs(t, err, os.ErrNotExist) + // assertion failed: error is "oops", not "file does not exist" (os.ErrNotExist) + + // complex types + + assert.DeepEqual(t, result, myStruct{Name: "title"}) + // assertion failed: ... (diff of the two structs) + assert.Assert(t, is.Len(items, 3)) + // assertion failed: expected [] (length 0) to have length 3 + assert.Assert(t, len(sequence) != 0) // use Assert for NotEmpty + // assertion failed: len(sequence) is 0 + assert.Assert(t, is.Contains(mapping, "key")) + // assertion failed: map[other:1] does not contain key + + // pointers and interface + + assert.Assert(t, ref == nil) + // assertion failed: ref is not nil + assert.Assert(t, ref != nil) // use Assert for NotNil + // assertion failed: ref is nil + +# Assert and Check + +[Assert] and [Check] are very similar, they both accept a [cmp.Comparison], and fail +the test when the comparison fails. The one difference is that Assert uses +[testing.T.FailNow] to fail the test, which will end the test execution immediately. +Check uses [testing.T.Fail] to fail the test, which allows it to return the +result of the comparison, then proceed with the rest of the test case. + +Like [testing.T.FailNow], [Assert] must be called from the goroutine running the test, +not from other goroutines created during the test. [Check] is safe to use from any +goroutine. + +# Comparisons + +Package [gotest.tools/v3/assert/cmp] provides +many common comparisons. Additional comparisons can be written to compare +values in other ways. See the example Assert (CustomComparison). + +# Automated migration from testify + +gty-migrate-from-testify is a command which translates Go source code from +testify assertions to the assertions provided by this package. + +See http://pkg.go.dev/gotest.tools/v3/assert/cmd/gty-migrate-from-testify. +*/ +package assert // import "gotest.tools/v3/assert" + +import ( + gocmp "github.com/google/go-cmp/cmp" + "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/internal/assert" +) + +// BoolOrComparison can be a bool, [cmp.Comparison], or error. See [Assert] for +// details about how this type is used. +type BoolOrComparison interface{} + +// TestingT is the subset of [testing.T] (see also [testing.TB]) used by the assert package. +type TestingT interface { + FailNow() + Fail() + Log(args ...interface{}) +} + +type helperT interface { + Helper() +} + +// Assert performs a comparison. If the comparison fails, the test is marked as +// failed, a failure message is logged, and execution is stopped immediately. +// +// The comparison argument may be one of three types: +// +// bool +// True is success. False is a failure. The failure message will contain +// the literal source code of the expression. +// +// cmp.Comparison +// Uses cmp.Result.Success() to check for success or failure. +// The comparison is responsible for producing a helpful failure message. +// http://pkg.go.dev/gotest.tools/v3/assert/cmp provides many common comparisons. +// +// error +// A nil value is considered success, and a non-nil error is a failure. +// The return value of error.Error is used as the failure message. +// +// Extra details can be added to the failure message using msgAndArgs. msgAndArgs +// may be either a single string, or a format string and args that will be +// passed to [fmt.Sprintf]. +// +// Assert uses [testing.TB.FailNow] to fail the test. Like t.FailNow, Assert must be called +// from the goroutine running the test function, not from other +// goroutines created during the test. Use [Check] from other goroutines. +func Assert(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !assert.Eval(t, assert.ArgsFromComparisonCall, comparison, msgAndArgs...) { + t.FailNow() + } +} + +// Check performs a comparison. If the comparison fails the test is marked as +// failed, a failure message is printed, and Check returns false. If the comparison +// is successful Check returns true. Check may be called from any goroutine. +// +// See [Assert] for details about the comparison arg and failure messages. +func Check(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) bool { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !assert.Eval(t, assert.ArgsFromComparisonCall, comparison, msgAndArgs...) { + t.Fail() + return false + } + return true +} + +// NilError fails the test immediately if err is not nil, and includes err.Error +// in the failure message. +// +// NilError uses [testing.TB.FailNow] to fail the test. Like t.FailNow, NilError must be +// called from the goroutine running the test function, not from other +// goroutines created during the test. Use [Check] from other goroutines. +func NilError(t TestingT, err error, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !assert.Eval(t, assert.ArgsAfterT, err, msgAndArgs...) { + t.FailNow() + } +} + +// Equal uses the == operator to assert two values are equal and fails the test +// if they are not equal. +// +// If the comparison fails Equal will use the variable names and types of +// x and y as part of the failure message to identify the actual and expected +// values. +// +// assert.Equal(t, actual, expected) +// // main_test.go:41: assertion failed: 1 (actual int) != 21 (expected int32) +// +// If either x or y are a multi-line string the failure message will include a +// unified diff of the two values. If the values only differ by whitespace +// the unified diff will be augmented by replacing whitespace characters with +// visible characters to identify the whitespace difference. +// +// Equal uses [testing.T.FailNow] to fail the test. Like t.FailNow, Equal must be +// called from the goroutine running the test function, not from other +// goroutines created during the test. Use [Check] with [cmp.Equal] from other +// goroutines. +func Equal(t TestingT, x, y interface{}, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !assert.Eval(t, assert.ArgsAfterT, cmp.Equal(x, y), msgAndArgs...) { + t.FailNow() + } +} + +// DeepEqual uses [github.com/google/go-cmp/cmp] +// to assert two values are equal and fails the test if they are not equal. +// +// Package [gotest.tools/v3/assert/opt] provides some additional +// commonly used Options. +// +// DeepEqual uses [testing.T.FailNow] to fail the test. Like t.FailNow, DeepEqual must be +// called from the goroutine running the test function, not from other +// goroutines created during the test. Use [Check] with [cmp.DeepEqual] from other +// goroutines. +func DeepEqual(t TestingT, x, y interface{}, opts ...gocmp.Option) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !assert.Eval(t, assert.ArgsAfterT, cmp.DeepEqual(x, y, opts...)) { + t.FailNow() + } +} + +// Error fails the test if err is nil, or if err.Error is not equal to expected. +// Both err.Error and expected will be included in the failure message. +// Error performs an exact match of the error text. Use [ErrorContains] if only +// part of the error message is relevant. Use [ErrorType] or [ErrorIs] to compare +// errors by type. +// +// Error uses [testing.T.FailNow] to fail the test. Like t.FailNow, Error must be +// called from the goroutine running the test function, not from other +// goroutines created during the test. Use [Check] with [cmp.Error] from other +// goroutines. +func Error(t TestingT, err error, expected string, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !assert.Eval(t, assert.ArgsAfterT, cmp.Error(err, expected), msgAndArgs...) { + t.FailNow() + } +} + +// ErrorContains fails the test if err is nil, or if err.Error does not +// contain the expected substring. Both err.Error and the expected substring +// will be included in the failure message. +// +// ErrorContains uses [testing.T.FailNow] to fail the test. Like t.FailNow, ErrorContains +// must be called from the goroutine running the test function, not from other +// goroutines created during the test. Use [Check] with [cmp.ErrorContains] from other +// goroutines. +func ErrorContains(t TestingT, err error, substring string, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !assert.Eval(t, assert.ArgsAfterT, cmp.ErrorContains(err, substring), msgAndArgs...) { + t.FailNow() + } +} + +// ErrorType fails the test if err is nil, or err is not the expected type. +// New code should use ErrorIs instead. +// +// Expected can be one of: +// +// func(error) bool +// The function should return true if the error is the expected type. +// +// struct{} or *struct{} +// A struct or a pointer to a struct. The assertion fails if the error is +// not of the same type. +// +// *interface{} +// A pointer to an interface type. The assertion fails if err does not +// implement the interface. +// +// reflect.Type +// The assertion fails if err does not implement the reflect.Type. +// +// ErrorType uses [testing.T.FailNow] to fail the test. Like t.FailNow, ErrorType +// must be called from the goroutine running the test function, not from other +// goroutines created during the test. Use [Check] with [cmp.ErrorType] from other +// goroutines. +// +// Deprecated: Use [ErrorIs] +func ErrorType(t TestingT, err error, expected interface{}, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !assert.Eval(t, assert.ArgsAfterT, cmp.ErrorType(err, expected), msgAndArgs...) { + t.FailNow() + } +} + +// ErrorIs fails the test if err is nil, or the error does not match expected +// when compared using errors.Is. See [errors.Is] for +// accepted arguments. +// +// ErrorIs uses [testing.T.FailNow] to fail the test. Like t.FailNow, ErrorIs +// must be called from the goroutine running the test function, not from other +// goroutines created during the test. Use [Check] with [cmp.ErrorIs] from other +// goroutines. +func ErrorIs(t TestingT, err error, expected error, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if !assert.Eval(t, assert.ArgsAfterT, cmp.ErrorIs(err, expected), msgAndArgs...) { + t.FailNow() + } +} diff --git a/vendor/gotest.tools/v3/assert/cmp/compare.go b/vendor/gotest.tools/v3/assert/cmp/compare.go new file mode 100644 index 00000000..118844f3 --- /dev/null +++ b/vendor/gotest.tools/v3/assert/cmp/compare.go @@ -0,0 +1,404 @@ +/*Package cmp provides Comparisons for Assert and Check*/ +package cmp // import "gotest.tools/v3/assert/cmp" + +import ( + "errors" + "fmt" + "reflect" + "regexp" + "strings" + + "github.com/google/go-cmp/cmp" + "gotest.tools/v3/internal/format" +) + +// Comparison is a function which compares values and returns [ResultSuccess] if +// the actual value matches the expected value. If the values do not match the +// [Result] will contain a message about why it failed. +type Comparison func() Result + +// DeepEqual compares two values using [github.com/google/go-cmp/cmp] +// and succeeds if the values are equal. +// +// The comparison can be customized using comparison Options. +// Package [gotest.tools/v3/assert/opt] provides some additional +// commonly used Options. +func DeepEqual(x, y interface{}, opts ...cmp.Option) Comparison { + return func() (result Result) { + defer func() { + if panicmsg, handled := handleCmpPanic(recover()); handled { + result = ResultFailure(panicmsg) + } + }() + diff := cmp.Diff(x, y, opts...) + if diff == "" { + return ResultSuccess + } + return multiLineDiffResult(diff, x, y) + } +} + +func handleCmpPanic(r interface{}) (string, bool) { + if r == nil { + return "", false + } + panicmsg, ok := r.(string) + if !ok { + panic(r) + } + switch { + case strings.HasPrefix(panicmsg, "cannot handle unexported field"): + return panicmsg, true + } + panic(r) +} + +func toResult(success bool, msg string) Result { + if success { + return ResultSuccess + } + return ResultFailure(msg) +} + +// RegexOrPattern may be either a [*regexp.Regexp] or a string that is a valid +// regexp pattern. +type RegexOrPattern interface{} + +// Regexp succeeds if value v matches regular expression re. +// +// Example: +// +// assert.Assert(t, cmp.Regexp("^[0-9a-f]{32}$", str)) +// r := regexp.MustCompile("^[0-9a-f]{32}$") +// assert.Assert(t, cmp.Regexp(r, str)) +func Regexp(re RegexOrPattern, v string) Comparison { + match := func(re *regexp.Regexp) Result { + return toResult( + re.MatchString(v), + fmt.Sprintf("value %q does not match regexp %q", v, re.String())) + } + + return func() Result { + switch regex := re.(type) { + case *regexp.Regexp: + return match(regex) + case string: + re, err := regexp.Compile(regex) + if err != nil { + return ResultFailure(err.Error()) + } + return match(re) + default: + return ResultFailure(fmt.Sprintf("invalid type %T for regex pattern", regex)) + } + } +} + +// Equal succeeds if x == y. See [gotest.tools/v3/assert.Equal] for full documentation. +func Equal(x, y interface{}) Comparison { + return func() Result { + switch { + case x == y: + return ResultSuccess + case isMultiLineStringCompare(x, y): + diff := format.UnifiedDiff(format.DiffConfig{A: x.(string), B: y.(string)}) + return multiLineDiffResult(diff, x, y) + } + return ResultFailureTemplate(` + {{- printf "%v" .Data.x}} ( + {{- with callArg 0 }}{{ formatNode . }} {{end -}} + {{- printf "%T" .Data.x -}} + ) != {{ printf "%v" .Data.y}} ( + {{- with callArg 1 }}{{ formatNode . }} {{end -}} + {{- printf "%T" .Data.y -}} + )`, + map[string]interface{}{"x": x, "y": y}) + } +} + +func isMultiLineStringCompare(x, y interface{}) bool { + strX, ok := x.(string) + if !ok { + return false + } + strY, ok := y.(string) + if !ok { + return false + } + return strings.Contains(strX, "\n") || strings.Contains(strY, "\n") +} + +func multiLineDiffResult(diff string, x, y interface{}) Result { + return ResultFailureTemplate(` +--- {{ with callArg 0 }}{{ formatNode . }}{{else}}←{{end}} ++++ {{ with callArg 1 }}{{ formatNode . }}{{else}}→{{end}} +{{ .Data.diff }}`, + map[string]interface{}{"diff": diff, "x": x, "y": y}) +} + +// Len succeeds if the sequence has the expected length. +func Len(seq interface{}, expected int) Comparison { + return func() (result Result) { + defer func() { + if e := recover(); e != nil { + result = ResultFailure(fmt.Sprintf("type %T does not have a length", seq)) + } + }() + value := reflect.ValueOf(seq) + length := value.Len() + if length == expected { + return ResultSuccess + } + msg := fmt.Sprintf("expected %s (length %d) to have length %d", seq, length, expected) + return ResultFailure(msg) + } +} + +// Contains succeeds if item is in collection. Collection may be a string, map, +// slice, or array. +// +// If collection is a string, item must also be a string, and is compared using +// [strings.Contains]. +// If collection is a Map, contains will succeed if item is a key in the map. +// If collection is a slice or array, item is compared to each item in the +// sequence using [reflect.DeepEqual]. +func Contains(collection interface{}, item interface{}) Comparison { + return func() Result { + colValue := reflect.ValueOf(collection) + if !colValue.IsValid() { + return ResultFailure("nil does not contain items") + } + msg := fmt.Sprintf("%v does not contain %v", collection, item) + + itemValue := reflect.ValueOf(item) + switch colValue.Type().Kind() { + case reflect.String: + if itemValue.Type().Kind() != reflect.String { + return ResultFailure("string may only contain strings") + } + return toResult( + strings.Contains(colValue.String(), itemValue.String()), + fmt.Sprintf("string %q does not contain %q", collection, item)) + + case reflect.Map: + if itemValue.Type() != colValue.Type().Key() { + return ResultFailure(fmt.Sprintf( + "%v can not contain a %v key", colValue.Type(), itemValue.Type())) + } + return toResult(colValue.MapIndex(itemValue).IsValid(), msg) + + case reflect.Slice, reflect.Array: + for i := 0; i < colValue.Len(); i++ { + if reflect.DeepEqual(colValue.Index(i).Interface(), item) { + return ResultSuccess + } + } + return ResultFailure(msg) + default: + return ResultFailure(fmt.Sprintf("type %T does not contain items", collection)) + } + } +} + +// Panics succeeds if f() panics. +func Panics(f func()) Comparison { + return func() (result Result) { + defer func() { + if err := recover(); err != nil { + result = ResultSuccess + } + }() + f() + return ResultFailure("did not panic") + } +} + +// Error succeeds if err is a non-nil error, and the error message equals the +// expected message. +func Error(err error, message string) Comparison { + return func() Result { + switch { + case err == nil: + return ResultFailure("expected an error, got nil") + case err.Error() != message: + return ResultFailure(fmt.Sprintf( + "expected error %q, got %s", message, formatErrorMessage(err))) + } + return ResultSuccess + } +} + +// ErrorContains succeeds if err is a non-nil error, and the error message contains +// the expected substring. +func ErrorContains(err error, substring string) Comparison { + return func() Result { + switch { + case err == nil: + return ResultFailure("expected an error, got nil") + case !strings.Contains(err.Error(), substring): + return ResultFailure(fmt.Sprintf( + "expected error to contain %q, got %s", substring, formatErrorMessage(err))) + } + return ResultSuccess + } +} + +type causer interface { + Cause() error +} + +func formatErrorMessage(err error) string { + //nolint:errorlint // unwrapping is not appropriate here + if _, ok := err.(causer); ok { + return fmt.Sprintf("%q\n%+v", err, err) + } + // This error was not wrapped with github.com/pkg/errors + return fmt.Sprintf("%q", err) +} + +// Nil succeeds if obj is a nil interface, pointer, or function. +// +// Use [gotest.tools/v3/assert.NilError] for comparing errors. Use Len(obj, 0) for comparing slices, +// maps, and channels. +func Nil(obj interface{}) Comparison { + msgFunc := func(value reflect.Value) string { + return fmt.Sprintf("%v (type %s) is not nil", reflect.Indirect(value), value.Type()) + } + return isNil(obj, msgFunc) +} + +func isNil(obj interface{}, msgFunc func(reflect.Value) string) Comparison { + return func() Result { + if obj == nil { + return ResultSuccess + } + value := reflect.ValueOf(obj) + kind := value.Type().Kind() + if kind >= reflect.Chan && kind <= reflect.Slice { + if value.IsNil() { + return ResultSuccess + } + return ResultFailure(msgFunc(value)) + } + + return ResultFailure(fmt.Sprintf("%v (type %s) can not be nil", value, value.Type())) + } +} + +// ErrorType succeeds if err is not nil and is of the expected type. +// +// Expected can be one of: +// +// func(error) bool +// +// Function should return true if the error is the expected type. +// +// type struct{}, type &struct{} +// +// A struct or a pointer to a struct. +// Fails if the error is not of the same type as expected. +// +// type &interface{} +// +// A pointer to an interface type. +// Fails if err does not implement the interface. +// +// reflect.Type +// +// Fails if err does not implement the [reflect.Type]. +// +// Deprecated: Use [ErrorIs] +func ErrorType(err error, expected interface{}) Comparison { + return func() Result { + switch expectedType := expected.(type) { + case func(error) bool: + return cmpErrorTypeFunc(err, expectedType) + case reflect.Type: + if expectedType.Kind() == reflect.Interface { + return cmpErrorTypeImplementsType(err, expectedType) + } + return cmpErrorTypeEqualType(err, expectedType) + case nil: + return ResultFailure("invalid type for expected: nil") + } + + expectedType := reflect.TypeOf(expected) + switch { + case expectedType.Kind() == reflect.Struct, isPtrToStruct(expectedType): + return cmpErrorTypeEqualType(err, expectedType) + case isPtrToInterface(expectedType): + return cmpErrorTypeImplementsType(err, expectedType.Elem()) + } + return ResultFailure(fmt.Sprintf("invalid type for expected: %T", expected)) + } +} + +func cmpErrorTypeFunc(err error, f func(error) bool) Result { + if f(err) { + return ResultSuccess + } + actual := "nil" + if err != nil { + actual = fmt.Sprintf("%s (%T)", err, err) + } + return ResultFailureTemplate(`error is {{ .Data.actual }} + {{- with callArg 1 }}, not {{ formatNode . }}{{end -}}`, + map[string]interface{}{"actual": actual}) +} + +func cmpErrorTypeEqualType(err error, expectedType reflect.Type) Result { + if err == nil { + return ResultFailure(fmt.Sprintf("error is nil, not %s", expectedType)) + } + errValue := reflect.ValueOf(err) + if errValue.Type() == expectedType { + return ResultSuccess + } + return ResultFailure(fmt.Sprintf("error is %s (%T), not %s", err, err, expectedType)) +} + +func cmpErrorTypeImplementsType(err error, expectedType reflect.Type) Result { + if err == nil { + return ResultFailure(fmt.Sprintf("error is nil, not %s", expectedType)) + } + errValue := reflect.ValueOf(err) + if errValue.Type().Implements(expectedType) { + return ResultSuccess + } + return ResultFailure(fmt.Sprintf("error is %s (%T), not %s", err, err, expectedType)) +} + +func isPtrToInterface(typ reflect.Type) bool { + return typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Interface +} + +func isPtrToStruct(typ reflect.Type) bool { + return typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Struct +} + +var ( + stdlibErrorNewType = reflect.TypeOf(errors.New("")) + stdlibFmtErrorType = reflect.TypeOf(fmt.Errorf("%w", fmt.Errorf(""))) +) + +// ErrorIs succeeds if errors.Is(actual, expected) returns true. See +// [errors.Is] for accepted argument values. +func ErrorIs(actual error, expected error) Comparison { + return func() Result { + if errors.Is(actual, expected) { + return ResultSuccess + } + + // The type of stdlib errors is excluded because the type is not relevant + // in those cases. The type is only important when it is a user defined + // custom error type. + return ResultFailureTemplate(`error is + {{- if not .Data.a }} nil,{{ else }} + {{- printf " \"%v\"" .Data.a }} + {{- if notStdlibErrorType .Data.a }} ({{ printf "%T" .Data.a }}){{ end }}, + {{- end }} not {{ printf "\"%v\"" .Data.x }} ( + {{- with callArg 1 }}{{ formatNode . }}{{ end }} + {{- if notStdlibErrorType .Data.x }}{{ printf " %T" .Data.x }}{{ end }})`, + map[string]interface{}{"a": actual, "x": expected}) + } +} diff --git a/vendor/gotest.tools/v3/assert/cmp/result.go b/vendor/gotest.tools/v3/assert/cmp/result.go new file mode 100644 index 00000000..9992ede5 --- /dev/null +++ b/vendor/gotest.tools/v3/assert/cmp/result.go @@ -0,0 +1,110 @@ +package cmp + +import ( + "bytes" + "fmt" + "go/ast" + "reflect" + "text/template" + + "gotest.tools/v3/internal/source" +) + +// A Result of a [Comparison]. +type Result interface { + Success() bool +} + +// StringResult is an implementation of [Result] that reports the error message +// string verbatim and does not provide any templating or formatting of the +// message. +type StringResult struct { + success bool + message string +} + +// Success returns true if the comparison was successful. +func (r StringResult) Success() bool { + return r.success +} + +// FailureMessage returns the message used to provide additional information +// about the failure. +func (r StringResult) FailureMessage() string { + return r.message +} + +// ResultSuccess is a constant which is returned by a [Comparison] to +// indicate success. +var ResultSuccess = StringResult{success: true} + +// ResultFailure returns a failed [Result] with a failure message. +func ResultFailure(message string) StringResult { + return StringResult{message: message} +} + +// ResultFromError returns [ResultSuccess] if err is nil. Otherwise [ResultFailure] +// is returned with the error message as the failure message. +func ResultFromError(err error) Result { + if err == nil { + return ResultSuccess + } + return ResultFailure(err.Error()) +} + +type templatedResult struct { + template string + data map[string]interface{} +} + +func (r templatedResult) Success() bool { + return false +} + +func (r templatedResult) FailureMessage(args []ast.Expr) string { + msg, err := renderMessage(r, args) + if err != nil { + return fmt.Sprintf("failed to render failure message: %s", err) + } + return msg +} + +func (r templatedResult) UpdatedExpected(stackIndex int) error { + // TODO: would be nice to have structured data instead of a map + return source.UpdateExpectedValue(stackIndex+1, r.data["x"], r.data["y"]) +} + +// ResultFailureTemplate returns a [Result] with a template string and data which +// can be used to format a failure message. The template may access data from .Data, +// the comparison args with the callArg function, and the formatNode function may +// be used to format the call args. +func ResultFailureTemplate(template string, data map[string]interface{}) Result { + return templatedResult{template: template, data: data} +} + +func renderMessage(result templatedResult, args []ast.Expr) (string, error) { + tmpl := template.New("failure").Funcs(template.FuncMap{ + "formatNode": source.FormatNode, + "callArg": func(index int) ast.Expr { + if index >= len(args) { + return nil + } + return args[index] + }, + // TODO: any way to include this from ErrorIS instead of here? + "notStdlibErrorType": func(typ interface{}) bool { + r := reflect.TypeOf(typ) + return r != stdlibFmtErrorType && r != stdlibErrorNewType + }, + }) + var err error + tmpl, err = tmpl.Parse(result.template) + if err != nil { + return "", err + } + buf := new(bytes.Buffer) + err = tmpl.Execute(buf, map[string]interface{}{ + "Data": result.data, + }) + return buf.String(), err +} diff --git a/vendor/gotest.tools/v3/icmd/command.go b/vendor/gotest.tools/v3/icmd/command.go new file mode 100644 index 00000000..a3e167a0 --- /dev/null +++ b/vendor/gotest.tools/v3/icmd/command.go @@ -0,0 +1,295 @@ +/*Package icmd executes binaries and provides convenient assertions for testing the results. + */ +package icmd // import "gotest.tools/v3/icmd" + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "strings" + "sync" + "time" + + "gotest.tools/v3/assert" + "gotest.tools/v3/assert/cmp" +) + +type helperT interface { + Helper() +} + +// None is a token to inform Result.Assert that the output should be empty +const None = "[NOTHING]" + +type lockedBuffer struct { + m sync.RWMutex + buf bytes.Buffer +} + +func (buf *lockedBuffer) Write(b []byte) (int, error) { + buf.m.Lock() + defer buf.m.Unlock() + return buf.buf.Write(b) +} + +func (buf *lockedBuffer) String() string { + buf.m.RLock() + defer buf.m.RUnlock() + return buf.buf.String() +} + +// Result stores the result of running a command +type Result struct { + Cmd *exec.Cmd + ExitCode int + Error error + // Timeout is true if the command was killed because it ran for too long + Timeout bool + outBuffer *lockedBuffer + errBuffer *lockedBuffer +} + +// Assert compares the Result against the Expected struct, and fails the test if +// any of the expectations are not met. +// +// This function is equivalent to assert.Assert(t, result.Equal(exp)). +func (r *Result) Assert(t assert.TestingT, exp Expected) *Result { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + assert.Assert(t, r.Equal(exp)) + return r +} + +// Equal compares the result to Expected. If the result doesn't match expected +// returns a formatted failure message with the command, stdout, stderr, exit code, +// and any failed expectations. +func (r *Result) Equal(exp Expected) cmp.Comparison { + return func() cmp.Result { + return cmp.ResultFromError(r.match(exp)) + } +} + +// Compare the result to Expected and return an error if they do not match. +func (r *Result) Compare(exp Expected) error { + return r.match(exp) +} + +func (r *Result) match(exp Expected) error { + errors := []string{} + add := func(format string, args ...interface{}) { + errors = append(errors, fmt.Sprintf(format, args...)) + } + + if exp.ExitCode != r.ExitCode { + add("ExitCode was %d expected %d", r.ExitCode, exp.ExitCode) + } + if exp.Timeout != r.Timeout { + if exp.Timeout { + add("Expected command to timeout") + } else { + add("Expected command to finish, but it hit the timeout") + } + } + if !matchOutput(exp.Out, r.Stdout()) { + add("Expected stdout to contain %q", exp.Out) + } + if !matchOutput(exp.Err, r.Stderr()) { + add("Expected stderr to contain %q", exp.Err) + } + switch { + // If a non-zero exit code is expected there is going to be an error. + // Don't require an error message as well as an exit code because the + // error message is going to be "exit status which is not useful + case exp.Error == "" && exp.ExitCode != 0: + case exp.Error == "" && r.Error != nil: + add("Expected no error") + case exp.Error != "" && r.Error == nil: + add("Expected error to contain %q, but there was no error", exp.Error) + case exp.Error != "" && !strings.Contains(r.Error.Error(), exp.Error): + add("Expected error to contain %q", exp.Error) + } + + if len(errors) == 0 { + return nil + } + return fmt.Errorf("%s\nFailures:\n%s", r, strings.Join(errors, "\n")) +} + +func matchOutput(expected string, actual string) bool { + switch expected { + case None: + return actual == "" + default: + return strings.Contains(actual, expected) + } +} + +func (r *Result) String() string { + var timeout string + if r.Timeout { + timeout = " (timeout)" + } + var errString string + if r.Error != nil { + errString = "\nError: " + r.Error.Error() + } + + return fmt.Sprintf(` +Command: %s +ExitCode: %d%s%s +Stdout: %v +Stderr: %v +`, + strings.Join(r.Cmd.Args, " "), + r.ExitCode, + timeout, + errString, + r.Stdout(), + r.Stderr()) +} + +// Expected is the expected output from a Command. This struct is compared to a +// Result struct by Result.Assert(). +type Expected struct { + ExitCode int + Timeout bool + Error string + Out string + Err string +} + +// Success is the default expected result. A Success result is one with a 0 +// ExitCode. +var Success = Expected{} + +// Stdout returns the stdout of the process as a string +func (r *Result) Stdout() string { + return r.outBuffer.String() +} + +// Stderr returns the stderr of the process as a string +func (r *Result) Stderr() string { + return r.errBuffer.String() +} + +// Combined returns the stdout and stderr combined into a single string +func (r *Result) Combined() string { + return r.outBuffer.String() + r.errBuffer.String() +} + +func (r *Result) setExitError(err error) { + if err == nil { + return + } + r.Error = err + r.ExitCode = processExitCode(err) +} + +// Cmd contains the arguments and options for a process to run as part of a test +// suite. +type Cmd struct { + Command []string + Timeout time.Duration + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + Dir string + Env []string + ExtraFiles []*os.File +} + +// Command create a simple Cmd with the specified command and arguments +func Command(command string, args ...string) Cmd { + return Cmd{Command: append([]string{command}, args...)} +} + +// RunCmd runs a command and returns a Result +func RunCmd(cmd Cmd, cmdOperators ...CmdOp) *Result { + result := StartCmd(cmd, cmdOperators...) + if result.Error != nil { + return result + } + return WaitOnCmd(cmd.Timeout, result) +} + +// RunCommand runs a command with default options, and returns a result +func RunCommand(command string, args ...string) *Result { + return RunCmd(Command(command, args...)) +} + +// StartCmd starts a command, but doesn't wait for it to finish +func StartCmd(cmd Cmd, cmdOperators ...CmdOp) *Result { + for _, op := range cmdOperators { + op(&cmd) + } + result := buildCmd(cmd) + if result.Error != nil { + return result + } + result.setExitError(result.Cmd.Start()) + return result +} + +// TODO: support exec.CommandContext +func buildCmd(cmd Cmd) *Result { + var execCmd *exec.Cmd + switch len(cmd.Command) { + case 1: + execCmd = exec.Command(cmd.Command[0]) + default: + execCmd = exec.Command(cmd.Command[0], cmd.Command[1:]...) + } + outBuffer := new(lockedBuffer) + errBuffer := new(lockedBuffer) + + execCmd.Stdin = cmd.Stdin + execCmd.Dir = cmd.Dir + execCmd.Env = cmd.Env + if cmd.Stdout != nil { + execCmd.Stdout = io.MultiWriter(outBuffer, cmd.Stdout) + } else { + execCmd.Stdout = outBuffer + } + if cmd.Stderr != nil { + execCmd.Stderr = io.MultiWriter(errBuffer, cmd.Stderr) + } else { + execCmd.Stderr = errBuffer + } + execCmd.ExtraFiles = cmd.ExtraFiles + + return &Result{ + Cmd: execCmd, + outBuffer: outBuffer, + errBuffer: errBuffer, + } +} + +// WaitOnCmd waits for a command to complete. If timeout is non-nil then +// only wait until the timeout. +func WaitOnCmd(timeout time.Duration, result *Result) *Result { + if timeout == time.Duration(0) { + result.setExitError(result.Cmd.Wait()) + return result + } + + done := make(chan error, 1) + // Wait for command to exit in a goroutine + go func() { + done <- result.Cmd.Wait() + }() + + select { + case <-time.After(timeout): + killErr := result.Cmd.Process.Kill() + if killErr != nil { + fmt.Printf("failed to kill (pid=%d): %v\n", result.Cmd.Process.Pid, killErr) + } + result.Timeout = true + case err := <-done: + result.setExitError(err) + } + return result +} diff --git a/vendor/gotest.tools/v3/icmd/exitcode.go b/vendor/gotest.tools/v3/icmd/exitcode.go new file mode 100644 index 00000000..4e48fc4f --- /dev/null +++ b/vendor/gotest.tools/v3/icmd/exitcode.go @@ -0,0 +1,23 @@ +package icmd + +import ( + "errors" + "os/exec" +) + +func processExitCode(err error) int { + if err == nil { + return 0 + } + + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + if exitErr.ProcessState == nil { + return 0 + } + if code := exitErr.ProcessState.ExitCode(); code != -1 { + return code + } + } + return 127 +} diff --git a/vendor/gotest.tools/v3/icmd/ops.go b/vendor/gotest.tools/v3/icmd/ops.go new file mode 100644 index 00000000..aa3bc1e8 --- /dev/null +++ b/vendor/gotest.tools/v3/icmd/ops.go @@ -0,0 +1,60 @@ +package icmd + +import ( + "io" + "os" + "time" +) + +// CmdOp is an operation which modified a Cmd structure used to execute commands +type CmdOp func(*Cmd) + +// WithTimeout sets the timeout duration of the command +func WithTimeout(timeout time.Duration) CmdOp { + return func(c *Cmd) { + c.Timeout = timeout + } +} + +// WithEnv sets the environment variable of the command. +// Each arguments are in the form of KEY=VALUE +func WithEnv(env ...string) CmdOp { + return func(c *Cmd) { + c.Env = env + } +} + +// Dir sets the working directory of the command +func Dir(path string) CmdOp { + return func(c *Cmd) { + c.Dir = path + } +} + +// WithStdin sets the standard input of the command to the specified reader +func WithStdin(r io.Reader) CmdOp { + return func(c *Cmd) { + c.Stdin = r + } +} + +// WithStdout sets the standard output of the command to the specified writer +func WithStdout(w io.Writer) CmdOp { + return func(c *Cmd) { + c.Stdout = w + } +} + +// WithStderr sets the standard error of the command to the specified writer +func WithStderr(w io.Writer) CmdOp { + return func(c *Cmd) { + c.Stderr = w + } +} + +// WithExtraFile adds a file descriptor to the command +func WithExtraFile(f *os.File) CmdOp { + return func(c *Cmd) { + c.ExtraFiles = append(c.ExtraFiles, f) + } +} diff --git a/vendor/gotest.tools/v3/internal/assert/assert.go b/vendor/gotest.tools/v3/internal/assert/assert.go new file mode 100644 index 00000000..2dd80255 --- /dev/null +++ b/vendor/gotest.tools/v3/internal/assert/assert.go @@ -0,0 +1,161 @@ +// Package assert provides internal utilties for assertions. +package assert + +import ( + "fmt" + "go/ast" + "go/token" + "reflect" + + "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/internal/format" + "gotest.tools/v3/internal/source" +) + +// LogT is the subset of testing.T used by the assert package. +type LogT interface { + Log(args ...interface{}) +} + +type helperT interface { + Helper() +} + +const failureMessage = "assertion failed: " + +// Eval the comparison and print a failure messages if the comparison has failed. +func Eval( + t LogT, + argSelector argSelector, + comparison interface{}, + msgAndArgs ...interface{}, +) bool { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + var success bool + switch check := comparison.(type) { + case bool: + if check { + return true + } + logFailureFromBool(t, msgAndArgs...) + + // Undocumented legacy comparison without Result type + case func() (success bool, message string): + success = runCompareFunc(t, check, msgAndArgs...) + + case nil: + return true + + case error: + msg := failureMsgFromError(check) + t.Log(format.WithCustomMessage(failureMessage+msg, msgAndArgs...)) + + case cmp.Comparison: + success = RunComparison(t, argSelector, check, msgAndArgs...) + + case func() cmp.Result: + success = RunComparison(t, argSelector, check, msgAndArgs...) + + default: + t.Log(fmt.Sprintf("invalid Comparison: %v (%T)", check, check)) + } + return success +} + +func runCompareFunc( + t LogT, + f func() (success bool, message string), + msgAndArgs ...interface{}, +) bool { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + if success, message := f(); !success { + t.Log(format.WithCustomMessage(failureMessage+message, msgAndArgs...)) + return false + } + return true +} + +func logFailureFromBool(t LogT, msgAndArgs ...interface{}) { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + const stackIndex = 3 // Assert()/Check(), assert(), logFailureFromBool() + args, err := source.CallExprArgs(stackIndex) + if err != nil { + t.Log(err.Error()) + return + } + + const comparisonArgIndex = 1 // Assert(t, comparison) + if len(args) <= comparisonArgIndex { + t.Log(failureMessage + "but assert failed to find the expression to print") + return + } + + msg, err := boolFailureMessage(args[comparisonArgIndex]) + if err != nil { + t.Log(err.Error()) + msg = "expression is false" + } + + t.Log(format.WithCustomMessage(failureMessage+msg, msgAndArgs...)) +} + +func failureMsgFromError(err error) string { + // Handle errors with non-nil types + v := reflect.ValueOf(err) + if v.Kind() == reflect.Ptr && v.IsNil() { + return fmt.Sprintf("error is not nil: error has type %T", err) + } + return "error is not nil: " + err.Error() +} + +func boolFailureMessage(expr ast.Expr) (string, error) { + if binaryExpr, ok := expr.(*ast.BinaryExpr); ok { + x, err := source.FormatNode(binaryExpr.X) + if err != nil { + return "", err + } + y, err := source.FormatNode(binaryExpr.Y) + if err != nil { + return "", err + } + + switch binaryExpr.Op { + case token.NEQ: + return x + " is " + y, nil + case token.EQL: + return x + " is not " + y, nil + case token.GTR: + return x + " is <= " + y, nil + case token.LSS: + return x + " is >= " + y, nil + case token.GEQ: + return x + " is less than " + y, nil + case token.LEQ: + return x + " is greater than " + y, nil + } + } + + if unaryExpr, ok := expr.(*ast.UnaryExpr); ok && unaryExpr.Op == token.NOT { + x, err := source.FormatNode(unaryExpr.X) + if err != nil { + return "", err + } + return x + " is true", nil + } + + if ident, ok := expr.(*ast.Ident); ok { + return ident.Name + " is false", nil + } + + formatted, err := source.FormatNode(expr) + if err != nil { + return "", err + } + return "expression is false: " + formatted, nil +} diff --git a/vendor/gotest.tools/v3/internal/assert/result.go b/vendor/gotest.tools/v3/internal/assert/result.go new file mode 100644 index 00000000..bb8741eb --- /dev/null +++ b/vendor/gotest.tools/v3/internal/assert/result.go @@ -0,0 +1,146 @@ +package assert + +import ( + "errors" + "fmt" + "go/ast" + + "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/internal/format" + "gotest.tools/v3/internal/source" +) + +// RunComparison and return Comparison.Success. If the comparison fails a messages +// will be printed using t.Log. +func RunComparison( + t LogT, + argSelector argSelector, + f cmp.Comparison, + msgAndArgs ...interface{}, +) bool { + if ht, ok := t.(helperT); ok { + ht.Helper() + } + result := f() + if result.Success() { + return true + } + + if source.IsUpdate() { + if updater, ok := result.(updateExpected); ok { + const stackIndex = 3 // Assert/Check, assert, RunComparison + err := updater.UpdatedExpected(stackIndex) + switch { + case err == nil: + return true + case errors.Is(err, source.ErrNotFound): + // do nothing, fallthrough to regular failure message + default: + t.Log("failed to update source", err) + return false + } + } + } + + var message string + switch typed := result.(type) { + case resultWithComparisonArgs: + const stackIndex = 3 // Assert/Check, assert, RunComparison + args, err := source.CallExprArgs(stackIndex) + if err != nil { + t.Log(err.Error()) + } + message = typed.FailureMessage(filterPrintableExpr(argSelector(args))) + case resultBasic: + message = typed.FailureMessage() + default: + message = fmt.Sprintf("comparison returned invalid Result type: %T", result) + } + + t.Log(format.WithCustomMessage(failureMessage+message, msgAndArgs...)) + return false +} + +type resultWithComparisonArgs interface { + FailureMessage(args []ast.Expr) string +} + +type resultBasic interface { + FailureMessage() string +} + +type updateExpected interface { + UpdatedExpected(stackIndex int) error +} + +// filterPrintableExpr filters the ast.Expr slice to only include Expr that are +// easy to read when printed and contain relevant information to an assertion. +// +// Ident and SelectorExpr are included because they print nicely and the variable +// names may provide additional context to their values. +// BasicLit and CompositeLit are excluded because their source is equivalent to +// their value, which is already available. +// Other types are ignored for now, but could be added if they are relevant. +func filterPrintableExpr(args []ast.Expr) []ast.Expr { + result := make([]ast.Expr, len(args)) + for i, arg := range args { + if isShortPrintableExpr(arg) { + result[i] = arg + continue + } + + if starExpr, ok := arg.(*ast.StarExpr); ok { + result[i] = starExpr.X + continue + } + } + return result +} + +func isShortPrintableExpr(expr ast.Expr) bool { + switch expr.(type) { + case *ast.Ident, *ast.SelectorExpr, *ast.IndexExpr, *ast.SliceExpr: + return true + case *ast.BinaryExpr, *ast.UnaryExpr: + return true + default: + // CallExpr, ParenExpr, TypeAssertExpr, KeyValueExpr, StarExpr + return false + } +} + +type argSelector func([]ast.Expr) []ast.Expr + +// ArgsAfterT selects args starting at position 1. Used when the caller has a +// testing.T as the first argument, and the args to select should follow it. +func ArgsAfterT(args []ast.Expr) []ast.Expr { + if len(args) < 1 { + return nil + } + return args[1:] +} + +// ArgsFromComparisonCall selects args from the CallExpression at position 1. +// Used when the caller has a testing.T as the first argument, and the args to +// select are passed to the cmp.Comparison at position 1. +func ArgsFromComparisonCall(args []ast.Expr) []ast.Expr { + if len(args) <= 1 { + return nil + } + if callExpr, ok := args[1].(*ast.CallExpr); ok { + return callExpr.Args + } + return nil +} + +// ArgsAtZeroIndex selects args from the CallExpression at position 1. +// Used when the caller accepts a single cmp.Comparison argument. +func ArgsAtZeroIndex(args []ast.Expr) []ast.Expr { + if len(args) == 0 { + return nil + } + if callExpr, ok := args[0].(*ast.CallExpr); ok { + return callExpr.Args + } + return nil +} diff --git a/vendor/gotest.tools/v3/internal/difflib/LICENSE b/vendor/gotest.tools/v3/internal/difflib/LICENSE new file mode 100644 index 00000000..c67dad61 --- /dev/null +++ b/vendor/gotest.tools/v3/internal/difflib/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2013, Patrick Mezard +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + The names of its contributors may not be used to endorse or promote +products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/gotest.tools/v3/internal/difflib/difflib.go b/vendor/gotest.tools/v3/internal/difflib/difflib.go new file mode 100644 index 00000000..9bf506b6 --- /dev/null +++ b/vendor/gotest.tools/v3/internal/difflib/difflib.go @@ -0,0 +1,423 @@ +/*Package difflib is a partial port of Python difflib module. + +Original source: https://github.com/pmezard/go-difflib + +This file is trimmed to only the parts used by this repository. +*/ +package difflib // import "gotest.tools/v3/internal/difflib" + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +// Match stores line numbers of size of match +type Match struct { + A int + B int + Size int +} + +// OpCode identifies the type of diff +type OpCode struct { + Tag byte + I1 int + I2 int + J1 int + J2 int +} + +// SequenceMatcher compares sequence of strings. The basic +// algorithm predates, and is a little fancier than, an algorithm +// published in the late 1980's by Ratcliff and Obershelp under the +// hyperbolic name "gestalt pattern matching". The basic idea is to find +// the longest contiguous matching subsequence that contains no "junk" +// elements (R-O doesn't address junk). The same idea is then applied +// recursively to the pieces of the sequences to the left and to the right +// of the matching subsequence. This does not yield minimal edit +// sequences, but does tend to yield matches that "look right" to people. +// +// SequenceMatcher tries to compute a "human-friendly diff" between two +// sequences. Unlike e.g. UNIX(tm) diff, the fundamental notion is the +// longest *contiguous* & junk-free matching subsequence. That's what +// catches peoples' eyes. The Windows(tm) windiff has another interesting +// notion, pairing up elements that appear uniquely in each sequence. +// That, and the method here, appear to yield more intuitive difference +// reports than does diff. This method appears to be the least vulnerable +// to synching up on blocks of "junk lines", though (like blank lines in +// ordinary text files, or maybe "

" lines in HTML files). That may be +// because this is the only method of the 3 that has a *concept* of +// "junk" . +// +// Timing: Basic R-O is cubic time worst case and quadratic time expected +// case. SequenceMatcher is quadratic time for the worst case and has +// expected-case behavior dependent in a complicated way on how many +// elements the sequences have in common; best case time is linear. +type SequenceMatcher struct { + a []string + b []string + b2j map[string][]int + IsJunk func(string) bool + autoJunk bool + bJunk map[string]struct{} + matchingBlocks []Match + fullBCount map[string]int + bPopular map[string]struct{} + opCodes []OpCode +} + +// NewMatcher returns a new SequenceMatcher +func NewMatcher(a, b []string) *SequenceMatcher { + m := SequenceMatcher{autoJunk: true} + m.SetSeqs(a, b) + return &m +} + +// SetSeqs sets two sequences to be compared. +func (m *SequenceMatcher) SetSeqs(a, b []string) { + m.SetSeq1(a) + m.SetSeq2(b) +} + +// SetSeq1 sets the first sequence to be compared. The second sequence to be compared is +// not changed. +// +// SequenceMatcher computes and caches detailed information about the second +// sequence, so if you want to compare one sequence S against many sequences, +// use .SetSeq2(s) once and call .SetSeq1(x) repeatedly for each of the other +// sequences. +// +// See also SetSeqs() and SetSeq2(). +func (m *SequenceMatcher) SetSeq1(a []string) { + if &a == &m.a { + return + } + m.a = a + m.matchingBlocks = nil + m.opCodes = nil +} + +// SetSeq2 sets the second sequence to be compared. The first sequence to be compared is +// not changed. +func (m *SequenceMatcher) SetSeq2(b []string) { + if &b == &m.b { + return + } + m.b = b + m.matchingBlocks = nil + m.opCodes = nil + m.fullBCount = nil + m.chainB() +} + +func (m *SequenceMatcher) chainB() { + // Populate line -> index mapping + b2j := map[string][]int{} + for i, s := range m.b { + indices := b2j[s] + indices = append(indices, i) + b2j[s] = indices + } + + // Purge junk elements + m.bJunk = map[string]struct{}{} + if m.IsJunk != nil { + junk := m.bJunk + for s := range b2j { + if m.IsJunk(s) { + junk[s] = struct{}{} + } + } + for s := range junk { + delete(b2j, s) + } + } + + // Purge remaining popular elements + popular := map[string]struct{}{} + n := len(m.b) + if m.autoJunk && n >= 200 { + ntest := n/100 + 1 + for s, indices := range b2j { + if len(indices) > ntest { + popular[s] = struct{}{} + } + } + for s := range popular { + delete(b2j, s) + } + } + m.bPopular = popular + m.b2j = b2j +} + +func (m *SequenceMatcher) isBJunk(s string) bool { + _, ok := m.bJunk[s] + return ok +} + +// Find longest matching block in a[alo:ahi] and b[blo:bhi]. +// +// If IsJunk is not defined: +// +// Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where +// alo <= i <= i+k <= ahi +// blo <= j <= j+k <= bhi +// and for all (i',j',k') meeting those conditions, +// k >= k' +// i <= i' +// and if i == i', j <= j' +// +// In other words, of all maximal matching blocks, return one that +// starts earliest in a, and of all those maximal matching blocks that +// start earliest in a, return the one that starts earliest in b. +// +// If IsJunk is defined, first the longest matching block is +// determined as above, but with the additional restriction that no +// junk element appears in the block. Then that block is extended as +// far as possible by matching (only) junk elements on both sides. So +// the resulting block never matches on junk except as identical junk +// happens to be adjacent to an "interesting" match. +// +// If no blocks match, return (alo, blo, 0). +func (m *SequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) Match { + // CAUTION: stripping common prefix or suffix would be incorrect. + // E.g., + // ab + // acab + // Longest matching block is "ab", but if common prefix is + // stripped, it's "a" (tied with "b"). UNIX(tm) diff does so + // strip, so ends up claiming that ab is changed to acab by + // inserting "ca" in the middle. That's minimal but unintuitive: + // "it's obvious" that someone inserted "ac" at the front. + // Windiff ends up at the same place as diff, but by pairing up + // the unique 'b's and then matching the first two 'a's. + besti, bestj, bestsize := alo, blo, 0 + + // find longest junk-free match + // during an iteration of the loop, j2len[j] = length of longest + // junk-free match ending with a[i-1] and b[j] + j2len := map[int]int{} + for i := alo; i != ahi; i++ { + // look at all instances of a[i] in b; note that because + // b2j has no junk keys, the loop is skipped if a[i] is junk + newj2len := map[int]int{} + for _, j := range m.b2j[m.a[i]] { + // a[i] matches b[j] + if j < blo { + continue + } + if j >= bhi { + break + } + k := j2len[j-1] + 1 + newj2len[j] = k + if k > bestsize { + besti, bestj, bestsize = i-k+1, j-k+1, k + } + } + j2len = newj2len + } + + // Extend the best by non-junk elements on each end. In particular, + // "popular" non-junk elements aren't in b2j, which greatly speeds + // the inner loop above, but also means "the best" match so far + // doesn't contain any junk *or* popular non-junk elements. + for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) && + m.a[besti-1] == m.b[bestj-1] { + besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 + } + for besti+bestsize < ahi && bestj+bestsize < bhi && + !m.isBJunk(m.b[bestj+bestsize]) && + m.a[besti+bestsize] == m.b[bestj+bestsize] { + bestsize += 1 + } + + // Now that we have a wholly interesting match (albeit possibly + // empty!), we may as well suck up the matching junk on each + // side of it too. Can't think of a good reason not to, and it + // saves post-processing the (possibly considerable) expense of + // figuring out what to do with it. In the case of an empty + // interesting match, this is clearly the right thing to do, + // because no other kind of match is possible in the regions. + for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) && + m.a[besti-1] == m.b[bestj-1] { + besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 + } + for besti+bestsize < ahi && bestj+bestsize < bhi && + m.isBJunk(m.b[bestj+bestsize]) && + m.a[besti+bestsize] == m.b[bestj+bestsize] { + bestsize += 1 + } + + return Match{A: besti, B: bestj, Size: bestsize} +} + +// GetMatchingBlocks returns a list of triples describing matching subsequences. +// +// Each triple is of the form (i, j, n), and means that +// a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in +// i and in j. It's also guaranteed that if (i, j, n) and (i', j', n') are +// adjacent triples in the list, and the second is not the last triple in the +// list, then i+n != i' or j+n != j'. IOW, adjacent triples never describe +// adjacent equal blocks. +// +// The last triple is a dummy, (len(a), len(b), 0), and is the only +// triple with n==0. +func (m *SequenceMatcher) GetMatchingBlocks() []Match { + if m.matchingBlocks != nil { + return m.matchingBlocks + } + + var matchBlocks func(alo, ahi, blo, bhi int, matched []Match) []Match + matchBlocks = func(alo, ahi, blo, bhi int, matched []Match) []Match { + match := m.findLongestMatch(alo, ahi, blo, bhi) + i, j, k := match.A, match.B, match.Size + if match.Size > 0 { + if alo < i && blo < j { + matched = matchBlocks(alo, i, blo, j, matched) + } + matched = append(matched, match) + if i+k < ahi && j+k < bhi { + matched = matchBlocks(i+k, ahi, j+k, bhi, matched) + } + } + return matched + } + matched := matchBlocks(0, len(m.a), 0, len(m.b), nil) + + // It's possible that we have adjacent equal blocks in the + // matching_blocks list now. + nonAdjacent := []Match{} + i1, j1, k1 := 0, 0, 0 + for _, b := range matched { + // Is this block adjacent to i1, j1, k1? + i2, j2, k2 := b.A, b.B, b.Size + if i1+k1 == i2 && j1+k1 == j2 { + // Yes, so collapse them -- this just increases the length of + // the first block by the length of the second, and the first + // block so lengthened remains the block to compare against. + k1 += k2 + } else { + // Not adjacent. Remember the first block (k1==0 means it's + // the dummy we started with), and make the second block the + // new block to compare against. + if k1 > 0 { + nonAdjacent = append(nonAdjacent, Match{i1, j1, k1}) + } + i1, j1, k1 = i2, j2, k2 + } + } + if k1 > 0 { + nonAdjacent = append(nonAdjacent, Match{i1, j1, k1}) + } + + nonAdjacent = append(nonAdjacent, Match{len(m.a), len(m.b), 0}) + m.matchingBlocks = nonAdjacent + return m.matchingBlocks +} + +// GetOpCodes returns a list of 5-tuples describing how to turn a into b. +// +// Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple +// has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the +// tuple preceding it, and likewise for j1 == the previous j2. +// +// The tags are characters, with these meanings: +// +// 'r' (replace): a[i1:i2] should be replaced by b[j1:j2] +// +// 'd' (delete): a[i1:i2] should be deleted, j1==j2 in this case. +// +// 'i' (insert): b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case. +// +// 'e' (equal): a[i1:i2] == b[j1:j2] +func (m *SequenceMatcher) GetOpCodes() []OpCode { + if m.opCodes != nil { + return m.opCodes + } + i, j := 0, 0 + matching := m.GetMatchingBlocks() + opCodes := make([]OpCode, 0, len(matching)) + for _, m := range matching { + // invariant: we've pumped out correct diffs to change + // a[:i] into b[:j], and the next matching block is + // a[ai:ai+size] == b[bj:bj+size]. So we need to pump + // out a diff to change a[i:ai] into b[j:bj], pump out + // the matching block, and move (i,j) beyond the match + ai, bj, size := m.A, m.B, m.Size + tag := byte(0) + if i < ai && j < bj { + tag = 'r' + } else if i < ai { + tag = 'd' + } else if j < bj { + tag = 'i' + } + if tag > 0 { + opCodes = append(opCodes, OpCode{tag, i, ai, j, bj}) + } + i, j = ai+size, bj+size + // the list of matching blocks is terminated by a + // sentinel with size 0 + if size > 0 { + opCodes = append(opCodes, OpCode{'e', ai, i, bj, j}) + } + } + m.opCodes = opCodes + return m.opCodes +} + +// GetGroupedOpCodes isolates change clusters by eliminating ranges with no changes. +// +// Return a generator of groups with up to n lines of context. +// Each group is in the same format as returned by GetOpCodes(). +func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode { + if n < 0 { + n = 3 + } + codes := m.GetOpCodes() + if len(codes) == 0 { + codes = []OpCode{{'e', 0, 1, 0, 1}} + } + // Fixup leading and trailing groups if they show no changes. + if codes[0].Tag == 'e' { + c := codes[0] + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + codes[0] = OpCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2} + } + if codes[len(codes)-1].Tag == 'e' { + c := codes[len(codes)-1] + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + codes[len(codes)-1] = OpCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)} + } + nn := n + n + groups := [][]OpCode{} + group := []OpCode{} + for _, c := range codes { + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + // End the current group and start a new one whenever + // there is a large range with no changes. + if c.Tag == 'e' && i2-i1 > nn { + group = append(group, OpCode{c.Tag, i1, min(i2, i1+n), + j1, min(j2, j1+n)}) + groups = append(groups, group) + group = []OpCode{} + i1, j1 = max(i1, i2-n), max(j1, j2-n) + } + group = append(group, OpCode{c.Tag, i1, i2, j1, j2}) + } + if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') { + groups = append(groups, group) + } + return groups +} diff --git a/vendor/gotest.tools/v3/internal/format/diff.go b/vendor/gotest.tools/v3/internal/format/diff.go new file mode 100644 index 00000000..4f6c07a3 --- /dev/null +++ b/vendor/gotest.tools/v3/internal/format/diff.go @@ -0,0 +1,162 @@ +// Package format provides utilities for formatting diffs and messages. +package format + +import ( + "bytes" + "fmt" + "strings" + "unicode" + + "gotest.tools/v3/internal/difflib" +) + +const ( + contextLines = 2 +) + +// DiffConfig for a unified diff +type DiffConfig struct { + A string + B string + From string + To string +} + +// UnifiedDiff is a modified version of difflib.WriteUnifiedDiff with better +// support for showing the whitespace differences. +func UnifiedDiff(conf DiffConfig) string { + a := strings.SplitAfter(conf.A, "\n") + b := strings.SplitAfter(conf.B, "\n") + groups := difflib.NewMatcher(a, b).GetGroupedOpCodes(contextLines) + if len(groups) == 0 { + return "" + } + + buf := new(bytes.Buffer) + writeFormat := func(format string, args ...interface{}) { + buf.WriteString(fmt.Sprintf(format, args...)) + } + writeLine := func(prefix string, s string) { + buf.WriteString(prefix + s) + } + if hasWhitespaceDiffLines(groups, a, b) { + writeLine = visibleWhitespaceLine(writeLine) + } + formatHeader(writeFormat, conf) + for _, group := range groups { + formatRangeLine(writeFormat, group) + for _, opCode := range group { + in, out := a[opCode.I1:opCode.I2], b[opCode.J1:opCode.J2] + switch opCode.Tag { + case 'e': + formatLines(writeLine, " ", in) + case 'r': + formatLines(writeLine, "-", in) + formatLines(writeLine, "+", out) + case 'd': + formatLines(writeLine, "-", in) + case 'i': + formatLines(writeLine, "+", out) + } + } + } + return buf.String() +} + +// hasWhitespaceDiffLines returns true if any diff groups is only different +// because of whitespace characters. +func hasWhitespaceDiffLines(groups [][]difflib.OpCode, a, b []string) bool { + for _, group := range groups { + in, out := new(bytes.Buffer), new(bytes.Buffer) + for _, opCode := range group { + if opCode.Tag == 'e' { + continue + } + for _, line := range a[opCode.I1:opCode.I2] { + in.WriteString(line) + } + for _, line := range b[opCode.J1:opCode.J2] { + out.WriteString(line) + } + } + if removeWhitespace(in.String()) == removeWhitespace(out.String()) { + return true + } + } + return false +} + +func removeWhitespace(s string) string { + var result []rune + for _, r := range s { + if !unicode.IsSpace(r) { + result = append(result, r) + } + } + return string(result) +} + +func visibleWhitespaceLine(ws func(string, string)) func(string, string) { + mapToVisibleSpace := func(r rune) rune { + switch r { + case '\n': + case ' ': + return '·' + case '\t': + return '▷' + case '\v': + return '▽' + case '\r': + return '↵' + case '\f': + return '↓' + default: + if unicode.IsSpace(r) { + return '�' + } + } + return r + } + return func(prefix, s string) { + ws(prefix, strings.Map(mapToVisibleSpace, s)) + } +} + +func formatHeader(wf func(string, ...interface{}), conf DiffConfig) { + if conf.From != "" || conf.To != "" { + wf("--- %s\n", conf.From) + wf("+++ %s\n", conf.To) + } +} + +func formatRangeLine(wf func(string, ...interface{}), group []difflib.OpCode) { + first, last := group[0], group[len(group)-1] + range1 := formatRangeUnified(first.I1, last.I2) + range2 := formatRangeUnified(first.J1, last.J2) + wf("@@ -%s +%s @@\n", range1, range2) +} + +// Convert range to the "ed" format +func formatRangeUnified(start, stop int) string { + // Per the diff spec at http://www.unix.org/single_unix_specification/ + beginning := start + 1 // lines start numbering with one + length := stop - start + if length == 1 { + return fmt.Sprintf("%d", beginning) + } + if length == 0 { + beginning-- // empty ranges begin at line just before the range + } + return fmt.Sprintf("%d,%d", beginning, length) +} + +func formatLines(writeLine func(string, string), prefix string, lines []string) { + for _, line := range lines { + writeLine(prefix, line) + } + // Add a newline if the last line is missing one so that the diff displays + // properly. + if !strings.HasSuffix(lines[len(lines)-1], "\n") { + writeLine("", "\n") + } +} diff --git a/vendor/gotest.tools/v3/internal/format/format.go b/vendor/gotest.tools/v3/internal/format/format.go new file mode 100644 index 00000000..5097e4bd --- /dev/null +++ b/vendor/gotest.tools/v3/internal/format/format.go @@ -0,0 +1,27 @@ +package format // import "gotest.tools/v3/internal/format" + +import "fmt" + +// Message accepts a msgAndArgs varargs and formats it using fmt.Sprintf +func Message(msgAndArgs ...interface{}) string { + switch len(msgAndArgs) { + case 0: + return "" + case 1: + return fmt.Sprintf("%v", msgAndArgs[0]) + default: + return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) + } +} + +// WithCustomMessage accepts one or two messages and formats them appropriately +func WithCustomMessage(source string, msgAndArgs ...interface{}) string { + custom := Message(msgAndArgs...) + switch { + case custom == "": + return source + case source == "": + return custom + } + return fmt.Sprintf("%s: %s", source, custom) +} diff --git a/vendor/gotest.tools/v3/internal/source/defers.go b/vendor/gotest.tools/v3/internal/source/defers.go new file mode 100644 index 00000000..392d9fe0 --- /dev/null +++ b/vendor/gotest.tools/v3/internal/source/defers.go @@ -0,0 +1,52 @@ +package source + +import ( + "fmt" + "go/ast" + "go/token" +) + +func scanToDeferLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node { + var matchedNode ast.Node + ast.Inspect(node, func(node ast.Node) bool { + switch { + case node == nil || matchedNode != nil: + return false + case fileset.Position(node.End()).Line == lineNum: + if funcLit, ok := node.(*ast.FuncLit); ok { + matchedNode = funcLit + return false + } + } + return true + }) + debug("defer line node: %s", debugFormatNode{matchedNode}) + return matchedNode +} + +func guessDefer(node ast.Node) (ast.Node, error) { + defers := collectDefers(node) + switch len(defers) { + case 0: + return nil, fmt.Errorf("failed to find expression in defer") + case 1: + return defers[0].Call, nil + default: + return nil, fmt.Errorf( + "ambiguous call expression: multiple (%d) defers in call block", + len(defers)) + } +} + +func collectDefers(node ast.Node) []*ast.DeferStmt { + var defers []*ast.DeferStmt + ast.Inspect(node, func(node ast.Node) bool { + if d, ok := node.(*ast.DeferStmt); ok { + defers = append(defers, d) + debug("defer: %s", debugFormatNode{d}) + return false + } + return true + }) + return defers +} diff --git a/vendor/gotest.tools/v3/internal/source/source.go b/vendor/gotest.tools/v3/internal/source/source.go new file mode 100644 index 00000000..a4fc24ee --- /dev/null +++ b/vendor/gotest.tools/v3/internal/source/source.go @@ -0,0 +1,148 @@ +// Package source provides utilities for handling source-code. +package source // import "gotest.tools/v3/internal/source" + +import ( + "bytes" + "errors" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "os" + "runtime" +) + +// FormattedCallExprArg returns the argument from an ast.CallExpr at the +// index in the call stack. The argument is formatted using FormatNode. +func FormattedCallExprArg(stackIndex int, argPos int) (string, error) { + args, err := CallExprArgs(stackIndex + 1) + if err != nil { + return "", err + } + if argPos >= len(args) { + return "", errors.New("failed to find expression") + } + return FormatNode(args[argPos]) +} + +// CallExprArgs returns the ast.Expr slice for the args of an ast.CallExpr at +// the index in the call stack. +func CallExprArgs(stackIndex int) ([]ast.Expr, error) { + _, filename, line, ok := runtime.Caller(stackIndex + 1) + if !ok { + return nil, errors.New("failed to get call stack") + } + debug("call stack position: %s:%d", filename, line) + + fileset := token.NewFileSet() + astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors) + if err != nil { + return nil, fmt.Errorf("failed to parse source file %s: %w", filename, err) + } + + expr, err := getCallExprArgs(fileset, astFile, line) + if err != nil { + return nil, fmt.Errorf("call from %s:%d: %w", filename, line, err) + } + return expr, nil +} + +func getNodeAtLine(fileset *token.FileSet, astFile ast.Node, lineNum int) (ast.Node, error) { + if node := scanToLine(fileset, astFile, lineNum); node != nil { + return node, nil + } + if node := scanToDeferLine(fileset, astFile, lineNum); node != nil { + node, err := guessDefer(node) + if err != nil || node != nil { + return node, err + } + } + return nil, nil +} + +func scanToLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node { + var matchedNode ast.Node + ast.Inspect(node, func(node ast.Node) bool { + switch { + case node == nil || matchedNode != nil: + return false + case fileset.Position(node.Pos()).Line == lineNum: + matchedNode = node + return false + } + return true + }) + return matchedNode +} + +func getCallExprArgs(fileset *token.FileSet, astFile ast.Node, line int) ([]ast.Expr, error) { + node, err := getNodeAtLine(fileset, astFile, line) + switch { + case err != nil: + return nil, err + case node == nil: + return nil, fmt.Errorf("failed to find an expression") + } + + debug("found node: %s", debugFormatNode{node}) + + visitor := &callExprVisitor{} + ast.Walk(visitor, node) + if visitor.expr == nil { + return nil, errors.New("failed to find call expression") + } + debug("callExpr: %s", debugFormatNode{visitor.expr}) + return visitor.expr.Args, nil +} + +type callExprVisitor struct { + expr *ast.CallExpr +} + +func (v *callExprVisitor) Visit(node ast.Node) ast.Visitor { + if v.expr != nil || node == nil { + return nil + } + debug("visit: %s", debugFormatNode{node}) + + switch typed := node.(type) { + case *ast.CallExpr: + v.expr = typed + return nil + case *ast.DeferStmt: + ast.Walk(v, typed.Call.Fun) + return nil + } + return v +} + +// FormatNode using go/format.Node and return the result as a string +func FormatNode(node ast.Node) (string, error) { + buf := new(bytes.Buffer) + err := format.Node(buf, token.NewFileSet(), node) + return buf.String(), err +} + +var debugEnabled = os.Getenv("GOTESTTOOLS_DEBUG") != "" + +func debug(format string, args ...interface{}) { + if debugEnabled { + fmt.Fprintf(os.Stderr, "DEBUG: "+format+"\n", args...) + } +} + +type debugFormatNode struct { + ast.Node +} + +func (n debugFormatNode) String() string { + if n.Node == nil { + return "none" + } + out, err := FormatNode(n.Node) + if err != nil { + return fmt.Sprintf("failed to format %s: %s", n.Node, err) + } + return fmt.Sprintf("(%T) %s", n.Node, out) +} diff --git a/vendor/gotest.tools/v3/internal/source/update.go b/vendor/gotest.tools/v3/internal/source/update.go new file mode 100644 index 00000000..5591bffd --- /dev/null +++ b/vendor/gotest.tools/v3/internal/source/update.go @@ -0,0 +1,171 @@ +package source + +import ( + "bytes" + "errors" + "flag" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "os" + "runtime" + "strings" +) + +// IsUpdate is returns true if the -update flag is set. It indicates the user +// running the tests would like to update any golden values. +func IsUpdate() bool { + if Update { + return true + } + return flag.Lookup("update").Value.(flag.Getter).Get().(bool) +} + +// Update is a shim for testing, and for compatibility with the old -update-golden +// flag. +var Update bool + +func init() { + if f := flag.Lookup("update"); f != nil { + getter, ok := f.Value.(flag.Getter) + msg := "some other package defined an incompatible -update flag, expected a flag.Bool" + if !ok { + panic(msg) + } + if _, ok := getter.Get().(bool); !ok { + panic(msg) + } + return + } + flag.Bool("update", false, "update golden values") +} + +// ErrNotFound indicates that UpdateExpectedValue failed to find the +// variable to update, likely because it is not a package level variable. +var ErrNotFound = fmt.Errorf("failed to find variable for update of golden value") + +// UpdateExpectedValue looks for a package-level variable with a name that +// starts with expected in the arguments to the caller. If the variable is +// found, the value of the variable will be updated to value of the other +// argument to the caller. +func UpdateExpectedValue(stackIndex int, x, y interface{}) error { + _, filename, line, ok := runtime.Caller(stackIndex + 1) + if !ok { + return errors.New("failed to get call stack") + } + debug("call stack position: %s:%d", filename, line) + + fileset := token.NewFileSet() + astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors|parser.ParseComments) + if err != nil { + return fmt.Errorf("failed to parse source file %s: %w", filename, err) + } + + expr, err := getCallExprArgs(fileset, astFile, line) + if err != nil { + return fmt.Errorf("call from %s:%d: %w", filename, line, err) + } + + if len(expr) < 3 { + debug("not enough arguments %d: %v", + len(expr), debugFormatNode{Node: &ast.CallExpr{Args: expr}}) + return ErrNotFound + } + + argIndex, ident := getIdentForExpectedValueArg(expr) + if argIndex < 0 || ident == nil { + debug("no arguments started with the word 'expected': %v", + debugFormatNode{Node: &ast.CallExpr{Args: expr}}) + return ErrNotFound + } + + value := x + if argIndex == 1 { + value = y + } + + strValue, ok := value.(string) + if !ok { + debug("value must be type string, got %T", value) + return ErrNotFound + } + return UpdateVariable(filename, fileset, astFile, ident, strValue) +} + +// UpdateVariable writes to filename the contents of astFile with the value of +// the variable updated to value. +func UpdateVariable( + filename string, + fileset *token.FileSet, + astFile *ast.File, + ident *ast.Ident, + value string, +) error { + obj := ident.Obj + if obj == nil { + return ErrNotFound + } + if obj.Kind != ast.Con && obj.Kind != ast.Var { + debug("can only update var and const, found %v", obj.Kind) + return ErrNotFound + } + + switch decl := obj.Decl.(type) { + case *ast.ValueSpec: + if len(decl.Names) != 1 { + debug("more than one name in ast.ValueSpec") + return ErrNotFound + } + + decl.Values[0] = &ast.BasicLit{ + Kind: token.STRING, + Value: "`" + value + "`", + } + + case *ast.AssignStmt: + if len(decl.Lhs) != 1 { + debug("more than one name in ast.AssignStmt") + return ErrNotFound + } + + decl.Rhs[0] = &ast.BasicLit{ + Kind: token.STRING, + Value: "`" + value + "`", + } + + default: + debug("can only update *ast.ValueSpec, found %T", obj.Decl) + return ErrNotFound + } + + var buf bytes.Buffer + if err := format.Node(&buf, fileset, astFile); err != nil { + return fmt.Errorf("failed to format file after update: %w", err) + } + + fh, err := os.Create(filename) + if err != nil { + return fmt.Errorf("failed to open file %v: %w", filename, err) + } + if _, err = fh.Write(buf.Bytes()); err != nil { + return fmt.Errorf("failed to write file %v: %w", filename, err) + } + if err := fh.Sync(); err != nil { + return fmt.Errorf("failed to sync file %v: %w", filename, err) + } + return nil +} + +func getIdentForExpectedValueArg(expr []ast.Expr) (int, *ast.Ident) { + for i := 1; i < 3; i++ { + switch e := expr[i].(type) { + case *ast.Ident: + if strings.HasPrefix(strings.ToLower(e.Name), "expected") { + return i, e + } + } + } + return -1, nil +} diff --git a/vendor/gotest.tools/v3/internal/source/version.go b/vendor/gotest.tools/v3/internal/source/version.go new file mode 100644 index 00000000..5fa8a903 --- /dev/null +++ b/vendor/gotest.tools/v3/internal/source/version.go @@ -0,0 +1,35 @@ +package source + +import ( + "runtime" + "strconv" + "strings" +) + +// GoVersionLessThan returns true if runtime.Version() is semantically less than +// version major.minor. Returns false if a release version can not be parsed from +// runtime.Version(). +func GoVersionLessThan(major, minor int64) bool { + version := runtime.Version() + // not a release version + if !strings.HasPrefix(version, "go") { + return false + } + version = strings.TrimPrefix(version, "go") + parts := strings.Split(version, ".") + if len(parts) < 2 { + return false + } + rMajor, err := strconv.ParseInt(parts[0], 10, 32) + if err != nil { + return false + } + if rMajor != major { + return rMajor < major + } + rMinor, err := strconv.ParseInt(parts[1], 10, 32) + if err != nil { + return false + } + return rMinor < minor +} diff --git a/vendor/modules.txt b/vendor/modules.txt index c6fa4d15..5e858c66 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -344,6 +344,13 @@ github.com/gorilla/mux github.com/grpc-ecosystem/grpc-gateway/v2/internal/httprule github.com/grpc-ecosystem/grpc-gateway/v2/runtime github.com/grpc-ecosystem/grpc-gateway/v2/utilities +# github.com/gruntwork-io/terratest v0.48.2 +## explicit; go 1.21 +github.com/gruntwork-io/terratest/modules/collections +github.com/gruntwork-io/terratest/modules/docker +github.com/gruntwork-io/terratest/modules/logger +github.com/gruntwork-io/terratest/modules/shell +github.com/gruntwork-io/terratest/modules/testing # github.com/hashicorp/errwrap v1.1.0 ## explicit github.com/hashicorp/errwrap @@ -1046,3 +1053,12 @@ gopkg.in/yaml.v2 # gopkg.in/yaml.v3 v3.0.1 ## explicit gopkg.in/yaml.v3 +# gotest.tools/v3 v3.5.1 +## explicit; go 1.17 +gotest.tools/v3/assert +gotest.tools/v3/assert/cmp +gotest.tools/v3/icmd +gotest.tools/v3/internal/assert +gotest.tools/v3/internal/difflib +gotest.tools/v3/internal/format +gotest.tools/v3/internal/source