377 lines
12 KiB
Go
377 lines
12 KiB
Go
package fejkdata
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
"github.com/larvit/fejkdata/internal/datafiles"
|
|
)
|
|
|
|
var shippedManifestFile = path.Join("data", datafiles.ManifestFile)
|
|
|
|
// withoutManifest is fsys with its manifest hidden, so it loads whole in New.
|
|
type withoutManifest struct{ fs.FS }
|
|
|
|
func (w withoutManifest) Open(name string) (fs.File, error) {
|
|
if name == datafiles.ManifestFile {
|
|
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
|
}
|
|
return w.FS.Open(name)
|
|
}
|
|
|
|
// newShippedWhole loads every shipped category in New, as a source without an index does, so
|
|
// a test reads the whole shipped tree.
|
|
func newShippedWhole(t testing.TB, opts ...Option) *Generator {
|
|
t.Helper()
|
|
data, err := fs.Sub(shippedFS, "data")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, err := New(append([]Option{WithoutShippedData(), WithDataFS(withoutManifest{data})}, opts...)...)
|
|
if err != nil {
|
|
t.Fatalf("New() = %v", err)
|
|
}
|
|
return f
|
|
}
|
|
|
|
// loadedCategories is the dot path of every category f has parsed and resolved.
|
|
func loadedCategories(f *Generator) []string {
|
|
var out []string
|
|
for _, s := range categorySites(&f.root) {
|
|
out = append(out, s.path)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// REGENERATE=1 rewrites the manifest.
|
|
func TestShippedManifestIsCurrent(t *testing.T) {
|
|
got, err := shippedManifest(newShippedWhole(t))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if os.Getenv("REGENERATE") == "1" {
|
|
if err := os.WriteFile(shippedManifestFile, got, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return
|
|
}
|
|
want, err := os.ReadFile(shippedManifestFile)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != string(want) {
|
|
t.Fatalf("%s is stale; regenerate it: docker compose run --rm --user \"$(id -u):$(id -g)\" generate", shippedManifestFile)
|
|
}
|
|
}
|
|
|
|
// shippedManifest is the manifest of whole's tree, one category a line.
|
|
func shippedManifest(whole *Generator) ([]byte, error) {
|
|
var b bytes.Buffer
|
|
b.WriteString("{\n\t\"index\": {")
|
|
for i, s := range categorySites(&whole.root) {
|
|
line, err := json.Marshal(indexEntry(&whole.root, s))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if i > 0 {
|
|
b.WriteString(",")
|
|
}
|
|
key, _ := json.Marshal(s.path)
|
|
b.WriteString("\n\t\t" + string(key) + ": " + string(line))
|
|
}
|
|
b.WriteString("\n\t}\n}\n")
|
|
return b.Bytes(), nil
|
|
}
|
|
|
|
func shippedIndex(t *testing.T) map[string]datafiles.IndexEntry {
|
|
t.Helper()
|
|
m, err := shippedSource.Manifest()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return m.Index
|
|
}
|
|
|
|
func TestNewLoadsNoShippedCategory(t *testing.T) {
|
|
f, err := New()
|
|
if err != nil {
|
|
t.Fatalf("New() = %v", err)
|
|
}
|
|
if got := loadedCategories(f); len(got) > 0 {
|
|
t.Fatalf("New() loaded %v", got)
|
|
}
|
|
}
|
|
|
|
func TestReachingACategoryLoadsWhatItReads(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
reach func(f *Generator) error
|
|
want, absent []string
|
|
}{
|
|
{
|
|
reach: func(f *Generator) error { _, err := f.Fake("sv_SE.person.first"); return err },
|
|
want: []string{"sv_SE.first-name", "sv_SE.last-name", "sv_SE.person", "sv_SE.sex", "sv_SE.title"},
|
|
},
|
|
{
|
|
reach: func(f *Generator) error { _, err := f.Fake("geo.SE.region"); return err },
|
|
want: []string{"geo.SE.locality", "geo.SE.municipality", "geo.SE.postal-code", "geo.SE.region", "geo.SE.street"},
|
|
absent: []string{"geo.SE.address", "geo.US.region"},
|
|
},
|
|
{
|
|
reach: func(f *Generator) error { _, err := f.FakeTemplate("{/misc.territory[SE].name}"); return err },
|
|
want: []string{"misc.territory", "misc.timezone"},
|
|
absent: []string{"misc.tld"},
|
|
},
|
|
{
|
|
reach: func(f *Generator) error { _, err := f.FakeRecord("misc.uuid"); return err },
|
|
want: []string{"misc.uuid"},
|
|
absent: []string{"misc.objectid"},
|
|
},
|
|
{
|
|
reach: func(f *Generator) error {
|
|
var v struct {
|
|
Name string `fake:"sv_SE.first-name"`
|
|
}
|
|
return f.FakeStruct(&v)
|
|
},
|
|
want: []string{"sv_SE.first-name"},
|
|
absent: []string{"sv_SE.person"},
|
|
},
|
|
} {
|
|
f, err := New()
|
|
if err != nil {
|
|
t.Fatalf("New() = %v", err)
|
|
}
|
|
if err := tc.reach(f); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := loadedCategories(f)
|
|
for _, w := range tc.want {
|
|
if !slices.Contains(got, w) {
|
|
t.Errorf("loaded %v, want %s among them", got, w)
|
|
}
|
|
}
|
|
for _, a := range tc.absent {
|
|
if slices.Contains(got, a) {
|
|
t.Errorf("loaded %v, want %s left unloaded", got, a)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestADataPathLoadsWhatItReads(t *testing.T) {
|
|
f, err := New(WithDataPath(writeData(t, map[string]string{"greeting": `"Hej {/sv_SE.person}!"`})))
|
|
if err != nil {
|
|
t.Fatalf("New() = %v", err)
|
|
}
|
|
got := loadedCategories(f)
|
|
for _, w := range []string{"greeting", "sv_SE.first-name", "sv_SE.person"} {
|
|
if !slices.Contains(got, w) {
|
|
t.Errorf("New loaded %v, want %s among them", got, w)
|
|
}
|
|
}
|
|
for _, a := range []string{"misc.uuid", "sv_SE.address", "geo.SE.region"} {
|
|
if slices.Contains(got, a) {
|
|
t.Errorf("New loaded %v, want %s left unloaded", got, a)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestADataPathLoadsTheShippedCategoriesReachingWhatItReplaces(t *testing.T) {
|
|
f, err := New(WithDataPath(writeFiles(t, map[string]string{
|
|
"sv_SE/last-name.json": `{"format":"{name}","rows":"last-name.tsv","key":"name"}`,
|
|
"sv_SE/last-name.tsv": "name\nSvensson\n",
|
|
})))
|
|
if err != nil {
|
|
t.Fatalf("New() = %v", err)
|
|
}
|
|
got := loadedCategories(f)
|
|
for _, w := range []string{"sv_SE.last-name", "sv_SE.person"} {
|
|
if !slices.Contains(got, w) {
|
|
t.Errorf("New loaded %v, want %s among them", got, w)
|
|
}
|
|
}
|
|
if slices.Contains(got, "misc.uuid") {
|
|
t.Errorf("New loaded %v, want misc.uuid left unloaded", got)
|
|
}
|
|
if _, err := New(WithDataPath(writeData(t, map[string]string{"sv_SE/last-name": `"x"`}))); err == nil || !strings.Contains(err.Error(), "sv_SE.person") {
|
|
t.Fatalf("New(a last-name the shipped person cannot read) = %v, want an error naming sv_SE.person", err)
|
|
}
|
|
}
|
|
|
|
func TestAnIndexedSourceStandsLikeTheShippedOne(t *testing.T) {
|
|
fsys := fstest.MapFS{
|
|
".fejkdata.json": {Data: []byte(`{"index": {"a": {"paths": [""], "reads": ["b"]}, "b": {"paths": [""]}, "sub.c": {"paths": ["", "d"]}}}`)},
|
|
"a.json": {Data: []byte(`"{/b}!"`)},
|
|
"b.json": {Data: []byte(`"x"`)},
|
|
"sub/c.json": {Data: []byte(`{"format":"{d}","d":"y"}`)},
|
|
}
|
|
f, err := New(WithoutShippedData(), WithDataFS(fsys), WithSeed(1))
|
|
if err != nil {
|
|
t.Fatalf("New() = %v", err)
|
|
}
|
|
if got := loadedCategories(f); len(got) > 0 {
|
|
t.Fatalf("New() loaded %v", got)
|
|
}
|
|
if got, want := f.List(), []string{"a", "b", "sub.c", "sub.c.d"}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("List() = %v, want %v", got, want)
|
|
}
|
|
if got := fake(t, f, "a"); got != "x!" {
|
|
t.Fatalf(`Fake("a") = %q, want "x!"`, got)
|
|
}
|
|
if got, want := loadedCategories(f), []string{"a", "b"}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("loaded %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestAnIndexedCategoryFailsAtFirstReachAndEveryReachAfter(t *testing.T) {
|
|
f, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{
|
|
".fejkdata.json": {Data: []byte(`{"index": {"bad": {"paths": [""]}, "good": {"paths": [""]}}}`)},
|
|
"bad.json": {Data: []byte(`"{/nope}"`)},
|
|
"good.json": {Data: []byte(`"fine"`)},
|
|
}))
|
|
if err != nil {
|
|
t.Fatalf("New() = %v", err)
|
|
}
|
|
_, first := f.Fake("bad")
|
|
if first == nil || !strings.Contains(first.Error(), "{/nope}") {
|
|
t.Fatalf(`Fake("bad") = %v, want an error naming {/nope}`, first)
|
|
}
|
|
if !errors.Is(first, ErrLoad) {
|
|
t.Fatalf(`Fake("bad") = %v, want it to wrap ErrLoad`, first)
|
|
}
|
|
if _, again := f.Fake("bad"); again == nil || again.Error() != first.Error() {
|
|
t.Fatalf(`Fake("bad") again = %v, want %v`, again, first)
|
|
}
|
|
if got := fake(t, f, "good"); got != "fine" {
|
|
t.Fatalf(`Fake("good") = %q`, got)
|
|
}
|
|
}
|
|
|
|
func TestABrokenManifestFailsNew(t *testing.T) {
|
|
for manifest, want := range map[string]string{
|
|
`{`: datafiles.ManifestFile,
|
|
`{"indx": {}}`: "indx",
|
|
`{"index": {"a..b": {"paths": [""]}}}`: "a..b",
|
|
`{"index": {}} {}`: "more than one JSON value",
|
|
`{"index": {"a": {"paths": [""], "parent": "b"}}}`: `parent "b" names no entry`,
|
|
`{"index": {"a": {"paths": [""], "reads": ["b..c"]}}}`: "b..c",
|
|
} {
|
|
_, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{".fejkdata.json": {Data: []byte(manifest)}}))
|
|
if err == nil || !strings.Contains(err.Error(), want) {
|
|
t.Errorf("New(manifest %s) = %v, want an error naming %s", manifest, err, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAnIndexedSourceReplacingAShippedCategoryLoadsItsReadersInNew(t *testing.T) {
|
|
_, err := New(WithDataFS(fstest.MapFS{
|
|
".fejkdata.json": {Data: []byte(`{"index": {"sv_SE.last-name": {"paths": [""]}}}`)},
|
|
"sv_SE/last-name.json": {Data: []byte(`"x"`)},
|
|
}))
|
|
if err == nil || !strings.Contains(err.Error(), "sv_SE.person") {
|
|
t.Fatalf("New() = %v, want an error naming sv_SE.person", err)
|
|
}
|
|
}
|
|
|
|
func TestOnDemandRendersAsTheWholeLoad(t *testing.T) {
|
|
whole := newShippedWhole(t)
|
|
paths := whole.List()
|
|
lazy, err := New()
|
|
if err != nil {
|
|
t.Fatalf("New() = %v", err)
|
|
}
|
|
if got := lazy.List(); !reflect.DeepEqual(got, paths) {
|
|
t.Fatalf("List() on demand = %v, want %v", got, paths)
|
|
}
|
|
a, err := New(WithSeed(7))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b := newShippedWhole(t, WithSeed(7))
|
|
for _, p := range paths {
|
|
for range 3 {
|
|
got, gotErr := a.Fake(p)
|
|
want, wantErr := b.Fake(p)
|
|
if got != want || (gotErr == nil) != (wantErr == nil) {
|
|
t.Fatalf("Fake(%q) on demand = %q, %v; whole = %q, %v", p, got, gotErr, want, wantErr)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEveryShippedCategoryLoadsAlone(t *testing.T) {
|
|
for _, p := range sortedNames(shippedIndex(t)) {
|
|
f, err := New()
|
|
if err != nil {
|
|
t.Fatalf("New() = %v", err)
|
|
}
|
|
if _, err := f.Fake(p); err != nil {
|
|
t.Errorf("Fake(%q) on a fresh New() = %v", p, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListRunsBesideAnOnDemandLoad(t *testing.T) {
|
|
f, err := New()
|
|
if err != nil {
|
|
t.Fatalf("New() = %v", err)
|
|
}
|
|
var wg sync.WaitGroup
|
|
for _, p := range []string{"sv_SE.person", "geo.US.address", "misc.territory"} {
|
|
wg.Add(2)
|
|
go func() {
|
|
defer wg.Done()
|
|
if _, err := f.Fake(p); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
f.List()
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestAStaleIndexEntryFailsItsFirstReachNamingTheManifest(t *testing.T) {
|
|
table := map[string]string{
|
|
"country.json": `{"format":"{name}","rows":"country.tsv","key":"code"}`,
|
|
"country.tsv": "code\tname\nSE\tSweden\n",
|
|
"city.json": `{"format":"{name}","rows":"city.tsv","key":"name","parent":"country"}`,
|
|
"city.tsv": "name\tcountry\nOslo\tSE\n",
|
|
"word.json": `{"format":"{a}","a":"x"}`,
|
|
}
|
|
for index, reach := range map[string]string{
|
|
`{"index": {"word": {"paths": [""]}}}`: "word",
|
|
`{"index": {"word": {"paths": ["", "a"], "reads": ["city"]}, "city": {"paths": [""]}, "country": {"paths": [""]}}}`: "word",
|
|
`{"index": {"city": {"paths": ["", "country", "name"]}, "country": {"paths": ["", "city", "city.country", "city.name", "code", "name"]}}}`: "city",
|
|
} {
|
|
fsys := fstest.MapFS{".fejkdata.json": {Data: []byte(index)}}
|
|
for name, data := range table {
|
|
fsys[name] = &fstest.MapFile{Data: []byte(data)}
|
|
}
|
|
f, err := New(WithoutShippedData(), WithDataFS(fsys))
|
|
if err != nil {
|
|
t.Fatalf("New(%s) = %v", index, err)
|
|
}
|
|
_, first := f.Fake(reach)
|
|
if first == nil || !strings.Contains(first.Error(), ".fejkdata.json") || !strings.Contains(first.Error(), "stale") {
|
|
t.Errorf("Fake(%q) under %s = %v, want the stale entry named", reach, index, first)
|
|
continue
|
|
}
|
|
if _, again := f.Fake(reach); again == nil || again.Error() != first.Error() {
|
|
t.Errorf("Fake(%q) again = %v, want %v", reach, again, first)
|
|
}
|
|
}
|
|
}
|