// Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 package filestore import ( "bytes" "context" "errors" "fmt" "io/fs" "log" "reflect" "sync" "sync/atomic" "testing" "time" "github.com/google/uuid" "github.com/wavetermdev/waveterm/pkg/ijson" ) func initDb(t *testing.T) { t.Logf("initializing db for %q", t.Name()) useTestingDb = true partDataSize = 50 warningCount = &atomic.Int32{} stopFlush.Store(true) err := InitFilestore() if err != nil { t.Fatalf("error initializing filestore: %v", err) } } func cleanupDb(t *testing.T) { t.Logf("cleaning up db for %q", t.Name()) if globalDB != nil { globalDB.Close() globalDB = nil } useTestingDb = false partDataSize = DefaultPartDataSize WFS.clearCache() if warningCount.Load() > 0 { t.Errorf("warning count: %d", warningCount.Load()) } if flushErrorCount.Load() > 0 { t.Errorf("flush error count: %d", flushErrorCount.Load()) } } func (s *FileStore) getCacheSize() int { s.Lock.Lock() defer s.Lock.Unlock() return len(s.Cache) } func (s *FileStore) clearCache() { s.Lock.Lock() defer s.Lock.Unlock() s.Cache = make(map[cacheKey]*CacheEntry) } //lint:ignore U1000 used for testing func (s *FileStore) dump() string { s.Lock.Lock() defer s.Lock.Unlock() var buf bytes.Buffer buf.WriteString(fmt.Sprintf("FileStore %d entries\n", len(s.Cache))) for _, v := range s.Cache { entryStr := v.dump() buf.WriteString(entryStr) buf.WriteString("\n") } return buf.String() } func TestCreate(t *testing.T) { initDb(t) defer cleanupDb(t) ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() zoneId := uuid.NewString() err := WFS.MakeFile(ctx, zoneId, "testfile", nil, FileOptsType{}) if err != nil { t.Fatalf("error creating file: %v", err) } file, err := WFS.Stat(ctx, zoneId, "testfile") if err != nil { t.Fatalf("error stating file: %v", err) } if file == nil { t.Fatalf("file not found") } if file.ZoneId != zoneId { t.Fatalf("zone id mismatch") } if file.Name != "testfile" { t.Fatalf("name mismatch") } if file.Size != 0 { t.Fatalf("size mismatch") } if file.CreatedTs == 0 { t.Fatalf("created ts zero") } if file.ModTs == 0 { t.Fatalf("mod ts zero") } if file.CreatedTs != file.ModTs { t.Fatalf("create ts != mod ts") } if len(file.Meta) != 0 { t.Fatalf("meta should have no values") } if file.Opts.Circular || file.Opts.IJson || file.Opts.MaxSize != 0 { t.Fatalf("opts not empty") } zoneIds, err := WFS.GetAllZoneIds(ctx) if err != nil { t.Fatalf("error getting zone ids: %v", err) } if len(zoneIds) != 1 { t.Fatalf("zone id count mismatch") } if zoneIds[0] != zoneId { t.Fatalf("zone id mismatch") } err = WFS.DeleteFile(ctx, zoneId, "testfile") if err != nil { t.Fatalf("error deleting file: %v", err) } zoneIds, err = WFS.GetAllZoneIds(ctx) if err != nil { t.Fatalf("error getting zone ids: %v", err) } if len(zoneIds) != 0 { t.Fatalf("zone id count mismatch") } } func containsFile(arr []*WaveFile, name string) bool { for _, f := range arr { if f.Name == name { return true } } return false } func TestDelete(t *testing.T) { initDb(t) defer cleanupDb(t) ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() zoneId := uuid.NewString() err := WFS.MakeFile(ctx, zoneId, "testfile", nil, FileOptsType{}) if err != nil { t.Fatalf("error creating file: %v", err) } err = WFS.DeleteFile(ctx, zoneId, "testfile") if err != nil { t.Fatalf("error deleting file: %v", err) } _, err = WFS.Stat(ctx, zoneId, "testfile") if err == nil || !errors.Is(err, fs.ErrNotExist) { t.Errorf("expected file not found error") } // create two files in same zone, use DeleteZone to delete err = WFS.MakeFile(ctx, zoneId, "testfile1", nil, FileOptsType{}) if err != nil { t.Fatalf("error creating file: %v", err) } err = WFS.MakeFile(ctx, zoneId, "testfile2", nil, FileOptsType{}) if err != nil { t.Fatalf("error creating file: %v", err) } files, err := WFS.ListFiles(ctx, zoneId) if err != nil { t.Fatalf("error listing files: %v", err) } if len(files) != 2 { t.Fatalf("file count mismatch") } if !containsFile(files, "testfile1") || !containsFile(files, "testfile2") { t.Fatalf("file names mismatch") } err = WFS.DeleteZone(ctx, zoneId) if err != nil { t.Fatalf("error deleting zone: %v", err) } files, err = WFS.ListFiles(ctx, zoneId) if err != nil { t.Fatalf("error listing files: %v", err) } if len(files) != 0 { t.Fatalf("file count mismatch") } } func checkMapsEqual(t *testing.T, m1 map[string]any, m2 map[string]any, msg string) { if len(m1) != len(m2) { t.Errorf("%s: map length mismatch", msg) } for k, v := range m1 { if m2[k] != v { t.Errorf("%s: value mismatch for key %q", msg, k) } } } func TestSetMeta(t *testing.T) { initDb(t) defer cleanupDb(t) ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() zoneId := uuid.NewString() err := WFS.MakeFile(ctx, zoneId, "testfile", nil, FileOptsType{}) if err != nil { t.Fatalf("error creating file: %v", err) } if WFS.getCacheSize() != 0 { t.Errorf("cache size mismatch -- should have 0 entries after create") } err = WFS.WriteMeta(ctx, zoneId, "testfile", map[string]any{"a": 5, "b": "hello", "q": 8}, false) if err != nil { t.Fatalf("error setting meta: %v", err) } file, err := WFS.Stat(ctx, zoneId, "testfile") if err != nil { t.Fatalf("error stating file: %v", err) } if file == nil { t.Fatalf("file not found") } checkMapsEqual(t, map[string]any{"a": 5, "b": "hello", "q": 8}, file.Meta, "meta") if WFS.getCacheSize() != 1 { t.Errorf("cache size mismatch") } err = WFS.WriteMeta(ctx, zoneId, "testfile", map[string]any{"a": 6, "c": "world", "d": 7, "q": nil}, true) if err != nil { t.Fatalf("error setting meta: %v", err) } file, err = WFS.Stat(ctx, zoneId, "testfile") if err != nil { t.Fatalf("error stating file: %v", err) } if file == nil { t.Fatalf("file not found") } checkMapsEqual(t, map[string]any{"a": 6, "b": "hello", "c": "world", "d": 7}, file.Meta, "meta") err = WFS.WriteMeta(ctx, zoneId, "testfile-notexist", map[string]any{"a": 6}, true) if err == nil { t.Fatalf("expected error setting meta") } err = nil } func checkFileSize(t *testing.T, ctx context.Context, zoneId string, name string, size int64) { file, err := WFS.Stat(ctx, zoneId, name) if err != nil { t.Errorf("error stating file %q: %v", name, err) return } if file == nil { t.Errorf("file %q not found", name) return } if file.Size != size { t.Errorf("size mismatch for file %q: expected %d, got %d", name, size, file.Size) } } func checkFileData(t *testing.T, ctx context.Context, zoneId string, name string, data string) { _, rdata, err := WFS.ReadFile(ctx, zoneId, name) if err != nil { t.Errorf("error reading data for file %q: %v", name, err) return } if string(rdata) != data { t.Errorf("data mismatch for file %q: expected %q, got %q", name, data, string(rdata)) } } func checkFileByteCount(t *testing.T, ctx context.Context, zoneId string, name string, val byte, expected int) { _, rdata, err := WFS.ReadFile(ctx, zoneId, name) if err != nil { t.Errorf("error reading data for file %q: %v", name, err) return } var count int for _, b := range rdata { if b == val { count++ } } if count != expected { t.Errorf("byte count mismatch for file %q: expected %d, got %d", name, expected, count) } } func checkFileDataAt(t *testing.T, ctx context.Context, zoneId string, name string, offset int64, data string) { _, rdata, err := WFS.ReadAt(ctx, zoneId, name, offset, int64(len(data))) if err != nil { t.Errorf("error reading data for file %q: %v", name, err) return } if string(rdata) != data { t.Errorf("data mismatch for file %q: expected %q, got %q", name, data, string(rdata)) } } func TestAppend(t *testing.T) { initDb(t) defer cleanupDb(t) ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() zoneId := uuid.NewString() fileName := "t2" err := WFS.MakeFile(ctx, zoneId, fileName, nil, FileOptsType{}) if err != nil { t.Fatalf("error creating file: %v", err) } err = WFS.AppendData(ctx, zoneId, fileName, []byte("hello")) if err != nil { t.Fatalf("error appending data: %v", err) } // fmt.Print(GBS.dump()) checkFileSize(t, ctx, zoneId, fileName, 5) checkFileData(t, ctx, zoneId, fileName, "hello") err = WFS.AppendData(ctx, zoneId, fileName, []byte(" world")) if err != nil { t.Fatalf("error appending data: %v", err) } // fmt.Print(GBS.dump()) checkFileSize(t, ctx, zoneId, fileName, 11) checkFileData(t, ctx, zoneId, fileName, "hello world") } func TestWriteFile(t *testing.T) { initDb(t) defer cleanupDb(t) ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() zoneId := uuid.NewString() fileName := "t3" err := WFS.MakeFile(ctx, zoneId, fileName, nil, FileOptsType{}) if err != nil { t.Fatalf("error creating file: %v", err) } err = WFS.WriteFile(ctx, zoneId, fileName, []byte("hello world!")) if err != nil { t.Fatalf("error writing data: %v", err) } checkFileData(t, ctx, zoneId, fileName, "hello world!") err = WFS.WriteFile(ctx, zoneId, fileName, []byte("goodbye world!")) if err != nil { t.Fatalf("error writing data: %v", err) } checkFileData(t, ctx, zoneId, fileName, "goodbye world!") err = WFS.WriteFile(ctx, zoneId, fileName, []byte("hello")) if err != nil { t.Fatalf("error writing data: %v", err) } checkFileData(t, ctx, zoneId, fileName, "hello") // circular file err = WFS.MakeFile(ctx, zoneId, "c1", nil, FileOptsType{Circular: true, MaxSize: 50}) if err != nil { t.Fatalf("error creating file: %v", err) } err = WFS.WriteFile(ctx, zoneId, "c1", []byte("123456789 123456789 123456789 123456789 123456789 apple")) if err != nil { t.Fatalf("error writing data: %v", err) } checkFileData(t, ctx, zoneId, "c1", "6789 123456789 123456789 123456789 123456789 apple") err = WFS.AppendData(ctx, zoneId, "c1", []byte(" banana")) if err != nil { t.Fatalf("error appending data: %v", err) } checkFileData(t, ctx, zoneId, "c1", "3456789 123456789 123456789 123456789 apple banana") } func TestCircularWrites(t *testing.T) { initDb(t) defer cleanupDb(t) ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() zoneId := uuid.NewString() err := WFS.MakeFile(ctx, zoneId, "c1", nil, FileOptsType{Circular: true, MaxSize: 50}) if err != nil { t.Fatalf("error creating file: %v", err) } err = WFS.WriteFile(ctx, zoneId, "c1", []byte("123456789 123456789 123456789 123456789 123456789 ")) if err != nil { t.Fatalf("error writing data: %v", err) } checkFileData(t, ctx, zoneId, "c1", "123456789 123456789 123456789 123456789 123456789 ") err = WFS.AppendData(ctx, zoneId, "c1", []byte("apple")) if err != nil { t.Fatalf("error appending data: %v", err) } checkFileData(t, ctx, zoneId, "c1", "6789 123456789 123456789 123456789 123456789 apple") err = WFS.WriteAt(ctx, zoneId, "c1", 0, []byte("foo")) if err != nil { t.Fatalf("error writing data: %v", err) } // content should be unchanged because write is before the beginning of circular offset checkFileData(t, ctx, zoneId, "c1", "6789 123456789 123456789 123456789 123456789 apple") err = WFS.WriteAt(ctx, zoneId, "c1", 5, []byte("a")) if err != nil { t.Fatalf("error writing data: %v", err) } checkFileSize(t, ctx, zoneId, "c1", 55) checkFileData(t, ctx, zoneId, "c1", "a789 123456789 123456789 123456789 123456789 apple") err = WFS.AppendData(ctx, zoneId, "c1", []byte(" banana")) if err != nil { t.Fatalf("error appending data: %v", err) } checkFileSize(t, ctx, zoneId, "c1", 62) checkFileData(t, ctx, zoneId, "c1", "3456789 123456789 123456789 123456789 apple banana") err = WFS.WriteAt(ctx, zoneId, "c1", 20, []byte("foo")) if err != nil { t.Fatalf("error writing data: %v", err) } checkFileSize(t, ctx, zoneId, "c1", 62) checkFileData(t, ctx, zoneId, "c1", "3456789 foo456789 123456789 123456789 apple banana") offset, _, _ := WFS.ReadFile(ctx, zoneId, "c1") if offset != 12 { t.Errorf("offset mismatch: expected 12, got %d", offset) } err = WFS.AppendData(ctx, zoneId, "c1", []byte(" world")) if err != nil { t.Fatalf("error appending data: %v", err) } checkFileSize(t, ctx, zoneId, "c1", 68) offset, _, _ = WFS.ReadFile(ctx, zoneId, "c1") if offset != 18 { t.Errorf("offset mismatch: expected 18, got %d", offset) } checkFileData(t, ctx, zoneId, "c1", "9 foo456789 123456789 123456789 apple banana world") err = WFS.AppendData(ctx, zoneId, "c1", []byte(" 123456789 123456789 123456789 123456789 bar456789 123456789")) if err != nil { t.Fatalf("error appending data: %v", err) } checkFileSize(t, ctx, zoneId, "c1", 128) checkFileData(t, ctx, zoneId, "c1", " 123456789 123456789 123456789 bar456789 123456789") err = withLock(WFS, zoneId, "c1", func(entry *CacheEntry) error { if entry == nil { return fmt.Errorf("entry not found") } if len(entry.DataEntries) != 1 { return fmt.Errorf("data entries mismatch: expected 1, got %d", len(entry.DataEntries)) } return nil }) if err != nil { t.Fatalf("error checking data entries: %v", err) } } func makeText(n int) string { var buf bytes.Buffer for i := 0; i < n; i++ { buf.WriteByte(byte('0' + (i % 10))) } return buf.String() } func TestMultiPart(t *testing.T) { initDb(t) defer cleanupDb(t) ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() zoneId := uuid.NewString() fileName := "m2" data := makeText(80) err := WFS.MakeFile(ctx, zoneId, fileName, nil, FileOptsType{}) if err != nil { t.Fatalf("error creating file: %v", err) } err = WFS.AppendData(ctx, zoneId, fileName, []byte(data)) if err != nil { t.Fatalf("error appending data: %v", err) } checkFileSize(t, ctx, zoneId, fileName, 80) checkFileData(t, ctx, zoneId, fileName, data) _, barr, err := WFS.ReadAt(ctx, zoneId, fileName, 42, 10) if err != nil { t.Fatalf("error reading data: %v", err) } if string(barr) != data[42:52] { t.Errorf("data mismatch: expected %q, got %q", data[42:52], string(barr)) } WFS.WriteAt(ctx, zoneId, fileName, 49, []byte("world")) checkFileSize(t, ctx, zoneId, fileName, 80) checkFileDataAt(t, ctx, zoneId, fileName, 49, "world") checkFileDataAt(t, ctx, zoneId, fileName, 48, "8world4") } func testIntMapsEq(t *testing.T, msg string, m map[int]int, expected map[int]int) { if len(m) != len(expected) { t.Errorf("%s: map length mismatch got:%d expected:%d", msg, len(m), len(expected)) return } for k, v := range m { if expected[k] != v { t.Errorf("%s: value mismatch for key %d, got:%d expected:%d", msg, k, v, expected[k]) } } } func TestComputePartMap(t *testing.T) { partDataSize = 100 defer func() { partDataSize = DefaultPartDataSize }() file := &WaveFile{} m := file.computePartMap(0, 250) testIntMapsEq(t, "map1", m, map[int]int{0: 100, 1: 100, 2: 50}) m = file.computePartMap(110, 40) log.Printf("map2:%#v\n", m) testIntMapsEq(t, "map2", m, map[int]int{1: 40}) m = file.computePartMap(110, 90) testIntMapsEq(t, "map3", m, map[int]int{1: 90}) m = file.computePartMap(110, 91) testIntMapsEq(t, "map4", m, map[int]int{1: 90, 2: 1}) m = file.computePartMap(820, 340) testIntMapsEq(t, "map5", m, map[int]int{8: 80, 9: 100, 10: 100, 11: 60}) // now test circular file = &WaveFile{Opts: FileOptsType{Circular: true, MaxSize: 1000}} m = file.computePartMap(10, 250) testIntMapsEq(t, "map6", m, map[int]int{0: 90, 1: 100, 2: 60}) m = file.computePartMap(990, 40) testIntMapsEq(t, "map7", m, map[int]int{9: 10, 0: 30}) m = file.computePartMap(990, 130) testIntMapsEq(t, "map8", m, map[int]int{9: 10, 0: 100, 1: 20}) m = file.computePartMap(5, 1105) testIntMapsEq(t, "map9", m, map[int]int{0: 100, 1: 10, 2: 100, 3: 100, 4: 100, 5: 100, 6: 100, 7: 100, 8: 100, 9: 100}) m = file.computePartMap(2005, 1105) testIntMapsEq(t, "map9", m, map[int]int{0: 100, 1: 10, 2: 100, 3: 100, 4: 100, 5: 100, 6: 100, 7: 100, 8: 100, 9: 100}) } func TestSimpleDBFlush(t *testing.T) { initDb(t) defer cleanupDb(t) ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() zoneId := uuid.NewString() fileName := "t1" err := WFS.MakeFile(ctx, zoneId, fileName, nil, FileOptsType{}) if err != nil { t.Fatalf("error creating file: %v", err) } err = WFS.WriteFile(ctx, zoneId, fileName, []byte("hello world!")) if err != nil { t.Fatalf("error writing data: %v", err) } checkFileData(t, ctx, zoneId, fileName, "hello world!") _, err = WFS.FlushCache(ctx) if err != nil { t.Fatalf("error flushing cache: %v", err) } if WFS.getCacheSize() != 0 { t.Errorf("cache size mismatch") } checkFileData(t, ctx, zoneId, fileName, "hello world!") if WFS.getCacheSize() != 0 { t.Errorf("cache size mismatch (after read)") } checkFileDataAt(t, ctx, zoneId, fileName, 6, "world!") checkFileSize(t, ctx, zoneId, fileName, 12) checkFileByteCount(t, ctx, zoneId, fileName, 'l', 3) } func TestConcurrentAppend(t *testing.T) { initDb(t) defer cleanupDb(t) ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() zoneId := uuid.NewString() fileName := "t1" err := WFS.MakeFile(ctx, zoneId, fileName, nil, FileOptsType{}) if err != nil { t.Fatalf("error creating file: %v", err) } var wg sync.WaitGroup for i := 0; i < 16; i++ { wg.Add(1) go func(n int) { defer wg.Done() const hexChars = "0123456789abcdef" ch := hexChars[n] for j := 0; j < 100; j++ { err := WFS.AppendData(ctx, zoneId, fileName, []byte{ch}) if err != nil { t.Errorf("error appending data (%d): %v", n, err) } if j == 50 { // ignore error here (concurrent flushing) WFS.FlushCache(ctx) } } }(i) } wg.Wait() checkFileSize(t, ctx, zoneId, fileName, 1600) checkFileByteCount(t, ctx, zoneId, fileName, 'a', 100) checkFileByteCount(t, ctx, zoneId, fileName, 'e', 100) WFS.FlushCache(ctx) checkFileSize(t, ctx, zoneId, fileName, 1600) checkFileByteCount(t, ctx, zoneId, fileName, 'a', 100) checkFileByteCount(t, ctx, zoneId, fileName, 'e', 100) } func jsonDeepEqual(d1 any, d2 any) bool { if d1 == nil && d2 == nil { return true } if d1 == nil || d2 == nil { return false } t1 := reflect.TypeOf(d1) t2 := reflect.TypeOf(d2) if t1 != t2 { return false } switch d1.(type) { case float64: return d1.(float64) == d2.(float64) case string: return d1.(string) == d2.(string) case bool: return d1.(bool) == d2.(bool) case []any: a1 := d1.([]any) a2 := d2.([]any) if len(a1) != len(a2) { return false } for i := 0; i < len(a1); i++ { if !jsonDeepEqual(a1[i], a2[i]) { return false } } return true case map[string]any: m1 := d1.(map[string]any) m2 := d2.(map[string]any) if len(m1) != len(m2) { return false } for k, v := range m1 { if !jsonDeepEqual(v, m2[k]) { return false } } return true default: return false } } func TestIJson(t *testing.T) { initDb(t) defer cleanupDb(t) ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() zoneId := uuid.NewString() fileName := "ij1" err := WFS.MakeFile(ctx, zoneId, fileName, nil, FileOptsType{IJson: true}) if err != nil { t.Fatalf("error creating file: %v", err) } rootSet := ijson.MakeSetCommand(nil, map[string]any{"tag": "div", "class": "root"}) err = WFS.AppendIJson(ctx, zoneId, fileName, rootSet) if err != nil { t.Fatalf("error appending ijson: %v", err) } _, fullData, err := WFS.ReadFile(ctx, zoneId, fileName) if err != nil { t.Fatalf("error reading file: %v", err) } cmds, err := ijson.ParseIJson(fullData) if err != nil { t.Fatalf("error parsing ijson: %v", err) } outData, err := ijson.ApplyCommands(nil, cmds, 0) if err != nil { t.Fatalf("error applying ijson: %v", err) } if !jsonDeepEqual(rootSet["data"], outData) { t.Errorf("data mismatch: expected %v, got %v", rootSet["data"], outData) } childrenAppend := ijson.MakeAppendCommand(ijson.Path{"children"}, map[string]any{"tag": "div", "class": "child"}) err = WFS.AppendIJson(ctx, zoneId, fileName, childrenAppend) if err != nil { t.Fatalf("error appending ijson: %v", err) } _, fullData, err = WFS.ReadFile(ctx, zoneId, fileName) if err != nil { t.Fatalf("error reading file: %v", err) } cmds, err = ijson.ParseIJson(fullData) if err != nil { t.Fatalf("error parsing ijson: %v", err) } if len(cmds) != 2 { t.Fatalf("command count mismatch: expected 2, got %d", len(cmds)) } outData, err = ijson.ApplyCommands(nil, cmds, 0) if err != nil { t.Fatalf("error applying ijson: %v", err) } if !jsonDeepEqual(ijson.M{"tag": "div", "class": "root", "children": ijson.A{ijson.M{"tag": "div", "class": "child"}}}, outData) { t.Errorf("data mismatch: expected %v, got %v", rootSet["data"], outData) } err = WFS.CompactIJson(ctx, zoneId, fileName) if err != nil { t.Fatalf("error compacting ijson: %v", err) } _, fullData, err = WFS.ReadFile(ctx, zoneId, fileName) if err != nil { t.Fatalf("error reading file: %v", err) } cmds, err = ijson.ParseIJson(fullData) if err != nil { t.Fatalf("error parsing ijson: %v", err) } if len(cmds) != 1 { t.Fatalf("command count mismatch: expected 1, got %d", len(cmds)) } outData, err = ijson.ApplyCommands(nil, cmds, 0) if err != nil { t.Fatalf("error applying ijson: %v", err) } if !jsonDeepEqual(ijson.M{"tag": "div", "class": "root", "children": ijson.A{ijson.M{"tag": "div", "class": "child"}}}, outData) { t.Errorf("data mismatch: expected %v, got %v", rootSet["data"], outData) } }