@@ -174,7 +174,186 @@ rm -rf "$EXPORT_DIR"
174174mkdir -p " $EXPORT_DIR "
175175
176176ri_log " Exporting screenshot attachments from $RESULT_BUNDLE "
177- if ! xcrun xcresulttool export --legacy --path " $RESULT_BUNDLE " --type file --output-path " $EXPORT_DIR " > /dev/null; then
177+ if ! python3 - " $RESULT_BUNDLE " " $EXPORT_DIR " << 'PY '
178+ import json
179+ import os
180+ import re
181+ import subprocess
182+ import sys
183+ from collections import deque
184+
185+
186+ def ri_log(message: str) -> None:
187+ print(f"[run-ios-ui-tests] {message}", file=sys.stderr)
188+
189+
190+ def run_xcresult(args):
191+ cmd = ["xcrun", "xcresulttool", *args]
192+ try:
193+ result = subprocess.run(
194+ cmd,
195+ check=True,
196+ stdout=subprocess.PIPE,
197+ stderr=subprocess.PIPE,
198+ )
199+ except subprocess.CalledProcessError as exc:
200+ ri_log(
201+ "xcresulttool command failed: {}\n{}".format(
202+ " ".join(cmd), exc.stderr.decode("utf-8", "ignore").strip()
203+ )
204+ )
205+ raise
206+ return result.stdout.decode("utf-8")
207+
208+
209+ def get_json(bundle_path, object_id=None):
210+ args = ["get", "--path", bundle_path, "--format", "json"]
211+ if object_id:
212+ args.extend(["--id", object_id])
213+ output = run_xcresult(args)
214+ return json.loads(output or "{}")
215+
216+
217+ def collect_nodes(node):
218+ attachments = []
219+ refs = []
220+ stack = [node]
221+ while stack:
222+ current = stack.pop()
223+ if isinstance(current, dict):
224+ attachment_block = current.get("attachments")
225+ if isinstance(attachment_block, dict):
226+ for item in attachment_block.get("_values", []):
227+ if isinstance(item, dict):
228+ attachments.append(item)
229+ for key, value in current.items():
230+ if key.endswith("Ref") and isinstance(value, dict):
231+ ref_id = value.get("id")
232+ if isinstance(ref_id, str) and ref_id:
233+ refs.append(ref_id)
234+ elif key.endswith("Refs") and isinstance(value, dict):
235+ for entry in value.get("_values", []):
236+ if isinstance(entry, dict):
237+ ref_id = entry.get("id")
238+ if isinstance(ref_id, str) and ref_id:
239+ refs.append(ref_id)
240+ if isinstance(value, (dict, list)):
241+ stack.append(value)
242+ elif isinstance(current, list):
243+ stack.extend(current)
244+ return attachments, refs
245+
246+
247+ def is_image_attachment(attachment):
248+ uti = (attachment.get("uniformTypeIdentifier") or "").lower()
249+ filename = (attachment.get("filename") or attachment.get("name") or "").lower()
250+ if filename.endswith((".png", ".jpg", ".jpeg")):
251+ return True
252+ if "png" in uti or "jpeg" in uti:
253+ return True
254+ return False
255+
256+
257+ def sanitize_filename(name):
258+ safe = re.sub(r"[^A-Za-z0-9_.-]", "_", name)
259+ return safe or "attachment"
260+
261+
262+ def ensure_extension(name, uti):
263+ base, ext = os.path.splitext(name)
264+ if ext:
265+ return name
266+ uti = uti.lower()
267+ if "png" in uti:
268+ return f"{name}.png"
269+ if "jpeg" in uti:
270+ return f"{name}.jpg"
271+ return name
272+
273+
274+ def export_attachment(bundle_path, attachment, destination_dir, used_names):
275+ payload = attachment.get("payloadRef") or {}
276+ attachment_id = payload.get("id")
277+ if not attachment_id:
278+ return
279+ name = attachment.get("filename") or attachment.get("name") or attachment_id
280+ name = ensure_extension(name, attachment.get("uniformTypeIdentifier") or "")
281+ name = sanitize_filename(name)
282+ candidate = name
283+ counter = 1
284+ while candidate in used_names:
285+ base, ext = os.path.splitext(name)
286+ candidate = f"{base}_{counter}{ext}"
287+ counter += 1
288+ used_names.add(candidate)
289+ output_path = os.path.join(destination_dir, candidate)
290+ run_xcresult(
291+ [
292+ "export",
293+ "--legacy",
294+ "--path",
295+ bundle_path,
296+ "--id",
297+ attachment_id,
298+ "--type",
299+ "file",
300+ "--output-path",
301+ output_path,
302+ ]
303+ )
304+ ri_log(f"Exported attachment {candidate}")
305+
306+
307+ def main():
308+ if len(sys.argv) != 3:
309+ ri_log("Expected bundle path and destination directory arguments")
310+ return 1
311+
312+ bundle_path, destination_dir = sys.argv[1:3]
313+ os.makedirs(destination_dir, exist_ok=True)
314+
315+ root = get_json(bundle_path)
316+ attachments, refs = collect_nodes(root)
317+ queue = deque(refs)
318+ seen_refs = set()
319+ seen_attachment_ids = set()
320+
321+ def handle_attachments(items):
322+ for attachment in items:
323+ payload = attachment.get("payloadRef") or {}
324+ attachment_id = payload.get("id")
325+ if not attachment_id or attachment_id in seen_attachment_ids:
326+ continue
327+ if not is_image_attachment(attachment):
328+ continue
329+ seen_attachment_ids.add(attachment_id)
330+ export_attachment(bundle_path, attachment, destination_dir, exported_names)
331+
332+ exported_names = set()
333+ handle_attachments(attachments)
334+
335+ while queue:
336+ ref_id = queue.popleft()
337+ if ref_id in seen_refs:
338+ continue
339+ seen_refs.add(ref_id)
340+ data = get_json(bundle_path, ref_id)
341+ items, nested_refs = collect_nodes(data)
342+ handle_attachments(items)
343+ for nested in nested_refs:
344+ if nested not in seen_refs:
345+ queue.append(nested)
346+
347+ if not exported_names:
348+ ri_log("No screenshot attachments were exported from xcresult bundle")
349+
350+ return 0
351+
352+
353+ if __name__ == "__main__":
354+ sys.exit(main())
355+ PY
356+ then
178357 ri_log " xcresulttool export failed"
179358 exit 11
180359fi
0 commit comments