Skip to content

Commit 208af6c

Browse files
xiantangclaude
andauthored
test: cover proxy handlers, entrypoint parsing, logger and checksum paths (#929)
* test: cover proxy handlers, entrypoint parsing, logger and checksum paths Raises runner package coverage from 82.5% to 85.7% by testing paths that had none: - proxy: worker script handler, Reload/BuildFailed delegation, Stop, the non-flusher and bad-request branches of the proxy/reload handlers, and streamCopy write/short-write errors - entrypoint.UnmarshalTOML: nil, string, array, and both error cases - logger: raw logger fallback, color fallback, silent/empty/add-time - engine: cacheFileChecksums exclusion rules and primeChecksum on an unreadable file, isExcludeFile patterns - config: buildDelay and rerunDelay - flag: appendableValue String/Set semantics Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: use string pattern for assert.Regexp golangci-lint (testifylint) rejects regexp.MustCompile inside assert.Regexp, which accepts the pattern directly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: make exclude_file pattern separator-aware on Windows isExcludeFile matches with filepath.Match against a path cleaned with the platform separator, so a "docs/*.md" pattern never matches on Windows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 61a601c commit 208af6c

6 files changed

Lines changed: 540 additions & 0 deletions

File tree

runner/config_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,22 @@ func TestReadConfigWithWrongPath(t *testing.T) {
313313
}
314314
}
315315

316+
func TestBuildAndRerunDelay(t *testing.T) {
317+
t.Parallel()
318+
config := Config{
319+
Build: cfgBuild{
320+
Delay: 300,
321+
RerunDelay: 700,
322+
},
323+
}
324+
if got, want := config.buildDelay(), 300*time.Millisecond; got != want {
325+
t.Fatalf("buildDelay() = %v, want %v", got, want)
326+
}
327+
if got, want := config.rerunDelay(), 700*time.Millisecond; got != want {
328+
t.Fatalf("rerunDelay() = %v, want %v", got, want)
329+
}
330+
}
331+
316332
func TestKillDelay(t *testing.T) {
317333
t.Parallel()
318334
config := Config{

runner/engine_checksum_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package runner
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// writeFile creates path (including parents) with the given contents.
13+
func writeFile(t *testing.T, path, contents string) {
14+
t.Helper()
15+
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
16+
require.NoError(t, os.WriteFile(path, []byte(contents), 0o644))
17+
}
18+
19+
func TestEngineCacheFileChecksums(t *testing.T) {
20+
tmpDir := t.TempDir()
21+
22+
writeFile(t, filepath.Join(tmpDir, "main.go"), "package main\n")
23+
writeFile(t, filepath.Join(tmpDir, "internal", "app.go"), "package internal\n")
24+
writeFile(t, filepath.Join(tmpDir, "README.md"), "docs\n") // extension not watched
25+
writeFile(t, filepath.Join(tmpDir, "skip.go"), "package skip\n") // exclude_file
26+
writeFile(t, filepath.Join(tmpDir, "app_test.go"), "package main\n") // exclude_regex
27+
writeFile(t, filepath.Join(tmpDir, "tmp", "main.go"), "package tmp\n")
28+
writeFile(t, filepath.Join(tmpDir, "vendor", "dep.go"), "package dep\n")
29+
writeFile(t, filepath.Join(tmpDir, ".hidden", "hidden.go"), "package hidden\n")
30+
31+
cfg := defaultConfig()
32+
cfg.Root = tmpDir
33+
cfg.TmpDir = "tmp"
34+
cfg.Build.IncludeExt = []string{"go"}
35+
cfg.Build.ExcludeDir = []string{"vendor"}
36+
cfg.Build.ExcludeFile = []string{"skip.go"}
37+
cfg.Build.ExcludeRegex = []string{"_test.go"}
38+
require.NoError(t, cfg.preprocess(nil))
39+
40+
engine, err := NewEngineWithConfig(&cfg, false)
41+
require.NoError(t, err)
42+
t.Cleanup(func() { _ = engine.watcher.Close() })
43+
44+
require.NoError(t, engine.cacheFileChecksums(cfg.Root))
45+
46+
cached := func(rel string) bool {
47+
path := filepath.Join(cfg.Root, rel)
48+
checksum, err := fileChecksum(path)
49+
require.NoError(t, err)
50+
// updateFileChecksum reports false when the checksum is already stored.
51+
return !engine.fileChecksums.updateFileChecksum(path, checksum)
52+
}
53+
54+
assert.True(t, cached("main.go"), "watched file should be cached")
55+
assert.True(t, cached(filepath.Join("internal", "app.go")), "watched file in subdir should be cached")
56+
57+
assert.False(t, cached("README.md"), "unwatched extension should not be cached")
58+
assert.False(t, cached("skip.go"), "exclude_file match should not be cached")
59+
assert.False(t, cached("app_test.go"), "exclude_regex match should not be cached")
60+
assert.False(t, cached(filepath.Join("vendor", "dep.go")), "exclude_dir content should not be cached")
61+
assert.False(t, cached(filepath.Join(".hidden", "hidden.go")), "hidden dir content should not be cached")
62+
}
63+
64+
func TestEngineCacheFileChecksumsMissingRoot(t *testing.T) {
65+
tmpDir := t.TempDir()
66+
67+
cfg := defaultConfig()
68+
cfg.Root = tmpDir
69+
require.NoError(t, cfg.preprocess(nil))
70+
71+
engine, err := NewEngineWithConfig(&cfg, false)
72+
require.NoError(t, err)
73+
t.Cleanup(func() { _ = engine.watcher.Close() })
74+
75+
assert.Error(t, engine.cacheFileChecksums(filepath.Join(tmpDir, "does-not-exist")))
76+
}
77+
78+
func TestEnginePrimeChecksumIgnoresUnreadableFile(t *testing.T) {
79+
tmpDir := t.TempDir()
80+
81+
cfg := defaultConfig()
82+
cfg.Root = tmpDir
83+
require.NoError(t, cfg.preprocess(nil))
84+
85+
engine, err := NewEngineWithConfig(&cfg, false)
86+
require.NoError(t, err)
87+
t.Cleanup(func() { _ = engine.watcher.Close() })
88+
89+
missing := filepath.Join(tmpDir, "gone.go")
90+
engine.primeChecksum(missing)
91+
92+
// Nothing was stored, so the first real update reports a change.
93+
writeFile(t, missing, "package main\n")
94+
checksum, err := fileChecksum(missing)
95+
require.NoError(t, err)
96+
assert.True(t, engine.fileChecksums.updateFileChecksum(missing, checksum))
97+
}
98+
99+
func TestEngineIsExcludeFile(t *testing.T) {
100+
tmpDir := t.TempDir()
101+
102+
cfg := defaultConfig()
103+
cfg.Root = tmpDir
104+
// isExcludeFile matches with filepath.Match, so the directory pattern has
105+
// to use the platform separator to match on Windows too.
106+
cfg.Build.ExcludeFile = []string{"skip.go", filepath.Join("docs", "*.md")}
107+
require.NoError(t, cfg.preprocess(nil))
108+
109+
engine, err := NewEngineWithConfig(&cfg, false)
110+
require.NoError(t, err)
111+
t.Cleanup(func() { _ = engine.watcher.Close() })
112+
113+
assert.True(t, engine.isExcludeFile(filepath.Join(cfg.Root, "skip.go")))
114+
assert.True(t, engine.isExcludeFile(filepath.Join(cfg.Root, "docs", "readme.md")))
115+
assert.False(t, engine.isExcludeFile(filepath.Join(cfg.Root, "main.go")))
116+
assert.False(t, engine.isExcludeFile(filepath.Join(cfg.Root, "docs", "nested", "readme.md")))
117+
}

runner/entrypoint_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package runner
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestEntrypointUnmarshalTOML(t *testing.T) {
11+
t.Parallel()
12+
13+
tests := []struct {
14+
name string
15+
value interface{}
16+
want entrypoint
17+
wantErr string
18+
}{
19+
{
20+
name: "nil clears the entrypoint",
21+
value: nil,
22+
want: nil,
23+
},
24+
{
25+
name: "string becomes a single element",
26+
value: "./tmp/main",
27+
want: entrypoint{"./tmp/main"},
28+
},
29+
{
30+
name: "array keeps binary and args",
31+
value: []interface{}{"./tmp/main", "server", ":8080"},
32+
want: entrypoint{"./tmp/main", "server", ":8080"},
33+
},
34+
{
35+
name: "empty array yields an empty entrypoint",
36+
value: []interface{}{},
37+
want: entrypoint{},
38+
},
39+
{
40+
name: "non-string array element is rejected",
41+
value: []interface{}{"./tmp/main", 42},
42+
wantErr: "entrypoint values must be strings, got int",
43+
},
44+
{
45+
name: "unsupported type is rejected",
46+
value: 42,
47+
wantErr: "entrypoint must be a string or array of strings, got int",
48+
},
49+
}
50+
51+
for _, tt := range tests {
52+
t.Run(tt.name, func(t *testing.T) {
53+
t.Parallel()
54+
55+
// Start from a non-empty value so nil is seen to clear it.
56+
e := entrypoint{"stale"}
57+
err := e.UnmarshalTOML(tt.value)
58+
59+
if tt.wantErr != "" {
60+
require.Error(t, err)
61+
assert.Contains(t, err.Error(), tt.wantErr)
62+
return
63+
}
64+
65+
require.NoError(t, err)
66+
assert.Equal(t, tt.want, e)
67+
})
68+
}
69+
}
70+
71+
func TestEntrypointBinaryAndArgs(t *testing.T) {
72+
t.Parallel()
73+
74+
var empty entrypoint
75+
assert.Empty(t, empty.binary())
76+
assert.Nil(t, empty.args())
77+
78+
single := entrypoint{"./tmp/main"}
79+
assert.Equal(t, "./tmp/main", single.binary())
80+
assert.Nil(t, single.args())
81+
82+
withArgs := entrypoint{"./tmp/main", "server", ":8080"}
83+
assert.Equal(t, "./tmp/main", withArgs.binary())
84+
assert.Equal(t, []string{"server", ":8080"}, withArgs.args())
85+
}

runner/flag_value_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package runner
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestAppendableValueString(t *testing.T) {
11+
t.Parallel()
12+
13+
var nilValue *appendableValue
14+
assert.Empty(t, nilValue.String(), "nil receiver should stringify to empty")
15+
16+
assert.Empty(t, (&appendableValue{}).String(), "nil target should stringify to empty")
17+
18+
s := "a,b"
19+
assert.Equal(t, "a,b", (&appendableValue{p: &s}).String())
20+
}
21+
22+
func TestAppendableValueSet(t *testing.T) {
23+
t.Parallel()
24+
25+
tests := []struct {
26+
name string
27+
initial string
28+
sets []string
29+
want string
30+
}{
31+
{
32+
name: "first set replaces the default",
33+
initial: "default",
34+
sets: []string{"a"},
35+
want: "a",
36+
},
37+
{
38+
name: "later sets append",
39+
initial: "default",
40+
sets: []string{"a", "b,c"},
41+
want: "a" + sliceCmdArgSeparator + "b,c",
42+
},
43+
{
44+
name: "empty append is ignored",
45+
initial: "default",
46+
sets: []string{"a", ""},
47+
want: "a",
48+
},
49+
{
50+
name: "append onto an emptied value does not add a separator",
51+
initial: "default",
52+
sets: []string{"", "b"},
53+
want: "b",
54+
},
55+
}
56+
57+
for _, tt := range tests {
58+
t.Run(tt.name, func(t *testing.T) {
59+
t.Parallel()
60+
61+
target := tt.initial
62+
v := &appendableValue{p: &target}
63+
for _, s := range tt.sets {
64+
require.NoError(t, v.Set(s))
65+
}
66+
assert.Equal(t, tt.want, target)
67+
assert.Equal(t, tt.want, v.String())
68+
})
69+
}
70+
}

runner/logger_extra_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package runner
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/fatih/color"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// captureColorOutput redirects the writer fatih/color logs to, so colored log
13+
// lines can be asserted on instead of leaking into the test output.
14+
func captureColorOutput(t *testing.T) *bytes.Buffer {
15+
t.Helper()
16+
var buf bytes.Buffer
17+
orig := color.Error
18+
origNoColor := color.NoColor
19+
color.Error = &buf
20+
color.NoColor = true
21+
t.Cleanup(func() {
22+
color.Error = orig
23+
color.NoColor = origNoColor
24+
})
25+
return &buf
26+
}
27+
28+
func TestGetColorFallsBackToWhite(t *testing.T) {
29+
t.Parallel()
30+
31+
assert.Equal(t, color.FgRed, getColor("red"))
32+
assert.Equal(t, color.FgWhite, getColor("white"))
33+
assert.Equal(t, color.FgWhite, getColor("not-a-color"))
34+
}
35+
36+
func TestGetLoggerFallsBackToRawLogger(t *testing.T) {
37+
// Not parallel: captureColorOutput swaps package-level color state.
38+
buf := captureColorOutput(t)
39+
40+
cfg := defaultConfig()
41+
l := newLogger(&cfg)
42+
require.NotNil(t, l)
43+
44+
// Known names come from the configured loggers.
45+
assert.NotNil(t, l.main())
46+
assert.NotNil(t, l.build())
47+
assert.NotNil(t, l.runner())
48+
assert.NotNil(t, l.watcher())
49+
50+
// An unknown name falls back to the raw logger, which still logs.
51+
l.getLogger("no-such-logger")("fallback message")
52+
assert.NotNil(t, rawLogger())
53+
assert.Empty(t, buf.String(), "raw logger should not write through color.Error")
54+
}
55+
56+
func TestNewLogFuncSilentSkipsOutput(t *testing.T) {
57+
buf := captureColorOutput(t)
58+
59+
logFn := newLogFunc("white", cfgLog{Silent: true})
60+
logFn("should not be printed")
61+
62+
assert.Empty(t, buf.String())
63+
}
64+
65+
func TestNewLogFuncSkipsEmptyMessage(t *testing.T) {
66+
buf := captureColorOutput(t)
67+
68+
logFn := newLogFunc("white", cfgLog{})
69+
logFn(" \n ")
70+
71+
assert.Empty(t, buf.String())
72+
}
73+
74+
func TestNewLogFuncAddsTime(t *testing.T) {
75+
buf := captureColorOutput(t)
76+
77+
logFn := newLogFunc("white", cfgLog{AddTime: true})
78+
logFn("hello %s", "air")
79+
80+
out := buf.String()
81+
assert.Contains(t, out, "hello air")
82+
assert.Regexp(t, `^\[\d{2}:\d{2}:\d{2}\] `, out)
83+
}
84+
85+
func TestNewLoggerReturnsNilWithoutConfig(t *testing.T) {
86+
t.Parallel()
87+
88+
assert.Nil(t, newLogger(nil))
89+
}

0 commit comments

Comments
 (0)