from __future__ import annotations import copy import json from pathlib import Path import tempfile import unittest from scripts import android_release as android from scripts.preview_release import sha256 class AndroidReleaseTests(unittest.TestCase): def identity(self): metadata = {"applicationId": android.APPLICATION_ID, "variantName": "preview", "elements": [ {"filters": [], "outputFile": "app-preview.apk", "versionCode": 18, "versionName": "2.0.0-preview"}, ]} badging = f"package: name='{android.APPLICATION_ID}' versionCode='18' versionName='2.0.0-preview' platformBuildVersionName='15'\n" signature = f"Signer #1 certificate SHA-256 digest: {android.CERTIFICATE_SHA256}\n" return metadata, badging, signature def test_accepts_current_preview_and_incremented_version(self): self.assertEqual(android.validate_identity(*self.identity(), "2.0.0"), 18) self.assertEqual(android.asset_name("2.0.0"), "Pebrel-v2.0.0-android-universal-preview.apk") def test_rejects_production_debug_and_split_variants(self): metadata, badging, signature = self.identity() for field, value in (("applicationId", android.APPLICATION_ID.removesuffix(".preview")), ("variantName", "debug")): changed = copy.deepcopy(metadata) changed[field] = value with self.subTest(field=field), self.assertRaises(ValueError): android.validate_identity(changed, badging, signature, "2.0.0") for value in ([{"filterType": "ABI", "value": "arm64-v8a"}], None): changed = copy.deepcopy(metadata) changed["elements"][0]["filters"] = value with self.assertRaises(ValueError): android.validate_identity(changed, badging, signature, "2.0.0") with self.assertRaisesRegex(ValueError, "debuggable"): android.validate_identity(metadata, badging + "application-debuggable\n", signature, "2.0.0") def test_rejects_wrong_signer_version_and_compiled_package(self): metadata, badging, signature = self.identity() for changed in ("", signature.replace(android.CERTIFICATE_SHA256, "a" * 64), signature + signature.replace("#1", "#2")): with self.assertRaisesRegex(ValueError, "signer"): android.validate_identity(metadata, badging, changed, "2.0.0") for code in (0, 17, True, "18"): changed = copy.deepcopy(metadata) changed["elements"][0]["versionCode"] = code with self.assertRaisesRegex(ValueError, "version"): android.validate_identity(changed, badging, signature, "2.0.0") with self.assertRaisesRegex(ValueError, "version"): android.validate_identity(metadata, badging, signature, "2.0.1") with self.assertRaisesRegex(ValueError, "Compiled"): android.validate_identity(metadata, badging.replace("versionCode='18'", "versionCode='19'"), signature, "2.0.0") def test_reports_require_executed_tests_and_reject_failures(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) with self.assertRaisesRegex(ValueError, "Missing"): android.test_summary(root) report = root / "TEST-unit.xml" report.write_text('', encoding="utf-8") self.assertEqual(android.test_summary(root), {"passed": 1, "skipped": 1}) invalid = [ '', '', '', '', ] for text in invalid: report.write_text(text, encoding="utf-8") with self.subTest(text=text), self.assertRaises(ValueError): android.test_summary(root) def test_aggregate_binds_evidence_to_source_apk_and_both_test_suites(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) name = android.asset_name("2.0.0") apk = root / name apk.write_bytes(b"APK byte identity fixture, not an installable package") data = {"schema_version": 1, "status": "passed", "commit": "a" * 40, "version": "2.0.0", "application_id": android.APPLICATION_ID, "version_code": 18, "certificate_sha256": android.CERTIFICATE_SHA256, "apk": name, "sha256": sha256(apk), "tests": {"unit": {"passed": 1}, "instrumented": {"passed": 1}}} report = root / "report.json" report.write_text(json.dumps(data), encoding="utf-8") android.validate_evidence(report, root, "2.0.0", "a" * 40) for field, value in (("commit", "b" * 40), ("sha256", "b" * 64), ("version_code", 17), ("certificate_sha256", "b" * 64), ("tests", {"unit": {"passed": 1}})): invalid = {**data, field: value} report.write_text(json.dumps(invalid), encoding="utf-8") with self.subTest(field=field), self.assertRaises(ValueError): android.validate_evidence(report, root, "2.0.0", "a" * 40) def test_release_workflow_requires_android_and_reuses_preview_signing(self): root = Path(__file__).resolve().parents[2] workflow = (root / ".github/workflows/release.yml").read_text(encoding="utf-8") android_workflow = (root / ".github/workflows/android-release.yml").read_text(encoding="utf-8") self.assertIn("uses: ./.github/workflows/android-release.yml", workflow) self.assertIn("needs: [prepare, linux, macos, windows, windows-arm64, android]", workflow) self.assertIn("release-dist/*.apk", workflow) for required in (":app:testDebugUnitTest", ":app:lintPreview", ":app:assemblePreview", ":app:connectedPreviewAndroidTest", "--commit", "stable-package-android", "stable-evidence-android", "aarch64-unknown-linux-musl", "x86_64-unknown-linux-musl"): self.assertIn(required, android_workflow) self.assertNotIn("continue-on-error", android_workflow) self.assertNotIn("assembleDebug", android_workflow) self.assertNotIn("keytool -genkey", android_workflow) if __name__ == "__main__": unittest.main()