field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

commit 49e3c5a8c9eb3da8fa8e215c1ae0747bf89befe0
parent b0c62230dc4a614938e38ba218a1c01812814220
Author: triesap <tyson@radroots.org>
Date:   Wed,  9 Sep 2026 15:09:46 +0000

ffi: qualify runtime scope and resource portability

- Run generated Kotlin against the exact native Rust producer with pinned tooling.
- Test scope, unsigned values, typed errors, cancellation and owned handle disposal.
- Integrate standalone bootstrap and offline smoke with strict result validation.
- Verify Rust, Swift, Kotlin, generated artifacts and unchanged mobile feature closure.

Diffstat:
MMakefile | 12+++++++++---
MREADME.md | 14++++++++++++--
MTera.xcodeproj/project.pbxproj | 4++++
MTeraFFI/provenance.json | 26+++++++++++++-------------
MTeraFFI/source.lock | 4++--
MTeraFFI/source/aarch64-apple-darwin.json | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
MTeraFFI/source/aarch64-apple-ios-sim.json | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
MTeraFFI/source/aarch64-apple-ios.json | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
ATeraTests/TeraBindingPortabilityTests.swift | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_ffi/tests/binding_portability.rs | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mproject.yml | 1+
Mrelease/provenance.json | 6+++---
Ascripts/kotlin-smoke.sh | 6++++++
Ascripts/kotlin_smoke.py | 271+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascripts/kotlin_smoke/build.gradle.kts | 42++++++++++++++++++++++++++++++++++++++++++
Ascripts/kotlin_smoke/gradle.lockfile | 41+++++++++++++++++++++++++++++++++++++++++
Ascripts/kotlin_smoke/gradle.properties | 7+++++++
Ascripts/kotlin_smoke/gradle/verification-metadata.xml | 439+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascripts/kotlin_smoke/gradle/wrapper/gradle-wrapper.jar | 0
Ascripts/kotlin_smoke/gradle/wrapper/gradle-wrapper.properties | 10++++++++++
Ascripts/kotlin_smoke/gradlew | 248+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascripts/kotlin_smoke/settings.gradle.kts | 13+++++++++++++
Ascripts/kotlin_smoke/src/test/kotlin/tera/smoke/MediaOwnershipTests.kt | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascripts/kotlin_smoke/src/test/kotlin/tera/smoke/ScopeLifecycleTests.kt | 186+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascripts/kotlin_smoke/src/test/kotlin/tera/smoke/SmokeFixture.kt | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/legacy_identifiers.py | 6++++--
Mscripts/swift-quality.sh | 2++
Ascripts/test_kotlin_smoke.py | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/test_legacy_identifiers.py | 12++++++++++++
Mscripts/verify-package-contract.sh | 1+
Mtest-fixtures/legacy-identifiers.v1.json | 6+++++-
31 files changed, 2131 insertions(+), 83 deletions(-)

diff --git a/Makefile b/Makefile @@ -12,7 +12,7 @@ SIMULATOR_DESTINATION := platform=iOS Simulator,name=$(SIMULATOR_NAME) ffi-source-write ffi-source-check \ ffi-candidate-build ffi-candidate-check \ swift-quality maintainability-check \ - linux-shared-rust \ + linux-shared-rust kotlin-smoke kotlin-smoke-bootstrap \ package-resolve package-build package-test project xcodegen xcode-resolve \ xcode-build-debug xcode-build-release unit-test ui-test api-snapshot-write \ api-snapshot-check release-evidence-write release-preflight verify clean distclean @@ -56,6 +56,12 @@ maintainability-check: doctor linux-shared-rust: doctor cargo extbuild run -- scripts/linux-shared-rust.sh +kotlin-smoke: doctor + cargo extbuild run -- scripts/kotlin-smoke.sh verify + +kotlin-smoke-bootstrap: doctor + cargo extbuild run -- scripts/kotlin-smoke.sh bootstrap + package-resolve: artifact-check package-contract-check cargo extbuild run -- scripts/swift-package.sh resolve @@ -65,7 +71,7 @@ project xcodegen: doctor xcode-resolve: artifact-check project cargo extbuild run -- scripts/xcode.sh resolve -bootstrap: persona-verifier-bootstrap ffi-bootstrap package-resolve xcode-resolve +bootstrap: persona-verifier-bootstrap ffi-bootstrap kotlin-smoke-bootstrap package-resolve xcode-resolve package-build: artifact-check package-contract-check cargo extbuild run -- scripts/xcode.sh package-build @@ -97,7 +103,7 @@ release-evidence-write: doctor release-preflight: artifact-check package-contract-check cargo extbuild run -- scripts/release-preflight.sh -verify: swift-quality linux-shared-rust artifact-check package-contract-check package-build package-test \ +verify: swift-quality linux-shared-rust kotlin-smoke artifact-check package-contract-check package-build package-test \ xcode-build-debug xcode-build-release unit-test ui-test api-snapshot-check clean: doctor diff --git a/README.md b/README.md @@ -37,6 +37,7 @@ extbuild output; this command does not install them into the native app. - XcodeGen - Rust `1.97.1-aarch64-apple-darwin` with `llvm-tools` and the iOS device and simulator targets - SwiftFormat and the locked Python verifier installed by bootstrap +- JDK 21 on Apple silicon for the generated Kotlin binding smoke - `cargo-extbuild` configured for the checkout Physical-device development additionally requires one exact paired, connected, @@ -61,8 +62,8 @@ certificate material are never checked into this public repository. The first bootstrap requires network access. It resolves exact foundation dependencies, builds the owned UniFFI XCFramework and Swift -bindings, resolves exact Swift package revisions, and generates the Xcode -project: +bindings, bootstraps the pinned Kotlin smoke dependencies, resolves exact Swift +package revisions, and generates the Xcode project: ```sh cargo extbuild doctor @@ -81,6 +82,15 @@ policy and the pinned Linux x86_64 shared-Rust runner. Run those focused checks independently with `make swift-quality` and `make linux-shared-rust`; both keep their build output under extbuild. +`make kotlin-smoke` generates Kotlin from the exact installed producer's native +host library and executes its scope, error, cancellation, subscription, shutdown, +and media-ownership tests through JNA. The pinned Gradle wrapper, dependency locks +and checksum verification are owned by this repository; generated Kotlin, native +test results and Gradle output remain under extbuild. Verification uses offline +dependency resolution after `make kotlin-smoke-bootstrap` or `make bootstrap`. +This test-only JVM harness is part of `make verify` and does not qualify Android +UI or a published release. + `make swift-quality` also applies the exact checked SwiftLint complexity baseline and the repository-owned Swift/Python maintainability ratchet. New Swift files are capped at 600 physical lines, new Python files at 800, and new diff --git a/Tera.xcodeproj/project.pbxproj b/Tera.xcodeproj/project.pbxproj @@ -56,6 +56,7 @@ EF7293C553BF1DB45EEEBD49 /* local-social-personas.v1.json in Resources */ = {isa = PBXBuildFile; fileRef = 27D9D40699A01FFB02F30B11 /* local-social-personas.v1.json */; }; F4AF91E71B691FE30C191852 /* TeraAddObservationStartupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44A4460928B20A21A94BECCE /* TeraAddObservationStartupTests.swift */; }; F8C87782857DC930D47E6A90 /* TeraRuntimeResourceTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D01C023E258AC3B3B1C27D /* TeraRuntimeResourceTaskTests.swift */; }; + FB585785DC906CB2AE4AE535 /* TeraBindingPortabilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E711687008866790042792 /* TeraBindingPortabilityTests.swift */; }; FBA9C3463CD1827392AF9599 /* TeraAddSchemaFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8ED2718D5E9E8980ADF3423 /* TeraAddSchemaFixtures.swift */; }; /* End PBXBuildFile section */ @@ -94,6 +95,7 @@ 46DDEE781FDD92A292FD847B /* TeraUserMessagesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraUserMessagesTests.swift; sourceTree = "<group>"; }; 5238B5F236C392C82CEF9838 /* TeraRootShellUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRootShellUITests.swift; sourceTree = "<group>"; }; 57CA176D0A7566A4ADD2CEA4 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; }; + 58E711687008866790042792 /* TeraBindingPortabilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraBindingPortabilityTests.swift; sourceTree = "<group>"; }; 61B6F0B16BFD8F046E9C163F /* TeraMediaStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraMediaStoreTests.swift; sourceTree = "<group>"; }; 61D8C0835D5A1D5C36D80020 /* TeraBackgroundUploadFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraBackgroundUploadFixtures.swift; sourceTree = "<group>"; }; 67ACCE25979BA86C1EC5BFD6 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; }; @@ -196,6 +198,7 @@ D8ED2718D5E9E8980ADF3423 /* TeraAddSchemaFixtures.swift */, 9F22CFABCF27A28408F68D25 /* TeraAddStoreTests.swift */, 61D8C0835D5A1D5C36D80020 /* TeraBackgroundUploadFixtures.swift */, + 58E711687008866790042792 /* TeraBindingPortabilityTests.swift */, 3B808E67E85CD36E49C38B6F /* TeraCalendarFFICompatibilityTests.swift */, 2A506546B0EC1792240C36F7 /* TeraClockTests.swift */, 93A3358AFC23C567DB9CB02F /* TeraErrorRecoveryTests.swift */, @@ -443,6 +446,7 @@ FBA9C3463CD1827392AF9599 /* TeraAddSchemaFixtures.swift in Sources */, A1AD2A380F09994786889AF1 /* TeraAddStoreTests.swift in Sources */, 6F53FF88A29F8DF93F074EB4 /* TeraBackgroundUploadFixtures.swift in Sources */, + FB585785DC906CB2AE4AE535 /* TeraBindingPortabilityTests.swift in Sources */, 772A9B1AD8DE1D2E0969F285 /* TeraCalendarFFICompatibilityTests.swift in Sources */, 70417FB635F06CC9829FB38C /* TeraClockTests.swift in Sources */, D918A664D9A4B9859A1A6503 /* TeraErrorRecoveryTests.swift in Sources */, diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -92,19 +92,19 @@ "sha256": "ad723743c7dec49a87559d72deb6f1d976718693e88cc324291c4d55ce795c5b" }, { - "bytes": 57776, + "bytes": 61732, "path": "source/aarch64-apple-darwin.json", - "sha256": "bd839eef0cc51e2e21958c344f9977d4f6d11d3657124ca4c72b6e4b06c9c9ee" + "sha256": "84ee6f7e6a892b2118be20f451a2bbb6b90e92a48a2e676127f8e75380761600" }, { - "bytes": 57620, + "bytes": 61576, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "ddb3f3eee7b63397186ad493b36eca9e2728f86746e78eded8986bba0efd4058" + "sha256": "3d5fac7931493190aaf9d8c6fac27e0b23ba3a90cabc70b62fc492f80d21fe5e" }, { - "bytes": 57616, + "bytes": 61572, "path": "source/aarch64-apple-ios.json", - "sha256": "166c059b443978162e5c0a73a8259468e8f111b237681a422c6764c1cbec5b23" + "sha256": "01120be5946961acb0184a1c643b1a3f28a49d358188781633271ed29381d4b1" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "dc13a799f881afe4464fdf2ddd3426917df2c183" + "tree": "6f16b615f8c48f08138dba1fc369d91cc9fa9c54" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -174,19 +174,19 @@ "sha256": "ab87eb6e3d4512acc3986120c38988f8e7559ac1781de76d9808e0bae338d1de" }, { - "bytes": 57776, + "bytes": 61732, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "bd839eef0cc51e2e21958c344f9977d4f6d11d3657124ca4c72b6e4b06c9c9ee" + "sha256": "84ee6f7e6a892b2118be20f451a2bbb6b90e92a48a2e676127f8e75380761600" }, { - "bytes": 57620, + "bytes": 61576, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "ddb3f3eee7b63397186ad493b36eca9e2728f86746e78eded8986bba0efd4058" + "sha256": "3d5fac7931493190aaf9d8c6fac27e0b23ba3a90cabc70b62fc492f80d21fe5e" }, { - "bytes": 57616, + "bytes": 61572, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "166c059b443978162e5c0a73a8259468e8f111b237681a422c6764c1cbec5b23" + "sha256": "01120be5946961acb0184a1c643b1a3f28a49d358188781633271ed29381d4b1" } ], "schema": "tera.installed-native-artifacts.v1" diff --git a/TeraFFI/source.lock b/TeraFFI/source.lock @@ -1,7 +1,7 @@ schema = "tera.installed-source.v1" repository = "https://github.com/radrootslabs/tera" -source_tree = "dc13a799f881afe4464fdf2ddd3426917df2c183" -manifest_sha256 = "4f78cfbfc30426566a2a7ac6c923914924dd28533d8090ec4f503d0a5288a2f7" +source_tree = "6f16b615f8c48f08138dba1fc369d91cc9fa9c54" +manifest_sha256 = "b1ec0407b61e560048d3f33ec6b1cb8819b4c7830bf1fb35e08688c45792fbbd" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -511,10 +511,10 @@ "sha256": "d21288656485d0812e1328a65bfccbdd29fe70b44258cc236a036f844cb6b532" }, "Makefile": { - "bytes": 3697, - "git_blob": "5d482a19029ba700ac21a717754f6b3af0d84391", + "bytes": 3934, + "git_blob": "84eeeb038bc0262e65dd9100fc76a9132be01daa", "mode": "100644", - "sha256": "88935753e663790d6b8b7d4f383de1c6df32430abbc506f076149b0289d6452c" + "sha256": "a772f1a6b7a004603c40b2f60b5f8f3914e7bff952cac21c49ea62363fa8d523" }, "TeraFFI/Makefile": { "bytes": 374, @@ -846,6 +846,12 @@ "mode": "100644", "sha256": "48e2fffa93f9a5befd2e0289ae0abf454a0e933bbda917c7864a5f741ded3c83" }, + "core/crates/tera_ffi/tests/binding_portability.rs": { + "bytes": 3321, + "git_blob": "efe7633c6dbf7c53ab62856971e63613e37bc5fa", + "mode": "100644", + "sha256": "71b57b2f29db34118dfcfa7f9fc62ed3444161e8a9284c167272c9a50b863aab" + }, "core/crates/tera_ffi/tests/compatibility.rs": { "bytes": 7548, "git_blob": "0802e9f46437cedd2d51c5b42f11aaa4accf4f37", @@ -1110,11 +1116,89 @@ "mode": "100755", "sha256": "36f5b334f88928c2bb96d5b0f9e560b148c0b4d449696c13ef0d201f1355db3e" }, + "scripts/kotlin-smoke.sh": { + "bytes": 213, + "git_blob": "8c4dc541245664fc0cd744c1fc2a94659bd910cd", + "mode": "100755", + "sha256": "26046966bece8c811a5b3edef191e2ea5b5c4be3187cf65a29f7edbe93cbb487" + }, + "scripts/kotlin_smoke.py": { + "bytes": 9647, + "git_blob": "aa1e9ab3c73c97d67bc27461d0ea178bf0e6a9a0", + "mode": "100644", + "sha256": "33799c94057e9413714918aa7c571953fae3b78af7d1f35d0084dcb42f554e9d" + }, + "scripts/kotlin_smoke/build.gradle.kts": { + "bytes": 1479, + "git_blob": "7177f4bc0581a9c015f2efac8cd1e894b0bf68c0", + "mode": "100644", + "sha256": "3fdeb7127dc05c7ec82169736de2433a0e7ebc772e72a639a234ffc3f8ff8e89" + }, + "scripts/kotlin_smoke/gradle.lockfile": { + "bytes": 4811, + "git_blob": "fbaa4f45ab9fc6a6e74a7a0410caad7c814b1194", + "mode": "100644", + "sha256": "4f8ffa5e91ded49997c336ae43e82e98777452614dc6b90915de53ee11e35468" + }, + "scripts/kotlin_smoke/gradle.properties": { + "bytes": 276, + "git_blob": "ae916c9e5d3a82a00297fbb2212392a73c9d2016", + "mode": "100644", + "sha256": "931efb69eb62a302b3e52e86f0fbe94fa407a40802a14e5962c66fdaccb59c9a" + }, + "scripts/kotlin_smoke/gradle/verification-metadata.xml": { + "bytes": 28546, + "git_blob": "043d566b33ad46e791f61ebfdd25831a00dd3040", + "mode": "100644", + "sha256": "115bd9b18fa65ba24ce14914028a09cf87b0a29830450a122c0bf991898cbf42" + }, + "scripts/kotlin_smoke/gradle/wrapper/gradle-wrapper.jar": { + "bytes": 48462, + "git_blob": "b1b8ef56b44f16b14dc800fa8103a6d89abb526f", + "mode": "100644", + "sha256": "497c8c2a7e5031f6aa847f88104aa80a93532ec32ee17bdb8d1d2f67a194a9c7" + }, + "scripts/kotlin_smoke/gradle/wrapper/gradle-wrapper.properties": { + "bytes": 369, + "git_blob": "d04e650cfc929c1aa87da0364e9ff7eb0980da80", + "mode": "100644", + "sha256": "a9323d49bbf21593027de23a20c6e99f033c7f669bb74e0ed225972287c1343a" + }, + "scripts/kotlin_smoke/gradlew": { + "bytes": 8654, + "git_blob": "b9bb139f790567973216cd313e69ae65789c3754", + "mode": "100755", + "sha256": "ab5c0cad16305af2e619c159c1f58dd68d07fab9c11e36701e109c0277407f7a" + }, + "scripts/kotlin_smoke/settings.gradle.kts": { + "bytes": 276, + "git_blob": "fd265d2d0ef24cc60c9743ecc6970e1aecedfab1", + "mode": "100644", + "sha256": "c79c68bfc632c9b55e7b2da5c39131e62e3530ce414baa5a4f7b589cbca40169" + }, + "scripts/kotlin_smoke/src/test/kotlin/tera/smoke/MediaOwnershipTests.kt": { + "bytes": 4638, + "git_blob": "fa3dd6842dc8ee79e6f6a4dab60d1787bf8ba8a3", + "mode": "100644", + "sha256": "483d9fc19115a63b1ac0e59b8200339450711569e7f95a05b34ebf6525878b75" + }, + "scripts/kotlin_smoke/src/test/kotlin/tera/smoke/ScopeLifecycleTests.kt": { + "bytes": 8959, + "git_blob": "797cac9ef8333394465d90cb26c3375fa4299208", + "mode": "100644", + "sha256": "01164253edfc8186d3ffb1aec19aef2f689b0ca448f9eac299aa1082f70c299e" + }, + "scripts/kotlin_smoke/src/test/kotlin/tera/smoke/SmokeFixture.kt": { + "bytes": 4450, + "git_blob": "f2e6816718c74f99e50317fefb8e4b99ad9eef6b", + "mode": "100644", + "sha256": "421a424616c69424e470548fb5fdae5049c140a6e5ce60920ea82e7e4bf8c49b" + }, "scripts/legacy_identifiers.py": { - "bytes": 9237, - "git_blob": "a52415f3391d69ea278673e7531faa4f38814587", + "bytes": 9279, + "git_blob": "30789f03dd290c3aab72ee266747ce660a466ea8", "mode": "100644", - "sha256": "743736ae176e84c7c46ba53bebe23f6435b5969d310bb57562b636c435ba8d2b" + "sha256": "e7ba8cbd3c07bdf5f2ac16afb883af397969b3a668b59b8769b1e64522bb0f28" }, "scripts/linux-shared-rust.sh": { "bytes": 1313, @@ -1177,10 +1261,10 @@ "sha256": "1803572abdb2e22bedd275724edae5f09c523b132bc59086aacca0f5183905d0" }, "scripts/swift-quality.sh": { - "bytes": 1989, - "git_blob": "1dc67baa1ccdb27e2816ca139aa0cb177bb40806", + "bytes": 2046, + "git_blob": "cd574c9e901a596142e8785828f5cb33cc4f0f3e", "mode": "100755", - "sha256": "63df08c308431557f4ca570d81f63167e5c02c084293c39fe877a66ff6e57280" + "sha256": "d876131049757d241f7490c16992224110cdcd1c78a40a76a3b2d1a420cd2aeb" }, "scripts/test_app_dependency_graph.py": { "bytes": 4007, @@ -1206,11 +1290,17 @@ "mode": "100644", "sha256": "707cf2cfc9de12e7409247c875289efc4a7b08c6df845ea10c337b84bb98e39b" }, + "scripts/test_kotlin_smoke.py": { + "bytes": 3606, + "git_blob": "b2f1cd37ebde5752b4545ab54f56c71d0fbf984c", + "mode": "100644", + "sha256": "9f0945240fa9cd33489d1f8b7bb0c0450ee91079ca2beb2b024164e4f5cfd71a" + }, "scripts/test_legacy_identifiers.py": { - "bytes": 5760, - "git_blob": "2822ae9309e539dd73ae19167f723a7014576408", + "bytes": 6406, + "git_blob": "8c772bf4e1cd982ab67da0c5a78fe1d2b13ac905", "mode": "100644", - "sha256": "4305514195999645f6e29e069f348e54fbd170b06a81754658f7c65dd4c51b64" + "sha256": "256f4358edcb861f3819b1b2159b7278bb88fff6b9b9f460871f4c8ecb6f76ea" }, "scripts/test_local_social_fixture.py": { "bytes": 50487, @@ -1231,10 +1321,10 @@ "sha256": "9073f8edaa80c71f7c1f718c4dcff954a30ac42218fbd19378803ecfb9b3b33f" }, "scripts/verify-package-contract.sh": { - "bytes": 1427, - "git_blob": "5b414baa6fad6b1308b028ac02670817bcace5aa", + "bytes": 1460, + "git_blob": "335f5ec00f343bf5dc984a117f365273949d34e2", "mode": "100755", - "sha256": "ba4d48ce7f3c31340e527a2e1c832818e007c8ccfc7e5493916d98a1ae855ac7" + "sha256": "eb8c7dd6eaa04fd72c876925d3caadeddfd90f2e18f67597df57064ac2fafa1e" }, "scripts/xcode.sh": { "bytes": 21183, @@ -1243,13 +1333,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 113352, - "git_blob": "73e20920888ea9005dc4d0411e9184584358c7fb", + "bytes": 113480, + "git_blob": "bbe6a3c6766726354da3067d0b26ff2ae10f7dd9", "mode": "100644", - "sha256": "06dc7dc511a27bae407721461d0372be3c7eba2689147aab5c64a4d975731f94" + "sha256": "a99d49b2c0c6e074f27e6ff04d6193a877abde3b689c442a260f8a093d10775b" } }, "policy": "staged_inputs", - "tree": "dc13a799f881afe4464fdf2ddd3426917df2c183" + "tree": "6f16b615f8c48f08138dba1fc369d91cc9fa9c54" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -507,10 +507,10 @@ "sha256": "d21288656485d0812e1328a65bfccbdd29fe70b44258cc236a036f844cb6b532" }, "Makefile": { - "bytes": 3697, - "git_blob": "5d482a19029ba700ac21a717754f6b3af0d84391", + "bytes": 3934, + "git_blob": "84eeeb038bc0262e65dd9100fc76a9132be01daa", "mode": "100644", - "sha256": "88935753e663790d6b8b7d4f383de1c6df32430abbc506f076149b0289d6452c" + "sha256": "a772f1a6b7a004603c40b2f60b5f8f3914e7bff952cac21c49ea62363fa8d523" }, "TeraFFI/Makefile": { "bytes": 374, @@ -842,6 +842,12 @@ "mode": "100644", "sha256": "48e2fffa93f9a5befd2e0289ae0abf454a0e933bbda917c7864a5f741ded3c83" }, + "core/crates/tera_ffi/tests/binding_portability.rs": { + "bytes": 3321, + "git_blob": "efe7633c6dbf7c53ab62856971e63613e37bc5fa", + "mode": "100644", + "sha256": "71b57b2f29db34118dfcfa7f9fc62ed3444161e8a9284c167272c9a50b863aab" + }, "core/crates/tera_ffi/tests/compatibility.rs": { "bytes": 7548, "git_blob": "0802e9f46437cedd2d51c5b42f11aaa4accf4f37", @@ -1106,11 +1112,89 @@ "mode": "100755", "sha256": "36f5b334f88928c2bb96d5b0f9e560b148c0b4d449696c13ef0d201f1355db3e" }, + "scripts/kotlin-smoke.sh": { + "bytes": 213, + "git_blob": "8c4dc541245664fc0cd744c1fc2a94659bd910cd", + "mode": "100755", + "sha256": "26046966bece8c811a5b3edef191e2ea5b5c4be3187cf65a29f7edbe93cbb487" + }, + "scripts/kotlin_smoke.py": { + "bytes": 9647, + "git_blob": "aa1e9ab3c73c97d67bc27461d0ea178bf0e6a9a0", + "mode": "100644", + "sha256": "33799c94057e9413714918aa7c571953fae3b78af7d1f35d0084dcb42f554e9d" + }, + "scripts/kotlin_smoke/build.gradle.kts": { + "bytes": 1479, + "git_blob": "7177f4bc0581a9c015f2efac8cd1e894b0bf68c0", + "mode": "100644", + "sha256": "3fdeb7127dc05c7ec82169736de2433a0e7ebc772e72a639a234ffc3f8ff8e89" + }, + "scripts/kotlin_smoke/gradle.lockfile": { + "bytes": 4811, + "git_blob": "fbaa4f45ab9fc6a6e74a7a0410caad7c814b1194", + "mode": "100644", + "sha256": "4f8ffa5e91ded49997c336ae43e82e98777452614dc6b90915de53ee11e35468" + }, + "scripts/kotlin_smoke/gradle.properties": { + "bytes": 276, + "git_blob": "ae916c9e5d3a82a00297fbb2212392a73c9d2016", + "mode": "100644", + "sha256": "931efb69eb62a302b3e52e86f0fbe94fa407a40802a14e5962c66fdaccb59c9a" + }, + "scripts/kotlin_smoke/gradle/verification-metadata.xml": { + "bytes": 28546, + "git_blob": "043d566b33ad46e791f61ebfdd25831a00dd3040", + "mode": "100644", + "sha256": "115bd9b18fa65ba24ce14914028a09cf87b0a29830450a122c0bf991898cbf42" + }, + "scripts/kotlin_smoke/gradle/wrapper/gradle-wrapper.jar": { + "bytes": 48462, + "git_blob": "b1b8ef56b44f16b14dc800fa8103a6d89abb526f", + "mode": "100644", + "sha256": "497c8c2a7e5031f6aa847f88104aa80a93532ec32ee17bdb8d1d2f67a194a9c7" + }, + "scripts/kotlin_smoke/gradle/wrapper/gradle-wrapper.properties": { + "bytes": 369, + "git_blob": "d04e650cfc929c1aa87da0364e9ff7eb0980da80", + "mode": "100644", + "sha256": "a9323d49bbf21593027de23a20c6e99f033c7f669bb74e0ed225972287c1343a" + }, + "scripts/kotlin_smoke/gradlew": { + "bytes": 8654, + "git_blob": "b9bb139f790567973216cd313e69ae65789c3754", + "mode": "100755", + "sha256": "ab5c0cad16305af2e619c159c1f58dd68d07fab9c11e36701e109c0277407f7a" + }, + "scripts/kotlin_smoke/settings.gradle.kts": { + "bytes": 276, + "git_blob": "fd265d2d0ef24cc60c9743ecc6970e1aecedfab1", + "mode": "100644", + "sha256": "c79c68bfc632c9b55e7b2da5c39131e62e3530ce414baa5a4f7b589cbca40169" + }, + "scripts/kotlin_smoke/src/test/kotlin/tera/smoke/MediaOwnershipTests.kt": { + "bytes": 4638, + "git_blob": "fa3dd6842dc8ee79e6f6a4dab60d1787bf8ba8a3", + "mode": "100644", + "sha256": "483d9fc19115a63b1ac0e59b8200339450711569e7f95a05b34ebf6525878b75" + }, + "scripts/kotlin_smoke/src/test/kotlin/tera/smoke/ScopeLifecycleTests.kt": { + "bytes": 8959, + "git_blob": "797cac9ef8333394465d90cb26c3375fa4299208", + "mode": "100644", + "sha256": "01164253edfc8186d3ffb1aec19aef2f689b0ca448f9eac299aa1082f70c299e" + }, + "scripts/kotlin_smoke/src/test/kotlin/tera/smoke/SmokeFixture.kt": { + "bytes": 4450, + "git_blob": "f2e6816718c74f99e50317fefb8e4b99ad9eef6b", + "mode": "100644", + "sha256": "421a424616c69424e470548fb5fdae5049c140a6e5ce60920ea82e7e4bf8c49b" + }, "scripts/legacy_identifiers.py": { - "bytes": 9237, - "git_blob": "a52415f3391d69ea278673e7531faa4f38814587", + "bytes": 9279, + "git_blob": "30789f03dd290c3aab72ee266747ce660a466ea8", "mode": "100644", - "sha256": "743736ae176e84c7c46ba53bebe23f6435b5969d310bb57562b636c435ba8d2b" + "sha256": "e7ba8cbd3c07bdf5f2ac16afb883af397969b3a668b59b8769b1e64522bb0f28" }, "scripts/linux-shared-rust.sh": { "bytes": 1313, @@ -1173,10 +1257,10 @@ "sha256": "1803572abdb2e22bedd275724edae5f09c523b132bc59086aacca0f5183905d0" }, "scripts/swift-quality.sh": { - "bytes": 1989, - "git_blob": "1dc67baa1ccdb27e2816ca139aa0cb177bb40806", + "bytes": 2046, + "git_blob": "cd574c9e901a596142e8785828f5cb33cc4f0f3e", "mode": "100755", - "sha256": "63df08c308431557f4ca570d81f63167e5c02c084293c39fe877a66ff6e57280" + "sha256": "d876131049757d241f7490c16992224110cdcd1c78a40a76a3b2d1a420cd2aeb" }, "scripts/test_app_dependency_graph.py": { "bytes": 4007, @@ -1202,11 +1286,17 @@ "mode": "100644", "sha256": "707cf2cfc9de12e7409247c875289efc4a7b08c6df845ea10c337b84bb98e39b" }, + "scripts/test_kotlin_smoke.py": { + "bytes": 3606, + "git_blob": "b2f1cd37ebde5752b4545ab54f56c71d0fbf984c", + "mode": "100644", + "sha256": "9f0945240fa9cd33489d1f8b7bb0c0450ee91079ca2beb2b024164e4f5cfd71a" + }, "scripts/test_legacy_identifiers.py": { - "bytes": 5760, - "git_blob": "2822ae9309e539dd73ae19167f723a7014576408", + "bytes": 6406, + "git_blob": "8c772bf4e1cd982ab67da0c5a78fe1d2b13ac905", "mode": "100644", - "sha256": "4305514195999645f6e29e069f348e54fbd170b06a81754658f7c65dd4c51b64" + "sha256": "256f4358edcb861f3819b1b2159b7278bb88fff6b9b9f460871f4c8ecb6f76ea" }, "scripts/test_local_social_fixture.py": { "bytes": 50487, @@ -1227,10 +1317,10 @@ "sha256": "9073f8edaa80c71f7c1f718c4dcff954a30ac42218fbd19378803ecfb9b3b33f" }, "scripts/verify-package-contract.sh": { - "bytes": 1427, - "git_blob": "5b414baa6fad6b1308b028ac02670817bcace5aa", + "bytes": 1460, + "git_blob": "335f5ec00f343bf5dc984a117f365273949d34e2", "mode": "100755", - "sha256": "ba4d48ce7f3c31340e527a2e1c832818e007c8ccfc7e5493916d98a1ae855ac7" + "sha256": "eb8c7dd6eaa04fd72c876925d3caadeddfd90f2e18f67597df57064ac2fafa1e" }, "scripts/xcode.sh": { "bytes": 21183, @@ -1239,13 +1329,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 113352, - "git_blob": "73e20920888ea9005dc4d0411e9184584358c7fb", + "bytes": 113480, + "git_blob": "bbe6a3c6766726354da3067d0b26ff2ae10f7dd9", "mode": "100644", - "sha256": "06dc7dc511a27bae407721461d0372be3c7eba2689147aab5c64a4d975731f94" + "sha256": "a99d49b2c0c6e074f27e6ff04d6193a877abde3b689c442a260f8a093d10775b" } }, "policy": "staged_inputs", - "tree": "dc13a799f881afe4464fdf2ddd3426917df2c183" + "tree": "6f16b615f8c48f08138dba1fc369d91cc9fa9c54" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -507,10 +507,10 @@ "sha256": "d21288656485d0812e1328a65bfccbdd29fe70b44258cc236a036f844cb6b532" }, "Makefile": { - "bytes": 3697, - "git_blob": "5d482a19029ba700ac21a717754f6b3af0d84391", + "bytes": 3934, + "git_blob": "84eeeb038bc0262e65dd9100fc76a9132be01daa", "mode": "100644", - "sha256": "88935753e663790d6b8b7d4f383de1c6df32430abbc506f076149b0289d6452c" + "sha256": "a772f1a6b7a004603c40b2f60b5f8f3914e7bff952cac21c49ea62363fa8d523" }, "TeraFFI/Makefile": { "bytes": 374, @@ -842,6 +842,12 @@ "mode": "100644", "sha256": "48e2fffa93f9a5befd2e0289ae0abf454a0e933bbda917c7864a5f741ded3c83" }, + "core/crates/tera_ffi/tests/binding_portability.rs": { + "bytes": 3321, + "git_blob": "efe7633c6dbf7c53ab62856971e63613e37bc5fa", + "mode": "100644", + "sha256": "71b57b2f29db34118dfcfa7f9fc62ed3444161e8a9284c167272c9a50b863aab" + }, "core/crates/tera_ffi/tests/compatibility.rs": { "bytes": 7548, "git_blob": "0802e9f46437cedd2d51c5b42f11aaa4accf4f37", @@ -1106,11 +1112,89 @@ "mode": "100755", "sha256": "36f5b334f88928c2bb96d5b0f9e560b148c0b4d449696c13ef0d201f1355db3e" }, + "scripts/kotlin-smoke.sh": { + "bytes": 213, + "git_blob": "8c4dc541245664fc0cd744c1fc2a94659bd910cd", + "mode": "100755", + "sha256": "26046966bece8c811a5b3edef191e2ea5b5c4be3187cf65a29f7edbe93cbb487" + }, + "scripts/kotlin_smoke.py": { + "bytes": 9647, + "git_blob": "aa1e9ab3c73c97d67bc27461d0ea178bf0e6a9a0", + "mode": "100644", + "sha256": "33799c94057e9413714918aa7c571953fae3b78af7d1f35d0084dcb42f554e9d" + }, + "scripts/kotlin_smoke/build.gradle.kts": { + "bytes": 1479, + "git_blob": "7177f4bc0581a9c015f2efac8cd1e894b0bf68c0", + "mode": "100644", + "sha256": "3fdeb7127dc05c7ec82169736de2433a0e7ebc772e72a639a234ffc3f8ff8e89" + }, + "scripts/kotlin_smoke/gradle.lockfile": { + "bytes": 4811, + "git_blob": "fbaa4f45ab9fc6a6e74a7a0410caad7c814b1194", + "mode": "100644", + "sha256": "4f8ffa5e91ded49997c336ae43e82e98777452614dc6b90915de53ee11e35468" + }, + "scripts/kotlin_smoke/gradle.properties": { + "bytes": 276, + "git_blob": "ae916c9e5d3a82a00297fbb2212392a73c9d2016", + "mode": "100644", + "sha256": "931efb69eb62a302b3e52e86f0fbe94fa407a40802a14e5962c66fdaccb59c9a" + }, + "scripts/kotlin_smoke/gradle/verification-metadata.xml": { + "bytes": 28546, + "git_blob": "043d566b33ad46e791f61ebfdd25831a00dd3040", + "mode": "100644", + "sha256": "115bd9b18fa65ba24ce14914028a09cf87b0a29830450a122c0bf991898cbf42" + }, + "scripts/kotlin_smoke/gradle/wrapper/gradle-wrapper.jar": { + "bytes": 48462, + "git_blob": "b1b8ef56b44f16b14dc800fa8103a6d89abb526f", + "mode": "100644", + "sha256": "497c8c2a7e5031f6aa847f88104aa80a93532ec32ee17bdb8d1d2f67a194a9c7" + }, + "scripts/kotlin_smoke/gradle/wrapper/gradle-wrapper.properties": { + "bytes": 369, + "git_blob": "d04e650cfc929c1aa87da0364e9ff7eb0980da80", + "mode": "100644", + "sha256": "a9323d49bbf21593027de23a20c6e99f033c7f669bb74e0ed225972287c1343a" + }, + "scripts/kotlin_smoke/gradlew": { + "bytes": 8654, + "git_blob": "b9bb139f790567973216cd313e69ae65789c3754", + "mode": "100755", + "sha256": "ab5c0cad16305af2e619c159c1f58dd68d07fab9c11e36701e109c0277407f7a" + }, + "scripts/kotlin_smoke/settings.gradle.kts": { + "bytes": 276, + "git_blob": "fd265d2d0ef24cc60c9743ecc6970e1aecedfab1", + "mode": "100644", + "sha256": "c79c68bfc632c9b55e7b2da5c39131e62e3530ce414baa5a4f7b589cbca40169" + }, + "scripts/kotlin_smoke/src/test/kotlin/tera/smoke/MediaOwnershipTests.kt": { + "bytes": 4638, + "git_blob": "fa3dd6842dc8ee79e6f6a4dab60d1787bf8ba8a3", + "mode": "100644", + "sha256": "483d9fc19115a63b1ac0e59b8200339450711569e7f95a05b34ebf6525878b75" + }, + "scripts/kotlin_smoke/src/test/kotlin/tera/smoke/ScopeLifecycleTests.kt": { + "bytes": 8959, + "git_blob": "797cac9ef8333394465d90cb26c3375fa4299208", + "mode": "100644", + "sha256": "01164253edfc8186d3ffb1aec19aef2f689b0ca448f9eac299aa1082f70c299e" + }, + "scripts/kotlin_smoke/src/test/kotlin/tera/smoke/SmokeFixture.kt": { + "bytes": 4450, + "git_blob": "f2e6816718c74f99e50317fefb8e4b99ad9eef6b", + "mode": "100644", + "sha256": "421a424616c69424e470548fb5fdae5049c140a6e5ce60920ea82e7e4bf8c49b" + }, "scripts/legacy_identifiers.py": { - "bytes": 9237, - "git_blob": "a52415f3391d69ea278673e7531faa4f38814587", + "bytes": 9279, + "git_blob": "30789f03dd290c3aab72ee266747ce660a466ea8", "mode": "100644", - "sha256": "743736ae176e84c7c46ba53bebe23f6435b5969d310bb57562b636c435ba8d2b" + "sha256": "e7ba8cbd3c07bdf5f2ac16afb883af397969b3a668b59b8769b1e64522bb0f28" }, "scripts/linux-shared-rust.sh": { "bytes": 1313, @@ -1173,10 +1257,10 @@ "sha256": "1803572abdb2e22bedd275724edae5f09c523b132bc59086aacca0f5183905d0" }, "scripts/swift-quality.sh": { - "bytes": 1989, - "git_blob": "1dc67baa1ccdb27e2816ca139aa0cb177bb40806", + "bytes": 2046, + "git_blob": "cd574c9e901a596142e8785828f5cb33cc4f0f3e", "mode": "100755", - "sha256": "63df08c308431557f4ca570d81f63167e5c02c084293c39fe877a66ff6e57280" + "sha256": "d876131049757d241f7490c16992224110cdcd1c78a40a76a3b2d1a420cd2aeb" }, "scripts/test_app_dependency_graph.py": { "bytes": 4007, @@ -1202,11 +1286,17 @@ "mode": "100644", "sha256": "707cf2cfc9de12e7409247c875289efc4a7b08c6df845ea10c337b84bb98e39b" }, + "scripts/test_kotlin_smoke.py": { + "bytes": 3606, + "git_blob": "b2f1cd37ebde5752b4545ab54f56c71d0fbf984c", + "mode": "100644", + "sha256": "9f0945240fa9cd33489d1f8b7bb0c0450ee91079ca2beb2b024164e4f5cfd71a" + }, "scripts/test_legacy_identifiers.py": { - "bytes": 5760, - "git_blob": "2822ae9309e539dd73ae19167f723a7014576408", + "bytes": 6406, + "git_blob": "8c772bf4e1cd982ab67da0c5a78fe1d2b13ac905", "mode": "100644", - "sha256": "4305514195999645f6e29e069f348e54fbd170b06a81754658f7c65dd4c51b64" + "sha256": "256f4358edcb861f3819b1b2159b7278bb88fff6b9b9f460871f4c8ecb6f76ea" }, "scripts/test_local_social_fixture.py": { "bytes": 50487, @@ -1227,10 +1317,10 @@ "sha256": "9073f8edaa80c71f7c1f718c4dcff954a30ac42218fbd19378803ecfb9b3b33f" }, "scripts/verify-package-contract.sh": { - "bytes": 1427, - "git_blob": "5b414baa6fad6b1308b028ac02670817bcace5aa", + "bytes": 1460, + "git_blob": "335f5ec00f343bf5dc984a117f365273949d34e2", "mode": "100755", - "sha256": "ba4d48ce7f3c31340e527a2e1c832818e007c8ccfc7e5493916d98a1ae855ac7" + "sha256": "eb8c7dd6eaa04fd72c876925d3caadeddfd90f2e18f67597df57064ac2fafa1e" }, "scripts/xcode.sh": { "bytes": 21183, @@ -1239,13 +1329,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 113352, - "git_blob": "73e20920888ea9005dc4d0411e9184584358c7fb", + "bytes": 113480, + "git_blob": "bbe6a3c6766726354da3067d0b26ff2ae10f7dd9", "mode": "100644", - "sha256": "06dc7dc511a27bae407721461d0372be3c7eba2689147aab5c64a4d975731f94" + "sha256": "a99d49b2c0c6e074f27e6ff04d6193a877abde3b689c442a260f8a093d10775b" } }, "policy": "staged_inputs", - "tree": "dc13a799f881afe4464fdf2ddd3426917df2c183" + "tree": "6f16b615f8c48f08138dba1fc369d91cc9fa9c54" } } diff --git a/TeraTests/TeraBindingPortabilityTests.swift b/TeraTests/TeraBindingPortabilityTests.swift @@ -0,0 +1,71 @@ +@testable import TeraApp +import TeraKitBindings +import XCTest + +final class TeraBindingPortabilityTests: XCTestCase { + func testActualCallbacksPreserveUnsignedContextBoundariesAndScope() async throws { + let fixture = try MediaOwnershipFixture() + defer { fixture.remove() } + let runtime = try await fixture.runtime() + let pair = AsyncStream.makeStream(of: TeraRuntimeChange.self, bufferingPolicy: .bufferingNewest(8)) + let observer = TeraGeneratedRuntimeObserver(continuation: pair.continuation) + let handle = try runtime.subscribeChanges(observer: observer) + defer { handle.unsubscribe(); observer.finish() } + let initial = try await next(pair.stream) + XCTAssertEqual(initial.kind, .initial) + XCTAssertEqual(initial.epoch.count, 32) + for (index, generation) in [UInt64(0), UInt64(1), UInt64(Int64.max) + 1, UInt64.max].enumerated() { + let context = context(generation: generation) + _ = try await runtime.phase1RefreshToday(context: context, nowUnixS: 1_800_000_000, update: .incremental) + let changed = try await next(pair.stream) + XCTAssertEqual(changed.scope.publicKey, initial.scope.publicKey) + XCTAssertEqual(changed.scope.sourceGeneration, initial.scope.sourceGeneration) + XCTAssertEqual(changed.scope.context?.id, context.id) + XCTAssertEqual(changed.scope.context?.generation, generation) + XCTAssertEqual(changed.epoch, initial.epoch) + XCTAssertEqual(changed.revision.rawValue, UInt64(index + 1)) + } + _ = try await runtime.shutdown() + } + + func testInvalidContextErrorsCrossTheGeneratedBoundaryWithoutAnInvalidation() async throws { + let fixture = try MediaOwnershipFixture() + defer { fixture.remove() } + let runtime = try await fixture.runtime() + let pair = AsyncStream.makeStream(of: TeraRuntimeChange.self, bufferingPolicy: .bufferingNewest(8)) + let observer = TeraGeneratedRuntimeObserver(continuation: pair.continuation) + let handle = try runtime.subscribeChanges(observer: observer) + defer { handle.unsubscribe(); observer.finish() } + _ = try await next(pair.stream) + var unsupported = context(generation: 1) + unsupported.schemaVersion = .max + var invalid = context(generation: 1) + invalid.id = "" + for (context, code) in [(invalid, "invalid_local_network"), (unsupported, "unsupported_schema_version")] { + do { + _ = try await runtime.phase1RefreshToday(context: context, nowUnixS: 1_800_000_000, update: .incremental) + XCTFail("Invalid context must not be admitted") + } catch let TeraAppError.Failure(report) { + XCTAssertEqual(report.code, code) + } + } + try runtime.configurePublicRelays(writableRelays: ["wss://write.example"]) + let changed = try await next(pair.stream) + XCTAssertEqual(changed.kind, .relay) + _ = try await runtime.shutdown() + } + + private func context(generation: UInt64) -> FfiLocalNetworkRecord { + FfiLocalNetworkRecord(schemaVersion: 1, id: "nearby", label: "Nearby", relayUrls: ["wss://relay.example"], locality: nil, followedAuthors: [], generation: generation) + } + + private func next(_ stream: AsyncStream<TeraRuntimeChange>) async throws -> TeraRuntimeChange { + let value = await withTaskGroup(of: TeraRuntimeChange?.self) { group in + group.addTask { await stream.first { _ in true } } + group.addTask { try? await Task.sleep(for: .seconds(5)); return nil } + defer { group.cancelAll() } + return await group.next() ?? nil + } + return try XCTUnwrap(value, "Native callback deadline") + } +} diff --git a/core/crates/tera_ffi/tests/binding_portability.rs b/core/crates/tera_ffi/tests/binding_portability.rs @@ -0,0 +1,103 @@ +use std::{sync::mpsc, time::Duration}; + +use tera_ffi::{ + FfiInvalidationRevision, FfiLocalNetworkRecord, FfiRuntimeChangeKind, FfiRuntimeChangeRecord, + FfiTodayProjectionUpdate, TeraRuntimeObserver, +}; + +mod support; + +struct Observer(mpsc::Sender<FfiRuntimeChangeRecord>); + +impl TeraRuntimeObserver for Observer { + fn on_change(&self, change: FfiRuntimeChangeRecord) { + let _ = self.0.send(change); + } +} + +fn context(generation: u64) -> FfiLocalNetworkRecord { + FfiLocalNetworkRecord { + schema_version: 1, + id: "nearby".into(), + label: "Nearby".into(), + relay_urls: vec!["wss://relay.example".into()], + locality: None, + followed_authors: vec![], + generation, + } +} + +#[tokio::test] +async fn actual_context_callbacks_preserve_unsigned_boundaries_and_runtime_scope() { + let (_root, runtime) = support::runtime().await; + let (sender, receiver) = mpsc::channel(); + let handle = runtime + .subscribe_changes(Box::new(Observer(sender))) + .unwrap(); + let initial = receiver.recv_timeout(Duration::from_secs(5)).unwrap(); + assert_eq!(initial.scope.public_key, support::PUBLIC_KEY); + assert_eq!(initial.scope.source_generation, support::GENERATION); + for (index, generation) in [0, 1, (i64::MAX as u64) + 1, u64::MAX] + .into_iter() + .enumerate() + { + let context = context(generation); + runtime + .phase1_refresh_today( + context.clone(), + 1_800_000_000, + FfiTodayProjectionUpdate::Incremental, + ) + .await + .unwrap(); + let changed = receiver.recv_timeout(Duration::from_secs(5)).unwrap(); + assert_eq!(changed.scope.context, Some(context)); + assert_eq!(changed.epoch, initial.epoch); + assert_eq!( + changed.revision, + FfiInvalidationRevision::Current { + value: index as u64 + 1 + } + ); + } + handle.unsubscribe(); + assert_eq!(runtime.shutdown().await.unwrap().state, "closed"); +} + +#[tokio::test] +async fn invalid_contexts_fail_before_emitting_a_callback() { + let (_root, runtime) = support::runtime().await; + let (sender, receiver) = mpsc::channel(); + let handle = runtime + .subscribe_changes(Box::new(Observer(sender))) + .unwrap(); + receiver.recv_timeout(Duration::from_secs(5)).unwrap(); + let mut unsupported = context(1); + unsupported.schema_version = u16::MAX; + let mut invalid = context(1); + invalid.id.clear(); + for (context, expected) in [ + (invalid, "invalid_local_network"), + (unsupported, "unsupported_schema_version"), + ] { + let error = runtime + .phase1_refresh_today( + context, + 1_800_000_000, + FfiTodayProjectionUpdate::Incremental, + ) + .await + .unwrap_err(); + assert_eq!(error.report().code, expected); + } + runtime + .configure_public_relays(vec!["wss://write.example".into()]) + .unwrap(); + assert_eq!( + receiver.recv_timeout(Duration::from_secs(5)).unwrap().kind, + FfiRuntimeChangeKind::Relay + ); + assert!(receiver.try_recv().is_err()); + handle.unsubscribe(); + runtime.shutdown().await.unwrap(); +} diff --git a/project.yml b/project.yml @@ -74,6 +74,7 @@ targets: - path: TeraTests/TeraMediaStoreTests.swift - path: TeraTests/TeraMediaFileFixture.swift - path: TeraTests/TeraMediaOwnershipFFITests.swift + - path: TeraTests/TeraBindingPortabilityTests.swift - path: TeraTests/TeraOpenedMediaCloseFixture.swift - path: TeraTests/TeraOpenedMediaTests.swift - path: TeraTests/TeraAddStoreTests.swift diff --git a/release/provenance.json b/release/provenance.json @@ -2,11 +2,11 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "ab87eb6e3d4512acc3986120c38988f8e7559ac1781de76d9808e0bae338d1de", - "ffi_provenance_sha256": "4f78cfbfc30426566a2a7ac6c923914924dd28533d8090ec4f503d0a5288a2f7", + "ffi_provenance_sha256": "b1ec0407b61e560048d3f33ec6b1cb8819b4c7830bf1fb35e08688c45792fbbd", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "6b5ce897d5273290febc8b831663e12dea759cb0084f182ba7949e1c593fc410", - "xcode_project_sha256": "d3f7dcba94b3ab8a35c5f7d8a641eca4b6f2529147cc3dbe086b591105af0aff" + "xcode_project_sha256": "4e668fefb0f9c1ce1a0cfff65c96360cbddc935cb45ceeb405c7507613bc35e0" }, "disposition": "unsigned", "platforms": [ @@ -22,7 +22,7 @@ "lib_revision": "ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb", "source_date_epoch": 1787871027, "swift_package_lock_sha256": "94ae067a374726cdaf6b4ca0a5e44663c57fcdc5334060c5ffef5e79cfbf04c0", - "tera_ffi_source_tree": "dc13a799f881afe4464fdf2ddd3426917df2c183", + "tera_ffi_source_tree": "6f16b615f8c48f08138dba1fc369d91cc9fa9c54", "xcode_package_lock_sha256": "c7f41934ea25f7a287bdc4f3a6ecabbf09a3a0bdd0f5a3e58183f355ca814096" }, "version": "0.1.0-alpha" diff --git a/scripts/kotlin-smoke.sh b/scripts/kotlin-smoke.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -eu + +repo_root=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +exec uv run --offline --frozen --project "$repo_root/scripts/persona-verifier" \ + python "$repo_root/scripts/kotlin_smoke.py" "$@" diff --git a/scripts/kotlin_smoke.py b/scripts/kotlin_smoke.py @@ -0,0 +1,271 @@ +"""Run the test-only generated Kotlin/JNA boundary on the governed native host.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import platform +import re +import subprocess +import sys +import xml.etree.ElementTree as ET +from pathlib import Path +from typing import Any + +import ffi_artifacts as artifacts +import ffi_installed as installed +import ffi_provenance as provenance +import ffi_source as source + +HARNESS = "scripts/kotlin_smoke" +HOST = "aarch64-apple-darwin" +LIBRARY = f"native/{HOST}/libtera_ffi.dylib" +WRAPPER_HASHES = { + "gradlew": "ab5c0cad16305af2e619c159c1f58dd68d07fab9c11e36701e109c0277407f7a", + "gradle/wrapper/gradle-wrapper.jar": "497c8c2a7e5031f6aa847f88104aa80a93532ec32ee17bdb8d1d2f67a194a9c7", +} +EXPECTED_CASES = { + "tera.smoke.ScopeLifecycleTests/scopeAndUnsignedContextsCrossRustWithoutSignedNarrowing", + "tera.smoke.ScopeLifecycleTests/cancelledCloseWaitRetainsNativeCallbackDrainAndClosedAdmission", + "tera.smoke.ScopeLifecycleTests/independentSubscriptionDisposalStopsOnlyItsObserver", + "tera.smoke.ScopeLifecycleTests/errorRecoveryAndProtectedDataFailureRemainTyped", + "tera.smoke.ScopeLifecycleTests/cancellationHandleKeepsIdentityAndDisposesIdempotently", + "tera.smoke.MediaOwnershipTests/callerCloseBeforeGeneratedConversionPreservesTheAdmittedBytes", + "tera.smoke.MediaOwnershipTests/recycledCallerSlotAndDisposedSourceWrapperCannotSubstituteBytes", + "tera.smoke.MediaOwnershipTests/admissionRejectsInvalidDescriptorsAndWrongTypesSynchronously", +} + + +def require(condition: bool, message: str) -> None: + if not condition: + raise source.ProvenanceError(message) + + +def output_root(root: Path) -> Path: + require( + bool(os.environ.get("EXT_BUILD_RUN_ACTIVE")), "Kotlin smoke requires extbuild" + ) + project = os.environ.get("EXT_BUILD_PROJECT_DIR") + require(bool(project), "Kotlin smoke output routing is absent") + return provenance.output_path( + root, HOST, str(Path(project) / "target/kotlin_smoke") + ) + + +def verify_host(root: Path, *, offline: bool) -> str: + require( + platform.system() == "Darwin" and platform.machine() == "arm64", + "Kotlin smoke host is unsupported", + ) + java = subprocess.check_output(["java", "--version"], text=True, timeout=30).strip() + require( + bool(re.match(r"(?:openjdk|java) 21[. ]", java)), + "Kotlin smoke requires the discovered JDK 21", + ) + cache = os.environ.get("GRADLE_USER_HOME") + require(bool(cache) and Path(cache).is_absolute(), "Gradle cache routing is absent") + require( + not Path(cache).resolve().is_relative_to(root), + "Gradle cache is inside the source repository", + ) + for name, digest in WRAPPER_HASHES.items(): + require( + artifacts.file_record(root / HARNESS, name)["sha256"] == digest, + "Gradle wrapper bytes differ", + ) + properties = ( + root / HARNESS / "gradle/wrapper/gradle-wrapper.properties" + ).read_text() + require( + "distributionUrl=https\\://services.gradle.org/distributions/gradle-9.5.0-bin.zip\n" + in properties + and "distributionSha256Sum=553c78f50dafcd54d65b9a444649057857469edf836431389695608536d6b746\n" + in properties, + "Gradle distribution pin differs", + ) + if offline: + # --offline governs Gradle dependency resolution; the wrapper can still + # download its distribution on first use. Require its completed install. + distributions = Path(cache) / "wrapper/dists/gradle-9.5.0-bin" + require( + any( + p.is_file() and (p.parent / "gradle-9.5.0/bin/gradle").is_file() + for p in distributions.glob("*/gradle-9.5.0-bin.zip.ok") + ), + "Gradle 9.5.0 is not bootstrapped; run make kotlin-smoke-bootstrap", + ) + return java + + +def native_input( + root: Path, output: Path +) -> tuple[dict[str, Any], Path, dict[str, Any]]: + manifest = installed.check(root) + candidate = manifest["candidate"] + tree = candidate["source"]["tree"] + base = output.parent / "tera_ffi/candidates" / tree + require(base.resolve() == base, "Kotlin native candidate contains a symlink") + record = artifacts.file_record(base, LIBRARY) + require( + record in candidate["files"], + "Kotlin smoke native library does not match the installed producer", + ) + return candidate, base / LIBRARY, record + + +def generate(root: Path, output: Path, library: Path) -> Path: + destination = provenance.output_path(root, HOST, str(output / "generated")) + argv = [ + "cargo", + "run", + "-p", + "tera_bindgen", + "--locked", + "--", + "generate", + str(library), + "--library", + "--language", + "kotlin", + "--metadata-no-deps", + "--no-format", + "--out-dir", + str(destination), + "--config", + "core/crates/tera_ffi/uniffi.toml", + ] + subprocess.run(argv, cwd=root, check=True, timeout=600) + generated = destination / "uniffi/tera_core/tera_core.kt" + require( + generated.is_file() and not generated.is_symlink(), + "Generated Kotlin source is absent", + ) + require( + list(destination.rglob("*.kt")) == [generated], + "Generated Kotlin source inventory differs", + ) + return generated + + +def test_results(directory: Path) -> dict[str, Any]: + files = sorted(directory.glob("TEST-*.xml")) + require(bool(files), "Kotlin smoke produced no test results") + cases = [] + for path in files: + require( + path.resolve() == path and not path.is_symlink(), + "Kotlin test result contains a symlink", + ) + require( + path.stat().st_size <= 2_000_000, + "Kotlin test result exceeds its byte bound", + ) + suite = ET.fromstring(path.read_bytes()) + require(suite.tag == "testsuite", "Kotlin test result has an invalid root") + require( + all( + suite.attrib.get(key) == "0" + for key in ("failures", "errors", "skipped") + ), + "Kotlin smoke has failed or skipped tests", + ) + observed = suite.findall("testcase") + require( + len(observed) == int(suite.attrib["tests"]) and bool(observed), + "Kotlin test result count differs", + ) + require( + all(not list(case) for case in observed), + "Kotlin test case contains a failure or skip", + ) + cases.extend( + f"{case.attrib['classname']}/{case.attrib['name']}" for case in observed + ) + require( + len(cases) == len(set(cases)) and set(cases) == EXPECTED_CASES, + "Kotlin smoke inventory is incomplete", + ) + return {"passed": len(cases), "failed": 0, "skipped": 0, "cases": sorted(cases)} + + +def run(root: Path, *, offline: bool) -> None: + output = output_root(root) + java = verify_host(root, offline=offline) + candidate, library, native = native_input(root, output) + generated = generate(root, output, library) + result_directory = output / "build/test-results/test" + require( + result_directory.resolve() == result_directory, + "Kotlin result directory contains a symlink", + ) + # Remove only prior XML for this exact owned task so stale results cannot pass. + for path in result_directory.glob("TEST-*.xml"): + require(not path.is_symlink(), "Kotlin result is a symlink") + path.unlink() + argv = [ + str(root / HARNESS / "gradlew"), + "--no-daemon", + "--console=plain", + "--dependency-verification=strict", + "--project-cache-dir", + str(output / "project_cache"), + f"-Pkotlin.project.persistent.dir={output / 'kotlin'}", + "-p", + str(root / HARNESS), + "test", + ] + if offline: + argv.append("--offline") + environment = dict( + os.environ, + TERA_KOTLIN_SMOKE_ROOT=str(output), + TERA_KOTLIN_NATIVE_DIR=str(library.parent), + ) + subprocess.run(argv, cwd=root, env=environment, check=True, timeout=600) + results = test_results(result_directory) + require( + installed.check(root)["candidate"] == candidate, + "Producer changed during Kotlin smoke", + ) + require( + artifacts.file_record(library.parent.parent.parent, LIBRARY) == native, + "Native library changed during Kotlin smoke", + ) + report = { + "schema": "tera.kotlin-binding-smoke.v1", + "source": candidate["source"], + "native": native, + "generated_sha256": hashlib.sha256(generated.read_bytes()).hexdigest(), + "jdk": java, + "host": HOST, + "dependency_resolution": "offline" if offline else "bootstrap", + "gradle_argv": argv, + "results": results, + "disposition": "local_native_binding_smoke_only", + } + provenance.write_atomic(output / "result.json", provenance.encoded(report)) + print(json.dumps(report, indent=2)) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("mode", choices=("verify", "bootstrap")) + args = parser.parse_args() + try: + run(Path(__file__).resolve().parent.parent, offline=args.mode == "verify") + except ( + OSError, + ValueError, + ET.ParseError, + subprocess.SubprocessError, + source.ProvenanceError, + ) as error: + print(f"Kotlin binding smoke failed: {error}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/kotlin_smoke/build.gradle.kts b/scripts/kotlin_smoke/build.gradle.kts @@ -0,0 +1,42 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") version "2.4.10" +} + +val externalRoot = file(providers.environmentVariable("TERA_KOTLIN_SMOKE_ROOT").get()) +val projectOutput = file(providers.environmentVariable("EXT_BUILD_PROJECT_DIR").get()) +require(!providers.environmentVariable("EXT_BUILD_RUN_ACTIVE").orNull.isNullOrBlank()) +require(externalRoot.isAbsolute && !externalRoot.toPath().startsWith(rootDir.toPath())) +require(externalRoot.toPath().startsWith(projectOutput.toPath())) +layout.buildDirectory.set(externalRoot.resolve("build")) + +kotlin { + jvmToolchain(21) + sourceSets.main { kotlin.srcDir(externalRoot.resolve("generated")) } +} + +dependencies { + implementation("net.java.dev.jna:jna:5.17.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") + testImplementation(kotlin("test-junit")) +} + +dependencyLocking { + lockAllConfigurations() + lockMode.set(LockMode.STRICT) +} + +tasks.named<KotlinCompile>("compileTestKotlin") { + compilerOptions.allWarningsAsErrors.set(true) +} + +tasks.test { + useJUnit() + maxParallelForks = 1 + systemProperty("jna.library.path", providers.environmentVariable("TERA_KOTLIN_NATIVE_DIR").get()) + systemProperty("tera.smoke.data", externalRoot.resolve("test_data").absolutePath) + testLogging { events("passed", "failed", "skipped") } + // A smoke run must execute the native boundary even when compilation is cached. + outputs.upToDateWhen { false } +} diff --git a/scripts/kotlin_smoke/gradle.lockfile b/scripts/kotlin_smoke/gradle.lockfile @@ -0,0 +1,41 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath +net.java.dev.jna:jna:5.17.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:1.3=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-build-tools-api:2.4.0=kotlinAbiValidationCompatClasspath +org.jetbrains.kotlin:kotlin-build-tools-api:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.0=kotlinAbiValidationCompatClasspath +org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.0=kotlinAbiValidationCompatClasspath +org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.0=kotlinAbiValidationCompatClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-compiler-runner:2.4.0=kotlinAbiValidationCompatClasspath +org.jetbrains.kotlin:kotlin-compiler-runner:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-daemon-client:2.4.0=kotlinAbiValidationCompatClasspath +org.jetbrains.kotlin:kotlin-daemon-client:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.0=kotlinAbiValidationCompatClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:2.4.10=kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-reflect:1.6.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-script-runtime:2.4.0=kotlinAbiValidationCompatClasspath +org.jetbrains.kotlin:kotlin-script-runtime:2.4.10=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-scripting-common:2.4.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:2.4.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:2.4.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-jvm:2.4.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-stdlib:2.4.0=kotlinAbiValidationCompatClasspath +org.jetbrains.kotlin:kotlin-stdlib:2.4.10=compileClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-test-junit:2.4.10=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-test:2.4.10=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-tooling-core:2.4.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:13.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath +org.jetbrains:annotations:23.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty=annotationProcessor,implementationDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDefExtensions,testAnnotationProcessor,testImplementationDependenciesMetadata,testKotlinScriptDefExtensions diff --git a/scripts/kotlin_smoke/gradle.properties b/scripts/kotlin_smoke/gradle.properties @@ -0,0 +1,7 @@ +org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 +org.gradle.caching=true +org.gradle.parallel=false +org.gradle.java.installations.auto-download=false +kotlin.code.style=official +kotlin.compiler.execution.strategy=in-process +kotlin.project.persistent.dir.gradle.disableWrite=true diff --git a/scripts/kotlin_smoke/gradle/verification-metadata.xml b/scripts/kotlin_smoke/gradle/verification-metadata.xml @@ -0,0 +1,439 @@ +<?xml version="1.0" encoding="UTF-8"?> +<verification-metadata xmlns="https://schema.gradle.org/dependency-verification" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd"> + <configuration> + <verify-metadata>true</verify-metadata> + <verify-signatures>false</verify-signatures> + </configuration> + <components> + <component group="com.google.code.gson" name="gson" version="2.11.0"> + <artifact name="gson-2.11.0.jar"> + <sha256 value="57928d6e5a6edeb2abd3770a8f95ba44dce45f3b23b7a9dc2b309c581552a78b" origin="Generated by Gradle"/> + </artifact> + <artifact name="gson-2.11.0.pom"> + <sha256 value="c0e547bea998888e6e25c5886a90e762272bc88b5275343dd2c05ded6ca2e360" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.code.gson" name="gson-parent" version="2.11.0"> + <artifact name="gson-parent-2.11.0.pom"> + <sha256 value="8acb1f3b72a6f026916ac0735bad9aab0293d527edb7b365327def13a9367b7a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.errorprone" name="error_prone_annotations" version="2.27.0"> + <artifact name="error_prone_annotations-2.27.0.jar"> + <sha256 value="24c923372c58e35d0b9f16a028929bb9aedc77521867c274f2bd0735df5ba1f5" origin="Generated by Gradle"/> + </artifact> + <artifact name="error_prone_annotations-2.27.0.pom"> + <sha256 value="4ca5a35d61235e16549ac346d1e34551cdf0fe27e84aa57e03cbeb255ea4e5da" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.errorprone" name="error_prone_parent" version="2.27.0"> + <artifact name="error_prone_parent-2.27.0.pom"> + <sha256 value="fa81829d049559df6927f9c9a6fd6bbd09f8b50e6d4736ae72c8300b6c3d7654" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="junit" name="junit" version="4.13.2"> + <artifact name="junit-4.13.2.jar"> + <sha256 value="8e495b634469d64fb8acfa3495a065cbacc8a0fff55ce1e31007be4c16dc57d3" origin="Generated by Gradle"/> + </artifact> + <artifact name="junit-4.13.2.pom"> + <sha256 value="569b6977ee4603c965c1c46c3058fa6e969291b0160eb6964dd092cd89eadd94" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="net.java.dev.jna" name="jna" version="5.17.0"> + <artifact name="jna-5.17.0.jar"> + <sha256 value="b3a9408e7c51e08ef0e3bfcc08f443f6ec0f6191ba8cd7c18d53d2b22e5bdbc0" origin="Generated by Gradle"/> + </artifact> + <artifact name="jna-5.17.0.pom"> + <sha256 value="501a0ff05d84a4ad10f6de25be94f49398b70a31be8f3a0ed9f4c6b44fbefee4" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.hamcrest" name="hamcrest-core" version="1.3"> + <artifact name="hamcrest-core-1.3.jar"> + <sha256 value="66fdef91e9739348df7a096aa384a5685f4e875584cce89386a7a47251c4d8e9" origin="Generated by Gradle"/> + </artifact> + <artifact name="hamcrest-core-1.3.pom"> + <sha256 value="fde386a7905173a1b103de6ab820727584b50d0e32282e2797787c20a64ffa93" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.hamcrest" name="hamcrest-parent" version="1.3"> + <artifact name="hamcrest-parent-1.3.pom"> + <sha256 value="6d535f94efb663bdb682c9f27a50335394688009642ba7a9677504bc1be4129b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains" name="annotations" version="13.0"> + <artifact name="annotations-13.0.jar"> + <sha256 value="ace2a10dc8e2d5fd34925ecac03e4988b2c0f851650c94b8cef49ba1bd111478" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotations-13.0.pom"> + <sha256 value="965aeb2bedff369819bdde1bf7a0b3b89b8247dd69c88b86375d76163bb8c397" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains" name="annotations" version="23.0.0"> + <artifact name="annotations-23.0.0.jar"> + <sha256 value="7b0f19724082cbfcbc66e5abea2b9bc92cf08a1ea11e191933ed43801eb3cd05" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotations-23.0.0.pom"> + <sha256 value="c9490f655132328df2cfbcfdf743f53fc3916d6c1d10437175a6ca6e3a67771c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="fus-statistics-gradle-plugin" version="2.4.10"> + <artifact name="fus-statistics-gradle-plugin-2.4.10-gradle813.jar"> + <sha256 value="d312eb7d69a8d5ac5377c712546665c9b94a63ca8fc9cdd315431f10672e8091" origin="Generated by Gradle"/> + </artifact> + <artifact name="fus-statistics-gradle-plugin-2.4.10.module"> + <sha256 value="8ed5af5c6da4a233cad89f0bedacfccfd3ef0e19396a020bb974e2fedbe23e97" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-build-statistics" version="2.4.10"> + <artifact name="kotlin-build-statistics-2.4.10.jar"> + <sha256 value="ba2748b67fb7b24ee1401eecfee32eb67dd055c85a59a5742188f795477a11cc" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-build-statistics-2.4.10.pom"> + <sha256 value="01cc32ad3b11274444939d06424cbf43a3e207e877683d1c851196ca19088ac1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-build-tools-api" version="2.4.0"> + <artifact name="kotlin-build-tools-api-2.4.0.jar"> + <sha256 value="66917335497f9ab75a36c34b3d90bad2bc8dc256c34b2413ecd0c9467fcf597d" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-build-tools-api-2.4.0.pom"> + <sha256 value="9686fa8f16867b2aba2e5c0e4cfc4a0aeecf59fe6de419f89f7bd838fdde8e74" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-build-tools-api" version="2.4.10"> + <artifact name="kotlin-build-tools-api-2.4.10.jar"> + <sha256 value="3953d283e7710c990672e403a87df393d9726a9bf3e172194ebb5c33e062fcb0" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-build-tools-api-2.4.10.pom"> + <sha256 value="78242a9440f92400eaf3df8d2108ad706f669a53125845a7d31d3df7c4de4439" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-build-tools-compat" version="2.4.10"> + <artifact name="kotlin-build-tools-compat-2.4.10.jar"> + <sha256 value="cdcc50d8016f4250e7dc61822f28f227ba9a9b7d3f9eb5503c315eb12188d952" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-build-tools-compat-2.4.10.pom"> + <sha256 value="a081a796f828ae644b59950f072cd00bd64ac74f12b12e830399da8de2e4fa34" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-build-tools-cri-impl" version="2.4.0"> + <artifact name="kotlin-build-tools-cri-impl-2.4.0.jar"> + <sha256 value="5168b866945f6ac1ffcb6d4c88cf582497249469c2e4a4eafc0bcd35b5a264a9" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-build-tools-cri-impl-2.4.0.pom"> + <sha256 value="856d59395489276e93edb4390c99397de83e20d27572c5238c56ed21e43433da" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-build-tools-cri-impl" version="2.4.10"> + <artifact name="kotlin-build-tools-cri-impl-2.4.10.jar"> + <sha256 value="781c95a28f053ded2d183b818bdae98db3d6d15ebc0555cecf64800fea003b3b" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-build-tools-cri-impl-2.4.10.pom"> + <sha256 value="ada6ecb7cb544ef2fcee73e252c31f80c8e342c3bdb51bb9999fe57fec041fd2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-build-tools-impl" version="2.4.0"> + <artifact name="kotlin-build-tools-impl-2.4.0.jar"> + <sha256 value="076c0a7f84d303f1e390774410c8678b8b584faa3f642435f93d2e4e81dabe7c" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-build-tools-impl-2.4.0.pom"> + <sha256 value="485ca3d511aa384207db5706e0741dc79ace45d40e0489f9ef29731aafb14f7c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-build-tools-impl" version="2.4.10"> + <artifact name="kotlin-build-tools-impl-2.4.10.jar"> + <sha256 value="4a32f63522ef4726afbdee1783f05698499abc7a5ecade3a6cafa3e4074562ee" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-build-tools-impl-2.4.10.pom"> + <sha256 value="f1b431c37717f0d472cd053dc81d893204cb59280f8035d636f534bc7c5d8db4" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-compiler-embeddable" version="2.4.0"> + <artifact name="kotlin-compiler-embeddable-2.4.0.jar"> + <sha256 value="c2b25e8c1c93ec416ba4327f5e31eaec0f0c8847241b9353e294b8db9dce564f" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-compiler-embeddable-2.4.0.pom"> + <sha256 value="f5d78540108e7bbc9162253ce52c42507ead2e530945213dd2a3b793b590c53c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-compiler-embeddable" version="2.4.10"> + <artifact name="kotlin-compiler-embeddable-2.4.10.jar"> + <sha256 value="9309638a2ee03e6bde9ef4b7444055a94b84ab906563675d71ff9aecb64da913" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-compiler-embeddable-2.4.10.pom"> + <sha256 value="c3adb4f66fbfaa843c019b402d2c7e5f0880d32956bd8dd84022946177251102" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-compiler-runner" version="2.4.0"> + <artifact name="kotlin-compiler-runner-2.4.0.jar"> + <sha256 value="ad9608152740e92d94a278b4ff2ca82c4bf5ff8ef219220e3c07aeb554638cee" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-compiler-runner-2.4.0.pom"> + <sha256 value="5e1ed156cb8d38b61e2ccc546dbd8b2bde778367eb298f8875bd820fbfd0b0f2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-compiler-runner" version="2.4.10"> + <artifact name="kotlin-compiler-runner-2.4.10.jar"> + <sha256 value="9e869d66504572852f421bcb1625c0d564c2b3c8cecbf6cd5fef7e7f33e0013e" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-compiler-runner-2.4.10.pom"> + <sha256 value="180738762c8941c4e30d7174a92071955bd869587332d785e61391167c82bc5a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-daemon-client" version="2.4.0"> + <artifact name="kotlin-daemon-client-2.4.0.jar"> + <sha256 value="f546f0b3699c4984f320dd03080de05cad61064208ba8f37df0d3f8f4a7fd14a" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-daemon-client-2.4.0.pom"> + <sha256 value="143793627382d905eda93e152a28c993f6323fc4b748370a45c7badc99914a13" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-daemon-client" version="2.4.10"> + <artifact name="kotlin-daemon-client-2.4.10.jar"> + <sha256 value="a3d1a5854fd92766de1ca37ff003a822d3da2039902008384dd1f7375f3d5cf9" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-daemon-client-2.4.10.pom"> + <sha256 value="f6de9e3a0e89add8e043fa76866d9a2ff10fb87963b1396377bbb00c229fde6e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-daemon-embeddable" version="2.4.0"> + <artifact name="kotlin-daemon-embeddable-2.4.0.jar"> + <sha256 value="383093af5e078435b60043d19bfa83eb6191be0f4d13a28fb1c8a0f3ab24fe66" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-daemon-embeddable-2.4.0.pom"> + <sha256 value="cb36e780fcb39ee09aab6e398f9bd5ea27f982bb6e19329742a20eb2ebdc2936" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-daemon-embeddable" version="2.4.10"> + <artifact name="kotlin-daemon-embeddable-2.4.10.jar"> + <sha256 value="547d969a2eab602fe0bdfd9fcb1b563a5c6d30099d3351394d6427eba8288467" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-daemon-embeddable-2.4.10.pom"> + <sha256 value="44ed1a490a4b646313fc0bc2683897a1eac4d8c6b57c0825d3e81fa7ca376d35" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin" version="2.4.10"> + <artifact name="kotlin-gradle-plugin-2.4.10-gradle813.jar"> + <sha256 value="13e22c869df3972db9496b47bf2815fbcfb61e0439f96cbd8d6aeb912b481375" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-gradle-plugin-2.4.10.module"> + <sha256 value="0e7713d84605da0bee038b578f31795882ccd721220a79cc36736ac9af9cb412" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-annotations" version="2.4.10"> + <artifact name="kotlin-gradle-plugin-annotations-2.4.10.jar"> + <sha256 value="15c3b98f18c874b9ac5be2565f3724b3eaf6c6cf57dc784d73516a22a9e470cf" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-gradle-plugin-annotations-2.4.10.pom"> + <sha256 value="44fc5a0a711c92df16f00d3575d0f2c4a264f43ab2f588e455b3c6043124fd53" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-api" version="2.4.10"> + <artifact name="kotlin-gradle-plugin-api-2.4.10-gradle813.jar"> + <sha256 value="74c4e4f7d037e850bab666db12c90a49e065d9495fd2dfdaa7bc39dd963083d6" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-gradle-plugin-api-2.4.10.module"> + <sha256 value="ade28e1831b222e161e00c120d5c9d56f9d4fe3b0aabed9e54912d71ab6ee8cb" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-idea" version="2.4.10"> + <artifact name="kotlin-gradle-plugin-idea-2.4.10.jar"> + <sha256 value="086734484c7edf19a620ab1a617d097c40e03bc52602e77840ea5ebd6380e514" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-gradle-plugin-idea-2.4.10.module"> + <sha256 value="7e883fbfe9d2c9ced4a8fa99217a8dd413bb14a7ea932edbbda35d5e91a5119a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-idea-proto" version="2.4.10"> + <artifact name="kotlin-gradle-plugin-idea-proto-2.4.10.jar"> + <sha256 value="355ac566ff74d74d230f7cb6d55e2ed50158b4c28fc186fc8d8268e8fe9868f4" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-gradle-plugin-idea-proto-2.4.10.pom"> + <sha256 value="074ee0dd408de542a8e4b726daad829dc33a9e56ddb4f43a00eb9b9f67aff17f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugins-bom" version="2.4.10"> + <artifact name="kotlin-gradle-plugins-bom-2.4.10.module"> + <sha256 value="48eb4756284d1d7ea24639d22861f15ff0cb685a15331a6c6a3674ff44baf029" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-gradle-plugins-bom-2.4.10.pom"> + <sha256 value="733fccda03ae34773e7b5c85d0e278c897df283370059cb39825f7c0381e49b6" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-klib-commonizer-api" version="2.4.10"> + <artifact name="kotlin-klib-commonizer-api-2.4.10.jar"> + <sha256 value="971bb968b4af10932bcc18f3395721ee48cc166597b50926731aeeff334086da" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-klib-commonizer-api-2.4.10.pom"> + <sha256 value="139b3ce8d655807eb68a45798f04329760b2106669ab332d2ac108843d758b29" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-klib-commonizer-embeddable" version="2.4.10"> + <artifact name="kotlin-klib-commonizer-embeddable-2.4.10.jar"> + <sha256 value="c5e543412f81a32ff4611a886e18399fa5afa15c77e3ef2fdc0faa397ad8abce" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-klib-commonizer-embeddable-2.4.10.pom"> + <sha256 value="bebdd5b6115a8d377edd8d00e8c1d4cfcacbf2bc0bdbb7248d43c5c7a19bafab" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-native-utils" version="2.4.10"> + <artifact name="kotlin-native-utils-2.4.10.jar"> + <sha256 value="76ec55f967dc8f57481a27b3e7868cc97cb32964d6d285517aef310c3d754a44" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-native-utils-2.4.10.pom"> + <sha256 value="9b91e38cfc84eaa34183140192c307ddc68f4a4c6035f554c4c22e15f33916c7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-reflect" version="1.6.10"> + <artifact name="kotlin-reflect-1.6.10.jar"> + <sha256 value="3277ac102ae17aad10a55abec75ff5696c8d109790396434b496e75087854203" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-reflect-1.6.10.pom"> + <sha256 value="57905524274a00ae028aaccc27283f6bc5925a934a046c1cc5d06c8ee4d6d5a9" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-script-runtime" version="2.4.0"> + <artifact name="kotlin-script-runtime-2.4.0.jar"> + <sha256 value="196e8ecbf63daf477e34e67c2f871ee232ae7bf8b377432d876d1bbf9d273de3" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-script-runtime-2.4.0.pom"> + <sha256 value="ce705c50e7e0afb22298acff0a983cdb6ea0f7fe2d1b473cee42b38405bc357f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-script-runtime" version="2.4.10"> + <artifact name="kotlin-script-runtime-2.4.10.jar"> + <sha256 value="f4a5b30c2fdfbe386b097101b7989dbeea3743c06396136664784e08e15c6899" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-script-runtime-2.4.10.pom"> + <sha256 value="edcf0e2cd709646c4e5945be0f0a4920a0f3f417f328756746a29d92b23b1593" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-scripting-common" version="2.4.10"> + <artifact name="kotlin-scripting-common-2.4.10.jar"> + <sha256 value="d5399982cdbf5994f7f3a731afacfbc5c21b797d97ffd7328b62cea670103076" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-scripting-common-2.4.10.pom"> + <sha256 value="185636a91c18f09f8af87f87ea1788c2d55f72a6880210a0365a5a6a44d61833" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-scripting-compiler-embeddable" version="2.4.10"> + <artifact name="kotlin-scripting-compiler-embeddable-2.4.10.jar"> + <sha256 value="156c68ff816251c9d297c90d77e7e8e513eaad594277cee50a36faeddbe88935" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-scripting-compiler-embeddable-2.4.10.pom"> + <sha256 value="07d9913a83317c28ca7704be0c109a1429a276236e43be8b98f87dbd4d02afad" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-scripting-compiler-impl-embeddable" version="2.4.10"> + <artifact name="kotlin-scripting-compiler-impl-embeddable-2.4.10.jar"> + <sha256 value="1dcfc5495d8c74966fcbbd39240a9c0f8ab36bb4faad2ea5ffc000c5d7785171" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-scripting-compiler-impl-embeddable-2.4.10.pom"> + <sha256 value="a80b463dc4e3de57555c4e1b6e73658d40ac215b2788f4c193f56c91b3494ca9" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-scripting-jvm" version="2.4.10"> + <artifact name="kotlin-scripting-jvm-2.4.10.jar"> + <sha256 value="0b7518219da69b427e0b3f840f13e280db9e61a839c7d3ff094de4630ddbc9f5" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-scripting-jvm-2.4.10.pom"> + <sha256 value="403a12911dc71b4fa4c2e3dc305cd98e0ef3fea1fabce72d0a656dcf890b3775" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="2.4.0"> + <artifact name="kotlin-stdlib-2.4.0.jar"> + <sha256 value="ccb14ff83fabcb11458b798dbc9824748ccdffeec79c9aba789e6ed1cda86b1c" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-2.4.0.module"> + <sha256 value="c24a47dcbd9295d776f443be41d77ac245309601ed053711dee2b0254be02db6" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="2.4.10"> + <artifact name="kotlin-stdlib-2.4.10.jar"> + <sha256 value="4ec0293bc3751423b203f1d8493251c57c42e73eb6377a6b8560d0974ff0a6df" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-2.4.10.module"> + <sha256 value="1c04a98f1200c8705d015df08977211589eecbc87cf1c4c6d28a92682fe8446e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-test" version="2.4.10"> + <artifact name="kotlin-test-2.4.10.jar"> + <sha256 value="5d334a7b17ce9b223e4db35e355f0082fb0551d36c0424a599bf35686ba2ab7b" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-test-2.4.10.module"> + <sha256 value="afcb7814693d94d3e62433b47b437d76fc01b842f58b1ada4422a044d29cd4f0" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-test-junit" version="2.4.10"> + <artifact name="kotlin-test-junit-2.4.10.jar"> + <sha256 value="197cb7a42ff773eb0c76fd747f259dd15589790364d1559ff2d38e098d506401" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-test-junit-2.4.10.module"> + <sha256 value="4682343e9b075e0adaf189fc1353e85c85dcc721afb1cec8c01b42f3968db882" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-tooling-core" version="2.4.10"> + <artifact name="kotlin-tooling-core-2.4.10.jar"> + <sha256 value="59ea305372e3a2b7417764dcfecf645066a0d9b1792ab40afd15c1cc8535b58a" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-tooling-core-2.4.10.pom"> + <sha256 value="590248328a863d8f000ef063ac0ee8b4483f52d3e22fa51ca243ce7f6a78c75c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-util-io" version="2.4.10"> + <artifact name="kotlin-util-io-2.4.10.jar"> + <sha256 value="06a9a0f89884c1c1a86baa96260238d927fb027077ba67bb5d2958af6fd776d7" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-util-io-2.4.10.pom"> + <sha256 value="3c9ca43ebff84366e7ff28742b7475553619c5f0f8acc2d7a979a6f4b437c81a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-util-klib" version="2.4.10"> + <artifact name="kotlin-util-klib-2.4.10.jar"> + <sha256 value="408d3bd16e0f513ac2f4f04a78871701924836081c2cc774aa2b3f739278e430" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-util-klib-2.4.10.pom"> + <sha256 value="631dbfefce3aefea1de3573d8dc1e54919be9af88c142505d3352127d84abb84" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-util-klib-metadata" version="2.4.10"> + <artifact name="kotlin-util-klib-metadata-2.4.10.jar"> + <sha256 value="f9b0cc33542c53d83004ab4aeff6f6515d509d26e14e78f420eeae2a459f4927" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-util-klib-metadata-2.4.10.pom"> + <sha256 value="606f70d8788a23f9b017039107d005248d0887481f0815e6284ba9a6933a7b7b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin.jvm" name="org.jetbrains.kotlin.jvm.gradle.plugin" version="2.4.10"> + <artifact name="org.jetbrains.kotlin.jvm.gradle.plugin-2.4.10.pom"> + <sha256 value="5b762d1fbe93d3cd5c8cdb8548dd9593dbc41e0d6d9d9dff36c9252678226992" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-bom" version="1.9.0"> + <artifact name="kotlinx-coroutines-bom-1.9.0.pom"> + <sha256 value="bea5511e9001f2c593ab5080df131b219ab5e2085cc5979ce583eacd4946fd78" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core" version="1.9.0"> + <artifact name="kotlinx-coroutines-core-1.9.0.module"> + <sha256 value="ad534034a953b4e12cbeeb874c66adf8b1ca14df15fe0d2e6547aa34e86dfeca" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-jvm" version="1.8.0"> + <artifact name="kotlinx-coroutines-core-jvm-1.8.0.jar"> + <sha256 value="9860906a1937490bf5f3b06d2f0e10ef451e65b95b269f22daf68a3d1f5065c5" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlinx-coroutines-core-jvm-1.8.0.module"> + <sha256 value="ff6a22da40040938751db0ae21177e76517dbf126a76796f5426727bf76c1228" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-jvm" version="1.9.0"> + <artifact name="kotlinx-coroutines-core-jvm-1.9.0.jar"> + <sha256 value="ad89c2892235e670f222d819cb3d81188143cb19a05b59df9889ae4269f5c70a" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlinx-coroutines-core-jvm-1.9.0.module"> + <sha256 value="b321a899e40d3ce345707aa2cfda9983ad0dcc69fea74a9b8bf906a16c1cf8a9" origin="Generated by Gradle"/> + </artifact> + </component> + </components> +</verification-metadata> diff --git a/scripts/kotlin_smoke/gradle/wrapper/gradle-wrapper.jar b/scripts/kotlin_smoke/gradle/wrapper/gradle-wrapper.jar Binary files differ. diff --git a/scripts/kotlin_smoke/gradle/wrapper/gradle-wrapper.properties b/scripts/kotlin_smoke/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,10 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionSha256Sum=553c78f50dafcd54d65b9a444649057857469edf836431389695608536d6b746 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip +networkTimeout=60000 +retries=3 +retryBackOffMs=1000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/scripts/kotlin_smoke/gradlew b/scripts/kotlin_smoke/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/scripts/kotlin_smoke/settings.gradle.kts b/scripts/kotlin_smoke/settings.gradle.kts @@ -0,0 +1,13 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { mavenCentral() } +} + +rootProject.name = "tera_binding_smoke" diff --git a/scripts/kotlin_smoke/src/test/kotlin/tera/smoke/MediaOwnershipTests.kt b/scripts/kotlin_smoke/src/test/kotlin/tera/smoke/MediaOwnershipTests.kt @@ -0,0 +1,109 @@ +package tera.smoke + +import com.sun.jna.Library +import com.sun.jna.Native +import java.nio.file.Files +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlinx.coroutines.runBlocking +import uniffi.tera_core.FfiConverterTypeFfiMediaFile +import uniffi.tera_core.FfiMediaFile +import uniffi.tera_core.TeraAppException + +internal interface SmokeLibC : Library { + fun open(path: String, flags: Int): Int + fun close(descriptor: Int): Int + fun dup2(source: Int, target: Int): Int + fun read(descriptor: Int, bytes: ByteArray, size: Long): Long +} + +internal val LIBC = Native.load("c", SmokeLibC::class.java) + +internal class SmokeFile(path: Path) : AutoCloseable { + val descriptor: Int = LIBC.open(path.toString(), 0).also { check(it >= 0) } + private var open = true + + override fun close() { + if (open) { + open = false + check(LIBC.close(descriptor) == 0) + } + } +} + +class MediaOwnershipTests { + @Test + fun callerCloseBeforeGeneratedConversionPreservesTheAdmittedBytes(): Unit = runBlocking { + SmokeFixture().use { fixture -> + val runtime = fixture.runtime() + try { + SmokeFile(fixture.original).use { original -> + FfiMediaFile(original.descriptor.toULong(), fixture.bytes.size.toULong()).use { file -> + original.close() + val saved = runtime.phase1SaveDraft( + "31".repeat(16), fixture.draft(listOf(fixture.media(file))), UNIX_S, null, UNIX_MS, + ) + assertEquals(fixture.sha256, assertNotNull(saved.form).media.single().sha256) + } + } + } finally { + runtime.shutdown() + runtime.close() + } + } + } + + @Test + fun recycledCallerSlotAndDisposedSourceWrapperCannotSubstituteBytes(): Unit = runBlocking { + SmokeFixture().use { fixture -> + val runtime = fixture.runtime() + val replacementPath = fixture.root.resolve("replacement.png") + Files.write(replacementPath, ByteArray(fixture.bytes.size)) + try { + SmokeFile(fixture.original).use { original -> + SmokeFile(replacementPath).use { replacement -> + val source = FfiMediaFile(original.descriptor.toULong(), fixture.bytes.size.toULong()) + // Clone while the source wrapper is live. Raw generated pointer + // converters must never be called on an already disposed wrapper. + val retained = FfiConverterTypeFfiMediaFile.lift(FfiConverterTypeFfiMediaFile.lower(source)) + source.close() + source.close() + retained.use { file -> + assertEquals(original.descriptor, LIBC.dup2(replacement.descriptor, original.descriptor)) + val substituted = ByteArray(fixture.bytes.size) + assertEquals(substituted.size.toLong(), LIBC.read(original.descriptor, substituted, substituted.size.toLong())) + assertTrue(substituted.all { it == 0.toByte() }) + val saved = runtime.phase1SaveDraft( + "32".repeat(16), fixture.draft(listOf(fixture.media(file))), UNIX_S, null, UNIX_MS, + ) + assertEquals(fixture.sha256, assertNotNull(saved.form).media.single().sha256) + } + } + } + } finally { + runtime.shutdown() + runtime.close() + } + } + } + + @Test + fun admissionRejectsInvalidDescriptorsAndWrongTypesSynchronously() { + assertFailsWith<TeraAppException.Failure> { FfiMediaFile(ULong.MAX_VALUE, 24uL) } + SmokeFixture().use { fixture -> + SmokeFile(fixture.root).use { directory -> + assertFailsWith<TeraAppException.Failure> { FfiMediaFile(directory.descriptor.toULong(), 24uL) } + } + SmokeFile(fixture.original).use { original -> + val failure = assertFailsWith<TeraAppException.Failure> { + FfiMediaFile(original.descriptor.toULong(), fixture.bytes.size.toULong() + 1uL) + } + assertEquals("media_size_mismatch", failure.report.code) + } + } + } +} diff --git a/scripts/kotlin_smoke/src/test/kotlin/tera/smoke/ScopeLifecycleTests.kt b/scripts/kotlin_smoke/src/test/kotlin/tera/smoke/ScopeLifecycleTests.kt @@ -0,0 +1,186 @@ +package tera.smoke + +import java.nio.file.Files +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.async +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import uniffi.tera_core.FfiInvalidationRevision +import uniffi.tera_core.FfiLocalNetworkRecord +import uniffi.tera_core.FfiMediaOperation +import uniffi.tera_core.FfiRecoveryDisposition +import uniffi.tera_core.FfiRetryDisposition +import uniffi.tera_core.FfiRuntimeChangeKind +import uniffi.tera_core.FfiTodayProjectionUpdate +import uniffi.tera_core.ProtectedDataAvailability +import uniffi.tera_core.TeraAppException +import uniffi.tera_core.classifyErrorRecovery + +class ScopeLifecycleTests { + @Test + fun scopeAndUnsignedContextsCrossRustWithoutSignedNarrowing(): Unit = runBlocking { + SmokeFixture().use { fixture -> + val runtime = fixture.runtime() + var firstEpoch = "" + try { + val observer = SmokeObserver() + runtime.subscribeChanges(observer).use { subscription -> + val initial = observer.next() + assertEquals(FfiRuntimeChangeKind.INITIAL, initial.kind) + assertEquals(PUBLIC_KEY, initial.scope.publicKey) + assertEquals(GENERATION, initial.scope.sourceGeneration) + assertNull(initial.scope.context) + assertEquals(32, initial.epoch.length) + firstEpoch = initial.epoch + for ((index, generation) in listOf(0uL, 1uL, Long.MAX_VALUE.toULong() + 1uL, ULong.MAX_VALUE).withIndex()) { + val context = FfiLocalNetworkRecord(1u, "nearby", "Nearby", listOf("wss://relay.example"), null, emptyList(), generation) + runtime.phase1RefreshToday(context, UNIX_S, FfiTodayProjectionUpdate.INCREMENTAL) + val changed = observer.next() + assertEquals(context, changed.scope.context) + assertEquals(initial.epoch, changed.epoch) + assertEquals(FfiInvalidationRevision.Current((index + 1).toULong()), changed.revision) + } + val valid = FfiLocalNetworkRecord(1u, "nearby", "Nearby", listOf("wss://relay.example"), null, emptyList(), 1uL) + for ((invalid, code) in listOf( + valid.copy(id = "") to "invalid_local_network", + valid.copy(schemaVersion = UShort.MAX_VALUE) to "unsupported_schema_version", + )) { + val failure = assertFailsWith<TeraAppException.Failure> { + runtime.phase1RefreshToday(invalid, UNIX_S, FfiTodayProjectionUpdate.INCREMENTAL) + } + assertEquals(code, failure.report.code) + } + runtime.configurePublicRelays(listOf("wss://write.example")) + assertEquals(FfiRuntimeChangeKind.RELAY, observer.next().kind) + assertTrue(subscription.isActive()) + subscription.unsubscribe() + subscription.unsubscribe() + assertFalse(subscription.isActive()) + } + } finally { + runtime.shutdown() + runtime.close() + } + val reopened = fixture.runtime() + try { + assertEquals(PUBLIC_KEY, reopened.identityStatus().publicKey) + assertTrue(reopened.identityStatus().hostSignerConfigured) + val observer = SmokeObserver() + reopened.subscribeChanges(observer).use { + assertNotEquals(firstEpoch, observer.next().epoch) + assertTrue(it.isActive()) + } + } finally { + reopened.shutdown() + reopened.close() + } + } + } + + @Test + fun cancelledCloseWaitRetainsNativeCallbackDrainAndClosedAdmission(): Unit = runBlocking { + SmokeFixture().use { fixture -> + val runtime = fixture.runtime() + val observer = SmokeObserver(paused = true) + val subscription = runtime.subscribeChanges(observer) + try { + observer.next() + val closing = async(start = CoroutineStart.UNDISPATCHED) { runtime.shutdown() } + withTimeout(5_000) { while (!runtime.info().sdkClosed) delay(1) } + assertFalse(closing.isCompleted) + closing.cancelAndJoin() + assertFalse(subscription.isActive()) + val failure = assertFailsWith<TeraAppException.Failure> { runtime.sdkStorageStatus() } + assertEquals("client_closed", failure.report.code) + val repeated = async(start = CoroutineStart.UNDISPATCHED) { runtime.shutdown() } + assertFalse(repeated.isCompleted) + observer.released.countDown() + val receipt = withTimeout(5_000) { repeated.await() } + assertEquals("closed", receipt.state) + assertTrue(receipt.alreadyClosed) + assertTrue(runtime.shutdown().alreadyClosed) + } finally { + observer.released.countDown() + subscription.close() + runtime.shutdown() + runtime.close() + } + } + } + + @Test + fun independentSubscriptionDisposalStopsOnlyItsObserver(): Unit = runBlocking { + SmokeFixture().use { fixture -> + val runtime = fixture.runtime() + val first = SmokeObserver() + val second = SmokeObserver() + val a = runtime.subscribeChanges(first) + val b = runtime.subscribeChanges(second) + try { + assertEquals(first.next(), second.next()) + // Closing the live generated handle must dispose its Rust subscription. + a.close() + a.close() + runtime.configurePublicRelays(listOf("wss://write.example")) + assertEquals(FfiRuntimeChangeKind.RELAY, second.next().kind) + assertEquals(0, first.pending()) + assertTrue(b.isActive()) + runtime.shutdown() + assertEquals(FfiRuntimeChangeKind.LIFECYCLE, second.next().kind) + assertFalse(b.isActive()) + } finally { + a.close() + b.close() + runtime.shutdown() + runtime.close() + } + } + } + + @Test + fun errorRecoveryAndProtectedDataFailureRemainTyped(): Unit = runBlocking { + for ((code, disposition, retry) in listOf( + Triple("draft_revision_conflict", FfiRecoveryDisposition.STALE_REVISION, FfiRetryDisposition.AFTER_RECOVERY), + Triple("ios.runtime.cancelled", FfiRecoveryDisposition.OUTCOME_UNKNOWN, FfiRetryDisposition.RECONCILE_EXISTING_OPERATION), + Triple("deadline_exceeded", FfiRecoveryDisposition.OUTCOME_UNKNOWN, FfiRetryDisposition.RECONCILE_EXISTING_OPERATION), + Triple("unknown_future_retryable_network", FfiRecoveryDisposition.UNKNOWN, FfiRetryDisposition.NOT_ALLOWED), + )) { + val decision = classifyErrorRecovery(1u, code) + assertEquals(disposition, decision.disposition) + assertEquals(retry, decision.retry) + } + assertEquals(FfiRecoveryDisposition.UNSUPPORTED_VERSION, classifyErrorRecovery(UShort.MAX_VALUE, "client_closed").disposition) + SmokeFixture().use { fixture -> + val failure = assertFailsWith<TeraAppException.Failure> { fixture.runtime(ProtectedDataAvailability.UNAVAILABLE) } + assertEquals("protected_data_unavailable", failure.report.code) + assertEquals(1u.toUShort(), failure.report.schemaVersion) + assertFalse(Files.exists(fixture.owner.resolve("runtime.sqlite"))) + assertFalse(failure.report.safeMessage.contains(fixture.root.toString())) + } + } + + @Test + fun cancellationHandleKeepsIdentityAndDisposesIdempotently() { + val operation = FfiMediaOperation() + val identity = operation.operationId() + assertEquals(32, identity.length) + assertFalse(operation.isCancelled()) + operation.cancel() + operation.cancel() + assertTrue(operation.isCancelled()) + assertEquals(identity, operation.operationId()) + FfiMediaOperation().use { assertNotEquals(identity, it.operationId()) } + operation.close() + operation.close() + assertFailsWith<IllegalStateException> { operation.isCancelled() } + } +} diff --git a/scripts/kotlin_smoke/src/test/kotlin/tera/smoke/SmokeFixture.kt b/scripts/kotlin_smoke/src/test/kotlin/tera/smoke/SmokeFixture.kt @@ -0,0 +1,93 @@ +package tera.smoke + +import java.nio.file.Files +import java.nio.file.Path +import java.security.MessageDigest +import java.util.HexFormat +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlin.test.assertNotNull +import uniffi.tera_core.FfiAddCommandType +import uniffi.tera_core.FfiAddDraftInput +import uniffi.tera_core.FfiBlossomEndpointAuthority +import uniffi.tera_core.FfiBlossomHostKind +import uniffi.tera_core.FfiMediaFile +import uniffi.tera_core.FfiPreparedMediaInput +import uniffi.tera_core.FfiRuntimeChangeDelivery +import uniffi.tera_core.FfiRuntimeChangeKind +import uniffi.tera_core.FfiRuntimeChangeRecord +import uniffi.tera_core.HostSigningRequest +import uniffi.tera_core.HostSigningResult +import uniffi.tera_core.ProtectedDataAvailability +import uniffi.tera_core.SignerAvailabilityRecord +import uniffi.tera_core.SignerStatusRecord +import uniffi.tera_core.TeraHostSigner +import uniffi.tera_core.TeraRuntime +import uniffi.tera_core.TeraRuntimeObserver + +internal const val PUBLIC_KEY = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" +internal val GENERATION = "04".repeat(32) +internal const val UNIX_MS = 1_800_000_000_000uL +internal const val UNIX_S = 1_800_000_000uL + +internal object UnavailableSigner : TeraHostSigner { + override suspend fun signerStatus() = SignerStatusRecord(1u, SignerAvailabilityRecord.UNAVAILABLE) + override suspend fun sign(request: HostSigningRequest): HostSigningResult = + error("Binding smoke must never request a signature") +} + +internal class SmokeFixture : AutoCloseable { + val root: Path = Files.createTempDirectory( + Files.createDirectories(Path.of(System.getProperty("tera.smoke.data"))), "binding-", + ) + val owner: Path = Files.createDirectories(root.resolve("radroots/users").resolve(PUBLIC_KEY)) + val bytes = byteArrayOf(137.toByte(), 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 2, 0, 0, 0, 2) + val original: Path = root.resolve("original.png").also { Files.write(it, bytes) } + val sha256: String = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)) + + suspend fun runtime(protectedData: ProtectedDataAvailability = ProtectedDataAvailability.AVAILABLE): TeraRuntime = + TeraRuntime.withHostSigner(root.toString(), PUBLIC_KEY, GENERATION, UNIX_MS, protectedData, UnavailableSigner) + .also { runtime -> + runtime.configureBlossom( + FfiBlossomHostKind.SIMULATOR, FfiBlossomEndpointAuthority.LOOPBACK_DEVELOPMENT, + "http://127.0.0.1:3000", emptyList(), + ) + } + + fun media(file: FfiMediaFile) = FfiPreparedMediaInput( + schemaVersion = 2u, opaqueReference = "media:$sha256", file = file, + sha256 = sha256, mediaType = "image/png", byteSize = bytes.size.toULong(), + width = 2u, height = 2u, alt = "Binding fixture", preparedAtUnixS = UNIX_S, + ) + + fun draft(media: List<FfiPreparedMediaInput>) = FfiAddDraftInput( + schemaVersion = 1u, commandType = FfiAddCommandType.CREATE_PHOTO_UPDATE, + content = "Binding smoke", identifier = null, title = null, summary = null, + location = null, eventTiming = null, eventStartDate = null, eventEndDate = null, + eventStartUnixS = null, eventEndUnixS = null, eventTimezone = null, + priceAmount = null, currency = null, unit = null, quantity = null, + foodPublishedAtUnixS = null, foodStatus = null, media = media, + ) + + override fun close() { + Files.walk(root).use { paths -> + paths.sorted(Comparator.reverseOrder()).forEach { Files.delete(it) } + } + } +} + +internal class SmokeObserver(private val paused: Boolean = false) : TeraRuntimeObserver { + private val events = ArrayBlockingQueue<FfiRuntimeChangeRecord>(64) + val released = CountDownLatch(1) + + override fun onChange(change: FfiRuntimeChangeRecord) { + check(events.offer(change)) { "The bounded smoke observer overflowed" } + if (paused && change.kind == FfiRuntimeChangeKind.INITIAL && change.delivery == FfiRuntimeChangeDelivery.CHANGE) { + check(released.await(10, TimeUnit.SECONDS)) { "The test did not release its callback" } + } + } + + fun next(): FfiRuntimeChangeRecord = assertNotNull(events.poll(5, TimeUnit.SECONDS), "Native callback deadline") + fun pending(): Int = events.size +} diff --git a/scripts/legacy_identifiers.py b/scripts/legacy_identifiers.py @@ -50,12 +50,14 @@ TEXT_SUFFIXES = { ".json", ".yml", ".md", + ".kt", + ".kts", } IDENTIFIER = re.compile(r"(?i)\b[a-z0-9_]*radroots[a-z0-9_]*(?:[.-][a-z0-9_-]+)*") DECLARATION = re.compile( r"(?m)^\s*(?:(?:public|private|internal|fileprivate|final|open|indirect|" r"nonisolated|static|async|unsafe|pub(?:\([^\n)]*\))?)\s+)*" - r"(?:class|struct|enum|actor|protocol|typealias|trait|type|fn|func|static\s+(?:var|let))\s+" + r"(?:class|struct|enum|actor|protocol|typealias|trait|type|fn|fun|func|static\s+(?:var|let))\s+" r"(?i:radroots)[a-zA-Z0-9_]*\b" ) CATEGORY_FIELDS = {"owner", "reason", "reader", "removal_condition"} @@ -236,7 +238,7 @@ def policy_index(policy: Any) -> dict[tuple[str, str], int]: def inspect_sources(sources: dict[str, str]) -> collections.Counter: observed = collections.Counter() for path, text in sources.items(): - if Path(path).suffix in {".swift", ".rs"}: + if Path(path).suffix in {".swift", ".rs", ".kt", ".kts"}: require( not Path(path).name.lower().startswith("radroots"), "app-owned source filename uses the legacy brand", diff --git a/scripts/swift-quality.sh b/scripts/swift-quality.sh @@ -36,6 +36,8 @@ readonly -a PYTHON_QUALITY_PATHS=( scripts/test_app_dependency_graph.py scripts/test_maintainability_ratchet.py scripts/test_package_contract.py + scripts/kotlin_smoke.py + scripts/test_kotlin_smoke.py ) command -v swiftformat >/dev/null || { diff --git a/scripts/test_kotlin_smoke.py b/scripts/test_kotlin_smoke.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +import os +import sys +import tempfile +import unittest +import xml.etree.ElementTree as ET +from pathlib import Path +from unittest.mock import patch + +SCRIPTS = Path(__file__).resolve().parent +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + +import ffi_source as source # noqa: E402 +import kotlin_smoke as smoke # noqa: E402 + + +class KotlinSmokeTests(unittest.TestCase): + def setUp(self) -> None: + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name).resolve() + self.path = self.root / "TEST-smoke.xml" + self.suite = ET.Element( + "testsuite", tests="8", failures="0", errors="0", skipped="0" + ) + for identity in sorted(smoke.EXPECTED_CASES): + name, case = identity.split("/") + ET.SubElement(self.suite, "testcase", classname=name, name=case) + + def write(self) -> None: + self.path.write_bytes(ET.tostring(self.suite)) + + def test_only_complete_executed_inventory_is_green(self) -> None: + self.write() + result = smoke.test_results(self.root) + self.assertEqual(result["passed"], 8) + self.assertEqual((result["failed"], result["skipped"]), (0, 0)) + + def test_empty_and_missing_results_cannot_pass(self) -> None: + with self.assertRaises(source.ProvenanceError): + smoke.test_results(self.root) + self.suite.clear() + self.suite.attrib.update(tests="0", failures="0", errors="0", skipped="0") + self.write() + with self.assertRaises(source.ProvenanceError): + smoke.test_results(self.root) + + def test_failed_skipped_or_error_results_cannot_pass(self) -> None: + for key in ("failures", "errors", "skipped"): + with self.subTest(key=key): + self.suite.set(key, "1") + self.write() + with self.assertRaises(source.ProvenanceError): + smoke.test_results(self.root) + self.suite.set(key, "0") + + def test_case_failures_cannot_hide_behind_zero_suite_totals(self) -> None: + ET.SubElement(self.suite.find("testcase"), "failure") + self.write() + with self.assertRaises(source.ProvenanceError): + smoke.test_results(self.root) + + def test_renamed_duplicate_or_missing_cases_cannot_pass(self) -> None: + cases = self.suite.findall("testcase") + cases[-1].attrib = cases[0].attrib.copy() + self.write() + with self.assertRaises(source.ProvenanceError): + smoke.test_results(self.root) + self.suite.remove(cases[-1]) + self.suite.set("tests", "7") + self.write() + with self.assertRaises(source.ProvenanceError): + smoke.test_results(self.root) + + def test_result_symlinks_are_rejected(self) -> None: + self.write() + alias = self.root / "TEST-alias.xml" + alias.symlink_to(self.path) + with self.assertRaises(source.ProvenanceError): + smoke.test_results(self.root) + + def test_output_requires_extbuild_and_cannot_enter_source(self) -> None: + with patch.dict(os.environ, {}, clear=True): + with self.assertRaises(source.ProvenanceError): + smoke.output_root(self.root) + with patch.dict( + os.environ, + {"EXT_BUILD_RUN_ACTIVE": "1", "EXT_BUILD_PROJECT_DIR": str(self.root)}, + ): + with self.assertRaises(source.ProvenanceError): + smoke.output_root(self.root) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_legacy_identifiers.py b/scripts/test_legacy_identifiers.py @@ -46,6 +46,18 @@ class LegacyIdentifierTests(unittest.TestCase): def test_shared_and_persisted_names_are_exact_exceptions(self) -> None: self.assertEqual(legacy.validate(self.policy, self.sources)["identifiers"], 2) + def test_kotlin_binding_harness_keeps_exact_naming_boundaries(self) -> None: + for extension in ("kt", "kts"): + path = f"scripts/kotlin_smoke/source.{extension}" + self.assertTrue(legacy.selected(path)) + policy = copy.deepcopy(self.policy) + entry = next( + e for e in policy["entries"] if e["identifier"] == "RadrootsKit" + ) + entry["occurrences"].append({"path": path, "count": 1}) + with self.assertRaisesRegex(legacy.LegacyIdentifierError, "declaration"): + legacy.validate(policy, {**self.sources, path: "fun RadrootsKit() {}"}) + def test_unknown_identifier_is_rejected_even_in_an_allowed_file(self) -> None: sources = { self.path: self.sources[self.path] + 'let old = "radroots.unapproved.v1"\n' diff --git a/scripts/verify-package-contract.sh b/scripts/verify-package-contract.sh @@ -19,6 +19,7 @@ sh "$repo_root/scripts/ffi-provenance.sh" contract-check scripts/test_ffi_provenance.py \ scripts/test_ffi_artifacts.py \ scripts/test_ffi_installed.py \ + scripts/test_kotlin_smoke.py \ scripts/test_local_social_fixture.py ) diff --git a/test-fixtures/legacy-identifiers.v1.json b/test-fixtures/legacy-identifiers.v1.json @@ -1108,7 +1108,7 @@ }, { "path": "scripts/test_legacy_identifiers.py", - "count": 8 + "count": 10 }, { "path": "scripts/test_package_contract.py", @@ -1795,6 +1795,10 @@ "count": 1 }, { + "path": "scripts/kotlin_smoke/src/test/kotlin/tera/smoke/SmokeFixture.kt", + "count": 1 + }, + { "path": "scripts/legacy_identifiers.py", "count": 3 },