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,142 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReadLastSyncAttempt(t *testing.T) {
|
||||
t.Run("missing file returns zero time", func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "does-not-exist")
|
||||
assert.True(t, ReadLastSyncAttempt(path).IsZero())
|
||||
})
|
||||
|
||||
t.Run("empty file returns zero time", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "lastrun")
|
||||
require.NoError(t, os.WriteFile(path, []byte(""), 0o600))
|
||||
assert.True(t, ReadLastSyncAttempt(path).IsZero())
|
||||
})
|
||||
|
||||
t.Run("unparseable contents return zero time", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "lastrun")
|
||||
require.NoError(t, os.WriteFile(path, []byte("not-a-number"), 0o600))
|
||||
assert.True(t, ReadLastSyncAttempt(path).IsZero())
|
||||
})
|
||||
|
||||
t.Run("valid epoch with trailing whitespace is parsed", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "lastrun")
|
||||
want := time.Unix(1700000000, 0)
|
||||
require.NoError(t, os.WriteFile(path, []byte("1700000000\n"), 0o600))
|
||||
assert.True(t, ReadLastSyncAttempt(path).Equal(want))
|
||||
})
|
||||
}
|
||||
|
||||
func TestWriteLastSyncAttempt(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "lastrun")
|
||||
|
||||
before := time.Now()
|
||||
require.NoError(t, WriteLastSyncAttempt(path))
|
||||
after := time.Now()
|
||||
|
||||
got := ReadLastSyncAttempt(path)
|
||||
require.False(t, got.IsZero())
|
||||
|
||||
// Allow 1s slack on both ends to absorb second-level truncation by
|
||||
// WriteLastSyncAttempt's Unix() serialization.
|
||||
assert.False(t, got.Before(before.Add(-time.Second)), "got=%s before=%s", got, before)
|
||||
assert.False(t, got.After(after.Add(time.Second)), "got=%s after=%s", got, after)
|
||||
}
|
||||
|
||||
func TestSyncCooldownElapsed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "lastrun")
|
||||
|
||||
t.Run("missing lastrun is treated as old enough", func(t *testing.T) {
|
||||
assert.True(t, SyncCooldownElapsed(path, time.Hour))
|
||||
})
|
||||
|
||||
t.Run("recent lastrun blocks", func(t *testing.T) {
|
||||
require.NoError(t, WriteLastSyncAttempt(path))
|
||||
assert.False(t, SyncCooldownElapsed(path, time.Hour))
|
||||
})
|
||||
|
||||
t.Run("old lastrun allows", func(t *testing.T) {
|
||||
old := time.Now().Add(-2 * time.Hour).Unix()
|
||||
require.NoError(t, os.WriteFile(path, []byte(strconv.FormatInt(old, 10)), 0o600))
|
||||
assert.True(t, SyncCooldownElapsed(path, time.Hour))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSyncLockSerializes(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "sync.lock")
|
||||
|
||||
first := NewSyncLock(path)
|
||||
ok, err := first.TryLock()
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
defer func() {
|
||||
require.NoError(t, first.Unlock())
|
||||
}()
|
||||
|
||||
t.Run("TryLock from a sibling lock fails while first is held", func(t *testing.T) {
|
||||
second := NewSyncLock(path)
|
||||
ok, err := second.TryLock()
|
||||
require.NoError(t, err)
|
||||
assert.False(t, ok)
|
||||
})
|
||||
|
||||
t.Run("TryLockContext from a sibling times out while first is held", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
second := NewSyncLock(path)
|
||||
ok, err := second.TryLockContext(ctx, 25*time.Millisecond)
|
||||
// On context cancel TryLockContext can return either (false, nil) or
|
||||
// (false, context.DeadlineExceeded) depending on timing; both satisfy
|
||||
// the "did not acquire" assertion.
|
||||
if err != nil {
|
||||
assert.ErrorIs(t, err, context.DeadlineExceeded)
|
||||
}
|
||||
assert.False(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSyncLockAllowsSecondAcquisitionAfterRelease(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "sync.lock")
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
first := NewSyncLock(path)
|
||||
ok, err := first.TryLock()
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
// assert (not require) is safe from a goroutine. require.NoError
|
||||
// calls t.FailNow which is not goroutine-safe.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
assert.NoError(t, first.Unlock())
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
second := NewSyncLock(path)
|
||||
ok, err = second.TryLock()
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
require.NoError(t, second.Unlock())
|
||||
}
|
||||
Reference in New Issue
Block a user