From 7c6b395d9c1f0b26ae6d0ed0e1a126de07836af8 Mon Sep 17 00:00:00 2001 From: Janos Bonic <86970079+janosdebugs@users.noreply.github.com> Date: Wed, 17 Jul 2024 23:08:26 +0200 Subject: [PATCH 01/10] Test context implementation Signed-off-by: Janos Bonic <86970079+janosdebugs@users.noreply.github.com> --- README.md | 23 +++++++++++++++++++++++ context.go | 26 ++++++++++++++++++++++++++ testcontext/README.md | 21 +++++++++++++++++++++ testcontext/context.go | 29 +++++++++++++++++++++++++++++ testcontext/context_test.go | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 135 insertions(+) create mode 100644 README.md create mode 100644 context.go create mode 100644 testcontext/README.md create mode 100644 testcontext/context.go create mode 100644 testcontext/context_test.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..7fb1f9a --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# OpenTofu test utilities + +This library contains the test fixtures and utilities for testing OpenTofu. This library will be changed to suit the needs of OpenTofu only, please do not use it for any other purpose. + +## Test context + +Some tests need to create external resources, such as cloud resources, which need to be cleaned up reliably when the test ends. Unfortunately, `go test` unceremoniously kills the test run if it encounters a timeout without leaving time for cleanup functions to run. You can call `tofutestutils.Context` to obtain a `context.Context` that will leave enough time to perform a cleanup: + +```go +package your_test + +import ( + "your" + + "github.com/opentofu/tofutestutils" +) + +func TestMyApplication(t *testing.T) { + ctx := tofutestutils.Context(t) + + your.App(ctx) +} +``` \ No newline at end of file diff --git a/context.go b/context.go new file mode 100644 index 0000000..7901c90 --- /dev/null +++ b/context.go @@ -0,0 +1,26 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 + +package tofutestutils + +import ( + "context" + "testing" + "time" + + "github.com/opentofu/tofutestutils/testcontext" +) + +const minCleanupSafety = time.Second * 30 +const maxCleanupSafety = time.Minute * 5 + +// Context returns a context configured for the test deadline. This function configures a context with 25% safety for +// any cleanup tasks to finish. For a more flexible function see testcontext.Context. +func Context(t *testing.T) context.Context { + return testcontext.Context( + t, + 4, + minCleanupSafety, + maxCleanupSafety, + ) +} diff --git a/testcontext/README.md b/testcontext/README.md new file mode 100644 index 0000000..e90ccc7 --- /dev/null +++ b/testcontext/README.md @@ -0,0 +1,21 @@ +# Test context + +Some tests need to create external resources, such as cloud resources, which need to be cleaned up reliably when the test ends. Unfortunately, `go test` unceremoniously kills the test run if it encounters a timeout without leaving time for cleanup functions to run. + +This package solves this problem by creating a `context.Context` that has a timeout earlier than the hard timeout of `tofu test`. You can pass this context to any parts of the application that need to abort before the test aborts. + +```go +package your_test + +import ( + "your" + + "github.com/opentofu/tofutestutils" +) + +func TestMyApplication(t *testing.T) { + ctx := tofutestutils.Context(t) + + your.App(ctx) +} +``` \ No newline at end of file diff --git a/testcontext/context.go b/testcontext/context.go new file mode 100644 index 0000000..c6bd102 --- /dev/null +++ b/testcontext/context.go @@ -0,0 +1,29 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 + +package testcontext + +import ( + "context" + "testing" + "time" +) + +// Context creates a context with a deadline that allows for enough time to clean up a test before the testing framework +// unceremoniously kills the process. The desired time is expressed as a fraction of the time remaining until the hard +// timeout, such as 4 for 25%. The minimumCleanupTime and maximumCleanupTime clamp the remaining time. +// +// For a simpler to use version of this function call tofutestutils.Context. +func Context(t *testing.T, cleanupTimeFraction int64, minimumCleanupTime time.Duration, maximumCleanupTime time.Duration) context.Context { + ctx := context.Background() + if deadline, ok := t.Deadline(); ok { + var cancel func() + timeoutDuration := time.Until(deadline) + cleanupSafety := min(max( + timeoutDuration/time.Duration(cleanupTimeFraction), minimumCleanupTime), maximumCleanupTime, + ) + ctx, cancel = context.WithDeadline(ctx, deadline.Add(-1*cleanupSafety)) + t.Cleanup(cancel) + } + return ctx +} diff --git a/testcontext/context_test.go b/testcontext/context_test.go new file mode 100644 index 0000000..558f4cd --- /dev/null +++ b/testcontext/context_test.go @@ -0,0 +1,36 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 + +package testcontext_test + +import ( + "testing" + "time" + + "github.com/opentofu/tofutestutils/testcontext" +) + +func TestContext(t *testing.T) { + const checkTime = 20 * time.Second + ctx := testcontext.Context( + t, + 4, + 30*time.Second, + 60*time.Second, + ) + if ctx == nil { + t.Fatalf("No context returned from testutils.Context") + } + tDeadline, tOk := t.Deadline() + ctxDeadline, ctxOk := ctx.Deadline() + if tOk != ctxOk { + t.Fatalf("The testutils.Context function does not correctly set up the deadline ('ok' value mismatch)") + } + if tOk { + if !ctxDeadline.Before(tDeadline.Add(checkTime)) { + t.Fatalf( + "The testutils.Context function does not correctly set up the deadline (not enough time left for cleanup)", + ) + } + } +} From 34d4faf4fceec7f0b5dd3283d47835eb47feb83d Mon Sep 17 00:00:00 2001 From: Janos <86970079+janosdebugs@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:06:48 +0200 Subject: [PATCH 02/10] README fix Signed-off-by: Janos <86970079+janosdebugs@users.noreply.github.com> --- README.md | 1 + testcontext/README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 7fb1f9a..fb3a667 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Some tests need to create external resources, such as cloud resources, which nee package your_test import ( + "testing" "your" "github.com/opentofu/tofutestutils" diff --git a/testcontext/README.md b/testcontext/README.md index e90ccc7..78df388 100644 --- a/testcontext/README.md +++ b/testcontext/README.md @@ -8,6 +8,7 @@ This package solves this problem by creating a `context.Context` that has a time package your_test import ( + "testing" "your" "github.com/opentofu/tofutestutils" From 5a5ff6365374d20cc01c08eaee840847ab7ec530 Mon Sep 17 00:00:00 2001 From: Janos Bonic <86970079+janosdebugs@users.noreply.github.com> Date: Wed, 17 Jul 2024 23:33:50 +0200 Subject: [PATCH 03/10] Adding random number generation Note: this commit contains code based on OpenTofu code and therefore, changes the license headers Signed-off-by: Janos Bonic <86970079+janosdebugs@users.noreply.github.com> --- .github/workflows/verify.yml | 2 + LICENSE | 1 + context.go | 2 + generate.go | 2 + internal/tools/license-headers/main.go | 4 + internal/tools/lint/main.go | 2 + random.go | 63 +++++++++++++++ testcontext/context.go | 2 + testcontext/context_test.go | 2 + testrandom/random.go | 105 +++++++++++++++++++++++++ testrandom/random_test.go | 103 ++++++++++++++++++++++++ 11 files changed, 288 insertions(+) create mode 100644 random.go create mode 100644 testrandom/random.go create mode 100644 testrandom/random_test.go diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index ced5f1f..5833e6f 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -1,5 +1,7 @@ # Copyright (c) The OpenTofu Authors # SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2023 HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 name: Verify permissions: {} on: diff --git a/LICENSE b/LICENSE index f027e05..c8a8176 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,5 @@ Copyright (c) The OpenTofu Authors +Copyright (c) 2014 HashiCorp, Inc. Mozilla Public License, version 2.0 diff --git a/context.go b/context.go index 7901c90..fa021fc 100644 --- a/context.go +++ b/context.go @@ -1,5 +1,7 @@ // Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 package tofutestutils diff --git a/generate.go b/generate.go index 59e779b..7eff00f 100644 --- a/generate.go +++ b/generate.go @@ -1,5 +1,7 @@ // Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 package tofutestutils diff --git a/internal/tools/license-headers/main.go b/internal/tools/license-headers/main.go index b48afb1..9b2fe1f 100644 --- a/internal/tools/license-headers/main.go +++ b/internal/tools/license-headers/main.go @@ -1,5 +1,7 @@ // Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 package main @@ -14,6 +16,8 @@ import ( func main() { header := `// Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 ` checkOnly := false diff --git a/internal/tools/lint/main.go b/internal/tools/lint/main.go index b772fe6..028b6bc 100644 --- a/internal/tools/lint/main.go +++ b/internal/tools/lint/main.go @@ -1,5 +1,7 @@ // Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 package main diff --git a/random.go b/random.go new file mode 100644 index 0000000..7e2c86e --- /dev/null +++ b/random.go @@ -0,0 +1,63 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tofutestutils + +import ( + "math/rand" + "testing" + + "github.com/opentofu/tofutestutils/testrandom" +) + +// DeterministicRandomID generates a pseudo-random identifier for the given test, using its name as a seed for +// randomness. This function guarantees that when queried in order, the values are always the same as long as the name +// of the test doesn't change. +func DeterministicRandomID(t *testing.T, length uint, characterSpace testrandom.CharacterRange) string { + return testrandom.DeterministicID(t, length, characterSpace) +} + +// RandomID returns a non-deterministic, pseudo-random identifier. +func RandomID(length uint, characterSpace testrandom.CharacterRange) string { + return testrandom.ID(length, characterSpace) +} + +// RandomIDPrefix returns a random identifier with a given prefix. The prefix length does not count towards the +// length. +func RandomIDPrefix(prefix string, length uint, characterSpace testrandom.CharacterRange) string { + return testrandom.IDPrefix(prefix, length, characterSpace) +} + +// RandomIDFromSource generates a random ID with the specified length based on the provided random parameter. +func RandomIDFromSource(random *rand.Rand, length uint, characterSpace testrandom.CharacterRange) string { + return testrandom.IDFromSource(random, length, characterSpace) +} + +// DeterministicRandomInt produces a deterministic random integer based on the test name between the specified min and +// max value (inclusive). +func DeterministicRandomInt(t *testing.T, min int, max int) int { + return testrandom.DeterministicInt(t, min, max) +} + +// RandomInt produces a random integer between the specified min and max value (inclusive). +func RandomInt(min int, max int) int { + return testrandom.Int(min, max) +} + +// RandomIntFromSource produces a random integer between the specified min and max value (inclusive). +func RandomIntFromSource(source *rand.Rand, min int, max int) int { + return testrandom.IntFromSource(source, min, max) +} + +// RandomSource produces a rand.Rand randomness source that is non-deterministic. +func RandomSource() *rand.Rand { + return testrandom.Source() +} + +// DeterministicRandomSource produces a rand.Rand that is deterministic based on the provided test name. It will always +// supply the same values as long as the test name doesn't change. +func DeterministicRandomSource(t *testing.T) *rand.Rand { + return testrandom.DeterministicSource(t) +} diff --git a/testcontext/context.go b/testcontext/context.go index c6bd102..48eda01 100644 --- a/testcontext/context.go +++ b/testcontext/context.go @@ -1,5 +1,7 @@ // Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 package testcontext diff --git a/testcontext/context_test.go b/testcontext/context_test.go index 558f4cd..25851a1 100644 --- a/testcontext/context_test.go +++ b/testcontext/context_test.go @@ -1,5 +1,7 @@ // Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 package testcontext_test diff --git a/testrandom/random.go b/testrandom/random.go new file mode 100644 index 0000000..3a72d3e --- /dev/null +++ b/testrandom/random.go @@ -0,0 +1,105 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testrandom + +import ( + "hash/crc64" + "math/rand" + "strings" + "sync" + "testing" + "time" +) + +// The functions below contain an assortment of random ID generation functions, partially ported and improved from the +// github.com/opentofu/opentofu/internal/legacy/helper/acctest package. + +var randomSources = map[string]*rand.Rand{} //nolint:gochecknoglobals //This variable stores the randomness sources for DeterministicID and needs to be global. +var randomLock = &sync.Mutex{} //nolint:gochecknoglobals //This variable is required to lock the randomSources above. + +// CharacterRange defines which characters to use for generating a random ID. +type CharacterRange string + +const ( + CharacterRangeAlphaNumeric CharacterRange = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + CharacterRangeAlphaNumericLower CharacterRange = "abcdefghijklmnopqrstuvwxyz0123456789" + CharacterRangeAlphaNumericUpper CharacterRange = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + CharacterRangeAlpha CharacterRange = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + CharacterRangeAlphaLower CharacterRange = "abcdefghijklmnopqrstuvwxyz" + CharacterRangeAlphaUpper CharacterRange = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +) + +// DeterministicID generates a pseudo-random identifier for the given test, using its name as a seed for +// randomness. This function guarantees that when queried in order, the values are always the same as long as the name +// of the test doesn't change. +func DeterministicID(t *testing.T, length uint, characterSpace CharacterRange) string { + return IDFromSource(DeterministicSource(t), length, characterSpace) +} + +// ID returns a non-deterministic, pseudo-random identifier. +func ID(length uint, characterSpace CharacterRange) string { + return IDFromSource(Source(), length, characterSpace) +} + +// IDPrefix returns a random identifier with a given prefix. The prefix length does not count towards the +// length. +func IDPrefix(prefix string, length uint, characterSpace CharacterRange) string { + return prefix + ID(length, characterSpace) +} + +// IDFromSource generates a random ID with the specified length based on the provided random parameter. +func IDFromSource(random *rand.Rand, length uint, characterSpace CharacterRange) string { + runes := []rune(characterSpace) + var builder strings.Builder + for i := uint(0); i < length; i++ { + builder.WriteRune(runes[random.Intn(len(runes))]) + } + return builder.String() +} + +// DeterministicInt produces a deterministic random integer based on the test name between the specified min and +// max value (inclusive). +func DeterministicInt(t *testing.T, min int, max int) int { + return IntFromSource(DeterministicSource(t), min, max) +} + +// Int produces a random integer between the specified min and max value (inclusive). +func Int(min int, max int) int { + return IntFromSource(Source(), min, max) +} + +// IntFromSource produces a random integer between the specified min and max value (inclusive). +func IntFromSource(source *rand.Rand, min int, max int) int { + // The logic for this function was moved from mock_value_composer.go + return source.Intn(max+1-min) + min +} + +// Source produces a rand.Rand randomness source that is non-deterministic. +func Source() *rand.Rand { + return rand.New(rand.NewSource(time.Now().UnixNano())) //nolint:gosec // Disabling gosec linting because this ID is for testing only. +} + +// DeterministicSource produces a rand.Rand that is deterministic based on the provided test name. It will always +// supply the same values as long as the test name doesn't change. +func DeterministicSource(t *testing.T) *rand.Rand { + var random *rand.Rand + name := t.Name() + var ok bool + randomLock.Lock() + random, ok = randomSources[name] + if !ok { + seed := crc64.Checksum([]byte(name), crc64.MakeTable(crc64.ECMA)) + random = rand.New(rand.NewSource(int64(seed))) //nolint:gosec //This random number generator is intentionally deterministic. + randomSources[name] = random + t.Cleanup(func() { + randomLock.Lock() + defer randomLock.Unlock() + delete(randomSources, name) + }) + } + randomLock.Unlock() + return random +} diff --git a/testrandom/random_test.go b/testrandom/random_test.go new file mode 100644 index 0000000..c55e95e --- /dev/null +++ b/testrandom/random_test.go @@ -0,0 +1,103 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testrandom_test + +import ( + "strings" + "testing" + + "github.com/opentofu/tofutestutils/testrandom" +) + +func TestDeterministicID(t *testing.T) { + const idLength = 12 + if t.Name() != "TestDeterministicID" { + t.Fatalf( + "The test name has changed, please update the test as it is used for seeding the random number " + + "generator.", + ) + } + if id := testrandom.DeterministicID( + t, + idLength, + testrandom.CharacterRangeAlphaNumeric, + ); id != "2Uw74WyQkh6P" { + t.Fatalf( + "Incorrect first pseudo-random ID returned: %s (the returned ID depends on the test name, make "+ + "sure to verify and update if you changed the test name)", + id, + ) + } + if id := testrandom.DeterministicID( + t, + idLength, + testrandom.CharacterRangeAlphaNumeric, + ); id != "F56iE3wkX1wR" { + t.Fatalf( + "Incorrect second pseudo-random ID returned: %s (the returned ID depends on the test name, make "+ + "sure to verify and update if you changed the test name)", + id, + ) + } +} + +func TestIDPrefix(t *testing.T) { + const testPrefix = "test-" + const idLength = 12 + id := testrandom.IDPrefix(testPrefix, idLength, testrandom.CharacterRangeAlphaNumeric) + if len(id) != idLength+len(testPrefix) { + t.Fatalf("Incorrect random ID length: %s", id) + } + if !strings.HasPrefix(id, testPrefix) { + t.Fatalf("Missing prefix: %s", id) + } +} + +func TestID(t *testing.T) { + const idLength = 12 + id := testrandom.ID(idLength, testrandom.CharacterRangeAlphaNumeric) + if len(id) != idLength { + t.Fatalf("Incorrect random ID length: %s", id) + } +} + +func TestDeterministicInt(t *testing.T) { + if t.Name() != "TestDeterministicInt" { + t.Fatalf( + "The test name has changed, please update the test as it is used for seeding the random number " + + "generator.", + ) + } + if i := testrandom.DeterministicInt( + t, + 1, + 42, + ); i != 39 { + t.Fatalf( + "Incorrect first pseudo-random int returned: %d (the returned int depends on the test name, make "+ + "sure to verify and update if you changed the test name)", + i, + ) + } + if i := testrandom.DeterministicInt( + t, + 1, + 42, + ); i != 17 { + t.Fatalf( + "Incorrect second pseudo-random int returned: %d (the returned int depends on the test name, make "+ + "sure to verify and update if you changed the test name)", + i, + ) + } +} + +func TestInt(t *testing.T) { + i := testrandom.Int(1, 42) + if i < 1 || i > 42 { + t.Fatalf("Invalid random integer returned %d (out of range)", i) + } +} From ce161515ae9643170539c699f4e733ee43d3b8f5 Mon Sep 17 00:00:00 2001 From: Janos <86970079+janosdebugs@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:06:12 +0200 Subject: [PATCH 04/10] Added readme Signed-off-by: Janos <86970079+janosdebugs@users.noreply.github.com> --- README.md | 27 ++++++++++++++++++++++++++- testrandom/README.md | 24 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 testrandom/README.md diff --git a/README.md b/README.md index fb3a667..82bcff4 100644 --- a/README.md +++ b/README.md @@ -21,4 +21,29 @@ func TestMyApplication(t *testing.T) { your.App(ctx) } -``` \ No newline at end of file +``` + +## Randomness + +The random functions provide a number of randomness sources for testing purposes, some deterministic, some not. + +To obtain a deterministic randomness source *tied to a test name*, you can use the `DeterministicSource` (implementing an `io.Reader`) as follows: + +```go +package your_test + +import ( + "testing" + "your" + + "github.com/opentofu/tofutestutils" +) + +func TestMyApp(t *testing.T) { + randomness := tofutestutils.DeterministicRandomSource(t) + + your.App(randomness) +} +``` + +For a full list of possible functions, please [check the Go docs](https://pkg.go.dev/github.com/opentofu/tofutestutils). \ No newline at end of file diff --git a/testrandom/README.md b/testrandom/README.md new file mode 100644 index 0000000..0ab4745 --- /dev/null +++ b/testrandom/README.md @@ -0,0 +1,24 @@ +# Randomness sources + +This folder contains a number of randomness sources for testing purposes, some deterministic, some not. + +To obtain a deterministic randomness source *tied to a test name*, you can use the `DeterministicSource` (implementing an `io.Reader`) as follows: + +```go +package your_test + +import ( + "testing" + "your" + + "github.com/opentofu/tofutestutils/testrandom" +) + +func TestMyApp(t *testing.T) { + randomness := testrandom.DeterministicSource(t) + + your.App(randomness) +} +``` + +For a full list of possible functions, please [check the Go docs](https://pkg.go.dev/github.com/opentofu/tofutestutils/testrandom). \ No newline at end of file From 55b9a127b759662a0473548fa9c61b34441e1b90 Mon Sep 17 00:00:00 2001 From: Janos <86970079+janosdebugs@users.noreply.github.com> Date: Mon, 12 Aug 2024 13:36:21 +0200 Subject: [PATCH 05/10] Adding Must() and Must2() for testing Signed-off-by: Janos <86970079+janosdebugs@users.noreply.github.com> --- must.go | 19 ++++++++++++ must_test.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 must.go create mode 100644 must_test.go diff --git a/must.go b/must.go new file mode 100644 index 0000000..8e9c108 --- /dev/null +++ b/must.go @@ -0,0 +1,19 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tofutestutils + +// Must turns an error into a panic. Use for tests only. +func Must(err error) { + if err != nil { + panic(err) + } +} + +// Must2 panics if err is an error, otherwise it returns the value. +func Must2[T any](value T, err error) T { + Must(err) + return value +} diff --git a/must_test.go b/must_test.go new file mode 100644 index 0000000..6ab122f --- /dev/null +++ b/must_test.go @@ -0,0 +1,82 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tofutestutils_test + +import ( + "fmt" + "testing" + + "github.com/opentofu/tofutestutils" +) + +func TestMust(t *testing.T) { + t.Logf("🔍 Checking if Must() panics with an error...") + paniced := false + func() { + defer func() { + e := recover() + paniced = e != nil + }() + + tofutestutils.Must(fmt.Errorf("this is an error")) + }() + if paniced == false { + t.Fatalf("❌ The Must() function did not panic.") + } + t.Logf("✅ The Must() function paniced properly.") + + t.Logf("🔍 Checking if Must() does not panic with nil...") + paniced = false + func() { + defer func() { + e := recover() + paniced = e != nil + }() + + tofutestutils.Must(nil) + }() + if paniced != false { + t.Fatalf("❌ The Must() function paniced.") + } + t.Logf("✅ The Must() function did not panic.") +} + +func TestMust2(t *testing.T) { + t.Logf("🔍 Checking if Must() panics with an error...") + paniced := false + + func() { + defer func() { + e := recover() + paniced = e != nil + }() + _ = tofutestutils.Must2(42, fmt.Errorf("this is an error")) + }() + if paniced == false { + t.Fatalf("❌ The Must2() function did not panic.") + } + t.Logf("✅ The Must2() function paniced properly.") + + t.Logf("🔍 Checking if Must2() panics does not panic with nil and returns the correct value...") + paniced = false + returned := 0 + func() { + defer func() { + e := recover() + paniced = e != nil + }() + + returned = tofutestutils.Must2(42, nil) + }() + if paniced != false { + t.Fatalf("❌ The Must2() function paniced.") + } + if returned != 42 { + t.Fatalf("❌ The Must2() function did not return the correct value: %d.", returned) + } + + t.Logf("✅ The Must2() function did not panic and returned the correct value.") +} From 6aabf24744e148c7de066c43918ded8b5cc7bd2b Mon Sep 17 00:00:00 2001 From: Janos <86970079+janosdebugs@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:10:06 +0200 Subject: [PATCH 06/10] Added readme Signed-off-by: Janos <86970079+janosdebugs@users.noreply.github.com> --- README.md | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 82bcff4..e7b2bc5 100644 --- a/README.md +++ b/README.md @@ -46,4 +46,38 @@ func TestMyApp(t *testing.T) { } ``` -For a full list of possible functions, please [check the Go docs](https://pkg.go.dev/github.com/opentofu/tofutestutils). \ No newline at end of file +For a full list of possible functions, please [check the Go docs](https://pkg.go.dev/github.com/opentofu/tofutestutils). + +## Handling errors + +This package also provides the `Must()` and `Must2()` functions to make test code easier to read. For example: + +```go +package your_test + +import ( + "fmt" + "testing" + + "github.com/opentofu/tofutestutils" +) + +func erroringFunction() error { + return fmt.Errorf("this is an error") +} + +func erroringFunctionWithReturn() (int, error) { + return 42, fmt.Errorf("this is an error") +} + +func TestMyApp(t *testing.T) { + // This will cause a panic: + tofutestutils.Must(erroringFunction()) +} + +func TestMyApp2(t *testing.T) { + // This will also cause a panic: + result := tofutestutils.Must2(erroringFunctionWithReturn()) + t.Logf("The number is: %d", result) +} +``` \ No newline at end of file From 611c4f9b64bef6d5e47192f10eb47fa300847836 Mon Sep 17 00:00:00 2001 From: Janos <86970079+janosdebugs@users.noreply.github.com> Date: Mon, 12 Aug 2024 13:59:12 +0200 Subject: [PATCH 07/10] Certificate Authority Signed-off-by: Janos <86970079+janosdebugs@users.noreply.github.com> --- README.md | 44 ++++++++ ca.go | 14 +++ testca/README.md | 48 +++++++++ testca/ca.go | 262 ++++++++++++++++++++++++++++++++++++++++++++++ testca/ca_test.go | 139 ++++++++++++++++++++++++ 5 files changed, 507 insertions(+) create mode 100644 ca.go create mode 100644 testca/README.md create mode 100644 testca/ca.go create mode 100644 testca/ca_test.go diff --git a/README.md b/README.md index e7b2bc5..8d803a4 100644 --- a/README.md +++ b/README.md @@ -80,4 +80,48 @@ func TestMyApp2(t *testing.T) { result := tofutestutils.Must2(erroringFunctionWithReturn()) t.Logf("The number is: %d", result) } +``` + +## Certificate authority + +When you need an x509 certificate for a server or a client, you can use the `tofutestutils.CA` function to obtain a `testca.CertificateAuthority` implementation using a pseudo-random number generator. You can use this to create a certificate for a socket server: + +```go +package your_test + +import ( + "crypto/tls" + "io" + "net" + "strconv" + "testing" + + "github.com/opentofu/tofutestutils" +) + +func TestMySocket(t *testing.T) { + ca := tofutestutils.CA(t) + + // Server side: + tlsListener := tofutestutils.Must2(tls.Listen("tcp", "127.0.0.1:0", ca.CreateLocalhostServerCert().GetServerTLSConfig())) + go func() { + conn, serverErr := tlsListener.Accept() + if serverErr != nil { + return + } + defer func() { + _ = conn.Close() + }() + _, _ = conn.Write([]byte("Hello world!")) + }() + + // Client side: + port := tlsListener.Addr().(*net.TCPAddr).Port + client := tofutestutils.Must2(tls.Dial("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), ca.GetClientTLSConfig())) + defer func() { + _ = client.Close() + }() + + t.Logf("%s", tofutestutils.Must2(io.ReadAll(client))) +} ``` \ No newline at end of file diff --git a/ca.go b/ca.go new file mode 100644 index 0000000..680f393 --- /dev/null +++ b/ca.go @@ -0,0 +1,14 @@ +package tofutestutils + +import ( + "testing" + "time" + + "github.com/opentofu/tofutestutils/testca" +) + +// CA returns a certificate authority configured for the provided test. This implementation will configure the CA to use +// a pseudorandom source. You can call testca.New() for more configuration options. +func CA(t *testing.T) testca.CertificateAuthority { + return testca.New(t, RandomSource(), time.Now) +} diff --git a/testca/README.md b/testca/README.md new file mode 100644 index 0000000..26ef0df --- /dev/null +++ b/testca/README.md @@ -0,0 +1,48 @@ +# Certificate authority + +This folder contains a basic x509 certificate authority implementation for testing purposes. You can use it whenever you need a certificate for servers or clients. + +```go +package your_test + +import ( + "crypto/tls" + "io" + "net" + "strconv" + "testing" + "time" + + "github.com/opentofu/tofutestutils" + "github.com/opentofu/tofutestutils/testca" + "github.com/opentofu/tofutestutils/testrandom" +) + +func TestMySocket(t *testing.T) { + // Configure a desired randomness and time source. You can use this to create deterministic behavior. + currentTimeSource := time.Now + ca := testca.New(t, testrandom.DeterministicSource(t), currentTimeSource) + + // Server side: + tlsListener := tofutestutils.Must2(tls.Listen("tcp", "127.0.0.1:0", ca.CreateLocalhostServerCert().GetServerTLSConfig())) + go func() { + conn, serverErr := tlsListener.Accept() + if serverErr != nil { + return + } + defer func() { + _ = conn.Close() + }() + _, _ = conn.Write([]byte("Hello world!")) + }() + + // Client side: + port := tlsListener.Addr().(*net.TCPAddr).Port + client := tofutestutils.Must2(tls.Dial("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), ca.GetClientTLSConfig())) + defer func() { + _ = client.Close() + }() + + t.Logf("%s", tofutestutils.Must2(io.ReadAll(client))) +} +``` \ No newline at end of file diff --git a/testca/ca.go b/testca/ca.go new file mode 100644 index 0000000..254ddf1 --- /dev/null +++ b/testca/ca.go @@ -0,0 +1,262 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testca + +import ( + "bytes" + "crypto" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "io" + "math/big" + "net" + "sync" + "testing" + "time" +) + +const caKeySize = 2048 +const expirationYears = 10 + +// New creates an x509 CA certificate that can produce certificates for testing purposes. Pass a desired randomSource +// to create a deterministic source of certificates alongside a deterministic timeSource. +func New(t *testing.T, randomSource io.Reader, timeSource func() time.Time) CertificateAuthority { + // We use a non-deterministic cheap randomness source because the certificate won't be reproducible anyway due to + // the NotBefore / NotAfter being different every time. We don't use crypto/rand.Rand because it can get blocked + // if not enough entropy is available and it doesn't matter for the test use case. + + now := timeSource() + + caCert := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"OpenTofu a Series of LF Projects, LLC"}, + Country: []string{"US"}, + }, + NotBefore: now, + NotAfter: now.AddDate(expirationYears, 0, 0), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + caPrivateKey, err := rsa.GenerateKey(randomSource, caKeySize) + if err != nil { + t.Skipf("Failed to create private key: %v", err) + } + caCertData, err := x509.CreateCertificate(randomSource, caCert, caCert, &caPrivateKey.PublicKey, caPrivateKey) + if err != nil { + t.Skipf("Failed to create CA certificate: %v", err) + } + caPEM := new(bytes.Buffer) + if err := pem.Encode(caPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: caCertData, + }); err != nil { + t.Skipf("Failed to encode CA cert: %v", err) + } + return &ca{ + t: t, + random: randomSource, + caCert: caCert, + caCertPEM: caPEM.Bytes(), + privateKey: caPrivateKey, + serial: big.NewInt(0), + lock: &sync.Mutex{}, + timeSource: timeSource, + } +} + +// CertConfig is the configuration structure for creating specialized certificates using +// CertificateAuthority.CreateConfiguredServerCert. +type CertConfig struct { + // IPAddresses contains a list of IP addresses that should be added to the SubjectAltName field of the certificate. + IPAddresses []string + // Hosts contains a list of host names that should be added to the SubjectAltName field of the certificate. + Hosts []string + // Subject is the subject (CN, etc) setting for the certificate. Most commonly, you will want the CN field to match + // one of hour host names. + Subject pkix.Name + // ExtKeyUsage describes the extended key usage. Typically, this should be: + // + // []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} + ExtKeyUsage []x509.ExtKeyUsage +} + +// KeyPair contains a certificate and private key in PEM format. +type KeyPair struct { + // Certificate contains an x509 certificate in PEM format. + Certificate []byte + // PrivateKey contains an RSA or other private key in PEM format. + PrivateKey []byte +} + +// GetPrivateKey returns a crypto.Signer for the private key. +func (k KeyPair) GetPrivateKey() crypto.PrivateKey { + block, _ := pem.Decode(k.PrivateKey) + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + panic(err) + } + return key +} + +// GetTLSCertificate returns the tls.Certificate based on this key pair. +func (k KeyPair) GetTLSCertificate() tls.Certificate { + cert, err := tls.X509KeyPair(k.Certificate, k.PrivateKey) + if err != nil { + panic(err) + } + return cert +} + +// GetServerTLSConfig returns a tls.Config suitable for a TLS server with this key pair. +func (k KeyPair) GetServerTLSConfig() *tls.Config { + return &tls.Config{ + Certificates: []tls.Certificate{ + k.GetTLSCertificate(), + }, + MinVersion: tls.VersionTLS12, + } +} + +// CertificateAuthority provides simple access to x509 CA functions for testing purposes only. +type CertificateAuthority interface { + // GetPEMCACert returns the CA certificate in PEM format. + GetPEMCACert() []byte + // GetCertPool returns an x509.CertPool configured for this CA. + GetCertPool() *x509.CertPool + // GetClientTLSConfig returns a *tls.Config with a valid cert pool configured for this CA. + GetClientTLSConfig() *tls.Config + // CreateLocalhostServerCert creates a server certificate pre-configured for "localhost", which is sufficient for + // most test cases. + CreateLocalhostServerCert() KeyPair + // CreateLocalhostClientCert creates a client certificate pre-configured for "localhost", which is sufficient for + // most test cases. + CreateLocalhostClientCert() KeyPair + // CreateConfiguredCert creates a certificate with a specialized configuration. + CreateConfiguredCert(config CertConfig) KeyPair +} + +type ca struct { + caCert *x509.Certificate + caCertPEM []byte + privateKey *rsa.PrivateKey + serial *big.Int + lock *sync.Mutex + t *testing.T + random io.Reader + timeSource func() time.Time +} + +func (c *ca) GetClientTLSConfig() *tls.Config { + certPool := c.GetCertPool() + + return &tls.Config{ + RootCAs: certPool, + MinVersion: tls.VersionTLS12, + } +} + +func (c *ca) GetCertPool() *x509.CertPool { + certPool := x509.NewCertPool() + certPool.AppendCertsFromPEM(c.caCertPEM) + return certPool +} + +func (c *ca) GetPEMCACert() []byte { + return c.caCertPEM +} + +func (c *ca) CreateConfiguredCert(config CertConfig) KeyPair { + c.lock.Lock() + defer c.lock.Unlock() + c.serial.Add(c.serial, big.NewInt(1)) + + ipAddresses := make([]net.IP, len(config.IPAddresses)) + for i, ip := range config.IPAddresses { + ipAddresses[i] = net.ParseIP(ip) + } + + now := c.timeSource() + + cert := &x509.Certificate{ + SerialNumber: c.serial, + Subject: config.Subject, + NotBefore: now, + NotAfter: now.AddDate(0, 0, 1), + SubjectKeyId: []byte{1}, + ExtKeyUsage: config.ExtKeyUsage, + KeyUsage: x509.KeyUsageDigitalSignature, + DNSNames: config.Hosts, + IPAddresses: ipAddresses, + } + certPrivKey, err := rsa.GenerateKey(c.random, caKeySize) + if err != nil { + c.t.Skipf("Failed to generate private key: %v", err) + } + certBytes, err := x509.CreateCertificate( + c.random, + cert, + c.caCert, + &certPrivKey.PublicKey, + c.privateKey, + ) + if err != nil { + c.t.Skipf("Failed to create certificate: %v", err) + } + certPrivKeyPEM := new(bytes.Buffer) + if err := pem.Encode(certPrivKeyPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey), + }); err != nil { + c.t.Skipf("Failed to encode private key: %v", err) + } + certPEM := new(bytes.Buffer) + if err := pem.Encode(certPEM, + &pem.Block{Type: "CERTIFICATE", Bytes: certBytes}, + ); err != nil { + c.t.Skipf("Failed to encode certificate: %v", err) + } + return KeyPair{ + Certificate: certPEM.Bytes(), + PrivateKey: certPrivKeyPEM.Bytes(), + } +} + +func (c *ca) CreateLocalhostServerCert() KeyPair { + return c.CreateConfiguredCert(CertConfig{ + IPAddresses: []string{"127.0.0.1", "::1"}, + Subject: pkix.Name{ + Country: []string{"US"}, + Organization: []string{"OpenTofu a Series of LF Projects, LLC"}, + CommonName: "localhost", + }, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + Hosts: []string{ + "localhost", + }, + }) +} + +func (c *ca) CreateLocalhostClientCert() KeyPair { + return c.CreateConfiguredCert(CertConfig{ + IPAddresses: []string{"127.0.0.1", "::1"}, + Subject: pkix.Name{ + Country: []string{"US"}, + Organization: []string{"OpenTofu a Series of LF Projects, LLC"}, + CommonName: "localhost", + }, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + Hosts: []string{ + "localhost", + }, + }) +} diff --git a/testca/ca_test.go b/testca/ca_test.go new file mode 100644 index 0000000..a0a4637 --- /dev/null +++ b/testca/ca_test.go @@ -0,0 +1,139 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testca_test + +import ( + "bytes" + "context" + "crypto/tls" + "io" + "net" + "strconv" + "testing" + "time" + + "github.com/opentofu/tofutestutils" + "github.com/opentofu/tofutestutils/testca" + "github.com/opentofu/tofutestutils/testrandom" +) + +func TestCA(t *testing.T) { + t.Run("correct", testCACorrectCertificate) + t.Run("incorrect", testCAIncorrectCertificate) +} + +func testCAIncorrectCertificate(t *testing.T) { + ca1 := testca.New(t, testrandom.Source(), time.Now) + ca2 := testca.New(t, testrandom.Source(), time.Now) + + if bytes.Equal(ca1.GetPEMCACert(), ca2.GetPEMCACert()) { + t.Fatalf("The two CA's have the same CA PEM!") + } + + done := make(chan struct{}) + var serverErr error + t.Logf("🍦 Setting up TLS server...") + tlsListener := tofutestutils.Must2(tls.Listen( + "tcp", + "127.0.0.1:0", + ca1.CreateLocalhostServerCert().GetServerTLSConfig()), + ) + t.Cleanup(func() { + t.Logf("🍦 Server closing listener...") + _ = tlsListener.Close() + }) + port := tlsListener.Addr().(*net.TCPAddr).Port + go func() { + defer close(done) + t.Logf("🍦 Server accepting connection...") + var conn net.Conn + conn, serverErr = tlsListener.Accept() + if serverErr != nil { + t.Logf("🍦 Server correctly received an error: %v", serverErr) + return + } + // Force a handshake even without read/write. The client automatically performs + // the handshake, but the server listener doesn't before reading. + serverErr = conn.(*tls.Conn).HandshakeContext(context.Background()) + if serverErr == nil { + t.Logf("❌ Server unexpectedly did not receive an error.") + } else { + t.Logf("🍦 Server correctly received an error: %v", serverErr) + } + _ = conn.Close() + }() + t.Logf("🔌 Client connecting to server...") + conn, err := tls.Dial( + "tcp", + net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), + ca2.GetClientTLSConfig(), + ) + if err == nil { + _ = conn.Close() + t.Fatalf("❌ The TLS connection succeeded despite the incorrect CA certificate.") + } + t.Logf("🔌 Client correctly received an error: %v", err) + <-done + if serverErr == nil { + t.Fatalf("❌ The TLS server didn't error despite the incorrect CA certificate.") + } +} + +func testCACorrectCertificate(t *testing.T) { + ca := testca.New(t, testrandom.Source(), time.Now) + const testGreeting = "Hello world!" + + var serverErr error + t.Cleanup(func() { + if serverErr != nil { + t.Fatalf("❌ TLS server failed: %v", serverErr) + } + }) + + done := make(chan struct{}) + + t.Logf("🍦 Setting up TLS server...") + tlsListener := tofutestutils.Must2(tls.Listen("tcp", "127.0.0.1:0", ca.CreateLocalhostServerCert().GetServerTLSConfig())) + t.Cleanup(func() { + t.Logf("🍦 Server closing listener...") + _ = tlsListener.Close() + }) + t.Logf("🍦 Starting TLS server...") + go func() { + defer close(done) + var conn net.Conn + t.Logf("🍦 Server accepting connection...") + conn, serverErr = tlsListener.Accept() + if serverErr != nil { + t.Errorf("❌ Server accept failed: %v", serverErr) + return + } + defer func() { + t.Logf("🍦 Server closing connection.") + _ = conn.Close() + }() + t.Logf("🍦 Server writing greeting...") + _, serverErr = conn.Write([]byte(testGreeting)) + if serverErr != nil { + t.Errorf("❌ Server write failed: %v", serverErr) + return + } + }() + t.Logf("🔌 Client connecting to server...") + port := tlsListener.Addr().(*net.TCPAddr).Port + client := tofutestutils.Must2(tls.Dial("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), ca.GetClientTLSConfig())) + defer func() { + t.Logf("🔌 Client closing connection...") + _ = client.Close() + }() + t.Logf("🔌 Client reading greeting...") + greeting := tofutestutils.Must2(io.ReadAll(client)) + if string(greeting) != testGreeting { + t.Fatalf("❌ Client received incorrect greeting: %s", greeting) + } + t.Logf("🔌 Waiting for server to finish...") + <-done +} From e669b8b51498e6b571e4e04f1ec4ff72d8085a5d Mon Sep 17 00:00:00 2001 From: Janos <86970079+janosdebugs@users.noreply.github.com> Date: Mon, 12 Aug 2024 15:20:44 +0200 Subject: [PATCH 08/10] License header Signed-off-by: Janos <86970079+janosdebugs@users.noreply.github.com> --- ca.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ca.go b/ca.go index 680f393..2a97300 100644 --- a/ca.go +++ b/ca.go @@ -1,3 +1,8 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package tofutestutils import ( From 87fe206319960747b9382adb8131f64261d52aee Mon Sep 17 00:00:00 2001 From: Janos <86970079+janosdebugs@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:01:35 +0200 Subject: [PATCH 09/10] Testing log adapter Signed-off-by: Janos <86970079+janosdebugs@users.noreply.github.com> --- go.mod | 59 ++++++++++ go.sum | 202 +++++++++++++++++++++++++++++++++ logger.go | 31 +++++ testlog/README.md | 3 + testlog/adapter.go | 87 ++++++++++++++ testlog/faket_test.go | 32 ++++++ testlog/golog.go | 20 ++++ testlog/golog_test.go | 54 +++++++++ testlog/hclog.go | 17 +++ testlog/hclog_test.go | 30 +++++ testlog/slog.go | 20 ++++ testlog/slog_test.go | 33 ++++++ testlog/testcontainers.go | 21 ++++ testlog/testcontainers_test.go | 32 ++++++ testlog/testingt.go | 13 +++ 15 files changed, 654 insertions(+) create mode 100644 logger.go create mode 100644 testlog/README.md create mode 100644 testlog/adapter.go create mode 100644 testlog/faket_test.go create mode 100644 testlog/golog.go create mode 100644 testlog/golog_test.go create mode 100644 testlog/hclog.go create mode 100644 testlog/hclog_test.go create mode 100644 testlog/slog.go create mode 100644 testlog/slog_test.go create mode 100644 testlog/testcontainers.go create mode 100644 testlog/testcontainers_test.go create mode 100644 testlog/testingt.go diff --git a/go.mod b/go.mod index 91b2b48..a20de01 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,62 @@ module github.com/opentofu/tofutestutils go 1.22 + +require ( + github.com/hashicorp/go-hclog v1.6.3 + github.com/testcontainers/testcontainers-go v0.32.0 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Microsoft/hcsshim v0.11.5 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/containerd/containerd v1.7.18 // indirect + github.com/containerd/errdefs v0.1.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v27.0.3+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/sys v0.19.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect +) diff --git a/go.sum b/go.sum index e69de29..bd59596 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,202 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/hcsshim v0.11.5 h1:haEcLNpj9Ka1gd3B3tAEs9CpE0c+1IhoL59w/exYU38= +github.com/Microsoft/hcsshim v0.11.5/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= +github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= +github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM= +github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= +github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE= +github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/testcontainers/testcontainers-go v0.32.0 h1:ug1aK08L3gCHdhknlTTwWjPHPS+/alvLJU/DRxTD/ME= +github.com/testcontainers/testcontainers-go v0.32.0/go.mod h1:CRHrzHLQhlXUsa5gXjTOfqIEJcrK5+xMDmBr/WMI88E= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20231012201019-e917dd12ba7a h1:fwgW9j3vHirt4ObdHoYNwuO24BEZjSzbh+zPaNWoiY8= +google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb h1:lK0oleSc7IQsUxO3U5TjL9DWlsxpEBemh+zpB7IqhWI= +google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..725a260 --- /dev/null +++ b/logger.go @@ -0,0 +1,31 @@ +package tofutestutils + +import ( + "log" + "log/slog" + "testing" + + "github.com/hashicorp/go-hclog" + "github.com/opentofu/tofutestutils/testlog" + "github.com/testcontainers/testcontainers-go" +) + +// NewGoTestLogger returns a log.Logger implementation that writes to testing.T. +func NewGoLogger(t *testing.T) *log.Logger { + return testlog.NewGoTestLogger(t) +} + +// NewSlogHandler produces an slog.Handler that writes to t. +func NewSlogHandler(t *testing.T) slog.Handler { + return testlog.NewSlogHandler(t) +} + +// NewHCLogAdapter returns a hclog.SinkAdapter-compatible logger that logs into a test facility. +func NewHCLogAdapter(t *testing.T) hclog.SinkAdapter { + return testlog.NewHCLogAdapter(t) +} + +// NewTestContainersLogger produces a logger for testcontainers. +func NewTestContainersLogger(t *testing.T) testcontainers.Logging { + return testlog.NewTestContainersLogger(t) +} diff --git a/testlog/README.md b/testlog/README.md new file mode 100644 index 0000000..4dbafe3 --- /dev/null +++ b/testlog/README.md @@ -0,0 +1,3 @@ +# Logging + +This package contains a set of logging tools that write to `*testing.T` as a log output. You can use `testlog.NewHCLogAdapter()`, `testlog.NewGoTestLogger()`, `testlog.NewSlogHandler()` or `testlog.NewTestContainersLogger()` to obtain a logger for your use case. \ No newline at end of file diff --git a/testlog/adapter.go b/testlog/adapter.go new file mode 100644 index 0000000..64b4000 --- /dev/null +++ b/testlog/adapter.go @@ -0,0 +1,87 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testlog + +import ( + "context" + "fmt" + "log/slog" + "strings" + + "github.com/hashicorp/go-hclog" +) + +func newAdapter(t testingT) *testLogAdapter { + adapter := &testLogAdapter{t: t} + t.Cleanup(func() { + _ = adapter.Close() + }) + return adapter +} + +type testLogAdapter struct { + t testingT + buf []byte +} + +func (t *testLogAdapter) Enabled(_ context.Context, _ slog.Level) bool { + t.t.Helper() + return true +} + +func (t *testLogAdapter) Handle(_ context.Context, record slog.Record) error { + t.t.Helper() + t.t.Logf("%s\t%s", record.Level, record.Message) + return nil +} + +func (t *testLogAdapter) WithAttrs(_ []slog.Attr) slog.Handler { + t.t.Helper() + return t +} + +func (t *testLogAdapter) WithGroup(_ string) slog.Handler { + t.t.Helper() + return t +} + +// Accept implements a hclog SinkAdapter. +func (t *testLogAdapter) Accept(name string, level hclog.Level, msg string, args ...interface{}) { + t.t.Helper() + msg = fmt.Sprintf(msg, args...) + t.t.Logf("%s\t%s\t%s", name, level.String(), msg) +} + +// Printf implements a standardized way to write logs, e.g. for the testcontainers package. +func (t *testLogAdapter) Printf(format string, v ...interface{}) { + t.t.Helper() + t.t.Logf(format, v...) +} + +// Write provides a Go log-compatible writer. +func (t *testLogAdapter) Write(p []byte) (int, error) { + t.t.Helper() + t.buf = append(t.buf, p...) + i := 0 + for i < len(t.buf) { + if t.buf[i] == '\n' { + t.t.Logf("%s", strings.TrimRight(string(t.buf[:i]), "\r")) + t.buf = t.buf[i+1:] + i = 0 + } else { + i++ + } + } + return len(p), nil +} + +func (t *testLogAdapter) Close() error { + t.t.Helper() + if len(t.buf) > 0 { + t.t.Logf("%s", t.buf) + } + return nil +} diff --git a/testlog/faket_test.go b/testlog/faket_test.go new file mode 100644 index 0000000..39ac36c --- /dev/null +++ b/testlog/faket_test.go @@ -0,0 +1,32 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testlog + +import ( + "fmt" +) + +type fakeT struct { + lines []string + cleanupFuncs []func() +} + +func (f *fakeT) Helper() { +} + +func (f *fakeT) Logf(format string, args ...interface{}) { + f.lines = append(f.lines, fmt.Sprintf(format, args...)) +} + +func (f *fakeT) Cleanup(cleanupFunc func()) { + f.cleanupFuncs = append(f.cleanupFuncs, cleanupFunc) +} + +func (f *fakeT) RunCleanup() { + for _, cleanupFunc := range f.cleanupFuncs { + cleanupFunc() + } +} diff --git a/testlog/golog.go b/testlog/golog.go new file mode 100644 index 0000000..999ebd7 --- /dev/null +++ b/testlog/golog.go @@ -0,0 +1,20 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testlog + +import ( + "log" + "testing" +) + +// NewGoTestLogger returns a log.Logger implementation that writes to testing.T. +func NewGoTestLogger(t *testing.T) *log.Logger { + return newGoTestLogger(t) +} + +func newGoTestLogger(t testingT) *log.Logger { + return log.New(newAdapter(t), "", 0) +} diff --git a/testlog/golog_test.go b/testlog/golog_test.go new file mode 100644 index 0000000..acf6c06 --- /dev/null +++ b/testlog/golog_test.go @@ -0,0 +1,54 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testlog + +import ( + "testing" +) + +func TestGoTestLogger(t *testing.T) { + t2 := &fakeT{} + const testString = "Hello world!" + + logger := newGoTestLogger(t2) + logger.Print(testString) + + if len(t2.lines) != 1 { + t.Fatalf("❌ Expected 1 line, got %d", len(t2.lines)) + } + t2.RunCleanup() + if len(t2.lines) != 1 { + t.Fatalf("❌ Expected 1 line, got %d", len(t2.lines)) + } + if t2.lines[0] != testString { + t.Fatalf("❌ Expected 'Hello world!', got '%s'", t2.lines[0]) + } + t.Logf("✅ Correctly logged text.") +} + +func TestGoTestLoggerMultiline(t *testing.T) { + t2 := &fakeT{} + const testString1 = "Hello" + const testString2 = "world!" + const testString = testString1 + "\n" + testString2 + logger := newGoTestLogger(t2) + logger.Print(testString) + + if len(t2.lines) != 2 { + t.Fatalf("❌ Expected 2 lines, got %d", len(t2.lines)) + } + t2.RunCleanup() + if len(t2.lines) != 2 { + t.Fatalf("❌ Expected 2 lines, got %d", len(t2.lines)) + } + if t2.lines[0] != testString1 { + t.Fatalf("❌ Expected '%s', got '%s'", testString1, t2.lines[0]) + } + if t2.lines[1] != testString2 { + t.Fatalf("❌ Expected '%s', got '%s'", testString2, t2.lines[0]) + } + t.Logf("✅ Correctly logged multiline text.") +} diff --git a/testlog/hclog.go b/testlog/hclog.go new file mode 100644 index 0000000..6577a68 --- /dev/null +++ b/testlog/hclog.go @@ -0,0 +1,17 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testlog + +import ( + "testing" + + "github.com/hashicorp/go-hclog" +) + +// NewHCLogAdapter returns a hclog.SinkAdapter-compatible logger that logs into a test facility. +func NewHCLogAdapter(t *testing.T) hclog.SinkAdapter { + return newAdapter(t) +} diff --git a/testlog/hclog_test.go b/testlog/hclog_test.go new file mode 100644 index 0000000..e4c4f43 --- /dev/null +++ b/testlog/hclog_test.go @@ -0,0 +1,30 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testlog + +import ( + "strings" + "testing" + + "github.com/hashicorp/go-hclog" +) + +func TestHCLogAdapter(t *testing.T) { + t2 := &fakeT{} + logger := newAdapter(t2) + const testString = "Hello world!" + + interceptLogger := hclog.NewInterceptLogger(nil) + interceptLogger.RegisterSink(logger) + interceptLogger.Log(hclog.Error, testString) + for _, line := range t2.lines { + if strings.Contains(line, testString) { + t.Logf("✅ Found the test string in the log output.") + return + } + } + t.Fatalf("❌ Failed to find test string in the log output.") +} diff --git a/testlog/slog.go b/testlog/slog.go new file mode 100644 index 0000000..8d210c9 --- /dev/null +++ b/testlog/slog.go @@ -0,0 +1,20 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testlog + +import ( + "log/slog" + "testing" +) + +// NewSlogHandler produces an slog.Handler that writes to t. +func NewSlogHandler(t *testing.T) slog.Handler { + return newSlogHandler(t) +} + +func newSlogHandler(t testingT) slog.Handler { + return newAdapter(t) +} diff --git a/testlog/slog_test.go b/testlog/slog_test.go new file mode 100644 index 0000000..b72a8f0 --- /dev/null +++ b/testlog/slog_test.go @@ -0,0 +1,33 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testlog + +import ( + "log/slog" + "strings" + "testing" +) + +func TestSlog(t *testing.T) { + const testString = "Hello world!" + + tLogger := &fakeT{} + + logger := slog.New(newSlogHandler(tLogger)) + + logger.Debug(testString) + + tLogger.RunCleanup() + + if len(tLogger.lines) != 1 { + t.Fatalf("❌ Incorrect number of lines in log: %d", len(tLogger.lines)) + } + + if !strings.Contains(tLogger.lines[0], testString) { + t.Fatalf("❌ The log output doesn't contain the test string: %s", tLogger.lines[0]) + } + t.Logf("✅ Correctly logged text.") +} diff --git a/testlog/testcontainers.go b/testlog/testcontainers.go new file mode 100644 index 0000000..9f3a238 --- /dev/null +++ b/testlog/testcontainers.go @@ -0,0 +1,21 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testlog + +import ( + "testing" + + "github.com/testcontainers/testcontainers-go" +) + +// NewTestContainersLogger produces a logger for testcontainers. +func NewTestContainersLogger(t *testing.T) testcontainers.Logging { + return newTestContainersLogger(t) +} + +func newTestContainersLogger(t testingT) testcontainers.Logging { + return newAdapter(t) +} diff --git a/testlog/testcontainers_test.go b/testlog/testcontainers_test.go new file mode 100644 index 0000000..facd9fc --- /dev/null +++ b/testlog/testcontainers_test.go @@ -0,0 +1,32 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testlog + +import ( + "strings" + "testing" +) + +func TestTestContainersLogging(t *testing.T) { + const testString = "Hello world!" + + tLogger := &fakeT{} + + logger := newTestContainersLogger(tLogger) + + logger.Printf(testString) + + tLogger.RunCleanup() + + if len(tLogger.lines) != 1 { + t.Fatalf("❌ Incorrect number of lines in log: %d", len(tLogger.lines)) + } + + if !strings.Contains(tLogger.lines[0], testString) { + t.Fatalf("❌ The log output doesn't contain the test string: %s", tLogger.lines[0]) + } + t.Logf("✅ Correctly logged text.") +} diff --git a/testlog/testingt.go b/testlog/testingt.go new file mode 100644 index 0000000..1e6f27f --- /dev/null +++ b/testlog/testingt.go @@ -0,0 +1,13 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testlog + +// testingT is a simplified interface to *testing.T. This interface is mainly used for internal testing purposes. +type testingT interface { + Logf(format string, args ...interface{}) + Cleanup(func()) + Helper() +} From fa2141c632cf3310f7628e34e0d0f52995ad9f86 Mon Sep 17 00:00:00 2001 From: Janos <86970079+janosdebugs@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:03:13 +0200 Subject: [PATCH 10/10] License fix Signed-off-by: Janos <86970079+janosdebugs@users.noreply.github.com> --- logger.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/logger.go b/logger.go index 725a260..1e61e82 100644 --- a/logger.go +++ b/logger.go @@ -1,3 +1,8 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package tofutestutils import ( @@ -10,7 +15,7 @@ import ( "github.com/testcontainers/testcontainers-go" ) -// NewGoTestLogger returns a log.Logger implementation that writes to testing.T. +// NewGoLogger returns a log.Logger implementation that writes to testing.T. func NewGoLogger(t *testing.T) *log.Logger { return testlog.NewGoTestLogger(t) }