Skip to content

Commit 6657a87

Browse files
committed
Add TempDir and NewLogFile test utilities
1 parent b9b2cf9 commit 6657a87

File tree

2 files changed

+255
-0
lines changed

2 files changed

+255
-0
lines changed

testing/fs.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package testing
19+
20+
import (
21+
"os"
22+
"path/filepath"
23+
"runtime"
24+
"strings"
25+
"testing"
26+
"unicode"
27+
"unicode/utf8"
28+
)
29+
30+
// TempDir creates a temporary directory that will be
31+
// removed if the tests passes. The temporary directory is
32+
// created by joining all elements from path, with the sanitised
33+
// test name.
34+
//
35+
// If path is empty, the temporary directory is created in os.TempDir.
36+
//
37+
// When tests are run with -v, the temporary directory absolute
38+
// path will be logged.
39+
func TempDir(t *testing.T, path ...string) string {
40+
rootDir := filepath.Join(path...)
41+
42+
if rootDir == "" {
43+
rootDir = os.TempDir()
44+
}
45+
46+
rootDir, err := filepath.Abs(rootDir)
47+
if err != nil {
48+
t.Fatalf("cannot get absolute path: %s", err)
49+
}
50+
51+
// Logic copied with small modifications from
52+
// the Go source code: testing/testing.go
53+
folderName := t.Name()
54+
mapper := func(r rune) rune {
55+
if r < utf8.RuneSelf {
56+
const allowed = "_-"
57+
if '0' <= r && r <= '9' ||
58+
'a' <= r && r <= 'z' ||
59+
'A' <= r && r <= 'Z' {
60+
return r
61+
}
62+
if strings.ContainsRune(allowed, r) {
63+
return r
64+
}
65+
} else if unicode.IsLetter(r) || unicode.IsNumber(r) {
66+
return r
67+
}
68+
return -1
69+
}
70+
folderName = strings.Map(mapper, folderName)
71+
72+
if err := os.MkdirAll(rootDir, 0o750); err != nil {
73+
t.Fatalf("error making test dir: %s: %s", rootDir, err)
74+
}
75+
76+
tempDir, err := os.MkdirTemp(rootDir, folderName)
77+
if err != nil {
78+
t.Fatalf("failed to make temp directory: %s", err)
79+
}
80+
81+
cleanup := func() {
82+
if !t.Failed() {
83+
if err := os.RemoveAll(tempDir); err != nil {
84+
// Ungly workaround Windows limitations
85+
// Windows does not support the Interrup signal, so it might
86+
// happen that Filebeat is still running, keeping it's registry
87+
// file open, thus preventing the temporary folder from being
88+
// removed. So we log the error and move on without failing the
89+
// test
90+
if runtime.GOOS == "windows" {
91+
t.Logf("[WARN] Could not remove temporatry directory '%s': %s", tempDir, err)
92+
} else {
93+
t.Errorf("could not remove temp dir '%s': %s", tempDir, err)
94+
}
95+
}
96+
} else {
97+
t.Logf("Temporary directory saved: %s", tempDir)
98+
}
99+
}
100+
t.Cleanup(cleanup)
101+
102+
return tempDir
103+
}

testing/logfile.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package testing
19+
20+
import (
21+
"bufio"
22+
"errors"
23+
"fmt"
24+
"io"
25+
"os"
26+
"strings"
27+
"testing"
28+
"time"
29+
30+
"github.com/stretchr/testify/assert"
31+
"github.com/stretchr/testify/require"
32+
)
33+
34+
// LogFile wraps a *os.File and makes it more suitable for tests.
35+
// Key feature:
36+
// - Methods to search and wait for substrings in lines are provided,
37+
// they keep track of the offset, ensuring ordering when
38+
// when searching.
39+
type LogFile struct {
40+
*os.File
41+
offset int64
42+
}
43+
44+
// NewLogFile returns a new LogFile which wraps a os.File meant to be used
45+
// for testing. Methods to search and wait for strings to appear are provided.
46+
// dir and pattern are passed directly to os.CreateTemp.
47+
// It is the callers responsibility to remove the file. To keep the file in
48+
// when the test fails, use [TempDir] to create a folder.
49+
func NewLogFile(t testing.TB, dir, pattern string) *LogFile {
50+
f, err := os.CreateTemp(dir, pattern)
51+
if err != nil {
52+
t.Fatalf("cannot create log file: %s", err)
53+
}
54+
55+
lf := &LogFile{
56+
File: f,
57+
}
58+
59+
t.Cleanup(func() {
60+
if err := f.Sync(); err != nil {
61+
t.Logf("cannot sync log file: %s", err)
62+
}
63+
64+
if err := f.Close(); err != nil {
65+
t.Logf("cannot close log file: %s", err)
66+
}
67+
})
68+
69+
return lf
70+
}
71+
72+
// WaitLogsContains waits for the specified string s to be present in the logs within
73+
// the given timeout duration and fails the test if s is not found.
74+
// It keeps track of the log file offset, reading only new lines. Each
75+
// subsequent call to WaitLogsContains will only check logs not yet evaluated.
76+
// msgAndArgs should be a format string and arguments that will be printed
77+
// if the logs are not found, providing additional context for debugging.
78+
func (l *LogFile) WaitLogsContains(t testing.TB, s string, timeout time.Duration, msgAndArgs ...any) {
79+
t.Helper()
80+
require.EventuallyWithT(
81+
t,
82+
func(c *assert.CollectT) {
83+
found, err := l.FindInLogs(s)
84+
if err != nil {
85+
c.Errorf("cannot check the log file: %s", err)
86+
return
87+
}
88+
89+
if !found {
90+
c.Errorf("did not find '%s' in the logs", s)
91+
}
92+
},
93+
timeout,
94+
100*time.Millisecond,
95+
msgAndArgs...)
96+
}
97+
98+
// LogContains searches for str in the log file keeping track of the
99+
// offset. If there is any issue reading the log file, then t.Fatalf is called,
100+
// if str is not present in the logs, t.Errorf is called.
101+
func (l *LogFile) LogContains(t testing.TB, str string) {
102+
t.Helper()
103+
found, err := l.FindInLogs(str)
104+
if err != nil {
105+
t.Fatalf("cannot read log file: %s", err)
106+
}
107+
108+
if !found {
109+
t.Errorf("'%s' not found in logs", str)
110+
}
111+
}
112+
113+
// FindInLogs searches for str in the log file keeping track of the offset.
114+
// It returns true if str is found in the logs. If there are any errors,
115+
// it returns false and the error
116+
func (l *LogFile) FindInLogs(str string) (bool, error) {
117+
// Open the file again so we can seek and not interfere with
118+
// the logger writing to it.
119+
f, err := os.Open(l.Name())
120+
if err != nil {
121+
return false, fmt.Errorf("cannot open log file for reading: %w", err)
122+
}
123+
124+
if _, err := f.Seek(l.offset, io.SeekStart); err != nil {
125+
return false, fmt.Errorf("cannot seek log file: %w", err)
126+
}
127+
128+
r := bufio.NewReader(f)
129+
for {
130+
data, err := r.ReadBytes('\n')
131+
line := string(data)
132+
l.offset += int64(len(data))
133+
134+
if err != nil {
135+
if !errors.Is(err, io.EOF) {
136+
return false, fmt.Errorf("error reading log file '%s': %w", l.Name(), err)
137+
}
138+
break
139+
}
140+
141+
if strings.Contains(line, str) {
142+
return true, nil
143+
}
144+
}
145+
146+
return false, nil
147+
}
148+
149+
// ResetOffset resets the log file offset
150+
func (l *LogFile) ResetOffset() {
151+
l.offset = 0
152+
}

0 commit comments

Comments
 (0)