@1aboveio/skills 1.0.15 → 1.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/resources.json +156 -152
- package/package.json +4 -4
- package/skills/app-dev/airflow/SKILL.md +1 -1
- package/skills/app-dev/android-development/SKILL.md +30 -1
- package/skills/app-dev/android-development/references/diagnosis.md +50 -0
- package/skills/app-dev/android-development/references/feature-work.md +3 -2
- package/skills/app-dev/android-development/references/gotchas.md +13 -0
- package/skills/app-dev/android-development/references/init.md +31 -2
- package/skills/app-dev/android-development/references/setup.md +16 -7
- package/skills/app-dev/android-development/references/ui-journeys.md +130 -0
- package/skills/app-dev/android-development/scripts/device-run.sh +93 -2
- package/skills/app-dev/android-development/scripts/failure_capture.py +331 -0
- package/skills/app-dev/android-development/scripts/init.mjs +129 -9
- package/skills/app-dev/android-development/scripts/maestro_failure.py +401 -0
- package/skills/app-dev/android-development/scripts/test_failure_capture.py +378 -0
- package/skills/app-dev/android-development/scripts/test_maestro_failure.py +462 -0
- package/skills/app-dev/app-debug/SKILL.md +1 -1
- package/skills/app-dev/better-auth/SKILL.md +1 -1
- package/skills/app-dev/data-catalog/SKILL.md +1 -1
- package/skills/app-dev/data-quality/SKILL.md +1 -1
- package/skills/app-dev/ios-foundation/SKILL.md +1 -1
- package/skills/app-dev/monorepo/SKILL.md +1 -1
- package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
- package/skills/app-dev/prisma-setup/SKILL.md +1 -1
- package/skills/app-dev/pyspark/SKILL.md +1 -1
- package/skills/app-dev/python-backend/SKILL.md +1 -1
- package/skills/app-dev/shadcn/SKILL.md +1 -1
- package/skills/app-dev/zod-v4/SKILL.md +1 -1
- package/skills/cicd/cloud-build/SKILL.md +1 -1
- package/skills/cicd/cloud-debug/SKILL.md +1 -1
- package/skills/cicd/cloud-deploy/SKILL.md +1 -1
- package/skills/cicd/docker/SKILL.md +1 -1
- package/skills/cicd/github-actions/SKILL.md +1 -1
- package/skills/cicd/github-flow/SKILL.md +1 -1
- package/skills/cicd/google-cloud/SKILL.md +1 -1
- package/skills/cicd/mergify/SKILL.md +1 -1
- package/skills/cicd/podman/SKILL.md +1 -1
- package/skills/content/article-review/SKILL.md +1 -1
- package/skills/content/article-to-image/SKILL.md +1 -1
- package/skills/content/flowchart-svg/SKILL.md +1 -1
- package/skills/content/image-gen/SKILL.md +1 -1
- package/skills/content/mermaid/SKILL.md +1 -1
- package/skills/content/wechat-article-writer/SKILL.md +1 -1
- package/skills/content/wechat-publisher/SKILL.md +1 -1
- package/skills/content/wechat-search/SKILL.md +1 -1
- package/skills/content/xai-search/SKILL.md +1 -1
- package/skills/core/distill-writing/SKILL.md +1 -1
- package/skills/core/doc-to-md/SKILL.md +1 -1
- package/skills/core/editorial-review/SKILL.md +1 -1
- package/skills/core/house-style/SKILL.md +1 -1
- package/skills/core/plain-language/SKILL.md +1 -1
- package/skills/core/polish-cn/SKILL.md +1 -1
- package/skills/core/reg-parser/SKILL.md +1 -1
- package/skills/core/terminology/SKILL.md +1 -1
- package/skills/core/write-doc/SKILL.md +1 -1
- package/skills/core/write-report/SKILL.md +1 -1
- package/skills/engineering/better-review/SKILL.md +1 -1
- package/skills/engineering/e2e-test/SKILL.md +1 -1
- package/skills/engineering/engineering/SKILL.md +1 -1
- package/skills/engineering/engineering-runtime/SKILL.md +1 -1
- package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
- package/skills/engineering/ensure-coverage/SKILL.md +1 -1
- package/skills/engineering/golden-path/SKILL.md +1 -1
- package/skills/engineering/harness-runtime/SKILL.md +4 -3
- package/skills/engineering/harness-runtime/agent-catalog.mjs +929 -0
- package/skills/engineering/harness-runtime/contracts.md +2 -0
- package/skills/engineering/harness-runtime/references/model-catalog.md +7 -1
- package/skills/engineering/resolve-issues/SKILL.md +18 -13
- package/skills/engineering/resolve-issues/references/delivery-progress.md +2 -0
- package/skills/engineering/resolve-issues/references/expire.md +6 -3
- package/skills/engineering/resolve-issues/references/exploration.md +60 -18
- package/skills/engineering/resolve-issues/references/implementation.md +7 -2
- package/skills/engineering/resolve-issues/references/preflight.md +65 -31
- package/skills/engineering/resolve-issues/references/review.md +22 -9
- package/skills/engineering/resolve-issues/references/shared-modules.md +142 -20
- package/skills/engineering/resolve-issues/references/task-plan.md +62 -8
- package/skills/engineering/resolve-issues/scripts/findings-reuse.mjs +1 -1
- package/skills/engineering/resolve-issues/scripts/next-actions.mjs +7 -7
- package/skills/engineering/resolve-issues/scripts/plan-sync.mjs +1 -1
- package/skills/engineering/resolve-issues/scripts/preflight-models.mjs +382 -592
- package/skills/engineering/resolve-issues/scripts/size-check.mjs +6 -5
- package/skills/engineering/resolve-release/SKILL.md +1 -1
- package/skills/engineering/rush-issues/SKILL.md +4 -4
- package/skills/engineering/rush-release/SKILL.md +1 -1
- package/skills/engineering/test-quality/SKILL.md +1 -1
- package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
- package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
- package/skills/risk-compliance/contract-review/SKILL.md +1 -1
- package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
- package/skills/risk-compliance/fraud-detection/SKILL.md +1 -1
- package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/portfolio-payment-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
- package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
- package/skills/engineering/resolve-issues/scripts/discover-models.mjs +0 -9
- package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +0 -9
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""Focused tests for failure_capture.py: no device, no adb, no emulator.
|
|
2
|
+
|
|
3
|
+
Run directly (`python3 test_failure_capture.py`) or through
|
|
4
|
+
`scripts/failure-capture.test.mjs`, which is what CI discovers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import contextlib
|
|
8
|
+
import io
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import unittest
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
16
|
+
|
|
17
|
+
from failure_capture import ( # noqa: E402
|
|
18
|
+
LOGCAT_NAME,
|
|
19
|
+
NATIVE_FAILURE_PREFIX,
|
|
20
|
+
RECORD_KEYS,
|
|
21
|
+
RECORD_NAME,
|
|
22
|
+
SCREENSHOT_NAME,
|
|
23
|
+
UI_DUMP_NAME,
|
|
24
|
+
AdbClient,
|
|
25
|
+
CaptureError,
|
|
26
|
+
FailureRecorder,
|
|
27
|
+
read_record,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FakeDevice:
|
|
32
|
+
"""Stands in for AdbClient: records calls and replays configured outcomes."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, fail=()):
|
|
35
|
+
self.fail = set(fail)
|
|
36
|
+
self.calls = []
|
|
37
|
+
|
|
38
|
+
def _capture(self, kind, destination, payload):
|
|
39
|
+
self.calls.append((kind, destination))
|
|
40
|
+
if kind in self.fail:
|
|
41
|
+
raise CaptureError("%s failed" % kind)
|
|
42
|
+
if payload is None:
|
|
43
|
+
return
|
|
44
|
+
with open(destination, "wb") as stream:
|
|
45
|
+
stream.write(payload)
|
|
46
|
+
|
|
47
|
+
def screencap(self, destination, timeout):
|
|
48
|
+
self.timeout = timeout
|
|
49
|
+
self._capture("screenshot", destination, b"\x89PNG\r\n")
|
|
50
|
+
|
|
51
|
+
def ui_dump(self, destination, timeout):
|
|
52
|
+
self.timeout = timeout
|
|
53
|
+
self._capture("uiDump", destination, b'<?xml version="1.0"?><hierarchy rotation="0"/>')
|
|
54
|
+
|
|
55
|
+
def logcat_tail(self, destination, lines, timeout):
|
|
56
|
+
self.timeout = timeout
|
|
57
|
+
self.lines = lines
|
|
58
|
+
self._capture("logcatTail", destination, b"E ActivityManager: crash\n")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RecorderTest(unittest.TestCase):
|
|
62
|
+
def setUp(self):
|
|
63
|
+
self.directory = tempfile.TemporaryDirectory()
|
|
64
|
+
self.after = self.directory.name
|
|
65
|
+
self.evidence = os.path.join(self.directory.name, "logs")
|
|
66
|
+
self.device = FakeDevice()
|
|
67
|
+
self.output = io.StringIO()
|
|
68
|
+
|
|
69
|
+
def tearDown(self):
|
|
70
|
+
self.directory.cleanup()
|
|
71
|
+
|
|
72
|
+
def recorder(self, **options):
|
|
73
|
+
options.setdefault("client", self.device)
|
|
74
|
+
return FailureRecorder(
|
|
75
|
+
"checkout smoke", self.evidence, stream=self.output, **options
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def record(self):
|
|
79
|
+
with open(os.path.join(self.evidence, RECORD_NAME), encoding="utf-8") as stream:
|
|
80
|
+
return json.load(stream)
|
|
81
|
+
|
|
82
|
+
def native_lines(self):
|
|
83
|
+
return [line for line in self.output.getvalue().splitlines() if line]
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def bytes(path):
|
|
87
|
+
with open(path, "rb") as stream:
|
|
88
|
+
return stream.read()
|
|
89
|
+
|
|
90
|
+
def test_a_passing_step_writes_nothing(self):
|
|
91
|
+
recorder = self.recorder()
|
|
92
|
+
with recorder.step("tap Pay"):
|
|
93
|
+
pass
|
|
94
|
+
self.assertFalse(os.path.exists(os.path.join(self.evidence, RECORD_NAME)))
|
|
95
|
+
self.assertEqual(self.device.calls, [])
|
|
96
|
+
self.assertEqual(self.output.getvalue(), "")
|
|
97
|
+
self.assertIsNone(recorder.failure)
|
|
98
|
+
|
|
99
|
+
def test_a_failing_step_records_evidence_and_re_raises(self):
|
|
100
|
+
recorder = self.recorder()
|
|
101
|
+
with self.assertRaisesRegex(ValueError, "terminal 500"):
|
|
102
|
+
with recorder.step("wait for receipt"):
|
|
103
|
+
raise ValueError("terminal 500")
|
|
104
|
+
|
|
105
|
+
record = self.record()
|
|
106
|
+
self.assertEqual(list(record), list(RECORD_KEYS))
|
|
107
|
+
self.assertEqual(record["case"], "checkout smoke")
|
|
108
|
+
self.assertEqual(record["step"], "wait for receipt")
|
|
109
|
+
self.assertEqual(record["error"], "ValueError: terminal 500")
|
|
110
|
+
self.assertEqual(record["screenshot"], SCREENSHOT_NAME)
|
|
111
|
+
self.assertEqual(record["uiDump"], UI_DUMP_NAME)
|
|
112
|
+
self.assertEqual(record["logcatTail"], LOGCAT_NAME)
|
|
113
|
+
self.assertEqual(record["evidenceDir"], self.evidence)
|
|
114
|
+
self.assertEqual(record["cleanupErrors"], [])
|
|
115
|
+
for name in (SCREENSHOT_NAME, UI_DUMP_NAME, LOGCAT_NAME):
|
|
116
|
+
self.assertTrue(os.path.isfile(os.path.join(self.evidence, name)), name)
|
|
117
|
+
|
|
118
|
+
line = self.native_lines()
|
|
119
|
+
self.assertEqual(len(line), 1)
|
|
120
|
+
self.assertTrue(line[0].startswith(NATIVE_FAILURE_PREFIX))
|
|
121
|
+
self.assertNotIn("\n", line[0])
|
|
122
|
+
compact = json.loads(line[0][len(NATIVE_FAILURE_PREFIX):])
|
|
123
|
+
self.assertEqual(list(compact), ["case", "step", "error", "evidenceDir"])
|
|
124
|
+
self.assertEqual(compact["error"], "ValueError: terminal 500")
|
|
125
|
+
self.assertEqual(compact["evidenceDir"], self.evidence)
|
|
126
|
+
|
|
127
|
+
def test_a_capture_failure_keeps_the_original_error(self):
|
|
128
|
+
recorder = self.recorder(client=FakeDevice(fail={"screenshot", "uiDump", "logcatTail"}))
|
|
129
|
+
with self.assertRaisesRegex(RuntimeError, "app crashed"):
|
|
130
|
+
with recorder.step("launch"):
|
|
131
|
+
raise RuntimeError("app crashed")
|
|
132
|
+
|
|
133
|
+
record = self.record()
|
|
134
|
+
self.assertEqual(record["error"], "RuntimeError: app crashed")
|
|
135
|
+
self.assertIsNone(record["screenshot"])
|
|
136
|
+
self.assertIsNone(record["uiDump"])
|
|
137
|
+
self.assertIsNone(record["logcatTail"])
|
|
138
|
+
self.assertEqual(len(record["cleanupErrors"]), 3)
|
|
139
|
+
self.assertTrue(record["cleanupErrors"][0].startswith("screenshot: "))
|
|
140
|
+
self.assertEqual(len(self.native_lines()), 1)
|
|
141
|
+
|
|
142
|
+
def test_only_the_innermost_step_is_recorded(self):
|
|
143
|
+
recorder = self.recorder()
|
|
144
|
+
with self.assertRaises(AssertionError):
|
|
145
|
+
with recorder.step("outer"):
|
|
146
|
+
with recorder.step("inner"):
|
|
147
|
+
raise AssertionError("selector mismatch")
|
|
148
|
+
self.assertEqual(self.record()["step"], "inner")
|
|
149
|
+
self.assertEqual(len(self.native_lines()), 1)
|
|
150
|
+
self.assertEqual(len(self.device.calls), 3)
|
|
151
|
+
|
|
152
|
+
def test_an_existing_record_is_never_overwritten(self):
|
|
153
|
+
os.makedirs(self.evidence)
|
|
154
|
+
kept = {
|
|
155
|
+
"case": "earlier case",
|
|
156
|
+
"step": "earlier step",
|
|
157
|
+
"error": "KeyError: 'Terminal'",
|
|
158
|
+
"screenshot": None,
|
|
159
|
+
"uiDump": None,
|
|
160
|
+
"logcatTail": None,
|
|
161
|
+
"evidenceDir": self.evidence,
|
|
162
|
+
"cleanupErrors": ["screenshot: adb vanished"],
|
|
163
|
+
}
|
|
164
|
+
path = os.path.join(self.evidence, RECORD_NAME)
|
|
165
|
+
with open(path, "w", encoding="utf-8") as stream:
|
|
166
|
+
json.dump(kept, stream)
|
|
167
|
+
before = self.bytes(path)
|
|
168
|
+
|
|
169
|
+
recorder = self.recorder()
|
|
170
|
+
with self.assertRaises(ValueError):
|
|
171
|
+
with recorder.step("later step"):
|
|
172
|
+
raise ValueError("later failure")
|
|
173
|
+
|
|
174
|
+
self.assertEqual(self.bytes(path), before)
|
|
175
|
+
self.assertEqual(self.device.calls, [])
|
|
176
|
+
parsed = json.loads(self.native_lines()[0][len(NATIVE_FAILURE_PREFIX):])
|
|
177
|
+
self.assertEqual(parsed["step"], "earlier step")
|
|
178
|
+
self.assertEqual(parsed["error"], "KeyError: 'Terminal'")
|
|
179
|
+
|
|
180
|
+
def test_the_second_failure_keeps_the_first_record(self):
|
|
181
|
+
recorder = self.recorder()
|
|
182
|
+
with self.assertRaises(ValueError):
|
|
183
|
+
with recorder.step("first"):
|
|
184
|
+
raise ValueError("first")
|
|
185
|
+
with self.assertRaises(ValueError):
|
|
186
|
+
with recorder.step("second"):
|
|
187
|
+
raise ValueError("second")
|
|
188
|
+
self.assertEqual(self.record()["step"], "first")
|
|
189
|
+
self.assertEqual(len(self.native_lines()), 1)
|
|
190
|
+
self.assertEqual(len(self.device.calls), 3)
|
|
191
|
+
|
|
192
|
+
def test_a_decorated_step_records_the_failure(self):
|
|
193
|
+
recorder = self.recorder()
|
|
194
|
+
|
|
195
|
+
@recorder.step("restart app")
|
|
196
|
+
def restart():
|
|
197
|
+
raise TimeoutError("package did not start")
|
|
198
|
+
|
|
199
|
+
with self.assertRaises(TimeoutError):
|
|
200
|
+
restart()
|
|
201
|
+
self.assertEqual(self.record()["step"], "restart app")
|
|
202
|
+
self.assertEqual(self.record()["error"], "TimeoutError: package did not start")
|
|
203
|
+
|
|
204
|
+
def test_a_cancelled_run_is_not_a_failed_case(self):
|
|
205
|
+
recorder = self.recorder()
|
|
206
|
+
with self.assertRaises(KeyboardInterrupt):
|
|
207
|
+
with recorder.step("long wait"):
|
|
208
|
+
raise KeyboardInterrupt()
|
|
209
|
+
self.assertIsNone(recorder.failure)
|
|
210
|
+
self.assertFalse(os.path.exists(os.path.join(self.evidence, RECORD_NAME)))
|
|
211
|
+
self.assertEqual(self.output.getvalue(), "")
|
|
212
|
+
|
|
213
|
+
def test_capture_settings_are_passed_to_the_device(self):
|
|
214
|
+
recorder = self.recorder(capture_timeout=2.5, logcat_lines=25)
|
|
215
|
+
with self.assertRaises(ValueError):
|
|
216
|
+
with recorder.step("tap"):
|
|
217
|
+
raise ValueError("tap")
|
|
218
|
+
self.assertEqual(self.device.timeout, 2.5)
|
|
219
|
+
self.assertEqual(self.device.lines, 25)
|
|
220
|
+
|
|
221
|
+
def test_an_unwritable_evidence_directory_is_reported_without_masking(self):
|
|
222
|
+
blocker = os.path.join(self.after, "blocked")
|
|
223
|
+
with open(blocker, "w", encoding="utf-8") as stream:
|
|
224
|
+
stream.write("not a directory")
|
|
225
|
+
recorder = FailureRecorder(
|
|
226
|
+
"case", os.path.join(blocker, "logs"), client=self.device, stream=self.output
|
|
227
|
+
)
|
|
228
|
+
with contextlib.redirect_stderr(io.StringIO()) as reported:
|
|
229
|
+
with self.assertRaisesRegex(ValueError, "boom"):
|
|
230
|
+
with recorder.step("step"):
|
|
231
|
+
raise ValueError("boom")
|
|
232
|
+
self.assertIn("failure record not written", reported.getvalue())
|
|
233
|
+
self.assertEqual(recorder.failure.error, "ValueError: boom")
|
|
234
|
+
self.assertTrue(recorder.failure.cleanupErrors)
|
|
235
|
+
self.assertEqual(len(self.native_lines()), 1)
|
|
236
|
+
|
|
237
|
+
def test_the_default_evidence_directory_follows_log_dir(self):
|
|
238
|
+
previous = os.environ.get("LOG_DIR")
|
|
239
|
+
os.environ["LOG_DIR"] = self.evidence
|
|
240
|
+
try:
|
|
241
|
+
recorder = FailureRecorder("case", client=self.device, stream=self.output)
|
|
242
|
+
self.assertEqual(recorder.evidence_dir, self.evidence)
|
|
243
|
+
finally:
|
|
244
|
+
if previous is None:
|
|
245
|
+
os.environ.pop("LOG_DIR")
|
|
246
|
+
else:
|
|
247
|
+
os.environ["LOG_DIR"] = previous
|
|
248
|
+
|
|
249
|
+
def test_case_defaults_to_the_device_case_variable(self):
|
|
250
|
+
previous = os.environ.get("DEVICE_CASE")
|
|
251
|
+
os.environ["DEVICE_CASE"] = "from-env"
|
|
252
|
+
try:
|
|
253
|
+
recorder = FailureRecorder(
|
|
254
|
+
None, self.evidence, client=self.device, stream=self.output
|
|
255
|
+
)
|
|
256
|
+
with self.assertRaises(ValueError):
|
|
257
|
+
with recorder.step("step"):
|
|
258
|
+
raise ValueError("boom")
|
|
259
|
+
self.assertEqual(self.record()["case"], "from-env")
|
|
260
|
+
finally:
|
|
261
|
+
if previous is None:
|
|
262
|
+
os.environ.pop("DEVICE_CASE")
|
|
263
|
+
else:
|
|
264
|
+
os.environ["DEVICE_CASE"] = previous
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class Result:
|
|
268
|
+
def __init__(self, returncode=0, stdout=b"", stderr=b""):
|
|
269
|
+
self.returncode = returncode
|
|
270
|
+
self.stdout = stdout
|
|
271
|
+
self.stderr = stderr
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class AdbClientTest(unittest.TestCase):
|
|
275
|
+
def setUp(self):
|
|
276
|
+
self.directory = tempfile.TemporaryDirectory()
|
|
277
|
+
self.after = self.directory.name
|
|
278
|
+
self.commands = []
|
|
279
|
+
|
|
280
|
+
def tearDown(self):
|
|
281
|
+
self.directory.cleanup()
|
|
282
|
+
|
|
283
|
+
def client(self, responses, serial="emulator-5580", **options):
|
|
284
|
+
def adb(command, timeout):
|
|
285
|
+
self.commands.append((command, timeout))
|
|
286
|
+
outcome = responses.pop(0)
|
|
287
|
+
if isinstance(outcome, BaseException):
|
|
288
|
+
raise outcome
|
|
289
|
+
return outcome
|
|
290
|
+
|
|
291
|
+
return AdbClient(serial=serial, adb=adb, **options)
|
|
292
|
+
|
|
293
|
+
def path(self, name):
|
|
294
|
+
return os.path.join(self.after, name)
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def bytes(path):
|
|
298
|
+
with open(path, "rb") as stream:
|
|
299
|
+
return stream.read()
|
|
300
|
+
|
|
301
|
+
def test_every_capture_names_the_serial_and_stays_bounded(self):
|
|
302
|
+
client = self.client(
|
|
303
|
+
[
|
|
304
|
+
Result(stdout=b"\x89PNG"),
|
|
305
|
+
Result(stdout=b"UI hiercharchy dumped"),
|
|
306
|
+
Result(stdout=b"<hierarchy/>"),
|
|
307
|
+
Result(stdout=b"line\n"),
|
|
308
|
+
]
|
|
309
|
+
)
|
|
310
|
+
client.screencap(self.path("s.png"), 3.0)
|
|
311
|
+
client.ui_dump(self.path("ui.xml"), 4.0)
|
|
312
|
+
client.logcat_tail(self.path("logcat.txt"), 120, 5.0)
|
|
313
|
+
|
|
314
|
+
self.assertEqual(
|
|
315
|
+
self.commands,
|
|
316
|
+
[
|
|
317
|
+
(["adb", "-s", "emulator-5580", "exec-out", "screencap", "-p"], 3.0),
|
|
318
|
+
(["adb", "-s", "emulator-5580", "shell", "uiautomator", "dump", "/sdcard/failure-capture.xml"], 4.0),
|
|
319
|
+
(["adb", "-s", "emulator-5580", "exec-out", "cat", "/sdcard/failure-capture.xml"], 4.0),
|
|
320
|
+
(["adb", "-s", "emulator-5580", "logcat", "-d", "-t", "120"], 5.0),
|
|
321
|
+
],
|
|
322
|
+
)
|
|
323
|
+
self.assertEqual(self.bytes(self.path("s.png")), b"\x89PNG")
|
|
324
|
+
self.assertEqual(self.bytes(self.path("logcat.txt")), b"line\n")
|
|
325
|
+
|
|
326
|
+
def test_without_a_serial_adb_uses_its_default_device(self):
|
|
327
|
+
client = self.client([Result(stdout=b"png")], serial=None)
|
|
328
|
+
client.screencap(self.path("s.png"), 1.0)
|
|
329
|
+
self.assertEqual(self.commands[0][0], ["adb", "exec-out", "screencap", "-p"])
|
|
330
|
+
|
|
331
|
+
def test_a_timeout_is_a_capture_error(self):
|
|
332
|
+
client = self.client([__import__("subprocess").TimeoutExpired("adb", 1.0)])
|
|
333
|
+
with self.assertRaisesRegex(CaptureError, "timed out after 1.0s"):
|
|
334
|
+
client.screencap(self.path("s.png"), 1.0)
|
|
335
|
+
self.assertFalse(os.path.exists(self.path("s.png")))
|
|
336
|
+
|
|
337
|
+
def test_a_non_zero_exit_is_a_capture_error_with_its_stderr(self):
|
|
338
|
+
client = self.client([Result(returncode=1, stderr=b"device offline")])
|
|
339
|
+
with self.assertRaisesRegex(CaptureError, "device offline"):
|
|
340
|
+
client.logcat_tail(self.path("logcat.txt"), 10, 1.0)
|
|
341
|
+
|
|
342
|
+
def test_empty_or_wrong_output_is_a_capture_error(self):
|
|
343
|
+
client = self.client([Result(stdout=b""), Result(stdout=b"UI hiercharchy dumped"), Result(stdout=b"junk")])
|
|
344
|
+
with self.assertRaisesRegex(CaptureError, "no image"):
|
|
345
|
+
client.screencap(self.path("s.png"), 1.0)
|
|
346
|
+
with self.assertRaisesRegex(CaptureError, "no hierarchy"):
|
|
347
|
+
client.ui_dump(self.path("ui.xml"), 1.0)
|
|
348
|
+
self.assertFalse(os.path.exists(self.path("s.png")))
|
|
349
|
+
self.assertFalse(os.path.exists(self.path("ui.xml")))
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class RecordTest(unittest.TestCase):
|
|
353
|
+
def setUp(self):
|
|
354
|
+
self.directory = tempfile.TemporaryDirectory()
|
|
355
|
+
self.evidence = self.directory.name
|
|
356
|
+
|
|
357
|
+
def tearDown(self):
|
|
358
|
+
self.directory.cleanup()
|
|
359
|
+
|
|
360
|
+
def test_an_unreadable_record_is_replaced(self):
|
|
361
|
+
path = os.path.join(self.evidence, RECORD_NAME)
|
|
362
|
+
with open(path, "w", encoding="utf-8") as stream:
|
|
363
|
+
stream.write("{not json")
|
|
364
|
+
|
|
365
|
+
recorder = FailureRecorder("case", self.evidence, client=FakeDevice(), stream=io.StringIO())
|
|
366
|
+
with self.assertRaises(ValueError):
|
|
367
|
+
with recorder.step("step"):
|
|
368
|
+
raise ValueError("boom")
|
|
369
|
+
|
|
370
|
+
record = read_record(self.evidence)
|
|
371
|
+
self.assertEqual(record.step, "step")
|
|
372
|
+
self.assertTrue(
|
|
373
|
+
any("replaced an unreadable record" in error for error in record.cleanupErrors)
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
if __name__ == "__main__":
|
|
378
|
+
unittest.main()
|