@1aboveio/skills 1.0.16 → 1.0.18
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 +178 -150
- package/README.md +1 -1
- package/package.json +5 -4
- package/skills/app-dev/airflow/SKILL.md +1 -1
- package/skills/app-dev/android-development/SKILL.md +14 -10
- package/skills/app-dev/android-development/references/diagnosis.md +4 -6
- package/skills/app-dev/android-development/references/feature-work.md +3 -2
- package/skills/app-dev/android-development/references/gotchas.md +7 -0
- package/skills/app-dev/android-development/references/init.md +5 -2
- package/skills/app-dev/android-development/references/setup.md +9 -5
- package/skills/app-dev/android-development/references/ui-journeys.md +12 -55
- package/skills/app-dev/android-development/scripts/failure_capture.py +23 -84
- package/skills/app-dev/android-development/scripts/init.mjs +2 -1
- package/skills/app-dev/android-development/scripts/maestro_failure.py +43 -387
- package/skills/app-dev/android-development/scripts/test_maestro_failure.py +113 -424
- 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/maestro/LICENSE +3 -0
- package/skills/app-dev/maestro/SKILL.md +112 -0
- package/skills/app-dev/maestro/references/android-connection.md +54 -0
- package/skills/app-dev/maestro/references/authoring.md +113 -0
- package/skills/app-dev/maestro/references/diagnosis.md +50 -0
- package/skills/app-dev/maestro/references/evidence.md +74 -0
- package/skills/app-dev/maestro/references/journeys.md +100 -0
- package/skills/app-dev/maestro/references/migration.md +35 -0
- package/skills/app-dev/maestro/release-check.mjs +95 -0
- package/skills/app-dev/maestro/scripts/failure_record.py +110 -0
- package/skills/app-dev/maestro/scripts/maestro_failure.py +481 -0
- package/skills/app-dev/maestro/scripts/test_maestro_failure.py +710 -0
- 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 +1 -1
- package/skills/engineering/harness-runtime/agent-catalog.mjs +46 -12
- package/skills/engineering/harness-runtime/contracts.md +1 -1
- package/skills/engineering/resolve-issues/SKILL.md +11 -9
- package/skills/engineering/resolve-issues/references/exploration.md +60 -18
- package/skills/engineering/resolve-issues/references/implementation.md +1 -1
- package/skills/engineering/resolve-issues/references/preflight.md +47 -22
- package/skills/engineering/resolve-issues/references/review.md +2 -1
- package/skills/engineering/resolve-issues/references/shared-modules.md +58 -4
- package/skills/engineering/resolve-issues/references/task-plan.md +30 -10
- 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/size-check.mjs +6 -5
- package/skills/engineering/resolve-release/SKILL.md +1 -1
- package/skills/engineering/rush-issues/SKILL.md +1 -1
- 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
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
"""Focused tests for maestro_failure.py: fixture Maestro output, no device, no Maestro.
|
|
2
|
+
|
|
3
|
+
The fixtures are the artifact layout Maestro's docs describe for a Flow folder
|
|
4
|
+
(manifest.json, commands.json, screenshots/, screen-hierarchy/, logs/), with the
|
|
5
|
+
commands.json entry shape taken from Maestro's source (TestOutputWriter and
|
|
6
|
+
CommandDebugMetadata) because the docs name the fields but show no example.
|
|
7
|
+
|
|
8
|
+
Run directly (`python3 test_maestro_failure.py`) or through
|
|
9
|
+
`maestro-failure.test.mjs`, which is what CI discovers.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import contextlib
|
|
13
|
+
import io
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
import tempfile
|
|
20
|
+
import unittest
|
|
21
|
+
|
|
22
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
23
|
+
sys.path.insert(0, HERE)
|
|
24
|
+
|
|
25
|
+
from failure_record import NATIVE_FAILURE_PREFIX, RECORD_KEYS, RECORD_NAME, read_record # noqa: E402
|
|
26
|
+
from maestro_failure import ( # noqa: E402
|
|
27
|
+
COMMANDS_JSON,
|
|
28
|
+
MaestroOutputError,
|
|
29
|
+
convert,
|
|
30
|
+
describe_command,
|
|
31
|
+
error_text,
|
|
32
|
+
main,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
CONVERTER = os.path.join(HERE, "maestro_failure.py")
|
|
36
|
+
|
|
37
|
+
SESSION = "2026-07-31_173617"
|
|
38
|
+
FLOW = "checkout_flow"
|
|
39
|
+
CASE = "checkout smoke"
|
|
40
|
+
|
|
41
|
+
LAUNCHED = {
|
|
42
|
+
"command": {"launchAppCommand": {"appId": "com.example.app", "optional": False}},
|
|
43
|
+
"metadata": {
|
|
44
|
+
"status": "COMPLETED",
|
|
45
|
+
"timestamp": 1700000000000,
|
|
46
|
+
"duration": 1200,
|
|
47
|
+
"sequenceNumber": 0,
|
|
48
|
+
"depth": 0,
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
TAPPED = {
|
|
53
|
+
"command": {
|
|
54
|
+
"tapOnElement": {
|
|
55
|
+
"selector": {"text": "Pay", "optional": False},
|
|
56
|
+
"label": "Tap Pay",
|
|
57
|
+
"optional": False,
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"metadata": {
|
|
61
|
+
"status": "COMPLETED",
|
|
62
|
+
"timestamp": 1700000001300,
|
|
63
|
+
"duration": 300,
|
|
64
|
+
"sequenceNumber": 1,
|
|
65
|
+
"depth": 0,
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
FAILED_RECEIPT = {
|
|
70
|
+
"command": {
|
|
71
|
+
"assertConditionCommand": {
|
|
72
|
+
"condition": {"visible": {"text": "Thank you", "optional": False}},
|
|
73
|
+
"optional": False,
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"metadata": {
|
|
77
|
+
"status": "FAILED",
|
|
78
|
+
"timestamp": 1700000001700,
|
|
79
|
+
"duration": 30000,
|
|
80
|
+
"sequenceNumber": 2,
|
|
81
|
+
"depth": 0,
|
|
82
|
+
"error": {
|
|
83
|
+
"message": 'Assertion is false: "Thank you" is visible',
|
|
84
|
+
"debugMessage": "Screen hierarchy has no match",
|
|
85
|
+
},
|
|
86
|
+
"artifacts": [
|
|
87
|
+
{"type": "SCREEN_HIERARCHY", "path": "screen-hierarchy/step-003-assertCondition.json"},
|
|
88
|
+
{"type": "SCREENSHOT", "path": "screenshots/step-003-assertCondition.png"},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class MaestroOutputCase(unittest.TestCase):
|
|
95
|
+
def setUp(self):
|
|
96
|
+
self.directory = tempfile.TemporaryDirectory()
|
|
97
|
+
self.root = self.directory.name
|
|
98
|
+
self.output = io.StringIO()
|
|
99
|
+
|
|
100
|
+
def tearDown(self):
|
|
101
|
+
self.directory.cleanup()
|
|
102
|
+
|
|
103
|
+
def write(self, relative, payload):
|
|
104
|
+
path = os.path.join(self.root, relative)
|
|
105
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
106
|
+
if isinstance(payload, bytes):
|
|
107
|
+
with open(path, "wb") as stream:
|
|
108
|
+
stream.write(payload)
|
|
109
|
+
else:
|
|
110
|
+
with open(path, "w", encoding="utf-8") as stream:
|
|
111
|
+
stream.write(payload)
|
|
112
|
+
return path
|
|
113
|
+
|
|
114
|
+
def write_json(self, relative, payload):
|
|
115
|
+
return self.write(relative, json.dumps(payload, indent=2))
|
|
116
|
+
|
|
117
|
+
def flow(self, steps, name=FLOW, session=SESSION, artifacts=None, manifest=True):
|
|
118
|
+
"""A Flow folder with `steps` in commands.json and the optional artifact files."""
|
|
119
|
+
base = os.path.join(session, name) if session else name
|
|
120
|
+
self.write_json(os.path.join(base, "commands.json"), steps)
|
|
121
|
+
if manifest:
|
|
122
|
+
self.write_json(
|
|
123
|
+
os.path.join(base, "manifest.json"),
|
|
124
|
+
{
|
|
125
|
+
"$schema": "https://storage.googleapis.com/maestro-schemas/artifact-manifest/v1.schema.json",
|
|
126
|
+
"entries": [
|
|
127
|
+
{"kind": "COMMAND_METADATA", "format": "JSON", "relativePath": "commands.json"}
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
for relative, payload in (artifacts or {}).items():
|
|
132
|
+
self.write(os.path.join(base, relative), payload)
|
|
133
|
+
return os.path.join(self.root, base)
|
|
134
|
+
|
|
135
|
+
def failing_flow(self, name=FLOW, session=SESSION):
|
|
136
|
+
"""The worked example: a launch and tap that passed, then a failed assertion."""
|
|
137
|
+
return self.flow(
|
|
138
|
+
[LAUNCHED, TAPPED, FAILED_RECEIPT],
|
|
139
|
+
name=name,
|
|
140
|
+
session=session,
|
|
141
|
+
artifacts={
|
|
142
|
+
"screenshots/step-003-assertCondition.png": b"\x89PNG\r\n\x1a\n",
|
|
143
|
+
"screen-hierarchy/step-003-assertCondition.json": '{"attributes": {"text": "Pay now"}}',
|
|
144
|
+
"logs/device-logcat.txt": "07-31 E ActivityManager: no match\n",
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def run_convert(self, maestro_dir, **options):
|
|
149
|
+
options.setdefault("stream", self.output)
|
|
150
|
+
return convert(maestro_dir, **options)
|
|
151
|
+
|
|
152
|
+
def cli(self, *argv):
|
|
153
|
+
"""Run main() and keep its stderr out of the test output."""
|
|
154
|
+
with contextlib.redirect_stderr(io.StringIO()):
|
|
155
|
+
return main(list(argv))
|
|
156
|
+
|
|
157
|
+
def cli_result(self, *argv):
|
|
158
|
+
"""Run main() and return (status, stderr, stdout) so a partial conversion is visible."""
|
|
159
|
+
stderr = io.StringIO()
|
|
160
|
+
stdout = io.StringIO()
|
|
161
|
+
with contextlib.redirect_stderr(stderr), contextlib.redirect_stdout(stdout):
|
|
162
|
+
status = main(list(argv))
|
|
163
|
+
return status, stderr.getvalue(), stdout.getvalue()
|
|
164
|
+
|
|
165
|
+
def record(self, evidence_dir):
|
|
166
|
+
path = os.path.join(evidence_dir, RECORD_NAME)
|
|
167
|
+
self.assertTrue(os.path.isfile(path), path)
|
|
168
|
+
with open(path, encoding="utf-8") as stream:
|
|
169
|
+
return json.load(stream)
|
|
170
|
+
|
|
171
|
+
def lines(self):
|
|
172
|
+
return [line for line in self.output.getvalue().splitlines() if line]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class FailingFlowTest(MaestroOutputCase):
|
|
176
|
+
def test_the_failing_step_becomes_the_record_and_the_line(self):
|
|
177
|
+
flow_dir = self.failing_flow()
|
|
178
|
+
failure = self.run_convert(flow_dir)
|
|
179
|
+
|
|
180
|
+
self.assertIsNotNone(failure)
|
|
181
|
+
record = self.record(flow_dir)
|
|
182
|
+
self.assertEqual(list(record), list(RECORD_KEYS))
|
|
183
|
+
self.assertEqual(record["case"], FLOW)
|
|
184
|
+
self.assertEqual(record["step"], 'Assert condition "Thank you"')
|
|
185
|
+
self.assertEqual(
|
|
186
|
+
record["error"],
|
|
187
|
+
'Assertion is false: "Thank you" is visible: Screen hierarchy has no match',
|
|
188
|
+
)
|
|
189
|
+
self.assertEqual(record["screenshot"], "screenshots/step-003-assertCondition.png")
|
|
190
|
+
self.assertEqual(record["uiDump"], "screen-hierarchy/step-003-assertCondition.json")
|
|
191
|
+
self.assertEqual(record["logcatTail"], "logs/device-logcat.txt")
|
|
192
|
+
self.assertEqual(record["evidenceDir"], flow_dir)
|
|
193
|
+
self.assertEqual(record["cleanupErrors"], [])
|
|
194
|
+
for name in (record["screenshot"], record["uiDump"], record["logcatTail"]):
|
|
195
|
+
self.assertTrue(os.path.isfile(os.path.join(flow_dir, name)), name)
|
|
196
|
+
|
|
197
|
+
lines = self.lines()
|
|
198
|
+
self.assertEqual(len(lines), 1)
|
|
199
|
+
self.assertTrue(lines[0].startswith(NATIVE_FAILURE_PREFIX))
|
|
200
|
+
self.assertEqual(
|
|
201
|
+
json.loads(lines[0][len(NATIVE_FAILURE_PREFIX) :]),
|
|
202
|
+
{
|
|
203
|
+
"case": FLOW,
|
|
204
|
+
"step": 'Assert condition "Thank you"',
|
|
205
|
+
"error": record["error"],
|
|
206
|
+
"evidenceDir": flow_dir,
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def test_an_explicit_case_name_is_used(self):
|
|
211
|
+
flow_dir = self.failing_flow()
|
|
212
|
+
self.run_convert(flow_dir, case=CASE)
|
|
213
|
+
self.assertEqual(self.record(flow_dir)["case"], CASE)
|
|
214
|
+
|
|
215
|
+
def test_a_labelled_step_uses_the_label(self):
|
|
216
|
+
labelled = json.loads(json.dumps(FAILED_RECEIPT))
|
|
217
|
+
labelled["command"] = {
|
|
218
|
+
"tapOnElement": {
|
|
219
|
+
"selector": {"text": "Pay", "optional": False},
|
|
220
|
+
"label": "Tap Pay",
|
|
221
|
+
"optional": False,
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
flow_dir = self.flow([TAPPED, labelled])
|
|
225
|
+
self.run_convert(flow_dir)
|
|
226
|
+
self.assertEqual(self.record(flow_dir)["step"], "Tap Pay")
|
|
227
|
+
|
|
228
|
+
def test_the_failing_step_wins_over_a_later_one(self):
|
|
229
|
+
later = json.loads(json.dumps(FAILED_RECEIPT))
|
|
230
|
+
later["metadata"]["timestamp"] = 1700000009000
|
|
231
|
+
later["metadata"]["sequenceNumber"] = 3
|
|
232
|
+
later["metadata"]["error"] = {"message": "second failure"}
|
|
233
|
+
later["metadata"]["artifacts"] = [
|
|
234
|
+
{"type": "SCREENSHOT", "path": "screenshots/step-004-tapOnElement.png"}
|
|
235
|
+
]
|
|
236
|
+
flow_dir = self.flow(
|
|
237
|
+
[TAPPED, FAILED_RECEIPT, later],
|
|
238
|
+
artifacts={"screenshots/step-003-assertCondition.png": b"png"},
|
|
239
|
+
)
|
|
240
|
+
self.run_convert(flow_dir)
|
|
241
|
+
|
|
242
|
+
record = self.record(flow_dir)
|
|
243
|
+
self.assertEqual(record["step"], 'Assert condition "Thank you"')
|
|
244
|
+
self.assertIn("Assertion is false", record["error"])
|
|
245
|
+
self.assertEqual(record["screenshot"], "screenshots/step-003-assertCondition.png")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class PassingFlowTest(MaestroOutputCase):
|
|
249
|
+
def test_a_passing_flow_writes_nothing(self):
|
|
250
|
+
flow_dir = self.flow(
|
|
251
|
+
[LAUNCHED, TAPPED],
|
|
252
|
+
artifacts={"logs/device-logcat.txt": "07-31 I ActivityManager: clean\n"},
|
|
253
|
+
)
|
|
254
|
+
self.assertIsNone(self.run_convert(flow_dir))
|
|
255
|
+
self.assertFalse(os.path.exists(os.path.join(flow_dir, RECORD_NAME)))
|
|
256
|
+
self.assertEqual(self.output.getvalue(), "")
|
|
257
|
+
|
|
258
|
+
def test_a_flow_with_no_steps_writes_nothing(self):
|
|
259
|
+
flow_dir = self.flow([])
|
|
260
|
+
self.assertIsNone(self.run_convert(flow_dir))
|
|
261
|
+
self.assertEqual(self.output.getvalue(), "")
|
|
262
|
+
|
|
263
|
+
def test_a_folder_without_commands_json_is_not_converted(self):
|
|
264
|
+
self.write(os.path.join(SESSION, FLOW, "manifest.json"), "{}")
|
|
265
|
+
with self.assertRaises(MaestroOutputError):
|
|
266
|
+
self.run_convert(os.path.join(self.root, SESSION))
|
|
267
|
+
self.assertEqual(self.cli("--maestro-dir", os.path.join(self.root, SESSION)), 2)
|
|
268
|
+
|
|
269
|
+
def test_a_missing_folder_is_a_usage_error(self):
|
|
270
|
+
self.assertEqual(self.cli("--maestro-dir", os.path.join(self.root, "absent")), 2)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class ExistingRecordTest(MaestroOutputCase):
|
|
274
|
+
def test_an_existing_record_is_kept_and_its_line_is_printed(self):
|
|
275
|
+
flow_dir = self.failing_flow()
|
|
276
|
+
kept = {
|
|
277
|
+
"case": CASE,
|
|
278
|
+
"step": "tap Pay",
|
|
279
|
+
"error": "RuntimeError: earlier failure",
|
|
280
|
+
"screenshot": None,
|
|
281
|
+
"uiDump": None,
|
|
282
|
+
"logcatTail": None,
|
|
283
|
+
"evidenceDir": flow_dir,
|
|
284
|
+
"cleanupErrors": [],
|
|
285
|
+
}
|
|
286
|
+
path = self.write_json(os.path.join(SESSION, FLOW, RECORD_NAME), kept)
|
|
287
|
+
|
|
288
|
+
failure = self.run_convert(flow_dir)
|
|
289
|
+
|
|
290
|
+
self.assertEqual(failure.case, CASE)
|
|
291
|
+
self.assertEqual(self.record(flow_dir), kept)
|
|
292
|
+
with open(path, encoding="utf-8") as stream:
|
|
293
|
+
self.assertIn("earlier failure", stream.read())
|
|
294
|
+
self.assertEqual(self.lines(), [failure.native_failure_line()])
|
|
295
|
+
|
|
296
|
+
def test_a_record_already_in_an_override_evidence_dir_is_kept(self):
|
|
297
|
+
flow_dir = self.failing_flow()
|
|
298
|
+
evidence_dir = os.path.join(self.root, "run-evidence")
|
|
299
|
+
kept = {"case": CASE, "step": None, "error": "RuntimeError: earlier failure", "cleanupErrors": []}
|
|
300
|
+
self.write_json(os.path.join("run-evidence", RECORD_NAME), kept)
|
|
301
|
+
|
|
302
|
+
self.run_convert(flow_dir, evidence_dir=evidence_dir)
|
|
303
|
+
|
|
304
|
+
self.assertEqual(self.record(evidence_dir)["error"], "RuntimeError: earlier failure")
|
|
305
|
+
# The first failure owns the folder, so no Maestro artifact was copied into it.
|
|
306
|
+
self.assertFalse(os.path.exists(os.path.join(evidence_dir, "screenshots")))
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class MissingOptionalFilesTest(MaestroOutputCase):
|
|
310
|
+
def test_absent_artifacts_are_null_and_noted_without_losing_the_error(self):
|
|
311
|
+
flow_dir = self.flow([TAPPED, FAILED_RECEIPT], manifest=False)
|
|
312
|
+
self.run_convert(flow_dir)
|
|
313
|
+
|
|
314
|
+
record = self.record(flow_dir)
|
|
315
|
+
self.assertIsNone(record["screenshot"])
|
|
316
|
+
self.assertIsNone(record["uiDump"])
|
|
317
|
+
self.assertIsNone(record["logcatTail"])
|
|
318
|
+
self.assertEqual(record["step"], 'Assert condition "Thank you"')
|
|
319
|
+
self.assertIn("Assertion is false", record["error"])
|
|
320
|
+
self.assertEqual(len(record["cleanupErrors"]), 3)
|
|
321
|
+
self.assertTrue(record["cleanupErrors"][0].startswith("screenshot: "))
|
|
322
|
+
self.assertTrue(record["cleanupErrors"][1].startswith("uiDump: "))
|
|
323
|
+
self.assertTrue(record["cleanupErrors"][2].startswith("logcatTail: "))
|
|
324
|
+
|
|
325
|
+
def test_the_error_survives_a_listed_artifact_that_is_not_on_disk(self):
|
|
326
|
+
broken = json.loads(json.dumps(FAILED_RECEIPT))
|
|
327
|
+
broken["metadata"]["artifacts"] = [
|
|
328
|
+
{"type": "SCREENSHOT", "path": "screenshots/step-003-assertCondition.png"}
|
|
329
|
+
]
|
|
330
|
+
flow_dir = self.flow([broken], artifacts={"logs/device-logcat.txt": "log\n"})
|
|
331
|
+
self.run_convert(flow_dir)
|
|
332
|
+
|
|
333
|
+
record = self.record(flow_dir)
|
|
334
|
+
self.assertIsNone(record["screenshot"])
|
|
335
|
+
self.assertEqual(record["logcatTail"], "logs/device-logcat.txt")
|
|
336
|
+
self.assertIn(
|
|
337
|
+
"screenshots/step-003-assertCondition.png is not in the Maestro output",
|
|
338
|
+
" ".join(record["cleanupErrors"]),
|
|
339
|
+
)
|
|
340
|
+
self.assertIn("Assertion is false", record["error"])
|
|
341
|
+
|
|
342
|
+
def test_an_unreadable_commands_json_is_not_silently_a_pass(self):
|
|
343
|
+
flow_dir = self.flow([FAILED_RECEIPT])
|
|
344
|
+
self.write(os.path.join(SESSION, FLOW, "commands.json"), "{not json")
|
|
345
|
+
with self.assertRaises(MaestroOutputError):
|
|
346
|
+
self.run_convert(flow_dir)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class UnsafeArtifactPathTest(MaestroOutputCase):
|
|
350
|
+
"""A path Maestro names is read and copied only inside the two folders involved."""
|
|
351
|
+
|
|
352
|
+
def refused(self, record, reason="outside the Maestro output"):
|
|
353
|
+
self.assertIsNone(record["screenshot"])
|
|
354
|
+
self.assertIn("Assertion is false", record["error"])
|
|
355
|
+
self.assertTrue(
|
|
356
|
+
any(
|
|
357
|
+
note.startswith("screenshot: ") and reason in note
|
|
358
|
+
for note in record["cleanupErrors"]
|
|
359
|
+
),
|
|
360
|
+
record["cleanupErrors"],
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def failing_flow_listing(self, artifacts):
|
|
364
|
+
"""The worked example, with the failing step naming `artifacts` instead."""
|
|
365
|
+
listed = json.loads(json.dumps(FAILED_RECEIPT))
|
|
366
|
+
listed["metadata"]["artifacts"] = artifacts
|
|
367
|
+
return self.flow([listed], artifacts={"logs/device-logcat.txt": "log\n"})
|
|
368
|
+
|
|
369
|
+
def test_an_absolute_artifact_path_is_not_read(self):
|
|
370
|
+
outside = self.write("outside.png", b"secret")
|
|
371
|
+
flow_dir = self.failing_flow_listing([{"type": "SCREENSHOT", "path": outside}])
|
|
372
|
+
|
|
373
|
+
self.run_convert(flow_dir)
|
|
374
|
+
|
|
375
|
+
record = self.record(flow_dir)
|
|
376
|
+
self.refused(record)
|
|
377
|
+
self.assertEqual(record["logcatTail"], "logs/device-logcat.txt")
|
|
378
|
+
|
|
379
|
+
def test_a_traversal_artifact_path_does_not_escape_the_evidence_folder(self):
|
|
380
|
+
self.write(os.path.join(SESSION, "outside.png"), b"secret")
|
|
381
|
+
flow_dir = self.failing_flow_listing(
|
|
382
|
+
[{"type": "SCREENSHOT", "path": "../outside.png"}]
|
|
383
|
+
)
|
|
384
|
+
evidence_dir = os.path.join(self.root, "run-evidence")
|
|
385
|
+
|
|
386
|
+
self.run_convert(flow_dir, evidence_dir=evidence_dir)
|
|
387
|
+
|
|
388
|
+
self.refused(self.record(evidence_dir))
|
|
389
|
+
self.assertFalse(os.path.exists(os.path.join(self.root, "outside.png")))
|
|
390
|
+
|
|
391
|
+
def test_an_artifact_behind_an_outside_symlink_is_refused(self):
|
|
392
|
+
outside = os.path.join(self.root, "outside")
|
|
393
|
+
os.makedirs(outside)
|
|
394
|
+
with open(os.path.join(outside, "step-003-assertCondition.png"), "wb") as stream:
|
|
395
|
+
stream.write(b"secret")
|
|
396
|
+
flow_dir = self.failing_flow_listing(
|
|
397
|
+
[{"type": "SCREENSHOT", "path": "screenshots/step-003-assertCondition.png"}]
|
|
398
|
+
)
|
|
399
|
+
os.symlink(outside, os.path.join(flow_dir, "screenshots"))
|
|
400
|
+
evidence_dir = os.path.join(self.root, "run-evidence")
|
|
401
|
+
|
|
402
|
+
self.run_convert(flow_dir, evidence_dir=evidence_dir)
|
|
403
|
+
|
|
404
|
+
self.refused(self.record(evidence_dir))
|
|
405
|
+
self.assertFalse(os.path.exists(os.path.join(evidence_dir, "screenshots")))
|
|
406
|
+
|
|
407
|
+
def test_an_evidence_folder_symlink_outside_is_refused(self):
|
|
408
|
+
outside = os.path.join(self.root, "outside")
|
|
409
|
+
os.makedirs(outside)
|
|
410
|
+
evidence_dir = os.path.join(self.root, "run-evidence")
|
|
411
|
+
os.makedirs(evidence_dir)
|
|
412
|
+
os.symlink(outside, os.path.join(evidence_dir, "screenshots"))
|
|
413
|
+
flow_dir = self.failing_flow()
|
|
414
|
+
|
|
415
|
+
self.run_convert(flow_dir, evidence_dir=evidence_dir)
|
|
416
|
+
|
|
417
|
+
record = self.record(evidence_dir)
|
|
418
|
+
self.refused(record, reason="would leave the evidence folder")
|
|
419
|
+
self.assertEqual(os.listdir(outside), [])
|
|
420
|
+
self.assertEqual(record["uiDump"], "screen-hierarchy/step-003-assertCondition.json")
|
|
421
|
+
self.assertEqual(record["logcatTail"], "logs/device-logcat.txt")
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class CommandsValidationTest(MaestroOutputCase):
|
|
425
|
+
"""Entries the converter cannot read are an error, never a silent pass."""
|
|
426
|
+
|
|
427
|
+
def test_a_list_holding_a_non_object_entry_is_not_a_pass(self):
|
|
428
|
+
flow_dir = self.flow([42])
|
|
429
|
+
|
|
430
|
+
with self.assertRaisesRegex(MaestroOutputError, "entry 0 is not an object"):
|
|
431
|
+
self.run_convert(flow_dir)
|
|
432
|
+
|
|
433
|
+
self.assertEqual(self.cli("--maestro-dir", flow_dir), 2)
|
|
434
|
+
self.assertFalse(os.path.exists(os.path.join(flow_dir, RECORD_NAME)))
|
|
435
|
+
|
|
436
|
+
def test_an_unknown_status_is_not_a_pass(self):
|
|
437
|
+
unknown = json.loads(json.dumps(FAILED_RECEIPT))
|
|
438
|
+
unknown["metadata"]["status"] = "ERROR"
|
|
439
|
+
flow_dir = self.flow([LAUNCHED, unknown])
|
|
440
|
+
|
|
441
|
+
with self.assertRaisesRegex(MaestroOutputError, "unsupported status 'ERROR'"):
|
|
442
|
+
self.run_convert(flow_dir)
|
|
443
|
+
|
|
444
|
+
self.assertEqual(self.cli("--maestro-dir", flow_dir), 2)
|
|
445
|
+
self.assertFalse(os.path.exists(os.path.join(flow_dir, RECORD_NAME)))
|
|
446
|
+
|
|
447
|
+
def test_an_empty_command_list_is_still_a_pass(self):
|
|
448
|
+
flow_dir = self.flow([])
|
|
449
|
+
|
|
450
|
+
self.assertIsNone(self.run_convert(flow_dir))
|
|
451
|
+
self.assertEqual(self.cli("--maestro-dir", flow_dir), 0)
|
|
452
|
+
self.assertFalse(os.path.exists(os.path.join(flow_dir, RECORD_NAME)))
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class IncompleteConversionTest(MaestroOutputCase):
|
|
456
|
+
"""A Flow that cannot be read makes the conversion partial, not a pass."""
|
|
457
|
+
|
|
458
|
+
def unreadable(self, name):
|
|
459
|
+
self.flow([LAUNCHED], name=name)
|
|
460
|
+
return self.write(os.path.join(SESSION, name, COMMANDS_JSON), "{not json")
|
|
461
|
+
|
|
462
|
+
def test_a_failing_flow_is_recorded_and_the_unreadable_one_is_reported(self):
|
|
463
|
+
failing_flow = self.failing_flow()
|
|
464
|
+
self.unreadable("login_flow")
|
|
465
|
+
session_dir = os.path.join(self.root, SESSION)
|
|
466
|
+
|
|
467
|
+
status, stderr, stdout = self.cli_result("--maestro-dir", session_dir)
|
|
468
|
+
|
|
469
|
+
self.assertEqual(status, 2)
|
|
470
|
+
self.assertIn("incomplete conversion", stderr)
|
|
471
|
+
self.assertIn("login_flow", stderr)
|
|
472
|
+
record = self.record(failing_flow)
|
|
473
|
+
self.assertIn("Assertion is false", record["error"])
|
|
474
|
+
self.assertTrue(
|
|
475
|
+
any("unreadable commands.json" in note for note in record["cleanupErrors"]),
|
|
476
|
+
record["cleanupErrors"],
|
|
477
|
+
)
|
|
478
|
+
lines = [line for line in stdout.splitlines() if line]
|
|
479
|
+
self.assertEqual(len(lines), 1)
|
|
480
|
+
self.assertTrue(lines[0].startswith(NATIVE_FAILURE_PREFIX))
|
|
481
|
+
|
|
482
|
+
def test_every_unreadable_flow_is_reported(self):
|
|
483
|
+
failing_flow = self.failing_flow()
|
|
484
|
+
self.unreadable("login_flow")
|
|
485
|
+
self.unreadable("cart_flow")
|
|
486
|
+
|
|
487
|
+
status, stderr, _ = self.cli_result("--maestro-dir", os.path.join(self.root, SESSION))
|
|
488
|
+
|
|
489
|
+
self.assertEqual(status, 2)
|
|
490
|
+
self.assertIn("login_flow", stderr)
|
|
491
|
+
self.assertIn("cart_flow", stderr)
|
|
492
|
+
record = self.record(failing_flow)
|
|
493
|
+
self.assertIn("Assertion is false", record["error"])
|
|
494
|
+
unreadable = [note for note in record["cleanupErrors"] if "unreadable" in note]
|
|
495
|
+
self.assertEqual(len(unreadable), 2, record["cleanupErrors"])
|
|
496
|
+
|
|
497
|
+
def test_a_session_of_only_unreadable_flows_writes_nothing(self):
|
|
498
|
+
login = self.unreadable("login_flow")
|
|
499
|
+
cart = self.unreadable("cart_flow")
|
|
500
|
+
session_dir = os.path.join(self.root, SESSION)
|
|
501
|
+
|
|
502
|
+
with self.assertRaises(MaestroOutputError):
|
|
503
|
+
self.run_convert(session_dir)
|
|
504
|
+
|
|
505
|
+
self.assertEqual(self.cli("--maestro-dir", session_dir), 2)
|
|
506
|
+
self.assertEqual(self.output.getvalue(), "")
|
|
507
|
+
for path in (login, cart):
|
|
508
|
+
self.assertFalse(os.path.exists(os.path.join(os.path.dirname(path), RECORD_NAME)))
|
|
509
|
+
|
|
510
|
+
def test_a_malformed_flow_does_not_discard_a_readable_failure(self):
|
|
511
|
+
failing_flow = self.failing_flow()
|
|
512
|
+
self.flow([42], name="login_flow")
|
|
513
|
+
session_dir = os.path.join(self.root, SESSION)
|
|
514
|
+
|
|
515
|
+
status, stderr, stdout = self.cli_result("--maestro-dir", session_dir)
|
|
516
|
+
|
|
517
|
+
self.assertEqual(status, 2)
|
|
518
|
+
self.assertIn("entry 0 is not an object", stderr)
|
|
519
|
+
self.assertIn("incomplete conversion", stderr)
|
|
520
|
+
record = self.record(failing_flow)
|
|
521
|
+
self.assertIn("Assertion is false", record["error"])
|
|
522
|
+
self.assertTrue(any("entry 0 is not an object" in note for note in record["cleanupErrors"]))
|
|
523
|
+
self.assertEqual(len(stdout.splitlines()), 1)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
class SessionFolderTest(MaestroOutputCase):
|
|
527
|
+
def test_the_session_folder_finds_the_failing_flow(self):
|
|
528
|
+
self.flow([LAUNCHED, TAPPED], name="login_flow")
|
|
529
|
+
failing_flow = self.failing_flow()
|
|
530
|
+
session_dir = os.path.join(self.root, SESSION)
|
|
531
|
+
|
|
532
|
+
failure = self.run_convert(session_dir)
|
|
533
|
+
|
|
534
|
+
self.assertEqual(failure.evidenceDir, failing_flow)
|
|
535
|
+
self.assertEqual(self.record(failing_flow)["case"], FLOW)
|
|
536
|
+
|
|
537
|
+
def test_the_earliest_failing_step_decides(self):
|
|
538
|
+
earlier = json.loads(json.dumps(FAILED_RECEIPT))
|
|
539
|
+
earlier["metadata"]["timestamp"] = 1700000000500
|
|
540
|
+
earlier["metadata"]["error"] = {"message": "earlier failure"}
|
|
541
|
+
earlier_dir = self.flow([LAUNCHED, earlier], name="login_flow")
|
|
542
|
+
later_dir = self.failing_flow(name="checkout_flow")
|
|
543
|
+
|
|
544
|
+
self.run_convert(os.path.join(self.root, SESSION))
|
|
545
|
+
|
|
546
|
+
self.assertEqual(self.record(earlier_dir)["error"], "earlier failure")
|
|
547
|
+
self.assertFalse(os.path.exists(os.path.join(later_dir, RECORD_NAME)))
|
|
548
|
+
|
|
549
|
+
def test_the_output_root_above_the_session_folder_works(self):
|
|
550
|
+
failing_flow = self.failing_flow()
|
|
551
|
+
self.run_convert(self.root)
|
|
552
|
+
self.assertTrue(os.path.isfile(os.path.join(failing_flow, RECORD_NAME)))
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
class EvidenceDirTest(MaestroOutputCase):
|
|
556
|
+
def test_an_override_evidence_dir_holds_copies_and_relative_names(self):
|
|
557
|
+
flow_dir = self.failing_flow()
|
|
558
|
+
evidence_dir = os.path.join(self.root, "run-evidence")
|
|
559
|
+
|
|
560
|
+
failure = self.run_convert(flow_dir, evidence_dir=evidence_dir)
|
|
561
|
+
|
|
562
|
+
record = self.record(evidence_dir)
|
|
563
|
+
self.assertEqual(record["evidenceDir"], evidence_dir)
|
|
564
|
+
self.assertEqual(failure.evidenceDir, evidence_dir)
|
|
565
|
+
self.assertEqual(record["screenshot"], "screenshots/step-003-assertCondition.png")
|
|
566
|
+
self.assertEqual(record["uiDump"], "screen-hierarchy/step-003-assertCondition.json")
|
|
567
|
+
self.assertEqual(record["logcatTail"], "logs/device-logcat.txt")
|
|
568
|
+
for name in (record["screenshot"], record["uiDump"], record["logcatTail"]):
|
|
569
|
+
self.assertTrue(os.path.isfile(os.path.join(evidence_dir, name)), name)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
class DocumentedShapeTest(MaestroOutputCase):
|
|
573
|
+
def test_the_documented_flat_entry_shape_is_accepted(self):
|
|
574
|
+
flow_dir = self.flow(
|
|
575
|
+
[
|
|
576
|
+
{"command": "launchApp", "status": "COMPLETED", "duration": 1200, "sequenceNumber": 0},
|
|
577
|
+
{
|
|
578
|
+
"command": "tapOn: Pay",
|
|
579
|
+
"status": "FAILED",
|
|
580
|
+
"duration": 300,
|
|
581
|
+
"sequenceNumber": 1,
|
|
582
|
+
"error": "Element not found: Pay",
|
|
583
|
+
"artifacts": ["screenshots/step-002-tapOn.png"],
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
artifacts={
|
|
587
|
+
"screenshots/step-002-tapOn.png": b"png",
|
|
588
|
+
"logs/device-logcat.txt": "log\n",
|
|
589
|
+
},
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
self.run_convert(flow_dir)
|
|
593
|
+
|
|
594
|
+
record = self.record(flow_dir)
|
|
595
|
+
self.assertEqual(record["step"], "tapOn: Pay")
|
|
596
|
+
self.assertEqual(record["error"], "Element not found: Pay")
|
|
597
|
+
self.assertEqual(record["screenshot"], "screenshots/step-002-tapOn.png")
|
|
598
|
+
self.assertEqual(record["uiDump"], None)
|
|
599
|
+
|
|
600
|
+
def test_a_step_named_screenshot_is_found_without_an_artifacts_list(self):
|
|
601
|
+
unlisted = json.loads(json.dumps(FAILED_RECEIPT))
|
|
602
|
+
del unlisted["metadata"]["artifacts"]
|
|
603
|
+
unlisted["metadata"]["sequenceNumber"] = 1
|
|
604
|
+
flow_dir = self.flow(
|
|
605
|
+
[unlisted],
|
|
606
|
+
artifacts={
|
|
607
|
+
"screenshots/step-002-assertCondition.png": b"png",
|
|
608
|
+
"screen-hierarchy/step-002-assertCondition.json": "{}",
|
|
609
|
+
"logs/device-logcat.txt": "log\n",
|
|
610
|
+
},
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
self.run_convert(flow_dir)
|
|
614
|
+
|
|
615
|
+
record = self.record(flow_dir)
|
|
616
|
+
self.assertEqual(record["screenshot"], "screenshots/step-002-assertCondition.png")
|
|
617
|
+
self.assertEqual(record["uiDump"], "screen-hierarchy/step-002-assertCondition.json")
|
|
618
|
+
|
|
619
|
+
def test_an_ios_device_log_is_used_when_there_is_no_logcat(self):
|
|
620
|
+
flow_dir = self.flow(
|
|
621
|
+
[FAILED_RECEIPT],
|
|
622
|
+
artifacts={"logs/device-simulator.log": "simulator log\n"},
|
|
623
|
+
)
|
|
624
|
+
self.run_convert(flow_dir)
|
|
625
|
+
self.assertEqual(self.record(flow_dir)["logcatTail"], "logs/device-simulator.log")
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
class ReadingTest(unittest.TestCase):
|
|
629
|
+
def test_a_command_without_a_label_reads_as_its_type_and_target(self):
|
|
630
|
+
self.assertEqual(describe_command({"scrollCommand": {"optional": False}}), "Scroll")
|
|
631
|
+
self.assertEqual(
|
|
632
|
+
describe_command({"launchAppCommand": {"appId": "com.example.app"}}),
|
|
633
|
+
'Launch app "com.example.app"',
|
|
634
|
+
)
|
|
635
|
+
self.assertEqual(describe_command("tapOn: Pay"), "tapOn: Pay")
|
|
636
|
+
self.assertIsNone(describe_command(None))
|
|
637
|
+
|
|
638
|
+
def test_error_text_keeps_the_debug_detail(self):
|
|
639
|
+
self.assertEqual(error_text({"error": "boom"}), "boom")
|
|
640
|
+
self.assertEqual(
|
|
641
|
+
error_text({"error": {"message": "boom", "debugMessage": "screen has no Foo"}}),
|
|
642
|
+
"boom: screen has no Foo",
|
|
643
|
+
)
|
|
644
|
+
self.assertEqual(error_text({"error": {"message": "boom"}}), "boom")
|
|
645
|
+
self.assertEqual(error_text({}), "Maestro marked the step FAILED and gave no error message")
|
|
646
|
+
|
|
647
|
+
def test_read_record_ignores_a_non_record(self):
|
|
648
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
649
|
+
self.assertIsNone(read_record(directory))
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def run_python(arguments, cwd):
|
|
653
|
+
"""Run a Python command with no inherited PYTHONPATH, so imports come from `arguments`."""
|
|
654
|
+
environment = {key: value for key, value in os.environ.items() if key != "PYTHONPATH"}
|
|
655
|
+
return subprocess.run(
|
|
656
|
+
[sys.executable, *arguments],
|
|
657
|
+
cwd=cwd,
|
|
658
|
+
env=environment,
|
|
659
|
+
capture_output=True,
|
|
660
|
+
text=True,
|
|
661
|
+
timeout=120,
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
class CommandLineTest(MaestroOutputCase):
|
|
666
|
+
def test_the_command_line_converts_from_another_working_directory(self):
|
|
667
|
+
flow_dir = self.failing_flow()
|
|
668
|
+
elsewhere = os.path.join(self.root, "elsewhere")
|
|
669
|
+
os.makedirs(elsewhere)
|
|
670
|
+
|
|
671
|
+
result = run_python([CONVERTER, "--maestro-dir", flow_dir, "--case", CASE], cwd=elsewhere)
|
|
672
|
+
|
|
673
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
674
|
+
lines = [line for line in result.stdout.splitlines() if line]
|
|
675
|
+
self.assertEqual(len(lines), 1)
|
|
676
|
+
self.assertTrue(lines[0].startswith(NATIVE_FAILURE_PREFIX))
|
|
677
|
+
record = self.record(flow_dir)
|
|
678
|
+
self.assertEqual(record["case"], CASE)
|
|
679
|
+
self.assertEqual(record["step"], 'Assert condition "Thank you"')
|
|
680
|
+
self.assertEqual(json.loads(lines[0][len(NATIVE_FAILURE_PREFIX) :])["case"], CASE)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
class StagedLayoutTest(MaestroOutputCase):
|
|
684
|
+
"""A copied ``skills/maestro/scripts`` folder, with no Android skill beside it."""
|
|
685
|
+
|
|
686
|
+
def stage_converter(self, root):
|
|
687
|
+
scripts = os.path.join(root, "skills", "maestro", "scripts")
|
|
688
|
+
os.makedirs(scripts)
|
|
689
|
+
for name in sorted(os.listdir(HERE)):
|
|
690
|
+
if name.endswith(".py"):
|
|
691
|
+
shutil.copyfile(os.path.join(HERE, name), os.path.join(scripts, name))
|
|
692
|
+
return os.path.join(scripts, "maestro_failure.py")
|
|
693
|
+
|
|
694
|
+
def test_the_converter_runs_from_a_staged_skill_folder(self):
|
|
695
|
+
flow_dir = self.failing_flow()
|
|
696
|
+
with tempfile.TemporaryDirectory() as staged:
|
|
697
|
+
converter = self.stage_converter(staged)
|
|
698
|
+
elsewhere = os.path.join(staged, "elsewhere")
|
|
699
|
+
os.makedirs(elsewhere)
|
|
700
|
+
result = run_python([converter, "--maestro-dir", flow_dir], cwd=elsewhere)
|
|
701
|
+
|
|
702
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
703
|
+
record = self.record(flow_dir)
|
|
704
|
+
self.assertEqual(record["case"], FLOW)
|
|
705
|
+
self.assertEqual(record["screenshot"], "screenshots/step-003-assertCondition.png")
|
|
706
|
+
self.assertEqual(len([line for line in result.stdout.splitlines() if line]), 1)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
if __name__ == "__main__":
|
|
710
|
+
unittest.main()
|