Files
wehub-resource-sync a06f331eb8
install-script / powershell-syntax (push) Waiting to run
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
install-script / install (macos-14) (push) Waiting to run
install-script / install (ubuntu-latest) (push) Waiting to run
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

273 lines
7.6 KiB
Go

package config
import (
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/excludes"
)
func newTestConfigManager(t *testing.T) *ConfigManager {
t.Helper()
cm, err := NewConfigManager("/tmp/nonexistent-gortex-cache-test/config.yaml")
require.NoError(t, err)
return cm
}
func writeGitignore(t *testing.T, dir, content string) {
t.Helper()
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte(content), 0o644))
}
func writeWorkspace(t *testing.T, dir, content string) {
t.Helper()
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gortex.yaml"), []byte(content), 0o644))
}
// bumpMtime forces a stat-visible change so invalidation fires regardless
// of the filesystem's mtime resolution.
func bumpMtime(t *testing.T, path string) {
t.Helper()
future := time.Now().Add(2 * time.Second)
require.NoError(t, os.Chtimes(path, future, future))
}
// countingReader wires a call counter into the cache's file reader so a
// test can assert that a cache hit never re-opens the `.gitignore`.
func countingReader(cm *ConfigManager) *int64 {
var reads int64
cm.excludeCache.readFn = func(p string) []string {
atomic.AddInt64(&reads, 1)
return loadRepoGitignore(p)
}
return &reads
}
func TestExcludeCache_CacheHitDoesNotReReadGitignore(t *testing.T) {
cm := newTestConfigManager(t)
repoDir := t.TempDir()
writeGitignore(t, repoDir, "node_modules/\n*.log\n")
cm.LoadWorkspaceConfig("r", repoDir)
reads := countingReader(cm)
first := cm.EffectiveExclude("r")
second := cm.EffectiveExclude("r")
assert.Contains(t, first, "node_modules/")
assert.Contains(t, second, "node_modules/")
assert.Contains(t, second, "*.log")
assert.Equal(t, first, second, "cache hit must return the same layered list")
assert.Equal(t, int64(1), atomic.LoadInt64(reads),
"the second call must be a cache hit, not a re-read")
}
func TestExcludeCache_IdenticalMatchingColdVsWarm(t *testing.T) {
cm := newTestConfigManager(t)
repoDir := t.TempDir()
writeGitignore(t, repoDir, "build/\n*.tmp\ndata/raw/\n")
cm.LoadWorkspaceConfig("r", repoDir)
cases := []struct {
path string
exclude bool
}{
{"build/output.o", true},
{"src/main.go", false},
{"scratch.tmp", true},
{"data/raw/big.bin", true},
{"data/clean/ok.csv", false},
{".git/config", true}, // builtin baseline
{"README.md", false},
}
assertMatches := func(list []string, label string) {
m := excludes.New(list)
for _, c := range cases {
assert.Equalf(t, c.exclude, m.MatchRel(c.path),
"%s: path %q", label, c.path)
}
}
assertMatches(cm.EffectiveExclude("r"), "cold") // populates the cache
assertMatches(cm.EffectiveExclude("r"), "warm") // cache hit
}
func TestExcludeCache_InvalidatesOnGitignoreChange(t *testing.T) {
cm := newTestConfigManager(t)
repoDir := t.TempDir()
writeGitignore(t, repoDir, "old-only/\n")
cm.LoadWorkspaceConfig("r", repoDir)
reads := countingReader(cm)
first := cm.EffectiveExclude("r")
assert.Contains(t, first, "old-only/")
// Different content (also a different byte size) plus a bumped mtime so
// the change is detected even on a coarse-resolution filesystem.
writeGitignore(t, repoDir, "new-only/\nsecond-new/\n")
bumpMtime(t, filepath.Join(repoDir, ".gitignore"))
second := cm.EffectiveExclude("r")
assert.Contains(t, second, "new-only/", "new patterns must take effect after the edit")
assert.Contains(t, second, "second-new/")
assert.NotContains(t, second, "old-only/", "stale patterns must be dropped")
assert.Equal(t, int64(2), atomic.LoadInt64(reads),
"the changed file must be re-read exactly once")
}
func TestExcludeCache_InvalidatesOnWorkspaceReload(t *testing.T) {
cm := newTestConfigManager(t)
repoDir := t.TempDir()
writeWorkspace(t, repoDir, "exclude:\n - \"first/**\"\n")
cm.LoadWorkspaceConfig("r", repoDir)
first := cm.EffectiveExclude("r")
assert.Contains(t, first, "first/**")
assert.NotContains(t, first, "second/**")
// Reloading swaps the cached *Config pointer, which must invalidate the
// merged entry even though the `.gitignore` (absent) is unchanged.
writeWorkspace(t, repoDir, "exclude:\n - \"second/**\"\n")
cm.LoadWorkspaceConfig("r", repoDir)
second := cm.EffectiveExclude("r")
assert.Contains(t, second, "second/**", "workspace edit must be reflected")
assert.NotContains(t, second, "first/**", "stale workspace patterns must be dropped")
}
func TestExcludeCache_NoGitignoreNeverReads(t *testing.T) {
cm := newTestConfigManager(t)
repoDir := t.TempDir()
cm.LoadWorkspaceConfig("r", repoDir)
reads := countingReader(cm)
got := cm.EffectiveExclude("r")
_ = cm.EffectiveExclude("r")
assert.Equal(t, excludes.Builtin, got, "absent .gitignore yields the builtin baseline")
assert.Equal(t, int64(0), atomic.LoadInt64(reads),
"an absent .gitignore must not trigger a file read")
}
func TestExcludeCache_ReturnedSliceIsClippedAndAppendSafe(t *testing.T) {
cm := newTestConfigManager(t)
repoDir := t.TempDir()
writeGitignore(t, repoDir, "z-marker/\n")
cm.LoadWorkspaceConfig("r", repoDir)
first := cm.EffectiveExclude("r")
require.Equal(t, len(first), cap(first),
"returned slice must be clipped so append reallocates")
// Appending to the returned slice must not disturb the shared cached
// value handed to every other reader.
_ = append(first, "mutant/**") //nolint:staticcheck // intentional: exercise append safety
second := cm.EffectiveExclude("r")
assert.NotContains(t, second, "mutant/**",
"appending to the returned slice must not corrupt the cache")
assert.Equal(t, first, second)
}
// uncachedEffectiveExclude reproduces the pre-cache EffectiveExclude body —
// a fresh `.gitignore` read plus a fresh merge on every call — so the
// benchmark can quote the baseline the cache replaces.
func uncachedEffectiveExclude(gc *GlobalConfig, ws *Config, repoPrefix, repoPath string) []string {
out := make([]string, 0, 32)
out = append(out, excludes.Builtin...)
if shouldRespectGitignore(ws) && repoPath != "" {
out = append(out, loadRepoGitignore(repoPath)...)
}
if gc != nil {
out = append(out, gc.Exclude...)
if entry := gc.FindRepoByPrefix(repoPrefix); entry != nil {
out = append(out, entry.Exclude...)
}
}
if ws != nil {
out = append(out, ws.Exclude...)
if len(ws.Exclude) == 0 {
out = append(out, ws.Index.Exclude...)
out = append(out, ws.Watch.Exclude...)
}
}
if ws != nil {
for _, inc := range ws.Include {
inc = strings.TrimSpace(inc)
if inc == "" {
continue
}
if !strings.HasPrefix(inc, "!") {
inc = "!" + inc
}
out = append(out, inc)
}
}
return out
}
const benchGitignore = `# build output
build/
dist/
*.log
*.tmp
node_modules/
vendor/
coverage/
.cache/
target/
__pycache__/
*.pyc
.venv/
data/raw/
data/interim/
testdata/large/
*.bin
*.gz
.DS_Store
`
func benchSetup(b *testing.B) *ConfigManager {
b.Helper()
cm, err := NewConfigManager("/tmp/nonexistent-gortex-cache-bench/config.yaml")
require.NoError(b, err)
repoDir := b.TempDir()
require.NoError(b, os.WriteFile(filepath.Join(repoDir, ".gitignore"), []byte(benchGitignore), 0o644))
cm.LoadWorkspaceConfig("r", repoDir)
return cm
}
func BenchmarkEffectiveExclude(b *testing.B) {
cm := benchSetup(b)
cm.EffectiveExclude("r") // warm the cache
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = cm.EffectiveExclude("r")
}
}
func BenchmarkEffectiveExcludeUncached(b *testing.B) {
cm := benchSetup(b)
cm.mu.RLock()
gc := cm.global
ws := cm.workspace["r"]
repoPath := cm.workspacePaths["r"]
cm.mu.RUnlock()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = uncachedEffectiveExclude(gc, ws, "r", repoPath)
}
}