From fc20b2fb157602518af8c7e68cfcfbdb224b406b Mon Sep 17 00:00:00 2001 From: Cheviiot <153805936+Cheviiot@users.noreply.github.com> Date: Sat, 25 Jul 2026 10:41:48 +1000 Subject: [PATCH] Add test coverage for cmd/vintner and internal/i18n Both were at 0% coverage. Focused on what's safely testable without touching the network or filesystem: flag validation (--architecture/ --host-arch, the same guard added in the stability pass), subcommand dispatch and aliases, help/usage error paths, and - for i18n - full language-detection table coverage plus a completeness check that every catalog key has both an EN and RU entry (an English-only or Russian-only entry would silently degrade rather than fail loudly, so this is worth locking in). cmd/vintner: 0% -> 28.8%, i18n: 0% -> 94.1%. --- cmd/vintner/download_test.go | 40 ++++++++++++++++ cmd/vintner/env_test.go | 23 +++++++++ cmd/vintner/install_test.go | 17 +++++++ cmd/vintner/main_test.go | 30 ++++++++++++ cmd/vintner/paths_test.go | 22 +++++++++ internal/i18n/i18n_test.go | 93 ++++++++++++++++++++++++++++++++++++ 6 files changed, 225 insertions(+) create mode 100644 cmd/vintner/download_test.go create mode 100644 cmd/vintner/env_test.go create mode 100644 cmd/vintner/install_test.go create mode 100644 cmd/vintner/main_test.go create mode 100644 cmd/vintner/paths_test.go create mode 100644 internal/i18n/i18n_test.go diff --git a/cmd/vintner/download_test.go b/cmd/vintner/download_test.go new file mode 100644 index 0000000..f094e25 --- /dev/null +++ b/cmd/vintner/download_test.go @@ -0,0 +1,40 @@ +package main + +import "testing" + +// TestRunDownloadRejectsInvalidArchFlags exercises the validation added +// after the flags are parsed, which must reject typos before runDownload +// gets anywhere near the network (FetchChannelManifest) - these tests would +// hang/fail on network access if that ordering ever regressed. +func TestRunDownloadRejectsInvalidArchFlags(t *testing.T) { + for _, tc := range []struct { + name string + args []string + }{ + {"bad architecture", []string{"--architecture", "x866"}}, + {"bad architecture, valid mixed with invalid", []string{"--architecture", "x64", "--architecture", "sparc"}}, + {"bad host-arch", []string{"--host-arch", "sparc"}}, + } { + t.Run(tc.name, func(t *testing.T) { + if code := runDownload(tc.args); code != 2 { + t.Errorf("runDownload(%v) = %d, want 2", tc.args, code) + } + }) + } +} + +func TestValidArchitectureSets(t *testing.T) { + for _, a := range []string{"x86", "x64", "arm", "arm64", "host"} { + if !validArchitectures[a] { + t.Errorf("validArchitectures[%q] = false, want true", a) + } + } + for _, a := range []string{"x86", "x64", "arm64"} { + if !validHostArchs[a] { + t.Errorf("validHostArchs[%q] = false, want true", a) + } + } + if validHostArchs["arm"] { + t.Error(`validHostArchs["arm"] = true, want false (no 32-bit ARM host toolchain exists)`) + } +} diff --git a/cmd/vintner/env_test.go b/cmd/vintner/env_test.go new file mode 100644 index 0000000..b7c50e5 --- /dev/null +++ b/cmd/vintner/env_test.go @@ -0,0 +1,23 @@ +package main + +import "testing" + +func TestRunEnvRequiresBin(t *testing.T) { + if code := runEnv(nil); code != 1 { + t.Errorf("runEnv(nil) = %d, want 1", code) + } +} + +func TestRunEnvRejectsMissingBinDir(t *testing.T) { + if code := runEnv([]string{"--bin", "/nonexistent/path/for/vintner/tests"}); code != 1 { + t.Errorf("runEnv with a nonexistent --bin = %d, want 1", code) + } +} + +func TestToUnixPathList(t *testing.T) { + got := toUnixPathList(`z:\vc\include;z:\kits\10\include`) + want := "/vc/include;/kits/10/include" + if got != want { + t.Errorf("toUnixPathList(...) = %q, want %q", got, want) + } +} diff --git a/cmd/vintner/install_test.go b/cmd/vintner/install_test.go new file mode 100644 index 0000000..497ec65 --- /dev/null +++ b/cmd/vintner/install_test.go @@ -0,0 +1,17 @@ +package main + +import "testing" + +func TestRunInstallRejectsExtraArgs(t *testing.T) { + if code := runInstall([]string{"one", "two"}); code != 1 { + t.Errorf("runInstall with two args = %d, want 1", code) + } +} + +func TestRunInstallHelp(t *testing.T) { + for _, flag := range []string{"-h", "--help"} { + if code := runInstall([]string{flag}); code != 1 { + t.Errorf("runInstall([%q]) = %d, want 1", flag, code) + } + } +} diff --git a/cmd/vintner/main_test.go b/cmd/vintner/main_test.go new file mode 100644 index 0000000..a0f0d4d --- /dev/null +++ b/cmd/vintner/main_test.go @@ -0,0 +1,30 @@ +package main + +import "testing" + +// TestRunCLIDispatch covers the subset of runCLI's switch that has no side +// effects (no filesystem/network touched) - the actual subcommand bodies +// (download/install/env) get their own focused tests. +func TestRunCLIDispatch(t *testing.T) { + for _, tc := range []struct { + name string + args []string + want int + }{ + {"no args prints usage", nil, 1}, + {"unknown subcommand", []string{"frobnicate"}, 1}, + {"help long", []string{"--help"}, 0}, + {"help short flag", []string{"-h"}, 0}, + {"help word", []string{"help"}, 0}, + {"help alias", []string{"h"}, 0}, + {"version word", []string{"version"}, 0}, + {"version alias", []string{"v"}, 0}, + {"version flag", []string{"--version"}, 0}, + } { + t.Run(tc.name, func(t *testing.T) { + if got := runCLI(tc.args); got != tc.want { + t.Errorf("runCLI(%v) = %d, want %d", tc.args, got, tc.want) + } + }) + } +} diff --git a/cmd/vintner/paths_test.go b/cmd/vintner/paths_test.go new file mode 100644 index 0000000..9b1643a --- /dev/null +++ b/cmd/vintner/paths_test.go @@ -0,0 +1,22 @@ +package main + +import ( + "os" + "path/filepath" + "testing" +) + +func TestDefaultToolchainDir(t *testing.T) { + home, err := os.UserHomeDir() + if err != nil { + t.Skipf("no home directory available: %v", err) + } + got, err := defaultToolchainDir() + if err != nil { + t.Fatalf("defaultToolchainDir() error: %v", err) + } + want := filepath.Join(home, ".vintner") + if got != want { + t.Errorf("defaultToolchainDir() = %q, want %q", got, want) + } +} diff --git a/internal/i18n/i18n_test.go b/internal/i18n/i18n_test.go new file mode 100644 index 0000000..b72c104 --- /dev/null +++ b/internal/i18n/i18n_test.go @@ -0,0 +1,93 @@ +package i18n + +import ( + "strings" + "testing" +) + +func TestDetect(t *testing.T) { + envKeys := []string{"VINTNER_LANG", "LC_ALL", "LC_MESSAGES", "LANG"} + clear := func() { + for _, k := range envKeys { + t.Setenv(k, "") + // t.Setenv("", "") leaves the var set-but-empty, which detect() + // already treats as "unset" (its loop skips v == "") - matches + // how a genuinely-unset env var behaves for this function. + } + } + + for _, tc := range []struct { + name string + env map[string]string + want Lang + }{ + {"nothing set defaults to English", nil, EN}, + {"VINTNER_LANG=ru", map[string]string{"VINTNER_LANG": "ru"}, RU}, + {"VINTNER_LANG=en", map[string]string{"VINTNER_LANG": "en"}, EN}, + {"VINTNER_LANG wins over a Russian LANG", map[string]string{"VINTNER_LANG": "en", "LANG": "ru_RU.UTF-8"}, EN}, + {"LC_ALL wins over LANG", map[string]string{"LC_ALL": "ru_RU.UTF-8", "LANG": "en_US.UTF-8"}, RU}, + {"LANG=ru_RU.UTF-8 alone", map[string]string{"LANG": "ru_RU.UTF-8"}, RU}, + {"LANG=en_US.UTF-8 alone", map[string]string{"LANG": "en_US.UTF-8"}, EN}, + {"unrelated locale defaults to English", map[string]string{"LANG": "de_DE.UTF-8"}, EN}, + {"case-insensitive RU prefix", map[string]string{"VINTNER_LANG": "RU"}, RU}, + } { + t.Run(tc.name, func(t *testing.T) { + clear() + for k, v := range tc.env { + t.Setenv(k, v) + } + if got := detect(); got != tc.want { + t.Errorf("detect() = %q, want %q", got, tc.want) + } + }) + } +} + +// TestCatalogCompleteness guards against adding an EN string without its RU +// counterpart (or vice versa) - a silent gap here degrades to showing the +// wrong language's text via T()'s EN-fallback rather than failing loudly. +func TestCatalogCompleteness(t *testing.T) { + for key, entry := range catalog { + en, hasEN := entry[EN] + if !hasEN || strings.TrimSpace(en) == "" { + t.Errorf("catalog[%q] has no (non-empty) English translation", key) + } + ru, hasRU := entry[RU] + if !hasRU || strings.TrimSpace(ru) == "" { + t.Errorf("catalog[%q] has no (non-empty) Russian translation", key) + } + } +} + +func TestTMissingKeyReturnsKeyItself(t *testing.T) { + got := T("no.such.key") + if got != "no.such.key" { + t.Errorf("T(unknown key) = %q, want the key itself", got) + } +} + +func TestTFallsBackToEnglish(t *testing.T) { + const testKey = "test.fallback.only.en" + catalog[testKey] = map[Lang]string{EN: "hello %s"} + defer delete(catalog, testKey) + + saved := current + current = RU + defer func() { current = saved }() + + if got := T(testKey, "world"); got != "hello world" { + t.Errorf("T(%q) with no RU entry = %q, want %q", testKey, got, "hello world") + } +} + +func TestTFormatsArgs(t *testing.T) { + saved := current + current = EN + defer func() { current = saved }() + + got := T("download.wdk_installed", "x64", "10.0.26100.1", "/dest") + want := "Installed WDK (x64) 10.0.26100.1 at /dest\n" + if got != want { + t.Errorf("T(download.wdk_installed, ...) = %q, want %q", got, want) + } +}