mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
test(mock): one failure plan replaces the per-call wrapper drivers
mock.Failures already made a method fail on every call. FailurePlan adds the call numbers, which is the shape of nearly every device fault the runner has to survive: the first read times out, the next one works. Expressing that by embedding the mock in a one-off wrapper put a different seven-line method in every test that needed one.
This commit is contained in:
1 parent
970dc9a6f5
commit
22296c5316
4 files changed
+34
-12
No files matched your search
@@ -3,6 +3,7 @@ package mock
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -49,19 +50,31 @@ type Action struct {
|
|||||||
Idle time.Duration
|
Idle time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FailurePlan makes one method fail. OnCalls names the 1-based calls that fail
|
||||||
|
// and every other call succeeds; an empty OnCalls fails every call. It exists
|
||||||
|
// because "the first read times out and the next one works" is the shape of
|
||||||
|
// nearly every device fault the runner has to survive, and expressing it by
|
||||||
|
// embedding the mock in a one-off wrapper puts a different seven-line method in
|
||||||
|
// every test that needs one.
|
||||||
|
type FailurePlan struct {
|
||||||
|
Err error
|
||||||
|
OnCalls []int
|
||||||
|
}
|
||||||
|
|
||||||
// Driver is an in-memory Driver implementation for unit tests.
|
// Driver is an in-memory Driver implementation for unit tests.
|
||||||
// Tests can program HierarchyJSON, ImageData, HealthInfo, and per-method
|
// Tests can program HierarchyJSON, ImageData, HealthInfo, and per-method
|
||||||
// Failures, and read back Actions to assert what the runner asked for.
|
// Failures, and read back Actions to assert what the runner asked for.
|
||||||
type Driver struct {
|
type Driver struct {
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
actions []Action
|
actions []Action
|
||||||
|
callCounts map[ActionKind]int
|
||||||
|
|
||||||
HierarchyJSON string
|
HierarchyJSON string
|
||||||
ImageData driver.Image
|
ImageData driver.Image
|
||||||
HealthInfo driver.Health
|
HealthInfo driver.Health
|
||||||
LogEntries []driver.LogEntry
|
LogEntries []driver.LogEntry
|
||||||
MetricsData driver.Metrics
|
MetricsData driver.Metrics
|
||||||
Failures map[ActionKind]error
|
Failures map[ActionKind]FailurePlan
|
||||||
|
|
||||||
// ReplacesText makes the mock assert the TextReplacer capability, so
|
// ReplacesText makes the mock assert the TextReplacer capability, so
|
||||||
// tests cover both the erase-before-type and replace-on-input paths.
|
// tests cover both the erase-before-type and replace-on-input paths.
|
||||||
@@ -90,7 +103,8 @@ type Driver struct {
|
|||||||
|
|
||||||
func New() *Driver {
|
func New() *Driver {
|
||||||
return &Driver{
|
return &Driver{
|
||||||
Failures: map[ActionKind]error{},
|
Failures: map[ActionKind]FailurePlan{},
|
||||||
|
callCounts: map[ActionKind]int{},
|
||||||
HealthInfo: driver.Health{
|
HealthInfo: driver.Health{
|
||||||
Ready: true,
|
Ready: true,
|
||||||
Version: "mock",
|
Version: "mock",
|
||||||
@@ -116,7 +130,15 @@ func (d *Driver) record(action Action) {
|
|||||||
func (d *Driver) failure(kind ActionKind) error {
|
func (d *Driver) failure(kind ActionKind) error {
|
||||||
d.mutex.Lock()
|
d.mutex.Lock()
|
||||||
defer d.mutex.Unlock()
|
defer d.mutex.Unlock()
|
||||||
return d.Failures[kind]
|
d.callCounts[kind]++
|
||||||
|
plan, planned := d.Failures[kind]
|
||||||
|
if !planned {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(plan.OnCalls) == 0 || slices.Contains(plan.OnCalls, d.callCounts[kind]) {
|
||||||
|
return plan.Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) Launch(_ context.Context, bundleID string, clearState bool, _ map[string]string) error {
|
func (d *Driver) Launch(_ context.Context, bundleID string, clearState bool, _ map[string]string) error {
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ func TestProgrammableScreenshotIsReturned(t *testing.T) {
|
|||||||
func TestFailureInjection(t *testing.T) {
|
func TestFailureInjection(t *testing.T) {
|
||||||
boom := errors.New("boom")
|
boom := errors.New("boom")
|
||||||
mock := New()
|
mock := New()
|
||||||
mock.Failures[ActionTap] = boom
|
mock.Failures[ActionTap] = FailurePlan{Err: boom}
|
||||||
|
|
||||||
if err := mock.Tap(context.Background(), 0, 0); !errors.Is(err, boom) {
|
if err := mock.Tap(context.Background(), 0, 0); !errors.Is(err, boom) {
|
||||||
t.Fatalf("expected boom, got %v", err)
|
t.Fatalf("expected boom, got %v", err)
|
||||||
|
|||||||
@@ -365,9 +365,9 @@ func TestRenderSummary_CountsTheStepsNothingJudged(t *testing.T) {
|
|||||||
// green iOS summary is indistinguishable from one over a silent app.
|
// green iOS summary is indistinguishable from one over a silent app.
|
||||||
func TestRunner_ReadsTheDriverCannotMakeAreNotPassedChecks(t *testing.T) {
|
func TestRunner_ReadsTheDriverCannotMakeAreNotPassedChecks(t *testing.T) {
|
||||||
state := newHarnessWithSpec(t, logErrorSpec)
|
state := newHarnessWithSpec(t, logErrorSpec)
|
||||||
state.mock.Failures[mockdriver.ActionRecentLogs] = driver.ErrNotSupported
|
state.mock.Failures[mockdriver.ActionRecentLogs] = mockdriver.FailurePlan{Err: driver.ErrNotSupported}
|
||||||
state.mock.Failures[mockdriver.ActionMetrics] = driver.ErrNotSupported
|
state.mock.Failures[mockdriver.ActionMetrics] = mockdriver.FailurePlan{Err: driver.ErrNotSupported}
|
||||||
state.mock.Failures[mockdriver.ActionHealth] = driver.ErrNotSupported
|
state.mock.Failures[mockdriver.ActionHealth] = mockdriver.FailurePlan{Err: driver.ErrNotSupported}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -877,7 +877,7 @@ func TestTraceActionFor_RecordsKindSpecificFields(t *testing.T) {
|
|||||||
|
|
||||||
func TestRunner_LogsWaitForIdleDriverErrors(t *testing.T) {
|
func TestRunner_LogsWaitForIdleDriverErrors(t *testing.T) {
|
||||||
state := newHarness(t)
|
state := newHarness(t)
|
||||||
state.mock.Failures[mockdriver.ActionWaitForIdle] = errors.New("sidecar lost gRPC stream")
|
state.mock.Failures[mockdriver.ActionWaitForIdle] = mockdriver.FailurePlan{Err: errors.New("sidecar lost gRPC stream")}
|
||||||
|
|
||||||
var logBuf bytes.Buffer
|
var logBuf bytes.Buffer
|
||||||
logger := slog.New(slog.NewTextHandler(&logBuf, &slog.HandlerOptions{Level: slog.LevelWarn}))
|
logger := slog.New(slog.NewTextHandler(&logBuf, &slog.HandlerOptions{Level: slog.LevelWarn}))
|
||||||
@@ -976,7 +976,7 @@ func TestApplyAction_InputTextSkipsEraseWhenTargetEmpty(t *testing.T) {
|
|||||||
func TestApplyAction_InputTextSurfacesFocusTapError(t *testing.T) {
|
func TestApplyAction_InputTextSurfacesFocusTapError(t *testing.T) {
|
||||||
t.Run("selector focus tap fails", func(t *testing.T) {
|
t.Run("selector focus tap fails", func(t *testing.T) {
|
||||||
driverMock := mockdriver.New()
|
driverMock := mockdriver.New()
|
||||||
driverMock.Failures[mockdriver.ActionTapSelector] = errors.New("adb unreachable")
|
driverMock.Failures[mockdriver.ActionTapSelector] = mockdriver.FailurePlan{Err: errors.New("adb unreachable")}
|
||||||
action := verifier.Action{Kind: verifier.ActionKindInputText, On: "id:username", Text: "alice"}
|
action := verifier.Action{Kind: verifier.ActionKindInputText, On: "id:username", Text: "alice"}
|
||||||
|
|
||||||
_, err := applyAction(context.Background(), driverMock, action, nil)
|
_, err := applyAction(context.Background(), driverMock, action, nil)
|
||||||
@@ -989,7 +989,7 @@ func TestApplyAction_InputTextSurfacesFocusTapError(t *testing.T) {
|
|||||||
})
|
})
|
||||||
t.Run("coordinate focus tap fails", func(t *testing.T) {
|
t.Run("coordinate focus tap fails", func(t *testing.T) {
|
||||||
driverMock := mockdriver.New()
|
driverMock := mockdriver.New()
|
||||||
driverMock.Failures[mockdriver.ActionTap] = errors.New("tap driver error")
|
driverMock.Failures[mockdriver.ActionTap] = mockdriver.FailurePlan{Err: errors.New("tap driver error")}
|
||||||
action := verifier.Action{Kind: verifier.ActionKindInputText, X: 10, Y: 20, Text: "alice"}
|
action := verifier.Action{Kind: verifier.ActionKindInputText, X: 10, Y: 20, Text: "alice"}
|
||||||
|
|
||||||
_, err := applyAction(context.Background(), driverMock, action, nil)
|
_, err := applyAction(context.Background(), driverMock, action, nil)
|
||||||
@@ -1639,7 +1639,7 @@ func TestRunner_ASourceAskedAndHandedNothingSaysSo(t *testing.T) {
|
|||||||
// here has an action to offer, and no step of this run gets to hear it.
|
// here has an action to offer, and no step of this run gets to hear it.
|
||||||
func TestRunner_AHeldStepIsNotRecordedAsASourceThatDeclined(t *testing.T) {
|
func TestRunner_AHeldStepIsNotRecordedAsASourceThatDeclined(t *testing.T) {
|
||||||
state := newHarness(t)
|
state := newHarness(t)
|
||||||
state.mock.Failures[mockdriver.ActionSnapshot] = errors.New("adb: device offline")
|
state.mock.Failures[mockdriver.ActionSnapshot] = mockdriver.FailurePlan{Err: errors.New("adb: device offline")}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ func TestRunner_WebInstallsTheStepsLogsInThePage(t *testing.T) {
|
|||||||
// be dropped in silence, under a comment claiming it was warned about.
|
// be dropped in silence, under a comment claiming it was warned about.
|
||||||
func TestRunner_ReportsALogFetchItCouldNotMake(t *testing.T) {
|
func TestRunner_ReportsALogFetchItCouldNotMake(t *testing.T) {
|
||||||
state := newHarnessWithSpec(t, lastActionSpec)
|
state := newHarnessWithSpec(t, lastActionSpec)
|
||||||
state.mock.Failures[mockdriver.ActionRecentLogs] = errors.New("adb: device offline")
|
state.mock.Failures[mockdriver.ActionRecentLogs] = mockdriver.FailurePlan{Err: errors.New("adb: device offline")}
|
||||||
|
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
logger := slog.New(slog.NewTextHandler(&buffer, &slog.HandlerOptions{Level: slog.LevelWarn}))
|
logger := slog.New(slog.NewTextHandler(&buffer, &slog.HandlerOptions{Level: slog.LevelWarn}))
|
||||||
|
|||||||
Reference in new issue
Block a user