fix(runner): warn on malformed screen snapshot (#13)

* fix(runner): warn on malformed screen snapshot

screenFromSnapshot swallowed json.Unmarshal errors, so a non-string
screen value silently became "" in the step log and trace while the
verifier still saw the raw JSON. Return the error and warn at the
call site, matching the hierarchy warning pattern.

* docs: clarify --avd is optional for uatu test

The CLI accepts --avd as an empty-string default (cmd/uatu/main.go:49)
and only requires it when no device is connected and multiple AVDs
exist (cmd/uatu/android_env.go:63). Docs and examples that showed it
as required or always-passed were misleading.
This commit is contained in:
pj authored and GitHub committed 2026-04-18 17:14:31 +07:00
1 parent 16e55086d8
commit d0578dbaaa
6 files changed
+49 -10

No files matched your search

+12 -6
View File
@@ -79,8 +79,12 @@ func Run(ctx context.Context, options Options) (Summary, error) {
if err := options.Verifier.PushSnapshot(verifier.Snapshots(snapshot.Snapshots), tree); err != nil {
return summary, fmt.Errorf("step %d push: %w", stepIndex, err)
}
screen, screenErr := screenFromSnapshot(snapshot.Snapshots)
if screenErr != nil {
fmt.Printf("warning: step %d screen: %v\n", stepIndex, screenErr)
}
fmt.Printf("step %d: screen=%q hierarchy=%d nodes\n",
stepIndex, screenFromSnapshot(snapshot.Snapshots), treeSize)
stepIndex, screen, treeSize)
verdicts := options.Verifier.EvaluateProperties()
violations := violationNames(verdicts)
@@ -95,7 +99,7 @@ func Run(ctx context.Context, options Options) (Summary, error) {
step := trace.Step{
Index: stepIndex,
Timestamp: stepStart,
Screen: screenFromSnapshot(snapshot.Snapshots),
Screen: screen,
Snapshots: snapshot.Snapshots,
Action: traceAction,
Violations: violations,
@@ -175,14 +179,16 @@ func violationNames(verdicts map[string]ltl.Verdict) []string {
return names
}
func screenFromSnapshot(snapshots map[string]json.RawMessage) string {
func screenFromSnapshot(snapshots map[string]json.RawMessage) (string, error) {
raw, ok := snapshots["screen"]
if !ok {
return ""
return "", nil
}
var screen string
_ = json.Unmarshal(raw, &screen)
return screen
if err := json.Unmarshal(raw, &screen); err != nil {
return "", err
}
return screen, nil
}
func applyAction(ctx context.Context, drv driver.Driver, action verifier.Action, tree *hierarchy.Tree) error {
+32
View File
@@ -235,6 +235,38 @@ func TestRunner_RecordsScreenFieldFromSnapshot(t *testing.T) {
}
}
func TestScreenFromSnapshot(t *testing.T) {
t.Run("string value returns screen", func(t *testing.T) {
snapshots := map[string]json.RawMessage{"screen": json.RawMessage(`"home"`)}
screen, err := screenFromSnapshot(snapshots)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if screen != "home" {
t.Errorf("screen = %q, want %q", screen, "home")
}
})
t.Run("missing key returns empty with no error", func(t *testing.T) {
screen, err := screenFromSnapshot(map[string]json.RawMessage{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if screen != "" {
t.Errorf("screen = %q, want empty", screen)
}
})
t.Run("non-string value returns error", func(t *testing.T) {
snapshots := map[string]json.RawMessage{"screen": json.RawMessage(`{"nested":1}`)}
screen, err := screenFromSnapshot(snapshots)
if err == nil {
t.Fatalf("expected error for non-string screen, got nil")
}
if screen != "" {
t.Errorf("screen = %q, want empty on error", screen)
}
})
}
func TestApplyAction_InputTextSurfacesFocusTapError(t *testing.T) {
t.Run("selector focus tap fails", func(t *testing.T) {
driverMock := mockdriver.New()