fix(topdata): close invariant JSON roots
build-binaries / build-binaries (pull_request) Successful in 2m15s
test-image / build-image (pull_request) Successful in 37s
test / test (pull_request) Successful in 1m27s

This commit is contained in:
2026-06-25 23:10:39 +02:00
parent 14fd1c2b41
commit 75bfb9f042
4 changed files with 180 additions and 85 deletions
+130 -2
View File
@@ -1,6 +1,7 @@
package topdata
import (
"bytes"
"errors"
"os"
"path/filepath"
@@ -309,6 +310,59 @@ func TestValidateProjectRejectsUnsupportedCanonicalJSON(t *testing.T) {
}
}
func TestValidateProjectRejectsUnsupportedInvariantBaseRoots(t *testing.T) {
tests := []struct {
name string
dataset string
base string
lock string
}{
{
name: "feat",
dataset: "feat",
base: `{
"output": "feat.2da",
"columns": ["LABEL", "FEAT", "DESCRIPTION"],
"rows": [
{"id": 0, "key": "feat:test", "LABEL": "FEAT_TEST", "FEAT": "100", "DESCRIPTION": "200"}
],
"typo": true
}` + "\n",
lock: `{"feat:test":0}` + "\n",
},
{
name: "masterfeats",
dataset: "masterfeats",
base: `{
"output": "masterfeats.2da",
"columns": ["LABEL", "STRREF", "DESCRIPTION"],
"rows": [
{"id": 0, "key": "masterfeats:test", "LABEL": "Test", "STRREF": "100", "DESCRIPTION": "200"}
],
"typo": true
}` + "\n",
lock: `{"masterfeats:test":0}` + "\n",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
root := testProjectRoot(t)
writeCanonicalContractScaffold(t, root)
datasetDir := filepath.Join(root, "topdata", "data", tc.dataset)
mkdirAll(t, datasetDir)
writeFile(t, filepath.Join(datasetDir, "base.json"), tc.base)
writeFile(t, filepath.Join(datasetDir, "lock.json"), tc.lock)
report := ValidateProject(testProject(root))
if !report.HasErrors() {
t.Fatalf("expected unsupported invariant base-root key to fail validation, got:\n%s", diagnosticsText(report.Diagnostics))
}
assertDiagnosticTextContainsAll(t, report.Diagnostics, "base.json root", "typo", "unsupported")
})
}
}
func TestValidateProjectRejectsUnsupportedGlobalJSON(t *testing.T) {
tests := []struct {
name string
@@ -339,7 +393,7 @@ func TestValidateProjectRejectsUnsupportedGlobalJSON(t *testing.T) {
}
]
}` + "\n",
fragments: []string{"require_present[0]", "typo", "unsupported"},
fragments: []string{"global injection 0 require_present[0]", "typo", "unsupported"},
},
{
name: "default rule is closed",
@@ -483,7 +537,7 @@ func TestValidateProjectReportsUnsupportedGlobalInjectionKeysAndInvalidAnyPresen
}
assertDiagnosticTextContainsAll(t, report.Diagnostics,
`global injection 0 contains unsupported key "when_present"`,
"any_present must contain at least one condition",
"global injection 0 any_present must contain at least one condition",
)
}
@@ -1133,6 +1187,80 @@ func TestBuildNativeRejectsUnsupportedCanonicalRoot(t *testing.T) {
}
}
func TestBuildNativeRejectsUnsupportedInvariantBaseRootsWithoutChangingOutput(t *testing.T) {
tests := []struct {
name string
dataset string
outputName string
base string
lock string
}{
{
name: "feat",
dataset: "feat",
outputName: "feat.2da",
base: `{
"output": "feat.2da",
"columns": ["LABEL", "FEAT", "DESCRIPTION"],
"rows": [
{"id": 0, "key": "feat:test", "LABEL": "FEAT_TEST", "FEAT": "100", "DESCRIPTION": "200"}
],
"typo": true
}` + "\n",
lock: `{"feat:test":0}` + "\n",
},
{
name: "masterfeats",
dataset: "masterfeats",
outputName: "masterfeats.2da",
base: `{
"output": "masterfeats.2da",
"columns": ["LABEL", "STRREF", "DESCRIPTION"],
"rows": [
{"id": 0, "key": "masterfeats:test", "LABEL": "Test", "STRREF": "100", "DESCRIPTION": "200"}
],
"typo": true
}` + "\n",
lock: `{"masterfeats:test":0}` + "\n",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
root := testProjectRoot(t)
writeCanonicalContractScaffold(t, root)
datasetDir := filepath.Join(root, "topdata", "data", tc.dataset)
mkdirAll(t, datasetDir)
writeFile(t, filepath.Join(datasetDir, "base.json"), tc.base)
writeFile(t, filepath.Join(datasetDir, "lock.json"), tc.lock)
proj := testProject(root)
proj.Config.TopData.ReferenceBuilder = ""
outputPath := filepath.Join(compiled2DAOutputDir(proj), tc.outputName)
mkdirAll(t, filepath.Dir(outputPath))
sentinel := []byte("pre-existing output sentinel\n")
writeBytes(t, outputPath, sentinel)
_, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
if err == nil {
t.Fatal("expected BuildNativeWithOptions to fail")
}
for _, fragment := range []string{"base.json root", "typo", "unsupported"} {
if !strings.Contains(err.Error(), fragment) {
t.Fatalf("error %q does not contain %q", err, fragment)
}
}
got, readErr := os.ReadFile(outputPath)
if readErr != nil {
t.Fatalf("read preserved output: %v", readErr)
}
if !bytes.Equal(got, sentinel) {
t.Fatalf("output changed after invalid build error:\ngot: %q\nwant: %q", got, sentinel)
}
})
}
}
func tableContainsCellValue(table twoDATable, column, want string) bool {
for _, row := range table.rows {
if row[column] == want {