1353 lines
37 KiB
Go
1353 lines
37 KiB
Go
package topdata
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestUnsupportedObjectKeysAreSorted(t *testing.T) {
|
|
obj := map[string]any{
|
|
"row": map[string]any{},
|
|
"when_present": []any{},
|
|
"aaa": true,
|
|
}
|
|
|
|
got := unsupportedObjectKeys(
|
|
obj,
|
|
"row",
|
|
"require_present",
|
|
"any_present",
|
|
"unless_present",
|
|
)
|
|
want := []string{"aaa", "when_present"}
|
|
if !slices.Equal(got, want) {
|
|
t.Fatalf("unsupportedObjectKeys() = %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestUnsupportedObjectKeysErrorNamesObjectAndContract(t *testing.T) {
|
|
err := unsupportedObjectKeysError(
|
|
"global injection 10",
|
|
map[string]any{"row": map[string]any{}, "when_present": []any{}},
|
|
"row",
|
|
"require_present",
|
|
"any_present",
|
|
"unless_present",
|
|
)
|
|
if err == nil {
|
|
t.Fatal("expected unsupported-property error")
|
|
}
|
|
for _, want := range []string{
|
|
"global injection 10",
|
|
`"when_present"`,
|
|
"row",
|
|
"require_present",
|
|
"any_present",
|
|
"unless_present",
|
|
} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Fatalf("error %q does not contain %q", err, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseGlobalConditionGroups(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
injection map[string]any
|
|
want globalConditionGroups
|
|
wantError string
|
|
}{
|
|
{
|
|
name: "all groups",
|
|
injection: map[string]any{
|
|
"require_present": []any{
|
|
map[string]any{"field": "FeatIndex", "id": "feat:required"},
|
|
},
|
|
"any_present": []any{
|
|
map[string]any{"field": "FeatIndex", "id": "feat:first"},
|
|
map[string]any{"field": "FeatIndex", "id": "feat:second"},
|
|
},
|
|
"unless_present": []any{
|
|
map[string]any{"field": "FeatIndex", "id": "feat:blocked"},
|
|
},
|
|
},
|
|
want: globalConditionGroups{
|
|
RequirePresent: []globalCondition{
|
|
{Field: "FeatIndex", ID: "feat:required"},
|
|
},
|
|
AnyPresent: []globalCondition{
|
|
{Field: "FeatIndex", ID: "feat:first"},
|
|
{Field: "FeatIndex", ID: "feat:second"},
|
|
},
|
|
UnlessPresent: []globalCondition{
|
|
{Field: "FeatIndex", ID: "feat:blocked"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "empty existing groups remain valid",
|
|
injection: map[string]any{
|
|
"require_present": []any{},
|
|
"unless_present": []any{},
|
|
},
|
|
want: globalConditionGroups{
|
|
RequirePresent: []globalCondition{},
|
|
UnlessPresent: []globalCondition{},
|
|
},
|
|
},
|
|
{
|
|
name: "require present null is rejected",
|
|
injection: map[string]any{
|
|
"require_present": nil,
|
|
},
|
|
wantError: "require_present must be an array",
|
|
},
|
|
{
|
|
name: "unless present null is rejected",
|
|
injection: map[string]any{
|
|
"unless_present": nil,
|
|
},
|
|
wantError: "unless_present must be an array",
|
|
},
|
|
{
|
|
name: "any present must be array",
|
|
injection: map[string]any{
|
|
"any_present": "feat:first",
|
|
},
|
|
wantError: "any_present must be an array",
|
|
},
|
|
{
|
|
name: "any present must not be empty",
|
|
injection: map[string]any{
|
|
"any_present": []any{},
|
|
},
|
|
wantError: "any_present must contain at least one condition",
|
|
},
|
|
{
|
|
name: "condition is closed",
|
|
injection: map[string]any{
|
|
"require_present": []any{
|
|
map[string]any{
|
|
"field": "FeatIndex",
|
|
"id": "feat:required",
|
|
"typo": true,
|
|
},
|
|
},
|
|
},
|
|
wantError: `require_present[0] contains unsupported key "typo"`,
|
|
},
|
|
{
|
|
name: "field is required",
|
|
injection: map[string]any{
|
|
"require_present": []any{
|
|
map[string]any{"id": "feat:required"},
|
|
},
|
|
},
|
|
wantError: "require_present[0].field is required",
|
|
},
|
|
{
|
|
name: "id is required",
|
|
injection: map[string]any{
|
|
"require_present": []any{
|
|
map[string]any{"field": "FeatIndex"},
|
|
},
|
|
},
|
|
wantError: "require_present[0].id is required",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := parseGlobalConditionGroups(tc.injection)
|
|
if tc.wantError != "" {
|
|
if err == nil {
|
|
t.Fatalf("parseGlobalConditionGroups() error = nil, want %q", tc.wantError)
|
|
}
|
|
if !strings.Contains(err.Error(), tc.wantError) {
|
|
t.Fatalf("parseGlobalConditionGroups() error = %q, want contains %q", err, tc.wantError)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("parseGlobalConditionGroups() error = %v", err)
|
|
}
|
|
if !reflect.DeepEqual(got, tc.want) {
|
|
t.Fatalf("parseGlobalConditionGroups() = %#v, want %#v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("absent groups remain valid", func(t *testing.T) {
|
|
got, err := parseGlobalConditionGroups(map[string]any{})
|
|
if err != nil {
|
|
t.Fatalf("parseGlobalConditionGroups() error = %v", err)
|
|
}
|
|
if !reflect.DeepEqual(got, globalConditionGroups{}) {
|
|
t.Fatalf("parseGlobalConditionGroups() = %#v, want zero value", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGlobalConditionGroupsMatch(t *testing.T) {
|
|
rowsWithoutBlocked := []map[string]any{
|
|
{"FeatIndex": map[string]any{"id": "feat:required"}},
|
|
{"FeatIndex": map[string]any{"id": "feat:second"}},
|
|
}
|
|
rowsWithBlocked := append([]map[string]any(nil), rowsWithoutBlocked...)
|
|
rowsWithBlocked = append(rowsWithBlocked, map[string]any{"FeatIndex": map[string]any{"id": "feat:blocked"}})
|
|
|
|
if !globalConditionGroupsMatch(globalConditionGroups{
|
|
RequirePresent: []globalCondition{
|
|
{Field: "FeatIndex", ID: "feat:required"},
|
|
},
|
|
AnyPresent: []globalCondition{
|
|
{Field: "FeatIndex", ID: "feat:first"},
|
|
{Field: "FeatIndex", ID: "feat:second"},
|
|
},
|
|
}, rowsWithoutBlocked) {
|
|
t.Fatal("expected required + later any_present alternative to match")
|
|
}
|
|
|
|
if globalConditionGroupsMatch(globalConditionGroups{
|
|
AnyPresent: []globalCondition{
|
|
{Field: "FeatIndex", ID: "feat:missing"},
|
|
},
|
|
}, rowsWithoutBlocked) {
|
|
t.Fatal("expected missing any_present alternatives to reject")
|
|
}
|
|
|
|
if globalConditionGroupsMatch(globalConditionGroups{
|
|
UnlessPresent: []globalCondition{
|
|
{Field: "FeatIndex", ID: "feat:blocked"},
|
|
},
|
|
}, rowsWithBlocked) {
|
|
t.Fatal("expected unless_present match to reject")
|
|
}
|
|
}
|
|
|
|
func TestValidateProjectRejectsUnsupportedCanonicalJSON(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setup func(t *testing.T, root string)
|
|
fragments []string
|
|
}{
|
|
{
|
|
name: "base root",
|
|
setup: func(t *testing.T, root string) {
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeCanonicalSpellsBase(t, root, `{
|
|
"output": "spells.2da",
|
|
"columns": ["Label"],
|
|
"rows": [
|
|
{"id": 0, "key": "spells:acid_fog", "Label": "AcidFog"}
|
|
],
|
|
"typo": true
|
|
}`+"\n")
|
|
},
|
|
fragments: []string{"base.json root", "typo", "unsupported"},
|
|
},
|
|
{
|
|
name: "plain rows root",
|
|
setup: func(t *testing.T, root string) {
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "categories.json"), `{
|
|
"output": "categories.2da",
|
|
"columns": ["Category"],
|
|
"rows": [
|
|
{"id": 0, "key": "categories:harmful", "Category": "Harmful"}
|
|
],
|
|
"typo": true
|
|
}`+"\n")
|
|
},
|
|
fragments: []string{"plain rows root", "typo", "unsupported"},
|
|
},
|
|
{
|
|
name: "module root",
|
|
setup: func(t *testing.T, root string) {
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeCanonicalSpellsDataset(t, root)
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "modules", "entry.json"), `{
|
|
"entries": {
|
|
"spells:burning_hands": {"Label": "BurningHands"}
|
|
},
|
|
"typo": true
|
|
}`+"\n")
|
|
},
|
|
fragments: []string{"module root", "typo", "unsupported"},
|
|
},
|
|
{
|
|
name: "global root",
|
|
setup: func(t *testing.T, root string) {
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeCanonicalSpellsDataset(t, root)
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), `{
|
|
"position": "append",
|
|
"typo": true
|
|
}`+"\n")
|
|
},
|
|
fragments: []string{"global.json root", "typo", "unsupported"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
tc.setup(t, root)
|
|
|
|
report := ValidateProject(testProject(root))
|
|
if !report.HasErrors() {
|
|
t.Fatalf("expected canonical unsupported-key validation error, got:\n%s", diagnosticsText(report.Diagnostics))
|
|
}
|
|
assertDiagnosticTextContainsAll(t, report.Diagnostics, tc.fragments...)
|
|
})
|
|
}
|
|
}
|
|
|
|
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
|
|
globalJSON string
|
|
fragments []string
|
|
}{
|
|
{
|
|
name: "injection object is closed",
|
|
globalJSON: `{
|
|
"injections": [
|
|
{
|
|
"row": {},
|
|
"when_present": []
|
|
}
|
|
]
|
|
}` + "\n",
|
|
fragments: []string{"global injection 0", "when_present", "unsupported"},
|
|
},
|
|
{
|
|
name: "condition object is closed",
|
|
globalJSON: `{
|
|
"injections": [
|
|
{
|
|
"row": {},
|
|
"require_present": [
|
|
{"field": "FeatIndex", "id": "feat:x", "typo": true}
|
|
]
|
|
}
|
|
]
|
|
}` + "\n",
|
|
fragments: []string{"global injection 0 require_present[0]", "typo", "unsupported"},
|
|
},
|
|
{
|
|
name: "default rule is closed",
|
|
globalJSON: `{
|
|
"defaults": [
|
|
{
|
|
"match": "all",
|
|
"values": {},
|
|
"typo": true
|
|
}
|
|
]
|
|
}` + "\n",
|
|
fragments: []string{"global default 0", "typo", "unsupported"},
|
|
},
|
|
{
|
|
name: "default match object is closed",
|
|
globalJSON: `{
|
|
"defaults": [
|
|
{
|
|
"match": {"source": "entries", "typo": true},
|
|
"values": {}
|
|
}
|
|
]
|
|
}` + "\n",
|
|
fragments: []string{"global default 0 match", "typo", "unsupported"},
|
|
},
|
|
{
|
|
name: "default format object is closed",
|
|
globalJSON: `{
|
|
"defaults": [
|
|
{
|
|
"match": "all",
|
|
"values": {
|
|
"ImpactScript": {"format": "ss_{id}", "typo": true}
|
|
}
|
|
}
|
|
]
|
|
}` + "\n",
|
|
fragments: []string{"global default 0 values.ImpactScript", "typo", "unsupported"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeCanonicalSpellsDataset(t, root)
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), tc.globalJSON)
|
|
|
|
report := ValidateProject(testProject(root))
|
|
if !report.HasErrors() {
|
|
t.Fatalf("expected global unsupported-key validation error, got:\n%s", diagnosticsText(report.Diagnostics))
|
|
}
|
|
assertDiagnosticTextContainsAll(t, report.Diagnostics, tc.fragments...)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateProjectAcceptsAnyPresent(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeCanonicalSpellsDataset(t, root)
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), `{
|
|
"injections": [
|
|
{
|
|
"row": {"ImpactScript": "ss_new_spell"},
|
|
"any_present": [
|
|
{"field": "FeatIndex", "id": "feat:gate"}
|
|
]
|
|
}
|
|
]
|
|
}`+"\n")
|
|
|
|
report := ValidateProject(testProject(root))
|
|
if report.HasErrors() {
|
|
t.Fatalf("expected non-empty any_present to validate cleanly, got:\n%s", diagnosticsText(report.Diagnostics))
|
|
}
|
|
}
|
|
|
|
func TestValidateProjectRejectsInvalidAnyPresent(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
injection string
|
|
fragments []string
|
|
}{
|
|
{
|
|
name: "string",
|
|
injection: `{
|
|
"row": {},
|
|
"any_present": "feat:gate"
|
|
}`,
|
|
fragments: []string{"any_present", "array"},
|
|
},
|
|
{
|
|
name: "empty list",
|
|
injection: `{
|
|
"row": {},
|
|
"any_present": []
|
|
}`,
|
|
fragments: []string{"any_present", "at least one condition"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeCanonicalSpellsDataset(t, root)
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), `{
|
|
"injections": [
|
|
`+tc.injection+`
|
|
]
|
|
}`+"\n")
|
|
|
|
report := ValidateProject(testProject(root))
|
|
if !report.HasErrors() {
|
|
t.Fatalf("expected invalid any_present validation error, got:\n%s", diagnosticsText(report.Diagnostics))
|
|
}
|
|
assertDiagnosticTextContainsAll(t, report.Diagnostics, tc.fragments...)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateProjectReportsUnsupportedGlobalInjectionKeysAndInvalidAnyPresent(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeCanonicalSpellsDataset(t, root)
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), `{
|
|
"injections": [
|
|
{
|
|
"row": {},
|
|
"when_present": [],
|
|
"any_present": []
|
|
}
|
|
]
|
|
}`+"\n")
|
|
|
|
report := ValidateProject(testProject(root))
|
|
if !report.HasErrors() {
|
|
t.Fatalf("expected invalid global injection to fail validation, got:\n%s", diagnosticsText(report.Diagnostics))
|
|
}
|
|
assertDiagnosticTextContainsAll(t, report.Diagnostics,
|
|
`global injection 0 contains unsupported key "when_present"`,
|
|
"global injection 0 any_present must contain at least one condition",
|
|
)
|
|
}
|
|
|
|
func TestValidateProjectValidatesGlobalColumnsOnce(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeCanonicalSpellsDataset(t, root)
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), `{
|
|
"columns": "ImpactScript",
|
|
"entries": {},
|
|
"overrides": []
|
|
}`+"\n")
|
|
|
|
report := ValidateProject(testProject(root))
|
|
if !report.HasErrors() {
|
|
t.Fatalf("expected invalid global columns file to fail validation, got:\n%s", diagnosticsText(report.Diagnostics))
|
|
}
|
|
text := diagnosticsText(report.Diagnostics)
|
|
if got := strings.Count(text, "columns must be a JSON array when present"); got != 1 {
|
|
t.Fatalf("expected one global columns diagnostic, got %d:\n%s", got, text)
|
|
}
|
|
}
|
|
|
|
func TestValidateProjectValidatesEveryCanonicalModuleContainer(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeCanonicalRepadjustDataset(t, root)
|
|
modulePath := filepath.Join(root, "topdata", "data", "repadjust", "modules", "combined.json")
|
|
writeFile(t, modulePath, `{
|
|
"entries": {
|
|
"repadjust:burning_hands": {"Label": "BurningHands"}
|
|
},
|
|
"overrides": [
|
|
{"key": "repadjust:test", "WIKISUMMARY": "deprecated"}
|
|
]
|
|
}`+"\n")
|
|
|
|
report := ValidateProject(testProject(root))
|
|
if !report.HasErrors() {
|
|
t.Fatalf("expected combined entries+overrides module to validate every container, got:\n%s", diagnosticsText(report.Diagnostics))
|
|
}
|
|
assertDiagnosticTextContainsAll(t, report.Diagnostics, "deprecated wiki field", "WIKISUMMARY")
|
|
|
|
writeFile(t, modulePath, `{
|
|
"entries": {
|
|
"repadjust:burning_hands": {"Label": "BurningHands"}
|
|
},
|
|
"overrides": [
|
|
{"key": "repadjust:test", "Label": "FixedLabel"}
|
|
]
|
|
}`+"\n")
|
|
|
|
report = ValidateProject(testProject(root))
|
|
if report.HasErrors() {
|
|
t.Fatalf("expected fixed combined entries+overrides module to validate cleanly, got:\n%s", diagnosticsText(report.Diagnostics))
|
|
}
|
|
}
|
|
|
|
func TestValidateProjectAcceptsCanonicalJSONContracts(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setup func(t *testing.T, root string)
|
|
}{
|
|
{
|
|
name: "columns only module",
|
|
setup: func(t *testing.T, root string) {
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeCanonicalSpellsDataset(t, root)
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "modules", "columns.json"), `{
|
|
"columns": ["ImpactScript"]
|
|
}`+"\n")
|
|
},
|
|
},
|
|
{
|
|
name: "entries only module",
|
|
setup: func(t *testing.T, root string) {
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeCanonicalSpellsDataset(t, root)
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "modules", "entries.json"), `{
|
|
"entries": {
|
|
"spells:burning_hands": {"Label": "BurningHands"}
|
|
}
|
|
}`+"\n")
|
|
},
|
|
},
|
|
{
|
|
name: "overrides only module",
|
|
setup: func(t *testing.T, root string) {
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeCanonicalSpellsDataset(t, root)
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "modules", "overrides.json"), `{
|
|
"overrides": [
|
|
{"key": "spells:acid_fog", "ImpactScript": "ss_acid_fog"}
|
|
]
|
|
}`+"\n")
|
|
},
|
|
},
|
|
{
|
|
name: "rows module",
|
|
setup: func(t *testing.T, root string) {
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeCanonicalSpellsDataset(t, root)
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "modules", "rows.json"), `{
|
|
"rows": [
|
|
{"key": "spells:burning_hands", "Label": "BurningHands"}
|
|
]
|
|
}`+"\n")
|
|
},
|
|
},
|
|
{
|
|
name: "current base root",
|
|
setup: func(t *testing.T, root string) {
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeCanonicalSpellsBase(t, root, `{
|
|
"output": "spells.2da",
|
|
"columns": ["Label"],
|
|
"rows": [
|
|
{"id": 0, "key": "spells:acid_fog", "Label": "AcidFog"}
|
|
]
|
|
}`+"\n")
|
|
},
|
|
},
|
|
{
|
|
name: "loose plain rows root",
|
|
setup: func(t *testing.T, root string) {
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "ruleset.json"), `{
|
|
"columns": ["Name", "Value"],
|
|
"rows": [
|
|
{"id": 0, "Name": "TEST_RULE", "Value": "1"}
|
|
]
|
|
}`+"\n")
|
|
},
|
|
},
|
|
{
|
|
name: "plain rows root compare reference false",
|
|
setup: func(t *testing.T, root string) {
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "categories.json"), `{
|
|
"output": "categories.2da",
|
|
"compare_reference": false,
|
|
"columns": ["Category"],
|
|
"rows": [
|
|
{"id": 0, "key": "categories:harmful", "Category": "Harmful"}
|
|
]
|
|
}`+"\n")
|
|
},
|
|
},
|
|
{
|
|
name: "row like payload helpers remain valid",
|
|
setup: func(t *testing.T, root string) {
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeCanonicalSpellsBase(t, root, `{
|
|
"output": "spells.2da",
|
|
"columns": ["Name", "Parent", "ImpactScript"],
|
|
"rows": [
|
|
{
|
|
"id": 0,
|
|
"key": "spells:acid_fog",
|
|
"Parent": "spells:acid_fog",
|
|
"ImpactScript": "ss_acid_fog",
|
|
"inherit": {"from": "Parent", "fields": ["ImpactScript"]},
|
|
"_tlk": {"name": "Acid Fog"},
|
|
"meta": {}
|
|
}
|
|
]
|
|
}`+"\n")
|
|
},
|
|
},
|
|
{
|
|
name: "global injection row payload stays open",
|
|
setup: func(t *testing.T, root string) {
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeCanonicalSpellsDataset(t, root)
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), `{
|
|
"injections": [
|
|
{
|
|
"row": {"ImpactScript": "ss_new_spell"}
|
|
}
|
|
]
|
|
}`+"\n")
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
tc.setup(t, root)
|
|
|
|
report := ValidateProject(testProject(root))
|
|
if report.HasErrors() {
|
|
t.Fatalf("expected canonical contract to validate cleanly, got:\n%s", diagnosticsText(report.Diagnostics))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildNativeAnyPresent(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
rows string
|
|
wantInjected bool
|
|
}{
|
|
{
|
|
name: "first alternative",
|
|
rows: `[
|
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}
|
|
]`,
|
|
wantInjected: true,
|
|
},
|
|
{
|
|
name: "later alternative",
|
|
rows: `[
|
|
{"id": 0, "SkillLabel": "Tumble", "SkillIndex": {"id": "skills:tumble"}, "ClassSkill": "1"}
|
|
]`,
|
|
wantInjected: true,
|
|
},
|
|
{
|
|
name: "no alternatives",
|
|
rows: `[
|
|
{"id": 0, "SkillLabel": "Concentration", "SkillIndex": {"id": "skills:concentration"}, "ClassSkill": "1"}
|
|
]`,
|
|
wantInjected: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{
|
|
"injections": [
|
|
{
|
|
"row": {
|
|
"SkillLabel": "ProfessionFarmer",
|
|
"SkillIndex": {"id": "skills:profession_farmer"},
|
|
"ClassSkill": "1"
|
|
},
|
|
"any_present": [
|
|
{"field": "SkillIndex", "id": "skills:athletics"},
|
|
{"field": "SkillIndex", "id": "skills:tumble"}
|
|
]
|
|
}
|
|
]
|
|
}`+"\n")
|
|
|
|
proj := testProject(root)
|
|
proj.Config.TopData.ReferenceBuilder = ""
|
|
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
|
if err != nil {
|
|
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
|
}
|
|
|
|
table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da"))
|
|
if got := tableContainsCellValue(table, "SkillLabel", "ProfessionFarmer"); got != tc.wantInjected {
|
|
t.Fatalf("ProfessionFarmer presence = %v, want %v", got, tc.wantInjected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildNativeCombinesGlobalConditionGroups(t *testing.T) {
|
|
t.Run("require_present and any_present", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
rows string
|
|
wantInjected bool
|
|
}{
|
|
{
|
|
name: "both pass",
|
|
rows: `[
|
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"},
|
|
{"id": 1, "SkillLabel": "Tumble", "SkillIndex": {"id": "skills:tumble"}, "ClassSkill": "1"}
|
|
]`,
|
|
wantInjected: true,
|
|
},
|
|
{
|
|
name: "require_present fails",
|
|
rows: `[
|
|
{"id": 0, "SkillLabel": "Tumble", "SkillIndex": {"id": "skills:tumble"}, "ClassSkill": "1"}
|
|
]`,
|
|
wantInjected: false,
|
|
},
|
|
{
|
|
name: "any_present fails",
|
|
rows: `[
|
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}
|
|
]`,
|
|
wantInjected: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{
|
|
"injections": [
|
|
{
|
|
"row": {
|
|
"SkillLabel": "DualGate",
|
|
"SkillIndex": {"id": "skills:lore"},
|
|
"ClassSkill": "1"
|
|
},
|
|
"require_present": [
|
|
{"field": "SkillIndex", "id": "skills:athletics"}
|
|
],
|
|
"any_present": [
|
|
{"field": "SkillIndex", "id": "skills:tumble"}
|
|
]
|
|
}
|
|
]
|
|
}`+"\n")
|
|
|
|
proj := testProject(root)
|
|
proj.Config.TopData.ReferenceBuilder = ""
|
|
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
|
if err != nil {
|
|
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
|
}
|
|
|
|
table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da"))
|
|
if got := tableContainsCellValue(table, "SkillLabel", "DualGate"); got != tc.wantInjected {
|
|
t.Fatalf("DualGate presence = %v, want %v", got, tc.wantInjected)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("any_present and unless_present", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
rows string
|
|
wantInjected bool
|
|
}{
|
|
{
|
|
name: "both pass",
|
|
rows: `[
|
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}
|
|
]`,
|
|
wantInjected: true,
|
|
},
|
|
{
|
|
name: "any_present fails",
|
|
rows: `[
|
|
{"id": 0, "SkillLabel": "Concentration", "SkillIndex": {"id": "skills:concentration"}, "ClassSkill": "1"}
|
|
]`,
|
|
wantInjected: false,
|
|
},
|
|
{
|
|
name: "unless_present fails",
|
|
rows: `[
|
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"},
|
|
{"id": 1, "SkillLabel": "ProfessionFarmer", "SkillIndex": {"id": "skills:profession_farmer"}, "ClassSkill": "1"}
|
|
]`,
|
|
wantInjected: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{
|
|
"injections": [
|
|
{
|
|
"row": {
|
|
"SkillLabel": "NeedsAnyWithoutBlocked",
|
|
"SkillIndex": {"id": "skills:lore"},
|
|
"ClassSkill": "1"
|
|
},
|
|
"any_present": [
|
|
{"field": "SkillIndex", "id": "skills:athletics"}
|
|
],
|
|
"unless_present": [
|
|
{"field": "SkillIndex", "id": "skills:profession_farmer"}
|
|
]
|
|
}
|
|
]
|
|
}`+"\n")
|
|
|
|
proj := testProject(root)
|
|
proj.Config.TopData.ReferenceBuilder = ""
|
|
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
|
if err != nil {
|
|
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
|
}
|
|
|
|
table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da"))
|
|
if got := tableContainsCellValue(table, "SkillLabel", "NeedsAnyWithoutBlocked"); got != tc.wantInjected {
|
|
t.Fatalf("NeedsAnyWithoutBlocked presence = %v, want %v", got, tc.wantInjected)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("require_present keeps all semantics", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
rows string
|
|
wantInjected bool
|
|
}{
|
|
{
|
|
name: "all present",
|
|
rows: `[
|
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"},
|
|
{"id": 1, "SkillLabel": "Tumble", "SkillIndex": {"id": "skills:tumble"}, "ClassSkill": "1"}
|
|
]`,
|
|
wantInjected: true,
|
|
},
|
|
{
|
|
name: "missing one required row",
|
|
rows: `[
|
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}
|
|
]`,
|
|
wantInjected: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{
|
|
"injections": [
|
|
{
|
|
"row": {
|
|
"SkillLabel": "AllRequired",
|
|
"SkillIndex": {"id": "skills:lore"},
|
|
"ClassSkill": "1"
|
|
},
|
|
"require_present": [
|
|
{"field": "SkillIndex", "id": "skills:athletics"},
|
|
{"field": "SkillIndex", "id": "skills:tumble"}
|
|
]
|
|
}
|
|
]
|
|
}`+"\n")
|
|
|
|
proj := testProject(root)
|
|
proj.Config.TopData.ReferenceBuilder = ""
|
|
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
|
if err != nil {
|
|
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
|
}
|
|
|
|
table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da"))
|
|
if got := tableContainsCellValue(table, "SkillLabel", "AllRequired"); got != tc.wantInjected {
|
|
t.Fatalf("AllRequired presence = %v, want %v", got, tc.wantInjected)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("unless_present keeps none semantics", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
rows string
|
|
wantInjected bool
|
|
}{
|
|
{
|
|
name: "none present",
|
|
rows: `[
|
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}
|
|
]`,
|
|
wantInjected: true,
|
|
},
|
|
{
|
|
name: "one blocked row present",
|
|
rows: `[
|
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"},
|
|
{"id": 1, "SkillLabel": "ProfessionFarmer", "SkillIndex": {"id": "skills:profession_farmer"}, "ClassSkill": "1"}
|
|
]`,
|
|
wantInjected: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{
|
|
"injections": [
|
|
{
|
|
"row": {
|
|
"SkillLabel": "NoneBlocked",
|
|
"SkillIndex": {"id": "skills:lore"},
|
|
"ClassSkill": "1"
|
|
},
|
|
"unless_present": [
|
|
{"field": "SkillIndex", "id": "skills:profession_farmer"},
|
|
{"field": "SkillIndex", "id": "skills:stealth"}
|
|
]
|
|
}
|
|
]
|
|
}`+"\n")
|
|
|
|
proj := testProject(root)
|
|
proj.Config.TopData.ReferenceBuilder = ""
|
|
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
|
if err != nil {
|
|
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
|
}
|
|
|
|
table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da"))
|
|
if got := tableContainsCellValue(table, "SkillLabel", "NoneBlocked"); got != tc.wantInjected {
|
|
t.Fatalf("NoneBlocked presence = %v, want %v", got, tc.wantInjected)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBuildNativeUsesCurrentRowsForAnyPresent(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
writeCanonicalPlainSkillsFixture(t, root, `[
|
|
{"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}
|
|
]`, `{
|
|
"injections": [
|
|
{
|
|
"row": {
|
|
"SkillLabel": "ProfessionFarmer",
|
|
"SkillIndex": {"id": "skills:profession_farmer"},
|
|
"ClassSkill": "1"
|
|
}
|
|
},
|
|
{
|
|
"row": {
|
|
"SkillLabel": "Lore",
|
|
"SkillIndex": {"id": "skills:lore"},
|
|
"ClassSkill": "1"
|
|
},
|
|
"any_present": [
|
|
{"field": "SkillIndex", "id": "skills:profession_farmer"}
|
|
]
|
|
}
|
|
]
|
|
}`+"\n")
|
|
|
|
proj := testProject(root)
|
|
proj.Config.TopData.ReferenceBuilder = ""
|
|
result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
|
if err != nil {
|
|
t.Fatalf("BuildNativeWithOptions failed: %v", err)
|
|
}
|
|
|
|
table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da"))
|
|
for _, label := range []string{"ProfessionFarmer", "Lore"} {
|
|
if !tableContainsCellValue(table, "SkillLabel", label) {
|
|
t.Fatalf("expected %s injection to apply after earlier mutation", label)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildNativeRejectsUnsupportedGlobalControlSyntax(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
globalJSON string
|
|
fragments []string
|
|
}{
|
|
{
|
|
name: "when_present injection key",
|
|
globalJSON: `{
|
|
"injections": [
|
|
{
|
|
"row": {"ImpactScript": "ss_new_spell"},
|
|
"when_present": []
|
|
}
|
|
]
|
|
}` + "\n",
|
|
fragments: []string{"global injection 0", "when_present", "unsupported"},
|
|
},
|
|
{
|
|
name: "unknown condition key",
|
|
globalJSON: `{
|
|
"injections": [
|
|
{
|
|
"row": {"ImpactScript": "ss_new_spell"},
|
|
"require_present": [
|
|
{"field": "FeatIndex", "id": "feat:gate", "typo": true}
|
|
]
|
|
}
|
|
]
|
|
}` + "\n",
|
|
fragments: []string{"require_present[0]", "typo", "unsupported"},
|
|
},
|
|
{
|
|
name: "empty any_present",
|
|
globalJSON: `{
|
|
"injections": [
|
|
{
|
|
"row": {"ImpactScript": "ss_new_spell"},
|
|
"any_present": []
|
|
}
|
|
]
|
|
}` + "\n",
|
|
fragments: []string{"any_present", "at least one condition"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeCanonicalSpellsDataset(t, root)
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), tc.globalJSON)
|
|
|
|
proj := testProject(root)
|
|
proj.Config.TopData.ReferenceBuilder = ""
|
|
_, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil)
|
|
if err == nil {
|
|
t.Fatal("expected BuildNativeWithOptions to fail")
|
|
}
|
|
for _, fragment := range tc.fragments {
|
|
if !strings.Contains(err.Error(), fragment) {
|
|
t.Fatalf("error %q does not contain %q", err, fragment)
|
|
}
|
|
}
|
|
if _, statErr := os.Stat(filepath.Join(compiled2DAOutputDir(proj), "spells.2da")); !errors.Is(statErr, os.ErrNotExist) {
|
|
t.Fatalf("expected spells.2da to remain absent, got err %v", statErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildNativeRejectsUnsupportedCanonicalRoot(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
writeCanonicalContractScaffold(t, root)
|
|
writeCanonicalSpellsBase(t, root, `{
|
|
"output": "spells.2da",
|
|
"columns": ["Label"],
|
|
"rows": [
|
|
{"id": 0, "key": "spells:acid_fog", "Label": "AcidFog"}
|
|
],
|
|
"typo": true
|
|
}`+"\n")
|
|
|
|
proj := testProject(root)
|
|
proj.Config.TopData.ReferenceBuilder = ""
|
|
_, 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)
|
|
}
|
|
}
|
|
if _, statErr := os.Stat(filepath.Join(compiled2DAOutputDir(proj), "spells.2da")); !errors.Is(statErr, os.ErrNotExist) {
|
|
t.Fatalf("expected spells.2da to remain absent, got err %v", statErr)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func writeCanonicalContractScaffold(t *testing.T, root string) {
|
|
t.Helper()
|
|
mkdirAll(t, filepath.Join(root, "topdata", "data"))
|
|
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
|
}
|
|
|
|
func writeCanonicalSpellsDataset(t *testing.T, root string) {
|
|
t.Helper()
|
|
writeCanonicalSpellsBase(t, root, `{
|
|
"output": "spells.2da",
|
|
"columns": ["Label", "ImpactScript"],
|
|
"rows": [
|
|
{"id": 0, "key": "spells:acid_fog", "Label": "AcidFog", "ImpactScript": "ss_acid_fog"}
|
|
]
|
|
}`+"\n")
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "lock.json"), `{"spells:acid_fog":0}`+"\n")
|
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "spells", "modules"))
|
|
}
|
|
|
|
func writeCanonicalSpellsBase(t *testing.T, root string, content string) {
|
|
t.Helper()
|
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "spells"))
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "spells", "base.json"), content)
|
|
}
|
|
|
|
func writeCanonicalRepadjustDataset(t *testing.T, root string) {
|
|
t.Helper()
|
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust", "modules"))
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "repadjust", "base.json"), `{
|
|
"output": "repadjust.2da",
|
|
"columns": ["Label"],
|
|
"rows": [
|
|
{"id": 0, "key": "repadjust:test", "Label": "TestLabel"}
|
|
]
|
|
}`+"\n")
|
|
}
|
|
|
|
func writeCanonicalPlainSkillsFixture(t *testing.T, root, datasetRows, globalJSON string) {
|
|
t.Helper()
|
|
writeCanonicalContractScaffold(t, root)
|
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "skills"))
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "skills", "global.json"), globalJSON)
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "skills", "guard.json"), `{
|
|
"key": "classes/skills:guard",
|
|
"output": "cls_skill_guard.2da",
|
|
"columns": ["SkillLabel", "SkillIndex", "ClassSkill"],
|
|
"rows": `+datasetRows+`
|
|
}`+"\n")
|
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "skills"))
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "base.json"), `{
|
|
"output": "skills.2da",
|
|
"columns": ["Label"],
|
|
"rows": [
|
|
{"id": 1, "key": "skills:athletics", "Label": "Athletics"},
|
|
{"id": 2, "key": "skills:tumble", "Label": "Tumble"},
|
|
{"id": 3, "key": "skills:concentration", "Label": "Concentration"},
|
|
{"id": 4, "key": "skills:profession_farmer", "Label": "ProfessionFarmer"},
|
|
{"id": 5, "key": "skills:lore", "Label": "Lore"},
|
|
{"id": 6, "key": "skills:stealth", "Label": "Stealth"}
|
|
]
|
|
}`+"\n")
|
|
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "lock.json"), `{
|
|
"skills:athletics": 1,
|
|
"skills:tumble": 2,
|
|
"skills:concentration": 3,
|
|
"skills:profession_farmer": 4,
|
|
"skills:lore": 5,
|
|
"skills:stealth": 6
|
|
}`+"\n")
|
|
}
|
|
|
|
func assertDiagnosticTextContainsAll(t *testing.T, diags []Diagnostic, fragments ...string) {
|
|
t.Helper()
|
|
text := diagnosticsText(diags)
|
|
for _, fragment := range fragments {
|
|
if !strings.Contains(text, fragment) {
|
|
t.Fatalf("expected diagnostics to contain %q, got:\n%s", fragment, text)
|
|
}
|
|
}
|
|
}
|