Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions testing/fs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package testing

import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"unicode"
"unicode/utf8"
)

// TempDir creates a temporary directory that will be
// removed if the tests passes. The temporary directory is
// created by joining all elements from path, with the sanitised
// test name.
//
// If path is empty, the temporary directory is created in os.TempDir.
//
// When tests are run with -v, the temporary directory absolute
// path will be logged.
func TempDir(t *testing.T, path ...string) string {
rootDir := filepath.Join(path...)

if rootDir == "" {
rootDir = os.TempDir()
}

rootDir, err := filepath.Abs(rootDir)
if err != nil {
t.Fatalf("cannot get absolute path: %s", err)
}

// Logic copied with small modifications from
// the Go source code: testing/testing.go
folderName := t.Name()
mapper := func(r rune) rune {
if r < utf8.RuneSelf {
const allowed = "_-"
if '0' <= r && r <= '9' ||
'a' <= r && r <= 'z' ||
'A' <= r && r <= 'Z' {
return r
}
if strings.ContainsRune(allowed, r) {
return r
}
} else if unicode.IsLetter(r) || unicode.IsNumber(r) {
return r
}
return -1
}
folderName = strings.Map(mapper, folderName)

if err := os.MkdirAll(rootDir, 0o750); err != nil {
t.Fatalf("error making test dir: %s: %s", rootDir, err)
}

tempDir, err := os.MkdirTemp(rootDir, folderName)
if err != nil {
t.Fatalf("failed to make temp directory: %s", err)
}

cleanup := func() {
if !t.Failed() {
if err := os.RemoveAll(tempDir); err != nil {
// Ungly workaround Windows limitations
// Windows does not support the Interrup signal, so it might
// happen that Filebeat is still running, keeping it's registry
// file open, thus preventing the temporary folder from being
// removed. So we log the error and move on without failing the
// test
if runtime.GOOS == "windows" {
t.Logf("[WARN] Could not remove temporatry directory '%s': %s", tempDir, err)
} else {
t.Errorf("could not remove temp dir '%s': %s", tempDir, err)
}
}
} else {
t.Logf("Temporary directory saved: %s", tempDir)
}
}
t.Cleanup(cleanup)

return tempDir
}
152 changes: 152 additions & 0 deletions testing/logfile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package testing

import (
"bufio"
"errors"
"fmt"
"io"
"os"
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// LogFile wraps a *os.File and makes it more suitable for tests.
// Key feature:
// - Methods to search and wait for substrings in lines are provided,
// they keep track of the offset, ensuring ordering when
// when searching.
type LogFile struct {
*os.File
offset int64
}

// NewLogFile returns a new LogFile which wraps a os.File meant to be used
// for testing. Methods to search and wait for strings to appear are provided.
// dir and pattern are passed directly to os.CreateTemp.
// It is the callers responsibility to remove the file. To keep the file in
// when the test fails, use [TempDir] to create a folder.
func NewLogFile(t testing.TB, dir, pattern string) *LogFile {
f, err := os.CreateTemp(dir, pattern)
if err != nil {
t.Fatalf("cannot create log file: %s", err)
}

lf := &LogFile{
File: f,
}

t.Cleanup(func() {
if err := f.Sync(); err != nil {
t.Logf("cannot sync log file: %s", err)
}

if err := f.Close(); err != nil {
t.Logf("cannot close log file: %s", err)
}
})

return lf
}

// WaitLogsContains waits for the specified string s to be present in the logs within
// the given timeout duration and fails the test if s is not found.
// It keeps track of the log file offset, reading only new lines. Each
// subsequent call to WaitLogsContains will only check logs not yet evaluated.
// msgAndArgs should be a format string and arguments that will be printed
// if the logs are not found, providing additional context for debugging.
func (l *LogFile) WaitLogsContains(t testing.TB, s string, timeout time.Duration, msgAndArgs ...any) {
t.Helper()
require.EventuallyWithT(
t,
func(c *assert.CollectT) {
found, err := l.FindInLogs(s)
if err != nil {
c.Errorf("cannot check the log file: %s", err)
return
}

if !found {
c.Errorf("did not find '%s' in the logs", s)
}
},
timeout,
100*time.Millisecond,
msgAndArgs...)
}

// LogContains searches for str in the log file keeping track of the
// offset. If there is any issue reading the log file, then t.Fatalf is called,
// if str is not present in the logs, t.Errorf is called.
func (l *LogFile) LogContains(t testing.TB, str string) {
t.Helper()
found, err := l.FindInLogs(str)
if err != nil {
t.Fatalf("cannot read log file: %s", err)
}

if !found {
t.Errorf("'%s' not found in logs", str)
}
}

// FindInLogs searches for str in the log file keeping track of the offset.
// It returns true if str is found in the logs. If there are any errors,
// it returns false and the error
func (l *LogFile) FindInLogs(str string) (bool, error) {
// Open the file again so we can seek and not interfere with
// the logger writing to it.
f, err := os.Open(l.Name())
if err != nil {
return false, fmt.Errorf("cannot open log file for reading: %w", err)
}

if _, err := f.Seek(l.offset, io.SeekStart); err != nil {
return false, fmt.Errorf("cannot seek log file: %w", err)
}

r := bufio.NewReader(f)
for {
data, err := r.ReadBytes('\n')
line := string(data)
l.offset += int64(len(data))

if err != nil {
if !errors.Is(err, io.EOF) {
return false, fmt.Errorf("error reading log file '%s': %w", l.Name(), err)
}
break
}

if strings.Contains(line, str) {
return true, nil
}
}

return false, nil
}

// ResetOffset resets the log file offset
func (l *LogFile) ResetOffset() {
l.offset = 0
}
Loading