2024-10-30 14:22:06 +00:00
|
|
|
package apptest
|
|
|
|
|
|
|
|
import (
|
2024-11-20 15:30:55 +00:00
|
|
|
"fmt"
|
2025-02-05 16:10:11 +00:00
|
|
|
"os"
|
|
|
|
"path"
|
2024-10-30 14:22:06 +00:00
|
|
|
"testing"
|
2024-11-20 15:30:55 +00:00
|
|
|
"time"
|
2024-10-30 14:22:06 +00:00
|
|
|
|
|
|
|
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
|
2024-11-20 15:30:55 +00:00
|
|
|
"github.com/google/go-cmp/cmp"
|
2024-10-30 14:22:06 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// TestCase holds the state and defines clean-up procedure common for all test
|
|
|
|
// cases.
|
|
|
|
type TestCase struct {
|
|
|
|
t *testing.T
|
|
|
|
cli *Client
|
2024-11-08 13:49:00 +00:00
|
|
|
|
2024-12-03 11:25:53 +00:00
|
|
|
startedApps map[string]Stopper
|
2024-11-08 13:49:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Stopper is an interface of objects that needs to be stopped via Stop() call
|
|
|
|
type Stopper interface {
|
|
|
|
Stop()
|
2024-10-30 14:22:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewTestCase creates a new test case.
|
|
|
|
func NewTestCase(t *testing.T) *TestCase {
|
2024-12-03 11:25:53 +00:00
|
|
|
return &TestCase{t, NewClient(), make(map[string]Stopper)}
|
|
|
|
}
|
|
|
|
|
|
|
|
// T returns the test state.
|
|
|
|
func (tc *TestCase) T() *testing.T {
|
|
|
|
return tc.t
|
2024-10-30 14:22:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Dir returns the directory name that should be used by as the -storageDataDir.
|
|
|
|
func (tc *TestCase) Dir() string {
|
|
|
|
return tc.t.Name()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Client returns an instance of the client that can be used for interacting with
|
|
|
|
// the app(s) under test.
|
|
|
|
func (tc *TestCase) Client() *Client {
|
|
|
|
return tc.cli
|
|
|
|
}
|
|
|
|
|
2024-11-08 13:49:00 +00:00
|
|
|
// Stop performs the test case clean up, such as closing all client connections
|
2024-10-30 14:22:06 +00:00
|
|
|
// and removing the -storageDataDir directory.
|
|
|
|
//
|
|
|
|
// Note that the -storageDataDir is not removed in case of test case failure to
|
2024-11-08 13:49:00 +00:00
|
|
|
// allow for further manual debugging.
|
|
|
|
func (tc *TestCase) Stop() {
|
2024-10-30 14:22:06 +00:00
|
|
|
tc.cli.CloseConnections()
|
2024-11-08 13:49:00 +00:00
|
|
|
for _, app := range tc.startedApps {
|
|
|
|
app.Stop()
|
|
|
|
}
|
2024-10-30 14:22:06 +00:00
|
|
|
if !tc.t.Failed() {
|
|
|
|
fs.MustRemoveAll(tc.Dir())
|
|
|
|
}
|
|
|
|
}
|
2024-11-08 13:49:00 +00:00
|
|
|
|
2024-11-21 18:39:17 +00:00
|
|
|
// MustStartDefaultVmsingle is a test helper function that starts an instance of
|
|
|
|
// vmsingle with defaults suitable for most tests.
|
|
|
|
func (tc *TestCase) MustStartDefaultVmsingle() *Vmsingle {
|
|
|
|
tc.t.Helper()
|
|
|
|
|
|
|
|
return tc.MustStartVmsingle("vmsingle", []string{
|
|
|
|
"-storageDataPath=" + tc.Dir() + "/vmsingle",
|
|
|
|
"-retentionPeriod=100y",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-11-08 13:49:00 +00:00
|
|
|
// MustStartVmsingle is a test helper function that starts an instance of
|
|
|
|
// vmsingle and fails the test if the app fails to start.
|
|
|
|
func (tc *TestCase) MustStartVmsingle(instance string, flags []string) *Vmsingle {
|
|
|
|
tc.t.Helper()
|
|
|
|
|
|
|
|
app, err := StartVmsingle(instance, flags, tc.cli)
|
|
|
|
if err != nil {
|
|
|
|
tc.t.Fatalf("Could not start %s: %v", instance, err)
|
|
|
|
}
|
2024-12-03 11:25:53 +00:00
|
|
|
tc.addApp(instance, app)
|
2024-11-08 13:49:00 +00:00
|
|
|
return app
|
|
|
|
}
|
|
|
|
|
|
|
|
// MustStartVmstorage is a test helper function that starts an instance of
|
|
|
|
// vmstorage and fails the test if the app fails to start.
|
|
|
|
func (tc *TestCase) MustStartVmstorage(instance string, flags []string) *Vmstorage {
|
|
|
|
tc.t.Helper()
|
|
|
|
|
|
|
|
app, err := StartVmstorage(instance, flags, tc.cli)
|
|
|
|
if err != nil {
|
|
|
|
tc.t.Fatalf("Could not start %s: %v", instance, err)
|
|
|
|
}
|
2024-12-03 11:25:53 +00:00
|
|
|
tc.addApp(instance, app)
|
2024-11-08 13:49:00 +00:00
|
|
|
return app
|
|
|
|
}
|
|
|
|
|
|
|
|
// MustStartVmselect is a test helper function that starts an instance of
|
|
|
|
// vmselect and fails the test if the app fails to start.
|
|
|
|
func (tc *TestCase) MustStartVmselect(instance string, flags []string) *Vmselect {
|
|
|
|
tc.t.Helper()
|
|
|
|
|
|
|
|
app, err := StartVmselect(instance, flags, tc.cli)
|
|
|
|
if err != nil {
|
|
|
|
tc.t.Fatalf("Could not start %s: %v", instance, err)
|
|
|
|
}
|
2024-12-03 11:25:53 +00:00
|
|
|
tc.addApp(instance, app)
|
2024-11-08 13:49:00 +00:00
|
|
|
return app
|
|
|
|
}
|
|
|
|
|
|
|
|
// MustStartVminsert is a test helper function that starts an instance of
|
|
|
|
// vminsert and fails the test if the app fails to start.
|
|
|
|
func (tc *TestCase) MustStartVminsert(instance string, flags []string) *Vminsert {
|
|
|
|
tc.t.Helper()
|
|
|
|
|
|
|
|
app, err := StartVminsert(instance, flags, tc.cli)
|
|
|
|
if err != nil {
|
|
|
|
tc.t.Fatalf("Could not start %s: %v", instance, err)
|
|
|
|
}
|
2024-12-03 11:25:53 +00:00
|
|
|
tc.addApp(instance, app)
|
2024-11-08 13:49:00 +00:00
|
|
|
return app
|
|
|
|
}
|
|
|
|
|
2025-02-17 12:02:33 +00:00
|
|
|
// Vmcluster represents a typical cluster setup: several vmstorage replicas, one
|
|
|
|
// vminsert, and one vmselect.
|
|
|
|
//
|
|
|
|
// Both Vmsingle and Vmcluster implement the PrometheusWriteQuerier used in
|
|
|
|
// business logic tests to abstract out the infrasture.
|
|
|
|
//
|
|
|
|
// This type is not suitable for infrastructure tests where custom cluster
|
|
|
|
// setups are often required.
|
|
|
|
type Vmcluster struct {
|
2024-11-20 15:30:55 +00:00
|
|
|
*Vminsert
|
|
|
|
*Vmselect
|
2025-02-17 12:02:33 +00:00
|
|
|
Vmstorages []*Vmstorage
|
2024-11-20 15:30:55 +00:00
|
|
|
}
|
|
|
|
|
2025-02-17 12:02:33 +00:00
|
|
|
// ForceFlush forces the ingested data to become visible for searching
|
|
|
|
// immediately.
|
|
|
|
func (c *Vmcluster) ForceFlush(t *testing.T) {
|
|
|
|
for _, s := range c.Vmstorages {
|
2024-11-20 15:30:55 +00:00
|
|
|
s.ForceFlush(t)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-02-05 16:10:11 +00:00
|
|
|
// MustStartVmauth is a test helper function that starts an instance of
|
|
|
|
// vmauth and fails the test if the app fails to start.
|
|
|
|
func (tc *TestCase) MustStartVmauth(instance string, flags []string, configFileYAML string) *Vmauth {
|
|
|
|
tc.t.Helper()
|
|
|
|
|
|
|
|
configFilePath := path.Join(tc.t.TempDir(), "config.yaml")
|
|
|
|
if err := os.WriteFile(configFilePath, []byte(configFileYAML), os.ModePerm); err != nil {
|
|
|
|
tc.t.Fatalf("cannot init vmauth: config file write failed: %s", err)
|
|
|
|
}
|
|
|
|
app, err := StartVmauth(instance, flags, tc.cli, configFilePath)
|
|
|
|
if err != nil {
|
|
|
|
tc.t.Fatalf("Could not start %s: %v", instance, err)
|
|
|
|
}
|
|
|
|
tc.addApp(instance, app)
|
|
|
|
return app
|
|
|
|
}
|
|
|
|
|
2024-12-18 21:40:44 +00:00
|
|
|
// MustStartDefaultCluster starts a typical cluster configuration with default
|
|
|
|
// flags.
|
|
|
|
func (tc *TestCase) MustStartDefaultCluster() PrometheusWriteQuerier {
|
|
|
|
tc.t.Helper()
|
|
|
|
|
|
|
|
return tc.MustStartCluster(&ClusterOptions{
|
|
|
|
Vmstorage1Instance: "vmstorage1",
|
|
|
|
Vmstorage2Instance: "vmstorage2",
|
|
|
|
VminsertInstance: "vminsert",
|
|
|
|
VmselectInstance: "vmselect",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// ClusterOptions holds the params for simple cluster configuration suitable for
|
|
|
|
// most tests.
|
2024-11-20 15:30:55 +00:00
|
|
|
//
|
|
|
|
// The cluster consists of two vmstorages, one vminsert and one vmselect, no
|
|
|
|
// data replication.
|
|
|
|
//
|
|
|
|
// Such configuration is suitable for tests that don't verify the
|
|
|
|
// cluster-specific behavior (such as sharding, replication, or multilevel
|
|
|
|
// vmselect) but instead just need a typical cluster configuration to verify
|
|
|
|
// some business logic (such as API surface, or MetricsQL). Such cluster
|
|
|
|
// tests usually come paired with corresponding vmsingle tests.
|
2024-12-18 21:40:44 +00:00
|
|
|
type ClusterOptions struct {
|
|
|
|
Vmstorage1Instance string
|
|
|
|
Vmstorage1Flags []string
|
|
|
|
Vmstorage2Instance string
|
|
|
|
Vmstorage2Flags []string
|
|
|
|
VminsertInstance string
|
|
|
|
VminsertFlags []string
|
|
|
|
VmselectInstance string
|
|
|
|
VmselectFlags []string
|
|
|
|
}
|
|
|
|
|
|
|
|
// MustStartCluster starts a typical cluster configuration with custom flags.
|
|
|
|
func (tc *TestCase) MustStartCluster(opts *ClusterOptions) PrometheusWriteQuerier {
|
2024-11-20 15:30:55 +00:00
|
|
|
tc.t.Helper()
|
|
|
|
|
2024-12-18 21:40:44 +00:00
|
|
|
opts.Vmstorage1Flags = append(opts.Vmstorage1Flags, []string{
|
|
|
|
"-storageDataPath=" + tc.Dir() + "/" + opts.Vmstorage1Instance,
|
2024-11-20 15:30:55 +00:00
|
|
|
"-retentionPeriod=100y",
|
2024-12-18 21:40:44 +00:00
|
|
|
}...)
|
|
|
|
vmstorage1 := tc.MustStartVmstorage(opts.Vmstorage1Instance, opts.Vmstorage1Flags)
|
|
|
|
|
|
|
|
opts.Vmstorage2Flags = append(opts.Vmstorage2Flags, []string{
|
|
|
|
"-storageDataPath=" + tc.Dir() + "/" + opts.Vmstorage2Instance,
|
2024-11-20 15:30:55 +00:00
|
|
|
"-retentionPeriod=100y",
|
2024-12-18 21:40:44 +00:00
|
|
|
}...)
|
|
|
|
vmstorage2 := tc.MustStartVmstorage(opts.Vmstorage2Instance, opts.Vmstorage2Flags)
|
|
|
|
|
|
|
|
opts.VminsertFlags = append(opts.VminsertFlags, []string{
|
2024-11-20 15:30:55 +00:00
|
|
|
"-storageNode=" + vmstorage1.VminsertAddr() + "," + vmstorage2.VminsertAddr(),
|
2024-12-18 21:40:44 +00:00
|
|
|
}...)
|
|
|
|
vminsert := tc.MustStartVminsert(opts.VminsertInstance, opts.VminsertFlags)
|
|
|
|
|
|
|
|
opts.VmselectFlags = append(opts.VmselectFlags, []string{
|
2024-11-20 15:30:55 +00:00
|
|
|
"-storageNode=" + vmstorage1.VmselectAddr() + "," + vmstorage2.VmselectAddr(),
|
2024-12-18 21:40:44 +00:00
|
|
|
}...)
|
|
|
|
vmselect := tc.MustStartVmselect(opts.VmselectInstance, opts.VmselectFlags)
|
2024-11-20 15:30:55 +00:00
|
|
|
|
2025-02-17 12:02:33 +00:00
|
|
|
return &Vmcluster{vminsert, vmselect, []*Vmstorage{vmstorage1, vmstorage2}}
|
2024-11-20 15:30:55 +00:00
|
|
|
}
|
|
|
|
|
2024-12-03 11:25:53 +00:00
|
|
|
func (tc *TestCase) addApp(instance string, app Stopper) {
|
|
|
|
if _, alreadyStarted := tc.startedApps[instance]; alreadyStarted {
|
|
|
|
tc.t.Fatalf("%s has already been started", instance)
|
|
|
|
}
|
|
|
|
tc.startedApps[instance] = app
|
|
|
|
}
|
|
|
|
|
|
|
|
// StopApp stops the app identified by the `instance` name and removes it from
|
|
|
|
// the collection of started apps.
|
|
|
|
func (tc *TestCase) StopApp(instance string) {
|
|
|
|
if app, exists := tc.startedApps[instance]; exists {
|
|
|
|
app.Stop()
|
|
|
|
delete(tc.startedApps, instance)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-02-14 11:35:51 +00:00
|
|
|
// StopPrometheusWriteQuerier stop all apps that are a part of the pwq.
|
|
|
|
func (tc *TestCase) StopPrometheusWriteQuerier(pwq PrometheusWriteQuerier) {
|
|
|
|
tc.t.Helper()
|
|
|
|
switch t := pwq.(type) {
|
|
|
|
case *Vmsingle:
|
|
|
|
tc.StopApp(t.Name())
|
2025-02-17 12:02:33 +00:00
|
|
|
case *Vmcluster:
|
2025-02-14 11:35:51 +00:00
|
|
|
tc.StopApp(t.Vminsert.Name())
|
|
|
|
tc.StopApp(t.Vmselect.Name())
|
2025-02-17 12:02:33 +00:00
|
|
|
for _, vmstorage := range t.Vmstorages {
|
2025-02-14 11:35:51 +00:00
|
|
|
tc.StopApp(vmstorage.Name())
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
tc.t.Fatalf("Unsupported type: %v", t)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-03 11:25:53 +00:00
|
|
|
// ForceFlush flushes zero or more storages.
|
|
|
|
func (tc *TestCase) ForceFlush(apps ...*Vmstorage) {
|
|
|
|
tc.t.Helper()
|
|
|
|
|
|
|
|
for _, app := range apps {
|
|
|
|
app.ForceFlush(tc.t)
|
|
|
|
}
|
2024-11-08 13:49:00 +00:00
|
|
|
}
|
2024-11-20 15:30:55 +00:00
|
|
|
|
|
|
|
// AssertOptions hold the assertion params, such as got and wanted values as
|
|
|
|
// well as the message that should be included into the assertion error message
|
|
|
|
// in case of failure.
|
|
|
|
//
|
|
|
|
// In VictoriaMetrics (especially the cluster version) the inserted data does
|
|
|
|
// not become visible for querying right away. Therefore, the first comparisons
|
|
|
|
// may fail. AssertOptions allow to configure how many times the actual result
|
|
|
|
// must be retrieved and compared with the expected one and for long to wait
|
|
|
|
// between the retries. If these two params (`Retries` and `Period`) are not
|
|
|
|
// set, the default values will be used.
|
|
|
|
//
|
|
|
|
// If it is known that the data is available, then the retry functionality can
|
|
|
|
// be disabled by setting the `DoNotRetry` field.
|
|
|
|
//
|
|
|
|
// AssertOptions are used by the TestCase.Assert() method, and this method uses
|
|
|
|
// cmp.Diff() from go-cmp package for comparing got and wanted values.
|
|
|
|
// AssertOptions, therefore, allows to pass cmp.Options to cmp.Diff() via
|
|
|
|
// `CmpOpts` field.
|
|
|
|
//
|
|
|
|
// Finally the `FailNow` field controls whether the assertion should fail using
|
|
|
|
// `testing.T.Errorf()` or `testing.T.Fatalf()`.
|
|
|
|
type AssertOptions struct {
|
|
|
|
Msg string
|
|
|
|
Got func() any
|
|
|
|
Want any
|
|
|
|
CmpOpts []cmp.Option
|
|
|
|
DoNotRetry bool
|
|
|
|
Retries int
|
|
|
|
Period time.Duration
|
|
|
|
FailNow bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// Assert compares the actual result with the expected one possibly multiple
|
|
|
|
// times in order to account for the fact that the inserted data does not become
|
|
|
|
// available for querying right away (especially in cluster version of
|
|
|
|
// VictoriaMetrics).
|
|
|
|
func (tc *TestCase) Assert(opts *AssertOptions) {
|
|
|
|
tc.t.Helper()
|
|
|
|
|
|
|
|
const (
|
|
|
|
defaultRetries = 20
|
|
|
|
defaultPeriod = 100 * time.Millisecond
|
|
|
|
)
|
|
|
|
|
|
|
|
if opts.DoNotRetry {
|
|
|
|
opts.Retries = 1
|
|
|
|
opts.Period = 0
|
|
|
|
} else {
|
|
|
|
if opts.Retries <= 0 {
|
|
|
|
opts.Retries = defaultRetries
|
|
|
|
}
|
|
|
|
if opts.Period <= 0 {
|
|
|
|
opts.Period = defaultPeriod
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var diff string
|
|
|
|
|
|
|
|
for range opts.Retries {
|
|
|
|
diff = cmp.Diff(opts.Want, opts.Got(), opts.CmpOpts...)
|
|
|
|
if diff == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
time.Sleep(opts.Period)
|
|
|
|
}
|
|
|
|
|
|
|
|
msg := fmt.Sprintf("%s (-want, +got):\n%s", opts.Msg, diff)
|
|
|
|
|
|
|
|
if opts.FailNow {
|
|
|
|
tc.t.Fatal(msg)
|
|
|
|
} else {
|
|
|
|
tc.t.Error(msg)
|
|
|
|
}
|
|
|
|
}
|