diff --git a/cmd/influxd/inspect/dump_wal/dump_wal.go b/cmd/influxd/inspect/dump_wal/dump_wal.go index a75556d5d2..cc2bce0213 100644 --- a/cmd/influxd/inspect/dump_wal/dump_wal.go +++ b/cmd/influxd/inspect/dump_wal/dump_wal.go @@ -33,7 +33,7 @@ It has two modes of operation, depending on the --find-duplicates flag. `, Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return dumpWAL.run(args) + return dumpWAL.run(cmd, args) }, } @@ -43,20 +43,20 @@ It has two modes of operation, depending on the --find-duplicates flag. return cmd } -func (dumpWAL *dumpWALCommand) run(args []string) error { +func (dumpWAL *dumpWALCommand) run(cmd *cobra.Command, args []string) error { // Process each WAL file. for _, path := range args { - if err := dumpWAL.processWALFile(path); err != nil { + if err := dumpWAL.processWALFile(cmd, path); err != nil { return err } } return nil } -func (dumpWAL *dumpWALCommand) processWALFile(path string) error { +func (dumpWAL *dumpWALCommand) processWALFile(cmd *cobra.Command, path string) error { if filepath.Ext(path) != "."+tsm1.WALFileExtension { - fmt.Fprintf(os.Stderr, "invalid wal file path, skipping %s\n", path) + cmd.Printf("invalid wal file path, skipping %s\n", path) return nil } @@ -83,7 +83,7 @@ func (dumpWAL *dumpWALCommand) processWALFile(path string) error { switch entry := entry.(type) { case *tsm1.WriteWALEntry: if !dumpWAL.findDuplicates { - fmt.Printf("[write] sz=%d\n", entry.MarshalSize()) + cmd.Printf("[write] sz=%d\n", entry.MarshalSize()) } keys := make([]string, 0, len(entry.Values)) @@ -109,31 +109,31 @@ func (dumpWAL *dumpWALCommand) processWALFile(path string) error { switch v := v.(type) { case tsm1.IntegerValue: - fmt.Printf("%s %vi %d\n", k, v.Value(), t) + cmd.Printf("%s %vi %d\n", k, v.Value(), t) case tsm1.UnsignedValue: - fmt.Printf("%s %vu %d\n", k, v.Value(), t) + cmd.Printf("%s %vu %d\n", k, v.Value(), t) case tsm1.FloatValue: - fmt.Printf("%s %v %d\n", k, v.Value(), t) + cmd.Printf("%s %v %d\n", k, v.Value(), t) case tsm1.BooleanValue: - fmt.Printf("%s %v %d\n", k, v.Value(), t) + cmd.Printf("%s %v %d\n", k, v.Value(), t) case tsm1.StringValue: - fmt.Printf("%s %q %d\n", k, v.Value(), t) + cmd.Printf("%s %q %d\n", k, v.Value(), t) default: - fmt.Printf("%s EMPTY\n", k) + cmd.Printf("%s EMPTY\n", k) } } } case *tsm1.DeleteWALEntry: - fmt.Printf("[delete] sz=%d\n", entry.MarshalSize()) + cmd.Printf("[delete] sz=%d\n", entry.MarshalSize()) for _, k := range entry.Keys { - fmt.Printf("%s\n", string(k)) + cmd.Printf("%s\n", string(k)) } case *tsm1.DeleteRangeWALEntry: - fmt.Printf("[delete-range] min=%d max=%d sz=%d\n", entry.Min, entry.Max, entry.MarshalSize()) + cmd.Printf("[delete-range] min=%d max=%d sz=%d\n", entry.Min, entry.Max, entry.MarshalSize()) for _, k := range entry.Keys { - fmt.Printf("%s\n", string(k)) + cmd.Printf("%s\n", string(k)) } default: @@ -146,7 +146,7 @@ func (dumpWAL *dumpWALCommand) processWALFile(path string) error { keys := make([]string, 0, len(duplicateKeys)) if len(duplicateKeys) == 0 { - fmt.Println("No duplicates or out of order timestamps found") + cmd.Println("No duplicates or out of order timestamps found") return nil } @@ -156,7 +156,7 @@ func (dumpWAL *dumpWALCommand) processWALFile(path string) error { sort.Strings(keys) for _, k := range keys { - fmt.Println(k) + cmd.Println(k) } } diff --git a/cmd/influxd/inspect/dump_wal/dump_wal_test.go b/cmd/influxd/inspect/dump_wal/dump_wal_test.go new file mode 100644 index 0000000000..8a7758a60a --- /dev/null +++ b/cmd/influxd/inspect/dump_wal/dump_wal_test.go @@ -0,0 +1,221 @@ +package dump_wal + +import ( + "bytes" + "fmt" + "io" + "os" + "testing" + + "github.com/golang/snappy" + "github.com/influxdata/influxdb/v2/tsdb/engine/tsm1" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" +) + +func Test_DumpWal_No_Args(t *testing.T) { + params := cmdParams{ + walPaths: []string{}, + expectErr: true, + expectedOut: "requires at least 1 arg(s), only received 0", + } + + runCommand(t, params) +} + +func Test_DumpWal_Bad_Path(t *testing.T) { + params := cmdParams{ + findDuplicates: false, + walPaths: []string{"badpath.wal"}, + expectErr: true, + expectedOut: "open badpath.wal: no such file or directory", + } + + runCommand(t, params) +} + +func Test_DumpWal_Wrong_File_Type(t *testing.T) { + // Creates a temporary .txt file (wrong extension) + dir, file := newTempWal(t, false, false) + defer os.RemoveAll(dir) + + params := cmdParams{ + walPaths: []string{file}, + expectedOut: fmt.Sprintf("invalid wal file path, skipping %s", file), + expectErr: false, + } + runCommand(t, params) +} + +func Test_DumpWal_File_Valid(t *testing.T) { + dir, file := newTempWal(t, true, false) + defer os.RemoveAll(dir) + + params := cmdParams{ + walPaths: []string{file}, + expectedOuts: []string{ + "[write]", + "cpu,host=A#!~#float 1.1 1", + "cpu,host=A#!~#int 1i 1", + "cpu,host=A#!~#bool true 1", + "cpu,host=A#!~#string \"string\" 1", + "cpu,host=A#!~#unsigned 10u 5", + }, + } + + runCommand(t, params) +} + +func Test_DumpWal_Find_Duplicates_None(t *testing.T) { + dir, file := newTempWal(t, true, false) + defer os.RemoveAll(dir) + + params := cmdParams{ + findDuplicates: true, + walPaths: []string{file}, + expectedOut: "No duplicates or out of order timestamps found", + } + + runCommand(t, params) +} + +func Test_DumpWal_Find_Duplicates_Present(t *testing.T) { + dir, file := newTempWal(t, true, true) + defer os.RemoveAll(dir) + + params := cmdParams{ + findDuplicates: true, + walPaths: []string{file}, + expectedOut: "cpu,host=A#!~#unsigned", + } + + runCommand(t, params) +} + +func newTempWal(t *testing.T, validExt bool, withDuplicate bool) (string, string) { + t.Helper() + + dir, err := os.MkdirTemp("", "dump-wal") + require.NoError(t, err) + var file *os.File + + if !validExt { + file, err := os.CreateTemp(dir, "dumpwaltest*.txt") + require.NoError(t, err) + return dir, file.Name() + } + + file, err = os.CreateTemp(dir, "dumpwaltest*"+"."+tsm1.WALFileExtension) + require.NoError(t, err) + + p1 := tsm1.NewValue(10, 1.1) + p2 := tsm1.NewValue(1, int64(1)) + p3 := tsm1.NewValue(1, true) + p4 := tsm1.NewValue(1, "string") + p5 := tsm1.NewValue(5, uint64(10)) + + values := map[string][]tsm1.Value{ + "cpu,host=A#!~#float": {p1}, + "cpu,host=A#!~#int": {p2}, + "cpu,host=A#!~#bool": {p3}, + "cpu,host=A#!~#string": {p4}, + "cpu,host=A#!~#unsigned": {p5}, + } + + if withDuplicate { + p6 := tsm1.NewValue(1, uint64(70)) + values = map[string][]tsm1.Value{ + "cpu,host=A#!~#unsigned": {p5, p6}, + } + } + + // Write to WAL File + writeWalFile(t, file, values) + + return dir, file.Name() +} + +func writeWalFile(t *testing.T, file *os.File, vals map[string][]tsm1.Value) { + t.Helper() + + e := &tsm1.WriteWALEntry{Values: vals} + b, err := e.Encode(nil) + require.NoError(t, err) + + w := tsm1.NewWALSegmentWriter(file) + err = w.Write(e.Type(), snappy.Encode(nil, b)) + require.NoError(t, err) + + err = w.Flush() + require.NoError(t, err) + + err = file.Sync() + require.NoError(t, err) +} + +type cmdParams struct { + findDuplicates bool + walPaths []string + expectedOut string + expectedOuts []string + expectErr bool + expectExactEqual bool +} + +func initCommand(t *testing.T, params cmdParams) *cobra.Command { + t.Helper() + + // Creates new command and sets args + cmd := NewDumpWALCommand() + + allArgs := params.walPaths + if params.findDuplicates { + allArgs = append(allArgs, "--find-duplicates") + } + + cmd.SetArgs(allArgs) + + return cmd +} + +func getOutput(t *testing.T, cmd *cobra.Command) []byte { + t.Helper() + + b := bytes.NewBufferString("") + cmd.SetOut(b) + cmd.SetErr(b) + require.NoError(t, cmd.Execute()) + + out, err := io.ReadAll(b) + require.NoError(t, err) + + return out +} + +func runCommand(t *testing.T, params cmdParams) { + t.Helper() + + cmd := initCommand(t, params) + + if params.expectErr { + require.EqualError(t, cmd.Execute(), params.expectedOut) + return + } + + // Get output + out := getOutput(t, cmd) + + // Check output + if params.expectExactEqual { + require.Equal(t, string(out), params.expectedOut) + return + } + + if params.expectedOut != "" { + require.Contains(t, string(out), params.expectedOut) + } else { + for _, output := range params.expectedOuts { + require.Contains(t, string(out), output) + } + } +}