mirror of
https://github.com/tw93/Mole.git
synced 2026-03-22 18:30:08 +00:00
fix: harden CI test stability and status collector resilience
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -43,6 +43,7 @@ tests/tmp-*
|
|||||||
|
|
||||||
# AI Assistant Instructions
|
# AI Assistant Instructions
|
||||||
.claude/
|
.claude/
|
||||||
|
.agents/
|
||||||
.gemini/
|
.gemini/
|
||||||
.kiro/
|
.kiro/
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
|||||||
@@ -92,10 +92,7 @@ func TestScanPathConcurrentBasic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDeletePathWithProgress(t *testing.T) {
|
func TestDeletePathWithProgress(t *testing.T) {
|
||||||
// Skip in CI environments where Finder may not be available.
|
skipIfFinderUnavailable(t)
|
||||||
if os.Getenv("CI") != "" {
|
|
||||||
t.Skip("Skipping Finder-dependent test in CI")
|
|
||||||
}
|
|
||||||
|
|
||||||
parent := t.TempDir()
|
parent := t.TempDir()
|
||||||
target := filepath.Join(parent, "target")
|
target := filepath.Join(parent, "target")
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestTrashPathWithProgress(t *testing.T) {
|
func TestTrashPathWithProgress(t *testing.T) {
|
||||||
// Skip in CI environments where Finder may not be available.
|
skipIfFinderUnavailable(t)
|
||||||
if os.Getenv("CI") != "" {
|
|
||||||
t.Skip("Skipping Finder-dependent test in CI")
|
|
||||||
}
|
|
||||||
|
|
||||||
parent := t.TempDir()
|
parent := t.TempDir()
|
||||||
target := filepath.Join(parent, "target")
|
target := filepath.Join(parent, "target")
|
||||||
@@ -42,10 +39,7 @@ func TestTrashPathWithProgress(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDeleteMultiplePathsCmdHandlesParentChild(t *testing.T) {
|
func TestDeleteMultiplePathsCmdHandlesParentChild(t *testing.T) {
|
||||||
// Skip in CI environments where Finder may not be available.
|
skipIfFinderUnavailable(t)
|
||||||
if os.Getenv("CI") != "" {
|
|
||||||
t.Skip("Skipping Finder-dependent test in CI")
|
|
||||||
}
|
|
||||||
|
|
||||||
base := t.TempDir()
|
base := t.TempDir()
|
||||||
parent := filepath.Join(base, "parent")
|
parent := filepath.Join(base, "parent")
|
||||||
|
|||||||
44
cmd/analyze/test_helpers_test.go
Normal file
44
cmd/analyze/test_helpers_test.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func skipIfFinderUnavailable(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if os.Getenv("CI") != "" {
|
||||||
|
t.Skip("Skipping Finder-dependent test in CI")
|
||||||
|
}
|
||||||
|
if os.Getenv("MOLE_SKIP_FINDER_TESTS") == "1" {
|
||||||
|
t.Skip("Skipping Finder-dependent test via MOLE_SKIP_FINDER_TESTS")
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("osascript"); err != nil {
|
||||||
|
t.Skipf("Skipping Finder-dependent test, osascript unavailable: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "osascript", "-e", `tell application "Finder" to get name`)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
text := strings.ToLower(string(output))
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
t.Skip("Skipping Finder-dependent test, Finder probe timed out")
|
||||||
|
}
|
||||||
|
if strings.Contains(text, "connection invalid") || strings.Contains(text, "can’t get application \"finder\"") || strings.Contains(text, "can't get application \"finder\"") {
|
||||||
|
t.Skipf("Skipping Finder-dependent test, Finder probe indicates unavailable session: %s", strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
reason := strings.TrimSpace(string(output))
|
||||||
|
if reason == "" {
|
||||||
|
reason = err.Error()
|
||||||
|
}
|
||||||
|
t.Skipf("Skipping Finder-dependent test, Finder unavailable: %s", reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -230,6 +230,9 @@ func (c *Collector) Collect() (MetricsSnapshot, error) {
|
|||||||
|
|
||||||
// Host info is cached by gopsutil; fetch once.
|
// Host info is cached by gopsutil; fetch once.
|
||||||
hostInfo, _ := host.Info()
|
hostInfo, _ := host.Info()
|
||||||
|
if hostInfo == nil {
|
||||||
|
hostInfo = &host.InfoStat{}
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
@@ -255,6 +258,18 @@ func (c *Collector) Collect() (MetricsSnapshot, error) {
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
errMu.Lock()
|
||||||
|
panicErr := fmt.Errorf("collector panic: %v", r)
|
||||||
|
if mergeErr == nil {
|
||||||
|
mergeErr = panicErr
|
||||||
|
} else {
|
||||||
|
mergeErr = fmt.Errorf("%v; %w", mergeErr, panicErr)
|
||||||
|
}
|
||||||
|
errMu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
if err := fn(); err != nil {
|
if err := fn(); err != nil {
|
||||||
errMu.Lock()
|
errMu.Lock()
|
||||||
if mergeErr == nil {
|
if mergeErr == nil {
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ func collectMemory() (MemoryStatus, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
swap, _ := mem.SwapMemory()
|
swap, _ := mem.SwapMemory()
|
||||||
|
if swap == nil {
|
||||||
|
swap = &mem.SwapMemoryStat{}
|
||||||
|
}
|
||||||
pressure := getMemoryPressure()
|
pressure := getMemoryPressure()
|
||||||
|
|
||||||
// On macOS, vm.Cached is 0, so we calculate from file-backed pages.
|
// On macOS, vm.Cached is 0, so we calculate from file-backed pages.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -13,10 +14,25 @@ import (
|
|||||||
"github.com/shirou/gopsutil/v4/net"
|
"github.com/shirou/gopsutil/v4/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ioCountersFunc = net.IOCounters
|
||||||
|
|
||||||
|
func collectIOCountersSafely(pernic bool) (stats []net.IOCountersStat, err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = fmt.Errorf("panic collecting network counters: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return ioCountersFunc(pernic)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Collector) collectNetwork(now time.Time) ([]NetworkStatus, error) {
|
func (c *Collector) collectNetwork(now time.Time) ([]NetworkStatus, error) {
|
||||||
stats, err := net.IOCounters(true)
|
stats, err := collectIOCountersSafely(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
// Some restricted environments can break netstat-backed collectors.
|
||||||
|
// Degrade gracefully to keep status output available.
|
||||||
|
c.rxHistoryBuf.Add(0)
|
||||||
|
c.txHistoryBuf.Add(0)
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map interface IPs.
|
// Map interface IPs.
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
gopsutilnet "github.com/shirou/gopsutil/v4/net"
|
||||||
|
)
|
||||||
|
|
||||||
func TestCollectProxyFromEnvSupportsAllProxy(t *testing.T) {
|
func TestCollectProxyFromEnvSupportsAllProxy(t *testing.T) {
|
||||||
env := map[string]string{
|
env := map[string]string{
|
||||||
@@ -58,3 +63,41 @@ func TestCollectProxyFromScutilOutputHTTPHostPort(t *testing.T) {
|
|||||||
t.Fatalf("unexpected host: %s", got.Host)
|
t.Fatalf("unexpected host: %s", got.Host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCollectIOCountersSafelyRecoversPanic(t *testing.T) {
|
||||||
|
original := ioCountersFunc
|
||||||
|
ioCountersFunc = func(bool) ([]gopsutilnet.IOCountersStat, error) {
|
||||||
|
panic("boom")
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { ioCountersFunc = original })
|
||||||
|
|
||||||
|
stats, err := collectIOCountersSafely(true)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error from panic recovery")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "panic collecting network counters") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(stats) != 0 {
|
||||||
|
t.Fatalf("expected empty stats when panic recovered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectIOCountersSafelyReturnsData(t *testing.T) {
|
||||||
|
original := ioCountersFunc
|
||||||
|
want := []gopsutilnet.IOCountersStat{
|
||||||
|
{Name: "en0", BytesRecv: 1, BytesSent: 2},
|
||||||
|
}
|
||||||
|
ioCountersFunc = func(bool) ([]gopsutilnet.IOCountersStat, error) {
|
||||||
|
return want, nil
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { ioCountersFunc = original })
|
||||||
|
|
||||||
|
got, err := collectIOCountersSafely(true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(got) != 1 || got[0].Name != "en0" {
|
||||||
|
t.Fatalf("unexpected stats: %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ clean_homebrew() {
|
|||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
# Keep behavior consistent with dry-run preview.
|
||||||
|
if is_path_whitelisted "$HOME/Library/Caches/Homebrew"; then
|
||||||
|
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Homebrew · skipped whitelist"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
# Skip if cleaned recently to avoid repeated heavy operations.
|
# Skip if cleaned recently to avoid repeated heavy operations.
|
||||||
local brew_cache_file="${HOME}/.cache/mole/brew_last_cleanup"
|
local brew_cache_file="${HOME}/.cache/mole/brew_last_cleanup"
|
||||||
local cache_valid_days=7
|
local cache_valid_days=7
|
||||||
|
|||||||
@@ -150,7 +150,11 @@ echo ""
|
|||||||
|
|
||||||
echo "3. Running Go tests..."
|
echo "3. Running Go tests..."
|
||||||
if command -v go > /dev/null 2>&1; then
|
if command -v go > /dev/null 2>&1; then
|
||||||
if go build ./... > /dev/null 2>&1 && go vet ./cmd/... > /dev/null 2>&1 && go test ./cmd/... > /dev/null 2>&1; then
|
GO_TEST_CACHE="${MOLE_GO_TEST_CACHE:-/tmp/mole-go-build-cache}"
|
||||||
|
mkdir -p "$GO_TEST_CACHE"
|
||||||
|
if GOCACHE="$GO_TEST_CACHE" go build ./... > /dev/null 2>&1 &&
|
||||||
|
GOCACHE="$GO_TEST_CACHE" go vet ./cmd/... > /dev/null 2>&1 &&
|
||||||
|
GOCACHE="$GO_TEST_CACHE" go test ./cmd/... > /dev/null 2>&1; then
|
||||||
printf "${GREEN}${ICON_SUCCESS} Go tests passed${NC}\n"
|
printf "${GREEN}${ICON_SUCCESS} Go tests passed${NC}\n"
|
||||||
else
|
else
|
||||||
printf "${RED}${ICON_ERROR} Go tests failed${NC}\n"
|
printf "${RED}${ICON_ERROR} Go tests failed${NC}\n"
|
||||||
@@ -182,9 +186,23 @@ echo ""
|
|||||||
|
|
||||||
echo "6. Testing installation..."
|
echo "6. Testing installation..."
|
||||||
# Skip if Homebrew mole is installed (install.sh will refuse to overwrite)
|
# Skip if Homebrew mole is installed (install.sh will refuse to overwrite)
|
||||||
if brew list mole &> /dev/null; then
|
install_test_home=""
|
||||||
|
if command -v brew > /dev/null 2>&1 && brew list mole &> /dev/null; then
|
||||||
printf "${GREEN}${ICON_SUCCESS} Installation test skipped, Homebrew${NC}\n"
|
printf "${GREEN}${ICON_SUCCESS} Installation test skipped, Homebrew${NC}\n"
|
||||||
elif ./install.sh --prefix /tmp/mole-test > /dev/null 2>&1; then
|
else
|
||||||
|
install_test_home="$(mktemp -d /tmp/mole-test-home.XXXXXX 2> /dev/null || true)"
|
||||||
|
if [[ -z "$install_test_home" ]]; then
|
||||||
|
install_test_home="/tmp/mole-test-home"
|
||||||
|
mkdir -p "$install_test_home"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -z "$install_test_home" ]]; then
|
||||||
|
:
|
||||||
|
elif HOME="$install_test_home" \
|
||||||
|
XDG_CONFIG_HOME="$install_test_home/.config" \
|
||||||
|
XDG_CACHE_HOME="$install_test_home/.cache" \
|
||||||
|
MO_NO_OPLOG=1 \
|
||||||
|
./install.sh --prefix /tmp/mole-test > /dev/null 2>&1; then
|
||||||
if [ -f /tmp/mole-test/mole ]; then
|
if [ -f /tmp/mole-test/mole ]; then
|
||||||
printf "${GREEN}${ICON_SUCCESS} Installation test passed${NC}\n"
|
printf "${GREEN}${ICON_SUCCESS} Installation test passed${NC}\n"
|
||||||
else
|
else
|
||||||
@@ -195,7 +213,10 @@ else
|
|||||||
printf "${RED}${ICON_ERROR} Installation test failed${NC}\n"
|
printf "${RED}${ICON_ERROR} Installation test failed${NC}\n"
|
||||||
((FAILED++))
|
((FAILED++))
|
||||||
fi
|
fi
|
||||||
safe_remove "/tmp/mole-test" true || true
|
MO_NO_OPLOG=1 safe_remove "/tmp/mole-test" true || true
|
||||||
|
if [[ -n "$install_test_home" ]]; then
|
||||||
|
MO_NO_OPLOG=1 safe_remove "$install_test_home" true || true
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
echo "==============================="
|
echo "==============================="
|
||||||
|
|||||||
Reference in New Issue
Block a user