diff --git a/cmd/homelab-deploy/main.go b/cmd/homelab-deploy/main.go index 758fe73..19d3a26 100644 --- a/cmd/homelab-deploy/main.go +++ b/cmd/homelab-deploy/main.go @@ -17,7 +17,7 @@ import ( "github.com/urfave/cli/v3" ) -const version = "0.2.0" +const version = "0.2.1" func main() { app := &cli.Command{ diff --git a/internal/builder/builder.go b/internal/builder/builder.go index 9894c3e..2c3c03d 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -6,6 +6,7 @@ import ( "log/slog" "regexp" "sort" + "strings" "sync" "time" @@ -18,6 +19,18 @@ import ( // Allows: alphanumeric, dashes, underscores, dots. var hostnameRegex = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) +// truncateOutput truncates output to the first and last N lines if it exceeds 2*N lines. +func truncateOutput(output string, keepLines int) string { + lines := strings.Split(output, "\n") + if len(lines) <= keepLines*2 { + return output + } + head := lines[:keepLines] + tail := lines[len(lines)-keepLines:] + omitted := len(lines) - keepLines*2 + return strings.Join(head, "\n") + fmt.Sprintf("\n\n... (%d lines omitted) ...\n\n", omitted) + strings.Join(tail, "\n") +} + // BuilderConfig holds the configuration for the builder. type BuilderConfig struct { NATSUrl string @@ -256,7 +269,7 @@ func (b *Builder) handleBuildRequest(subject string, data []byte) { DurationSeconds: hostDuration, } if !result.Success { - hostResult.Error = result.Stderr + hostResult.Error = truncateOutput(result.Stderr, 50) if hostResult.Error == "" && result.Error != nil { hostResult.Error = result.Error.Error() } diff --git a/internal/builder/builder_test.go b/internal/builder/builder_test.go new file mode 100644 index 0000000..eaca6f2 --- /dev/null +++ b/internal/builder/builder_test.go @@ -0,0 +1,115 @@ +package builder + +import ( + "strings" + "testing" +) + +func TestTruncateOutput(t *testing.T) { + tests := []struct { + name string + input string + keepLines int + wantLines int + wantOmit bool + }{ + { + name: "short output unchanged", + input: "line1\nline2\nline3", + keepLines: 50, + wantLines: 3, + wantOmit: false, + }, + { + name: "exactly at threshold unchanged", + input: strings.Join(makeLines(100), "\n"), + keepLines: 50, + wantLines: 100, + wantOmit: false, + }, + { + name: "over threshold truncated", + input: strings.Join(makeLines(150), "\n"), + keepLines: 50, + wantLines: 103, // 50 + 1 (empty) + 1 (omitted msg) + 1 (empty) + 50 + wantOmit: true, + }, + { + name: "large output truncated", + input: strings.Join(makeLines(1000), "\n"), + keepLines: 50, + wantLines: 103, + wantOmit: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := truncateOutput(tt.input, tt.keepLines) + gotLines := strings.Split(got, "\n") + + if len(gotLines) != tt.wantLines { + t.Errorf("got %d lines, want %d", len(gotLines), tt.wantLines) + } + + hasOmit := strings.Contains(got, "lines omitted") + if hasOmit != tt.wantOmit { + t.Errorf("got omit marker = %v, want %v", hasOmit, tt.wantOmit) + } + + if tt.wantOmit { + // Verify first and last lines are preserved + inputLines := strings.Split(tt.input, "\n") + firstLine := inputLines[0] + lastLine := inputLines[len(inputLines)-1] + if !strings.HasPrefix(got, firstLine+"\n") { + t.Errorf("first line not preserved, got prefix %q, want %q", + gotLines[0], firstLine) + } + if !strings.HasSuffix(got, lastLine) { + t.Errorf("last line not preserved, got suffix %q, want %q", + gotLines[len(gotLines)-1], lastLine) + } + } + }) + } +} + +func makeLines(n int) []string { + lines := make([]string, n) + for i := range lines { + lines[i] = "line " + strings.Repeat("x", i%80) + } + return lines +} + +func TestTruncateOutputPreservesContent(t *testing.T) { + // Create input with distinct first and last lines + lines := make([]string, 200) + for i := range lines { + lines[i] = "middle" + } + lines[0] = "FIRST" + lines[49] = "LAST_OF_HEAD" + lines[150] = "FIRST_OF_TAIL" + lines[199] = "LAST" + + input := strings.Join(lines, "\n") + got := truncateOutput(input, 50) + + if !strings.Contains(got, "FIRST") { + t.Error("missing FIRST") + } + if !strings.Contains(got, "LAST_OF_HEAD") { + t.Error("missing LAST_OF_HEAD") + } + if !strings.Contains(got, "FIRST_OF_TAIL") { + t.Error("missing FIRST_OF_TAIL") + } + if !strings.Contains(got, "LAST") { + t.Error("missing LAST") + } + if !strings.Contains(got, "(100 lines omitted)") { + t.Errorf("wrong omitted count, got: %s", got) + } +}