diff --git a/README.md b/README.md index 17ba311..3c7c1b6 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Infrastructure and build automation library for the Lethean ecosystem. Provides **Module**: `forge.lthn.ai/core/go-devops` **Licence**: EUPL-1.2 -**Language**: Go 1.25 +**Language**: Go 1.26 ## Quick Start diff --git a/cmd/dev/cmd_api_testgen_test.go b/cmd/dev/cmd_api_testgen_test.go index 900fc7c..f2395c2 100644 --- a/cmd/dev/cmd_api_testgen_test.go +++ b/cmd/dev/cmd_api_testgen_test.go @@ -3,7 +3,6 @@ package dev import ( "os" "path/filepath" - "reflect" "strings" "testing" @@ -81,7 +80,7 @@ func TestGeneratePublicAPITestFile_Good(t *testing.T) { mustTrue(t, strings.Contains(content, `const _ = impl.Answer`)) } -func TestGetExportedSymbols_Good_MultiFile(t *testing.T) { +func TestGetExportedSymbols_MultiFile_Good(t *testing.T) { tmpDir := t.TempDir() serviceDir := filepath.Join(tmpDir, "demo") @@ -111,7 +110,5 @@ type Ignored struct{} {Name: "Run", Kind: "func"}, {Name: "Value", Kind: "var"}, } - if !reflect.DeepEqual(want, symbols) { - t.Fatalf("want %v, got %v", want, symbols) - } + mustDeepEqual(t, want, symbols) } diff --git a/cmd/dev/cmd_file_sync_test.go b/cmd/dev/cmd_file_sync_test.go index 11df73a..7ac5fef 100644 --- a/cmd/dev/cmd_file_sync_test.go +++ b/cmd/dev/cmd_file_sync_test.go @@ -1,7 +1,6 @@ package dev import ( - "reflect" "testing" "dappco.re/go/cli/pkg/cli" @@ -14,30 +13,20 @@ func TestAddFileSyncCommand_Good(t *testing.T) { syncCmd, _, err := root.Find([]string{"dev", "sync"}) mustNoError(t, err) - if syncCmd == nil { - t.Fatal("expected non-nil sync command") - } + mustNotNil(t, syncCmd) yesFlag := syncCmd.Flags().Lookup("yes") - if yesFlag == nil { - t.Fatal("expected yes flag") - } + mustNotNil(t, yesFlag) mustEqual(t, "y", yesFlag.Shorthand) - if syncCmd.Flags().Lookup("dry-run") == nil { - t.Fatal("expected dry-run flag") - } - if syncCmd.Flags().Lookup("push") == nil { - t.Fatal("expected push flag") - } + mustNotNil(t, syncCmd.Flags().Lookup("dry-run")) + mustNotNil(t, syncCmd.Flags().Lookup("push")) } func TestSplitPatterns_Good(t *testing.T) { patterns := splitPatterns("packages/core-*, apps/* ,services/*,") want := []string{"packages/core-*", "apps/*", "services/*"} - if !reflect.DeepEqual(want, patterns) { - t.Fatalf("want %v, got %v", want, patterns) - } + mustDeepEqual(t, want, patterns) } func TestMatchGlob_Good(t *testing.T) { diff --git a/cmd/dev/cmd_issues.go b/cmd/dev/cmd_issues.go index 1543a8c..9a0f5c7 100644 --- a/cmd/dev/cmd_issues.go +++ b/cmd/dev/cmd_issues.go @@ -5,10 +5,10 @@ import ( "strings" "time" - "code.gitea.io/sdk/gitea" - "dappco.re/go/cli/pkg/cli" "dappco.re/go/i18n" + + "code.gitea.io/sdk/gitea" ) // Issue-specific styles (aliases to shared) diff --git a/cmd/dev/cmd_reviews.go b/cmd/dev/cmd_reviews.go index 16e2f69..657df60 100644 --- a/cmd/dev/cmd_reviews.go +++ b/cmd/dev/cmd_reviews.go @@ -5,10 +5,10 @@ import ( "strings" "time" - "code.gitea.io/sdk/gitea" - "dappco.re/go/cli/pkg/cli" "dappco.re/go/i18n" + + "code.gitea.io/sdk/gitea" ) // PR-specific styles (aliases to shared) diff --git a/cmd/dev/cmd_vm_test.go b/cmd/dev/cmd_vm_test.go index b51cd21..2281bf0 100644 --- a/cmd/dev/cmd_vm_test.go +++ b/cmd/dev/cmd_vm_test.go @@ -13,28 +13,12 @@ func TestAddVMStatusCommand_Good(t *testing.T) { statusCmd, _, err := root.Find([]string{"dev", "status"}) mustNoError(t, err) - if statusCmd == nil { - t.Fatal("expected non-nil status command") - } + mustNotNil(t, statusCmd) mustEqual(t, "status", statusCmd.Use) - mustContainsAlias(t, statusCmd.Aliases, "vm-status") + mustContainsString(t, statusCmd.Aliases, "vm-status") aliasCmd, _, err := root.Find([]string{"dev", "vm-status"}) mustNoError(t, err) - if aliasCmd == nil { - t.Fatal("expected non-nil alias command") - } - if statusCmd != aliasCmd { - t.Fatalf("want alias to be same command, got %v vs %v", statusCmd, aliasCmd) - } -} - -func mustContainsAlias(t *testing.T, haystack []string, needle string) { - t.Helper() - for _, s := range haystack { - if s == needle { - return - } - } - t.Fatalf("expected %v to contain %q", haystack, needle) + mustNotNil(t, aliasCmd) + mustTrue(t, statusCmd == aliasCmd) } diff --git a/cmd/dev/cmd_workflow_test.go b/cmd/dev/cmd_workflow_test.go index dcec060..82d6080 100644 --- a/cmd/dev/cmd_workflow_test.go +++ b/cmd/dev/cmd_workflow_test.go @@ -13,27 +13,18 @@ func TestFindWorkflows_Good(t *testing.T) { // Create a temp directory with workflow files tmpDir := t.TempDir() workflowsDir := filepath.Join(tmpDir, ".github", "workflows") - if err := io.Local.EnsureDir(workflowsDir); err != nil { - t.Fatalf("Failed to create workflows dir: %v", err) - } + mustNoError(t, io.Local.EnsureDir(workflowsDir)) // Create some workflow files for _, name := range []string{"qa.yml", "tests.yml", "codeql.yaml"} { - if err := io.Local.Write(filepath.Join(workflowsDir, name), "name: Test"); err != nil { - t.Fatalf("Failed to create workflow file: %v", err) - } + mustNoError(t, io.Local.Write(filepath.Join(workflowsDir, name), "name: Test")) } // Create a non-workflow file (should be ignored) - if err := io.Local.Write(filepath.Join(workflowsDir, "readme.md"), "# Workflows"); err != nil { - t.Fatalf("Failed to create readme file: %v", err) - } + mustNoError(t, io.Local.Write(filepath.Join(workflowsDir, "readme.md"), "# Workflows")) workflows := findWorkflows(tmpDir) - - if len(workflows) != 3 { - t.Errorf("Expected 3 workflows, got %d", len(workflows)) - } + mustLen(t, workflows, 3) // Check that all expected workflows are found found := make(map[string]bool) @@ -42,71 +33,51 @@ func TestFindWorkflows_Good(t *testing.T) { } for _, expected := range []string{"qa.yml", "tests.yml", "codeql.yaml"} { - if !found[expected] { - t.Errorf("Expected to find workflow %s", expected) - } + mustTrue(t, found[expected]) } } -func TestFindWorkflows_NoWorkflowsDir(t *testing.T) { +func TestFindWorkflows_NoWorkflowsDir_Bad(t *testing.T) { tmpDir := t.TempDir() workflows := findWorkflows(tmpDir) - if len(workflows) != 0 { - t.Errorf("Expected 0 workflows for non-existent dir, got %d", len(workflows)) - } + mustLen(t, workflows, 0) } func TestFindTemplateWorkflow_Good(t *testing.T) { tmpDir := t.TempDir() templatesDir := filepath.Join(tmpDir, ".github", "workflow-templates") - if err := io.Local.EnsureDir(templatesDir); err != nil { - t.Fatalf("Failed to create templates dir: %v", err) - } + mustNoError(t, io.Local.EnsureDir(templatesDir)) templateContent := "name: QA\non: [push]" - if err := io.Local.Write(filepath.Join(templatesDir, "qa.yml"), templateContent); err != nil { - t.Fatalf("Failed to create template file: %v", err) - } + mustNoError(t, io.Local.Write(filepath.Join(templatesDir, "qa.yml"), templateContent)) // Test finding with .yml extension result := findTemplateWorkflow(tmpDir, "qa.yml") - if result == "" { - t.Error("Expected to find qa.yml template") - } + mustTrue(t, result != "") // Test finding without extension (should auto-add .yml) result = findTemplateWorkflow(tmpDir, "qa") - if result == "" { - t.Error("Expected to find qa template without extension") - } + mustTrue(t, result != "") } -func TestFindTemplateWorkflow_FallbackToWorkflows(t *testing.T) { +func TestFindTemplateWorkflow_FallbackToWorkflows_Good(t *testing.T) { tmpDir := t.TempDir() workflowsDir := filepath.Join(tmpDir, ".github", "workflows") - if err := io.Local.EnsureDir(workflowsDir); err != nil { - t.Fatalf("Failed to create workflows dir: %v", err) - } + mustNoError(t, io.Local.EnsureDir(workflowsDir)) templateContent := "name: Tests\non: [push]" - if err := io.Local.Write(filepath.Join(workflowsDir, "tests.yml"), templateContent); err != nil { - t.Fatalf("Failed to create workflow file: %v", err) - } + mustNoError(t, io.Local.Write(filepath.Join(workflowsDir, "tests.yml"), templateContent)) result := findTemplateWorkflow(tmpDir, "tests.yml") - if result == "" { - t.Error("Expected to find tests.yml in workflows dir") - } + mustTrue(t, result != "") } -func TestFindTemplateWorkflow_NotFound(t *testing.T) { +func TestFindTemplateWorkflow_NotFound_Bad(t *testing.T) { tmpDir := t.TempDir() result := findTemplateWorkflow(tmpDir, "nonexistent.yml") - if result != "" { - t.Errorf("Expected empty string for non-existent template, got %s", result) - } + mustEqual(t, "", result) } func TestTemplateNames_Good(t *testing.T) { @@ -118,11 +89,8 @@ func TestTemplateNames_Good(t *testing.T) { names := slices.Sorted(maps.Keys(templateSet)) - if len(names) != 3 { - t.Fatalf("Expected 3 template names, got %d", len(names)) - } - - if names[0] != "a.yml" || names[1] != "m.yml" || names[2] != "z.yml" { - t.Fatalf("Expected sorted template names, got %v", names) - } + mustLen(t, names, 3) + mustEqual(t, "a.yml", names[0]) + mustEqual(t, "m.yml", names[1]) + mustEqual(t, "z.yml", names[2]) } diff --git a/cmd/dev/service.go b/cmd/dev/service.go index 8e2fa80..5c22f79 100644 --- a/cmd/dev/service.go +++ b/cmd/dev/service.go @@ -20,14 +20,18 @@ type Service struct { } func (s *Service) handleAction(_ *core.Core, _ core.Message) core.Result { - return core.Result{} + return core.Result{OK: true} } // doCommit shells out to claude for AI-assisted commit. func doCommit(ctx context.Context, repoPath string, allowEdit bool) error { prompt := "" if r := lib.Prompt("commit"); r.OK { - prompt = r.Value.(string) + value, ok := r.Value.(string) + if !ok { + return core.E("dev.commit", "commit prompt was not a string", nil) + } + prompt = value } tools := "Bash,Read,Glob,Grep" diff --git a/cmd/dev/test_helpers_test.go b/cmd/dev/test_helpers_test.go new file mode 100644 index 0000000..2a85ade --- /dev/null +++ b/cmd/dev/test_helpers_test.go @@ -0,0 +1,80 @@ +package dev + +import ( + "reflect" + "strings" + "testing" +) + +func mustNoError(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func mustEqual[T comparable](t *testing.T, want, got T) { + t.Helper() + if want != got { + t.Fatalf("want %v, got %v", want, got) + } +} + +func mustDeepEqual(t *testing.T, want, got any) { + t.Helper() + if !reflect.DeepEqual(want, got) { + t.Fatalf("want %v, got %v", want, got) + } +} + +func mustContains(t *testing.T, s, sub string) { + t.Helper() + if !strings.Contains(s, sub) { + t.Fatalf("expected %q to contain %q", s, sub) + } +} + +func mustContainsString(t *testing.T, haystack []string, needle string) { + t.Helper() + for _, s := range haystack { + if s == needle { + return + } + } + t.Fatalf("expected %v to contain %q", haystack, needle) +} + +func mustNotContains(t *testing.T, s, sub string) { + t.Helper() + if strings.Contains(s, sub) { + t.Fatalf("expected %q to not contain %q", s, sub) + } +} + +func mustTrue(t *testing.T, cond bool) { + t.Helper() + if !cond { + t.Fatal("expected true") + } +} + +func mustFalse(t *testing.T, cond bool) { + t.Helper() + if cond { + t.Fatal("expected false") + } +} + +func mustLen[T any](t *testing.T, got []T, want int) { + t.Helper() + if len(got) != want { + t.Fatalf("want length %d, got %d", want, len(got)) + } +} + +func mustNotNil(t *testing.T, v any) { + t.Helper() + if v == nil { + t.Fatal("expected non-nil") + } +} diff --git a/cmd/docs/cmd_sync_test.go b/cmd/docs/cmd_sync_test.go index bfde4d7..7cccc39 100644 --- a/cmd/docs/cmd_sync_test.go +++ b/cmd/docs/cmd_sync_test.go @@ -38,7 +38,7 @@ func TestCopyZensicalReadme_Good(t *testing.T) { } } -func TestResetOutputDir_ClearsExistingFiles(t *testing.T) { +func TestResetOutputDir_ClearsExistingFiles_Good(t *testing.T) { dir := t.TempDir() stale := filepath.Join(dir, "stale.md") diff --git a/cmd/setup/cmd_repo_test.go b/cmd/setup/cmd_repo_test.go index 24e3f70..00ebc59 100644 --- a/cmd/setup/cmd_repo_test.go +++ b/cmd/setup/cmd_repo_test.go @@ -6,7 +6,7 @@ import ( "testing" ) -func TestRunRepoSetup_CreatesCoreConfigs(t *testing.T) { +func TestRunRepoSetup_CreatesCoreConfigs_Good(t *testing.T) { dir := t.TempDir() mustNoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/test\n"), 0o644)) @@ -19,7 +19,7 @@ func TestRunRepoSetup_CreatesCoreConfigs(t *testing.T) { } } -func TestDetectProjectType_PrefersPackageOverComposer(t *testing.T) { +func TestDetectProjectType_PrefersPackageOverComposer_Good(t *testing.T) { dir := t.TempDir() mustNoError(t, os.WriteFile(filepath.Join(dir, "package.json"), []byte("{}\n"), 0o644)) mustNoError(t, os.WriteFile(filepath.Join(dir, "composer.json"), []byte("{}\n"), 0o644)) diff --git a/cmd/setup/cmd_wizard_test.go b/cmd/setup/cmd_wizard_test.go index eea49e9..2fac324 100644 --- a/cmd/setup/cmd_wizard_test.go +++ b/cmd/setup/cmd_wizard_test.go @@ -1,7 +1,6 @@ package setup import ( - "reflect" "testing" "dappco.re/go/scm/repos" @@ -30,7 +29,5 @@ func TestFilterReposByTypes_EmptyFilter_Good(t *testing.T) { filtered := filterReposByTypes(reposList, nil) mustLen(t, filtered, 2) - if !reflect.DeepEqual(reposList, filtered) { - t.Fatalf("want %v, got %v", reposList, filtered) - } + mustDeepEqual(t, reposList, filtered) } diff --git a/cmd/setup/test_helpers_test.go b/cmd/setup/test_helpers_test.go new file mode 100644 index 0000000..c2cc3b0 --- /dev/null +++ b/cmd/setup/test_helpers_test.go @@ -0,0 +1,66 @@ +package setup + +import ( + "reflect" + "strings" + "testing" +) + +func mustNoError(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func mustNoErrorf(t *testing.T, err error, format string, args ...any) { + t.Helper() + if err != nil { + t.Fatalf(format+": %v", append(args, err)...) + } +} + +func mustEqual[T comparable](t *testing.T, want, got T) { + t.Helper() + if want != got { + t.Fatalf("want %v, got %v", want, got) + } +} + +func mustDeepEqual(t *testing.T, want, got any) { + t.Helper() + if !reflect.DeepEqual(want, got) { + t.Fatalf("want %v, got %v", want, got) + } +} + +func mustContains(t *testing.T, s, sub string) { + t.Helper() + if !strings.Contains(s, sub) { + t.Fatalf("expected %q to contain %q", s, sub) + } +} + +func mustNotContains(t *testing.T, s, sub string) { + t.Helper() + if strings.Contains(s, sub) { + t.Fatalf("expected %q to not contain %q", s, sub) + } +} + +func mustLen[T any](t *testing.T, got []T, want int) { + t.Helper() + if len(got) != want { + t.Fatalf("want length %d, got %d", want, len(got)) + } +} + +func mustContainsString(t *testing.T, haystack []string, needle string) { + t.Helper() + for _, s := range haystack { + if s == needle { + return + } + } + t.Fatalf("expected %v to contain %q", haystack, needle) +} diff --git a/cmd/vanity-import/Dockerfile b/cmd/vanity-import/Dockerfile index dc9c984..9fcebfe 100644 --- a/cmd/vanity-import/Dockerfile +++ b/cmd/vanity-import/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.25-alpine AS build +FROM golang:1.26-alpine AS build WORKDIR /src COPY go.mod main.go ./ RUN go build -trimpath -ldflags="-w -s" -o /vanity-import . diff --git a/cmd/workspace/config.go b/cmd/workspace/config.go index c3f22fc..993b912 100644 --- a/cmd/workspace/config.go +++ b/cmd/workspace/config.go @@ -9,6 +9,7 @@ package workspace import ( + "fmt" "os" "path/filepath" @@ -36,24 +37,33 @@ func DefaultConfig() *WorkspaceConfig { // LoadConfig reads .core/workspace.yaml from the given directory, walking up to parent dirs. // Returns nil (no error) if no config file is found. func LoadConfig(dir string) (*WorkspaceConfig, error) { + absDir, err := filepath.Abs(dir) + if err != nil { + return nil, fmt.Errorf("workspace.LoadConfig: resolve %q: %w", dir, err) + } + + return loadConfig(filepath.Clean(absDir)) +} + +func loadConfig(dir string) (*WorkspaceConfig, error) { path := filepath.Join(dir, ".core", "workspace.yaml") if !coreio.Local.IsFile(path) { parent := filepath.Dir(dir) if parent != dir { - return LoadConfig(parent) + return loadConfig(parent) } return nil, nil } data, err := coreio.Local.Read(path) if err != nil { - return nil, log.E("workspace.LoadConfig", "failed to read workspace config", err) + return nil, fmt.Errorf("workspace.LoadConfig: failed to read workspace config: %w", err) } cfg := DefaultConfig() if err := yaml.Unmarshal([]byte(data), cfg); err != nil { - return nil, log.E("workspace.LoadConfig", "failed to parse workspace config", err) + return nil, fmt.Errorf("workspace.LoadConfig: failed to parse workspace config: %w", err) } return cfg, nil diff --git a/cmd/workspace/config_test.go b/cmd/workspace/config_test.go new file mode 100644 index 0000000..b6c61cb --- /dev/null +++ b/cmd/workspace/config_test.go @@ -0,0 +1,51 @@ +package workspace + +import ( + "os" + "path/filepath" + "testing" +) + +func TestLoadConfig_RelativeDirFindsParentConfig_Good(t *testing.T) { + root := t.TempDir() + mustNoError(t, os.MkdirAll(filepath.Join(root, ".core"), 0o755)) + mustNoError(t, os.MkdirAll(filepath.Join(root, "packages", "app"), 0o755)) + mustNoError(t, os.WriteFile(filepath.Join(root, ".core", "workspace.yaml"), []byte(`version: 1 +active: app +packages_dir: ./packages +`), 0o600)) + + originalWD, err := os.Getwd() + mustNoError(t, err) + t.Cleanup(func() { + mustNoError(t, os.Chdir(originalWD)) + }) + mustNoError(t, os.Chdir(filepath.Join(root, "packages", "app"))) + + cfg, err := LoadConfig(".") + mustNoError(t, err) + mustNotNil(t, cfg) + mustEqual(t, "app", cfg.Active) + mustEqual(t, "./packages", cfg.PackagesDir) +} + +func mustNoError(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func mustEqual[T comparable](t *testing.T, want, got T) { + t.Helper() + if want != got { + t.Fatalf("want %v, got %v", want, got) + } +} + +func mustNotNil(t *testing.T, got any) { + t.Helper() + if got == nil { + t.Fatal("expected non-nil") + } +} diff --git a/devkit/scan_secrets_test.go b/devkit/scan_secrets_test.go index b57a3c7..51f4c99 100644 --- a/devkit/scan_secrets_test.go +++ b/devkit/scan_secrets_test.go @@ -36,7 +36,7 @@ aws-access-key-id,creds.txt,7,1,AWS access key detected,AKIA1234567890ABCDEF mustEqual(t, "AKIA1234567890ABCDEF", findings[1].Snippet) } -func TestScanSecrets_ReportsFindingsOnExitError(t *testing.T) { +func TestScanSecrets_ReportsFindingsOnExitError_Good(t *testing.T) { originalRunner := scanSecretsRunner t.Cleanup(func() { scanSecretsRunner = originalRunner diff --git a/devkit/secret_test.go b/devkit/secret_test.go index dec3d12..44eec68 100644 --- a/devkit/secret_test.go +++ b/devkit/secret_test.go @@ -29,7 +29,7 @@ api_key: "ghp_abcdefghijklmnopqrstuvwxyz1234" mustEqual(t, "creds.txt", filepath.Base(findings[1].Path)) } -func TestScanDir_SkipsBinaryAndIgnoredDirs(t *testing.T) { +func TestScanDir_SkipsBinaryAndIgnoredDirs_Good(t *testing.T) { root := t.TempDir() mustNoError(t, os.Mkdir(filepath.Join(root, ".git"), 0o755)) @@ -41,7 +41,7 @@ func TestScanDir_SkipsBinaryAndIgnoredDirs(t *testing.T) { mustEmpty(t, findings) } -func TestScanDir_ReportsGenericAssignments(t *testing.T) { +func TestScanDir_ReportsGenericAssignments_Bad(t *testing.T) { root := t.TempDir() mustNoError(t, os.WriteFile(filepath.Join(root, "secrets.env"), []byte("client_secret: abcdefghijklmnop\n"), 0o600)) diff --git a/devkit/test_helpers_test.go b/devkit/test_helpers_test.go new file mode 100644 index 0000000..0dba87d --- /dev/null +++ b/devkit/test_helpers_test.go @@ -0,0 +1,48 @@ +package devkit + +import ( + "math" + "testing" +) + +func mustNoError(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func mustError(t *testing.T, err error) { + t.Helper() + if err == nil { + t.Fatal("expected error, got nil") + } +} + +func mustEqual[T comparable](t *testing.T, want, got T) { + t.Helper() + if want != got { + t.Fatalf("want %v, got %v", want, got) + } +} + +func mustLen[T any](t *testing.T, got []T, want int) { + t.Helper() + if len(got) != want { + t.Fatalf("want length %d, got %d", want, len(got)) + } +} + +func mustEmpty[T any](t *testing.T, got []T) { + t.Helper() + if len(got) != 0 { + t.Fatalf("expected empty, got %d entries", len(got)) + } +} + +func mustInDelta(t *testing.T, want, got, delta float64) { + t.Helper() + if math.Abs(want-got) > delta { + t.Fatalf("want %v±%v, got %v", want, delta, got) + } +} diff --git a/docs/development.md b/docs/development.md index f4a637f..cfbae77 100644 --- a/docs/development.md +++ b/docs/development.md @@ -4,7 +4,7 @@ | Tool | Minimum version | Purpose | |------|----------------|---------| -| Go | 1.25 | Build and test | +| Go | 1.26 | Build and test | | Task | any | Taskfile automation (optional, used by some builders) | | `govulncheck` | latest | Vulnerability scanning (`devkit.VulnCheck`) | | `gitleaks` | any | Secret scanning (`devkit.ScanSecrets`) | diff --git a/go.sum b/go.sum index 9f936da..c281c45 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,16 @@ code.gitea.io/sdk/gitea v0.23.2 h1:iJB1FDmLegwfwjX8gotBDHdPSbk/ZR8V9VmEJaVsJYg= code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM= codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 h1:HTCWpzyWQOHDWt3LzI6/d2jvUDsw/vgGRWm/8BTvcqI= codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0/go.mod h1:ZglEEDj+qkxYUb+SQIeqGtFxQrbaMYqIOgahNKb7uxs= +dappco.re/go/agent v0.8.0-alpha.1 h1:7jtrDGh5CHUVsvvQiG8gjQxfdlI+ncJrIHXEMksJ8bc= +dappco.re/go/agent v0.8.0-alpha.1/go.mod h1:jiShGsIfHS7b7rJXMdb30K+wKL8Kx8w/VUrLNDYRbCo= dappco.re/go/agent v0.11.0 h1:5PKzxJf+z0WF+QsxgkMwvDUODj38DGCx0uMk1KxtWkg= dappco.re/go/agent v0.11.0/go.mod h1:nBF4HMMSZD/YJg+MTHqTv71csgFlCyy62Ux084yjw+U= +dappco.re/go/cli v0.8.0-alpha.1 h1:UUnkSvAgNeRtu4kc96hr4WUpe9WTBxDY+1Co5IDVlbk= +dappco.re/go/cli v0.8.0-alpha.1/go.mod h1:jRJuSyEB7pAmyiAyTPSh7l1ens627vfxhBcUhi3sOEY= +dappco.re/go/config v0.8.0-alpha.1 h1:YpfPi7PHId0Wc2C/h07rmTZG06a+ONHrBLG9KDg45Uo= +dappco.re/go/config v0.8.0-alpha.1/go.mod h1:Ryvf7Fncq4p+mZQnHjP5h8OmDcbE2JBf99E6hDdpeN4= +dappco.re/go/container v0.8.0-alpha.1 h1:jrC308wXpooaHMjvhEvPwPfK4KOXTuFYz4y/Es+uhY4= +dappco.re/go/container v0.8.0-alpha.1/go.mod h1:5F+NPSBG3LtgfBTGvmGcVWLmax4LrmxBgexOHG4gnKc= dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk= dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= dappco.re/go/core/cli v0.5.2 h1:mo+PERo3lUytE+r3ArHr8o2nTftXjgPPsU/rn3ETXDM= @@ -22,6 +30,16 @@ dappco.re/go/core/log v0.1.2 h1:pQSZxKD8VycdvjNJmatXbPSq2OxcP2xHbF20zgFIiZI= dappco.re/go/core/log v0.1.2/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs= dappco.re/go/core/scm v0.6.1 h1:nQWr2AGreLzhp//2zZolol87TCKlzV2/I/hpBVkv0Gc= dappco.re/go/core/scm v0.6.1/go.mod h1:fYy/xazjyv84X8sxBIpTBikSdU5nQq4qf/IR2hXnd5E= +dappco.re/go/i18n v0.8.0-alpha.1 h1:9LI/PrF41XeQu69eOaBTz3LMrXTJ08O2f1EEATq9k5A= +dappco.re/go/i18n v0.8.0-alpha.1/go.mod h1:aSfWSAW2EVh/aMbMplc27URnjl6DvRVvWfvRC2my7AY= +dappco.re/go/inference v0.8.0-alpha.1 h1:Cc3YZr04rNSqqHQBm7v53mzfn6e17sf7oDe+TqQnzwo= +dappco.re/go/inference v0.8.0-alpha.1/go.mod h1:vMXtaGSKvom7B5rjOjzl4taSOXbbVmnsLlYd0X/PFo0= +dappco.re/go/io v0.8.0-alpha.1 h1:tIJ/Nd6lGr2DFEUj2HzGM8dPglS5bEAI4h2RAgzGCNE= +dappco.re/go/io v0.8.0-alpha.1/go.mod h1:5u1TImtXPdJKDgh59Nw4rsbMUkq02uVDDsL5bE1mhBk= +dappco.re/go/log v0.8.0-alpha.1 h1:eXTdrt88Ovbdm0KJkJDaEpgLUHUZgJ2xYEu2uN3eV4I= +dappco.re/go/log v0.8.0-alpha.1/go.mod h1:IC04Em9SfVTcXiWc1BqZDQfa1MtOuMDEermZkQcTz9c= +dappco.re/go/scm v0.8.0-alpha.1 h1:pXiO5Hp5tky3shekYERUK9KsQy9xoWQQW0I40mPyKvA= +dappco.re/go/scm v0.8.0-alpha.1/go.mod h1:11xL67SU5TJ+fTBLyqYDDwotl7Y1qy5rWY+JgEQ16UQ= github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs= github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= diff --git a/snapshot/snapshot_test.go b/snapshot/snapshot_test.go index d42cf10..92b3a83 100644 --- a/snapshot/snapshot_test.go +++ b/snapshot/snapshot_test.go @@ -52,7 +52,7 @@ func TestGenerate_Good(t *testing.T) { mustDeepEqual(t, []string{"core/media"}, snap.Modules) } -func TestGenerate_Good_NoDaemons(t *testing.T) { +func TestGenerate_NoDaemons_Good(t *testing.T) { m := &manifest.Manifest{ Code: "simple", Name: "Simple", @@ -75,7 +75,7 @@ func TestGenerate_Good_NoDaemons(t *testing.T) { } } -func TestGenerate_Bad_NilManifest(t *testing.T) { +func TestGenerate_NilManifest_Bad(t *testing.T) { _, err := Generate(nil, "abc123", "v1.0.0") mustErrorContains(t, err, "manifest is nil") } diff --git a/snapshot/test_helpers_test.go b/snapshot/test_helpers_test.go new file mode 100644 index 0000000..987f16a --- /dev/null +++ b/snapshot/test_helpers_test.go @@ -0,0 +1,45 @@ +package snapshot + +import ( + "reflect" + "strings" + "testing" +) + +func mustNoError(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func mustEqual[T comparable](t *testing.T, want, got T) { + t.Helper() + if want != got { + t.Fatalf("want %v, got %v", want, got) + } +} + +func mustDeepEqual(t *testing.T, want, got any) { + t.Helper() + if !reflect.DeepEqual(want, got) { + t.Fatalf("want %v, got %v", want, got) + } +} + +func mustLenMap[K comparable, V any](t *testing.T, m map[K]V, want int) { + t.Helper() + if len(m) != want { + t.Fatalf("want length %d, got %d", want, len(m)) + } +} + +func mustErrorContains(t *testing.T, err error, sub string) { + t.Helper() + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), sub) { + t.Fatalf("expected error %q to contain %q", err.Error(), sub) + } +} diff --git a/tests/cli/devops/Taskfile.yaml b/tests/cli/devops/Taskfile.yaml index 525eb40..404ccd1 100644 --- a/tests/cli/devops/Taskfile.yaml +++ b/tests/cli/devops/Taskfile.yaml @@ -16,6 +16,9 @@ tasks: dir: ../../.. cmds: - | + set -euo pipefail + IFS=$'\n\t' + export GOWORK=off export GOCACHE="${GOCACHE:-/tmp/go-devops-gocache}" export GOMODCACHE="${GOMODCACHE:-/tmp/go-devops-gomodcache}" @@ -35,6 +38,9 @@ tasks: dir: ../../.. cmds: - | + set -euo pipefail + IFS=$'\n\t' + export GOWORK=off export GOCACHE="${GOCACHE:-/tmp/go-devops-gocache}" export GOMODCACHE="${GOMODCACHE:-/tmp/go-devops-gomodcache}" @@ -54,6 +60,9 @@ tasks: dir: ../../.. cmds: - | + set -euo pipefail + IFS=$'\n\t' + export GOWORK=off export GOCACHE="${GOCACHE:-/tmp/go-devops-gocache}" export GOMODCACHE="${GOMODCACHE:-/tmp/go-devops-gomodcache}" diff --git a/tests/cli/devops/main.go b/tests/cli/devops/main.go index 9f35f96..edfd761 100644 --- a/tests/cli/devops/main.go +++ b/tests/cli/devops/main.go @@ -54,7 +54,7 @@ func runPlaybookSmoke(cmd *cli.Command, args []string) error { count := 0 err := filepath.WalkDir(dir, func(path string, entry os.DirEntry, err error) error { if err != nil { - return err + return fmt.Errorf("%s: %w", path, err) } if entry.IsDir() || !isYAML(path) { return nil @@ -62,7 +62,7 @@ func runPlaybookSmoke(cmd *cli.Command, args []string) error { raw, err := os.ReadFile(path) if err != nil { - return err + return fmt.Errorf("%s: %w", path, err) } var document any @@ -73,7 +73,7 @@ func runPlaybookSmoke(cmd *cli.Command, args []string) error { return nil }) if err != nil { - return err + return fmt.Errorf("walk %s: %w", dir, err) } if count == 0 { return fmt.Errorf("no playbook YAML files found in %s", dir)