package datafiles import ( "errors" "io/fs" "os" "path/filepath" "reflect" "strings" "testing" "testing/fstest" ) type handed struct { dir []string name string json any } func walk(t *testing.T, src Source) ([]handed, error) { t.Helper() var got []handed err := src.Walk(func(c Category) error { got = append(got, handed{c.Folders, c.Name, c.JSON.Any()}) return nil }) return got, err } func TestWalkHandsEachCategoryInOrder(t *testing.T) { got, err := walk(t, Source{fsys: fstest.MapFS{ "a.json": {Data: []byte(`{"x":"1"}`)}, "sub/b.json": {Data: []byte(`"y"`)}, "sub/notes.txt": {Data: []byte("not data")}, "empty/x.txt": {Data: []byte("not data")}, ".git/c.json": {Data: []byte(`"hidden"`)}, "sub/.d.json": {Data: []byte(`"hidden"`)}, "sub/deep/e.json": {Data: []byte(`"z"`)}, }}) if err != nil { t.Fatal(err) } want := []handed{ {nil, "a", map[string]any{"x": "1"}}, {[]string{"sub"}, "b", "y"}, {[]string{"sub", "deep"}, "e", "z"}, } if !reflect.DeepEqual(got, want) { t.Errorf("Walk handed %#v, want %#v", got, want) } } func TestReadRowsReadsOnlyAFileBesideTheCategory(t *testing.T) { src := Source{fsys: fstest.MapFS{ "t.json": {Data: []byte(`{"rows":"t.tsv"}`)}, "t.tsv": {Data: []byte("key\nA\n")}, "sub/u.tsv": {Data: []byte("key\nB\n")}, "sub/u.json": {Data: []byte(`{"rows":"u.tsv"}`)}, }, label: "lbl"} rows := map[string]string{} err := src.Walk(func(c Category) error { data, err := c.ReadRows(c.Name + ".tsv") rows[c.Name] = data if err != nil { return err } if _, err := c.ReadRows("x.tsv"); err == nil || err.Error() != "rows names x.tsv, which is not beside it in "+map[string]string{"t": "lbl", "u": "lbl/sub"}[c.Name] { t.Errorf("ReadRows(x.tsv) = %v, want an error saying x.tsv is not beside %s", err, c.Name) } return nil }) if err != nil || rows["t"] != "key\nA\n" || rows["u"] != "key\nB\n" { t.Errorf("Walk = %v, read %q", err, rows) } } func TestWalkIgnoresARowsFileNoCategoryNames(t *testing.T) { if _, err := walk(t, Source{fsys: fstest.MapFS{"a.json": {Data: []byte(`"x"`)}, "a.tsv": {Data: []byte("key\n")}}, label: "lbl"}); err != nil { t.Errorf("Walk = %v, want a.tsv ignored", err) } } func TestARowsFileMayBeHidden(t *testing.T) { var rows string err := Source{fsys: fstest.MapFS{"a.json": {Data: []byte(`"x"`)}, ".a.tsv": {Data: []byte("key\nA\n")}}}.Walk(func(c Category) error { var err error rows, err = c.ReadRows(".a.tsv") return err }) if err != nil || rows != "key\nA\n" { t.Errorf("Walk = %v, read %q, want the hidden rows file read", err, rows) } } func TestWalkLabelsWhatACategoryFileGetsWrong(t *testing.T) { for _, c := range []struct { name string fs fstest.MapFS compile error want string }{ {"its compile", fstest.MapFS{"a.json": {Data: []byte(`"x"`)}}, errors.New("bad"), "lbl/a.json: bad"}, {"its JSON", fstest.MapFS{"a.json": {Data: []byte(`{`)}}, nil, "lbl/a.json: line 1, column 1: unexpected end of JSON input"}, {"its name", fstest.MapFS{"a.b.json": {Data: []byte(`"x"`)}}, nil, "lbl/a.b.json: category "}, {"its folder's name", fstest.MapFS{"a.b/c.json": {Data: []byte(`"x"`)}}, nil, "lbl/a.b: folder "}, } { err := Source{fsys: c.fs, label: "lbl"}.Walk(func(Category) error { return c.compile }) if err == nil || !strings.HasPrefix(err.Error(), c.want) { t.Errorf("%s: Walk = %v, want it to start %q", c.name, err, c.want) } } } func TestWalkChecksAFolderNameOnlyWhenItHoldsData(t *testing.T) { if _, err := walk(t, Source{fsys: fstest.MapFS{"a.b/notes.txt": {Data: []byte("x")}, "c.json": {Data: []byte(`"x"`)}}}); err != nil { t.Errorf("Walk = %v, want a folder holding no data skipped whatever its name", err) } } func TestWalkRefusesADiskPathThatIsNoDirectory(t *testing.T) { dir := t.TempDir() for _, c := range []struct{ path, want string }{ {"", "a data path is empty"}, {filepath.Join(dir, "none"), "data path " + filepath.Join(dir, "none") + ": no such directory"}, } { _, err := walk(t, Dir(c.path)) if err == nil || err.Error() != c.want { t.Errorf("Walk(%q) = %v, want %q", c.path, err, c.want) } } } func TestLoadHandsOneCategoryAndItsRows(t *testing.T) { src := Source{fsys: fstest.MapFS{ "sub/t.json": {Data: []byte(`{"rows":"t.tsv"}`)}, "sub/t.tsv": {Data: []byte("key\nA\n")}, "sub/u.json": {Data: []byte(`{`)}, }} var got handed var rows string err := src.Load([]string{"sub"}, "t", func(c Category) error { got = handed{c.Folders, c.Name, c.JSON.Any()} var err error rows, err = c.ReadRows("t.tsv") return err }) if err != nil || got.name != "t" || !reflect.DeepEqual(got.dir, []string{"sub"}) || rows != "key\nA\n" { t.Errorf("Load = %v, handed %#v with rows %q", err, got, rows) } } func TestDirReadsTheDirectoryAndLabelsItsErrorsWithIt(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "a.json"), []byte(`{`), 0o600); err != nil { t.Fatal(err) } _, err := walk(t, Dir(dir)) if err == nil || !strings.HasPrefix(err.Error(), filepath.ToSlash(filepath.Join(dir, "a.json"))+": ") { t.Errorf("Walk = %v, want the error labelled with the directory", err) } if _, err := walk(t, Dir(filepath.Join(dir, "a.json"))); err == nil || err.Error() != "data path "+filepath.Join(dir, "a.json")+": not a directory" { t.Errorf("Walk = %v, want a file refused as no directory", err) } } func TestAMountedDirNamesItsFilesWhereTheyAreOnDisk(t *testing.T) { dir := t.TempDir() if err := os.MkdirAll(filepath.Join(dir, "sub"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "sub", "a.json"), []byte(`{`), 0o600); err != nil { t.Fatal(err) } _, err := walk(t, Dir(dir).Mounted("my.data")) if err == nil || !strings.HasPrefix(err.Error(), filepath.ToSlash(filepath.Join(dir, "sub", "a.json"))+": ") { t.Errorf("Walk = %v, want the error to name the file on disk", err) } } func TestAMountedTreeListsAndReadsUnderItsMount(t *testing.T) { m := mounted{fsys: fstest.MapFS{"a.json": {Data: []byte(`"x"`)}, "sub/b.json": {Data: []byte(`"y"`)}}, at: "my/data"} for dir, want := range map[string][]string{".": {"my"}, "my": {"data"}, "my/data": {"a.json", "sub"}, "my/data/sub": {"b.json"}} { entries, err := fs.ReadDir(m, dir) var got []string for _, e := range entries { got = append(got, e.Name()) } if err != nil || !reflect.DeepEqual(got, want) { t.Errorf("ReadDir(%q) = %v, %v, want %v", dir, got, err, want) } } if b, err := fs.ReadFile(m, "my/data/sub/b.json"); err != nil || string(b) != `"y"` { t.Errorf(`ReadFile("my/data/sub/b.json") = %q, %v, want "y"`, b, err) } if _, err := fs.ReadFile(m, "other/a.json"); !errors.Is(err, fs.ErrNotExist) { t.Errorf(`ReadFile("other/a.json") = %v, want it not to exist`, err) } } func TestAMountedSourceNamesItsManifestBesideItsFiles(t *testing.T) { dir := t.TempDir() for name, c := range map[string]struct { src Source want string }{ "an fs.FS": {FS(fstest.MapFS{}).Mounted("my.data"), "my/data/.fejkdata.json"}, "a data path": {Dir(dir).Mounted("my.data"), filepath.ToSlash(filepath.Join(dir, ".fejkdata.json"))}, } { if got := c.src.ManifestPath(); got != c.want { t.Errorf("%s: ManifestPath() = %q, want %q", name, got, c.want) } } m, err := FS(fstest.MapFS{ManifestFile: {Data: []byte(`{"namespace": "my.data"}`)}}).Mounted("my.data").Manifest() if err != nil || m.Namespace != "my.data" { t.Errorf("Manifest() after Mounted = %+v, %v, want the manifest read from beside the files", m, err) } }