libbeat/testing/integration/integration.go (205 lines of code) (raw):
// 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 integration
import (
"context"
"encoding/json"
"errors"
"fmt"
"os/exec"
"regexp"
"sync"
"testing"
)
const (
expectErrMsg = "cannot set expectations once the test started"
)
// BeatTest describes all operations involved
// in integration testing of a Beat
type BeatTest interface {
// Start the integration test
//
// The test runs until all the expectations are met (unless `ExpectStop` is used) or context was canceled or the Beat exits on its own.
Start(context.Context) BeatTest
// Wait until the test is over.
//
// `PrintOutput` might be helpful for debugging after calling this function.
Wait()
// ExpectStart sets an expectation that the Beat will report that it started.
ExpectStart() BeatTest
// ExpectStop sets an expectation that the Beat will exit by itself.
// The process exit code will be checked against the given value.
//
// User controls the timeout by passing the context in `Start`.
//
// All the output expectations would still work as usual, however,
// satisfying all expectations would not stop the Beat.
ExpectStop(exitCode int) BeatTest
// ExpectOutput registers an output watch for the given substrings.
//
// Every future output line produced by the Beat will be checked
// if it contains one of the given strings.
//
// If given multiple strings, they get checked in order:
// The first substring must be found first, then second, etc.
//
// For `AND` behavior use this function multiple times.
//
// This function should be used before `Start` because it's
// inspecting only the new output lines.
ExpectOutput(...string) BeatTest
// ExpectOutputRegex registers an output watch for the given regular expression..
//
// Every future output line produced by the Beat will be matched
// against the given regular expression.
//
// If given multiple expressions, they get checked in order.
// The first expression must match first, then second, etc.
//
// For `AND` behavior use this function multiple times.
//
// This function should be used before `Start` because it's
// inspecting only new outputs.
ExpectOutputRegex(...*regexp.Regexp) BeatTest
// PrintOutput prints last `limit` lines of the output
//
// It might be handy for inspecting the output in case of a failure.
// Use `limit=-1` to print the entire output (strongly discouraged).
//
// JSON lines of the output are formatted.
PrintOutput(lineCount int)
// PrintExpectations prints all currently set expectations
PrintExpectations()
// WithReportOptions sets the reporting options for the test.
WithReportOptions(ReportOptions) BeatTest
}
// ReportOptions describes all reporting options
type ReportOptions struct {
// PrintExpectationsBeforeStart if set to `true`, all the defined
// expectations will be printed before the test starts.
//
// Use it only if you have a manageable amount of expectations that
// would be readable in the output.
PrintExpectationsBeforeStart bool
// PrintLinesOnFail defines how many lines of the Beat output
// the test should print in case of failure (default 0).
//
// It uses `PrintOutput`, see its documentation for details.
PrintLinesOnFail int
// PrintConfig defines if the test prints out the entire configuration file
// in case of failure.
PrintConfigOnFail bool
}
// BeatTestOptions describes all options to run the test
type BeatTestOptions = RunBeatOptions
// NewBeatTest creates a new integration test for a Beat.
func NewBeatTest(t *testing.T, opts BeatTestOptions) BeatTest {
test := &beatTest{
t: t,
opts: opts,
}
return test
}
type beatTest struct {
t *testing.T
opts BeatTestOptions
reportOpts ReportOptions
expectations []OutputWatcher
expectedExitCode *int
beat *RunningBeat
mtx sync.Mutex
}
// Start implements the BeatTest interface.
func (b *beatTest) Start(ctx context.Context) BeatTest {
b.mtx.Lock()
defer b.mtx.Unlock()
if b.beat != nil {
b.t.Fatal("test cannot be startd multiple times")
return b
}
watcher := NewOverallWatcher(b.expectations)
b.t.Logf("running %s integration test...", b.opts.Beatname)
if b.reportOpts.PrintExpectationsBeforeStart {
b.printExpectations()
}
b.beat = RunBeat(ctx, b.t, b.opts, watcher)
return b
}
// Wait implements the BeatTest interface.
func (b *beatTest) Wait() {
b.mtx.Lock()
defer b.mtx.Unlock()
if b.beat == nil {
b.t.Fatal("test must start first before calling wait on it")
return
}
err := b.beat.Wait()
exitErr := &exec.ExitError{}
if !errors.As(err, &exitErr) {
b.t.Fatalf("unexpected error when stopping %s: %s", b.opts.Beatname, err)
return
}
exitCode := 0
if err != nil {
exitCode = exitErr.ExitCode()
}
b.t.Logf("%s stopped, exit code %d", b.opts.Beatname, exitCode)
if b.expectedExitCode != nil && exitCode != *b.expectedExitCode {
b.t.Cleanup(func() {
b.t.Logf("expected exit code %d, actual %d", *b.expectedExitCode, exitCode)
})
b.t.Fail()
}
if b.beat.watcher != nil {
b.t.Cleanup(func() {
b.t.Logf("\n\nExpectations are not met:\n\n%s\n\n", b.beat.watcher.String())
if b.reportOpts.PrintLinesOnFail != 0 {
b.PrintOutput(b.reportOpts.PrintLinesOnFail)
}
if b.reportOpts.PrintConfigOnFail {
b.PrintConfig()
}
})
b.t.Fail()
}
}
// ExpectOutput implements the BeatTest interface.
func (b *beatTest) ExpectOutput(lines ...string) BeatTest {
b.mtx.Lock()
defer b.mtx.Unlock()
if b.beat != nil {
b.t.Fatal(expectErrMsg)
return b
}
if len(lines) == 0 {
return b
}
if len(lines) == 1 {
l := escapeJSONCharacters(lines[0])
b.expectations = append(b.expectations, NewStringWatcher(l))
return b
}
watchers := make([]OutputWatcher, 0, len(lines))
for _, l := range lines {
escaped := escapeJSONCharacters(l)
watchers = append(watchers, NewStringWatcher(escaped))
}
b.expectations = append(b.expectations, NewInOrderWatcher(watchers))
return b
}
// ExpectOutputRegex implements the BeatTest interface.
func (b *beatTest) ExpectOutputRegex(exprs ...*regexp.Regexp) BeatTest {
b.mtx.Lock()
defer b.mtx.Unlock()
if b.beat != nil {
b.t.Fatal(expectErrMsg)
return b
}
if len(exprs) == 0 {
return b
}
if len(exprs) == 1 {
b.expectations = append(b.expectations, NewRegexpWatcher(exprs[0]))
return b
}
watchers := make([]OutputWatcher, 0, len(exprs))
for _, e := range exprs {
watchers = append(watchers, NewRegexpWatcher(e))
}
b.expectations = append(b.expectations, NewInOrderWatcher(watchers))
return b
}
// ExpectStart implements the BeatTest interface.
func (b *beatTest) ExpectStart() BeatTest {
b.mtx.Lock()
defer b.mtx.Unlock()
if b.beat != nil {
b.t.Fatal(expectErrMsg)
return b
}
expectedLine := fmt.Sprintf("%s start running.", b.opts.Beatname)
b.expectations = append(b.expectations, NewStringWatcher(expectedLine))
return b
}
// ExpectStop implements the BeatTest interface.
func (b *beatTest) ExpectStop(exitCode int) BeatTest {
b.mtx.Lock()
defer b.mtx.Unlock()
if b.beat != nil {
b.t.Fatal(expectErrMsg)
return b
}
b.opts.KeepRunning = true
b.expectedExitCode = &exitCode
return b
}
// PrintOutput implements the BeatTest interface.
func (b *beatTest) PrintOutput(lineCount int) {
b.mtx.Lock()
defer b.mtx.Unlock()
if b.beat == nil {
return
}
b.t.Logf("\n\nLast %d lines of the output:\n\n%s\n\n", lineCount, b.beat.CollectOutput(lineCount))
}
// PrintConfig prints the entire configuration file the Beat test ran with
func (b *beatTest) PrintConfig() {
b.mtx.Lock()
defer b.mtx.Unlock()
if b.beat == nil {
return
}
b.t.Logf("\n\nConfig file %s ran with:\n\n%s\n\n", b.opts.Beatname, b.opts.Config)
}
// WithReportOptions implements the BeatTest interface.
func (b *beatTest) WithReportOptions(opts ReportOptions) BeatTest {
b.mtx.Lock()
defer b.mtx.Unlock()
b.reportOpts = opts
return b
}
// PrintExpectations implements the BeatTest interface.
func (b *beatTest) PrintExpectations() {
b.mtx.Lock()
defer b.mtx.Unlock()
b.printExpectations()
}
// lock-free, so it can be used inside a lock
func (b *beatTest) printExpectations() {
overall := NewOverallWatcher(b.expectations)
b.t.Logf("set expectations:\n%s", overall)
if b.expectedExitCode != nil {
b.t.Logf("\nprocess is expected to exit with code %d\n\n", *b.expectedExitCode)
} else {
b.t.Log("\nprocess is expected to be killed once expectations are met\n\n")
}
}
// we know that we're going to inpect the JSON output from the Beat
// so we must take care of the escaped characters,
// e.g. backslashes in paths on Windows.
func escapeJSONCharacters(s string) string {
bytes, _ := json.Marshal(s)
// trimming quote marks
return string(bytes[1 : len(bytes)-1])
}