diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2026-06-18 23:23:36 +0200 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2026-06-19 20:16:11 +0200 |
| commit | 71d31d73e7dc3b9cdb3c53b4f343b1928a9e858b (patch) | |
| tree | e1cd6d01837ef6ef07504ba3893c6f5e55f8740b /testcapture | |
| parent | b95ec7449ebec844221f80ecbf7976f3af878eee (diff) | |
| download | go-gg-71d31d73e7dc3b9cdb3c53b4f343b1928a9e858b.tar.gz | |
add package testcapture, package assert (minimal implementation)
Diffstat (limited to 'testcapture')
| -rw-r--r-- | testcapture/capture.go | 416 | ||||
| -rw-r--r-- | testcapture/capture_test.go | 403 |
2 files changed, 819 insertions, 0 deletions
diff --git a/testcapture/capture.go b/testcapture/capture.go new file mode 100644 index 0000000..3ac4f52 --- /dev/null +++ b/testcapture/capture.go @@ -0,0 +1,416 @@ +// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> +// SPDX-License-Identifier: Apache-2.0 + +// Package testcapture contains [Capture], a function that executes test code in a way that captures error messages and side effects without failing the overall test. +// +// The main intended use case is testing test assertions where calls to e.g. t.Error() are an expected part of a successful test run. +package testcapture + +import ( + "context" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "runtime" + "slices" + "sync" + "sync/atomic" + + "go.xyrillian.de/gg/assert" +) + +// Result is returned by func [Capture]. +type Result struct { + // Outcome describes how the test ended. + Outcome Outcome + // Panic contains a payload recovered from a panic(), if Outcome is [OutcomePanicked]. + Panic any + // Messages contains log lines captured from t.Log() calls, or functions calling t.Log(), such as t.Error() and t.Fatal(); + // as well as data captured from t.Output().Write() calls. + Messages []Message + // Attrs contains attributes captured in t.Attr() calls. + Attrs map[string]string + // Artifacts holds the contents of any regular files that were created below t.ArtifactDir(), keyed with the path relative to t.ArtifactDir(). + Artifacts map[string]string +} + +// Outcome is an enum. +// It appears in type [Result]. +type Outcome string + +const ( + // OutcomeFinished describes a [Capture] that ended with the test running to completion. + OutcomeFinished Outcome = "finished" + // OutcomeFailed describes a [Capture] that ended early because of a t.FailNow() call. + OutcomeFailed Outcome = "failed" + // OutcomeSkipped describes a [Capture] that ended early because of a t.SkipNow() call. + OutcomeSkipped Outcome = "skipped" + // OutcomePanicked describes a [Capture] that ended early because of a panic() call. + OutcomePanicked Outcome = "panicked" +) + +// Message is a piece of log output captured by func [Capture]. +// It appears in type [Result]. +// - Each call to t.Log(), t.Logf() or their derived functions results in one Message instance of type [Log]. +// - Writing into t.Output() between two calls to t.Log(), t.Logf() etc. results in a single Message instance of type [Output], even if Write() is called multiple times. +type Message struct { + Message string + Type MessageType +} + +// Log is a shorthand for constructing [Message] objects of type [MessageTypeLog]. +func Log[T interface{ ~string }](message T) Message { + return Message{string(message), MessageTypeLog} +} + +// Output is a shorthand for constructing [Message] objects of type [MessageTypeOutput]. +func Output[T interface{ ~string | ~[]byte }](message T) Message { + return Message{string(message), MessageTypeOutput} +} + +// MessageType is an enum. +// It appears in type [Message]. +type MessageType string + +const ( + // MessageTypeLog describes [Message] instances created by calls to t.Log(), t.Logf(), or functions calling them, such as t.Error() and t.Fatal(). + MessageTypeLog MessageType = "log" + // MessageTypeOutput describes [Message] instances created by calls to t.Output().Write(). + MessageTypeOutput MessageType = "output" +) + +// Capture executes a test function with a stub implementation of [assert.TestingTB] that captures all calls to it. +// It is intended for unit-testing test assertions. +// +// The name argument is what will be reported in t.Name() within the test. +func Capture(ctx context.Context, name string, test func(assert.TestingTB)) Result { + r := Result{ + Outcome: OutcomeFinished, // can be overridden by Fail() or SkipNow() + } + executeCapture(ctx, name, &r, test) + return r +} + +// capturer is the implementation of [assert.TestingTB] used by func [Capture]. +type capturer struct { + context context.Context + cleanups []func() + name string + result *Result + state struct { + ArtifactDir string + } + + cleanupsMutex sync.Mutex // lock for access to the `cleanups` field + resultMutex sync.RWMutex // lock for access to the `result` field + stateMutex sync.Mutex // lock for access to the `state` field + nonlocalMutex sync.Mutex // lock for non-local effects like Setenv() or filesystem operations +} + +func executeCapture(ctx context.Context, name string, r *Result, test func(assert.TestingTB)) { + ctx, cancel := context.WithCancel(ctx) + t := capturer{ + context: ctx, + cleanups: nil, + name: name, + result: r, + } + defer func() { + t.setOutcome(recover()) + cancel() // T.Context() demands that the context be canceled before any cleanup handlers + for _, cleanup := range slices.Backward(t.cleanups) { + cleanup() + } + }() + test(&t) +} + +func (t *capturer) setOutcome(panicPayload any) { + t.resultMutex.Lock() + defer t.resultMutex.Unlock() + if panicPayload == nil { + return + } else if outcome, ok := panicPayload.(Outcome); ok { + t.result.Outcome = outcome + } else { + t.result.Outcome = OutcomePanicked + t.result.Panic = panicPayload + } +} + +func (t *capturer) pushOutput(buf []byte, msgType MessageType) { + t.resultMutex.Lock() + defer t.resultMutex.Unlock() + + // try to merge consecutive t.Output().Write() calls together + if msgType == MessageTypeOutput && len(t.result.Messages) > 0 { + idx := len(t.result.Messages) - 1 + msg := t.result.Messages[idx] + if msg.Type == MessageTypeOutput { + msg.Message = msg.Message + string(buf) + t.result.Messages[idx] = msg + return + } + } + + t.result.Messages = append(t.result.Messages, Message{ + Message: string(buf), + Type: msgType, + }) +} + +var tempdirID atomic.Uint64 + +func pickTempdir() (string, error) { + path := filepath.Join(os.TempDir(), fmt.Sprintf("gg-assert-capture-%d", tempdirID.Add(1))) + return path, os.MkdirAll(path, 0777) +} + +func collectArtifacts(dirPath string) (map[string]string, error) { + dir, err := os.OpenRoot(dirPath) + if err != nil { + return nil, err + } + dirFS := dir.FS() + + result := make(map[string]string) + err = fs.WalkDir(dirFS, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.Type().IsRegular() { + buf, err := fs.ReadFile(dirFS, path) + if err != nil { + return err + } + result[path] = string(buf) + } + return nil + }) + if err != nil { + return nil, err + } + + return result, os.RemoveAll(dirPath) +} + +// ArtifactDir implements the [assert.TestingTB] interface. +func (t *capturer) ArtifactDir() string { + t.stateMutex.Lock() + defer t.stateMutex.Unlock() + if t.state.ArtifactDir == "" { + path, err := pickTempdir() + if err != nil { + t.Fatal("in t.ArtifactDir(): ", err) + } + t.state.ArtifactDir = path + + t.Cleanup(func() { + artifacts, err := collectArtifacts(path) + if err == nil { + t.resultMutex.Lock() + defer t.resultMutex.Unlock() + t.result.Artifacts = artifacts + } else { + t.Error(err) + } + }) + } + return t.state.ArtifactDir +} + +// Attr implements the [assert.TestingTB] interface. +func (t *capturer) Attr(key, value string) { + t.resultMutex.Lock() + defer t.resultMutex.Unlock() + if t.result.Attrs == nil { + t.result.Attrs = make(map[string]string) + } + t.result.Attrs[key] = value +} + +// Chdir implements the [assert.TestingTB] interface. +func (t *capturer) Chdir(dir string) { + t.doChdir(dir) + + // the following is done outside of doChdir() because t.Setenv() also locks t.nonlocalMutex + switch runtime.GOOS { + case "windows", "plan9": + // these platforms do not use the PWD variable + default: + dir, err := os.Getwd() // returns an absolute path even if `dir` is not one + if err != nil { + t.Fatal(err) + } + t.Setenv("PWD", dir) + } +} + +func (t *capturer) doChdir(dir string) { + t.nonlocalMutex.Lock() + defer t.nonlocalMutex.Unlock() + + oldDir, err := os.Open(".") + if err != nil { + t.Fatal(err) + } + err = os.Chdir(dir) + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + err := oldDir.Chdir() + if err != nil { + t.Error("could not reset cwd changed by t.Chdir(): ", err) + } + }) +} + +// Cleanup implements the [assert.TestingTB] interface. +func (t *capturer) Cleanup(action func()) { + t.cleanupsMutex.Lock() + defer t.cleanupsMutex.Unlock() + t.cleanups = append(t.cleanups, action) +} + +// Context implements the [assert.TestingTB] interface. +func (t *capturer) Context() context.Context { + return t.context +} + +// Error implements the [assert.TestingTB] interface. +func (t *capturer) Error(args ...any) { + t.Log(args...) + t.Fail() +} + +// Errorf implements the [assert.TestingTB] interface. +func (t *capturer) Errorf(format string, args ...any) { + t.Logf(format, args...) + t.Fail() +} + +// Fail implements the [assert.TestingTB] interface. +func (t *capturer) Fail() { + t.resultMutex.Lock() + defer t.resultMutex.Unlock() + t.result.Outcome = OutcomeFailed +} + +// Failed implements the [assert.TestingTB] interface. +func (t *capturer) Failed() bool { + t.resultMutex.RLock() + defer t.resultMutex.RUnlock() + return t.result.Outcome == OutcomeFailed +} + +// FailNow implements the [assert.TestingTB] interface. +func (t *capturer) FailNow() { + panic(OutcomeFailed) +} + +// Fatal implements the [assert.TestingTB] interface. +func (t *capturer) Fatal(args ...any) { + t.Log(args...) + t.FailNow() +} + +// Fatalf implements the [assert.TestingTB] interface. +func (t *capturer) Fatalf(format string, args ...any) { + t.Logf(format, args...) + t.FailNow() +} + +// Helper implements the [assert.TestingTB] interface. +func (t *capturer) Helper() { + // no-op because we do not collect file and line information at the moment +} + +// Log implements the [assert.TestingTB] interface. +func (t *capturer) Log(args ...any) { + t.pushOutput(fmt.Append(nil, args...), MessageTypeLog) +} + +// Logf implements the [assert.TestingTB] interface. +func (t *capturer) Logf(format string, args ...any) { + t.pushOutput(fmt.Appendf(nil, format, args...), MessageTypeLog) +} + +// Name implements the [assert.TestingTB] interface. +func (t *capturer) Name() string { + return t.name +} + +// Output implements the [assert.TestingTB] interface. +func (t *capturer) Output() io.Writer { + return outputCapturer{t} +} + +type outputCapturer struct { + t *capturer +} + +// Write implements the [io.Writer] interface. +func (c outputCapturer) Write(buf []byte) (int, error) { + c.t.pushOutput(buf, MessageTypeOutput) + return len(buf), nil +} + +// Setenv implements the [assert.TestingTB] interface. +func (t *capturer) Setenv(key, value string) { + t.nonlocalMutex.Lock() + defer t.nonlocalMutex.Unlock() + + oldValue, hasOldValue := os.LookupEnv(key) + os.Setenv(key, value) + + t.Cleanup(func() { + if hasOldValue { + os.Setenv(key, oldValue) + } else { + os.Unsetenv(key) + } + }) +} + +// Skip implements the [assert.TestingTB] interface. +func (t *capturer) Skip(args ...any) { + t.Log(args...) + t.SkipNow() +} + +// Skipf implements the [assert.TestingTB] interface. +func (t *capturer) Skipf(format string, args ...any) { + t.Logf(format, args...) + t.SkipNow() +} + +// SkipNow implements the [assert.TestingTB] interface. +func (t *capturer) SkipNow() { + panic(OutcomeSkipped) +} + +// Skipped implements the [assert.TestingTB] interface. +func (t *capturer) Skipped() bool { + t.resultMutex.RLock() + defer t.resultMutex.RUnlock() + return t.result.Outcome == OutcomeSkipped +} + +// TempDir implements the [assert.TestingTB] interface. +func (t *capturer) TempDir() string { + path, err := pickTempdir() + if err != nil { + t.Fatal("in t.TempDir(): ", err) + } + t.Cleanup(func() { + err := os.RemoveAll(path) + if err != nil { + t.Error(err) + } + }) + return path +} diff --git a/testcapture/capture_test.go b/testcapture/capture_test.go new file mode 100644 index 0000000..9e53b71 --- /dev/null +++ b/testcapture/capture_test.go @@ -0,0 +1,403 @@ +// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> +// SPDX-License-Identifier: Apache-2.0 + +package testcapture_test + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "go.xyrillian.de/gg/assert" + "go.xyrillian.de/gg/testcapture" +) + +func TestCaptureArtifactDir(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + // test that writing a regular file works + fooPath := filepath.Join(t.ArtifactDir(), "foo.txt") + err := os.WriteFile(fooPath, []byte("Hello World."), 0666) + if err != nil { + t.Fatal(err) + } + + // test that nesting regular files into directories works + // (and also, implicitly, that multiple calls to ArtifactDir() return the same path) + barPath := filepath.Join(t.ArtifactDir(), "bar/a/b/c/d/data.json") + err = os.MkdirAll(filepath.Dir(barPath), 0777) + if err != nil { + t.Fatal(err) + } + err = os.WriteFile(barPath, []byte(`{"bar":42}`), 0666) + if err != nil { + t.Fatal(err) + } + + // test that empty directories are ignored by the capture + err = os.MkdirAll(filepath.Join(t.ArtifactDir(), "unused"), 0777) + if err != nil { + t.Fatal(err) + } + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFinished, + Artifacts: map[string]string{ + "foo.txt": "Hello World.", + "bar/a/b/c/d/data.json": `{"bar":42}`, + }, + }) +} + +func TestCaptureAttr(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + t.Attr("foo", "bar") + t.Attr("foo", "baz") + t.Attr("hello", "world") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFinished, + Attrs: map[string]string{ + "foo": "baz", + "hello": "world", + }, + }) +} + +func TestCaptureChdir(t *testing.T) { + var cwdInCapture string + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + // ArtifactDir() is used as a target for Chdir()... + dir := t.ArtifactDir() + t.Chdir(dir) + // ...because we have an easy way to check if we are actually in that dir + err := os.WriteFile("foo.txt", []byte("foo"), 0666) + if err != nil { + t.Error(err) + } + + // smuggle the cwd out of the capture for the cleanup test below + cwdInCapture, err = os.Getwd() + if err != nil { + t.Error(err) + } + + // t.Chdir() should have set $PWD to an absolute path + assert.Equal(t, os.Getenv("PWD"), cwdInCapture) + assert.Equal(t, filepath.IsAbs(cwdInCapture), true) + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFinished, + Artifacts: map[string]string{"foo.txt": "foo"}, + }) + + // check that we reset the working directory at the end of the test + cwdAfterCapture, err := os.Getwd() + if err != nil { + t.Error(err) + } + if cwdAfterCapture == cwdInCapture { + t.Error("cwd should have been reset, but still is", cwdInCapture) + } +} + +func TestCaptureCleanup(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + // test that cleanups run in reverse order of registration, and only after the test itself is done + t.Log("starting up") + t.Cleanup(func() { t.Log("first cleanup") }) + t.Cleanup(func() { t.Log("second cleanup") }) + t.Cleanup(func() { t.Log("third cleanup") }) + t.Log("shutting down") + + // test that cleanups run even after a panic + panic("kaboom") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomePanicked, + Messages: []testcapture.Message{ + testcapture.Log("starting up"), + testcapture.Log("shutting down"), + testcapture.Log("third cleanup"), + testcapture.Log("second cleanup"), + testcapture.Log("first cleanup"), + }, + Panic: "kaboom", + }) +} + +func TestCaptureContext(t *testing.T) { + ctx := context.WithValue(t.Context(), "foo", "bar") //nolint:staticcheck // we do not care about type collision risks for this simple test + result := testcapture.Capture(ctx, t.Name(), func(t assert.TestingTB) { + // test that the context is live within the test + err := t.Context().Err() + if err != nil { + t.Error(err) + } + + // test that the context is expired at cleanup time + t.Cleanup(func() { + err := t.Context().Err() + if err == nil { + t.Error("still alive!?") + } + }) + + // test that the context is derived from the one passed to Capture() + value := ctx.Value("foo") + if value != "bar" { + t.Error("did not see the value") + } + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFinished, + }) +} + +func TestCaptureError(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + simpleError := errors.New("bar") + t.Error("foo: ", simpleError) + // check that the test keeps going, and that t.Log() does not reset the Outcome + t.Log("still going") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFailed, + Messages: []testcapture.Message{ + testcapture.Log("foo: bar"), + testcapture.Log("still going"), + }, + }) +} + +func TestCaptureErrorf(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + t.Errorf("foo = %d", 42) + // check that the test keeps going, and that t.Log() does not reset the Outcome + t.Log("still going") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFailed, + Messages: []testcapture.Message{ + testcapture.Log("foo = 42"), + testcapture.Log("still going"), + }, + }) +} + +func TestCaptureFail(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + if !t.Failed() { + t.Log("looking good so far") + } + t.Fail() + if t.Failed() { + t.Log("still going") + } + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFailed, + Messages: []testcapture.Message{ + testcapture.Log("looking good so far"), + testcapture.Log("still going"), + }, + }) +} + +func TestCaptureFailNow(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + if !t.Failed() { + t.Log("looking good so far") + } + t.FailNow() + t.Log("still going") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFailed, + Messages: []testcapture.Message{ + testcapture.Log("looking good so far"), + // "still going" is not logged because FailNow() bails + }, + }) +} + +func TestCaptureFatal(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + t.Log("looking good so far") + t.Fatal("kaboom") + t.Log("still going") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFailed, + Messages: []testcapture.Message{ + testcapture.Log("looking good so far"), + testcapture.Log("kaboom"), + // "still going" is not logged because Fatal() bails + }, + }) +} + +func TestCaptureFatalf(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + t.Log("looking good so far") + t.Fatalf("kaboom %d", 42) + t.Log("still going") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFailed, + Messages: []testcapture.Message{ + testcapture.Log("looking good so far"), + testcapture.Log("kaboom 42"), + // "still going" is not logged because Fatalf() bails + }, + }) +} + +func TestCaptureName(t *testing.T) { + result := testcapture.Capture(t.Context(), "Harold", func(t assert.TestingTB) { + panic(t.Name() + " died") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomePanicked, + Panic: "Harold died", + }) +} + +func TestCaptureOutput(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + t.Log("hello 1") + for range 10 { + fmt.Fprintln(t.Output(), "a") + } + t.Log("hello 2") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFinished, + Messages: []testcapture.Message{ + testcapture.Log("hello 1"), + testcapture.Output(strings.Repeat("a\n", 10)), + testcapture.Log("hello 2"), + }, + }) +} + +func TestCapturePanic(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + t.Error("we are going to blow up") + panic("kaboom") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomePanicked, // Panicked takes precedence over Failed + Messages: []testcapture.Message{ + testcapture.Log("we are going to blow up"), + }, + Panic: "kaboom", + }) +} + +func TestCaptureSetenv(t *testing.T) { + t.Setenv("GG_TEST_SETENV_SCOPE", "outer") + + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + // test t.Setenv() overriding an existing variable + assert.Equal(t, os.Getenv("GG_TEST_SETENV_SCOPE"), "outer") + t.Setenv("GG_TEST_SETENV_SCOPE", "inner") + assert.Equal(t, os.Getenv("GG_TEST_SETENV_SCOPE"), "inner") + + // test t.Setenv() setting a fresh variable + // (this variable should be completely removed after the end of the test) + t.Setenv("GG_TEST_SETENV_PAYLOAD", "42") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFinished, + }) + + assert.Equal(t, os.Getenv("GG_TEST_SETENV_SCOPE"), "outer") + _, ok := os.LookupEnv("GG_TEST_SETENV_PAYLOAD") + assert.Equal(t, ok, false) +} + +func TestCaptureSkip(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + if !t.Skipped() { + t.Skip("this looks uninteresting") + } + t.Log("still going") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeSkipped, + Messages: []testcapture.Message{ + testcapture.Log("this looks uninteresting"), + // "still going" is not logged because Skip() bails + }, + }) +} + +func TestCaptureSkipf(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + if !t.Skipped() { + t.Skipf("pretty sure the answer is %d", 42) + } + t.Log("still going") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeSkipped, + Messages: []testcapture.Message{ + testcapture.Log("pretty sure the answer is 42"), + // "still going" is not logged because Skipf() bails + }, + }) +} + +func TestCaptureSkipNow(t *testing.T) { + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + if !t.Skipped() { + t.Log("this looks uninteresting") + } + t.SkipNow() + t.Log("still going") + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeSkipped, + Messages: []testcapture.Message{ + testcapture.Log("this looks uninteresting"), + // "still going" is not logged because Skip() bails + }, + }) +} + +func TestCaptureTempDir(t *testing.T) { + var path string + result := testcapture.Capture(t.Context(), t.Name(), func(t assert.TestingTB) { + // fill a TempDir with some stuff + path = t.TempDir() + err := os.MkdirAll(filepath.Join(path, "emptydir"), 0777) + if err != nil { + t.Error(err) + } + err = os.WriteFile(filepath.Join(path, "data.json"), []byte(`{"username":"admin"}`), 0666) + if err != nil { + t.Error(err) + } + + // check that each call to TempDir returns a new dir + otherPath := t.TempDir() + if otherPath == path { + t.Error("should have returned a different TempDir") + } + }) + assert.Equal(t, result, testcapture.Result{ + Outcome: testcapture.OutcomeFinished, + }) + + // check that TempDir was cleaned up + _, err := os.Stat(path) + if err == nil { + t.Error("TempDir was not cleaned up") + } else if !os.IsNotExist(err) { + t.Error(err) + } +} |
