Optimization Pass
This commit is contained in:
+27
-2
@@ -436,7 +436,12 @@ func runBuildTopData(ctx context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := topdata.BuildAndPackage(p, func(message string) {
|
opts, err := parseBuildTopDataArgs("build-topdata", ctx.args[1:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := topdata.BuildAndPackageWithOptions(p, opts, func(message string) {
|
||||||
fmt.Fprintf(ctx.stdout, "[build-topdata] %s\n", message)
|
fmt.Fprintf(ctx.stdout, "[build-topdata] %s\n", message)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -467,7 +472,12 @@ func runBuildTopPackage(ctx context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := topdata.BuildAndPackage(p, func(message string) {
|
opts, err := parseBuildTopDataArgs("build-top-package", ctx.args[1:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := topdata.BuildAndPackageWithOptions(p, opts, func(message string) {
|
||||||
fmt.Fprintf(ctx.stdout, "[build-top-package] %s\n", message)
|
fmt.Fprintf(ctx.stdout, "[build-top-package] %s\n", message)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -492,6 +502,21 @@ func runBuildTopPackage(ctx context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseBuildTopDataArgs(commandName string, args []string) (topdata.BuildAndPackageOptions, error) {
|
||||||
|
opts := topdata.BuildAndPackageOptions{}
|
||||||
|
for _, arg := range args {
|
||||||
|
switch arg {
|
||||||
|
case "--force":
|
||||||
|
opts.Force = true
|
||||||
|
case "-h", "--help":
|
||||||
|
return opts, fmt.Errorf("usage: %s [--force]", commandName)
|
||||||
|
default:
|
||||||
|
return opts, fmt.Errorf("unknown %s argument %q", commandName, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return opts, nil
|
||||||
|
}
|
||||||
|
|
||||||
func runCompareTopData(ctx context) error {
|
func runCompareTopData(ctx context) error {
|
||||||
p, err := loadProject(ctx.cwd)
|
p, err := loadProject(ctx.cwd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2063,6 +2063,9 @@ func featSourceID(value any) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *featGeneratedContext) resolveGeneratedFeatIdentityBySource(familyKey, slug string, rawSource any) (string, int, bool, error) {
|
func (c *featGeneratedContext) resolveGeneratedFeatIdentityBySource(familyKey, slug string, rawSource any) (string, int, bool, error) {
|
||||||
|
if ref, ok := rawSource.(string); ok && strings.HasPrefix(ref, "feat:") {
|
||||||
|
return c.resolveGeneratedFeatIdentityFromRef(familyKey, ref)
|
||||||
|
}
|
||||||
if rawSource != nil && rawSource != nullValue {
|
if rawSource != nil && rawSource != nullValue {
|
||||||
if parsedID, err := featSourceID(rawSource); err == nil && parsedID > 0 {
|
if parsedID, err := featSourceID(rawSource); err == nil && parsedID > 0 {
|
||||||
if generatedKey, ok := c.generatedFeatKeyForID(familyKey, parsedID); ok {
|
if generatedKey, ok := c.generatedFeatKeyForID(familyKey, parsedID); ok {
|
||||||
@@ -2073,6 +2076,15 @@ func (c *featGeneratedContext) resolveGeneratedFeatIdentityBySource(familyKey, s
|
|||||||
}
|
}
|
||||||
if rawMap, ok := rawSource.(map[string]any); ok {
|
if rawMap, ok := rawSource.(map[string]any); ok {
|
||||||
if ref, ok := rawMap["id"].(string); ok && strings.HasPrefix(ref, "feat:") {
|
if ref, ok := rawMap["id"].(string); ok && strings.HasPrefix(ref, "feat:") {
|
||||||
|
return c.resolveGeneratedFeatIdentityFromRef(familyKey, ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
featKey := c.preferredGeneratedFeatKey(familyKey, slug)
|
||||||
|
rowID, hasID, err := c.featID(featKey, rawSource)
|
||||||
|
return featKey, rowID, hasID, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *featGeneratedContext) resolveGeneratedFeatIdentityFromRef(familyKey, ref string) (string, int, bool, error) {
|
||||||
if generatedKey, rowID, ok := c.generatedFeatKeyFromCanonicalAlias(ref); ok {
|
if generatedKey, rowID, ok := c.generatedFeatKeyFromCanonicalAlias(ref); ok {
|
||||||
return generatedKey, rowID, true, nil
|
return generatedKey, rowID, true, nil
|
||||||
}
|
}
|
||||||
@@ -2086,11 +2098,6 @@ func (c *featGeneratedContext) resolveGeneratedFeatIdentityBySource(familyKey, s
|
|||||||
rowID, hasID, err := c.featID(ref, nil)
|
rowID, hasID, err := c.featID(ref, nil)
|
||||||
return ref, rowID, hasID, err
|
return ref, rowID, hasID, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
featKey := c.preferredGeneratedFeatKey(familyKey, slug)
|
|
||||||
rowID, hasID, err := c.featID(featKey, rawSource)
|
|
||||||
return featKey, rowID, hasID, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *featGeneratedContext) resolveGeneratedFeatIdentity(spec familyExpansionSpec, slug string, sourceRow map[string]any) (string, int, bool, error) {
|
func (c *featGeneratedContext) resolveGeneratedFeatIdentity(spec familyExpansionSpec, slug string, sourceRow map[string]any) (string, int, bool, error) {
|
||||||
if spec.IdentitySource == "child_source_value" {
|
if spec.IdentitySource == "child_source_value" {
|
||||||
|
|||||||
@@ -56,9 +56,41 @@ func BuildPackage(p *project.Project, progress func(string)) (PackageResult, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func BuildAndPackage(p *project.Project, progress func(string)) (PackageResult, error) {
|
func BuildAndPackage(p *project.Project, progress func(string)) (PackageResult, error) {
|
||||||
|
return BuildAndPackageWithOptions(p, BuildAndPackageOptions{}, progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
type BuildAndPackageOptions struct {
|
||||||
|
Force bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildAndPackageWithOptions(p *project.Project, opts BuildAndPackageOptions, progress func(string)) (PackageResult, error) {
|
||||||
if progress == nil {
|
if progress == nil {
|
||||||
progress = func(string) {}
|
progress = func(string) {}
|
||||||
}
|
}
|
||||||
|
if !opts.Force {
|
||||||
|
incrementalResult, ok, err := currentTopPackageResult(p)
|
||||||
|
if err != nil {
|
||||||
|
return PackageResult{}, err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
progress("Topdata outputs are current; skipping native build and top package refresh.")
|
||||||
|
return incrementalResult, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
nativeResult, ok, err := currentCompiledBuildResult(p)
|
||||||
|
if err != nil {
|
||||||
|
return PackageResult{}, err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
progress("Compiled topdata outputs are current; refreshing sow_top.hak and sow_tlk.tlk only.")
|
||||||
|
return packageBuiltTopData(p, nativeResult, progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildAndPackageFull(p, progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAndPackageFull(p *project.Project, progress func(string)) (PackageResult, error) {
|
||||||
nativeResult, err := BuildNative(p, progress)
|
nativeResult, err := BuildNative(p, progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PackageResult{}, err
|
return PackageResult{}, err
|
||||||
@@ -323,6 +355,109 @@ func packagedBuildResult(p *project.Project) (BuildResult, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func currentCompiledBuildResult(p *project.Project) (BuildResult, bool, error) {
|
||||||
|
nativeResult, err := packagedBuildResult(p)
|
||||||
|
if err == nil {
|
||||||
|
return nativeResult, true, nil
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "run build-topdata first") {
|
||||||
|
return BuildResult{}, false, nil
|
||||||
|
}
|
||||||
|
return BuildResult{}, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentTopPackageResult(p *project.Project) (PackageResult, bool, error) {
|
||||||
|
nativeResult, ok, err := currentCompiledBuildResult(p)
|
||||||
|
if err != nil || !ok {
|
||||||
|
return PackageResult{}, ok, err
|
||||||
|
}
|
||||||
|
|
||||||
|
outputHAK := filepath.Join(p.BuildDir(), PackageHAKFileName)
|
||||||
|
outputTLK := filepath.Join(p.BuildDir(), PackageTLKFileName)
|
||||||
|
hakInfo, err := os.Stat(outputHAK)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return PackageResult{}, false, nil
|
||||||
|
}
|
||||||
|
return PackageResult{}, false, fmt.Errorf("stat top package hak %s: %w", outputHAK, err)
|
||||||
|
}
|
||||||
|
tlkInfo, err := os.Stat(outputTLK)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return PackageResult{}, false, nil
|
||||||
|
}
|
||||||
|
return PackageResult{}, false, fmt.Errorf("stat top package tlk %s: %w", outputTLK, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newestSource, _, err := newestTopDataSource(p.TopDataSourceDir())
|
||||||
|
if err != nil {
|
||||||
|
return PackageResult{}, false, err
|
||||||
|
}
|
||||||
|
newestCompiled, err := newestFileModTime(nativeResult.Output2DADir, ".2da")
|
||||||
|
if err != nil {
|
||||||
|
return PackageResult{}, false, err
|
||||||
|
}
|
||||||
|
compiledTLKInfo, err := os.Stat(compiledTLKOutputPath(p))
|
||||||
|
if err != nil {
|
||||||
|
return PackageResult{}, false, fmt.Errorf("stat compiled tlk output %s: %w", compiledTLKOutputPath(p), err)
|
||||||
|
}
|
||||||
|
if compiledTLKInfo.ModTime().After(newestCompiled) {
|
||||||
|
newestCompiled = compiledTLKInfo.ModTime()
|
||||||
|
}
|
||||||
|
newestInput := newestSource
|
||||||
|
if newestCompiled.After(newestInput) {
|
||||||
|
newestInput = newestCompiled
|
||||||
|
}
|
||||||
|
|
||||||
|
oldestPackage := hakInfo.ModTime()
|
||||||
|
if tlkInfo.ModTime().Before(oldestPackage) {
|
||||||
|
oldestPackage = tlkInfo.ModTime()
|
||||||
|
}
|
||||||
|
if !newestInput.IsZero() && newestInput.After(oldestPackage) {
|
||||||
|
return PackageResult{}, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resources, assetFiles, err := collectTopPackageResources(p, nativeResult.Output2DADir)
|
||||||
|
if err != nil {
|
||||||
|
return PackageResult{}, false, err
|
||||||
|
}
|
||||||
|
return PackageResult{
|
||||||
|
Mode: "incremental-noop",
|
||||||
|
Output2DADir: nativeResult.Output2DADir,
|
||||||
|
OutputTLKDir: nativeResult.OutputTLKDir,
|
||||||
|
OutputHAKPath: outputHAK,
|
||||||
|
OutputTLKPath: outputTLK,
|
||||||
|
Files2DA: nativeResult.Files2DA,
|
||||||
|
FilesTLK: nativeResult.FilesTLK,
|
||||||
|
HAKResources: len(resources),
|
||||||
|
AssetFiles: assetFiles,
|
||||||
|
WikiOutputDir: nativeResult.WikiOutputDir,
|
||||||
|
WikiPages: nativeResult.WikiPages,
|
||||||
|
WikiStatus: nativeResult.WikiStatus,
|
||||||
|
}, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newestFileModTime(dir, extension string) (time.Time, error) {
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, fmt.Errorf("read output dir %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
newest := time.Time{}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() || !strings.EqualFold(filepath.Ext(entry.Name()), extension) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
info, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, fmt.Errorf("stat output %s: %w", filepath.Join(dir, entry.Name()), err)
|
||||||
|
}
|
||||||
|
if newest.IsZero() || info.ModTime().After(newest) {
|
||||||
|
newest = info.ModTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newest, nil
|
||||||
|
}
|
||||||
|
|
||||||
func countCompiledFiles(dir, extension string) (int, time.Time, error) {
|
func countCompiledFiles(dir, extension string) (int, time.Time, error) {
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+403
-3
@@ -265,6 +265,7 @@ func ValidateProject(p *project.Project) ValidationReport {
|
|||||||
validateTopPackageAssets(sourceDir, dataDir, &report)
|
validateTopPackageAssets(sourceDir, dataDir, &report)
|
||||||
validateNativeOutputCatalog(dataDir, &report)
|
validateNativeOutputCatalog(dataDir, &report)
|
||||||
validateNativeLockAllocation(dataDir, &report)
|
validateNativeLockAllocation(dataDir, &report)
|
||||||
|
validateGeneratedFeatFamilies(dataDir, &report)
|
||||||
validateItempropsRegistryGraph(dataDir, &report)
|
validateItempropsRegistryGraph(dataDir, &report)
|
||||||
if _, err := os.Stat(statePath); err == nil {
|
if _, err := os.Stat(statePath); err == nil {
|
||||||
report.Files++
|
report.Files++
|
||||||
@@ -289,6 +290,7 @@ func validateJSONFile(path, domain string, report *ValidationReport) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var payload any
|
var payload any
|
||||||
|
validateDuplicateJSONKeys(path, raw, report)
|
||||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
Severity: SeverityError,
|
Severity: SeverityError,
|
||||||
@@ -321,6 +323,62 @@ func validateJSONFile(path, domain string, report *ValidationReport) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateDuplicateJSONKeys(path string, raw []byte, report *ValidationReport) {
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(raw))
|
||||||
|
if err := scanJSONValueForDuplicateKeys(decoder, path, report); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanJSONValueForDuplicateKeys(decoder *json.Decoder, path string, report *ValidationReport) error {
|
||||||
|
token, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch delim := token.(type) {
|
||||||
|
case json.Delim:
|
||||||
|
switch delim {
|
||||||
|
case '{':
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
for decoder.More() {
|
||||||
|
rawKey, err := decoder.Token()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key, ok := rawKey.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("expected object key")
|
||||||
|
}
|
||||||
|
if _, ok := seen[key]; ok {
|
||||||
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
|
Severity: SeverityError,
|
||||||
|
Path: path,
|
||||||
|
Message: fmt.Sprintf("duplicate JSON object key %q", key),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
if err := scanJSONValueForDuplicateKeys(decoder, path, report); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err := decoder.Token()
|
||||||
|
return err
|
||||||
|
case '[':
|
||||||
|
for decoder.More() {
|
||||||
|
if err := scanJSONValueForDuplicateKeys(decoder, path, report); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err := decoder.Token()
|
||||||
|
return err
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func validateDataObject(path string, obj map[string]any, report *ValidationReport) {
|
func validateDataObject(path string, obj map[string]any, report *ValidationReport) {
|
||||||
base := filepath.Base(path)
|
base := filepath.Base(path)
|
||||||
slashed := filepath.ToSlash(path)
|
slashed := filepath.ToSlash(path)
|
||||||
@@ -689,10 +747,16 @@ func validateLegacyDialogEntries(path string, obj map[string]any, report *Valida
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateLockObject(path string, obj map[string]any, report *ValidationReport) {
|
func validateLockObject(path string, obj map[string]any, report *ValidationReport) {
|
||||||
|
ids := map[int]string{}
|
||||||
|
checkDuplicateIDs := !strings.Contains(filepath.ToSlash(path), "/data/itemprops/registry/lock.json")
|
||||||
for key, value := range obj {
|
for key, value := range obj {
|
||||||
|
var rowID int
|
||||||
switch value.(type) {
|
switch value.(type) {
|
||||||
case float64:
|
case float64:
|
||||||
// JSON numbers land here.
|
// JSON numbers land here.
|
||||||
|
if parsed, err := asInt(value); err == nil {
|
||||||
|
rowID = parsed
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
Severity: SeverityError,
|
Severity: SeverityError,
|
||||||
@@ -700,6 +764,19 @@ func validateLockObject(path string, obj map[string]any, report *ValidationRepor
|
|||||||
Message: fmt.Sprintf("lock entry %q must be numeric", key),
|
Message: fmt.Sprintf("lock entry %q must be numeric", key),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if checkDuplicateIDs {
|
||||||
|
if rowID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if other, ok := ids[rowID]; ok && other != key {
|
||||||
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
|
Severity: SeverityError,
|
||||||
|
Path: path,
|
||||||
|
Message: fmt.Sprintf("lock id collision between %q and %q at id %d", other, key, rowID),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ids[rowID] = key
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1030,6 +1107,304 @@ func validateNativeLockAllocation(dataDir string, report *ValidationReport) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type requiredFeatFamily struct {
|
||||||
|
FamilyKey string
|
||||||
|
Dataset string
|
||||||
|
Column string
|
||||||
|
Predicate string
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateGeneratedFeatFamilies(dataDir string, report *ValidationReport) {
|
||||||
|
featDataset, ok, err := discoverNamedNativeDataset(dataDir, "feat")
|
||||||
|
if err != nil {
|
||||||
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
|
Severity: SeverityError,
|
||||||
|
Path: dataDir,
|
||||||
|
Message: fmt.Sprintf("discover feat dataset for generated family validation: %v", err),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lockData, err := loadLockfile(featDataset.LockPath)
|
||||||
|
if err != nil {
|
||||||
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
|
Severity: SeverityError,
|
||||||
|
Path: featDataset.LockPath,
|
||||||
|
Message: fmt.Sprintf("load feat lockfile for generated family validation: %v", err),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, err := newFeatGeneratedContext(featDataset, lockData)
|
||||||
|
if err != nil {
|
||||||
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
|
Severity: SeverityError,
|
||||||
|
Path: featDataset.GeneratedDir,
|
||||||
|
Message: fmt.Sprintf("prepare generated family validation: %v", err),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
specs, specPaths, err := collectGeneratedFamilySpecs(featDataset.GeneratedDir)
|
||||||
|
if err != nil {
|
||||||
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
|
Severity: SeverityError,
|
||||||
|
Path: featDataset.GeneratedDir,
|
||||||
|
Message: fmt.Sprintf("load generated family specs: %v", err),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for familyKey, spec := range specs {
|
||||||
|
ctx.familySpecs[familyKey] = spec
|
||||||
|
}
|
||||||
|
|
||||||
|
required := []requiredFeatFamily{
|
||||||
|
{FamilyKey: "skillfocus", Dataset: "skills", Predicate: "accessible"},
|
||||||
|
{FamilyKey: "greaterskillfocus", Dataset: "skills", Predicate: "accessible"},
|
||||||
|
{FamilyKey: "weaponfocus", Dataset: "baseitems", Column: "WeaponFocusFeat"},
|
||||||
|
{FamilyKey: "weaponspecialization", Dataset: "baseitems", Column: "WeaponSpecializationFeat"},
|
||||||
|
{FamilyKey: "improvedcritical", Dataset: "baseitems", Column: "WeaponImprovedCriticalFeat"},
|
||||||
|
{FamilyKey: "overwhelmingcritical", Dataset: "baseitems", Column: "EpicWeaponOverwhelmingCriticalFeat"},
|
||||||
|
{FamilyKey: "greaterweaponfocus", Dataset: "baseitems", Column: "EpicWeaponFocusFeat"},
|
||||||
|
{FamilyKey: "greaterweaponspecialization", Dataset: "baseitems", Column: "EpicWeaponSpecializationFeat"},
|
||||||
|
{FamilyKey: "weaponofchoice", Dataset: "baseitems", Column: "WeaponOfChoiceFeat"},
|
||||||
|
}
|
||||||
|
for _, requirement := range required {
|
||||||
|
spec, ok := specs[requirement.FamilyKey]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
path := specPaths[requirement.FamilyKey]
|
||||||
|
if spec.ChildSource.Dataset != requirement.Dataset ||
|
||||||
|
spec.ChildSource.Column != requirement.Column ||
|
||||||
|
spec.ChildSource.Predicate != requirement.Predicate {
|
||||||
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
|
Severity: SeverityError,
|
||||||
|
Path: path,
|
||||||
|
Message: fmt.Sprintf("generated feat family %q must use child source dataset=%q column=%q predicate=%q", requirement.FamilyKey, requirement.Dataset, requirement.Column, requirement.Predicate),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
validateGeneratedFeatFamilyCompleteness(path, spec, ctx, report)
|
||||||
|
}
|
||||||
|
validateRequiredProductionGeneratedFamilies(featDataset.GeneratedDir, specs, report)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateRequiredProductionGeneratedFamilies(path string, specs map[string]familyExpansionSpec, report *ValidationReport) {
|
||||||
|
weaponFamilies := []string{
|
||||||
|
"weaponfocus",
|
||||||
|
"weaponspecialization",
|
||||||
|
"improvedcritical",
|
||||||
|
"overwhelmingcritical",
|
||||||
|
"greaterweaponfocus",
|
||||||
|
"greaterweaponspecialization",
|
||||||
|
}
|
||||||
|
for _, familyKey := range weaponFamilies {
|
||||||
|
if _, ok := specs[familyKey]; !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := specs["weaponofchoice"]; !ok {
|
||||||
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
|
Severity: SeverityError,
|
||||||
|
Path: path,
|
||||||
|
Message: "missing generated feat family \"weaponofchoice\"",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoverNamedNativeDataset(dataDir, name string) (nativeDataset, bool, error) {
|
||||||
|
datasets, err := discoverNativeDatasets(dataDir)
|
||||||
|
if err != nil {
|
||||||
|
return nativeDataset{}, false, err
|
||||||
|
}
|
||||||
|
for _, dataset := range datasets {
|
||||||
|
if dataset.Name == name {
|
||||||
|
return dataset, true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nativeDataset{}, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectGeneratedFamilySpecs(generatedDir string) (map[string]familyExpansionSpec, map[string]string, error) {
|
||||||
|
paths, err := collectModulePaths(generatedDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
specs := map[string]familyExpansionSpec{}
|
||||||
|
pathsByFamily := map[string]string{}
|
||||||
|
for _, path := range paths {
|
||||||
|
obj, err := loadJSONObject(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if !isFamilyExpansionObject(obj) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
spec, err := parseFamilyExpansionSpec(path, obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if previous, ok := pathsByFamily[spec.FamilyKey]; ok {
|
||||||
|
return nil, nil, fmt.Errorf("generated family %q is declared by both %s and %s", spec.FamilyKey, previous, path)
|
||||||
|
}
|
||||||
|
specs[spec.FamilyKey] = spec
|
||||||
|
pathsByFamily[spec.FamilyKey] = path
|
||||||
|
}
|
||||||
|
return specs, pathsByFamily, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateGeneratedFeatFamilyCompleteness(path string, spec familyExpansionSpec, ctx *featGeneratedContext, report *ValidationReport) {
|
||||||
|
sourceRows, err := ctx.datasetRows(spec.ChildSource.Dataset)
|
||||||
|
if err != nil {
|
||||||
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
|
Severity: SeverityError,
|
||||||
|
Path: path,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
moduleData, err := buildFamilyExpansionGeneratedModule(path, map[string]any{
|
||||||
|
"family": spec.Family,
|
||||||
|
"family_key": spec.FamilyKey,
|
||||||
|
"template": spec.Template,
|
||||||
|
"name_prefix": spec.NamePrefix,
|
||||||
|
"label_prefix": spec.LabelPrefix,
|
||||||
|
"constant_prefix": spec.ConstantPrefix,
|
||||||
|
"template_fields": validationStringSliceToAny(spec.TemplateFields),
|
||||||
|
"default_fields": spec.DefaultFields,
|
||||||
|
"apply_after_modules": spec.ApplyAfterModules,
|
||||||
|
"child_ref_field": spec.ChildRefField,
|
||||||
|
"identity_source": spec.IdentitySource,
|
||||||
|
"allow_existing_only": spec.AllowExistingOnly,
|
||||||
|
"auto_prereq_fields": stringMapToAny(spec.AutoPrereqFields),
|
||||||
|
"child_source": map[string]any{
|
||||||
|
"dataset": spec.ChildSource.Dataset,
|
||||||
|
"column": spec.ChildSource.Column,
|
||||||
|
"predicate": spec.ChildSource.Predicate,
|
||||||
|
},
|
||||||
|
"overrides": map[string]any{},
|
||||||
|
}, ctx)
|
||||||
|
if err != nil {
|
||||||
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
|
Severity: SeverityError,
|
||||||
|
Path: path,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expected := map[string]struct{}{}
|
||||||
|
familyAllowlist := spec.AllowExistingOnly && ctx.familyHasExistingRows(spec.FamilyKey)
|
||||||
|
for sourceKey, row := range sourceRows {
|
||||||
|
include, err := familyExpansionSourceEligible(spec, row)
|
||||||
|
if err != nil {
|
||||||
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
|
Severity: SeverityError,
|
||||||
|
Path: path,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if include {
|
||||||
|
if familyAllowlist {
|
||||||
|
slug := strings.TrimPrefix(sourceKey, spec.ChildSource.Dataset+":")
|
||||||
|
featKey, _, _, err := ctx.resolveGeneratedFeatIdentity(spec, slug, row)
|
||||||
|
if err != nil {
|
||||||
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
|
Severity: SeverityError,
|
||||||
|
Path: path,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !ctx.featKeyExists(featKey) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expected[sourceKey] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actual := map[string]string{}
|
||||||
|
overrides, _ := moduleData["overrides"].([]any)
|
||||||
|
for index, raw := range overrides {
|
||||||
|
override, ok := raw.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
source, ok := familySourceFromMeta(override["meta"])
|
||||||
|
if !ok || source == "" {
|
||||||
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
|
Severity: SeverityError,
|
||||||
|
Path: path,
|
||||||
|
Message: fmt.Sprintf("generated family %q row %d is missing family source metadata", spec.FamilyKey, index),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if previousKey, ok := actual[source]; ok {
|
||||||
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
|
Severity: SeverityError,
|
||||||
|
Path: path,
|
||||||
|
Message: fmt.Sprintf("generated family %q emits source %q more than once (%s and row %d)", spec.FamilyKey, source, previousKey, index),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
key, _ := override["key"].(string)
|
||||||
|
actual[source] = key
|
||||||
|
}
|
||||||
|
for source := range expected {
|
||||||
|
if _, ok := actual[source]; !ok {
|
||||||
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
|
Severity: SeverityError,
|
||||||
|
Path: path,
|
||||||
|
Message: fmt.Sprintf("generated family %q is missing source %q", spec.FamilyKey, source),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for source := range actual {
|
||||||
|
if _, ok := expected[source]; !ok {
|
||||||
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
|
Severity: SeverityError,
|
||||||
|
Path: path,
|
||||||
|
Message: fmt.Sprintf("generated family %q includes unexpected source %q", spec.FamilyKey, source),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func familySourceFromMeta(raw any) (string, bool) {
|
||||||
|
meta, ok := raw.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
family, ok := meta["family"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
source, ok := family["source"].(string)
|
||||||
|
return source, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func validationStringSliceToAny(items []string) []any {
|
||||||
|
if items == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]any, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
out = append(out, item)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringMapToAny(items map[string]string) map[string]any {
|
||||||
|
if items == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make(map[string]any, len(items))
|
||||||
|
for key, value := range items {
|
||||||
|
out[key] = value
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func validateTopPackageAssets(sourceDir, dataDir string, report *ValidationReport) {
|
func validateTopPackageAssets(sourceDir, dataDir string, report *ValidationReport) {
|
||||||
assetsDir := filepath.Join(sourceDir, "assets")
|
assetsDir := filepath.Join(sourceDir, "assets")
|
||||||
generated2DADir := filepath.Join(assetsDir, "2da")
|
generated2DADir := filepath.Join(assetsDir, "2da")
|
||||||
@@ -1237,6 +1612,8 @@ func restoreLockfiles(snapshot lockfileSnapshot) {
|
|||||||
|
|
||||||
func validateRowCollection(path string, obj map[string]any, rows []any, report *ValidationReport) {
|
func validateRowCollection(path string, obj map[string]any, rows []any, report *ValidationReport) {
|
||||||
columns := extractValidationColumns(obj)
|
columns := extractValidationColumns(obj)
|
||||||
|
declaredKeys := map[string]int{}
|
||||||
|
checkDuplicateKeys := filepath.Base(path) != "base.json"
|
||||||
rowHasIdentity := func(row map[string]any, fallbackID int) (string, bool) {
|
rowHasIdentity := func(row map[string]any, fallbackID int) (string, bool) {
|
||||||
if key, ok := row["key"].(string); ok && strings.TrimSpace(key) != "" {
|
if key, ok := row["key"].(string); ok && strings.TrimSpace(key) != "" {
|
||||||
return key, true
|
return key, true
|
||||||
@@ -1252,6 +1629,18 @@ func validateRowCollection(path string, obj map[string]any, rows []any, report *
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if checkDuplicateKeys {
|
||||||
|
if key, ok := row["key"].(string); ok && strings.TrimSpace(key) != "" {
|
||||||
|
if previous, ok := declaredKeys[key]; ok {
|
||||||
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
|
Severity: SeverityError,
|
||||||
|
Path: path,
|
||||||
|
Message: fmt.Sprintf("row %d repeats key %q already declared by row %d", index, key, previous),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
declaredKeys[key] = index
|
||||||
|
}
|
||||||
|
}
|
||||||
for key := range row {
|
for key := range row {
|
||||||
if isAuthoringOnlyField(key) && !preserveLegacyWikiColumnForPath(path, key) {
|
if isAuthoringOnlyField(key) && !preserveLegacyWikiColumnForPath(path, key) {
|
||||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||||
@@ -1604,7 +1993,7 @@ func compareNativeSelfCheck(p *project.Project, actual2DA, actualTLK string, pro
|
|||||||
if !slices.Equal(actual2DAFiles, expected2DAFiles) {
|
if !slices.Equal(actual2DAFiles, expected2DAFiles) {
|
||||||
mismatch += symmetricDiffCount(actual2DAFiles, expected2DAFiles)
|
mismatch += symmetricDiffCount(actual2DAFiles, expected2DAFiles)
|
||||||
}
|
}
|
||||||
comparedTLK, tlkMismatch, err := compareExactDirFiles(actualTLK, nativeResult.OutputTLKDir)
|
comparedTLK, tlkMismatch, err := compareExactDirFilesByExtension(actualTLK, nativeResult.OutputTLKDir, ".tlk")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, 0, 0, err
|
return 0, 0, 0, 0, err
|
||||||
}
|
}
|
||||||
@@ -1732,6 +2121,10 @@ func ioDiscardProgress(progress func(string)) *progressWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func listRelativeFiles(root string) ([]string, error) {
|
func listRelativeFiles(root string) ([]string, error) {
|
||||||
|
return listRelativeFilesByExtension(root, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func listRelativeFilesByExtension(root, extension string) ([]string, error) {
|
||||||
files := make([]string, 0)
|
files := make([]string, 0)
|
||||||
if _, err := os.Stat(root); err != nil {
|
if _, err := os.Stat(root); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -1746,6 +2139,9 @@ func listRelativeFiles(root string) ([]string, error) {
|
|||||||
if d.IsDir() {
|
if d.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if extension != "" && !strings.EqualFold(filepath.Ext(path), extension) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
rel, err := filepath.Rel(root, path)
|
rel, err := filepath.Rel(root, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1784,11 +2180,15 @@ func symmetricDiffCount(left, right []string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func compareExactDirFiles(actualRoot, expectedRoot string) (int, int, error) {
|
func compareExactDirFiles(actualRoot, expectedRoot string) (int, int, error) {
|
||||||
actualFiles, err := listRelativeFiles(actualRoot)
|
return compareExactDirFilesByExtension(actualRoot, expectedRoot, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareExactDirFilesByExtension(actualRoot, expectedRoot, extension string) (int, int, error) {
|
||||||
|
actualFiles, err := listRelativeFilesByExtension(actualRoot, extension)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
expectedFiles, err := listRelativeFiles(expectedRoot)
|
expectedFiles, err := listRelativeFilesByExtension(expectedRoot, extension)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,26 @@ func TestValidateProjectAcceptsInlineTLKSchema(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateProjectRejectsDuplicateJSONKeys(t *testing.T) {
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust"))
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "data", "repadjust", "base.json"), `{
|
||||||
|
"output": "repadjust.2da",
|
||||||
|
"output": "other.2da",
|
||||||
|
"columns": ["Label"],
|
||||||
|
"rows": [{"id": 0, "Label": "TEST_LABEL"}]
|
||||||
|
}`+"\n")
|
||||||
|
|
||||||
|
report := ValidateProject(testProject(root))
|
||||||
|
if !report.HasErrors() {
|
||||||
|
t.Fatal("expected duplicate JSON key validation error")
|
||||||
|
}
|
||||||
|
if !diagnosticsContain(report.Diagnostics, "duplicate JSON object key") {
|
||||||
|
t.Fatalf("expected duplicate JSON object key diagnostic, got %#v", report.Diagnostics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolvedTableRegistryRegistersConsistentTables(t *testing.T) {
|
func TestResolvedTableRegistryRegistersConsistentTables(t *testing.T) {
|
||||||
collected := []nativeCollectedDataset{
|
collected := []nativeCollectedDataset{
|
||||||
{
|
{
|
||||||
@@ -9085,6 +9105,72 @@ func TestBuildAndPackageIncludesCompiled2DAAndTopAssets(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildAndPackageNoOpsWhenOutputsAreCurrent(t *testing.T) {
|
||||||
|
root := topPackageTestProject(t)
|
||||||
|
proj := testProject(root)
|
||||||
|
proj.Config.TopData.ReferenceBuilder = ""
|
||||||
|
|
||||||
|
result, err := BuildAndPackage(proj, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("initial BuildAndPackage failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceTime := time.Now().Add(-4 * time.Hour)
|
||||||
|
outputTime := time.Now().Add(-2 * time.Hour)
|
||||||
|
setTopDataSourceTimes(t, root, sourceTime)
|
||||||
|
setBuildOutputTimes(t, result, outputTime)
|
||||||
|
|
||||||
|
beforeHAK := mustStat(t, result.OutputHAKPath).ModTime()
|
||||||
|
beforeTLK := mustStat(t, result.OutputTLKPath).ModTime()
|
||||||
|
incremental, err := BuildAndPackage(proj, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("incremental BuildAndPackage failed: %v", err)
|
||||||
|
}
|
||||||
|
if incremental.Mode != "incremental-noop" {
|
||||||
|
t.Fatalf("expected incremental-noop mode, got %q", incremental.Mode)
|
||||||
|
}
|
||||||
|
if after := mustStat(t, result.OutputHAKPath).ModTime(); !after.Equal(beforeHAK) {
|
||||||
|
t.Fatalf("expected hak mtime to be unchanged, got %s want %s", after, beforeHAK)
|
||||||
|
}
|
||||||
|
if after := mustStat(t, result.OutputTLKPath).ModTime(); !after.Equal(beforeTLK) {
|
||||||
|
t.Fatalf("expected tlk mtime to be unchanged, got %s want %s", after, beforeTLK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildAndPackageRefreshesPackageWhenCompiledOutputsAreCurrent(t *testing.T) {
|
||||||
|
root := topPackageTestProject(t)
|
||||||
|
proj := testProject(root)
|
||||||
|
proj.Config.TopData.ReferenceBuilder = ""
|
||||||
|
|
||||||
|
result, err := BuildAndPackage(proj, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("initial BuildAndPackage failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceTime := time.Now().Add(-6 * time.Hour)
|
||||||
|
compiledTime := time.Now().Add(-4 * time.Hour)
|
||||||
|
packageTime := time.Now().Add(-8 * time.Hour)
|
||||||
|
setTopDataSourceTimes(t, root, sourceTime)
|
||||||
|
setCompiledOutputTimes(t, result, compiledTime)
|
||||||
|
setFileTime(t, result.OutputHAKPath, packageTime)
|
||||||
|
|
||||||
|
compiled2DA := filepath.Join(result.Output2DADir, "repadjust.2da")
|
||||||
|
before2DA := mustStat(t, compiled2DA).ModTime()
|
||||||
|
refreshed, err := BuildAndPackage(proj, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("package refresh BuildAndPackage failed: %v", err)
|
||||||
|
}
|
||||||
|
if refreshed.Mode != "native" {
|
||||||
|
t.Fatalf("expected native mode from packaged compiled output, got %q", refreshed.Mode)
|
||||||
|
}
|
||||||
|
if after := mustStat(t, compiled2DA).ModTime(); !after.Equal(before2DA) {
|
||||||
|
t.Fatalf("expected compiled 2da mtime to be unchanged, got %s want %s", after, before2DA)
|
||||||
|
}
|
||||||
|
if after := mustStat(t, result.OutputHAKPath).ModTime(); !after.After(packageTime) {
|
||||||
|
t.Fatalf("expected hak to be refreshed after %s, got %s", packageTime, after)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNativeCompileGroupStatsCountSourceFragmentsForBaseDatasets(t *testing.T) {
|
func TestNativeCompileGroupStatsCountSourceFragmentsForBaseDatasets(t *testing.T) {
|
||||||
root := testProjectRoot(t)
|
root := testProjectRoot(t)
|
||||||
modulesDir := filepath.Join(root, "placeables", "modules")
|
modulesDir := filepath.Join(root, "placeables", "modules")
|
||||||
@@ -9136,6 +9222,80 @@ func TestNativeCompileGroupStatsCountSourceFragmentsForBaseDatasets(t *testing.T
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func topPackageTestProject(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
root := testProjectRoot(t)
|
||||||
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust"))
|
||||||
|
mkdirAll(t, filepath.Join(root, "topdata", "assets", "gui"))
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "data", "repadjust", "base.json"), `{
|
||||||
|
"output": "repadjust.2da",
|
||||||
|
"columns": ["Label"],
|
||||||
|
"rows": [{"id": 0, "Label": "TEST_LABEL"}]
|
||||||
|
}`+"\n")
|
||||||
|
writeFile(t, filepath.Join(root, "topdata", "assets", "gui", "testicon.png"), "icon-data")
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
func setTopDataSourceTimes(t *testing.T, root string, modTime time.Time) {
|
||||||
|
t.Helper()
|
||||||
|
setTreeFileTimes(t, filepath.Join(root, "topdata"), modTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setBuildOutputTimes(t *testing.T, result PackageResult, modTime time.Time) {
|
||||||
|
t.Helper()
|
||||||
|
setCompiledOutputTimes(t, result, modTime)
|
||||||
|
setFileTime(t, result.OutputHAKPath, modTime)
|
||||||
|
setFileTime(t, result.OutputTLKPath, modTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCompiledOutputTimes(t *testing.T, result PackageResult, modTime time.Time) {
|
||||||
|
t.Helper()
|
||||||
|
setTreeFileTimes(t, result.Output2DADir, modTime)
|
||||||
|
setTreeFileTimes(t, result.OutputTLKDir, modTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setTreeFileTimes(t *testing.T, root string, modTime time.Time) {
|
||||||
|
t.Helper()
|
||||||
|
err := filepath.WalkDir(root, func(path string, d os.DirEntry, walkErr error) error {
|
||||||
|
if walkErr != nil {
|
||||||
|
return walkErr
|
||||||
|
}
|
||||||
|
if d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return os.Chtimes(path, modTime, modTime)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("set file times under %s: %v", root, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setFileTime(t *testing.T, path string, modTime time.Time) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.Chtimes(path, modTime, modTime); err != nil {
|
||||||
|
t.Fatalf("set file time %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustStat(t *testing.T, path string) os.FileInfo {
|
||||||
|
t.Helper()
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("stat %s: %v", path, err)
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func diagnosticsContain(diagnostics []Diagnostic, text string) bool {
|
||||||
|
for _, diagnostic := range diagnostics {
|
||||||
|
if strings.Contains(diagnostic.Message, text) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateProjectAcceptsPNGTopPackageAsset(t *testing.T) {
|
func TestValidateProjectAcceptsPNGTopPackageAsset(t *testing.T) {
|
||||||
root := testProjectRoot(t)
|
root := testProjectRoot(t)
|
||||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust"))
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust"))
|
||||||
|
|||||||
Reference in New Issue
Block a user