Test that a mounted module names a broken file beside its namespace, that a manifest refuses reads, and that each shipped module renders beside the modules its categories reach

This commit is contained in:
2026-10-11 13:23:30 +02:00
committed by Lillem4n
parent 89cafb1a21
commit 6a62840373
7 changed files with 32 additions and 131 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ func TestPackageImports(t *testing.T) {
"data/misc": nil, "data/misc": nil,
"data/svse": nil, "data/svse": nil,
"internal/builtinfunc": {"internal/datatype", "internal/drawstate", "internal/grammar", "internal/invariant", "internal/proven"}, "internal/builtinfunc": {"internal/datatype", "internal/drawstate", "internal/grammar", "internal/invariant", "internal/proven"},
"internal/datafiles": {"internal/grammar", "internal/jsonvalue"}, "internal/datafiles": {"internal/grammar", "internal/invariant", "internal/jsonvalue"},
"internal/datatype": nil, "internal/datatype": nil,
"internal/drawstate": nil, "internal/drawstate": nil,
"internal/grammar": nil, "internal/grammar": nil,
+4 -6
View File
@@ -58,7 +58,7 @@ func (f *Generator) loadCallerPath(path string) (string, []string, error) {
return "", nil, fmt.Errorf("fejkdata: %w", err) return "", nil, fmt.Errorf("fejkdata: %w", err)
} }
if c, ok := categoryOn(&f.root, segs); ok && c.unloaded() { if c, ok := categoryOn(&f.root, segs); ok && c.unloaded() {
if err := loadReached(&f.root, f.manifestReads, nil, []categoryAt{c}); err != nil { if err := loadReached(&f.root, nil, []categoryAt{c}); err != nil {
return "", nil, fmt.Errorf("fejkdata: %w", loadError{err}) return "", nil, fmt.Errorf("fejkdata: %w", loadError{err})
} }
} }
@@ -105,9 +105,8 @@ func needs(root *folder, s categorySite) []categoryAt {
// loadReached loads the unloaded categories among wanted and every unloaded category they // loadReached loads the unloaded categories among wanted and every unloaded category they
// or sites need, then runs sites and all it loaded through a whole load's pipeline. On an // or sites need, then runs sites and all it loaded through a whole load's pipeline. On an
// error it puts back what it loaded, so the next call fails the same way, and notes which // error it puts back what it loaded, so the next call fails the same way.
// namespace of reads, every namespace the loaded manifests read, nothing in root provides. func loadReached(root *folder, sites []categorySite, wanted []categoryAt) error {
func loadReached(root *folder, reads []string, sites []categorySite, wanted []categoryAt) error {
queue := wanted queue := wanted
for _, s := range sites { for _, s := range sites {
queue = append(queue, needs(root, s)...) queue = append(queue, needs(root, s)...)
@@ -119,9 +118,8 @@ func loadReached(root *folder, reads []string, sites []categorySite, wanted []ca
} }
if err != nil { if err != nil {
b.putBack() b.putBack()
return withMissingModules(err, root, reads)
} }
return nil return err
} }
// batch is the categories one loadReached loaded, each beside the entry it was loaded from. // batch is the categories one loadReached loaded, each beside the entry it was loaded from.
+4 -5
View File
@@ -36,7 +36,7 @@ func (f *Generator) NewTemplate(input string) (*Template, error) {
} }
f.mu.Lock() f.mu.Lock()
defer f.mu.Unlock() defer f.mu.Unlock()
if err := loadInline(&f.root, f.manifestReads, n, "template", false); err != nil { if err := loadInline(&f.root, n, "template", false); err != nil {
return nil, f.noDataNote(fmt.Errorf("fejkdata: %w", err)) return nil, f.noDataNote(fmt.Errorf("fejkdata: %w", err))
} }
return &Template{g: f, n: n}, nil return &Template{g: f, n: n}, nil
@@ -84,14 +84,13 @@ func inputValue(input string) jsonvalue.Value {
return raw return raw
} }
// loadInline loads the unloaded categories n reads, then runs n through the pipeline against root; // loadInline loads the unloaded categories n reads, then runs n through the pipeline against root.
// reads is every namespace the loaded manifests read. func loadInline(root *folder, n node, label string, typedByGo bool) error {
func loadInline(root *folder, reads []string, n node, label string, typedByGo bool) error {
nodes := inlineNodes(n, label) nodes := inlineNodes(n, label)
if err := refuseFolderRefs(nodes); err != nil { if err := refuseFolderRefs(nodes); err != nil {
return err return err
} }
if err := loadReached(root, reads, nil, referenced(root, nil, nodes)); err != nil { if err := loadReached(root, nil, referenced(root, nil, nodes)); err != nil {
return loadError{err} return loadError{err}
} }
return pipeline{ return pipeline{
-21
View File
@@ -121,27 +121,6 @@ type noEntry struct{ seg string }
func (e noEntry) Error() string { return fmt.Sprintf("no entry %q", e.seg) } func (e noEntry) Error() string { return fmt.Sprintf("no entry %q", e.seg) }
// withMissingModules adds to err, where a path names nothing, each namespace of reads that
// nothing in root provides, as the modules a loaded manifest reads and none loaded; or else
// that no loaded manifest names a module to add, the mistake being the path's own.
func withMissingModules(err error, root *folder, reads []string) error {
var ne noEntry
if !errors.As(err, &ne) {
return err
}
var missing []string
for _, namespace := range reads {
segs := strings.Split(namespace, ".")
if _, i := folderAt(root, segs); i < len(segs) {
missing = append(missing, namespace)
}
}
if len(missing) == 0 {
return fmt.Errorf("%w, and no loaded manifest names a module to add", err)
}
return fmt.Errorf("%w; is a module missing? The loaded modules read %s, which nothing loaded provides", err, strings.Join(missing, ", "))
}
// noDataNote adds to err, where a segment names nothing and f holds no data at all, that no // noDataNote adds to err, where a segment names nothing and f holds no data at all, that no
// data is loaded. // data is loaded.
func (f *Generator) noDataNote(err error) error { func (f *Generator) noDataNote(err error) error {
+1 -1
View File
@@ -206,7 +206,7 @@ func TestNewErrorPathIsCanonical(t *testing.T) {
{ {
"bad reference reached through another reference", "bad reference reached through another reference",
map[string]string{"a": `"{/b}"`, "b": `"{/nope}"`}, map[string]string{"a": `"{/b}"`, "b": `"{/nope}"`},
`fejkdata: b: reference {/nope}: no entry "nope", and no loaded manifest names a module to add`, `fejkdata: b: reference {/nope}: no entry "nope"`,
}, },
} }
for _, c := range cases { for _, c := range cases {
+18 -92
View File
@@ -1,7 +1,6 @@
package fejkdata package fejkdata
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -110,7 +109,7 @@ func loadedCategories(f *Generator) []string {
// REGENERATE=1 rewrites the manifests. // REGENERATE=1 rewrites the manifests.
func TestShippedManifestsAreCurrent(t *testing.T) { func TestShippedManifestsAreCurrent(t *testing.T) {
modules := shippedModules(t) modules := shippedModules(t)
manifests, err := shippedManifests(newShippedWhole(t), modules) manifests, _, err := shippedManifests(newShippedWhole(t), modules)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -133,8 +132,9 @@ func TestShippedManifestsAreCurrent(t *testing.T) {
} }
// shippedManifests is each module's manifest, read from whole's tree: its index, one category a // shippedManifests is each module's manifest, read from whole's tree: its index, one category a
// line, and every other module it reads, directly or through another. // line, and its namespace; and the namespace of every other module each reads, directly or
func shippedManifests(whole *Generator, modules []shippedModule) ([][]byte, error) { // through another.
func shippedManifests(whole *Generator, modules []shippedModule) ([][]byte, [][]string, error) {
owner := func(path string) int { owner := func(path string) int {
return slices.IndexFunc(modules, func(m shippedModule) bool { return m.holds(path) }) return slices.IndexFunc(modules, func(m shippedModule) bool { return m.holds(path) })
} }
@@ -143,11 +143,11 @@ func shippedManifests(whole *Generator, modules []shippedModule) ([][]byte, erro
for _, s := range categorySites(&whole.root) { for _, s := range categorySites(&whole.root) {
i := owner(s.path) i := owner(s.path)
if i < 0 { if i < 0 {
return nil, fmt.Errorf("no shipped module holds %s", s.path) return nil, nil, fmt.Errorf("no shipped module holds %s", s.path)
} }
line, err := json.Marshal(indexEntry(s)) line, err := json.Marshal(indexEntry(s))
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
key, _ := json.Marshal(s.path) key, _ := json.Marshal(s.path)
index[i] = append(index[i], "\n\t\t"+string(key)+": "+string(line)) index[i] = append(index[i], "\n\t\t"+string(key)+": "+string(line))
@@ -158,18 +158,13 @@ func shippedManifests(whole *Generator, modules []shippedModule) ([][]byte, erro
} }
} }
out := make([][]byte, len(modules)) out := make([][]byte, len(modules))
reads := make([][]string, len(modules))
for i := range modules { for i := range modules {
var b bytes.Buffer
namespace, _ := json.Marshal(modules[i].namespace) namespace, _ := json.Marshal(modules[i].namespace)
b.WriteString("{\n\t\"index\": {" + strings.Join(index[i], ",") + "\n\t},\n\t\"namespace\": " + string(namespace)) out[i] = []byte("{\n\t\"index\": {" + strings.Join(index[i], ",") + "\n\t},\n\t\"namespace\": " + string(namespace) + "\n}\n")
if reads := readsOf(i, direct, modules); len(reads) > 0 { reads[i] = readsOf(i, direct, modules)
line, _ := json.Marshal(reads)
b.WriteString(",\n\t\"reads\": " + string(line))
}
b.WriteString("\n}\n")
out[i] = b.Bytes()
} }
return out, nil return out, reads, nil
} }
// readsOf is the namespace of every module module i reads, directly or through another. // readsOf is the namespace of every module module i reads, directly or through another.
@@ -205,13 +200,17 @@ func shippedIndex(t *testing.T) map[string]datafiles.IndexEntry {
func TestEachShippedModuleRendersBesideTheModulesItReads(t *testing.T) { func TestEachShippedModuleRendersBesideTheModulesItReads(t *testing.T) {
modules := shippedModules(t) modules := shippedModules(t)
for _, m := range modules { _, reads, err := shippedManifests(newShippedWhole(t), modules)
if err != nil {
t.Fatal(err)
}
for i, m := range modules {
manifest, err := datafiles.FS(m.fsys).Manifest() manifest, err := datafiles.FS(m.fsys).Manifest()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
set := []fs.FS{m.fsys} set := []fs.FS{m.fsys}
for _, read := range manifest.Reads { for _, read := range reads[i] {
i := slices.IndexFunc(modules, func(r shippedModule) bool { return r.namespace == read }) i := slices.IndexFunc(modules, func(r shippedModule) bool { return r.namespace == read })
if i < 0 { if i < 0 {
t.Fatalf("%s reads %s, which no shipped module is", m.dir, read) t.Fatalf("%s reads %s, which no shipped module is", m.dir, read)
@@ -230,83 +229,12 @@ func TestEachShippedModuleRendersBesideTheModulesItReads(t *testing.T) {
} }
} }
func TestAReadNoModuleProvidesNamesTheReadersModules(t *testing.T) {
f, err := New(WithDataFS(svse.FS))
if err != nil {
t.Fatalf("New(WithDataFS(svse.FS)) = %v", err)
}
if _, err := f.Fake("sv_SE.address"); err == nil || !strings.Contains(err.Error(), "is a module missing? The loaded modules read geo.SE, which nothing loaded provides") {
t.Errorf(`Fake("sv_SE.address") = %v, want the module it reads named`, err)
}
for name, opt := range map[string]Option{
"no manifest": WithDataPath(writeData(t, map[string]string{"x": `"{/y}"`})),
"no reads in one": WithDataFS(fstest.MapFS{".fejkdata.json": {Data: []byte(`{"index": {"x": {"paths": [""]}}}`)}, "x.json": {Data: []byte(`"{/y}"`)}}),
} {
f, err := New(opt)
if err == nil {
_, err = f.Fake("x")
}
if err == nil || !strings.Contains(err.Error(), "no loaded manifest names a module to add") {
t.Errorf("%s: x = %v, want it said that no loaded module names one", name, err)
}
}
}
func TestADataPathManifestNamesWhatItReads(t *testing.T) {
dir := writeFiles(t, map[string]string{
".fejkdata.json": `{"reads": ["places"]}`,
"x.json": `"{/place}"`,
})
if _, err := New(WithDataPath(dir)); err == nil || !strings.Contains(err.Error(), "is a module missing? The loaded modules read places, which nothing loaded provides") {
t.Errorf("New = %v, want the module x reads named", err)
}
}
func TestAnIndexedModuleNamesWhatItReads(t *testing.T) {
f, err := New(WithDataFS(fstest.MapFS{
".fejkdata.json": {Data: []byte(`{"index": {"x": {"paths": [""]}}, "reads": ["places"]}`)},
"x.json": {Data: []byte(`"{/place}"`)},
}))
if err != nil {
t.Fatalf("New = %v", err)
}
const note = "is a module missing? The loaded modules read places, which nothing loaded provides"
for name, reach := range map[string]func() error{
"Fake": func() error { _, err := f.Fake("x"); return err },
"FakeTemplate": func() error { _, err := f.FakeTemplate("{/x}"); return err },
"FakeStruct": func() error {
return f.FakeStruct(&struct {
A string `fake:"/x"`
}{})
},
} {
if err := reach(); err == nil || strings.Count(err.Error(), note) != 1 {
t.Errorf("%s reaching x = %v, want the module x reads named once", name, err)
}
}
if _, err := f.FakeTemplate("{/nope}"); err == nil || strings.Contains(err.Error(), "module") {
t.Errorf("FakeTemplate({/nope}) = %v, want no module named: no load failed", err)
}
}
func TestATypoUnderLoadedModulesNamesNoModule(t *testing.T) {
for name, opts := range map[string][]Option{
"every shipped module": {withShipped()},
"a stand-in providing what a module reads": {WithDataFS(svse.FS, fstest.MapFS{"geo/SE/address.json": {Data: []byte(`"x"`)}})},
} {
_, err := New(append(opts, WithDataPath(writeData(t, map[string]string{"x": `"{/sv_SE.persn}"`})))...)
if err == nil || !strings.HasSuffix(err.Error(), `no entry "persn", and no loaded manifest names a module to add`) {
t.Errorf("%s: New = %v, want the typo reported with no module to add", name, err)
}
}
}
func TestAMountedModuleNamesItsFilesByItsNamespace(t *testing.T) { func TestAMountedModuleNamesItsFilesByItsNamespace(t *testing.T) {
_, err := New(WithDataFS(fstest.MapFS{ _, err := New(WithDataFS(fstest.MapFS{
".fejkdata.json": {Data: []byte(`{"namespace": "my.data"}`)}, ".fejkdata.json": {Data: []byte(`{"namespace": "my.data"}`)},
"word.json": {Data: []byte(`{`)}, "word.json": {Data: []byte(`{`)},
})) }))
if err == nil || !strings.Contains(err.Error(), "my/data/word.json") { if err == nil || !strings.Contains(err.Error(), "my.data (word.json)") {
t.Errorf("New = %v, want the broken file named under its namespace", err) t.Errorf("New = %v, want the broken file named under its namespace", err)
} }
} }
@@ -505,9 +433,7 @@ func TestABrokenManifestFailsNewNamingEveryMistake(t *testing.T) {
`{"index": {"a": {"paths": ["", "x", "x"]}}}`: {`paths item 3 repeats "x"`}, `{"index": {"a": {"paths": ["", "x", "x"]}}}`: {`paths item 3 repeats "x"`},
`{"index": {"a": {"paths": [""], "parent": "b"}}}`: {`parent "b" names no entry`}, `{"index": {"a": {"paths": [""], "parent": "b"}}}`: {`parent "b" names no entry`},
`{"index": {"a": {"paths": [""], "reads": ["b"]}}}`: {`unknown key "reads"`}, `{"index": {"a": {"paths": [""], "reads": ["b"]}}}`: {`unknown key "reads"`},
`{"reads": "b"}`: {"reads must be a list, not a string"}, `{"reads": ["b"]}`: {`unknown key "reads"; a manifest holds index and namespace`},
`{"reads": ["", 3]}`: {"reads item 1:", "reads item 2 must be a string, not a number"},
`{"reads": ["a", "a"]}`: {`reads item 2 repeats "a"`},
`{"namespace": 5}`: {"namespace must be a string, not a number"}, `{"namespace": 5}`: {"namespace must be a string, not a number"},
`{"namespace": "a..b"}`: {`namespace "a..b"`}, `{"namespace": "a..b"}`: {`namespace "a..b"`},
`{"index": {"b.x": {"paths": [""]}}, "namespace": "a"}`: {`index entry "b.x" lies outside the namespace "a"`}, `{"index": {"b.x": {"paths": [""]}}, "namespace": "a"}`: {`index entry "b.x" lies outside the namespace "a"`},
+4 -5
View File
@@ -55,7 +55,7 @@ func (f *Generator) structShapeOf(t reflect.Type) (*structShape, error) {
if label == "" { if label == "" {
label = "struct" label = "struct"
} }
sc := &structCompile{root: &f.root, reads: f.manifestReads, visiting: map[reflect.Type]bool{}, structsLeft: maxStructs} sc := &structCompile{root: &f.root, visiting: map[reflect.Type]bool{}, structsLeft: maxStructs}
shape, err := sc.compileShape(t, label) shape, err := sc.compileShape(t, label)
if f.structs == nil { if f.structs == nil {
f.structs = map[reflect.Type]structResult{} f.structs = map[reflect.Type]structResult{}
@@ -85,7 +85,6 @@ func (s *structShape) empty() bool { return s.record == nil && len(s.nested) ==
// rather than filled without end, and how many more structs it may walk. // rather than filled without end, and how many more structs it may walk.
type structCompile struct { type structCompile struct {
root *folder root *folder
reads []string
visiting map[reflect.Type]bool visiting map[reflect.Type]bool
structsLeft int structsLeft int
} }
@@ -112,7 +111,7 @@ func (sc *structCompile) compileShape(t reflect.Type, label string) (*structShap
return nil, err return nil, err
} }
if len(c.tags) > 0 { if len(c.tags) > 0 {
if err := c.shape.compileRecord(sc.root, sc.reads, t, label, c.tags); err != nil { if err := c.shape.compileRecord(sc.root, t, label, c.tags); err != nil {
return nil, err return nil, err
} }
} }
@@ -256,13 +255,13 @@ func checkTaggedType(sf reflect.StructField) error {
// compileRecord compiles the tagged fields of t as one record, and proves each column holds // compileRecord compiles the tagged fields of t as one record, and proves each column holds
// only what its field's Go type can. // only what its field's Go type can.
func (s *structShape) compileRecord(root *folder, reads []string, t reflect.Type, label string, tags map[string]jsonvalue.Value) error { func (s *structShape) compileRecord(root *folder, t reflect.Type, label string, tags map[string]jsonvalue.Value) error {
tags["format"] = jsonvalue.String("") tags["format"] = jsonvalue.String("")
n, err := compileFrom(jsonvalue.Object(tags), atGoTop) n, err := compileFrom(jsonvalue.Object(tags), atGoTop)
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", label, err) return fmt.Errorf("%s: %w", label, err)
} }
if err := loadInline(root, reads, n, label, true); err != nil { if err := loadInline(root, n, label, true); err != nil {
return err return err
} }
record, err := recordOf(n) record, err := recordOf(n)