feat: add in-reader search and restructure documentation
- Add RDEPUBReaderSearchBarView with animated show/hide, keyword navigation, and match counting integrated into the reader controller - Restructure docs: replace scattered design docs with consolidated BUSINESS_LOGIC.md and UML_CLASS_DIAGRAMS.md; update ARCHITECTURE.md - Add SearchTests and FanrenParseTimeTest; enhance LargeBookOnDemandTests - Add scripts/run_ui_regression.sh and summarize_ui_results.py for automated UI test execution and reporting Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
d20196ee34
commit
0e7c0577e3
Executable
+159
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PROJECT_PATH="${PROJECT_PATH:-$ROOT_DIR/ReadViewDemo/ReadViewDemo.xcodeproj}"
|
||||
SCHEME_NAME="${SCHEME_NAME:-ReadViewDemo}"
|
||||
CONFIGURATION="${CONFIGURATION:-Debug}"
|
||||
SIMULATOR_NAME="${SIMULATOR_NAME:-iPhone 16 Plus}"
|
||||
DESTINATION="${DESTINATION:-platform=iOS Simulator,name=${SIMULATOR_NAME}}"
|
||||
RESULTS_ROOT="${RESULTS_ROOT:-$ROOT_DIR/.artifacts/ui-tests}"
|
||||
DERIVED_DATA_PATH="${DERIVED_DATA_PATH:-$RESULTS_ROOT/DerivedData}"
|
||||
ONLY_TESTING="${ONLY_TESTING:-}"
|
||||
TEST_PLAN="${TEST_PLAN:-}"
|
||||
TEST_TYPE_ARG="${1:-}"
|
||||
TEST_TYPE="${TEST_TYPE:-${TEST_TYPE_ARG:-quick}}"
|
||||
TIMESTAMP="$(date +"%Y%m%d-%H%M%S")"
|
||||
RUN_DIR="$RESULTS_ROOT/$TIMESTAMP"
|
||||
XCRESULT_PATH="$RUN_DIR/ReadViewDemoUITests.xcresult"
|
||||
RAW_LOG_PATH="$RUN_DIR/xcodebuild.log"
|
||||
REPORT_MD_PATH="$RUN_DIR/UI-Test-Report.md"
|
||||
REPORT_JSON_PATH="$RUN_DIR/ui-test-results.json"
|
||||
REPORT_TXT_PATH="$RUN_DIR/summary.txt"
|
||||
SUMMARY_SCRIPT="$ROOT_DIR/scripts/summarize_ui_results.py"
|
||||
|
||||
TARGET_NAME="ReadViewDemoUITests"
|
||||
|
||||
QUICK_TESTS=(
|
||||
BookmarkManagementTests
|
||||
BookmarkTests
|
||||
DisplayTypeTests
|
||||
ErrorAndEdgeCaseTests
|
||||
HighlightsManagementTests
|
||||
LocationPersistenceTests
|
||||
PageNavigationTests
|
||||
ReaderAnnotationExtendedTests
|
||||
ReaderAnnotationTests
|
||||
ReaderOpenCloseTests
|
||||
ReaderToolbarTests
|
||||
SearchTests
|
||||
SelectionMenuTests
|
||||
SettingsEffectTests
|
||||
SettingsExtendedTests
|
||||
SettingsPanelTests
|
||||
TOCInteractionTests
|
||||
TableOfContentsTests
|
||||
ToolbarStateTests
|
||||
)
|
||||
|
||||
LARGE_BOOK_TESTS=(
|
||||
ConfigurableWindowTests
|
||||
ConcurrentParsingTests
|
||||
LargeBookOnDemandTests
|
||||
)
|
||||
|
||||
PERFORMANCE_TESTS=(
|
||||
FanrenParseTimeTest
|
||||
MetadataParseBenchmarkTests
|
||||
)
|
||||
|
||||
mkdir -p "$RUN_DIR" "$DERIVED_DATA_PATH"
|
||||
|
||||
append_only_testing_items() {
|
||||
local raw_list="$1"
|
||||
local item
|
||||
IFS=',' read -r -a ONLY_TESTING_ITEMS <<< "$raw_list"
|
||||
for item in "${ONLY_TESTING_ITEMS[@]}"; do
|
||||
local trimmed_item
|
||||
trimmed_item="$(echo "$item" | xargs)"
|
||||
if [[ -n "$trimmed_item" ]]; then
|
||||
XCODEBUILD_ARGS+=(-only-testing:"$trimmed_item")
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
apply_test_type_filter() {
|
||||
local selected_type="$1"
|
||||
local test_name
|
||||
case "$selected_type" in
|
||||
quick)
|
||||
for test_name in "${QUICK_TESTS[@]}"; do
|
||||
XCODEBUILD_ARGS+=(-only-testing:"$TARGET_NAME/$test_name")
|
||||
done
|
||||
;;
|
||||
large-book)
|
||||
for test_name in "${LARGE_BOOK_TESTS[@]}"; do
|
||||
XCODEBUILD_ARGS+=(-only-testing:"$TARGET_NAME/$test_name")
|
||||
done
|
||||
;;
|
||||
performance)
|
||||
for test_name in "${PERFORMANCE_TESTS[@]}"; do
|
||||
XCODEBUILD_ARGS+=(-only-testing:"$TARGET_NAME/$test_name")
|
||||
done
|
||||
;;
|
||||
all)
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported TEST_TYPE: $selected_type" >&2
|
||||
echo "Supported values: quick, large-book, performance, all" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
echo "UI regression run"
|
||||
echo " project: $PROJECT_PATH"
|
||||
echo " scheme: $SCHEME_NAME"
|
||||
echo " configuration:$CONFIGURATION"
|
||||
echo " destination: $DESTINATION"
|
||||
echo " test type: $TEST_TYPE"
|
||||
echo " results: $RUN_DIR"
|
||||
|
||||
XCODEBUILD_ARGS=(
|
||||
xcodebuild
|
||||
test
|
||||
-project "$PROJECT_PATH"
|
||||
-scheme "$SCHEME_NAME"
|
||||
-configuration "$CONFIGURATION"
|
||||
-destination "$DESTINATION"
|
||||
-resultBundlePath "$XCRESULT_PATH"
|
||||
-derivedDataPath "$DERIVED_DATA_PATH"
|
||||
)
|
||||
|
||||
if [[ -n "$TEST_PLAN" ]]; then
|
||||
XCODEBUILD_ARGS+=(-testPlan "$TEST_PLAN")
|
||||
fi
|
||||
|
||||
if [[ -n "$ONLY_TESTING" ]]; then
|
||||
append_only_testing_items "$ONLY_TESTING"
|
||||
else
|
||||
apply_test_type_filter "$TEST_TYPE"
|
||||
fi
|
||||
|
||||
set +e
|
||||
"${XCODEBUILD_ARGS[@]}" 2>&1 | tee "$RAW_LOG_PATH"
|
||||
TEST_EXIT_CODE=${PIPESTATUS[0]}
|
||||
set -e
|
||||
|
||||
python3 "$SUMMARY_SCRIPT" \
|
||||
--xcresult "$XCRESULT_PATH" \
|
||||
--raw-log "$RAW_LOG_PATH" \
|
||||
--output-md "$REPORT_MD_PATH" \
|
||||
--output-json "$REPORT_JSON_PATH" \
|
||||
--output-txt "$REPORT_TXT_PATH" \
|
||||
--test-type "$TEST_TYPE" \
|
||||
--xcodebuild-exit-code "$TEST_EXIT_CODE"
|
||||
|
||||
echo
|
||||
cat "$REPORT_TXT_PATH"
|
||||
echo
|
||||
echo "Artifacts"
|
||||
echo " summary: $REPORT_TXT_PATH"
|
||||
echo " report: $REPORT_MD_PATH"
|
||||
echo " json: $REPORT_JSON_PATH"
|
||||
echo " xcresult: $XCRESULT_PATH"
|
||||
echo " raw log: $RAW_LOG_PATH"
|
||||
|
||||
exit "$TEST_EXIT_CODE"
|
||||
Executable
+465
@@ -0,0 +1,465 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class FailureItem:
|
||||
test_name: str
|
||||
target_name: str
|
||||
category: str
|
||||
summary: str
|
||||
failure_text: str
|
||||
test_identifier: str | None = None
|
||||
|
||||
|
||||
def run_json_command(command: list[str]) -> dict[str, Any] | list[Any] | None:
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
return None
|
||||
|
||||
stdout = completed.stdout.strip()
|
||||
if not stdout:
|
||||
return None
|
||||
try:
|
||||
return json.loads(stdout)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def compact_text(text: str) -> str:
|
||||
return re.sub(r"\s+", " ", text).strip()
|
||||
|
||||
|
||||
def summarize_failure_text(text: str) -> str:
|
||||
compact = compact_text(text)
|
||||
if not compact:
|
||||
return "No failure message captured"
|
||||
|
||||
for delimiter in (" - ", ". ", "\n", "…"):
|
||||
if delimiter in compact:
|
||||
head = compact.split(delimiter, 1)[0].strip()
|
||||
if len(head) >= 12:
|
||||
return head[:220]
|
||||
return compact[:220]
|
||||
|
||||
|
||||
def classify_failure(text: str, test_name: str) -> str:
|
||||
haystack = f"{test_name} {text}".lower()
|
||||
|
||||
if any(token in haystack for token in [
|
||||
"timed out",
|
||||
"timeout",
|
||||
"failed to fulfill",
|
||||
"waitfordemoreaderstate",
|
||||
"waitforexistence",
|
||||
"等待",
|
||||
"超时",
|
||||
]):
|
||||
return "Timeout"
|
||||
|
||||
if any(token in haystack for token in [
|
||||
"no matches found",
|
||||
"failed to find",
|
||||
"does not exist",
|
||||
"existence",
|
||||
"not hittable",
|
||||
"不存在",
|
||||
"未出现",
|
||||
]):
|
||||
return "Element Missing"
|
||||
|
||||
if any(token in haystack for token in [
|
||||
"simulator",
|
||||
"lost connection",
|
||||
"test runner exited",
|
||||
"application state",
|
||||
"quit unexpectedly",
|
||||
"connection interrupted",
|
||||
"xcodebuild",
|
||||
]):
|
||||
return "Environment"
|
||||
|
||||
if any(token in haystack for token in [
|
||||
"page",
|
||||
"pagination",
|
||||
"bookpagemap",
|
||||
"parsems",
|
||||
"readerstate",
|
||||
"location",
|
||||
"页码",
|
||||
"分页",
|
||||
"位置",
|
||||
]):
|
||||
return "Navigation/State"
|
||||
|
||||
if any(token in haystack for token in [
|
||||
"xctassert",
|
||||
"assertion",
|
||||
"xctfail",
|
||||
"failed:",
|
||||
]):
|
||||
return "Assertion Failure"
|
||||
|
||||
return "Functional Failure"
|
||||
|
||||
|
||||
def normalize_root_cause(text: str) -> str:
|
||||
compact = summarize_failure_text(text).lower()
|
||||
compact = re.sub(r"\b\d+\b", "<n>", compact)
|
||||
compact = re.sub(r"'[^']+'", "'<value>'", compact)
|
||||
compact = re.sub(r'"[^"]+"', '"<value>"', compact)
|
||||
return compact[:180]
|
||||
|
||||
|
||||
def load_summary_from_xcresult(xcresult_path: Path) -> dict[str, Any] | None:
|
||||
return run_json_command([
|
||||
"xcrun",
|
||||
"xcresulttool",
|
||||
"get",
|
||||
"test-results",
|
||||
"summary",
|
||||
"--path",
|
||||
str(xcresult_path),
|
||||
"--compact",
|
||||
])
|
||||
|
||||
|
||||
def load_tests_from_xcresult(xcresult_path: Path) -> dict[str, Any] | None:
|
||||
return run_json_command([
|
||||
"xcrun",
|
||||
"xcresulttool",
|
||||
"get",
|
||||
"test-results",
|
||||
"tests",
|
||||
"--path",
|
||||
str(xcresult_path),
|
||||
"--compact",
|
||||
])
|
||||
|
||||
|
||||
def flatten_failed_test_nodes(node: dict[str, Any], out: list[dict[str, Any]]) -> None:
|
||||
node_type = node.get("nodeType")
|
||||
result = node.get("result")
|
||||
if node_type == "Test Case" and result == "Failed":
|
||||
out.append(node)
|
||||
for child in node.get("children", []) or []:
|
||||
flatten_failed_test_nodes(child, out)
|
||||
|
||||
|
||||
def extract_failures(
|
||||
summary: dict[str, Any] | None,
|
||||
tests_payload: dict[str, Any] | None,
|
||||
raw_log_path: Path | None,
|
||||
) -> list[FailureItem]:
|
||||
failures: list[FailureItem] = []
|
||||
|
||||
raw_failures = None
|
||||
if summary:
|
||||
raw_failures = summary.get("testFailures")
|
||||
|
||||
if isinstance(raw_failures, dict):
|
||||
raw_failures = [raw_failures]
|
||||
|
||||
if isinstance(raw_failures, list):
|
||||
for item in raw_failures:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
test_name = item.get("testName") or "Unknown test"
|
||||
target_name = item.get("targetName") or "Unknown target"
|
||||
failure_text = item.get("failureText") or ""
|
||||
failures.append(FailureItem(
|
||||
test_name=test_name,
|
||||
target_name=target_name,
|
||||
category=classify_failure(failure_text, test_name),
|
||||
summary=summarize_failure_text(failure_text),
|
||||
failure_text=compact_text(failure_text),
|
||||
test_identifier=item.get("testIdentifierString") or item.get("testIdentifierURL"),
|
||||
))
|
||||
|
||||
if failures:
|
||||
return dedupe_failures(failures)
|
||||
|
||||
if tests_payload:
|
||||
failed_nodes: list[dict[str, Any]] = []
|
||||
for node in tests_payload.get("testNodes", []) or []:
|
||||
flatten_failed_test_nodes(node, failed_nodes)
|
||||
for node in failed_nodes:
|
||||
test_name = node.get("name") or "Unknown test"
|
||||
details = node.get("details") or ""
|
||||
failures.append(FailureItem(
|
||||
test_name=test_name,
|
||||
target_name="Unknown target",
|
||||
category=classify_failure(details, test_name),
|
||||
summary=summarize_failure_text(details),
|
||||
failure_text=compact_text(details),
|
||||
test_identifier=node.get("nodeIdentifier") or node.get("nodeIdentifierURL"),
|
||||
))
|
||||
|
||||
if failures:
|
||||
return dedupe_failures(failures)
|
||||
|
||||
if raw_log_path and raw_log_path.exists():
|
||||
log_text = raw_log_path.read_text(encoding="utf-8", errors="replace")
|
||||
matches = re.findall(
|
||||
r"Test Case '-\[(.*?)\]' failed .*?\n(.*?)(?=\nTest Case '-\[|\Z)",
|
||||
log_text,
|
||||
flags=re.S,
|
||||
)
|
||||
for test_name, block in matches:
|
||||
failure_text = compact_text(block)
|
||||
failures.append(FailureItem(
|
||||
test_name=test_name,
|
||||
target_name="Unknown target",
|
||||
category=classify_failure(failure_text, test_name),
|
||||
summary=summarize_failure_text(failure_text),
|
||||
failure_text=failure_text,
|
||||
))
|
||||
|
||||
return dedupe_failures(failures)
|
||||
|
||||
|
||||
def dedupe_failures(failures: list[FailureItem]) -> list[FailureItem]:
|
||||
deduped: dict[tuple[str, str], FailureItem] = {}
|
||||
for failure in failures:
|
||||
key = (failure.test_name, failure.summary)
|
||||
deduped.setdefault(key, failure)
|
||||
return list(deduped.values())
|
||||
|
||||
|
||||
def build_root_causes(failures: list[FailureItem]) -> list[dict[str, Any]]:
|
||||
grouped: dict[str, list[FailureItem]] = defaultdict(list)
|
||||
for failure in failures:
|
||||
grouped[normalize_root_cause(failure.failure_text)].append(failure)
|
||||
|
||||
ranked = sorted(
|
||||
grouped.items(),
|
||||
key=lambda item: (-len(item[1]), item[0]),
|
||||
)
|
||||
result = []
|
||||
for normalized_text, items in ranked[:5]:
|
||||
result.append({
|
||||
"summary": items[0].summary,
|
||||
"category": items[0].category,
|
||||
"count": len(items),
|
||||
"tests": [failure.test_name for failure in items[:5]],
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def build_report_payload(
|
||||
args: argparse.Namespace,
|
||||
summary: dict[str, Any] | None,
|
||||
tests_payload: dict[str, Any] | None,
|
||||
failures: list[FailureItem],
|
||||
) -> dict[str, Any]:
|
||||
device_summaries = []
|
||||
if summary:
|
||||
devices_and_configurations = summary.get("devicesAndConfigurations")
|
||||
if isinstance(devices_and_configurations, dict):
|
||||
devices_and_configurations = [devices_and_configurations]
|
||||
if isinstance(devices_and_configurations, list):
|
||||
for item in devices_and_configurations:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
device = item.get("device") or {}
|
||||
config = item.get("testPlanConfiguration") or {}
|
||||
device_summaries.append({
|
||||
"device_name": device.get("deviceName"),
|
||||
"model_name": device.get("modelName"),
|
||||
"os_version": device.get("osVersion"),
|
||||
"configuration_name": config.get("configurationName"),
|
||||
"passed_tests": item.get("passedTests"),
|
||||
"failed_tests": item.get("failedTests"),
|
||||
})
|
||||
|
||||
categories = Counter(failure.category for failure in failures)
|
||||
|
||||
payload = {
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"xcodebuild_exit_code": args.xcodebuild_exit_code,
|
||||
"test_type": args.test_type,
|
||||
"artifacts": {
|
||||
"xcresult": str(Path(args.xcresult).resolve()) if args.xcresult else None,
|
||||
"raw_log": str(Path(args.raw_log).resolve()) if args.raw_log else None,
|
||||
},
|
||||
"summary": {
|
||||
"title": summary.get("title") if summary else None,
|
||||
"environment_description": summary.get("environmentDescription") if summary else None,
|
||||
"result": summary.get("result") if summary else ("Failed" if failures else "Passed"),
|
||||
"total_test_count": summary.get("totalTestCount") if summary else None,
|
||||
"passed_tests": summary.get("passedTests") if summary else None,
|
||||
"failed_tests": summary.get("failedTests") if summary else len(failures),
|
||||
"skipped_tests": summary.get("skippedTests") if summary else None,
|
||||
"devices": device_summaries,
|
||||
},
|
||||
"failure_categories": dict(sorted(categories.items(), key=lambda item: (-item[1], item[0]))),
|
||||
"top_root_causes": build_root_causes(failures),
|
||||
"failures": [asdict(failure) for failure in failures],
|
||||
"token_saving_notes": [
|
||||
"Raw xcodebuild output is intentionally omitted from this report.",
|
||||
"Use the xcresult or raw log paths only when a single failure needs deeper inspection.",
|
||||
"Failures are deduplicated by test name and compact summary.",
|
||||
],
|
||||
}
|
||||
|
||||
if tests_payload and not payload["summary"]["total_test_count"]:
|
||||
flat_failed_nodes: list[dict[str, Any]] = []
|
||||
for node in tests_payload.get("testNodes", []) or []:
|
||||
flatten_failed_test_nodes(node, flat_failed_nodes)
|
||||
payload["summary"]["failed_tests"] = len(flat_failed_nodes)
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def render_markdown(payload: dict[str, Any]) -> str:
|
||||
summary = payload["summary"]
|
||||
lines = [
|
||||
"# UI Test Report",
|
||||
"",
|
||||
"## Snapshot",
|
||||
"",
|
||||
f"- Test type: `{payload.get('test_type')}`",
|
||||
f"- Result: `{summary.get('result')}`",
|
||||
f"- xcodebuild exit code: `{payload['xcodebuild_exit_code']}`",
|
||||
f"- Total tests: `{summary.get('total_test_count')}`",
|
||||
f"- Passed: `{summary.get('passed_tests')}`",
|
||||
f"- Failed: `{summary.get('failed_tests')}`",
|
||||
f"- Skipped: `{summary.get('skipped_tests')}`",
|
||||
]
|
||||
|
||||
if summary.get("environment_description"):
|
||||
lines.append(f"- Environment: `{summary['environment_description']}`")
|
||||
|
||||
artifacts = payload["artifacts"]
|
||||
lines.extend([
|
||||
f"- xcresult: `{artifacts.get('xcresult')}`",
|
||||
f"- raw log: `{artifacts.get('raw_log')}`",
|
||||
"",
|
||||
])
|
||||
|
||||
if payload["failure_categories"]:
|
||||
lines.extend([
|
||||
"## Failure Categories",
|
||||
"",
|
||||
])
|
||||
for category, count in payload["failure_categories"].items():
|
||||
lines.append(f"- `{category}`: {count}")
|
||||
lines.append("")
|
||||
|
||||
if payload["top_root_causes"]:
|
||||
lines.extend([
|
||||
"## Top Root Causes",
|
||||
"",
|
||||
])
|
||||
for item in payload["top_root_causes"]:
|
||||
lines.append(f"- `{item['category']}` x{item['count']}: {item['summary']}")
|
||||
lines.append("")
|
||||
|
||||
lines.extend([
|
||||
"## Failed Tests",
|
||||
"",
|
||||
])
|
||||
if payload["failures"]:
|
||||
for failure in payload["failures"]:
|
||||
lines.append(
|
||||
f"- `{failure['test_name']}` [{failure['category']}] {failure['summary']}"
|
||||
)
|
||||
else:
|
||||
lines.append("- None")
|
||||
lines.append("")
|
||||
|
||||
lines.extend([
|
||||
"## Token-Saving Notes",
|
||||
"",
|
||||
])
|
||||
for note in payload["token_saving_notes"]:
|
||||
lines.append(f"- {note}")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def render_text_summary(payload: dict[str, Any]) -> str:
|
||||
summary = payload["summary"]
|
||||
total = summary.get("total_test_count")
|
||||
passed = summary.get("passed_tests")
|
||||
failed = summary.get("failed_tests")
|
||||
skipped = summary.get("skipped_tests")
|
||||
lines = [
|
||||
"UI test summary",
|
||||
f" type: {payload.get('test_type')}",
|
||||
f" result: {summary.get('result')}",
|
||||
f" tests: total={total} passed={passed} failed={failed} skipped={skipped}",
|
||||
]
|
||||
if payload["failure_categories"]:
|
||||
category_text = ", ".join(
|
||||
f"{name}={count}" for name, count in payload["failure_categories"].items()
|
||||
)
|
||||
lines.append(f" categories: {category_text}")
|
||||
if payload["top_root_causes"]:
|
||||
top = payload["top_root_causes"][0]
|
||||
lines.append(f" top root cause: {top['summary']} ({top['count']} tests)")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Summarize UI test xcresult output into a low-token report.")
|
||||
parser.add_argument("--xcresult", help="Path to .xcresult bundle", default="")
|
||||
parser.add_argument("--raw-log", help="Path to xcodebuild raw log", default="")
|
||||
parser.add_argument("--output-md", required=True, help="Markdown report output path")
|
||||
parser.add_argument("--output-json", required=True, help="JSON report output path")
|
||||
parser.add_argument("--output-txt", required=True, help="Plain-text summary output path")
|
||||
parser.add_argument("--test-type", default="unknown", help="Selected UI test group")
|
||||
parser.add_argument("--xcodebuild-exit-code", type=int, required=True, help="Exit code from xcodebuild")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
xcresult_path = Path(args.xcresult) if args.xcresult else None
|
||||
raw_log_path = Path(args.raw_log) if args.raw_log else None
|
||||
|
||||
summary = None
|
||||
tests_payload = None
|
||||
if xcresult_path and xcresult_path.exists():
|
||||
summary = load_summary_from_xcresult(xcresult_path)
|
||||
tests_payload = load_tests_from_xcresult(xcresult_path)
|
||||
|
||||
failures = extract_failures(summary, tests_payload, raw_log_path)
|
||||
payload = build_report_payload(args, summary, tests_payload, failures)
|
||||
|
||||
output_md = Path(args.output_md)
|
||||
output_json = Path(args.output_json)
|
||||
output_txt = Path(args.output_txt)
|
||||
output_md.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
output_json.write_text(
|
||||
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
output_md.write_text(render_markdown(payload) + "\n", encoding="utf-8")
|
||||
output_txt.write_text(render_text_summary(payload) + "\n", encoding="utf-8")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user