mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 17:50:12 +00:00
175 lines
5.3 KiB
Go
175 lines
5.3 KiB
Go
//go:build linux && agentcompat
|
|
|
|
package scenario
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/nezhahq/nezha/integration/agentcompat/internal/client"
|
|
processharness "github.com/nezhahq/nezha/integration/agentcompat/internal/process"
|
|
)
|
|
|
|
type stressSQLiteHoldControlProbe struct {
|
|
mu sync.Mutex
|
|
calls []string
|
|
started chan struct{}
|
|
done chan struct{}
|
|
once sync.Once
|
|
}
|
|
|
|
func newStressSQLiteHoldControlProbe() *stressSQLiteHoldControlProbe {
|
|
return &stressSQLiteHoldControlProbe{started: make(chan struct{}), done: make(chan struct{})}
|
|
}
|
|
|
|
func (probe *stressSQLiteHoldControlProbe) record(call string) {
|
|
probe.mu.Lock()
|
|
defer probe.mu.Unlock()
|
|
probe.calls = append(probe.calls, call)
|
|
}
|
|
|
|
func (probe *stressSQLiteHoldControlProbe) ArmSQLiteHold(context.Context) (client.SQLiteHoldReceipt, error) {
|
|
probe.record("arm")
|
|
return client.SQLiteHoldReceipt{ID: "ERERERERERERERERERERERERERERERERERERERERERE", State: client.SQLiteHoldStateArmed}, nil
|
|
}
|
|
|
|
func (probe *stressSQLiteHoldControlProbe) WaitForSQLiteHold(ctx context.Context, receipt client.SQLiteHoldReceipt, target client.SQLiteHoldState) (client.SQLiteHoldReceipt, error) {
|
|
if target == client.SQLiteHoldStateSelected {
|
|
select {
|
|
case <-probe.started:
|
|
case <-ctx.Done():
|
|
return client.SQLiteHoldReceipt{}, ctx.Err()
|
|
}
|
|
}
|
|
probe.record("wait-" + string(target))
|
|
receipt.State = target
|
|
return receipt, nil
|
|
}
|
|
|
|
func (probe *stressSQLiteHoldControlProbe) ReleaseSQLiteHold(context.Context, client.SQLiteHoldReceipt) (client.SQLiteHoldReceipt, error) {
|
|
probe.record("release")
|
|
probe.once.Do(func() { close(probe.done) })
|
|
return client.SQLiteHoldReceipt{State: client.SQLiteHoldStateReleased}, nil
|
|
}
|
|
|
|
func (probe *stressSQLiteHoldControlProbe) AbortSQLiteHold(context.Context, client.SQLiteHoldReceipt) (client.SQLiteHoldReceipt, error) {
|
|
probe.record("abort")
|
|
probe.once.Do(func() { close(probe.done) })
|
|
return client.SQLiteHoldReceipt{State: client.SQLiteHoldStateAborted}, nil
|
|
}
|
|
|
|
type stressSQLiteJournalWatchProbe struct {
|
|
control *stressSQLiteHoldControlProbe
|
|
}
|
|
|
|
func (watch stressSQLiteJournalWatchProbe) Wait(ctx context.Context) error {
|
|
select {
|
|
case <-watch.control.done:
|
|
watch.control.record("watch-wait")
|
|
return nil
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
func (watch stressSQLiteJournalWatchProbe) Close() error {
|
|
watch.control.record("watch-close")
|
|
return nil
|
|
}
|
|
|
|
func TestDrainStressSQLiteJournalOrdersHoldLifecycleBeforeCompletion(t *testing.T) {
|
|
// Given
|
|
control := newStressSQLiteHoldControlProbe()
|
|
journalPath := filepath.Join(t.TempDir(), "dashboard.sqlite-journal")
|
|
|
|
// When
|
|
err := drainStressSQLiteJournal(t.Context(), control, func(ctx context.Context) error {
|
|
control.record("writer-start")
|
|
close(control.started)
|
|
select {
|
|
case <-control.done:
|
|
control.record("writer-complete")
|
|
return nil
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}, journalPath, func(path string) (stressSQLiteJournalWatch, error) {
|
|
require.Equal(t, journalPath, path)
|
|
control.record("watch-open")
|
|
return stressSQLiteJournalWatchProbe{control: control}, nil
|
|
})
|
|
|
|
// Then
|
|
require.NoError(t, err)
|
|
control.mu.Lock()
|
|
calls := append([]string(nil), control.calls...)
|
|
control.mu.Unlock()
|
|
require.Less(t, callIndex(calls, "arm"), callIndex(calls, "writer-start"))
|
|
require.Less(t, callIndex(calls, "writer-start"), callIndex(calls, "wait-selected"))
|
|
require.Less(t, callIndex(calls, "wait-selected"), callIndex(calls, "wait-finalizing"))
|
|
require.Less(t, callIndex(calls, "wait-finalizing"), callIndex(calls, "watch-open"))
|
|
require.Less(t, callIndex(calls, "watch-open"), callIndex(calls, "release"))
|
|
require.Less(t, callIndex(calls, "release"), callIndex(calls, "watch-wait"))
|
|
require.Less(t, callIndex(calls, "release"), callIndex(calls, "writer-complete"))
|
|
require.Less(t, callIndex(calls, "watch-wait"), callIndex(calls, "watch-close"))
|
|
}
|
|
|
|
func TestDrainStressSQLiteJournalAbortsWriterWhenWatchCannotOpen(t *testing.T) {
|
|
// Given
|
|
control := newStressSQLiteHoldControlProbe()
|
|
watchErr := errors.New("watch unavailable")
|
|
|
|
// When
|
|
err := drainStressSQLiteJournal(t.Context(), control, func(ctx context.Context) error {
|
|
close(control.started)
|
|
select {
|
|
case <-control.done:
|
|
return nil
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}, filepath.Join(t.TempDir(), "dashboard.sqlite-journal"), func(string) (stressSQLiteJournalWatch, error) {
|
|
return nil, watchErr
|
|
})
|
|
|
|
// Then
|
|
require.ErrorIs(t, err, watchErr)
|
|
control.mu.Lock()
|
|
calls := append([]string(nil), control.calls...)
|
|
control.mu.Unlock()
|
|
require.Contains(t, calls, "abort")
|
|
}
|
|
|
|
func TestObserveStressDashboardSQLiteJournalRejectsFirstHeldSample(t *testing.T) {
|
|
// Given
|
|
path := filepath.Join(t.TempDir(), "dashboard.sqlite-journal")
|
|
require.NoError(t, os.WriteFile(path, []byte("journal"), 0o600))
|
|
file, err := os.Open(path)
|
|
require.NoError(t, err)
|
|
observer := observeStressDashboardSQLiteJournal(path)
|
|
|
|
// When
|
|
heldErr := observer(t.Context(), processharness.Sample{PID: os.Getpid()})
|
|
require.NoError(t, file.Close())
|
|
releasedErr := observer(t.Context(), processharness.Sample{PID: os.Getpid()})
|
|
|
|
// Then
|
|
require.ErrorIs(t, heldErr, ErrStressSQLiteJournalNotDrained)
|
|
require.NoError(t, releasedErr)
|
|
}
|
|
|
|
func callIndex(calls []string, wanted string) int {
|
|
for index, call := range calls {
|
|
if call == wanted {
|
|
return index
|
|
}
|
|
}
|
|
return len(calls)
|
|
}
|