1349 lines
39 KiB
Go
1349 lines
39 KiB
Go
package project
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestLoadYAMLConfig(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
|
module:
|
|
name: Test Module
|
|
resref: testmod
|
|
paths:
|
|
source: src
|
|
assets: assets
|
|
build: output
|
|
haks:
|
|
- name: core
|
|
priority: 1
|
|
max_bytes: 1024
|
|
split: false
|
|
include:
|
|
- core/**
|
|
`)
|
|
|
|
proj, err := Load(root)
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
if proj.ConfigSource.Name != ConfigFile || proj.ConfigSource.Format != "yaml" || proj.ConfigSource.Legacy {
|
|
t.Fatalf("unexpected config source: %#v", proj.ConfigSource)
|
|
}
|
|
if got, want := proj.Config.Module.Name, "Test Module"; got != want {
|
|
t.Fatalf("expected module name %q, got %q", want, got)
|
|
}
|
|
if got, want := proj.BuildDir(), filepath.Join(root, "output"); got != want {
|
|
t.Fatalf("expected build dir %s, got %s", want, got)
|
|
}
|
|
}
|
|
|
|
func TestEffectiveConfigAppliesVisibleToolkitDefaults(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
|
module:
|
|
name: Test Module
|
|
resref: testmod
|
|
paths:
|
|
source: src
|
|
`)
|
|
|
|
proj, err := Load(root)
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
|
|
effective := proj.EffectiveConfig()
|
|
if got, want := effective.Paths.Build, "build"; got != want {
|
|
t.Fatalf("expected default build path %q, got %q", want, got)
|
|
}
|
|
if got, want := effective.Outputs.HAKManifest, "haks.json"; got != want {
|
|
t.Fatalf("expected default HAK manifest %q, got %q", want, got)
|
|
}
|
|
if got, want := strings.Join(effective.Extract.Archives, ","), "testmod.mod"; got != want {
|
|
t.Fatalf("expected default extract archives %q, got %q", want, got)
|
|
}
|
|
if effective.Extract.ConsumeArchives == nil || *effective.Extract.ConsumeArchives {
|
|
t.Fatalf("expected default extract consume_archives false, got %#v", effective.Extract.ConsumeArchives)
|
|
}
|
|
if got, want := effective.TopData.PackageHAK, "sow_top.hak"; got != want {
|
|
t.Fatalf("expected default topdata HAK %q, got %q", want, got)
|
|
}
|
|
if got, want := strings.Join(effective.Validation.BuiltinScriptPrefixes, ","), "ga_,gc_,gen_,gui_,nw_,nwg_,ta_,x0_,x1_,x2_,x3_"; got != want {
|
|
t.Fatalf("expected default built-in script prefixes %q, got %q", want, got)
|
|
}
|
|
if got := strings.Join(effective.Validation.RequiredFields["ifo"], ","); got != "Mod_Name" {
|
|
t.Fatalf("expected default IFO required fields, got %#v", effective.Validation.RequiredFields)
|
|
}
|
|
if got, want := strings.Join(effective.Music.ConvertExtensions, ","), ".flac,.m4a,.mp3,.ogg,.wav"; got != want {
|
|
t.Fatalf("expected default music convert extensions %q, got %q", want, got)
|
|
}
|
|
if prov := effective.Provenance["paths.build"]; prov.Source != "toolkit default" {
|
|
t.Fatalf("expected paths.build toolkit default provenance, got %#v", prov)
|
|
}
|
|
if prov := effective.Provenance["module.name"]; prov.Source != "yaml" {
|
|
t.Fatalf("expected module.name YAML provenance, got %#v", prov)
|
|
}
|
|
}
|
|
|
|
func TestEffectiveConfigHonorsValidationConfig(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
|
module:
|
|
name: Test Module
|
|
resref: testmod
|
|
paths:
|
|
source: src
|
|
validation:
|
|
builtin_script_prefixes:
|
|
- custom_
|
|
- NW_
|
|
required_fields:
|
|
ifo:
|
|
- Mod_Name
|
|
- Mod_Hak
|
|
`)
|
|
|
|
proj, err := Load(root)
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
got := strings.Join(proj.EffectiveConfig().Validation.BuiltinScriptPrefixes, ",")
|
|
if want := "custom_,nw_"; got != want {
|
|
t.Fatalf("expected configured validation prefixes %q, got %q", want, got)
|
|
}
|
|
if got := strings.Join(proj.EffectiveConfig().Validation.RequiredFields["ifo"], ","); got != "Mod_Hak,Mod_Name" {
|
|
t.Fatalf("expected configured required fields, got %#v", proj.EffectiveConfig().Validation.RequiredFields)
|
|
}
|
|
}
|
|
|
|
func TestScanUsesYAMLMusicConvertExtensionsWithinConfiguredDatasetRoots(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.MkdirAll(filepath.Join(root, "assets", "audio", "westgate"), 0o755); err != nil {
|
|
t.Fatalf("mkdir westgate audio: %v", err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Join(root, "assets", "audio", "other"), 0o755); err != nil {
|
|
t.Fatalf("mkdir other audio: %v", err)
|
|
}
|
|
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
|
module:
|
|
name: Test Module
|
|
resref: testmod
|
|
paths:
|
|
source: src
|
|
assets: assets
|
|
music:
|
|
defaults:
|
|
convert_extensions:
|
|
- .flac
|
|
datasets:
|
|
westgate_audio:
|
|
source: audio/westgate
|
|
convert_extensions:
|
|
- .aac
|
|
`)
|
|
writeProjectFile(t, filepath.Join(root, "assets", "audio", "westgate", "theme.aac"), "aac")
|
|
writeProjectFile(t, filepath.Join(root, "assets", "audio", "westgate", "theme.flac"), "flac")
|
|
writeProjectFile(t, filepath.Join(root, "assets", "audio", "other", "ignored.aac"), "aac")
|
|
|
|
proj, err := Load(root)
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
if err := proj.Scan(); err != nil {
|
|
t.Fatalf("Scan returned error: %v", err)
|
|
}
|
|
if !slices.Contains(proj.Inventory.AssetFiles, "audio/westgate/theme.aac") {
|
|
t.Fatalf("expected configured dataset AAC file to be discovered, got %#v", proj.Inventory.AssetFiles)
|
|
}
|
|
if !slices.Contains(proj.Inventory.AssetFiles, "audio/westgate/theme.flac") {
|
|
t.Fatalf("expected configured dataset FLAC file to be discovered, got %#v", proj.Inventory.AssetFiles)
|
|
}
|
|
if slices.Contains(proj.Inventory.AssetFiles, "audio/other/ignored.aac") {
|
|
t.Fatalf("did not expect non-dataset AAC file to be discovered, got %#v", proj.Inventory.AssetFiles)
|
|
}
|
|
}
|
|
|
|
func TestEffectiveConfigHonorsConfiguredOutputAndCacheFields(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
|
module:
|
|
name: Test Module
|
|
resref: testmod
|
|
paths:
|
|
build: output
|
|
cache: cache
|
|
outputs:
|
|
module_archive: modules/{module.resref}.mod
|
|
hak_manifest: manifests/haks.json
|
|
hak_archive: haks/{hak.name}.hak
|
|
scripts:
|
|
cache: "{paths.cache}/compiled"
|
|
topdata:
|
|
source: topdata
|
|
build: "{paths.cache}"
|
|
compiled_2da_dir: tables
|
|
compiled_tlk: dialog.tlk
|
|
package_hak: custom_top.hak
|
|
package_tlk: custom_dialog.tlk
|
|
wiki:
|
|
output_root: docs
|
|
pages_dir: pages
|
|
state_file: wiki-state.json
|
|
autogen:
|
|
cache:
|
|
root: "{paths.cache}/autogen"
|
|
`)
|
|
|
|
proj, err := Load(root)
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
|
|
if got, want := proj.ModuleArchivePath(), filepath.Join(root, "output", "modules", "testmod.mod"); got != want {
|
|
t.Fatalf("expected module archive path %s, got %s", want, got)
|
|
}
|
|
if got, want := proj.HAKManifestPath(), filepath.Join(root, "output", "manifests", "haks.json"); got != want {
|
|
t.Fatalf("expected HAK manifest path %s, got %s", want, got)
|
|
}
|
|
if got, want := proj.HAKArchivePath("core_01"), filepath.Join(root, "output", "haks", "core_01.hak"); got != want {
|
|
t.Fatalf("expected HAK archive path %s, got %s", want, got)
|
|
}
|
|
if got, want := proj.ScriptCacheDir(), filepath.Join(root, "cache", "compiled"); got != want {
|
|
t.Fatalf("expected script cache path %s, got %s", want, got)
|
|
}
|
|
if got, want := proj.TopDataCompiled2DADir(), filepath.Join(root, "cache", "tables"); got != want {
|
|
t.Fatalf("expected topdata 2da path %s, got %s", want, got)
|
|
}
|
|
if got, want := proj.TopDataWikiStatePath(), filepath.Join(root, "cache", "docs", "wiki-state.json"); got != want {
|
|
t.Fatalf("expected wiki state path %s, got %s", want, got)
|
|
}
|
|
if got, want := proj.AutogenCachePath("manifest.json"), filepath.Join(root, "cache", "autogen", "manifest.json"); got != want {
|
|
t.Fatalf("expected autogen cache path %s, got %s", want, got)
|
|
}
|
|
}
|
|
|
|
func TestEffectiveConfigHonorsConfiguredCompilerExtractWikiAndAutogenPolicy(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
|
module:
|
|
name: Test Module
|
|
resref: testmod
|
|
paths:
|
|
source: src
|
|
tools: custom-tools
|
|
scripts:
|
|
source_dir: module-scripts
|
|
compiler:
|
|
path: bin/compiler
|
|
search:
|
|
- "{paths.tools}/compiler"
|
|
env:
|
|
path: TEST_COMPILER
|
|
nwn_root:
|
|
- TEST_NWN_ROOT
|
|
nwn_user_directory:
|
|
- TEST_NWN_USER
|
|
extract:
|
|
layout: nwn_canonical_json
|
|
hak_discovery: configured_haks
|
|
cleanup_stale: false
|
|
merge:
|
|
gff_json:
|
|
- target: module/module.ifo.json
|
|
preserve_fields:
|
|
- Mod_Entry_Area
|
|
merge_lists:
|
|
- field: Mod_Area_list
|
|
key_field: Area_Name
|
|
strategy: preserve_existing_order_append_new
|
|
topdata:
|
|
wiki:
|
|
source: topdata/wiki
|
|
managed_namespaces:
|
|
- skills
|
|
renderer: nodebb_tiptap_html
|
|
link_strategy: preserve_westgate_wiki_links
|
|
namespaces_file: namespaces.yaml
|
|
tables_file: custom-tables.yaml
|
|
visibility_file: visibility.yaml
|
|
templates_dir: templates
|
|
manual_sections_dir: manual-sections
|
|
deploy_manifest: wiki-manifest.json
|
|
deploy_edit_summary: Custom summary
|
|
title_prefix_min_length: 4
|
|
status_listing_scope: namespace
|
|
stale_pages:
|
|
default: report
|
|
live_cleanup: archive
|
|
autogen:
|
|
cache:
|
|
root: "{paths.cache}/released"
|
|
max_age: 30m
|
|
refresh_env: TEST_AUTOGEN_REFRESH
|
|
release_source:
|
|
provider: gitea
|
|
server_url_env: TEST_ASSETS_SERVER
|
|
repo_env: TEST_ASSETS_REPO
|
|
`)
|
|
|
|
proj, err := Load(root)
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
effective := proj.EffectiveConfig()
|
|
if got, want := proj.ScriptSourceDir(), filepath.Join(root, "src", "module-scripts"); got != want {
|
|
t.Fatalf("expected script source dir %s, got %s", want, got)
|
|
}
|
|
if got, want := proj.ScriptCompilerPath(), filepath.Join(root, "bin", "compiler"); got != want {
|
|
t.Fatalf("expected compiler path %s, got %s", want, got)
|
|
}
|
|
if got, want := effective.Scripts.Compiler.Search[0], "custom-tools/compiler"; got != want {
|
|
t.Fatalf("expected compiler search %q, got %q", want, got)
|
|
}
|
|
if got, want := effective.Scripts.Compiler.Env.Path, "TEST_COMPILER"; got != want {
|
|
t.Fatalf("expected compiler env %q, got %q", want, got)
|
|
}
|
|
if got, want := effective.Extract.HAKDiscovery, "configured_haks"; got != want {
|
|
t.Fatalf("expected extract hak discovery %q, got %q", want, got)
|
|
}
|
|
if effective.Extract.CleanupStale == nil || *effective.Extract.CleanupStale {
|
|
t.Fatalf("expected cleanup_stale false, got %#v", effective.Extract.CleanupStale)
|
|
}
|
|
if got, want := len(effective.Extract.Merge.GFFJSON), 1; got != want {
|
|
t.Fatalf("expected %d gff json merge rule, got %d", want, got)
|
|
}
|
|
rule := effective.Extract.Merge.GFFJSON[0]
|
|
if got, want := rule.Target, "module/module.ifo.json"; got != want {
|
|
t.Fatalf("expected merge target %q, got %q", want, got)
|
|
}
|
|
if got, want := rule.PreserveFields, []string{"Mod_Entry_Area"}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("expected preserve fields %#v, got %#v", want, got)
|
|
}
|
|
if got, want := rule.MergeLists[0].Strategy, "preserve_existing_order_append_new"; got != want {
|
|
t.Fatalf("expected merge list strategy %q, got %q", want, got)
|
|
}
|
|
if got, want := effective.TopData.Wiki.DeployManifest, "wiki-manifest.json"; got != want {
|
|
t.Fatalf("expected wiki deploy manifest %q, got %q", want, got)
|
|
}
|
|
if got, want := effective.TopData.Wiki.Source, "topdata/wiki"; got != want {
|
|
t.Fatalf("expected wiki source %q, got %q", want, got)
|
|
}
|
|
if got, want := effective.TopData.Wiki.Renderer, "nodebb_tiptap_html"; got != want {
|
|
t.Fatalf("expected wiki renderer %q, got %q", want, got)
|
|
}
|
|
if got, want := effective.TopData.Wiki.LinkStrategy, "preserve_westgate_wiki_links"; got != want {
|
|
t.Fatalf("expected wiki link strategy %q, got %q", want, got)
|
|
}
|
|
if got, want := effective.TopData.Wiki.NamespacesFile, "namespaces.yaml"; got != want {
|
|
t.Fatalf("expected wiki namespaces file %q, got %q", want, got)
|
|
}
|
|
if got, want := effective.TopData.Wiki.TablesFile, "custom-tables.yaml"; got != want {
|
|
t.Fatalf("expected wiki tables file %q, got %q", want, got)
|
|
}
|
|
if got, want := effective.TopData.Wiki.VisibilityFile, "visibility.yaml"; got != want {
|
|
t.Fatalf("expected wiki visibility file %q, got %q", want, got)
|
|
}
|
|
if got, want := effective.TopData.Wiki.TemplatesDir, "templates"; got != want {
|
|
t.Fatalf("expected wiki templates dir %q, got %q", want, got)
|
|
}
|
|
if got, want := effective.TopData.Wiki.ManualSectionsDir, "manual-sections"; got != want {
|
|
t.Fatalf("expected wiki manual sections dir %q, got %q", want, got)
|
|
}
|
|
if got, want := effective.TopData.Wiki.StalePages.Default, "report"; got != want {
|
|
t.Fatalf("expected wiki stale default %q, got %q", want, got)
|
|
}
|
|
if got, want := effective.TopData.Wiki.StalePages.LiveCleanup, "archive"; got != want {
|
|
t.Fatalf("expected wiki stale live cleanup %q, got %q", want, got)
|
|
}
|
|
if got, want := effective.TopData.Wiki.TitlePrefixMinLength, 4; got != want {
|
|
t.Fatalf("expected wiki title prefix minimum length %d, got %d", want, got)
|
|
}
|
|
if got, want := effective.TopData.Wiki.StatusListingScope, "namespace"; got != want {
|
|
t.Fatalf("expected wiki status listing scope %q, got %q", want, got)
|
|
}
|
|
if got, want := effective.Autogen.Cache.MaxAge, "30m"; got != want {
|
|
t.Fatalf("expected autogen cache max age %q, got %q", want, got)
|
|
}
|
|
if got, want := proj.AutogenRefreshEnv(), "TEST_AUTOGEN_REFRESH"; got != want {
|
|
t.Fatalf("expected autogen refresh env %q, got %q", want, got)
|
|
}
|
|
}
|
|
|
|
func TestProjectValidationAcceptsWikiStalePurgePolicy(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "src"))
|
|
mkdirAll(t, filepath.Join(root, "build"))
|
|
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
|
module:
|
|
name: Test Module
|
|
resref: testmod
|
|
paths:
|
|
source: src
|
|
build: build
|
|
topdata:
|
|
wiki:
|
|
stale_pages:
|
|
default: report
|
|
live_cleanup: purge
|
|
`)
|
|
|
|
proj, err := Load(root)
|
|
if err != nil {
|
|
t.Fatalf("load project: %v", err)
|
|
}
|
|
if err := proj.ValidateLayout(); err != nil {
|
|
t.Fatalf("validate purge stale policy: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestProjectValidationRejectsUnsupportedWikiStalePolicies(t *testing.T) {
|
|
for _, policy := range []string{"unpublish", "remove-everything"} {
|
|
t.Run(policy, func(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "src"))
|
|
mkdirAll(t, filepath.Join(root, "build"))
|
|
writeProjectFile(t, filepath.Join(root, ConfigFile), fmt.Sprintf(`
|
|
module:
|
|
name: Test Module
|
|
resref: testmod
|
|
paths:
|
|
source: src
|
|
build: build
|
|
topdata:
|
|
wiki:
|
|
stale_pages:
|
|
live_cleanup: %s
|
|
`, policy))
|
|
|
|
proj, err := Load(root)
|
|
if err != nil {
|
|
t.Fatalf("load project: %v", err)
|
|
}
|
|
err = proj.ValidateLayout()
|
|
if err == nil || !strings.Contains(err.Error(), policy) {
|
|
t.Fatalf("expected %q stale policy validation failure, got %v", policy, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateLayoutRejectsInvalidExtractMergeRules(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config string
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "empty target",
|
|
config: `
|
|
extract:
|
|
merge:
|
|
gff_json:
|
|
- target: ""
|
|
preserve_fields: [Mod_Entry_Area]
|
|
`,
|
|
wantErr: "extract.merge.gff_json[0].target must not be empty",
|
|
},
|
|
{
|
|
name: "duplicate target",
|
|
config: `
|
|
extract:
|
|
merge:
|
|
gff_json:
|
|
- target: module/module.ifo.json
|
|
- target: module/module.ifo.json
|
|
`,
|
|
wantErr: "extract.merge.gff_json target \"module/module.ifo.json\" is configured more than once",
|
|
},
|
|
{
|
|
name: "empty merge list field",
|
|
config: `
|
|
extract:
|
|
merge:
|
|
gff_json:
|
|
- target: module/module.ifo.json
|
|
merge_lists:
|
|
- field: ""
|
|
key_field: Area_Name
|
|
strategy: preserve_existing_order_append_new
|
|
`,
|
|
wantErr: "extract.merge.gff_json[0].merge_lists[0].field must not be empty",
|
|
},
|
|
{
|
|
name: "unsupported strategy",
|
|
config: `
|
|
extract:
|
|
merge:
|
|
gff_json:
|
|
- target: module/module.ifo.json
|
|
merge_lists:
|
|
- field: Mod_Area_list
|
|
key_field: Area_Name
|
|
strategy: replace
|
|
`,
|
|
wantErr: "extract.merge.gff_json[0].merge_lists[0].strategy \"replace\" is not supported",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
|
module:
|
|
name: Test Module
|
|
resref: testmod
|
|
paths:
|
|
source: src
|
|
build: build
|
|
`+tt.config)
|
|
|
|
proj, err := Load(root)
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
err = proj.ValidateLayout()
|
|
if err == nil {
|
|
t.Fatalf("expected validation error containing %q", tt.wantErr)
|
|
}
|
|
if !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Fatalf("expected validation error containing %q, got %v", tt.wantErr, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestActiveOverridesAreVisibleAndSensitiveValuesAreMasked(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
|
module:
|
|
name: Test Module
|
|
resref: testmod
|
|
paths:
|
|
source: src
|
|
`)
|
|
t.Setenv("SOW_BUILD_HAKS_KEEP_EXISTING", "1")
|
|
t.Setenv("NODEBB_API_TOKEN", "secret-token")
|
|
|
|
proj, err := Load(root)
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
overrides := proj.ActiveOverrides()
|
|
var sawKeepExisting, sawMaskedToken bool
|
|
for _, override := range overrides {
|
|
if override.Key == "build.keep_existing_haks" && override.Value == "1" {
|
|
sawKeepExisting = true
|
|
}
|
|
if override.Key == "wiki.token" && override.Value == "<set>" {
|
|
sawMaskedToken = true
|
|
}
|
|
}
|
|
if !sawKeepExisting {
|
|
t.Fatalf("expected build keep existing override in %#v", overrides)
|
|
}
|
|
if !sawMaskedToken {
|
|
t.Fatalf("expected masked wiki token override in %#v", overrides)
|
|
}
|
|
}
|
|
|
|
func TestEffectiveConfigJSONIsDeterministic(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
|
module:
|
|
name: Test Module
|
|
resref: testmod
|
|
paths:
|
|
source: src
|
|
`)
|
|
|
|
proj, err := Load(root)
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
first, err := proj.EffectiveConfigJSON()
|
|
if err != nil {
|
|
t.Fatalf("EffectiveConfigJSON returned error: %v", err)
|
|
}
|
|
second, err := proj.EffectiveConfigJSON()
|
|
if err != nil {
|
|
t.Fatalf("EffectiveConfigJSON returned error: %v", err)
|
|
}
|
|
if !bytes.Equal(first, second) {
|
|
t.Fatalf("effective config JSON is not deterministic")
|
|
}
|
|
if !bytes.Contains(first, []byte(`"hak_manifest": "haks.json"`)) {
|
|
t.Fatalf("effective config JSON missing HAK manifest default: %s", first)
|
|
}
|
|
}
|
|
|
|
func TestLoadYMLConfig(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeProjectFile(t, filepath.Join(root, ConfigFileYML), `
|
|
module:
|
|
name: Test Module
|
|
resref: testmod
|
|
paths:
|
|
source: src
|
|
build: build
|
|
`)
|
|
|
|
proj, err := Load(root)
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
if proj.ConfigSource.Name != ConfigFileYML || proj.ConfigSource.Format != "yaml" || proj.ConfigSource.Legacy {
|
|
t.Fatalf("unexpected config source: %#v", proj.ConfigSource)
|
|
}
|
|
}
|
|
|
|
func TestLoadLegacyJSONConfig(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeProjectFile(t, filepath.Join(root, LegacyConfigFile), `{
|
|
"module": {
|
|
"name": "Legacy Module",
|
|
"resref": "legacy"
|
|
},
|
|
"paths": {
|
|
"source": "src",
|
|
"build": "build"
|
|
}
|
|
}`+"\n")
|
|
|
|
proj, err := Load(root)
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
if proj.ConfigSource.Name != LegacyConfigFile || proj.ConfigSource.Format != "json" || !proj.ConfigSource.Legacy {
|
|
t.Fatalf("unexpected config source: %#v", proj.ConfigSource)
|
|
}
|
|
}
|
|
|
|
func TestLoadPrefersYAMLOverLegacyJSON(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeProjectFile(t, filepath.Join(root, LegacyConfigFile), `{
|
|
"module": {
|
|
"name": "Legacy Module",
|
|
"resref": "legacy"
|
|
}
|
|
}`+"\n")
|
|
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
|
module:
|
|
name: YAML Module
|
|
resref: yamlmod
|
|
paths:
|
|
source: src
|
|
build: build
|
|
`)
|
|
|
|
proj, err := Load(root)
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
if proj.ConfigSource.Name != ConfigFile {
|
|
t.Fatalf("expected YAML config to win, got %#v", proj.ConfigSource)
|
|
}
|
|
if got, want := proj.Config.Module.ResRef, "yamlmod"; got != want {
|
|
t.Fatalf("expected resref %q, got %q", want, got)
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsMalformedYAML(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeProjectFile(t, filepath.Join(root, ConfigFile), "module:\n name: Test\n resref: bad\n")
|
|
|
|
if _, err := Load(root); err == nil {
|
|
t.Fatal("expected malformed YAML error")
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsUnknownYAMLFields(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeProjectFile(t, filepath.Join(root, ConfigFile), `
|
|
module:
|
|
name: Test Module
|
|
resref: testmod
|
|
paths:
|
|
source: src
|
|
build: build
|
|
unexpected: true
|
|
`)
|
|
|
|
err := loadAndValidate(root)
|
|
if err == nil {
|
|
t.Fatal("expected unknown field error")
|
|
}
|
|
if !strings.Contains(err.Error(), "field unexpected not found") {
|
|
t.Fatalf("expected unknown field error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestFindRootUsesYAMLConfig(t *testing.T) {
|
|
root := t.TempDir()
|
|
child := filepath.Join(root, "nested", "dir")
|
|
mkdirAll(t, child)
|
|
writeProjectFile(t, filepath.Join(root, ConfigFile), "module:\n name: Test\n resref: test\n")
|
|
|
|
got, err := FindRoot(child)
|
|
if err != nil {
|
|
t.Fatalf("FindRoot returned error: %v", err)
|
|
}
|
|
if got != root {
|
|
t.Fatalf("expected root %s, got %s", root, got)
|
|
}
|
|
}
|
|
|
|
func TestTopDataAssetsDirPrefersExplicitConfig(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "src"))
|
|
mkdirAll(t, filepath.Join(root, "assets"))
|
|
mkdirAll(t, filepath.Join(root, "build"))
|
|
mkdirAll(t, filepath.Join(root, "custom-assets"))
|
|
|
|
proj := &Project{
|
|
Root: root,
|
|
Config: Config{
|
|
Module: ModuleConfig{Name: "Test", ResRef: "test"},
|
|
Paths: PathConfig{Source: "src", Assets: "assets", Build: "build"},
|
|
TopData: TopDataConfig{
|
|
Assets: "custom-assets",
|
|
},
|
|
},
|
|
}
|
|
|
|
result := proj.TopDataAssetsDir()
|
|
expected := filepath.Join(root, "custom-assets")
|
|
if result != expected {
|
|
t.Errorf("expected %s, got %s", expected, result)
|
|
}
|
|
}
|
|
|
|
func TestTopDataAssetsDirDoesNotAssumeSiblingRepo(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "src"))
|
|
mkdirAll(t, filepath.Join(root, "assets"))
|
|
mkdirAll(t, filepath.Join(root, "build"))
|
|
|
|
parentDir := filepath.Dir(root)
|
|
siblingPath := filepath.Join(parentDir, "sow-assets")
|
|
mkdirAll(t, siblingPath)
|
|
mkdirAll(t, filepath.Join(siblingPath, "assets", "part", "belt"))
|
|
|
|
proj := &Project{
|
|
Root: root,
|
|
Config: Config{
|
|
Module: ModuleConfig{Name: "Test", ResRef: "test"},
|
|
Paths: PathConfig{Source: "src", Assets: "assets", Build: "build"},
|
|
TopData: TopDataConfig{
|
|
Assets: "sow-assets",
|
|
},
|
|
},
|
|
}
|
|
|
|
result := proj.TopDataAssetsDir()
|
|
expected := filepath.Join(root, "sow-assets")
|
|
if result != expected {
|
|
t.Errorf("expected unresolved local path %s, got %s", expected, result)
|
|
}
|
|
}
|
|
|
|
func TestTopDataAssetsDirReturnsEmptyForNoConfig(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "src"))
|
|
mkdirAll(t, filepath.Join(root, "assets"))
|
|
mkdirAll(t, filepath.Join(root, "build"))
|
|
|
|
proj := &Project{
|
|
Root: root,
|
|
Config: Config{
|
|
Module: ModuleConfig{Name: "Test", ResRef: "test"},
|
|
Paths: PathConfig{Source: "src", Assets: "assets", Build: "build"},
|
|
TopData: TopDataConfig{},
|
|
},
|
|
}
|
|
|
|
result := proj.TopDataAssetsDir()
|
|
if result != "" {
|
|
t.Errorf("expected empty string, got %s", result)
|
|
}
|
|
}
|
|
|
|
func TestModuleArchivePathUsesConfiguredBuildDirAndResRef(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "output"))
|
|
|
|
proj := &Project{
|
|
Root: root,
|
|
Config: Config{
|
|
Module: ModuleConfig{Name: "Test", ResRef: "testmod"},
|
|
Paths: PathConfig{Build: "output"},
|
|
},
|
|
}
|
|
|
|
got := proj.ModuleArchivePath()
|
|
want := filepath.Join(root, "output", "testmod.mod")
|
|
if got != want {
|
|
t.Fatalf("expected %s, got %s", want, got)
|
|
}
|
|
}
|
|
|
|
func TestHAKPathsUseConfiguredBuildDir(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "output"))
|
|
|
|
proj := &Project{
|
|
Root: root,
|
|
Config: Config{
|
|
Paths: PathConfig{Build: "output"},
|
|
},
|
|
}
|
|
|
|
if got, want := proj.HAKManifestPath(), filepath.Join(root, "output", "haks.json"); got != want {
|
|
t.Fatalf("expected manifest path %s, got %s", want, got)
|
|
}
|
|
if got, want := proj.HAKArchivePath("core_01"), filepath.Join(root, "output", "core_01.hak"); got != want {
|
|
t.Fatalf("expected hak path %s, got %s", want, got)
|
|
}
|
|
}
|
|
|
|
func TestTopDataPackagePathsUseConfiguredNamesAndBuildDir(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "output"))
|
|
|
|
proj := &Project{
|
|
Root: root,
|
|
Config: Config{
|
|
Paths: PathConfig{Build: "output"},
|
|
TopData: TopDataConfig{
|
|
PackageHAK: "custom_top.hak",
|
|
PackageTLK: "custom_dialog.tlk",
|
|
},
|
|
},
|
|
}
|
|
|
|
if got, want := proj.TopDataPackageHAKPath(), filepath.Join(root, "output", "custom_top.hak"); got != want {
|
|
t.Fatalf("expected top hak path %s, got %s", want, got)
|
|
}
|
|
if got, want := proj.TopDataPackageTLKPath(), filepath.Join(root, "output", "custom_dialog.tlk"); got != want {
|
|
t.Fatalf("expected top tlk path %s, got %s", want, got)
|
|
}
|
|
}
|
|
|
|
func TestValidateLayoutAllowsMissingAssetsDir(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "src"))
|
|
mkdirAll(t, filepath.Join(root, "build"))
|
|
|
|
proj := &Project{
|
|
Root: root,
|
|
Config: Config{
|
|
Module: ModuleConfig{Name: "Test", ResRef: "test"},
|
|
Paths: PathConfig{Source: "src", Assets: "assets", Build: "build"},
|
|
},
|
|
}
|
|
|
|
if err := proj.ValidateLayout(); err != nil {
|
|
t.Fatalf("ValidateLayout returned error: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, "assets")); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("expected missing assets dir to remain absent, got err=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateLayoutRejectsInvalidTopDataPackageOutputNames(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "src"))
|
|
mkdirAll(t, filepath.Join(root, "build"))
|
|
|
|
proj := &Project{
|
|
Root: root,
|
|
Config: Config{
|
|
Module: ModuleConfig{Name: "Test", ResRef: "test"},
|
|
Paths: PathConfig{Source: "src", Build: "build"},
|
|
TopData: TopDataConfig{
|
|
Source: "topdata",
|
|
PackageHAK: "nested/custom_top.hak",
|
|
PackageTLK: "custom_dialog.txt",
|
|
},
|
|
},
|
|
}
|
|
|
|
err := proj.ValidateLayout()
|
|
if err == nil {
|
|
t.Fatal("expected topdata output validation error")
|
|
}
|
|
if !strings.Contains(err.Error(), "topdata.package_hak") {
|
|
t.Fatalf("expected package_hak validation error, got %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "topdata.package_tlk") {
|
|
t.Fatalf("expected package_tlk validation error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateLayoutRejectsUnsafeGeneratedOutputPaths(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "src"))
|
|
mkdirAll(t, filepath.Join(root, "build"))
|
|
|
|
proj := &Project{
|
|
Root: root,
|
|
Config: Config{
|
|
Module: ModuleConfig{Name: "Test", ResRef: "test"},
|
|
Paths: PathConfig{Source: "src", Build: "build"},
|
|
Outputs: OutputConfig{
|
|
HAKManifest: "../haks.json",
|
|
},
|
|
TopData: TopDataConfig{
|
|
Source: "topdata",
|
|
Compiled2DADir: "../2da",
|
|
},
|
|
},
|
|
}
|
|
|
|
err := proj.ValidateLayout()
|
|
if err == nil {
|
|
t.Fatal("expected unsafe output path validation error")
|
|
}
|
|
if !strings.Contains(err.Error(), "outputs.hak_manifest") {
|
|
t.Fatalf("expected HAK manifest path validation error, got %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "topdata.compiled_2da_dir") {
|
|
t.Fatalf("expected topdata path validation error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateLayoutRejectsInvalidTopDataWikiConfig(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "src"))
|
|
mkdirAll(t, filepath.Join(root, "build"))
|
|
|
|
proj := &Project{
|
|
Root: root,
|
|
Config: Config{
|
|
Module: ModuleConfig{Name: "Test", ResRef: "test"},
|
|
Paths: PathConfig{Source: "src", Build: "build"},
|
|
TopData: TopDataConfig{
|
|
Source: "topdata",
|
|
Wiki: TopDataWikiConfig{
|
|
Source: "../wiki",
|
|
Renderer: "dokuwiki",
|
|
LinkStrategy: "rewrite_links",
|
|
StatusListingScope: "global",
|
|
NamespacesFile: "../namespaces.yaml",
|
|
TablesFile: "../tables.yaml",
|
|
VisibilityFile: "../visibility.yaml",
|
|
TemplatesDir: ".",
|
|
ManualSectionsDir: "../manual",
|
|
StalePages: TopDataWikiStalePagesConfig{
|
|
Default: "delete",
|
|
LiveCleanup: "delete",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := proj.ValidateLayout()
|
|
if err == nil {
|
|
t.Fatal("expected invalid wiki config validation error")
|
|
}
|
|
for _, needle := range []string{
|
|
"topdata.wiki.source",
|
|
"topdata.wiki.renderer",
|
|
"topdata.wiki.link_strategy",
|
|
"topdata.wiki.status_listing_scope",
|
|
"topdata.wiki.namespaces_file",
|
|
"topdata.wiki.tables_file",
|
|
"topdata.wiki.visibility_file",
|
|
"topdata.wiki.templates_dir",
|
|
"topdata.wiki.manual_sections_dir",
|
|
"topdata.wiki.stale_pages.default",
|
|
"topdata.wiki.stale_pages.live_cleanup",
|
|
} {
|
|
if !strings.Contains(err.Error(), needle) {
|
|
t.Fatalf("expected validation error to mention %s, got %v", needle, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateLayoutRejectsRootSourceAndAssetPaths(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "build"))
|
|
|
|
proj := &Project{
|
|
Root: root,
|
|
Config: Config{
|
|
Module: ModuleConfig{Name: "Test", ResRef: "test"},
|
|
Paths: PathConfig{Source: ".", Assets: ".", Build: "build"},
|
|
},
|
|
}
|
|
|
|
err := proj.ValidateLayout()
|
|
if err == nil {
|
|
t.Fatal("expected root source and asset path validation error")
|
|
}
|
|
if !strings.Contains(err.Error(), "paths.source") {
|
|
t.Fatalf("expected source path validation error, got %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "paths.assets") {
|
|
t.Fatalf("expected assets path validation error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateLayoutRejectsInvalidAutogenConsumerConfig(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "src"))
|
|
mkdirAll(t, filepath.Join(root, "build"))
|
|
|
|
proj := &Project{
|
|
Root: root,
|
|
Config: Config{
|
|
Module: ModuleConfig{Name: "Test", ResRef: "test"},
|
|
Paths: PathConfig{Source: "src", Build: "build"},
|
|
Autogen: AutogenConfig{
|
|
Consumers: []AutogenConsumerConfig{
|
|
{
|
|
ID: "head_visualeffects",
|
|
Producer: "head_visualeffects",
|
|
Dataset: "visualeffects",
|
|
Mode: "unsupported",
|
|
Root: "vfxs",
|
|
Include: []string{"head_accessories/**/*.mdl"},
|
|
Derive: AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"},
|
|
Manifest: AutogenManifestConfig{
|
|
ReleaseTag: "head-vfx-manifest-current",
|
|
AssetName: "nested/sow-head-vfx-manifest.json",
|
|
CacheName: "sow-head-vfx-manifest.txt",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := proj.ValidateLayout()
|
|
if err == nil {
|
|
t.Fatal("expected autogen validation error")
|
|
}
|
|
if !strings.Contains(err.Error(), "autogen.consumers[0].mode") {
|
|
t.Fatalf("expected autogen mode validation error, got %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "autogen.consumers[0].manifest.asset_name") {
|
|
t.Fatalf("expected autogen asset_name validation error, got %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "autogen.consumers[0].manifest.cache_name") {
|
|
t.Fatalf("expected autogen cache_name validation error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateLayoutAcceptsGeneratedTopData2DAConfig(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "assets"))
|
|
mkdirAll(t, filepath.Join(root, "build"))
|
|
proj := Project{
|
|
Root: root,
|
|
Config: Config{
|
|
Module: ModuleConfig{Name: "Test", ResRef: "testmod"},
|
|
Paths: PathConfig{Assets: "assets"},
|
|
Generated: GeneratedConfig{
|
|
TopData2DA: []GeneratedTopData2DAConfig{
|
|
{
|
|
ID: "parts",
|
|
Source: "topdata",
|
|
Output: "{paths.cache}/generated-assets/parts-2da",
|
|
IncludeDatasets: []string{"parts/**"},
|
|
PackageRoot: "part",
|
|
Autogen: AutogenConsumerConfig{
|
|
ID: "parts",
|
|
Mode: "parts_rows",
|
|
Root: "part",
|
|
Include: []string{"**/*.mdl"},
|
|
Derive: AutogenDeriveConfig{Kind: "trailing_numeric_suffix", GroupFrom: "first_path_segment"},
|
|
PartsRows: PartsRowsConfig{
|
|
RowDefaults: map[string]string{"COSTMODIFIER": "0"},
|
|
ACBonus: PartsRowsACBonusConfig{
|
|
Default: PartsRowsACBonusPolicy{Strategy: "ascending_row_id_sort_key", Divisor: 100, Format: "%.2f"},
|
|
Datasets: map[string]PartsRowsACBonusPolicy{
|
|
"chest": {Strategy: "fixed", Value: "0.00"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ID: "cachedmodels",
|
|
Source: "topdata",
|
|
Output: "{paths.cache}/generated-assets/cachedmodels-2da",
|
|
IncludeDatasets: []string{"cachedmodels"},
|
|
PackageRoot: "vfxs",
|
|
Autogen: AutogenConsumerConfig{
|
|
ID: "cachedmodels",
|
|
Mode: "cachedmodels_rows",
|
|
Root: "vfxs",
|
|
Include: []string{"head_accessories/**/*.mdl", "head_features/**/*.mdl"},
|
|
Derive: AutogenDeriveConfig{Kind: "model_stem", GroupFrom: "first_path_segment"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
if err := proj.ValidateLayout(); err != nil {
|
|
t.Fatalf("ValidateLayout returned error: %v", err)
|
|
}
|
|
effective := proj.EffectiveConfig()
|
|
if got, want := effective.Generated.TopData2DA[0].Output, ".cache/generated-assets/parts-2da"; got != want {
|
|
t.Fatalf("expected expanded output %q, got %q", want, got)
|
|
}
|
|
}
|
|
|
|
func TestValidateLayoutRejectsEscapingGeneratedTopData2DAConfig(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "assets"))
|
|
mkdirAll(t, filepath.Join(root, "build"))
|
|
proj := Project{
|
|
Root: root,
|
|
Config: Config{
|
|
Module: ModuleConfig{Name: "Test", ResRef: "testmod"},
|
|
Paths: PathConfig{Assets: "assets"},
|
|
Generated: GeneratedConfig{
|
|
TopData2DA: []GeneratedTopData2DAConfig{
|
|
{
|
|
ID: "parts",
|
|
Source: "../topdata",
|
|
Output: "{paths.cache}/generated-assets/parts-2da",
|
|
IncludeDatasets: []string{"parts/**"},
|
|
PackageRoot: "part",
|
|
Autogen: AutogenConsumerConfig{
|
|
ID: "parts",
|
|
Mode: "parts_rows",
|
|
Root: "part",
|
|
Include: []string{"**/*.mdl"},
|
|
Derive: AutogenDeriveConfig{Kind: "trailing_numeric_suffix", GroupFrom: "first_path_segment"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := proj.ValidateLayout()
|
|
if err == nil || !strings.Contains(err.Error(), "generated_assets.topdata_2da[0].source") {
|
|
t.Fatalf("expected generated topdata source validation error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateLayoutRejectsInvalidGeneratedPartsRowsACBonusConfig(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "assets"))
|
|
mkdirAll(t, filepath.Join(root, "build"))
|
|
proj := Project{
|
|
Root: root,
|
|
Config: Config{
|
|
Module: ModuleConfig{Name: "Test", ResRef: "testmod"},
|
|
Paths: PathConfig{Assets: "assets"},
|
|
Generated: GeneratedConfig{
|
|
TopData2DA: []GeneratedTopData2DAConfig{
|
|
{
|
|
ID: "parts",
|
|
Source: "topdata",
|
|
Output: "{paths.cache}/generated-assets/parts-2da",
|
|
IncludeDatasets: []string{"parts/**"},
|
|
PackageRoot: "part",
|
|
Autogen: AutogenConsumerConfig{
|
|
ID: "parts",
|
|
Mode: "parts_rows",
|
|
Root: "part",
|
|
Include: []string{"**/*.mdl"},
|
|
Derive: AutogenDeriveConfig{Kind: "trailing_numeric_suffix", GroupFrom: "first_path_segment"},
|
|
PartsRows: PartsRowsConfig{
|
|
ACBonus: PartsRowsACBonusConfig{
|
|
Default: PartsRowsACBonusPolicy{Strategy: "descending_row_id_sort_key", MaxRowID: 0, Divisor: -1, Format: "%d"},
|
|
Datasets: map[string]PartsRowsACBonusPolicy{
|
|
"chest": {Strategy: "fixed"},
|
|
"robe": {Strategy: "unknown"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := proj.ValidateLayout()
|
|
if err == nil {
|
|
t.Fatal("expected invalid parts_rows acbonus config to fail")
|
|
}
|
|
text := err.Error()
|
|
for _, want := range []string{
|
|
"generated_assets.topdata_2da[0].autogen.parts_rows.acbonus.default.max_row_id",
|
|
"generated_assets.topdata_2da[0].autogen.parts_rows.acbonus.default.divisor",
|
|
"generated_assets.topdata_2da[0].autogen.parts_rows.acbonus.default.format",
|
|
"generated_assets.topdata_2da[0].autogen.parts_rows.acbonus.datasets.chest.value",
|
|
"generated_assets.topdata_2da[0].autogen.parts_rows.acbonus.datasets.robe.strategy",
|
|
} {
|
|
if !strings.Contains(text, want) {
|
|
t.Fatalf("expected validation error containing %q, got %v", want, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateLayoutRejectsDuplicateHAKNames(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "src"))
|
|
mkdirAll(t, filepath.Join(root, "build"))
|
|
|
|
proj := &Project{
|
|
Root: root,
|
|
Config: Config{
|
|
Module: ModuleConfig{Name: "Test", ResRef: "test"},
|
|
Paths: PathConfig{Source: "src", Build: "build"},
|
|
HAKs: []HAKConfig{
|
|
{Name: "core", Priority: 1, Include: []string{"core/**"}},
|
|
{Name: "CORE", Priority: 2, Include: []string{"other/**"}},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := proj.ValidateLayout()
|
|
if err == nil {
|
|
t.Fatal("expected duplicate hak validation error")
|
|
}
|
|
if !strings.Contains(err.Error(), "duplicated") {
|
|
t.Fatalf("expected duplicate validation error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateLayoutRejectsAmbiguousMusicPrefixes(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "assets"))
|
|
mkdirAll(t, filepath.Join(root, "build"))
|
|
|
|
proj := &Project{
|
|
Root: root,
|
|
Config: Config{
|
|
Module: ModuleConfig{Name: "Test", ResRef: "test"},
|
|
Paths: PathConfig{Assets: "assets", Build: "build"},
|
|
Music: MusicConfig{
|
|
Prefixes: map[string]string{
|
|
"envi/music": "mus_",
|
|
"/envi/music": "mus2_",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := proj.ValidateLayout()
|
|
if err == nil {
|
|
t.Fatal("expected music prefix validation error")
|
|
}
|
|
if !strings.Contains(err.Error(), "normalize to the same path") {
|
|
t.Fatalf("expected ambiguous prefix validation error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestScanAllowsMissingAssetsDir(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkdirAll(t, filepath.Join(root, "src"))
|
|
mkdirAll(t, filepath.Join(root, "build"))
|
|
|
|
proj := &Project{
|
|
Root: root,
|
|
Config: Config{
|
|
Module: ModuleConfig{Name: "Test", ResRef: "test"},
|
|
Paths: PathConfig{Source: "src", Assets: "assets", Build: "build"},
|
|
},
|
|
}
|
|
|
|
if err := proj.Scan(); err != nil {
|
|
t.Fatalf("Scan returned error: %v", err)
|
|
}
|
|
if len(proj.Inventory.AssetFiles) != 0 {
|
|
t.Fatalf("expected no asset files, got %#v", proj.Inventory.AssetFiles)
|
|
}
|
|
}
|
|
|
|
func mkdirAll(t *testing.T, path string) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(path, 0o755); err != nil {
|
|
t.Fatalf("mkdir %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
func TestCloneWithHAKNamesFiltersConfiguredHAKs(t *testing.T) {
|
|
proj := &Project{
|
|
Root: "/tmp/test",
|
|
Config: Config{
|
|
Module: ModuleConfig{Name: "Test", ResRef: "test"},
|
|
Paths: PathConfig{Source: "src", Assets: "assets", Build: "build"},
|
|
HAKs: []HAKConfig{
|
|
{Name: "sow_over", Priority: 1, Include: []string{"over/**"}},
|
|
{Name: "sow_core", Priority: 2, Include: []string{"core/**"}},
|
|
{Name: "sow_item", Priority: 3, Include: []string{"item/**"}},
|
|
},
|
|
},
|
|
}
|
|
|
|
filtered, err := proj.CloneWithHAKNames([]string{"sow_core", "sow_item"})
|
|
if err != nil {
|
|
t.Fatalf("CloneWithHAKNames returned error: %v", err)
|
|
}
|
|
|
|
if filtered == proj {
|
|
t.Fatalf("expected filtered clone, got original pointer")
|
|
}
|
|
|
|
got := make([]string, 0, len(filtered.Config.HAKs))
|
|
for _, hak := range filtered.Config.HAKs {
|
|
got = append(got, hak.Name)
|
|
}
|
|
want := []string{"sow_core", "sow_item"}
|
|
if !slices.Equal(got, want) {
|
|
t.Fatalf("unexpected filtered haks: got %v want %v", got, want)
|
|
}
|
|
|
|
if len(proj.Config.HAKs) != 3 {
|
|
t.Fatalf("original project config should remain unchanged, got %d haks", len(proj.Config.HAKs))
|
|
}
|
|
}
|
|
|
|
func TestCloneWithHAKNamesRejectsUnknownHAKs(t *testing.T) {
|
|
proj := &Project{
|
|
Config: Config{
|
|
HAKs: []HAKConfig{
|
|
{Name: "sow_core", Priority: 1, Include: []string{"core/**"}},
|
|
},
|
|
},
|
|
}
|
|
|
|
if _, err := proj.CloneWithHAKNames([]string{"missing_hak"}); err == nil {
|
|
t.Fatal("expected error for unknown hak name")
|
|
}
|
|
}
|
|
|
|
func loadAndValidate(root string) error {
|
|
proj, err := Load(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return proj.ValidateLayout()
|
|
}
|
|
|
|
func writeProjectFile(t *testing.T, path, content string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, []byte(strings.TrimPrefix(content, "\n")), 0o644); err != nil {
|
|
t.Fatalf("write %s: %v", path, err)
|
|
}
|
|
}
|