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:
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
},