mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 12:18:39 +00:00
Use the Google APIs x86 image for API 26, matching Chromium’s Oreo test configuration, while retaining the other API images. Retry transient missing UI roots with fresh XML and bounded adb calls; preserve failure status and a screenshot when XML capture is exhausted. Keep all application code, label assertions, instrumentation and experimental release policy unchanged.
418 lines
22 KiB
Python
418 lines
22 KiB
Python
"""Android source delivery and release-proof boundaries, without device access."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import tarfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from ouroboros.tools.release_sync import (
|
|
DESKTOP_DOWNLOAD_IDS,
|
|
RELEASE_ASSET_TEMPLATES,
|
|
VERSION_CARRIER_SPANS,
|
|
release_asset_download_url,
|
|
release_asset_name,
|
|
version_carrier_desyncs,
|
|
)
|
|
|
|
|
|
REPO = Path(__file__).resolve().parents[1]
|
|
SPEC = importlib.util.spec_from_file_location("android_release", REPO / "scripts/build_android_release.py")
|
|
assert SPEC and SPEC.loader
|
|
builder = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(builder)
|
|
|
|
|
|
def test_android_assets_share_the_release_registry_without_changing_desktop_carriers():
|
|
assert release_asset_name("android-arm64", "7.1.0-rc.1") == "Ouroboros-7.1.0-rc.1-android-arm64.tar.gz"
|
|
assert release_asset_name("android-apk", "7.1.0-rc.1") == "Ouroboros-7.1.0-rc.1-android.apk"
|
|
assert len(RELEASE_ASSET_TEMPLATES) == 9
|
|
assert len(DESKTOP_DOWNLOAD_IDS) == 7
|
|
assert not any("android" in span.carrier_id for span in VERSION_CARRIER_SPANS)
|
|
references = "".join(
|
|
f"[download-{key}]: {release_asset_download_url(key, '7.1.0')}\n"
|
|
for key in DESKTOP_DOWNLOAD_IDS
|
|
)
|
|
assert version_carrier_desyncs("7.1.0", download_readme_text=references) == []
|
|
|
|
|
|
def _archive_fixture(tmp_path, monkeypatch):
|
|
source = tmp_path / "source"
|
|
stage = tmp_path / "stage"
|
|
output = tmp_path / "dist"
|
|
for path in (source, stage, output):
|
|
path.mkdir()
|
|
for name in builder.REQUIRED_FILES:
|
|
path = source / name
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text("first-party source\n", encoding="utf-8")
|
|
# A local credential/cache is not part of git's tracked source inventory.
|
|
(source / "android/private.keystore").write_bytes(b"never publish")
|
|
monkeypatch.setattr(builder, "ROOT", source)
|
|
monkeypatch.setattr(builder, "run", lambda *_args, **_kwargs: "\0".join(builder.REQUIRED_FILES))
|
|
|
|
def bundle(_root, bundle_path, manifest_path, **kwargs):
|
|
assert kwargs["source_branch"] == "ouroboros"
|
|
bundle_path.write_bytes(b"git bundle fixture")
|
|
manifest_path.write_text(json.dumps({
|
|
"source_sha": "a" * 40, "release_tag": "v7.1.0",
|
|
"bundle_sha256": builder.RELEASE["sha256_file"](bundle_path),
|
|
}))
|
|
|
|
monkeypatch.setitem(builder.BUNDLE, "build_bundle", bundle)
|
|
apk = output / "Ouroboros-7.1.0-android.apk"
|
|
apk.write_bytes(b"verified APK fixture")
|
|
identity = {"packageName": "ai.ouroboros.android", "versionName": "7.1.0",
|
|
"versionCode": 12, "signerSha256": "b" * 64}
|
|
args = argparse.Namespace(out=output, apk=apk, source_branch="ouroboros",
|
|
commit="a" * 40, tag="v7.1.0", version_code=12)
|
|
archive = builder.create_archive(args, stage, "7.1.0", identity)
|
|
return args, archive, identity
|
|
|
|
|
|
def test_archive_carries_only_tracked_source_and_binds_every_delivered_byte(tmp_path, monkeypatch):
|
|
args, archive, identity = _archive_fixture(tmp_path, monkeypatch)
|
|
with tarfile.open(archive) as handle:
|
|
names = handle.getnames()
|
|
assert not any("private.keystore" in name for name in names)
|
|
assert "Ouroboros-Android/docs/ANDROID_RECOVERY.md" in names
|
|
manifest = json.load(handle.extractfile("Ouroboros-Android/android_release_manifest.json"))
|
|
assert set(manifest["files"]) == set(builder.REQUIRED_FILES) | {"repo.bundle", "repo_bundle_manifest.json"}
|
|
assert manifest["sourceCommit"] == args.commit
|
|
assert manifest["referenceApk"] == {"name": args.apk.name, **builder.file_record(args.apk), **identity}
|
|
|
|
|
|
@pytest.mark.parametrize("tamper", [False, True])
|
|
def test_final_archive_inspection_checks_bundle_and_installer_before_receipt(tmp_path, monkeypatch, tamper):
|
|
args, archive, identity = _archive_fixture(tmp_path, monkeypatch)
|
|
calls = []
|
|
|
|
def run(argv, **kwargs):
|
|
calls.append(argv)
|
|
return args.commit + "\n" if argv[:2] == ["git", "rev-parse"] else ""
|
|
|
|
monkeypatch.setattr(builder, "run", run)
|
|
monkeypatch.setattr(builder, "apk_identity", lambda *_args: identity)
|
|
if tamper:
|
|
args.apk.write_bytes(b"different APK after packaging")
|
|
with pytest.raises(ValueError, match="Final Android APK differs"):
|
|
builder.inspect_archive(args, archive, tmp_path / "extracted")
|
|
else:
|
|
assert builder.inspect_archive(args, archive, tmp_path / "extracted") == identity
|
|
assert any(argv[:2] == ["git", "clone"] for argv in calls)
|
|
assert any(
|
|
Path(argv[1]).name == "install.py"
|
|
and Path(argv[1]).parent.name == "android"
|
|
and argv[-1] == "--help"
|
|
for argv in calls
|
|
)
|
|
|
|
|
|
def test_release_builder_refuses_missing_key_before_source_or_compiler_work(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(builder.sys, "argv", [
|
|
"build_android_release.py", "--sdk", str(tmp_path), "--java-home", str(tmp_path),
|
|
"--keystore", str(tmp_path / "absent.keystore"),
|
|
"--keystore-pass-file", str(tmp_path / "absent.password"),
|
|
"--version-code", "12", "--source-branch", "ouroboros",
|
|
"--out", str(tmp_path / "out"), "--work", str(tmp_path / "work"),
|
|
])
|
|
monkeypatch.setattr(builder, "run", lambda *_args, **_kwargs: pytest.fail("no build may run without signing input"))
|
|
with pytest.raises(SystemExit) as exc:
|
|
builder.main()
|
|
assert exc.value.code == 2
|
|
assert not (tmp_path / "out").exists()
|
|
|
|
|
|
def test_android_ci_is_fork_safe_and_experimental_for_publication():
|
|
workflow = (REPO / ".github/workflows/ci.yml").read_text(encoding="utf-8")
|
|
validation = workflow.split(" android-test:", 1)[1].split(" android-build:", 1)[0]
|
|
release = workflow.split(" android-build:", 1)[1].split(" release-preflight:", 1)[0]
|
|
publication = workflow.split(" release:\n", 1)[1]
|
|
assert "secrets." not in validation
|
|
assert "--create-development-key" in validation
|
|
assert "python -m pytest android/tests tests/test_android_release.py" in validation
|
|
assert "--create-development-key" not in release
|
|
assert "if: startsWith(github.ref, 'refs/tags/v')" in release
|
|
assert "needs: [android-test, android-emulator-smoke, release-preflight]" in release
|
|
assert "publisher signing credentials are required" in release
|
|
assert "android-build" in next(line for line in publication.splitlines() if "needs:" in line)
|
|
assert "secrets." not in release.split("- name: Generate Android source", 1)[1]
|
|
assert "always() && !cancelled()" in publication
|
|
assert "needs.android-build.result == 'success'" not in publication
|
|
assert "needs.build.result == 'success'" in publication
|
|
assert "needs.skill-smoke.result == 'success'" in publication
|
|
assert "fromJSON(steps.release_proof.outputs.files_json)" in publication
|
|
assert "--android-build-result" in publication
|
|
assert "--android-attestation-result" in publication
|
|
assert "continue-on-error" not in publication
|
|
assert "draft: true" in publication
|
|
|
|
|
|
def test_android_ci_has_representative_emulator_matrix_without_calling_it_device_qualification():
|
|
import yaml
|
|
|
|
workflow = (REPO / ".github/workflows/ci.yml").read_text(encoding="utf-8")
|
|
smoke = workflow.split(" android-emulator-smoke:", 1)[1].split(" # The publisher key", 1)[0]
|
|
assert "api-level: [26, 29, 30, 33, 36]" in smoke
|
|
assert "needs: android-test" in smoke
|
|
assert "needs.android-test.outputs.android_changed == 'true'" in smoke
|
|
assert "startsWith(github.ref, 'refs/tags/v')" in smoke
|
|
assert "adb install -r" in smoke
|
|
assert "dumpsys package ai.ouroboros.android" in smoke
|
|
assert "SELinux" not in smoke
|
|
steps = yaml.safe_load(workflow)["jobs"]["android-emulator-smoke"]["steps"]
|
|
setup = next(step for step in steps if step.get("uses", "").startswith("android-actions/setup-android@"))
|
|
assert setup["with"]["packages"].split() == ["platform-tools"]
|
|
|
|
|
|
@pytest.mark.parametrize("job_name", ["android-test", "android-build"])
|
|
@pytest.mark.skipif(os.name == "nt", reason="Exercises the Ubuntu workflow's POSIX Bash step")
|
|
def test_android_sdk_cache_uses_the_exported_runner_path(tmp_path, job_name):
|
|
import shutil
|
|
import subprocess
|
|
import yaml
|
|
|
|
bash = shutil.which("bash")
|
|
if not bash:
|
|
pytest.skip("the workflow's Bash runner is unavailable")
|
|
jobs = yaml.safe_load((REPO / ".github/workflows/ci.yml").read_text(encoding="utf-8"))["jobs"]
|
|
steps = jobs[job_name]["steps"]
|
|
cache_index = next(i for i, step in enumerate(steps) if step.get("uses", "").startswith("actions/cache@"))
|
|
exporter = next(step["run"] for step in steps[:cache_index]
|
|
if "ANDROID_HOME=" in step.get("run", "") and "GITHUB_ENV" in step["run"])
|
|
sdk = tmp_path / "SDK from runner"
|
|
for relative in ("platforms/android-36", "build-tools/36.0.0"):
|
|
(sdk / relative).mkdir(parents=True)
|
|
env_file = tmp_path / "job-env"
|
|
result = subprocess.run([bash, "-c", exporter], capture_output=True, text=True,
|
|
env={**os.environ, "ANDROID_HOME": str(sdk), "GITHUB_ENV": str(env_file)})
|
|
assert result.returncode == 0, result.stderr
|
|
exported = dict(line.split("=", 1) for line in env_file.read_text(encoding="utf-8").splitlines())
|
|
assert exported["ANDROID_HOME"] == str(sdk)
|
|
paths = steps[cache_index]["with"]["path"].splitlines()
|
|
assert {path.replace("${{ env.ANDROID_HOME }}", exported["ANDROID_HOME"]) for path in paths} == {
|
|
str(sdk / "platforms/android-36"), str(sdk / "build-tools/36.0.0"),
|
|
}
|
|
|
|
|
|
def test_android_smoke_requirements_do_not_claim_a_device_was_tested():
|
|
checks = builder.RELEASE["REQUIRED_SMOKE_CHECKS"]
|
|
assert checks["android-arm64"] == {"embedded_repo_bundle", "android_source_manifest", "usb_installer_help"}
|
|
assert checks["android-apk"] == {"apk_signature", "apk_package_version"}
|
|
|
|
|
|
def test_default_host_build_uses_the_shared_root_asset_path():
|
|
source = (REPO / "android/host/build.py").read_text(encoding="utf-8")
|
|
assert 'source.parents[1] / "assets" / "icon_1024.png"' in source
|
|
assert (REPO / "assets/icon_1024.png").is_file()
|
|
|
|
|
|
@pytest.mark.parametrize(("event", "path", "ref", "expected"), [
|
|
("pull_request", "docs/readme.md", "refs/pull/1/merge", False),
|
|
("pull_request", "ouroboros/core.py", "refs/pull/1/merge", False),
|
|
("pull_request", "android/host/change.java", "refs/pull/1/merge", True),
|
|
("push", "android/host/change.java", "refs/heads/ouroboros", True),
|
|
("push", "docs/readme.md", "refs/tags/v7.0.0", True),
|
|
("schedule", "android/host/change.java", "refs/heads/main", False),
|
|
])
|
|
@pytest.mark.skipif(os.name == "nt", reason="Exercises the Ubuntu workflow's POSIX Bash step")
|
|
def test_android_emulator_selection_uses_event_diff(tmp_path, event, path, ref, expected):
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import yaml
|
|
|
|
bash = shutil.which("bash")
|
|
if not bash:
|
|
pytest.skip("the workflow's Bash runner is unavailable")
|
|
jobs = yaml.safe_load((REPO / ".github/workflows/ci.yml").read_text(encoding="utf-8"))["jobs"]
|
|
script = next(step["run"] for step in jobs["android-test"]["steps"] if step.get("id") == "android_changes")
|
|
subprocess.run(["git", "init", "-q", str(tmp_path)], check=True)
|
|
(tmp_path / "README").write_text("base", encoding="utf-8")
|
|
subprocess.run(["git", "add", "."], cwd=tmp_path, check=True)
|
|
subprocess.run(["git", "-c", "user.name=Test", "-c", "user.email=test@example.invalid", "commit", "-qm", "base"], cwd=tmp_path, check=True)
|
|
base = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=tmp_path, text=True).strip()
|
|
changed = tmp_path / path
|
|
changed.parent.mkdir(parents=True, exist_ok=True)
|
|
changed.write_text("change", encoding="utf-8")
|
|
subprocess.run(["git", "add", "."], cwd=tmp_path, check=True)
|
|
subprocess.run(["git", "-c", "user.name=Test", "-c", "user.email=test@example.invalid", "commit", "-qm", "change"], cwd=tmp_path, check=True)
|
|
head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=tmp_path, text=True).strip()
|
|
output = tmp_path / "output"
|
|
env = {**os.environ, "GITHUB_SHA": head, "GITHUB_OUTPUT": str(output),
|
|
"PR_BASE": base if event == "pull_request" else "", "PR_HEAD": head if event == "pull_request" else "",
|
|
"PUSH_BASE": base if event == "push" else ""}
|
|
result = subprocess.run([bash, "-c", script], cwd=tmp_path, env=env, capture_output=True, text=True)
|
|
assert result.returncode == 0, result.stderr
|
|
detected = output.read_text(encoding="utf-8").strip().split("=", 1)[1]
|
|
expression = jobs["android-emulator-smoke"]["if"]
|
|
for key, value in {"github.event_name": event, "github.ref": ref,
|
|
"needs.android-test.outputs.android_changed": detected}.items():
|
|
expression = expression.replace(key, repr(value))
|
|
expression = expression.replace("&&", " and ").replace("||", " or ")
|
|
assert eval(expression, {"__builtins__": {}}, {"startsWith": str.startswith}) is expected
|
|
|
|
|
|
@pytest.mark.parametrize(("failed_job", "result", "cancelled", "expected"), [
|
|
("android-build", "failure", False, True),
|
|
("android-build", "cancelled", False, True),
|
|
("android-build", "skipped", False, True),
|
|
*[(job, "failure", False, False) for job in ("build", "release-preflight", "marker-guards",
|
|
"ui-smoke", "docker-ui-smoke", "docker-portable-test", "skill-smoke")],
|
|
(None, "success", True, False),
|
|
])
|
|
def test_release_requires_desktop_gates_and_respects_workflow_cancel(failed_job, result, cancelled, expected):
|
|
import re
|
|
import yaml
|
|
|
|
job = yaml.safe_load((REPO / ".github/workflows/ci.yml").read_text(encoding="utf-8"))["jobs"]["release"]
|
|
expression = job["if"].removeprefix("${{").removesuffix("}}")
|
|
for name in job["needs"]:
|
|
expression = expression.replace(f"needs.{name}.result", repr(result if name == failed_job else "success"))
|
|
expression = expression.replace("github.ref", repr("refs/tags/v7.0.0"))
|
|
expression = re.sub(r"!(?!=)", "not ", expression).replace("&&", " and ").replace("||", " or ")
|
|
assert eval(f"({expression.strip()})", {"__builtins__": {}}, {
|
|
"always": lambda: True, "cancelled": lambda: cancelled, "startsWith": str.startswith,
|
|
}) is expected
|
|
|
|
|
|
@pytest.mark.parametrize(("failed_platform", "expected"), [("android-apk", 0), ("macos-arm64", 1), (None, 0)])
|
|
@pytest.mark.skipif(os.name == "nt", reason="Executes the Ubuntu release step with a POSIX gh fixture")
|
|
def test_attestation_command_failure_excludes_android_but_stops_desktop(tmp_path, failed_platform, expected):
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import yaml
|
|
|
|
bash = shutil.which("bash")
|
|
if not bash:
|
|
pytest.skip("the workflow's Bash runner is unavailable")
|
|
workflow = yaml.safe_load((REPO / ".github/workflows/ci.yml").read_text(encoding="utf-8"))
|
|
script = next(step["run"] for step in workflow["jobs"]["release"]["steps"] if step.get("id") == "verify_artifacts")
|
|
binaries = tmp_path / "bin"
|
|
binaries.mkdir()
|
|
(binaries / "python").symlink_to(sys.executable)
|
|
gh = binaries / "gh"
|
|
gh.write_text(f"#!{sys.executable}\nimport os,sys,pathlib\n"
|
|
"with open(os.environ['GH_CALL_LOG'],'a',encoding='utf-8') as stream:\n"
|
|
" stream.write(pathlib.Path(sys.argv[3]).name+'\\n')\n"
|
|
"raise SystemExit(1 if pathlib.Path(sys.argv[3]).name == os.environ['FAIL_ARTIFACT'] else 0)\n",
|
|
encoding="utf-8")
|
|
gh.chmod(0o755)
|
|
version = (REPO / "VERSION").read_text(encoding="utf-8").strip()
|
|
output, calls = tmp_path / "output", tmp_path / "calls"
|
|
env = {**os.environ, "PATH": str(binaries) + os.pathsep + os.environ.get("PATH", ""),
|
|
"RUNNER_TEMP": str(tmp_path), "GITHUB_OUTPUT": str(output), "ANDROID_BUILD_RESULT": "success",
|
|
"GITHUB_REPOSITORY": "example/source", "GITHUB_SHA": "a" * 40, "GITHUB_REF": "refs/tags/v" + version,
|
|
"GH_CALL_LOG": str(calls), "FAIL_ARTIFACT": release_asset_name(failed_platform, version) if failed_platform else ""}
|
|
result = subprocess.run([bash, "-c", script], cwd=REPO, env=env, capture_output=True, text=True)
|
|
assert result.returncode == expected, result.stderr
|
|
if expected == 0:
|
|
outcome = "failure" if failed_platform else "success"
|
|
assert output.read_text(encoding="utf-8").strip() == "android_result=" + outcome
|
|
observed = calls.read_text(encoding="utf-8").splitlines()
|
|
assert {release_asset_name(key, version) for key in DESKTOP_DOWNLOAD_IDS} <= set(observed)
|
|
|
|
|
|
@pytest.mark.parametrize("api", [26, 29, 30, 33, 36])
|
|
@pytest.mark.skipif(os.name == "nt", reason="Exercises the Ubuntu emulator setup's Bash step")
|
|
def test_emulator_image_selection_uses_google_apis_only_for_oreo(tmp_path, api):
|
|
import shutil
|
|
import subprocess
|
|
import yaml
|
|
|
|
bash = shutil.which("bash")
|
|
if not bash:
|
|
pytest.skip("the workflow's Bash runner is unavailable")
|
|
jobs = yaml.safe_load((REPO / ".github/workflows/ci.yml").read_text(encoding="utf-8"))["jobs"]
|
|
script = next(step["run"] for step in jobs["android-emulator-smoke"]["steps"]
|
|
if step.get("name") == "Install emulator API and compiler")
|
|
script = script.replace("${{ matrix.api-level }}", str(api))
|
|
sdk = tmp_path / "sdk"
|
|
(sdk / "emulator").mkdir(parents=True)
|
|
executable = sdk / "emulator/emulator"
|
|
executable.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
|
|
executable.chmod(0o755)
|
|
setup = ('sdkmanager() { printf "%s\\n" "$@" > "$RUNNER_TEMP/sdk-args"; }\n'
|
|
'avdmanager() { cat >/dev/null; printf "%s\\n" "$@" > "$RUNNER_TEMP/avd-args"; }\n')
|
|
result = subprocess.run([bash, "-c", setup + script], capture_output=True, text=True,
|
|
env={**os.environ, "RUNNER_TEMP": str(tmp_path), "ANDROID_HOME": str(sdk),
|
|
"GITHUB_ENV": str(tmp_path / "job-env")})
|
|
assert result.returncode == 0, result.stderr
|
|
expected = "system-images;android-26;google_apis;x86" if api == 26 else f"system-images;android-{api};default;x86_64"
|
|
for name in ("sdk-args", "avd-args"):
|
|
args = (tmp_path / name).read_text(encoding="utf-8").splitlines()
|
|
assert [arg for arg in args if arg.startswith("system-images;")] == [expected]
|
|
|
|
|
|
@pytest.mark.parametrize(("case", "expected_exit", "attempts"), [
|
|
("recover", 0, 3), ("dump_error", 7, 5), ("stale", 9, 5), ("crash_dialog", 1, 1),
|
|
])
|
|
@pytest.mark.skipif(os.name == "nt", reason="Exercises the Ubuntu capture and EXIT trap in Bash")
|
|
def test_emulator_capture_retries_fresh_xml_and_preserves_failure_evidence(tmp_path, case, expected_exit, attempts):
|
|
import shutil
|
|
import subprocess
|
|
import yaml
|
|
|
|
bash = shutil.which("bash")
|
|
if not bash:
|
|
pytest.skip("the workflow's Bash runner is unavailable")
|
|
jobs = yaml.safe_load((REPO / ".github/workflows/ci.yml").read_text(encoding="utf-8"))["jobs"]
|
|
script = next(step["run"] for step in jobs["android-emulator-smoke"]["steps"]
|
|
if step.get("name") == "Boot emulator and smoke install/start")
|
|
capture = "capture_native_ui() {" + script.split("capture_native_ui() {", 1)[1].split("\n}\n", 1)[0] + "\n}\n"
|
|
trap = next(line for line in script.splitlines() if line.startswith("trap "))
|
|
assertion = next(line for line in script.splitlines() if line.startswith("grep -Fq 'text="))
|
|
smoke = tmp_path / "android-smoke"
|
|
screenshots = smoke / "screenshots"
|
|
screenshots.mkdir(parents=True)
|
|
old_xml = '<hierarchy><node text="Ouroboros access to your phone"/></hierarchy>'
|
|
(tmp_path / "remote.xml").write_text(old_xml, encoding="utf-8")
|
|
(screenshots / "01-access-setup.xml").write_text(old_xml, encoding="utf-8")
|
|
(tmp_path / "count").write_text("0", encoding="utf-8")
|
|
(smoke / "emulator.log").write_text("emulator evidence\n", encoding="utf-8")
|
|
# Command doubles exercise the real workflow shell, without starting adb or a device.
|
|
commands = r'''
|
|
set -euo pipefail
|
|
timeout() { shift; "$@"; }
|
|
sleep() { :; }
|
|
kill() { :; }
|
|
adb() {
|
|
case "$1 $2" in
|
|
"shell rm") rm -f "$REMOTE_XML" ;;
|
|
"shell uiautomator")
|
|
count=$(( $(cat "$COUNT") + 1 )); printf '%s' "$count" > "$COUNT"
|
|
case "$CASE" in
|
|
recover) if [ "$count" -lt 3 ]; then return 0; fi ;;
|
|
dump_error) return 7 ;;
|
|
stale) return 0 ;;
|
|
crash_dialog) printf '<hierarchy><node text="Ouroboros has stopped"/></hierarchy>' > "$REMOTE_XML"; return 0 ;;
|
|
esac
|
|
printf '<hierarchy><node text="Ouroboros access to your phone"/></hierarchy>' > "$REMOTE_XML"
|
|
;;
|
|
"pull /data/local/tmp/obo-native-ui.xml")
|
|
if [ ! -s "$REMOTE_XML" ]; then return 9; fi
|
|
cp "$REMOTE_XML" "$3" ;;
|
|
"exec-out screencap") printf 'screenshot command executed' ;;
|
|
"logcat -d") printf 'full logcat evidence' ;;
|
|
*) return 88 ;;
|
|
esac
|
|
}
|
|
'''
|
|
env = {**os.environ, "CASE": case, "COUNT": str(tmp_path / "count"),
|
|
"REMOTE_XML": str(tmp_path / "remote.xml"), "RUNNER_TEMP": str(tmp_path),
|
|
"SCREENSHOTS": str(screenshots), "LOG": str(smoke / "emulator.log"), "emulator_pid": "fixture"}
|
|
result = subprocess.run([bash, "-c", commands + trap + "\n" + capture
|
|
+ "capture_native_ui 01-access-setup\n" + assertion],
|
|
env=env, capture_output=True, text=True)
|
|
assert result.returncode == expected_exit, result.stderr
|
|
assert int((tmp_path / "count").read_text(encoding="utf-8")) == attempts
|
|
assert (screenshots / "01-access-setup.png").read_bytes() == b"screenshot command executed"
|
|
assert (smoke / "logcat.txt").read_text(encoding="utf-8") == "full logcat evidence"
|
|
if case in {"stale", "dump_error"}:
|
|
assert not (screenshots / "01-access-setup.xml").exists()
|