@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,401 @@
|
|
|
1
|
+
"""Convert a Maestro test-output session or Flow folder into the device failure record.
|
|
2
|
+
|
|
3
|
+
Maestro's ``--test-output-dir`` session holds one self-contained folder per Flow
|
|
4
|
+
(`test reports and artifacts <https://docs.maestro.dev/maestro-flows/workspace-management/test-reports-and-artifacts>`_):
|
|
5
|
+
|
|
6
|
+
<output>/2026-07-31_173617/ # session folder
|
|
7
|
+
├── maestro.log
|
|
8
|
+
└── checkout_flow/ # one folder per Flow
|
|
9
|
+
├── manifest.json
|
|
10
|
+
├── commands.json # one entry per executed step
|
|
11
|
+
├── screenshots/ # step-<NNN>-<type>.png, failing step only
|
|
12
|
+
├── screen-hierarchy/ # step-<NNN>-<type>.json
|
|
13
|
+
└── logs/device-logcat.txt
|
|
14
|
+
|
|
15
|
+
``--maestro-dir`` takes the session folder or one Flow folder. The converter picks the
|
|
16
|
+
first failed step, reuses ``failure_capture`` to write ``failure.json`` and prints the one
|
|
17
|
+
``NATIVE_FAILURE {...}`` line, so the record shape cannot drift from the other layers:
|
|
18
|
+
|
|
19
|
+
python3 maestro_failure.py --maestro-dir "$LOG_DIR/2026-07-31_173617" --case checkout
|
|
20
|
+
|
|
21
|
+
A passing Flow writes nothing. An existing ``failure.json`` is never replaced, so the first
|
|
22
|
+
failure keeps its evidence. ``screenshot``, ``uiDump`` and ``logcatTail`` are paths relative
|
|
23
|
+
to ``evidenceDir``: the Flow folder itself, unless ``--evidence-dir`` names another run
|
|
24
|
+
folder, in which case the artifacts are copied there. A missing artifact is ``null`` and the
|
|
25
|
+
reason goes to ``cleanupErrors``; it never replaces ``error``.
|
|
26
|
+
|
|
27
|
+
Field mapping, with what could not be confirmed from the docs
|
|
28
|
+
-------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
Confirmed from Maestro's source (``TestOutputWriter.saveCommands`` and
|
|
31
|
+
``CommandDebugMetadata``), because the docs name the fields but show no example:
|
|
32
|
+
|
|
33
|
+
- ``commands.json`` is an array of ``{"command": <command object>, "metadata": {...}}``.
|
|
34
|
+
- ``metadata.status`` is one of PENDING, RUNNING, COMPLETED, FAILED, WARNED, SKIPPED;
|
|
35
|
+
``metadata.timestamp`` is epoch milliseconds; ``sequenceNumber`` starts at 0.
|
|
36
|
+
- ``metadata.error`` is the exception serialized as ``{"message", "debugMessage"}``; keys
|
|
37
|
+
are omitted when empty.
|
|
38
|
+
- ``metadata.artifacts`` is a list of ``{"type": "SCREENSHOT"|"SCREEN_HIERARCHY"|...,
|
|
39
|
+
"path": "<flow-folder-relative>"}``.
|
|
40
|
+
|
|
41
|
+
Confirmed from the docs: ``screenshots/``, ``screen-hierarchy/``, ``logs/maestro.log``,
|
|
42
|
+
``logs/device-logcat.txt`` (Android), ``manifest.json``, and that only the failing step is
|
|
43
|
+
captured outside ``--analyze``.
|
|
44
|
+
|
|
45
|
+
Assumed, and not confirmed from the docs:
|
|
46
|
+
|
|
47
|
+
- The *step description*: ``commands.json`` stores the command object, not a rendered
|
|
48
|
+
description (Maestro's ``originalDescription`` is not serialized). ``step`` is the
|
|
49
|
+
command's own ``label`` when it has one, otherwise a mechanical reading of the command
|
|
50
|
+
type plus its first target (``text``/``textRegex``/``id``/``point``/``appId``/``link``/
|
|
51
|
+
``path``). It is deliberately not a re-implementation of Maestro's phrasing.
|
|
52
|
+
- Artifact file names ``step-<NNN>-<type>`` with ``NNN`` = ``sequenceNumber`` + 1. This was
|
|
53
|
+
observed in Maestro's own tests, not in the docs, so it is only the fallback when
|
|
54
|
+
``metadata.artifacts`` does not name the file.
|
|
55
|
+
- ``logs/device-simulator.log`` as the iOS fallback for the device log; the Android target
|
|
56
|
+
is ``logs/device-logcat.txt``.
|
|
57
|
+
- Which Flow failed first in a multi-Flow session: the earliest failing step's
|
|
58
|
+
``timestamp`` decides, with the folder name as the tie-break.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
from __future__ import annotations
|
|
62
|
+
|
|
63
|
+
import argparse
|
|
64
|
+
import json
|
|
65
|
+
import os
|
|
66
|
+
import re
|
|
67
|
+
import shutil
|
|
68
|
+
import sys
|
|
69
|
+
|
|
70
|
+
from failure_capture import Failure, read_record, write_record
|
|
71
|
+
|
|
72
|
+
COMMANDS_JSON = "commands.json"
|
|
73
|
+
SCREENSHOT_DIR = "screenshots"
|
|
74
|
+
HIERARCHY_DIR = "screen-hierarchy"
|
|
75
|
+
DEVICE_LOGS = ("logs/device-logcat.txt", "logs/device-simulator.log")
|
|
76
|
+
FAILED_STATUS = "FAILED"
|
|
77
|
+
NO_ERROR_MESSAGE = "Maestro marked the step FAILED and gave no error message"
|
|
78
|
+
TARGET_KEYS = ("text", "textRegex", "id", "point", "appId", "link", "path")
|
|
79
|
+
# The record attribute, the Maestro artifact kind, and the reason when it is absent.
|
|
80
|
+
RECORD_ARTIFACTS = (
|
|
81
|
+
("screenshot", "SCREENSHOT", "no failing-step screenshot in the Maestro output"),
|
|
82
|
+
("uiDump", "SCREEN_HIERARCHY", "no screen hierarchy in the Maestro output"),
|
|
83
|
+
("logcatTail", "LOGCAT", "no device log in the Maestro output"),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
_CAMEL_WORD = re.compile(r"(?<=[a-z0-9])([A-Z])")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class MaestroOutputError(RuntimeError):
|
|
90
|
+
"""The given folder holds no usable Maestro commands.json."""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _text(value):
|
|
94
|
+
if value is None:
|
|
95
|
+
return ""
|
|
96
|
+
return str(value).strip()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def load_commands(flow_dir):
|
|
100
|
+
"""The executed steps of `flow_dir`, or None when its commands.json is unusable."""
|
|
101
|
+
try:
|
|
102
|
+
with open(os.path.join(flow_dir, COMMANDS_JSON), "r", encoding="utf-8") as stream:
|
|
103
|
+
payload = json.load(stream)
|
|
104
|
+
except (OSError, ValueError):
|
|
105
|
+
return None
|
|
106
|
+
if not isinstance(payload, list):
|
|
107
|
+
return None
|
|
108
|
+
return [entry for entry in payload if isinstance(entry, dict)]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _pieces(entry):
|
|
112
|
+
"""(command, metadata) for one entry, in the current or the documented flat shape."""
|
|
113
|
+
metadata = entry.get("metadata")
|
|
114
|
+
if isinstance(metadata, dict):
|
|
115
|
+
return entry.get("command"), metadata
|
|
116
|
+
return entry.get("command"), entry
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _status(metadata):
|
|
120
|
+
return _text(metadata.get("status")).upper()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def failed_steps(entries):
|
|
124
|
+
"""(command, metadata) for each step Maestro marked FAILED, in execution order."""
|
|
125
|
+
steps = []
|
|
126
|
+
for entry in entries:
|
|
127
|
+
command, metadata = _pieces(entry)
|
|
128
|
+
if _status(metadata) == FAILED_STATUS:
|
|
129
|
+
steps.append((command, metadata))
|
|
130
|
+
return steps
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _timestamp(metadata):
|
|
134
|
+
"""The step's epoch-millisecond timestamp, or infinity when Maestro did not record one."""
|
|
135
|
+
value = metadata.get("timestamp")
|
|
136
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
137
|
+
return float("inf")
|
|
138
|
+
return float(value)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def flow_dirs(root, max_depth=3):
|
|
142
|
+
"""Folders holding a commands.json, `root` included, at most `max_depth` levels down."""
|
|
143
|
+
if os.path.isfile(os.path.join(root, COMMANDS_JSON)):
|
|
144
|
+
return [root]
|
|
145
|
+
found = []
|
|
146
|
+
base_depth = os.path.abspath(root).rstrip(os.sep).count(os.sep)
|
|
147
|
+
for current, directories, files in os.walk(root):
|
|
148
|
+
depth = os.path.abspath(current).rstrip(os.sep).count(os.sep) - base_depth
|
|
149
|
+
directories.sort()
|
|
150
|
+
if COMMANDS_JSON in files:
|
|
151
|
+
found.append(current)
|
|
152
|
+
# A Flow folder's own subfolders hold artifacts, never another Flow.
|
|
153
|
+
directories[:] = []
|
|
154
|
+
elif depth >= max_depth:
|
|
155
|
+
directories[:] = []
|
|
156
|
+
return found
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def first_failure(root):
|
|
160
|
+
"""(flow dir, command, metadata) for the earliest failing step under `root`, or None."""
|
|
161
|
+
directories = flow_dirs(root)
|
|
162
|
+
if not directories:
|
|
163
|
+
raise MaestroOutputError("no %s under %s" % (COMMANDS_JSON, root))
|
|
164
|
+
unreadable = []
|
|
165
|
+
failures = []
|
|
166
|
+
for directory in directories:
|
|
167
|
+
entries = load_commands(directory)
|
|
168
|
+
if entries is None:
|
|
169
|
+
unreadable.append(directory)
|
|
170
|
+
continue
|
|
171
|
+
steps = failed_steps(entries)
|
|
172
|
+
if steps:
|
|
173
|
+
command, metadata = steps[0]
|
|
174
|
+
failures.append((_timestamp(metadata), directory, command, metadata))
|
|
175
|
+
if not failures:
|
|
176
|
+
if unreadable:
|
|
177
|
+
raise MaestroOutputError(
|
|
178
|
+
"no readable %s in %s" % (COMMANDS_JSON, ", ".join(sorted(unreadable)))
|
|
179
|
+
)
|
|
180
|
+
return None
|
|
181
|
+
failures.sort(key=lambda failure: (failure[0], failure[1]))
|
|
182
|
+
_, directory, command, metadata = failures[0]
|
|
183
|
+
return directory, command, metadata
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def humanize(command_type):
|
|
187
|
+
"""`tapOnElement` -> `Tap on element`, `assertScreenshotCommand` -> `Assert screenshot`."""
|
|
188
|
+
name = command_type[: -len("Command")] if command_type.endswith("Command") else command_type
|
|
189
|
+
spaced = _CAMEL_WORD.sub(r" \1", name).lower()
|
|
190
|
+
return spaced[:1].upper() + spaced[1:]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _target(value, depth=5):
|
|
194
|
+
"""The first selector or target text inside a command, for a readable step."""
|
|
195
|
+
if depth <= 0:
|
|
196
|
+
return None
|
|
197
|
+
if isinstance(value, dict):
|
|
198
|
+
for key in TARGET_KEYS:
|
|
199
|
+
candidate = value.get(key)
|
|
200
|
+
if isinstance(candidate, bool):
|
|
201
|
+
continue
|
|
202
|
+
if isinstance(candidate, (str, int, float)) and _text(candidate):
|
|
203
|
+
return candidate
|
|
204
|
+
children = list(value.values())
|
|
205
|
+
elif isinstance(value, list):
|
|
206
|
+
children = value
|
|
207
|
+
else:
|
|
208
|
+
return None
|
|
209
|
+
for child in children:
|
|
210
|
+
found = _target(child, depth - 1)
|
|
211
|
+
if found is not None:
|
|
212
|
+
return found
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def describe_command(command):
|
|
217
|
+
"""The step recorded for the failing command, or None when Maestro stored none."""
|
|
218
|
+
if isinstance(command, str):
|
|
219
|
+
return _text(command) or None
|
|
220
|
+
if not isinstance(command, dict):
|
|
221
|
+
return None
|
|
222
|
+
for command_type, value in command.items():
|
|
223
|
+
if value is None:
|
|
224
|
+
continue
|
|
225
|
+
label = value.get("label") if isinstance(value, dict) else None
|
|
226
|
+
if _text(label):
|
|
227
|
+
return _text(label)
|
|
228
|
+
reading = humanize(command_type)
|
|
229
|
+
target = _target(value)
|
|
230
|
+
return reading if target is None else "%s %s" % (reading, json.dumps(target, ensure_ascii=False))
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def error_text(metadata):
|
|
235
|
+
"""The step's error message, keeping Maestro's debug detail when it adds anything."""
|
|
236
|
+
error = metadata.get("error")
|
|
237
|
+
if isinstance(error, dict):
|
|
238
|
+
message = _text(error.get("message"))
|
|
239
|
+
detail = _text(error.get("debugMessage"))
|
|
240
|
+
if message and detail and detail not in message:
|
|
241
|
+
return "%s: %s" % (message, detail)
|
|
242
|
+
return message or detail or NO_ERROR_MESSAGE
|
|
243
|
+
text = _text(error)
|
|
244
|
+
return text or NO_ERROR_MESSAGE
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _listed_artifacts(metadata):
|
|
248
|
+
"""{kind: relative path} from the step's artifacts list, in either shape."""
|
|
249
|
+
artifacts = metadata.get("artifacts")
|
|
250
|
+
if not isinstance(artifacts, list):
|
|
251
|
+
return {}
|
|
252
|
+
found = {}
|
|
253
|
+
for artifact in artifacts:
|
|
254
|
+
kind = ""
|
|
255
|
+
if isinstance(artifact, dict):
|
|
256
|
+
kind = _text(artifact.get("type")).upper()
|
|
257
|
+
path = artifact.get("path")
|
|
258
|
+
else:
|
|
259
|
+
path = artifact
|
|
260
|
+
if not isinstance(path, str) or not path:
|
|
261
|
+
continue
|
|
262
|
+
if not kind:
|
|
263
|
+
# The documented flat shape names the file without a kind.
|
|
264
|
+
lowered = path.lower()
|
|
265
|
+
if lowered.endswith(".png"):
|
|
266
|
+
kind = "SCREENSHOT"
|
|
267
|
+
elif lowered.endswith(".json") and "hierarch" in lowered:
|
|
268
|
+
kind = "SCREEN_HIERARCHY"
|
|
269
|
+
if kind:
|
|
270
|
+
found.setdefault(kind, path)
|
|
271
|
+
return found
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _step_named_artifacts(flow_dir, metadata):
|
|
275
|
+
"""Fallback: the files Maestro names after the step, step-<NNN>-<type>.(png|json)."""
|
|
276
|
+
number = metadata.get("sequenceNumber")
|
|
277
|
+
prefix = "step-%03d-" % (number + 1) if isinstance(number, int) and not isinstance(number, bool) else "step-"
|
|
278
|
+
found = {}
|
|
279
|
+
for kind, directory, suffix in (
|
|
280
|
+
("SCREENSHOT", SCREENSHOT_DIR, ".png"),
|
|
281
|
+
("SCREEN_HIERARCHY", HIERARCHY_DIR, ".json"),
|
|
282
|
+
):
|
|
283
|
+
try:
|
|
284
|
+
names = sorted(os.listdir(os.path.join(flow_dir, directory)))
|
|
285
|
+
except OSError:
|
|
286
|
+
continue
|
|
287
|
+
for name in names:
|
|
288
|
+
if name.startswith(prefix) and name.endswith(suffix):
|
|
289
|
+
found[kind] = "%s/%s" % (directory, name)
|
|
290
|
+
break
|
|
291
|
+
return found
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def step_artifacts(flow_dir, metadata):
|
|
295
|
+
"""{kind: relative path} for the failing step, as `commands.json` names it.
|
|
296
|
+
|
|
297
|
+
A path Maestro listed is kept even when the file is absent, so the record names the
|
|
298
|
+
artifact it expected instead of a generic gap. The step-named fallback only fills a
|
|
299
|
+
kind Maestro did not list, and only with a file that is actually there.
|
|
300
|
+
"""
|
|
301
|
+
found = {}
|
|
302
|
+
listed = _listed_artifacts(metadata)
|
|
303
|
+
named = _step_named_artifacts(flow_dir, metadata)
|
|
304
|
+
for kind in ("SCREENSHOT", "SCREEN_HIERARCHY"):
|
|
305
|
+
relative = listed.get(kind) or named.get(kind)
|
|
306
|
+
if relative:
|
|
307
|
+
found[kind] = relative
|
|
308
|
+
for relative in DEVICE_LOGS:
|
|
309
|
+
if os.path.isfile(os.path.join(flow_dir, relative)):
|
|
310
|
+
found["LOGCAT"] = relative
|
|
311
|
+
break
|
|
312
|
+
return found
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _store(flow_dir, evidence_dir, relative, failure, attribute):
|
|
316
|
+
"""Copy `relative` into `evidence_dir` when they differ; record the failure otherwise."""
|
|
317
|
+
source = os.path.join(flow_dir, relative)
|
|
318
|
+
if not os.path.isfile(source):
|
|
319
|
+
failure.cleanupErrors.append("%s: %s is not in the Maestro output" % (attribute, relative))
|
|
320
|
+
return None
|
|
321
|
+
if os.path.abspath(evidence_dir) == os.path.abspath(flow_dir):
|
|
322
|
+
return relative
|
|
323
|
+
destination = os.path.join(evidence_dir, relative)
|
|
324
|
+
try:
|
|
325
|
+
os.makedirs(os.path.dirname(destination) or evidence_dir, exist_ok=True)
|
|
326
|
+
shutil.copyfile(source, destination)
|
|
327
|
+
except OSError as problem:
|
|
328
|
+
failure.cleanupErrors.append("%s: copy of %s failed: %s" % (attribute, relative, problem))
|
|
329
|
+
return None
|
|
330
|
+
return relative
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def build_failure(flow_dir, command, metadata, case, evidence_dir):
|
|
334
|
+
"""The `Failure` for one failed Maestro step, with the evidence found in `flow_dir`."""
|
|
335
|
+
failure = Failure(
|
|
336
|
+
case=case or os.path.basename(os.path.abspath(flow_dir)),
|
|
337
|
+
step=describe_command(command),
|
|
338
|
+
error=error_text(metadata),
|
|
339
|
+
evidenceDir=evidence_dir,
|
|
340
|
+
)
|
|
341
|
+
found = step_artifacts(flow_dir, metadata)
|
|
342
|
+
for attribute, kind, reason in RECORD_ARTIFACTS:
|
|
343
|
+
relative = found.get(kind)
|
|
344
|
+
if relative is None:
|
|
345
|
+
failure.cleanupErrors.append("%s: %s" % (attribute, reason))
|
|
346
|
+
continue
|
|
347
|
+
setattr(failure, attribute, _store(flow_dir, evidence_dir, relative, failure, attribute))
|
|
348
|
+
return failure
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def convert(maestro_dir, case=None, evidence_dir=None, stream=None):
|
|
352
|
+
"""Write the failure record for the first failed Flow under `maestro_dir`.
|
|
353
|
+
|
|
354
|
+
Returns the record written, or the earlier record kept in `evidence_dir`, or None when
|
|
355
|
+
no Flow failed. Raises `MaestroOutputError` when no readable `commands.json` is there.
|
|
356
|
+
"""
|
|
357
|
+
stream = stream if stream is not None else sys.stdout
|
|
358
|
+
selected = first_failure(maestro_dir)
|
|
359
|
+
if selected is None:
|
|
360
|
+
return None
|
|
361
|
+
flow_dir, command, metadata = selected
|
|
362
|
+
evidence = os.path.abspath(evidence_dir or flow_dir)
|
|
363
|
+
kept = read_record(evidence)
|
|
364
|
+
if kept is not None:
|
|
365
|
+
# The first failure owns this evidence folder; a later conversion only repeats it.
|
|
366
|
+
print(kept.native_failure_line(), file=stream, flush=True)
|
|
367
|
+
return kept
|
|
368
|
+
failure = build_failure(flow_dir, command, metadata, case, evidence)
|
|
369
|
+
failure = write_record(failure, evidence)
|
|
370
|
+
print(failure.native_failure_line(), file=stream, flush=True)
|
|
371
|
+
return failure
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def main(argv=None):
|
|
375
|
+
parser = argparse.ArgumentParser(
|
|
376
|
+
description="Convert a Maestro test-output session or Flow folder into the device failure record."
|
|
377
|
+
)
|
|
378
|
+
parser.add_argument(
|
|
379
|
+
"--maestro-dir",
|
|
380
|
+
required=True,
|
|
381
|
+
help="Maestro --test-output-dir session folder, or one Flow folder holding commands.json",
|
|
382
|
+
)
|
|
383
|
+
parser.add_argument("--case", help="case name for the record; defaults to the Flow folder name")
|
|
384
|
+
parser.add_argument(
|
|
385
|
+
"--evidence-dir",
|
|
386
|
+
help="run evidence folder that holds failure.json; defaults to the Flow folder",
|
|
387
|
+
)
|
|
388
|
+
options = parser.parse_args(argv)
|
|
389
|
+
if not os.path.isdir(options.maestro_dir):
|
|
390
|
+
print("maestro folder not found: %s" % options.maestro_dir, file=sys.stderr)
|
|
391
|
+
return 2
|
|
392
|
+
try:
|
|
393
|
+
convert(options.maestro_dir, case=options.case, evidence_dir=options.evidence_dir)
|
|
394
|
+
except MaestroOutputError as problem:
|
|
395
|
+
print("not converted: %s" % problem, file=sys.stderr)
|
|
396
|
+
return 2
|
|
397
|
+
return 0
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
if __name__ == "__main__":
|
|
401
|
+
sys.exit(main())
|