mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
feat: Add Support for Optimistic Cloud Sync (#273)
* feat: Add support for background sync * refactor: Maintain single source of truth for command defn * fix: Code review fixes * fix: Code review fixes * docs: Add corner case inline doc
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/safedep/pmg/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// spawnRecorder is a detachedSpawner stub that captures invocations instead
|
||||
// of actually forking. We swap it in via withMockSpawner.
|
||||
type spawnRecorder struct {
|
||||
mu sync.Mutex
|
||||
calls []spawnCall
|
||||
err error
|
||||
}
|
||||
|
||||
type spawnCall struct {
|
||||
name string
|
||||
args []string
|
||||
}
|
||||
|
||||
func (r *spawnRecorder) spawn(name string, args ...string) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
cp := make([]string, len(args))
|
||||
copy(cp, args)
|
||||
r.calls = append(r.calls, spawnCall{name: name, args: cp})
|
||||
return r.err
|
||||
}
|
||||
|
||||
func (r *spawnRecorder) callCount() int {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return len(r.calls)
|
||||
}
|
||||
|
||||
func withMockSpawner(t *testing.T) *spawnRecorder {
|
||||
t.Helper()
|
||||
rec := &spawnRecorder{}
|
||||
prev := spawnDetached
|
||||
spawnDetached = rec.spawn
|
||||
t.Cleanup(func() { spawnDetached = prev })
|
||||
return rec
|
||||
}
|
||||
|
||||
func newAutoSyncConfig(t *testing.T) *config.RuntimeConfig {
|
||||
t.Helper()
|
||||
tmpDir := t.TempDir()
|
||||
t.Setenv("PMG_CONFIG_DIR", tmpDir)
|
||||
// Make sure no PMG_DISABLE_TELEMETRY leak from the host environment can
|
||||
// fool the analytics short-circuit in MaybeSpawnBackgroundSync.
|
||||
t.Setenv("PMG_DISABLE_TELEMETRY", "false")
|
||||
|
||||
// Re-init the config so configDir picks up the tmpDir we just set.
|
||||
// Without this, cfg.CloudSyncLastRunPath() points at the user's real
|
||||
// config dir, which may not even exist in CI.
|
||||
config.Reload()
|
||||
t.Cleanup(config.Reload)
|
||||
|
||||
cfg := config.Get()
|
||||
cfg.Config.Cloud.Enabled = true
|
||||
cfg.Config.Cloud.AutoSync.Enabled = true
|
||||
cfg.Config.Cloud.AutoSync.MinInterval = 15 * time.Minute
|
||||
cfg.Config.Cloud.AutoSync.Timeout = time.Minute
|
||||
cfg.Config.DisableTelemetry = false
|
||||
return cfg
|
||||
}
|
||||
|
||||
func TestMaybeSpawnBackgroundSyncSpawnsByDefault(t *testing.T) {
|
||||
rec := withMockSpawner(t)
|
||||
cfg := newAutoSyncConfig(t)
|
||||
|
||||
MaybeSpawnBackgroundSync(cfg)
|
||||
|
||||
require.Equal(t, 1, rec.callCount())
|
||||
assert.Equal(t, []string{"cloud", "sync-background"}, rec.calls[0].args)
|
||||
assert.NotEmpty(t, rec.calls[0].name, "spawned name should be a resolved binary path")
|
||||
}
|
||||
|
||||
func TestMaybeSpawnBackgroundSyncShortCircuits(t *testing.T) {
|
||||
t.Run("nil config", func(t *testing.T) {
|
||||
rec := withMockSpawner(t)
|
||||
MaybeSpawnBackgroundSync(nil)
|
||||
assert.Equal(t, 0, rec.callCount())
|
||||
})
|
||||
|
||||
t.Run("cloud disabled", func(t *testing.T) {
|
||||
rec := withMockSpawner(t)
|
||||
cfg := newAutoSyncConfig(t)
|
||||
cfg.Config.Cloud.Enabled = false
|
||||
|
||||
MaybeSpawnBackgroundSync(cfg)
|
||||
assert.Equal(t, 0, rec.callCount())
|
||||
})
|
||||
|
||||
t.Run("auto_sync disabled", func(t *testing.T) {
|
||||
rec := withMockSpawner(t)
|
||||
cfg := newAutoSyncConfig(t)
|
||||
cfg.Config.Cloud.AutoSync.Enabled = false
|
||||
|
||||
MaybeSpawnBackgroundSync(cfg)
|
||||
assert.Equal(t, 0, rec.callCount())
|
||||
})
|
||||
|
||||
t.Run("telemetry disabled via config", func(t *testing.T) {
|
||||
rec := withMockSpawner(t)
|
||||
cfg := newAutoSyncConfig(t)
|
||||
cfg.Config.DisableTelemetry = true
|
||||
|
||||
MaybeSpawnBackgroundSync(cfg)
|
||||
assert.Equal(t, 0, rec.callCount())
|
||||
})
|
||||
|
||||
t.Run("we are the sync-background child", func(t *testing.T) {
|
||||
rec := withMockSpawner(t)
|
||||
cfg := newAutoSyncConfig(t)
|
||||
|
||||
t.Cleanup(func() { isBackgroundSyncChild = false })
|
||||
MarkBackgroundSyncChild()
|
||||
|
||||
MaybeSpawnBackgroundSync(cfg)
|
||||
assert.Equal(t, 0, rec.callCount())
|
||||
})
|
||||
}
|
||||
|
||||
func TestMaybeSpawnBackgroundSyncRespectsCooldown(t *testing.T) {
|
||||
rec := withMockSpawner(t)
|
||||
cfg := newAutoSyncConfig(t)
|
||||
|
||||
// Recent attempt → cooldown still in effect → no spawn.
|
||||
require.NoError(t, WriteLastSyncAttempt(cfg.CloudSyncLastRunPath()))
|
||||
|
||||
MaybeSpawnBackgroundSync(cfg)
|
||||
assert.Equal(t, 0, rec.callCount())
|
||||
}
|
||||
|
||||
func TestMaybeSpawnBackgroundSyncSpawnsAfterCooldownElapses(t *testing.T) {
|
||||
rec := withMockSpawner(t)
|
||||
cfg := newAutoSyncConfig(t)
|
||||
|
||||
old := time.Now().Add(-time.Hour).Unix()
|
||||
require.NoError(t, os.WriteFile(cfg.CloudSyncLastRunPath(),
|
||||
[]byte(strconv.FormatInt(old, 10)), 0o600))
|
||||
|
||||
MaybeSpawnBackgroundSync(cfg)
|
||||
assert.Equal(t, 1, rec.callCount())
|
||||
}
|
||||
|
||||
func TestMaybeSpawnBackgroundSyncSwallowsSpawnError(t *testing.T) {
|
||||
rec := withMockSpawner(t)
|
||||
rec.err = os.ErrPermission
|
||||
cfg := newAutoSyncConfig(t)
|
||||
|
||||
// We can't observe log.Warnf directly, but we can assert the function
|
||||
// neither panics nor blocks when the underlying spawn fails.
|
||||
assert.NotPanics(t, func() { MaybeSpawnBackgroundSync(cfg) })
|
||||
assert.Equal(t, 1, rec.callCount(), "spawner should still be invoked once")
|
||||
}
|
||||
Reference in New Issue
Block a user