From 9384d905d1a41e466d6cf7242d3efbb78e70c1cf Mon Sep 17 00:00:00 2001 From: vickydotbat Date: Thu, 14 May 2026 15:11:08 +0200 Subject: [PATCH] Enable plain TTY logging mode + loose-data processing --- internal/app/app.go | 20 ++++++++++-- internal/app/app_test.go | 23 +++++++++++-- internal/topdata/native.go | 55 ++++++++++++++++++++++---------- internal/topdata/top_package.go | 3 ++ internal/topdata/topdata_test.go | 42 ++++++++++++++++++++---- 5 files changed, 115 insertions(+), 28 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index 7f4cd26..e4235f2 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -139,6 +139,20 @@ func isInteractiveTTY(w io.Writer) bool { return (info.Mode() & os.ModeCharDevice) != 0 } +func spinnerEnabledFor(w io.Writer, level logLevel) bool { + mode := strings.TrimSpace(strings.ToLower(os.Getenv("SOW_TOOLS_TTY_MODE"))) + switch mode { + case "plain", "off", "0", "false", "no": + return false + case "spinner", "on", "1", "true", "yes": + return level == logLevelNormal && isInteractiveTTY(w) + case "", "auto": + return isInteractiveTTY(w) && strings.TrimSpace(os.Getenv("CI")) == "" && level == logLevelNormal + default: + return isInteractiveTTY(w) && strings.TrimSpace(os.Getenv("CI")) == "" && level == logLevelNormal + } +} + var commands = []command{ { name: "build", @@ -605,7 +619,7 @@ func newProjectConsole(ctx context, p *project.Project, commandName string) *pro commandName: commandName, commandLabel: projectCommandLabel(commandName), level: ctx.logLevel, - spinnerEnabled: isInteractiveTTY(ctx.stderr) && strings.TrimSpace(os.Getenv("CI")) == "" && ctx.logLevel == logLevelNormal, + spinnerEnabled: spinnerEnabledFor(ctx.stderr, ctx.logLevel), } } @@ -787,7 +801,7 @@ func newBuildHAKConsole(ctx context, p *project.Project, opts buildHAKOptions) * projectName: p.Config.Module.Name, planOnly: opts.planOnly, level: opts.logLevel, - spinnerEnabled: isInteractiveTTY(ctx.stderr) && strings.TrimSpace(os.Getenv("CI")) == "" && opts.logLevel == logLevelNormal, + spinnerEnabled: spinnerEnabledFor(ctx.stderr, opts.logLevel), } } @@ -962,7 +976,7 @@ func newTopdataConsole(ctx context, p *project.Project, commandName string) *top commandName: commandName, commandLabel: topdataCommandLabel(commandName), level: ctx.logLevel, - spinnerEnabled: isInteractiveTTY(ctx.stderr) && strings.TrimSpace(os.Getenv("CI")) == "" && ctx.logLevel == logLevelNormal, + spinnerEnabled: spinnerEnabledFor(ctx.stderr, ctx.logLevel), } } diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 67df544..f42d6ef 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -35,8 +35,7 @@ topdata: sourceTime := time.Now().Add(-2 * time.Hour) outputTime := time.Now().Add(-1 * time.Hour) - setFileTime(t, filepath.Join(root, "topdata", "assets", "gui", "testicon.png"), sourceTime) - setFileTime(t, filepath.Join(root, "topdata", "data", "repadjust", "base.json"), sourceTime) + setTreeTime(t, filepath.Join(root, "topdata"), sourceTime) setFileTime(t, filepath.Join(root, ".cache", "2da", "repadjust.2da"), outputTime) setFileTime(t, filepath.Join(root, "build", "sow_tlk.tlk"), outputTime) @@ -130,6 +129,19 @@ haks: } } +func setTreeTime(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 + } + return os.Chtimes(path, modTime, modTime) + }) + if err != nil { + t.Fatalf("set tree time under %s: %v", root, err) + } +} + func TestRunBuildHAKsVerboseListsMappings(t *testing.T) { root := t.TempDir() mkdirAll(t, filepath.Join(root, "assets", "envi", "music", "westgate")) @@ -212,6 +224,13 @@ func TestTopdataConsoleSuppressesProgressInNormalMode(t *testing.T) { } } +func TestSpinnerEnabledForHonorsPlainTTYMode(t *testing.T) { + t.Setenv("SOW_TOOLS_TTY_MODE", "plain") + if spinnerEnabledFor(&bytes.Buffer{}, logLevelNormal) { + t.Fatal("expected plain tty mode to disable spinner output") + } +} + func TestTopdataConsoleDebugProgressAndRelativePaths(t *testing.T) { var stdout bytes.Buffer console := &topdataConsole{ diff --git a/internal/topdata/native.go b/internal/topdata/native.go index 66edc85..cc26e70 100644 --- a/internal/topdata/native.go +++ b/internal/topdata/native.go @@ -503,6 +503,27 @@ func countModuleFiles(dir string) int { func discoverNativeDatasets(dataDir string) ([]nativeDataset, error) { var datasets []nativeDataset + appendPlainDataset := func(rootPath, filePath, relName string) error { + tableData, err := loadJSONObject(filePath) + if err != nil { + return err + } + outputName, _ := tableData["output"].(string) + if strings.TrimSpace(outputName) == "" { + outputName = nativeDatasetDefaultOutputName(filePath, nativeDatasetPlain) + } + datasets = append(datasets, nativeDataset{ + Kind: nativeDatasetPlain, + Name: filepath.ToSlash(relName), + RootPath: rootPath, + BasePath: filePath, + LockPath: filepath.Join(rootPath, "lock.json"), + OutputName: outputName, + Spec: specForDataset(filepath.ToSlash(relName)), + CompareReference: optionalBool(tableData["compare_reference"], true), + }) + return nil + } err := filepath.WalkDir(dataDir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err @@ -519,6 +540,23 @@ func discoverNativeDatasets(dataDir string) ([]nativeDataset, error) { return err } if rel == "." { + entries, err := os.ReadDir(path) + if err != nil { + return err + } + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + if !strings.HasSuffix(strings.ToLower(name), ".json") || name == "lock.json" || name == "base.json" { + continue + } + filePath := filepath.Join(path, name) + if err := appendPlainDataset(path, filePath, strings.TrimSuffix(name, filepath.Ext(name))); err != nil { + return err + } + } return nil } if filepath.ToSlash(rel) == "parts/overrides" { @@ -564,24 +602,9 @@ func discoverNativeDatasets(dataDir string) ([]nativeDataset, error) { name := entry.Name() if strings.HasSuffix(strings.ToLower(name), ".json") && name != "lock.json" { filePath := filepath.Join(path, name) - tableData, err := loadJSONObject(filePath) - if err != nil { + if err := appendPlainDataset(path, filePath, filepath.Join(rel, strings.TrimSuffix(name, filepath.Ext(name)))); err != nil { return err } - outputName, _ := tableData["output"].(string) - if strings.TrimSpace(outputName) == "" { - outputName = nativeDatasetDefaultOutputName(filePath, nativeDatasetPlain) - } - datasets = append(datasets, nativeDataset{ - Kind: nativeDatasetPlain, - Name: filepath.ToSlash(filepath.Join(rel, strings.TrimSuffix(name, filepath.Ext(name)))), - RootPath: path, - BasePath: filePath, - LockPath: filepath.Join(path, "lock.json"), - OutputName: outputName, - Spec: specForDataset(filepath.ToSlash(filepath.Join(rel, strings.TrimSuffix(name, filepath.Ext(name))))), - CompareReference: optionalBool(tableData["compare_reference"], true), - }) } } return nil diff --git a/internal/topdata/top_package.go b/internal/topdata/top_package.go index 9c08230..d398e0e 100644 --- a/internal/topdata/top_package.go +++ b/internal/topdata/top_package.go @@ -267,6 +267,9 @@ func newestTopDataSource(p *project.Project) (time.Time, string, error) { if d.IsDir() && shouldSkipTopDataSourceDir(path, skipDirs) { return filepath.SkipDir } + if path == sourceDir { + return nil + } info, err := d.Info() if err != nil { return err diff --git a/internal/topdata/topdata_test.go b/internal/topdata/topdata_test.go index 9aa0eff..e58b692 100644 --- a/internal/topdata/topdata_test.go +++ b/internal/topdata/topdata_test.go @@ -6892,7 +6892,7 @@ func TestBuildGeneratedApplyAfterPrunesLowLockedID(t *testing.T) { } } -func TestBuildOverrideNullValueKeyNullifiesEntireRow(t *testing.T) { +func TestBuildOverrideAsteriskKeyRemovesIdentityWithoutNullingRow(t *testing.T) { root := testProjectRoot(t) mkdirAll(t, filepath.Join(root, "topdata", "data", "dense", "modules")) writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n") @@ -6925,8 +6925,8 @@ func TestBuildOverrideNullValueKeyNullifiesEntireRow(t *testing.T) { if err != nil { t.Fatalf("read dense.2da: %v", err) } - if !strings.Contains(string(got), "7\t****\t****\n") { - t.Fatalf("expected override key \"****\" to nullify row 7, got:\n%s", string(got)) + if !strings.Contains(string(got), "7\tShouldNotLeak\tstill-not-kept\n") { + t.Fatalf("expected override key \"****\" to remove identity without blanking row 7, got:\n%s", string(got)) } lockRaw, err := os.ReadFile(filepath.Join(root, "topdata", "data", "dense", "lock.json")) if err != nil { @@ -11084,13 +11084,13 @@ func setTreeFileTimes(t *testing.T, root string, modTime time.Time) { if walkErr != nil { return walkErr } - if d.IsDir() { - return nil + if err := os.Chtimes(path, modTime, modTime); err != nil { + return err } - return os.Chtimes(path, modTime, modTime) + return nil }) if err != nil { - t.Fatalf("set file times under %s: %v", root, err) + t.Fatalf("set tree times under %s: %v", root, err) } } @@ -11302,6 +11302,34 @@ func TestValidateProjectWarnsWhenLockKeepsNormalizedEquivalentKeysAlive(t *testi } } +func TestBuildNativeSupportsLoosePlainTableAtTopdataDataRoot(t *testing.T) { + root := testProjectRoot(t) + mkdirAll(t, filepath.Join(root, "topdata", "data")) + writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n") + writeFile(t, filepath.Join(root, "topdata", "data", "ruleset.json"), `{ + "columns": ["Name", "Value"], + "rows": [ + {"id": 0, "Name": "TEST_RULE", "Value": "1"} + ] +}`+"\n") + + proj := testProject(root) + proj.Config.TopData.ReferenceBuilder = "" + + report := ValidateProject(proj) + if report.HasErrors() { + t.Fatalf("expected loose root table to validate cleanly, got:\n%s", diagnosticsText(report.Diagnostics)) + } + + result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil) + if err != nil { + t.Fatalf("expected loose root table to build: %v", err) + } + if _, err := os.Stat(filepath.Join(result.Output2DADir, "ruleset.2da")); err != nil { + t.Fatalf("expected ruleset.2da output for loose root table: %v", err) + } +} + func TestValidateProjectErrorsOnTopPackageAssetCollision(t *testing.T) { root := testProjectRoot(t) mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust"))