Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33
+175
-31
@@ -2,28 +2,42 @@ package fejkdata
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"encoding/json"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/larvit/fejkdata/internal/datafiles"
|
||||
)
|
||||
|
||||
const shippedIndexFile = "shippedindex.go"
|
||||
var shippedManifestFile = path.Join("data", datafiles.ManifestFile)
|
||||
|
||||
// newShippedWhole loads every shipped category in New, as a --data-path load does, so a test
|
||||
// reads the whole shipped tree.
|
||||
// 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(data)}, opts...)...)
|
||||
f, err := New(append([]Option{WithoutShippedData(), WithDataFS(withoutManifest{data})}, opts...)...)
|
||||
if err != nil {
|
||||
t.Fatalf("New() = %v", err)
|
||||
}
|
||||
@@ -39,45 +53,62 @@ func loadedCategories(f *Generator) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// REGENERATE=1 rewrites the index.
|
||||
func TestShippedIndexIsCurrent(t *testing.T) {
|
||||
got, err := shippedIndexSource(newShippedWhole(t))
|
||||
// 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(shippedIndexFile, got, 0o644); err != nil {
|
||||
if err := os.WriteFile(shippedManifestFile, got, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
file, err := os.ReadFile(shippedIndexFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want, err := format.Source(file) // docs/decisions.md#the-format-check-runs-on-the-latest-go-only
|
||||
want, err := os.ReadFile(shippedManifestFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != string(want) {
|
||||
t.Fatalf("%s is stale for data/; regenerate it: docker compose run --rm --user \"$(id -u):$(id -g)\" generate", shippedIndexFile)
|
||||
t.Fatalf("%s is stale; regenerate it: docker compose run --rm --user \"$(id -u):$(id -g)\" generate", shippedManifestFile)
|
||||
}
|
||||
}
|
||||
|
||||
func shippedIndexSource(whole *Generator) ([]byte, error) {
|
||||
// shippedManifest is the manifest of whole's tree, one category a line.
|
||||
func shippedManifest(whole *Generator) ([]byte, error) {
|
||||
var b bytes.Buffer
|
||||
b.WriteString("// Code generated by go generate. DO NOT EDIT.\n\npackage fejkdata\n\nvar shippedIndex = map[string]shippedEntry{\n")
|
||||
for _, s := range categorySites(&whole.root) {
|
||||
fmt.Fprintf(&b, "%q: {", s.path)
|
||||
if t, isTable := s.n.(*table); isTable && t.rows.Options().Parent != "" {
|
||||
fmt.Fprintf(&b, "parent: %q, ", t.rows.Options().Parent)
|
||||
b.WriteString("{\n\t\"index\": {")
|
||||
for i, s := range categorySites(&whole.root) {
|
||||
e := datafiles.IndexEntry{Paths: paths(s.n, false), Reads: categoryReads(&whole.root, s)}
|
||||
if t, isTable := s.n.(*table); isTable {
|
||||
e.Parent = t.rows.Options().Parent
|
||||
}
|
||||
ps := paths(s.n, false)
|
||||
sort.Strings(ps)
|
||||
fmt.Fprintf(&b, "paths: %#v},\n", ps)
|
||||
sort.Strings(e.Paths)
|
||||
line, err := json.Marshal(e)
|
||||
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")
|
||||
return format.Source(b.Bytes())
|
||||
b.WriteString("\n\t}\n}\n")
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func shippedIndex(t *testing.T) map[string]datafiles.IndexEntry {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(shippedManifestFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m datafiles.Manifest
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return m.Index
|
||||
}
|
||||
|
||||
func TestNewLoadsNoShippedCategory(t *testing.T) {
|
||||
@@ -146,16 +177,115 @@ func TestReachingACategoryLoadsWhatItReads(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestADataPathLoadsTheShippedSetWhole(t *testing.T) {
|
||||
f, err := New(WithDataPath(writeData(t, map[string]string{"x": `"y"`})))
|
||||
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)
|
||||
}
|
||||
if got, want := loadedCategories(f), loadedCategories(newShippedWhole(t, WithDataPath(writeData(t, map[string]string{"x": `"y"`})))); !reflect.DeepEqual(got, want) {
|
||||
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 _, 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",
|
||||
} {
|
||||
_, 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()
|
||||
@@ -183,7 +313,7 @@ func TestOnDemandRendersAsTheWholeLoad(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEveryShippedCategoryLoadsAlone(t *testing.T) {
|
||||
for _, p := range sortedNames(shippedIndex) {
|
||||
for _, p := range sortedNames(shippedIndex(t)) {
|
||||
f, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("New() = %v", err)
|
||||
@@ -215,3 +345,17 @@ func TestListRunsBesideAnOnDemandLoad(t *testing.T) {
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// categoryReads is the sorted path of every other category s's templates reference.
|
||||
func categoryReads(root *folder, s categorySite) []string {
|
||||
seen := map[string]bool{s.path: true}
|
||||
var out []string
|
||||
for _, c := range referenced(root, s.dir, siteNodes([]categorySite{s})) {
|
||||
if p := c.path(); !seen[p] {
|
||||
seen[p] = true
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user