@hot-updater/react-native 1.0.0-rc.17 → 1.0.0-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/checkForUpdate.js +7 -9
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/clientPlugin.js +7 -43
- package/lib/commonjs/clientPlugin.js.map +1 -1
- package/lib/commonjs/httpClient.js +7 -9
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/index.js +10 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +4 -4
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/pluginHost.js +26 -190
- package/lib/commonjs/pluginHost.js.map +1 -1
- package/lib/commonjs/releaseCatalogCache.js +7 -59
- package/lib/commonjs/releaseCatalogCache.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/module/checkForUpdate.js +1 -3
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/clientPlugin.js +1 -41
- package/lib/module/clientPlugin.js.map +1 -1
- package/lib/module/httpClient.js +1 -3
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +1 -1
- package/lib/module/native.js.map +1 -1
- package/lib/module/pluginHost.js +22 -187
- package/lib/module/pluginHost.js.map +1 -1
- package/lib/module/releaseCatalogCache.js +3 -54
- package/lib/module/releaseCatalogCache.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/clientPlugin.d.ts +1 -184
- package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -1
- package/lib/typescript/commonjs/httpClient.d.ts +1 -2
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +1 -1
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/pluginHost.d.ts +3 -28
- package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -1
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts +1 -13
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/types.d.ts +1 -1
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.types.d.ts +2 -2
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/clientPlugin.d.ts +1 -184
- package/lib/typescript/module/clientPlugin.d.ts.map +1 -1
- package/lib/typescript/module/httpClient.d.ts +1 -2
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +1 -1
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/pluginHost.d.ts +3 -28
- package/lib/typescript/module/pluginHost.d.ts.map +1 -1
- package/lib/typescript/module/releaseCatalogCache.d.ts +1 -13
- package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/types.d.ts +1 -1
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/wrap.types.d.ts +2 -2
- package/package.json +7 -16
- package/src/checkForUpdate.ts +4 -6
- package/src/clientPlugin.ts +17 -231
- package/src/httpClient.ts +4 -4
- package/src/index.ts +7 -0
- package/src/native.ts +1 -1
- package/src/pluginHost.ts +24 -241
- package/src/releaseCatalogCache.ts +7 -136
- package/src/sdkVersion.ts +1 -1
- package/src/types.ts +1 -1
- package/src/wrap.types.ts +2 -2
- package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +0 -46
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +0 -2236
- package/android/src/test/java/com/hotupdater/HotUpdaterExceptionTest.kt +0 -220
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +0 -264
- package/android/src/test/java/com/hotupdater/HotUpdaterRecoveryStateTest.kt +0 -97
- package/android/src/test/java/com/hotupdater/InstallIdentityServiceTest.kt +0 -71
- package/android/src/test/java/com/hotupdater/KeyValueStorageServiceTest.kt +0 -106
- package/android/src/test/java/com/hotupdater/NativeConfigUtilsTest.kt +0 -20
- package/android/src/test/java/com/hotupdater/NoBackupContext.kt +0 -11
- package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +0 -397
- package/android/src/test/java/com/hotupdater/PendingCrashRecoveryTest.kt +0 -44
- package/android/src/test/java/com/hotupdater/PreviousProcessExitTest.kt +0 -57
- package/android/src/test/java/com/hotupdater/RelativePathResolverTest.kt +0 -41
- package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +0 -118
- package/android/src/test/java/com/hotupdater/TarBrArchiveExtractorTest.kt +0 -206
- package/lib/commonjs/appReady.spec.js +0 -360
- package/lib/commonjs/appReady.spec.js.map +0 -1
- package/lib/commonjs/appReady.test-utils.js +0 -25
- package/lib/commonjs/appReady.test-utils.js.map +0 -1
- package/lib/commonjs/baseURL.js +0 -16
- package/lib/commonjs/baseURL.js.map +0 -1
- package/lib/commonjs/catalogCacheNative.spec.js +0 -31
- package/lib/commonjs/catalogCacheNative.spec.js.map +0 -1
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +0 -936
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +0 -1
- package/lib/commonjs/entryImportGraph.spec.js +0 -50
- package/lib/commonjs/entryImportGraph.spec.js.map +0 -1
- package/lib/commonjs/httpClient.spec.js +0 -171
- package/lib/commonjs/httpClient.spec.js.map +0 -1
- package/lib/commonjs/index.spec.js +0 -395
- package/lib/commonjs/index.spec.js.map +0 -1
- package/lib/commonjs/iosInterop.spec.js +0 -18
- package/lib/commonjs/iosInterop.spec.js.map +0 -1
- package/lib/commonjs/native.spec.js +0 -910
- package/lib/commonjs/native.spec.js.map +0 -1
- package/lib/commonjs/pluginHost.spec.js +0 -292
- package/lib/commonjs/pluginHost.spec.js.map +0 -1
- package/lib/commonjs/plugins/insights/eventId.js +0 -129
- package/lib/commonjs/plugins/insights/eventId.js.map +0 -1
- package/lib/commonjs/plugins/insights/eventId.spec.js +0 -47
- package/lib/commonjs/plugins/insights/eventId.spec.js.map +0 -1
- package/lib/commonjs/plugins/insights/index.js +0 -272
- package/lib/commonjs/plugins/insights/index.js.map +0 -1
- package/lib/commonjs/plugins/insights/insights.spec.js +0 -640
- package/lib/commonjs/plugins/insights/insights.spec.js.map +0 -1
- package/lib/commonjs/plugins/insights/sender.js +0 -128
- package/lib/commonjs/plugins/insights/sender.js.map +0 -1
- package/lib/commonjs/plugins/insights/sender.spec.js +0 -265
- package/lib/commonjs/plugins/insights/sender.spec.js.map +0 -1
- package/lib/commonjs/plugins/insights/state.js +0 -145
- package/lib/commonjs/plugins/insights/state.js.map +0 -1
- package/lib/commonjs/releaseCatalogCache.spec.js +0 -213
- package/lib/commonjs/releaseCatalogCache.spec.js.map +0 -1
- package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +0 -64
- package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +0 -1
- package/lib/commonjs/store.notification.spec.js +0 -84
- package/lib/commonjs/store.notification.spec.js.map +0 -1
- package/lib/commonjs/store.spec.js +0 -182
- package/lib/commonjs/store.spec.js.map +0 -1
- package/lib/commonjs/updateError.spec.js +0 -149
- package/lib/commonjs/updateError.spec.js.map +0 -1
- package/lib/commonjs/wrap.progress.spec.js +0 -195
- package/lib/commonjs/wrap.progress.spec.js.map +0 -1
- package/lib/commonjs/wrap.spec.js +0 -232
- package/lib/commonjs/wrap.spec.js.map +0 -1
- package/lib/module/appReady.spec.js +0 -359
- package/lib/module/appReady.spec.js.map +0 -1
- package/lib/module/appReady.test-utils.js +0 -19
- package/lib/module/appReady.test-utils.js.map +0 -1
- package/lib/module/baseURL.js +0 -11
- package/lib/module/baseURL.js.map +0 -1
- package/lib/module/catalogCacheNative.spec.js +0 -31
- package/lib/module/catalogCacheNative.spec.js.map +0 -1
- package/lib/module/checkForUpdate.releaseCatalog.spec.js +0 -935
- package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +0 -1
- package/lib/module/entryImportGraph.spec.js +0 -50
- package/lib/module/entryImportGraph.spec.js.map +0 -1
- package/lib/module/httpClient.spec.js +0 -171
- package/lib/module/httpClient.spec.js.map +0 -1
- package/lib/module/index.spec.js +0 -394
- package/lib/module/index.spec.js.map +0 -1
- package/lib/module/iosInterop.spec.js +0 -18
- package/lib/module/iosInterop.spec.js.map +0 -1
- package/lib/module/native.spec.js +0 -909
- package/lib/module/native.spec.js.map +0 -1
- package/lib/module/pluginHost.spec.js +0 -291
- package/lib/module/pluginHost.spec.js.map +0 -1
- package/lib/module/plugins/insights/eventId.js +0 -122
- package/lib/module/plugins/insights/eventId.js.map +0 -1
- package/lib/module/plugins/insights/eventId.spec.js +0 -47
- package/lib/module/plugins/insights/eventId.spec.js.map +0 -1
- package/lib/module/plugins/insights/index.js +0 -268
- package/lib/module/plugins/insights/index.js.map +0 -1
- package/lib/module/plugins/insights/insights.spec.js +0 -639
- package/lib/module/plugins/insights/insights.spec.js.map +0 -1
- package/lib/module/plugins/insights/sender.js +0 -123
- package/lib/module/plugins/insights/sender.js.map +0 -1
- package/lib/module/plugins/insights/sender.spec.js +0 -265
- package/lib/module/plugins/insights/sender.spec.js.map +0 -1
- package/lib/module/plugins/insights/state.js +0 -139
- package/lib/module/plugins/insights/state.js.map +0 -1
- package/lib/module/releaseCatalogCache.spec.js +0 -213
- package/lib/module/releaseCatalogCache.spec.js.map +0 -1
- package/lib/module/releaseCatalogCacheNativeContract.spec.js +0 -64
- package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +0 -1
- package/lib/module/store.notification.spec.js +0 -85
- package/lib/module/store.notification.spec.js.map +0 -1
- package/lib/module/store.spec.js +0 -181
- package/lib/module/store.spec.js.map +0 -1
- package/lib/module/updateError.spec.js +0 -149
- package/lib/module/updateError.spec.js.map +0 -1
- package/lib/module/wrap.progress.spec.js +0 -196
- package/lib/module/wrap.progress.spec.js.map +0 -1
- package/lib/module/wrap.spec.js +0 -233
- package/lib/module/wrap.spec.js.map +0 -1
- package/lib/typescript/commonjs/appReady.test-utils.d.ts +0 -9
- package/lib/typescript/commonjs/appReady.test-utils.d.ts.map +0 -1
- package/lib/typescript/commonjs/baseURL.d.ts +0 -4
- package/lib/typescript/commonjs/baseURL.d.ts.map +0 -1
- package/lib/typescript/commonjs/plugins/insights/eventId.d.ts +0 -18
- package/lib/typescript/commonjs/plugins/insights/eventId.d.ts.map +0 -1
- package/lib/typescript/commonjs/plugins/insights/index.d.ts +0 -40
- package/lib/typescript/commonjs/plugins/insights/index.d.ts.map +0 -1
- package/lib/typescript/commonjs/plugins/insights/sender.d.ts +0 -74
- package/lib/typescript/commonjs/plugins/insights/sender.d.ts.map +0 -1
- package/lib/typescript/commonjs/plugins/insights/state.d.ts +0 -46
- package/lib/typescript/commonjs/plugins/insights/state.d.ts.map +0 -1
- package/lib/typescript/module/appReady.test-utils.d.ts +0 -9
- package/lib/typescript/module/appReady.test-utils.d.ts.map +0 -1
- package/lib/typescript/module/baseURL.d.ts +0 -4
- package/lib/typescript/module/baseURL.d.ts.map +0 -1
- package/lib/typescript/module/plugins/insights/eventId.d.ts +0 -18
- package/lib/typescript/module/plugins/insights/eventId.d.ts.map +0 -1
- package/lib/typescript/module/plugins/insights/index.d.ts +0 -40
- package/lib/typescript/module/plugins/insights/index.d.ts.map +0 -1
- package/lib/typescript/module/plugins/insights/sender.d.ts +0 -74
- package/lib/typescript/module/plugins/insights/sender.d.ts.map +0 -1
- package/lib/typescript/module/plugins/insights/state.d.ts +0 -46
- package/lib/typescript/module/plugins/insights/state.d.ts.map +0 -1
- package/plugins/insights/package.json +0 -8
- package/src/appReady.spec.ts +0 -393
- package/src/appReady.test-utils.ts +0 -31
- package/src/baseURL.ts +0 -15
- package/src/catalogCacheNative.spec.ts +0 -63
- package/src/checkForUpdate.releaseCatalog.spec.ts +0 -966
- package/src/entryImportGraph.spec.ts +0 -65
- package/src/httpClient.spec.ts +0 -216
- package/src/index.spec.ts +0 -434
- package/src/iosInterop.spec.ts +0 -30
- package/src/native.spec.ts +0 -978
- package/src/pluginHost.spec.ts +0 -297
- package/src/plugins/insights/eventId.spec.ts +0 -60
- package/src/plugins/insights/eventId.ts +0 -171
- package/src/plugins/insights/index.ts +0 -367
- package/src/plugins/insights/insights.spec.ts +0 -732
- package/src/plugins/insights/sender.spec.ts +0 -287
- package/src/plugins/insights/sender.ts +0 -221
- package/src/plugins/insights/state.ts +0 -184
- package/src/releaseCatalogCache.spec.ts +0 -304
- package/src/releaseCatalogCacheNativeContract.spec.ts +0 -80
- package/src/store.notification.spec.ts +0 -102
- package/src/store.spec.ts +0 -215
- package/src/updateError.spec.ts +0 -144
- package/src/wrap.progress.spec.ts +0 -232
- package/src/wrap.spec.ts +0 -262
|
@@ -1,2236 +0,0 @@
|
|
|
1
|
-
package com.hotupdater
|
|
2
|
-
|
|
3
|
-
import android.content.ContextWrapper
|
|
4
|
-
import kotlinx.coroutines.asCoroutineDispatcher
|
|
5
|
-
import kotlinx.coroutines.delay
|
|
6
|
-
import kotlinx.coroutines.runBlocking
|
|
7
|
-
import org.json.JSONObject
|
|
8
|
-
import org.junit.Assert.assertEquals
|
|
9
|
-
import org.junit.Assert.assertFalse
|
|
10
|
-
import org.junit.Assert.assertNotNull
|
|
11
|
-
import org.junit.Assert.assertNull
|
|
12
|
-
import org.junit.Assert.assertTrue
|
|
13
|
-
import org.junit.Rule
|
|
14
|
-
import org.junit.Test
|
|
15
|
-
import org.junit.rules.TemporaryFolder
|
|
16
|
-
import java.io.File
|
|
17
|
-
import java.io.IOException
|
|
18
|
-
import java.net.ConnectException
|
|
19
|
-
import java.net.SocketTimeoutException
|
|
20
|
-
import java.net.URL
|
|
21
|
-
import java.net.UnknownHostException
|
|
22
|
-
import java.util.Base64
|
|
23
|
-
import java.util.concurrent.CopyOnWriteArrayList
|
|
24
|
-
import java.util.concurrent.Executors
|
|
25
|
-
import java.util.concurrent.atomic.AtomicInteger
|
|
26
|
-
import javax.net.ssl.SSLHandshakeException
|
|
27
|
-
|
|
28
|
-
class BundleFileStorageServiceTest {
|
|
29
|
-
@get:Rule
|
|
30
|
-
val temporaryFolder = TemporaryFolder()
|
|
31
|
-
|
|
32
|
-
@Test
|
|
33
|
-
fun `unfinished staging launch rolls back to stable on cold start`() {
|
|
34
|
-
assertStagingLaunchAcrossColdStart(hasStableBundle = true)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
@Test
|
|
38
|
-
fun `unfinished staging launch rolls back to built in on cold start`() {
|
|
39
|
-
assertStagingLaunchAcrossColdStart(hasStableBundle = false)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
@Test
|
|
43
|
-
fun `completed staging launch remains trusted on cold start`() {
|
|
44
|
-
assertStagingLaunchAcrossColdStart(hasStableBundle = true, completesLaunch = true)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
@Test
|
|
48
|
-
fun `completed first OTA launch remains trusted on cold start`() {
|
|
49
|
-
assertStagingLaunchAcrossColdStart(hasStableBundle = false, completesLaunch = true)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
private fun assertStagingLaunchAcrossColdStart(
|
|
53
|
-
hasStableBundle: Boolean,
|
|
54
|
-
completesLaunch: Boolean = false,
|
|
55
|
-
) {
|
|
56
|
-
val rootDir = temporaryFolder.newFolder()
|
|
57
|
-
val preferences = InMemoryPreferencesService()
|
|
58
|
-
val stableBundleId = if (hasStableBundle) "stable-bundle" else null
|
|
59
|
-
listOfNotNull(stableBundleId, "hung-bundle").forEach { bundleId ->
|
|
60
|
-
val directory = createBundleDir(rootDir, bundleId)
|
|
61
|
-
writeFile(directory, "index.android.bundle")
|
|
62
|
-
writeManifest(directory, listOf("index.android.bundle"))
|
|
63
|
-
}
|
|
64
|
-
writeMetadata(
|
|
65
|
-
rootDir,
|
|
66
|
-
BundleMetadata(
|
|
67
|
-
isolationKey = TEST_ISOLATION_KEY,
|
|
68
|
-
stableBundleId = stableBundleId,
|
|
69
|
-
stagingBundleId = "hung-bundle",
|
|
70
|
-
verificationPending = true,
|
|
71
|
-
),
|
|
72
|
-
)
|
|
73
|
-
val firstProcess = createService(rootDir, preferences)
|
|
74
|
-
val firstLaunch = firstProcess.prepareLaunch(null)
|
|
75
|
-
assertEquals("hung-bundle", firstLaunch.launchedBundleId)
|
|
76
|
-
assertTrue(firstLaunch.shouldRollbackOnCrash)
|
|
77
|
-
|
|
78
|
-
assertEquals("hung-bundle", firstProcess.prepareLaunch(null).launchedBundleId)
|
|
79
|
-
assertFalse(firstProcess.getCrashHistory().contains("hung-bundle"))
|
|
80
|
-
if (completesLaunch) {
|
|
81
|
-
firstProcess.markLaunchCompleted("hung-bundle")
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Issue #1321: simulate process termination without a crash marker.
|
|
85
|
-
val secondProcess = createService(rootDir, preferences)
|
|
86
|
-
val nextLaunch = secondProcess.prepareLaunch(null)
|
|
87
|
-
assertEquals(if (completesLaunch) "hung-bundle" else stableBundleId, nextLaunch.launchedBundleId)
|
|
88
|
-
assertFalse(nextLaunch.shouldRollbackOnCrash)
|
|
89
|
-
assertEquals(!completesLaunch, secondProcess.getCrashHistory().contains("hung-bundle"))
|
|
90
|
-
if (!completesLaunch) {
|
|
91
|
-
assertEquals("RECOVERED", secondProcess.notifyAppReady()["status"])
|
|
92
|
-
}
|
|
93
|
-
val thirdProcess = createService(rootDir, preferences)
|
|
94
|
-
assertEquals(nextLaunch.launchedBundleId, thirdProcess.prepareLaunch(null).launchedBundleId)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
@Test
|
|
98
|
-
fun `resolveBundleFile uses single manifest bundle at root`() {
|
|
99
|
-
val rootDir = temporaryFolder.newFolder("root-manifest-bundle")
|
|
100
|
-
val service = createService(rootDir)
|
|
101
|
-
val bundleDir = createBundleDir(rootDir, "bundle-root")
|
|
102
|
-
val expectedBundleFile = writeFile(bundleDir, "foo.android.bundle")
|
|
103
|
-
|
|
104
|
-
writeManifest(bundleDir, listOf("foo.android.bundle"))
|
|
105
|
-
|
|
106
|
-
assertResolvedBundlePath(service, bundleDir, expectedBundleFile)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
@Test
|
|
110
|
-
fun `resolveBundleFile uses single nested manifest bundle`() {
|
|
111
|
-
val rootDir = temporaryFolder.newFolder("nested-manifest-bundle")
|
|
112
|
-
val service = createService(rootDir)
|
|
113
|
-
val bundleDir = createBundleDir(rootDir, "bundle-nested")
|
|
114
|
-
val expectedBundleFile = writeFile(bundleDir, "dist/foo.android.bundle")
|
|
115
|
-
|
|
116
|
-
writeManifest(bundleDir, listOf("dist/foo.android.bundle"))
|
|
117
|
-
|
|
118
|
-
assertResolvedBundlePath(service, bundleDir, expectedBundleFile)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
@Test
|
|
122
|
-
fun `resolveBundleFile rejects a manifest with missing assets even when root index exists`() {
|
|
123
|
-
val rootDir = temporaryFolder.newFolder("no-android-candidate")
|
|
124
|
-
val service = createService(rootDir)
|
|
125
|
-
val bundleDir = createBundleDir(rootDir, "bundle-no-candidate")
|
|
126
|
-
writeFile(bundleDir, "index.android.bundle")
|
|
127
|
-
|
|
128
|
-
writeManifest(bundleDir, listOf("index.ios.bundle", "assets/image.png"))
|
|
129
|
-
|
|
130
|
-
assertNull(invokeResolveBundleFile(service, bundleDir))
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
@Test
|
|
134
|
-
fun `resolveBundleFile rejects a manifest with multiple android bundle candidates`() {
|
|
135
|
-
val rootDir = temporaryFolder.newFolder("multiple-android-candidates")
|
|
136
|
-
val service = createService(rootDir)
|
|
137
|
-
val bundleDir = createBundleDir(rootDir, "bundle-multiple-candidates")
|
|
138
|
-
writeFile(bundleDir, "index.android.bundle")
|
|
139
|
-
|
|
140
|
-
writeFile(bundleDir, "foo.android.bundle")
|
|
141
|
-
writeFile(bundleDir, "dist/bar.android.bundle")
|
|
142
|
-
writeManifest(bundleDir, listOf("foo.android.bundle", "dist/bar.android.bundle"))
|
|
143
|
-
|
|
144
|
-
assertNull(invokeResolveBundleFile(service, bundleDir))
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
@Test
|
|
148
|
-
fun `resolveBundleFile returns null when manifest escapes root and no fallback exists`() {
|
|
149
|
-
val rootDir = temporaryFolder.newFolder("escaped-manifest-path")
|
|
150
|
-
val service = createService(rootDir)
|
|
151
|
-
val bundleDir = createBundleDir(rootDir, "bundle-escaped-path")
|
|
152
|
-
|
|
153
|
-
writeFile(bundleStoreDir(rootDir), "outside.android.bundle")
|
|
154
|
-
writeManifest(bundleDir, listOf("../outside.android.bundle"))
|
|
155
|
-
|
|
156
|
-
assertNull(invokeResolveBundleFile(service, bundleDir))
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
@Test
|
|
160
|
-
fun `resolveBundleFile returns null when manifest target is missing and no fallback exists`() {
|
|
161
|
-
val rootDir = temporaryFolder.newFolder("missing-manifest-target")
|
|
162
|
-
val service = createService(rootDir)
|
|
163
|
-
val bundleDir = createBundleDir(rootDir, "bundle-missing-target")
|
|
164
|
-
|
|
165
|
-
writeManifest(bundleDir, listOf("dist/missing.android.bundle"))
|
|
166
|
-
|
|
167
|
-
assertNull(invokeResolveBundleFile(service, bundleDir))
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
@Test
|
|
171
|
-
fun `resolveBundleFile rejects root index without manifest`() {
|
|
172
|
-
val rootDir = temporaryFolder.newFolder("legacy-root-index")
|
|
173
|
-
val service = createService(rootDir)
|
|
174
|
-
val bundleDir = createBundleDir(rootDir, "bundle-legacy")
|
|
175
|
-
writeFile(bundleDir, "index.android.bundle")
|
|
176
|
-
|
|
177
|
-
assertNull(invokeResolveBundleFile(service, bundleDir))
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
@Test
|
|
181
|
-
fun `resolveBundleFile returns null when manifest and root index are both missing`() {
|
|
182
|
-
val rootDir = temporaryFolder.newFolder("missing-everything")
|
|
183
|
-
val service = createService(rootDir)
|
|
184
|
-
val bundleDir = createBundleDir(rootDir, "bundle-invalid")
|
|
185
|
-
|
|
186
|
-
assertNull(invokeResolveBundleFile(service, bundleDir))
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
@Test
|
|
190
|
-
fun `prepareLaunch rolls back invalid staging and selects stable bundle`() {
|
|
191
|
-
val rootDir = temporaryFolder.newFolder("rollback-to-stable")
|
|
192
|
-
val preferences = InMemoryPreferencesService()
|
|
193
|
-
val service = createService(rootDir, preferences)
|
|
194
|
-
|
|
195
|
-
val stagingDir = createBundleDir(rootDir, "staging-bundle")
|
|
196
|
-
writeFile(stagingDir, "dist/staging.android.bundle")
|
|
197
|
-
writeManifest(
|
|
198
|
-
stagingDir,
|
|
199
|
-
listOf(
|
|
200
|
-
"dist/staging.android.bundle",
|
|
201
|
-
"assets/missing.png",
|
|
202
|
-
),
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
val stableDir = createBundleDir(rootDir, "stable-bundle")
|
|
206
|
-
val stableBundleFile = writeFile(stableDir, "dist/stable.android.bundle")
|
|
207
|
-
writeManifest(stableDir, listOf("dist/stable.android.bundle"))
|
|
208
|
-
|
|
209
|
-
writeMetadata(
|
|
210
|
-
rootDir,
|
|
211
|
-
BundleMetadata(
|
|
212
|
-
isolationKey = TEST_ISOLATION_KEY,
|
|
213
|
-
stableBundleId = stableDir.name,
|
|
214
|
-
stagingBundleId = stagingDir.name,
|
|
215
|
-
verificationPending = true,
|
|
216
|
-
),
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
val selection = service.prepareLaunch(null)
|
|
220
|
-
val report = service.notifyAppReady()
|
|
221
|
-
|
|
222
|
-
assertEquals(stableBundleFile.absolutePath, selection.bundleUrl)
|
|
223
|
-
assertEquals(stableDir.name, selection.launchedBundleId)
|
|
224
|
-
assertFalse(selection.shouldRollbackOnCrash)
|
|
225
|
-
assertFalse(stagingDir.exists())
|
|
226
|
-
assertEquals("RECOVERED", report["status"])
|
|
227
|
-
assertEquals(stagingDir.name, report["fromBundleId"])
|
|
228
|
-
assertEquals(stableDir.name, report["toBundleId"])
|
|
229
|
-
assertEquals("appVersion", report["updateStrategy"])
|
|
230
|
-
|
|
231
|
-
val metadata = loadMetadata(rootDir)
|
|
232
|
-
assertNotNull(metadata)
|
|
233
|
-
assertEquals(stableDir.name, metadata?.stagingBundleId)
|
|
234
|
-
assertNull(metadata?.stableBundleId)
|
|
235
|
-
assertFalse(metadata?.verificationPending ?: true)
|
|
236
|
-
assertEquals(stableBundleFile.absolutePath, preferences.getItem("HotUpdaterBundleURL"))
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
@Test
|
|
240
|
-
fun `prepareLaunch falls back to built in bundle when staging and stable are both invalid`() {
|
|
241
|
-
val rootDir = temporaryFolder.newFolder("fallback-to-built-in")
|
|
242
|
-
val service = createService(rootDir)
|
|
243
|
-
|
|
244
|
-
val stagingDir = createBundleDir(rootDir, "staging-bundle")
|
|
245
|
-
writeManifest(stagingDir, listOf("dist/missing.android.bundle"))
|
|
246
|
-
|
|
247
|
-
val stableDir = createBundleDir(rootDir, "stable-bundle")
|
|
248
|
-
writeManifest(stableDir, listOf("../outside.android.bundle"))
|
|
249
|
-
|
|
250
|
-
writeMetadata(
|
|
251
|
-
rootDir,
|
|
252
|
-
BundleMetadata(
|
|
253
|
-
isolationKey = TEST_ISOLATION_KEY,
|
|
254
|
-
stableBundleId = stableDir.name,
|
|
255
|
-
stagingBundleId = stagingDir.name,
|
|
256
|
-
verificationPending = true,
|
|
257
|
-
),
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
val selection = service.prepareLaunch(null)
|
|
261
|
-
val report = service.notifyAppReady()
|
|
262
|
-
|
|
263
|
-
assertEquals("assets://index.android.bundle", selection.bundleUrl)
|
|
264
|
-
assertNull(selection.launchedBundleId)
|
|
265
|
-
assertFalse(selection.shouldRollbackOnCrash)
|
|
266
|
-
assertFalse(stagingDir.exists())
|
|
267
|
-
assertEquals("RECOVERED", report["status"])
|
|
268
|
-
assertEquals(stagingDir.name, report["fromBundleId"])
|
|
269
|
-
assertEquals(HotUpdaterImpl.getMinBundleId(), report["toBundleId"])
|
|
270
|
-
assertEquals("appVersion", report["updateStrategy"])
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
@Test
|
|
274
|
-
fun `getBundleId falls back to built in while staging verification is pending`() {
|
|
275
|
-
val rootDir = temporaryFolder.newFolder("pending-staging-built-in")
|
|
276
|
-
val service = createService(rootDir)
|
|
277
|
-
|
|
278
|
-
val stagingDir = createBundleDir(rootDir, "staging-bundle")
|
|
279
|
-
writeFile(stagingDir, "index.android.bundle")
|
|
280
|
-
writeManifest(stagingDir, listOf("index.android.bundle"))
|
|
281
|
-
|
|
282
|
-
writeMetadata(
|
|
283
|
-
rootDir,
|
|
284
|
-
BundleMetadata(
|
|
285
|
-
isolationKey = TEST_ISOLATION_KEY,
|
|
286
|
-
stableBundleId = null,
|
|
287
|
-
stagingBundleId = stagingDir.name,
|
|
288
|
-
verificationPending = true,
|
|
289
|
-
),
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
assertNull(service.getBundleId())
|
|
293
|
-
assertEquals("", service.getBaseURL())
|
|
294
|
-
assertTrue(service.getManifest().isEmpty())
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
@Test
|
|
298
|
-
fun `getBundleId returns launched staging bundle while verification is pending`() {
|
|
299
|
-
val rootDir = temporaryFolder.newFolder("pending-staging-active")
|
|
300
|
-
val preferences = InMemoryPreferencesService()
|
|
301
|
-
val service = createService(rootDir, preferences)
|
|
302
|
-
|
|
303
|
-
val stagingDir = createBundleDir(rootDir, "staging-bundle")
|
|
304
|
-
val stagingBundleFile = writeFile(stagingDir, "index.android.bundle")
|
|
305
|
-
writeManifest(stagingDir, listOf("index.android.bundle"))
|
|
306
|
-
|
|
307
|
-
writeMetadata(
|
|
308
|
-
rootDir,
|
|
309
|
-
BundleMetadata(
|
|
310
|
-
isolationKey = TEST_ISOLATION_KEY,
|
|
311
|
-
stableBundleId = null,
|
|
312
|
-
stagingBundleId = stagingDir.name,
|
|
313
|
-
verificationPending = true,
|
|
314
|
-
),
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
preferences.setItem("HotUpdaterBundleURL", stagingBundleFile.absolutePath)
|
|
318
|
-
|
|
319
|
-
assertEquals(stagingDir.name, service.getBundleId())
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
@Test
|
|
323
|
-
fun `markLaunchCompleted records update applied transition`() {
|
|
324
|
-
val rootDir = temporaryFolder.newFolder("update-applied-transition")
|
|
325
|
-
val service = createService(rootDir)
|
|
326
|
-
|
|
327
|
-
val stableDir = createBundleDir(rootDir, "stable-bundle")
|
|
328
|
-
writeFile(stableDir, "index.android.bundle")
|
|
329
|
-
writeManifest(stableDir, listOf("index.android.bundle"))
|
|
330
|
-
|
|
331
|
-
val stagingDir = createBundleDir(rootDir, "staging-bundle")
|
|
332
|
-
writeFile(stagingDir, "index.android.bundle")
|
|
333
|
-
writeManifest(stagingDir, listOf("index.android.bundle"))
|
|
334
|
-
|
|
335
|
-
writeMetadata(
|
|
336
|
-
rootDir,
|
|
337
|
-
BundleMetadata(
|
|
338
|
-
isolationKey = TEST_ISOLATION_KEY,
|
|
339
|
-
stableBundleId = stableDir.name,
|
|
340
|
-
stagingBundleId = stagingDir.name,
|
|
341
|
-
verificationPending = true,
|
|
342
|
-
),
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
assertEquals(mapOf("status" to "PENDING"), service.notifyAppReady())
|
|
346
|
-
|
|
347
|
-
service.markLaunchCompleted(stagingDir.name)
|
|
348
|
-
|
|
349
|
-
assertEquals(
|
|
350
|
-
mapOf(
|
|
351
|
-
"status" to "UPDATE_APPLIED",
|
|
352
|
-
"fromBundleId" to stableDir.name,
|
|
353
|
-
"toBundleId" to stagingDir.name,
|
|
354
|
-
"updateStrategy" to "appVersion",
|
|
355
|
-
),
|
|
356
|
-
service.notifyAppReady(),
|
|
357
|
-
)
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
@Test
|
|
361
|
-
fun `manifest driven install reuses matching built in asset before first OTA`() =
|
|
362
|
-
runBlocking {
|
|
363
|
-
val rootDir = temporaryFolder.newFolder("first-ota-manifest-install")
|
|
364
|
-
val bundleContent = "target-bundle"
|
|
365
|
-
val imageContent = "target-image"
|
|
366
|
-
val assets =
|
|
367
|
-
linkedMapOf(
|
|
368
|
-
"index.android.bundle" to sha256(rootDir, bundleContent),
|
|
369
|
-
"assets/image.png" to sha256(rootDir, imageContent),
|
|
370
|
-
)
|
|
371
|
-
val manifest = manifestJson("target-bundle", assets)
|
|
372
|
-
val downloads =
|
|
373
|
-
MappingDownloadService(
|
|
374
|
-
mapOf(
|
|
375
|
-
"https://example.com/manifest.json" to manifest,
|
|
376
|
-
"https://example.com/index.android.bundle" to bundleContent,
|
|
377
|
-
),
|
|
378
|
-
)
|
|
379
|
-
val service =
|
|
380
|
-
createService(
|
|
381
|
-
rootDir,
|
|
382
|
-
downloadService = downloads,
|
|
383
|
-
builtInAssetResolver =
|
|
384
|
-
MappingBuiltInAssetResolver(
|
|
385
|
-
mapOf("assets/image.png" to imageContent),
|
|
386
|
-
),
|
|
387
|
-
)
|
|
388
|
-
|
|
389
|
-
val progress = CopyOnWriteArrayList<UpdateProgressPayload>()
|
|
390
|
-
service.updateBundle(
|
|
391
|
-
bundleId = "target-bundle",
|
|
392
|
-
manifestUrl = "https://example.com/manifest.json",
|
|
393
|
-
manifestFileHash = sha256(rootDir, manifest),
|
|
394
|
-
assets =
|
|
395
|
-
assets.mapValues { (path, hash) ->
|
|
396
|
-
ChangedAssetDescriptor(
|
|
397
|
-
fileUrl = "https://example.com/$path",
|
|
398
|
-
fileHash = hash,
|
|
399
|
-
)
|
|
400
|
-
},
|
|
401
|
-
progressCallback = { progress.add(it) },
|
|
402
|
-
)
|
|
403
|
-
|
|
404
|
-
assertEquals(1, progress.last().details?.totalFilesCount)
|
|
405
|
-
assertEquals(
|
|
406
|
-
listOf("index.android.bundle"),
|
|
407
|
-
progress
|
|
408
|
-
.last()
|
|
409
|
-
.details
|
|
410
|
-
?.files
|
|
411
|
-
?.map { it.path },
|
|
412
|
-
)
|
|
413
|
-
assertTrue(progress.any { it.progress in 0.15..0.2 && it.details?.totalFilesCount == 0 })
|
|
414
|
-
val targetDir = File(bundleStoreDir(rootDir), "target-bundle")
|
|
415
|
-
assertEquals(bundleContent, File(targetDir, "index.android.bundle").readText())
|
|
416
|
-
assertEquals(imageContent, File(targetDir, "assets/image.png").readText())
|
|
417
|
-
assertEquals("target-bundle", loadMetadata(rootDir)?.stagingBundleId)
|
|
418
|
-
assertEquals(
|
|
419
|
-
listOf(
|
|
420
|
-
"https://example.com/manifest.json",
|
|
421
|
-
"https://example.com/index.android.bundle",
|
|
422
|
-
),
|
|
423
|
-
downloads.calls,
|
|
424
|
-
)
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
@Test
|
|
428
|
-
fun `manifest driven install downloads original when matching current asset is corrupt`() =
|
|
429
|
-
runBlocking {
|
|
430
|
-
val rootDir = temporaryFolder.newFolder("corrupt-current-asset")
|
|
431
|
-
val preferences = InMemoryPreferencesService()
|
|
432
|
-
val targetContent = "verified-target-bundle"
|
|
433
|
-
val targetHash = sha256(rootDir, targetContent)
|
|
434
|
-
val activeDir = createBundleDir(rootDir, "active-bundle")
|
|
435
|
-
val activeBundleFile = writeFile(activeDir, "index.android.bundle", "corrupt")
|
|
436
|
-
File(activeDir, "manifest.json").writeText(
|
|
437
|
-
manifestJson("active-bundle", mapOf("index.android.bundle" to targetHash)),
|
|
438
|
-
)
|
|
439
|
-
preferences.setItem("HotUpdaterBundleURL", activeBundleFile.absolutePath)
|
|
440
|
-
|
|
441
|
-
val targetManifest =
|
|
442
|
-
manifestJson("target-bundle", mapOf("index.android.bundle" to targetHash))
|
|
443
|
-
val downloads =
|
|
444
|
-
MappingDownloadService(
|
|
445
|
-
mapOf(
|
|
446
|
-
"https://example.com/manifest.json" to targetManifest,
|
|
447
|
-
"https://example.com/index.android.bundle" to targetContent,
|
|
448
|
-
),
|
|
449
|
-
)
|
|
450
|
-
val service = createService(rootDir, preferences, downloads)
|
|
451
|
-
|
|
452
|
-
service.updateBundle(
|
|
453
|
-
bundleId = "target-bundle",
|
|
454
|
-
manifestUrl = "https://example.com/manifest.json",
|
|
455
|
-
manifestFileHash = sha256(rootDir, targetManifest),
|
|
456
|
-
assets =
|
|
457
|
-
mapOf(
|
|
458
|
-
"index.android.bundle" to
|
|
459
|
-
ChangedAssetDescriptor(
|
|
460
|
-
fileUrl = "https://example.com/index.android.bundle",
|
|
461
|
-
fileHash = targetHash,
|
|
462
|
-
),
|
|
463
|
-
),
|
|
464
|
-
progressCallback = {},
|
|
465
|
-
)
|
|
466
|
-
|
|
467
|
-
assertEquals(
|
|
468
|
-
targetContent,
|
|
469
|
-
File(bundleStoreDir(rootDir), "target-bundle/index.android.bundle").readText(),
|
|
470
|
-
)
|
|
471
|
-
assertTrue(downloads.calls.contains("https://example.com/index.android.bundle"))
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
@Test
|
|
475
|
-
fun `catalog high water rejects replay and survives channel reset`() =
|
|
476
|
-
runBlocking {
|
|
477
|
-
val rootDir = temporaryFolder.newFolder("release-high-water")
|
|
478
|
-
val service = createService(rootDir)
|
|
479
|
-
|
|
480
|
-
assertTrue(
|
|
481
|
-
service.acceptReleaseCatalog(
|
|
482
|
-
catalogId = "project-a",
|
|
483
|
-
scopeKey = "scope-production",
|
|
484
|
-
generation = 2,
|
|
485
|
-
catalogHash = "hash-2",
|
|
486
|
-
channel = "production",
|
|
487
|
-
selectionContextHash = "context-2",
|
|
488
|
-
),
|
|
489
|
-
)
|
|
490
|
-
assertFalse(
|
|
491
|
-
service.acceptReleaseCatalog(
|
|
492
|
-
catalogId = "project-a",
|
|
493
|
-
scopeKey = "scope-production",
|
|
494
|
-
generation = 1,
|
|
495
|
-
catalogHash = "hash-1",
|
|
496
|
-
channel = "production",
|
|
497
|
-
selectionContextHash = "context-1",
|
|
498
|
-
),
|
|
499
|
-
)
|
|
500
|
-
assertFalse(
|
|
501
|
-
service.acceptReleaseCatalog(
|
|
502
|
-
catalogId = "project-a",
|
|
503
|
-
scopeKey = "scope-production",
|
|
504
|
-
generation = 2,
|
|
505
|
-
catalogHash = "different-hash",
|
|
506
|
-
channel = "production",
|
|
507
|
-
selectionContextHash = "context-2",
|
|
508
|
-
),
|
|
509
|
-
)
|
|
510
|
-
|
|
511
|
-
assertTrue(service.resetChannel())
|
|
512
|
-
val metadata = loadMetadata(rootDir)
|
|
513
|
-
assertEquals(
|
|
514
|
-
CatalogHighWater(generation = 2, catalogHash = "hash-2"),
|
|
515
|
-
metadata?.highestSeenCatalogs?.get("project-a|scope-production"),
|
|
516
|
-
)
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
@Test
|
|
520
|
-
fun `same bundle adoption refreshes its receipt without changing bytes`() =
|
|
521
|
-
runBlocking {
|
|
522
|
-
val rootDir = temporaryFolder.newFolder("same-bundle-adoption")
|
|
523
|
-
val service = createService(rootDir)
|
|
524
|
-
val bundleDir = createBundleDir(rootDir, "bundle-one")
|
|
525
|
-
writeFile(bundleDir, "index.android.bundle")
|
|
526
|
-
writeManifest(bundleDir, listOf("index.android.bundle"))
|
|
527
|
-
val oldSelection =
|
|
528
|
-
releaseSelection(
|
|
529
|
-
releaseId = "release-one",
|
|
530
|
-
bundleId = bundleDir.name,
|
|
531
|
-
generation = 1,
|
|
532
|
-
catalogHash = "hash-1",
|
|
533
|
-
selectionContextHash = "context-1",
|
|
534
|
-
)
|
|
535
|
-
writeMetadata(
|
|
536
|
-
rootDir,
|
|
537
|
-
BundleMetadata(
|
|
538
|
-
isolationKey = TEST_ISOLATION_KEY,
|
|
539
|
-
stagingBundleId = bundleDir.name,
|
|
540
|
-
stagingSelection = oldSelection,
|
|
541
|
-
),
|
|
542
|
-
)
|
|
543
|
-
assertTrue(
|
|
544
|
-
service.acceptReleaseCatalog(
|
|
545
|
-
catalogId = "project-a",
|
|
546
|
-
scopeKey = "scope-production",
|
|
547
|
-
generation = 2,
|
|
548
|
-
catalogHash = "hash-2",
|
|
549
|
-
channel = "production",
|
|
550
|
-
selectionContextHash = "context-2",
|
|
551
|
-
),
|
|
552
|
-
)
|
|
553
|
-
val newSelection =
|
|
554
|
-
releaseSelection(
|
|
555
|
-
releaseId = "release-two",
|
|
556
|
-
bundleId = bundleDir.name,
|
|
557
|
-
generation = 2,
|
|
558
|
-
catalogHash = "hash-2",
|
|
559
|
-
selectionContextHash = "context-2",
|
|
560
|
-
)
|
|
561
|
-
|
|
562
|
-
assertTrue(service.commitReleaseSelection(newSelection))
|
|
563
|
-
|
|
564
|
-
assertTrue(bundleDir.isDirectory)
|
|
565
|
-
assertEquals(newSelection, loadMetadata(rootDir)?.stagingSelection)
|
|
566
|
-
assertFalse(loadMetadata(rootDir)?.verificationPending ?: true)
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
@Test
|
|
570
|
-
fun `crash restores the complete stable receipt but retains newer high water`() {
|
|
571
|
-
val rootDir = temporaryFolder.newFolder("release-crash-ledger")
|
|
572
|
-
val service = createService(rootDir)
|
|
573
|
-
val stableDir = createBundleDir(rootDir, "bundle-one")
|
|
574
|
-
writeFile(stableDir, "index.android.bundle")
|
|
575
|
-
writeManifest(stableDir, listOf("index.android.bundle"))
|
|
576
|
-
val stagingDir = createBundleDir(rootDir, "bundle-two")
|
|
577
|
-
writeFile(stagingDir, "index.android.bundle")
|
|
578
|
-
writeManifest(stagingDir, listOf("index.android.bundle"))
|
|
579
|
-
val stableSelection =
|
|
580
|
-
releaseSelection("release-one", stableDir.name, 1, "hash-1", "context-1")
|
|
581
|
-
val stagingSelection =
|
|
582
|
-
releaseSelection("release-two", stagingDir.name, 2, "hash-2", "context-2")
|
|
583
|
-
writeMetadata(
|
|
584
|
-
rootDir,
|
|
585
|
-
BundleMetadata(
|
|
586
|
-
isolationKey = TEST_ISOLATION_KEY,
|
|
587
|
-
stableBundleId = stableDir.name,
|
|
588
|
-
stagingBundleId = stagingDir.name,
|
|
589
|
-
stableSelection = stableSelection,
|
|
590
|
-
stagingSelection = stagingSelection,
|
|
591
|
-
pendingUpdateStrategy = "appVersion",
|
|
592
|
-
pendingTransition =
|
|
593
|
-
PendingSelectionTransition(
|
|
594
|
-
fromReleaseId = stableSelection.releaseId,
|
|
595
|
-
fromBundleId = stableSelection.bundleId,
|
|
596
|
-
toReleaseId = stagingSelection.releaseId,
|
|
597
|
-
toBundleId = stagingSelection.bundleId,
|
|
598
|
-
),
|
|
599
|
-
verificationPending = true,
|
|
600
|
-
highestSeenCatalogs =
|
|
601
|
-
mapOf(
|
|
602
|
-
"project-a|scope-production" to
|
|
603
|
-
CatalogHighWater(generation = 2, catalogHash = "hash-2"),
|
|
604
|
-
),
|
|
605
|
-
currentSelectionContexts =
|
|
606
|
-
mapOf("project-a|scope-production" to "production\ncontext-2"),
|
|
607
|
-
),
|
|
608
|
-
)
|
|
609
|
-
|
|
610
|
-
val launch =
|
|
611
|
-
service.prepareLaunch(
|
|
612
|
-
PendingCrashRecovery(
|
|
613
|
-
launchedBundleId = stagingDir.name,
|
|
614
|
-
shouldRollback = true,
|
|
615
|
-
),
|
|
616
|
-
)
|
|
617
|
-
val report = service.notifyAppReady()
|
|
618
|
-
val metadata = loadMetadata(rootDir)
|
|
619
|
-
|
|
620
|
-
assertEquals(stableDir.name, launch.launchedBundleId)
|
|
621
|
-
assertEquals("RECOVERED", report["status"])
|
|
622
|
-
assertEquals("release-two", report["fromReleaseId"])
|
|
623
|
-
assertEquals("release-one", report["toReleaseId"])
|
|
624
|
-
assertEquals(stableSelection, metadata?.stagingSelection)
|
|
625
|
-
assertEquals(
|
|
626
|
-
CatalogHighWater(generation = 2, catalogHash = "hash-2"),
|
|
627
|
-
metadata?.highestSeenCatalogs?.get("project-a|scope-production"),
|
|
628
|
-
)
|
|
629
|
-
assertTrue(service.getCrashHistory().contains(stagingDir.name))
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
@Test
|
|
633
|
-
fun `manifest driven install moves blocking work off caller dispatcher`() {
|
|
634
|
-
val rootDir = temporaryFolder.newFolder("manifest-install-dispatcher")
|
|
635
|
-
val preferences = InMemoryPreferencesService()
|
|
636
|
-
val downloadService = RecordingFailedDownloadService()
|
|
637
|
-
val service = createService(rootDir, preferences, downloadService)
|
|
638
|
-
val activeDir = createBundleDir(rootDir, "active-bundle")
|
|
639
|
-
val activeBundleFile = writeFile(activeDir, "index.android.bundle")
|
|
640
|
-
writeManifest(activeDir, listOf("index.android.bundle"))
|
|
641
|
-
|
|
642
|
-
preferences.setItem("HotUpdaterBundleURL", activeBundleFile.absolutePath)
|
|
643
|
-
|
|
644
|
-
Executors
|
|
645
|
-
.newSingleThreadExecutor { runnable -> Thread(runnable, "manifest-caller") }
|
|
646
|
-
.asCoroutineDispatcher()
|
|
647
|
-
.use { callerDispatcher ->
|
|
648
|
-
runBlocking(callerDispatcher) {
|
|
649
|
-
val result =
|
|
650
|
-
runCatching {
|
|
651
|
-
service.updateBundle(
|
|
652
|
-
bundleId = "target-bundle",
|
|
653
|
-
manifestUrl = "https://example.com/manifest.json",
|
|
654
|
-
manifestFileHash = "manifest-hash",
|
|
655
|
-
assets = emptyMap(),
|
|
656
|
-
progressCallback = {},
|
|
657
|
-
)
|
|
658
|
-
}
|
|
659
|
-
assertTrue(result.exceptionOrNull() is HotUpdaterException)
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
val manifestCall = downloadService.calls.first()
|
|
664
|
-
assertEquals("https://example.com/manifest.json", manifestCall.first)
|
|
665
|
-
assertFalse(
|
|
666
|
-
"Manifest install ran on the caller dispatcher: ${manifestCall.second}",
|
|
667
|
-
manifestCall.second.contains("manifest-caller"),
|
|
668
|
-
)
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
@Test
|
|
672
|
-
fun `reused bytes cannot bypass mismatched descriptor contracts`() =
|
|
673
|
-
runBlocking {
|
|
674
|
-
for (kind in listOf("missing", "mismatch", "extra", "missing-original")) {
|
|
675
|
-
val root = temporaryFolder.newFolder(kind)
|
|
676
|
-
val hash = sha256(root, "target")
|
|
677
|
-
val manifest = manifestJson("target", mapOf("index.android.bundle" to hash))
|
|
678
|
-
val downloads = MappingDownloadService(mapOf("https://example.com/manifest.json" to manifest))
|
|
679
|
-
val service =
|
|
680
|
-
createService(
|
|
681
|
-
root,
|
|
682
|
-
downloadService = downloads,
|
|
683
|
-
builtInAssetResolver = MappingBuiltInAssetResolver(mapOf("index.android.bundle" to "target")),
|
|
684
|
-
)
|
|
685
|
-
val descriptors = mutableMapOf("index.android.bundle" to ChangedAssetDescriptor("https://example.com/bundle", hash))
|
|
686
|
-
when (kind) {
|
|
687
|
-
"missing" -> {
|
|
688
|
-
descriptors.clear()
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
"mismatch" -> {
|
|
692
|
-
descriptors["index.android.bundle"] = ChangedAssetDescriptor("https://example.com/bundle", "wrong")
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
"extra" -> {
|
|
696
|
-
descriptors["extra.png"] = ChangedAssetDescriptor("https://example.com/extra", hash)
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
"missing-original" -> {
|
|
700
|
-
descriptors["index.android.bundle"] = ChangedAssetDescriptor(null, hash)
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
val result =
|
|
704
|
-
runCatching {
|
|
705
|
-
service.updateBundle(
|
|
706
|
-
"target",
|
|
707
|
-
"https://example.com/manifest.json",
|
|
708
|
-
sha256(root, manifest),
|
|
709
|
-
descriptors,
|
|
710
|
-
) {}
|
|
711
|
-
}
|
|
712
|
-
assertTrue("Contract $kind must fail closed", result.isFailure)
|
|
713
|
-
assertNull(loadMetadata(root)?.stagingBundleId)
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
@Test
|
|
718
|
-
fun `cached target is repaired before activation`() =
|
|
719
|
-
runBlocking {
|
|
720
|
-
val root = temporaryFolder.newFolder("corrupt-cached-target")
|
|
721
|
-
val hash = sha256(root, "correct")
|
|
722
|
-
val manifest = manifestJson("target", mapOf("index.android.bundle" to hash))
|
|
723
|
-
val cached = createBundleDir(root, "target")
|
|
724
|
-
writeFile(cached, "manifest.json", manifest)
|
|
725
|
-
writeFile(cached, "index.android.bundle", "CORRUPT")
|
|
726
|
-
val downloads =
|
|
727
|
-
MappingDownloadService(
|
|
728
|
-
mapOf(
|
|
729
|
-
"https://example.com/manifest.json" to manifest,
|
|
730
|
-
"https://example.com/bundle" to "correct",
|
|
731
|
-
),
|
|
732
|
-
)
|
|
733
|
-
val service = createService(root, downloadService = downloads, builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()))
|
|
734
|
-
service.updateBundle(
|
|
735
|
-
"target",
|
|
736
|
-
"https://example.com/manifest.json",
|
|
737
|
-
sha256(root, manifest),
|
|
738
|
-
mapOf("index.android.bundle" to ChangedAssetDescriptor("https://example.com/bundle", hash)),
|
|
739
|
-
) {}
|
|
740
|
-
assertEquals("correct", File(cached, "index.android.bundle").readText())
|
|
741
|
-
assertEquals("target", loadMetadata(root)?.stagingBundleId)
|
|
742
|
-
assertTrue(downloads.calls.contains("https://example.com/bundle"))
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
@Test
|
|
746
|
-
fun `failed final rename restores existing bundle`() =
|
|
747
|
-
runBlocking {
|
|
748
|
-
val root = temporaryFolder.newFolder("failed-final-rename")
|
|
749
|
-
val preferences = InMemoryPreferencesService()
|
|
750
|
-
val target = createCompleteBundle(root, "target", "old")
|
|
751
|
-
val oldBundleFile = File(target, "index.android.bundle")
|
|
752
|
-
preferences.setItem("HotUpdaterBundleURL", oldBundleFile.absolutePath)
|
|
753
|
-
writeMetadata(
|
|
754
|
-
root,
|
|
755
|
-
BundleMetadata(
|
|
756
|
-
isolationKey = TEST_ISOLATION_KEY,
|
|
757
|
-
stagingBundleId = "target",
|
|
758
|
-
),
|
|
759
|
-
)
|
|
760
|
-
val manifest = manifestJson("target", mapOf("index.android.bundle" to sha256(root, "new")))
|
|
761
|
-
val downloads =
|
|
762
|
-
MappingDownloadService(
|
|
763
|
-
mapOf(
|
|
764
|
-
"https://example.com/manifest.json" to manifest,
|
|
765
|
-
"https://example.com/bundle" to "new",
|
|
766
|
-
),
|
|
767
|
-
)
|
|
768
|
-
val service =
|
|
769
|
-
createService(
|
|
770
|
-
root,
|
|
771
|
-
preferences = preferences,
|
|
772
|
-
downloadService = downloads,
|
|
773
|
-
builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()),
|
|
774
|
-
directoryRenamer = { source, destination ->
|
|
775
|
-
if (source.name == "target.tmp" && destination.name == "target") {
|
|
776
|
-
false
|
|
777
|
-
} else {
|
|
778
|
-
source.renameTo(destination)
|
|
779
|
-
}
|
|
780
|
-
},
|
|
781
|
-
)
|
|
782
|
-
|
|
783
|
-
val result =
|
|
784
|
-
runCatching {
|
|
785
|
-
service.updateBundle(
|
|
786
|
-
"target",
|
|
787
|
-
"https://example.com/manifest.json",
|
|
788
|
-
sha256(root, manifest),
|
|
789
|
-
mapOf(
|
|
790
|
-
"index.android.bundle" to
|
|
791
|
-
ChangedAssetDescriptor("https://example.com/bundle", sha256(root, "new")),
|
|
792
|
-
),
|
|
793
|
-
) {}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
assertUpdateFailure(
|
|
797
|
-
result.exceptionOrNull(),
|
|
798
|
-
"MOVE_OPERATION_FAILED",
|
|
799
|
-
UpdateFailure.install(UpdateFailureReason.STORAGE),
|
|
800
|
-
)
|
|
801
|
-
assertEquals("old", oldBundleFile.readText())
|
|
802
|
-
assertFalse(File(bundleStoreDir(root), "target.install-backup").exists())
|
|
803
|
-
assertEquals(oldBundleFile.absolutePath, preferences.getItem("HotUpdaterBundleURL"))
|
|
804
|
-
assertEquals("target", loadMetadata(root)?.stagingBundleId)
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
@Test
|
|
808
|
-
fun `metadata write failure restores preference and existing bundle`() =
|
|
809
|
-
runBlocking {
|
|
810
|
-
val root = temporaryFolder.newFolder("failed-metadata-write")
|
|
811
|
-
val preferences = InMemoryPreferencesService()
|
|
812
|
-
val target = createCompleteBundle(root, "target", "old")
|
|
813
|
-
val oldBundleFile = File(target, "index.android.bundle")
|
|
814
|
-
preferences.setItem("HotUpdaterBundleURL", oldBundleFile.absolutePath)
|
|
815
|
-
val previousMetadata =
|
|
816
|
-
BundleMetadata(
|
|
817
|
-
isolationKey = TEST_ISOLATION_KEY,
|
|
818
|
-
stagingBundleId = "target",
|
|
819
|
-
)
|
|
820
|
-
writeMetadata(root, previousMetadata)
|
|
821
|
-
val newHash = sha256(root, "new")
|
|
822
|
-
val manifest = manifestJson("target", mapOf("index.android.bundle" to newHash))
|
|
823
|
-
val service =
|
|
824
|
-
createService(
|
|
825
|
-
root,
|
|
826
|
-
preferences = preferences,
|
|
827
|
-
downloadService =
|
|
828
|
-
MappingDownloadService(
|
|
829
|
-
mapOf(
|
|
830
|
-
"https://example.com/manifest.json" to manifest,
|
|
831
|
-
"https://example.com/bundle" to "new",
|
|
832
|
-
),
|
|
833
|
-
),
|
|
834
|
-
builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()),
|
|
835
|
-
metadataWriter = { _, _ -> false },
|
|
836
|
-
)
|
|
837
|
-
|
|
838
|
-
val result =
|
|
839
|
-
runCatching {
|
|
840
|
-
service.updateBundle(
|
|
841
|
-
"target",
|
|
842
|
-
"https://example.com/manifest.json",
|
|
843
|
-
sha256(root, manifest),
|
|
844
|
-
mapOf(
|
|
845
|
-
"index.android.bundle" to ChangedAssetDescriptor("https://example.com/bundle", newHash),
|
|
846
|
-
),
|
|
847
|
-
) {}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
assertUpdateFailure(
|
|
851
|
-
result.exceptionOrNull(),
|
|
852
|
-
"UNKNOWN_ERROR",
|
|
853
|
-
UpdateFailure.install(UpdateFailureReason.STORAGE),
|
|
854
|
-
)
|
|
855
|
-
assertEquals("old", oldBundleFile.readText())
|
|
856
|
-
assertFalse(File(bundleStoreDir(root), "target.install-backup").exists())
|
|
857
|
-
assertEquals(oldBundleFile.absolutePath, preferences.getItem("HotUpdaterBundleURL"))
|
|
858
|
-
assertEquals(previousMetadata.stagingBundleId, loadMetadata(root)?.stagingBundleId)
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
@Test
|
|
862
|
-
fun `preference write failure aborts before metadata commit and restores existing bundle`() =
|
|
863
|
-
runBlocking {
|
|
864
|
-
val root = temporaryFolder.newFolder("failed-preference-write")
|
|
865
|
-
val target = createCompleteBundle(root, "target", "old")
|
|
866
|
-
val oldBundleFile = File(target, "index.android.bundle")
|
|
867
|
-
var preferenceWrites = 0
|
|
868
|
-
val preferences =
|
|
869
|
-
InMemoryPreferencesService { key, _ ->
|
|
870
|
-
if (key == "HotUpdaterBundleURL") preferenceWrites++
|
|
871
|
-
preferenceWrites == 2
|
|
872
|
-
}
|
|
873
|
-
preferences.setItem("HotUpdaterBundleURL", oldBundleFile.absolutePath)
|
|
874
|
-
writeMetadata(
|
|
875
|
-
root,
|
|
876
|
-
BundleMetadata(
|
|
877
|
-
isolationKey = TEST_ISOLATION_KEY,
|
|
878
|
-
stagingBundleId = "target",
|
|
879
|
-
),
|
|
880
|
-
)
|
|
881
|
-
val newHash = sha256(root, "new")
|
|
882
|
-
val manifest = manifestJson("target", mapOf("index.android.bundle" to newHash))
|
|
883
|
-
var metadataWrites = 0
|
|
884
|
-
val service =
|
|
885
|
-
createService(
|
|
886
|
-
root,
|
|
887
|
-
preferences = preferences,
|
|
888
|
-
downloadService =
|
|
889
|
-
MappingDownloadService(
|
|
890
|
-
mapOf(
|
|
891
|
-
"https://example.com/manifest.json" to manifest,
|
|
892
|
-
"https://example.com/bundle" to "new",
|
|
893
|
-
),
|
|
894
|
-
),
|
|
895
|
-
builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()),
|
|
896
|
-
metadataWriter = { metadata, file ->
|
|
897
|
-
metadataWrites++
|
|
898
|
-
metadata.saveToFile(file)
|
|
899
|
-
},
|
|
900
|
-
)
|
|
901
|
-
|
|
902
|
-
val result =
|
|
903
|
-
runCatching {
|
|
904
|
-
service.updateBundle(
|
|
905
|
-
"target",
|
|
906
|
-
"https://example.com/manifest.json",
|
|
907
|
-
sha256(root, manifest),
|
|
908
|
-
mapOf(
|
|
909
|
-
"index.android.bundle" to ChangedAssetDescriptor("https://example.com/bundle", newHash),
|
|
910
|
-
),
|
|
911
|
-
) {}
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
assertUpdateFailure(
|
|
915
|
-
result.exceptionOrNull(),
|
|
916
|
-
"UNKNOWN_ERROR",
|
|
917
|
-
UpdateFailure.install(UpdateFailureReason.STORAGE),
|
|
918
|
-
)
|
|
919
|
-
assertEquals(0, metadataWrites)
|
|
920
|
-
assertEquals("old", oldBundleFile.readText())
|
|
921
|
-
assertEquals(oldBundleFile.absolutePath, preferences.getItem("HotUpdaterBundleURL"))
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
@Test
|
|
925
|
-
fun `startup restores interrupted promotion backup when final is absent`() {
|
|
926
|
-
val root = temporaryFolder.newFolder("missing-final-recovery")
|
|
927
|
-
val backup = createCompleteBundle(root, "target.install-backup", "old", manifestBundleId = "target")
|
|
928
|
-
|
|
929
|
-
createService(root)
|
|
930
|
-
|
|
931
|
-
val restored = File(bundleStoreDir(root), "target")
|
|
932
|
-
assertEquals("old", File(restored, "index.android.bundle").readText())
|
|
933
|
-
assertFalse(backup.exists())
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
@Test
|
|
937
|
-
fun `startup keeps complete final when install backup also exists`() {
|
|
938
|
-
val root = temporaryFolder.newFolder("complete-final-recovery")
|
|
939
|
-
val final = createCompleteBundle(root, "target", "new")
|
|
940
|
-
val backup = createCompleteBundle(root, "target.install-backup", "old", manifestBundleId = "target")
|
|
941
|
-
|
|
942
|
-
createService(root)
|
|
943
|
-
|
|
944
|
-
assertEquals("new", File(final, "index.android.bundle").readText())
|
|
945
|
-
assertFalse(backup.exists())
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
@Test
|
|
949
|
-
fun `retry reuses completed staging files and replaces partial files without stale assets`() =
|
|
950
|
-
runBlocking {
|
|
951
|
-
val root = temporaryFolder.newFolder("interrupted-retry")
|
|
952
|
-
val hashes = mapOf("index.android.bundle" to sha256(root, "correct"), "assets/image.png" to sha256(root, "image"))
|
|
953
|
-
val manifest = manifestJson("target", hashes)
|
|
954
|
-
val staging = createBundleDir(root, "target.tmp")
|
|
955
|
-
writeFile(staging, "index.android.bundle", "partial")
|
|
956
|
-
writeFile(staging, "assets/image.png", "image")
|
|
957
|
-
writeFile(staging, "assets/obsolete.png", "obsolete")
|
|
958
|
-
val downloads =
|
|
959
|
-
MappingDownloadService(
|
|
960
|
-
mapOf(
|
|
961
|
-
"https://example.com/manifest.json" to manifest,
|
|
962
|
-
"https://example.com/index.android.bundle" to "correct",
|
|
963
|
-
),
|
|
964
|
-
)
|
|
965
|
-
val service = createService(root, downloadService = downloads, builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()))
|
|
966
|
-
service.updateBundle(
|
|
967
|
-
"target",
|
|
968
|
-
"https://example.com/manifest.json",
|
|
969
|
-
sha256(root, manifest),
|
|
970
|
-
hashes.mapValues { (path, hash) -> ChangedAssetDescriptor("https://example.com/$path", hash) },
|
|
971
|
-
) {}
|
|
972
|
-
assertEquals(listOf("https://example.com/manifest.json", "https://example.com/index.android.bundle"), downloads.calls)
|
|
973
|
-
val target = File(bundleStoreDir(root), "target")
|
|
974
|
-
assertEquals("image", File(target, "assets/image.png").readText())
|
|
975
|
-
assertEquals("correct", File(target, "index.android.bundle").readText())
|
|
976
|
-
assertFalse(File(target, "assets/obsolete.png").exists())
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
@Test
|
|
980
|
-
fun `independent downloads overlap with at most four active requests`() =
|
|
981
|
-
runBlocking {
|
|
982
|
-
val root = temporaryFolder.newFolder("parallel-originals")
|
|
983
|
-
val paths = listOf("index.android.bundle") + (0..8).map { "assets/$it.png" }
|
|
984
|
-
val hash = sha256(root, "content")
|
|
985
|
-
val manifest = manifestJson("target", paths.associateWith { hash })
|
|
986
|
-
val mapping =
|
|
987
|
-
MappingDownloadService(
|
|
988
|
-
mapOf("https://example.com/manifest.json" to manifest) + paths.associate { "https://example.com/$it" to "content" },
|
|
989
|
-
)
|
|
990
|
-
val active = AtomicInteger()
|
|
991
|
-
val maximum = AtomicInteger()
|
|
992
|
-
val downloader =
|
|
993
|
-
object : DownloadService {
|
|
994
|
-
override suspend fun downloadFile(
|
|
995
|
-
fileUrl: URL,
|
|
996
|
-
destination: File,
|
|
997
|
-
fileSizeCallback: ((Long) -> Unit)?,
|
|
998
|
-
progressCallback: (DownloadProgress) -> Unit,
|
|
999
|
-
): DownloadResult {
|
|
1000
|
-
val isAsset = fileUrl.path != "/manifest.json"
|
|
1001
|
-
if (isAsset) {
|
|
1002
|
-
val count = active.incrementAndGet()
|
|
1003
|
-
maximum.updateAndGet { maxOf(it, count) }
|
|
1004
|
-
}
|
|
1005
|
-
try {
|
|
1006
|
-
delay(25)
|
|
1007
|
-
return mapping.downloadFile(fileUrl, destination, fileSizeCallback, progressCallback)
|
|
1008
|
-
} finally {
|
|
1009
|
-
if (isAsset) active.decrementAndGet()
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
val service = createService(root, downloadService = downloader, builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()))
|
|
1014
|
-
val progress = CopyOnWriteArrayList<UpdateProgressPayload>()
|
|
1015
|
-
service.updateBundle(
|
|
1016
|
-
"target",
|
|
1017
|
-
"https://example.com/manifest.json",
|
|
1018
|
-
sha256(root, manifest),
|
|
1019
|
-
paths.associateWith { ChangedAssetDescriptor("https://example.com/$it", hash) },
|
|
1020
|
-
progressCallback = progress::add,
|
|
1021
|
-
)
|
|
1022
|
-
assertTrue("Expected concurrent requests, observed ${maximum.get()}", maximum.get() > 1)
|
|
1023
|
-
assertTrue("Concurrency must be bounded", maximum.get() <= 4)
|
|
1024
|
-
assertEquals(paths.size, progress.last().details?.completedFilesCount)
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
@Test
|
|
1028
|
-
fun `archive installs as one actual network file at equal planned bytes`() =
|
|
1029
|
-
runBlocking {
|
|
1030
|
-
val root = temporaryFolder.newFolder("archive-selected")
|
|
1031
|
-
val archiveBytes = Base64.getDecoder().decode(TarBrArchiveExtractorTest.VALID)
|
|
1032
|
-
val contents = mapOf("assets/image.png" to "image", "index.android.bundle" to "bundle")
|
|
1033
|
-
val hashes = contents.mapValues { sha256(root, it.value) }
|
|
1034
|
-
val manifest =
|
|
1035
|
-
manifestJson(
|
|
1036
|
-
bundleId = "target",
|
|
1037
|
-
assets = hashes,
|
|
1038
|
-
byteSizes =
|
|
1039
|
-
contents.mapValues {
|
|
1040
|
-
it.value
|
|
1041
|
-
.toByteArray()
|
|
1042
|
-
.size
|
|
1043
|
-
.toLong()
|
|
1044
|
-
},
|
|
1045
|
-
downloadByteSizes = mapOf("assets/image.png" to 50, "index.android.bundle" to 55),
|
|
1046
|
-
archive = archiveJson(root, archiveBytes, tarByteSize = 3072),
|
|
1047
|
-
)
|
|
1048
|
-
val downloads =
|
|
1049
|
-
BinaryMappingDownloadService(
|
|
1050
|
-
mapOf(
|
|
1051
|
-
MANIFEST_URL to manifest.toByteArray(),
|
|
1052
|
-
ARCHIVE_URL to archiveBytes,
|
|
1053
|
-
),
|
|
1054
|
-
)
|
|
1055
|
-
val service = createService(root, downloadService = downloads, builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()))
|
|
1056
|
-
val progress = CopyOnWriteArrayList<UpdateProgressPayload>()
|
|
1057
|
-
|
|
1058
|
-
service.updateBundle(
|
|
1059
|
-
bundleId = "target",
|
|
1060
|
-
manifestUrl = MANIFEST_URL,
|
|
1061
|
-
manifestFileHash = sha256(root, manifest),
|
|
1062
|
-
assets = hashes.mapValues { (path, hash) -> ChangedAssetDescriptor("https://example.com/$path", hash) },
|
|
1063
|
-
archiveUrl = ARCHIVE_URL,
|
|
1064
|
-
progressCallback = progress::add,
|
|
1065
|
-
)
|
|
1066
|
-
|
|
1067
|
-
assertEquals(listOf(MANIFEST_URL, ARCHIVE_URL), downloads.calls)
|
|
1068
|
-
assertEquals(
|
|
1069
|
-
listOf("bundle.tar.br"),
|
|
1070
|
-
progress
|
|
1071
|
-
.last()
|
|
1072
|
-
.details
|
|
1073
|
-
?.files
|
|
1074
|
-
?.map { it.path },
|
|
1075
|
-
)
|
|
1076
|
-
assertEquals(
|
|
1077
|
-
archiveBytes.size.toLong(),
|
|
1078
|
-
progress
|
|
1079
|
-
.last()
|
|
1080
|
-
.details
|
|
1081
|
-
?.files
|
|
1082
|
-
?.single()
|
|
1083
|
-
?.downloadedBytes,
|
|
1084
|
-
)
|
|
1085
|
-
val installed = File(bundleStoreDir(root), "target")
|
|
1086
|
-
assertEquals("bundle", File(installed, "index.android.bundle").readText())
|
|
1087
|
-
assertEquals("image", File(installed, "assets/image.png").readText())
|
|
1088
|
-
val installedManifest = JSONObject(File(installed, "manifest.json").readText())
|
|
1089
|
-
assertEquals(archiveBytes.size.toLong(), installedManifest.getJSONObject("archive").getLong("downloadByteSize"))
|
|
1090
|
-
assertEquals(6, installedManifest.getJSONObject("assets").getJSONObject("index.android.bundle").getLong("byteSize"))
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
@Test
|
|
1094
|
-
fun `full network plan accepts only bounded TAR framing overhead`() =
|
|
1095
|
-
runBlocking {
|
|
1096
|
-
val contents = mapOf("assets/image.png" to "image", "index.android.bundle" to "bundle")
|
|
1097
|
-
val logicalBytes = contents.values.sumOf { it.toByteArray().size.toLong() }
|
|
1098
|
-
val archiveBytes = Base64.getDecoder().decode(TarBrArchiveExtractorTest.VALID)
|
|
1099
|
-
val individualBytes = archiveBytes.size.toLong() - 1
|
|
1100
|
-
val downloadByteSizes =
|
|
1101
|
-
mapOf(
|
|
1102
|
-
"assets/image.png" to individualBytes / 2,
|
|
1103
|
-
"index.android.bundle" to individualBytes - individualBytes / 2,
|
|
1104
|
-
)
|
|
1105
|
-
|
|
1106
|
-
val selectedRoot = temporaryFolder.newFolder("archive-framing-selected")
|
|
1107
|
-
val selectedHashes = contents.mapValues { sha256(selectedRoot, it.value) }
|
|
1108
|
-
val selectedManifest =
|
|
1109
|
-
manifestJson(
|
|
1110
|
-
bundleId = "target",
|
|
1111
|
-
assets = selectedHashes,
|
|
1112
|
-
byteSizes =
|
|
1113
|
-
contents.mapValues {
|
|
1114
|
-
it.value
|
|
1115
|
-
.toByteArray()
|
|
1116
|
-
.size
|
|
1117
|
-
.toLong()
|
|
1118
|
-
},
|
|
1119
|
-
downloadByteSizes = downloadByteSizes,
|
|
1120
|
-
archive = archiveJson(selectedRoot, archiveBytes, tarByteSize = 3072),
|
|
1121
|
-
)
|
|
1122
|
-
val selectedDownloads =
|
|
1123
|
-
BinaryMappingDownloadService(
|
|
1124
|
-
mapOf(MANIFEST_URL to selectedManifest.toByteArray(), ARCHIVE_URL to archiveBytes),
|
|
1125
|
-
)
|
|
1126
|
-
createService(
|
|
1127
|
-
selectedRoot,
|
|
1128
|
-
downloadService = selectedDownloads,
|
|
1129
|
-
builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()),
|
|
1130
|
-
).updateBundle(
|
|
1131
|
-
bundleId = "target",
|
|
1132
|
-
manifestUrl = MANIFEST_URL,
|
|
1133
|
-
manifestFileHash = sha256(selectedRoot, selectedManifest),
|
|
1134
|
-
assets = selectedHashes.mapValues { (path, hash) -> ChangedAssetDescriptor("https://example.com/$path", hash) },
|
|
1135
|
-
archiveUrl = ARCHIVE_URL,
|
|
1136
|
-
progressCallback = {},
|
|
1137
|
-
)
|
|
1138
|
-
assertEquals(listOf(MANIFEST_URL, ARCHIVE_URL), selectedDownloads.calls)
|
|
1139
|
-
|
|
1140
|
-
val declinedRoot = temporaryFolder.newFolder("archive-framing-declined")
|
|
1141
|
-
val declinedHashes = contents.mapValues { sha256(declinedRoot, it.value) }
|
|
1142
|
-
val minimumTarBytes = 1024L
|
|
1143
|
-
val firstByteOutsideBound = individualBytes + (minimumTarBytes - logicalBytes) + 1
|
|
1144
|
-
val declinedManifest =
|
|
1145
|
-
manifestJson(
|
|
1146
|
-
bundleId = "target",
|
|
1147
|
-
assets = declinedHashes,
|
|
1148
|
-
byteSizes =
|
|
1149
|
-
contents.mapValues {
|
|
1150
|
-
it.value
|
|
1151
|
-
.toByteArray()
|
|
1152
|
-
.size
|
|
1153
|
-
.toLong()
|
|
1154
|
-
},
|
|
1155
|
-
downloadByteSizes = downloadByteSizes,
|
|
1156
|
-
archive =
|
|
1157
|
-
archiveJson(declinedRoot, archiveBytes, tarByteSize = minimumTarBytes)
|
|
1158
|
-
.put("downloadByteSize", firstByteOutsideBound),
|
|
1159
|
-
)
|
|
1160
|
-
val declinedDownloads =
|
|
1161
|
-
BinaryMappingDownloadService(
|
|
1162
|
-
mapOf(MANIFEST_URL to declinedManifest.toByteArray()) +
|
|
1163
|
-
contents.mapKeys { "https://example.com/${it.key}" }.mapValues { it.value.toByteArray() },
|
|
1164
|
-
)
|
|
1165
|
-
createService(
|
|
1166
|
-
declinedRoot,
|
|
1167
|
-
downloadService = declinedDownloads,
|
|
1168
|
-
builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()),
|
|
1169
|
-
).updateBundle(
|
|
1170
|
-
bundleId = "target",
|
|
1171
|
-
manifestUrl = MANIFEST_URL,
|
|
1172
|
-
manifestFileHash = sha256(declinedRoot, declinedManifest),
|
|
1173
|
-
assets = declinedHashes.mapValues { (path, hash) -> ChangedAssetDescriptor("https://example.com/$path", hash) },
|
|
1174
|
-
archiveUrl = ARCHIVE_URL,
|
|
1175
|
-
progressCallback = {},
|
|
1176
|
-
)
|
|
1177
|
-
assertFalse(declinedDownloads.calls.contains(ARCHIVE_URL))
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
@Test
|
|
1181
|
-
fun `corrupt archive is discarded before exactly one ordinary file pass`() =
|
|
1182
|
-
runBlocking {
|
|
1183
|
-
val root = temporaryFolder.newFolder("archive-fallback")
|
|
1184
|
-
val archiveBytes = Base64.getDecoder().decode(TarBrArchiveExtractorTest.VALID)
|
|
1185
|
-
val corruptArchive = archiveBytes.clone().apply { this[lastIndex] = (this[lastIndex].toInt() xor 1).toByte() }
|
|
1186
|
-
val contents = mapOf("assets/image.png" to "image", "index.android.bundle" to "bundle")
|
|
1187
|
-
val hashes = contents.mapValues { sha256(root, it.value) }
|
|
1188
|
-
val manifest =
|
|
1189
|
-
manifestJson(
|
|
1190
|
-
bundleId = "target",
|
|
1191
|
-
assets = hashes,
|
|
1192
|
-
byteSizes =
|
|
1193
|
-
contents.mapValues {
|
|
1194
|
-
it.value
|
|
1195
|
-
.toByteArray()
|
|
1196
|
-
.size
|
|
1197
|
-
.toLong()
|
|
1198
|
-
},
|
|
1199
|
-
downloadByteSizes = contents.mapValues { 100L },
|
|
1200
|
-
archive = archiveJson(root, archiveBytes, tarByteSize = 3072),
|
|
1201
|
-
)
|
|
1202
|
-
val downloads =
|
|
1203
|
-
BinaryMappingDownloadService(
|
|
1204
|
-
mapOf(MANIFEST_URL to manifest.toByteArray(), ARCHIVE_URL to corruptArchive) +
|
|
1205
|
-
contents.mapKeys { "https://example.com/${it.key}" }.mapValues { it.value.toByteArray() },
|
|
1206
|
-
)
|
|
1207
|
-
val service = createService(root, downloadService = downloads, builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()))
|
|
1208
|
-
val progress = CopyOnWriteArrayList<UpdateProgressPayload>()
|
|
1209
|
-
|
|
1210
|
-
service.updateBundle(
|
|
1211
|
-
bundleId = "target",
|
|
1212
|
-
manifestUrl = MANIFEST_URL,
|
|
1213
|
-
manifestFileHash = sha256(root, manifest),
|
|
1214
|
-
assets = hashes.mapValues { (path, hash) -> ChangedAssetDescriptor("https://example.com/$path", hash) },
|
|
1215
|
-
archiveUrl = ARCHIVE_URL,
|
|
1216
|
-
progressCallback = progress::add,
|
|
1217
|
-
)
|
|
1218
|
-
|
|
1219
|
-
assertEquals(1, downloads.calls.count { it == ARCHIVE_URL })
|
|
1220
|
-
assertEquals(1, downloads.calls.count { it == "https://example.com/index.android.bundle" })
|
|
1221
|
-
assertEquals(1, downloads.calls.count { it == "https://example.com/assets/image.png" })
|
|
1222
|
-
assertEquals(
|
|
1223
|
-
hashes.keys.sorted(),
|
|
1224
|
-
progress
|
|
1225
|
-
.last()
|
|
1226
|
-
.details
|
|
1227
|
-
?.files
|
|
1228
|
-
?.map { it.path },
|
|
1229
|
-
)
|
|
1230
|
-
assertFalse(File(bundleStoreDir(root), "target.archive.tmp").exists())
|
|
1231
|
-
assertFalse(File(bundleStoreDir(root), "target.local.tmp").exists())
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
@Test
|
|
1235
|
-
fun `partial local reuse keeps strict cost comparison with two network files`() =
|
|
1236
|
-
runBlocking {
|
|
1237
|
-
val root = temporaryFolder.newFolder("archive-one-remaining")
|
|
1238
|
-
val archiveBytes = Base64.getDecoder().decode(TarBrArchiveExtractorTest.VALID)
|
|
1239
|
-
val contents =
|
|
1240
|
-
mapOf(
|
|
1241
|
-
"assets/image.png" to "image",
|
|
1242
|
-
"assets/other.png" to "other",
|
|
1243
|
-
"index.android.bundle" to "bundle",
|
|
1244
|
-
)
|
|
1245
|
-
val hashes = contents.mapValues { sha256(root, it.value) }
|
|
1246
|
-
val manifest =
|
|
1247
|
-
manifestJson(
|
|
1248
|
-
bundleId = "target",
|
|
1249
|
-
assets = hashes,
|
|
1250
|
-
byteSizes =
|
|
1251
|
-
contents.mapValues {
|
|
1252
|
-
it.value
|
|
1253
|
-
.toByteArray()
|
|
1254
|
-
.size
|
|
1255
|
-
.toLong()
|
|
1256
|
-
},
|
|
1257
|
-
downloadByteSizes = contents.mapValues { 1L },
|
|
1258
|
-
archive = archiveJson(root, archiveBytes, tarByteSize = 3072),
|
|
1259
|
-
)
|
|
1260
|
-
val bundleUrl = "https://example.com/index.android.bundle"
|
|
1261
|
-
val otherUrl = "https://example.com/assets/other.png"
|
|
1262
|
-
val downloads =
|
|
1263
|
-
BinaryMappingDownloadService(
|
|
1264
|
-
mapOf(
|
|
1265
|
-
MANIFEST_URL to manifest.toByteArray(),
|
|
1266
|
-
bundleUrl to "bundle".toByteArray(),
|
|
1267
|
-
otherUrl to "other".toByteArray(),
|
|
1268
|
-
),
|
|
1269
|
-
)
|
|
1270
|
-
val service =
|
|
1271
|
-
createService(
|
|
1272
|
-
rootDir = root,
|
|
1273
|
-
downloadService = downloads,
|
|
1274
|
-
builtInAssetResolver = MappingBuiltInAssetResolver(mapOf("assets/image.png" to "image")),
|
|
1275
|
-
)
|
|
1276
|
-
|
|
1277
|
-
service.updateBundle(
|
|
1278
|
-
bundleId = "target",
|
|
1279
|
-
manifestUrl = MANIFEST_URL,
|
|
1280
|
-
manifestFileHash = sha256(root, manifest),
|
|
1281
|
-
assets = hashes.mapValues { (path, hash) -> ChangedAssetDescriptor("https://example.com/$path", hash) },
|
|
1282
|
-
archiveUrl = ARCHIVE_URL,
|
|
1283
|
-
progressCallback = {},
|
|
1284
|
-
)
|
|
1285
|
-
|
|
1286
|
-
assertFalse(downloads.calls.contains(ARCHIVE_URL))
|
|
1287
|
-
assertEquals(1, downloads.calls.count { it == bundleUrl })
|
|
1288
|
-
assertEquals(1, downloads.calls.count { it == otherUrl })
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
@Test
|
|
1292
|
-
fun `archive selection declines incomplete expensive and overflowing cost plans`() =
|
|
1293
|
-
runBlocking {
|
|
1294
|
-
data class CostCase(
|
|
1295
|
-
val name: String,
|
|
1296
|
-
val downloadSizes: Map<String, Long>,
|
|
1297
|
-
val archiveSize: Long,
|
|
1298
|
-
val tarSize: Long = 3072,
|
|
1299
|
-
val logicalSizes: (Map<String, String>) -> Map<String, Long> = { contents ->
|
|
1300
|
-
contents.mapValues {
|
|
1301
|
-
it.value
|
|
1302
|
-
.toByteArray()
|
|
1303
|
-
.size
|
|
1304
|
-
.toLong()
|
|
1305
|
-
}
|
|
1306
|
-
},
|
|
1307
|
-
val descriptors: (Map<String, String>) -> Map<String, ChangedAssetDescriptor>,
|
|
1308
|
-
)
|
|
1309
|
-
|
|
1310
|
-
val cases =
|
|
1311
|
-
listOf(
|
|
1312
|
-
CostCase("missing", mapOf("index.android.bundle" to 100), 1) { hashes ->
|
|
1313
|
-
hashes.mapValues { (path, hash) -> ChangedAssetDescriptor("https://example.com/$path", hash) }
|
|
1314
|
-
},
|
|
1315
|
-
CostCase("larger", mapOf("index.android.bundle" to 50, "assets/image.png" to 50), 4000) { hashes ->
|
|
1316
|
-
hashes.mapValues { (path, hash) -> ChangedAssetDescriptor("https://example.com/$path", hash) }
|
|
1317
|
-
},
|
|
1318
|
-
CostCase("zero-archive", mapOf("index.android.bundle" to 100, "assets/image.png" to 100), 0) { hashes ->
|
|
1319
|
-
hashes.mapValues { (path, hash) -> ChangedAssetDescriptor("https://example.com/$path", hash) }
|
|
1320
|
-
},
|
|
1321
|
-
CostCase("short-tar", mapOf("index.android.bundle" to 100, "assets/image.png" to 100), 1, tarSize = 1023) { hashes ->
|
|
1322
|
-
hashes.mapValues { (path, hash) -> ChangedAssetDescriptor("https://example.com/$path", hash) }
|
|
1323
|
-
},
|
|
1324
|
-
CostCase(
|
|
1325
|
-
"negative-framing",
|
|
1326
|
-
mapOf("index.android.bundle" to 100, "assets/image.png" to 100),
|
|
1327
|
-
1,
|
|
1328
|
-
tarSize = 1024,
|
|
1329
|
-
logicalSizes = { contents -> contents.mapValues { 600L } },
|
|
1330
|
-
) { hashes ->
|
|
1331
|
-
hashes.mapValues { (path, hash) -> ChangedAssetDescriptor("https://example.com/$path", hash) }
|
|
1332
|
-
},
|
|
1333
|
-
CostCase(
|
|
1334
|
-
"logical-overflow",
|
|
1335
|
-
mapOf("index.android.bundle" to 100, "assets/image.png" to 100),
|
|
1336
|
-
1,
|
|
1337
|
-
tarSize = 9_007_199_254_740_991,
|
|
1338
|
-
logicalSizes = {
|
|
1339
|
-
mapOf(
|
|
1340
|
-
"index.android.bundle" to 9_007_199_254_740_991,
|
|
1341
|
-
"assets/image.png" to 1,
|
|
1342
|
-
)
|
|
1343
|
-
},
|
|
1344
|
-
) { hashes ->
|
|
1345
|
-
hashes.mapValues { (path, hash) -> ChangedAssetDescriptor("https://example.com/$path", hash) }
|
|
1346
|
-
},
|
|
1347
|
-
CostCase(
|
|
1348
|
-
"framed-limit-overflow",
|
|
1349
|
-
mapOf("index.android.bundle" to 9_007_199_254_740_991, "assets/image.png" to 0),
|
|
1350
|
-
1,
|
|
1351
|
-
) { hashes ->
|
|
1352
|
-
hashes.mapValues { (path, hash) -> ChangedAssetDescriptor("https://example.com/$path", hash) }
|
|
1353
|
-
},
|
|
1354
|
-
CostCase("unknown-patch", mapOf("index.android.bundle" to 100, "assets/image.png" to 100), 1) { hashes ->
|
|
1355
|
-
hashes.mapValues { (path, hash) ->
|
|
1356
|
-
ChangedAssetDescriptor(
|
|
1357
|
-
fileUrl = "https://example.com/$path",
|
|
1358
|
-
fileHash = hash,
|
|
1359
|
-
patch =
|
|
1360
|
-
if (path == "index.android.bundle") {
|
|
1361
|
-
BsdiffPatchDescriptor("bsdiff", "base", "a", "b", "https://example.com/patch")
|
|
1362
|
-
} else {
|
|
1363
|
-
null
|
|
1364
|
-
},
|
|
1365
|
-
)
|
|
1366
|
-
}
|
|
1367
|
-
},
|
|
1368
|
-
CostCase("known-patch", mapOf("index.android.bundle" to 100, "assets/image.png" to 1), 3) { hashes ->
|
|
1369
|
-
hashes.mapValues { (path, hash) ->
|
|
1370
|
-
ChangedAssetDescriptor(
|
|
1371
|
-
fileUrl = "https://example.com/$path",
|
|
1372
|
-
fileHash = hash,
|
|
1373
|
-
patch =
|
|
1374
|
-
if (path == "index.android.bundle") {
|
|
1375
|
-
BsdiffPatchDescriptor("bsdiff", "base", "a", "b", "https://example.com/patch", byteSize = 1)
|
|
1376
|
-
} else {
|
|
1377
|
-
null
|
|
1378
|
-
},
|
|
1379
|
-
)
|
|
1380
|
-
}
|
|
1381
|
-
},
|
|
1382
|
-
CostCase("negative-patch", mapOf("index.android.bundle" to 100, "assets/image.png" to 100), 1) { hashes ->
|
|
1383
|
-
hashes.mapValues { (path, hash) ->
|
|
1384
|
-
ChangedAssetDescriptor(
|
|
1385
|
-
fileUrl = "https://example.com/$path",
|
|
1386
|
-
fileHash = hash,
|
|
1387
|
-
patch =
|
|
1388
|
-
if (path == "index.android.bundle") {
|
|
1389
|
-
BsdiffPatchDescriptor("bsdiff", "base", "a", "b", "https://example.com/patch", byteSize = -1)
|
|
1390
|
-
} else {
|
|
1391
|
-
null
|
|
1392
|
-
},
|
|
1393
|
-
)
|
|
1394
|
-
}
|
|
1395
|
-
},
|
|
1396
|
-
CostCase("overflowing-patch", mapOf("index.android.bundle" to 100, "assets/image.png" to 100), 1) { hashes ->
|
|
1397
|
-
hashes.mapValues { (path, hash) ->
|
|
1398
|
-
ChangedAssetDescriptor(
|
|
1399
|
-
fileUrl = "https://example.com/$path",
|
|
1400
|
-
fileHash = hash,
|
|
1401
|
-
patch =
|
|
1402
|
-
if (path == "index.android.bundle") {
|
|
1403
|
-
BsdiffPatchDescriptor(
|
|
1404
|
-
"bsdiff",
|
|
1405
|
-
"base",
|
|
1406
|
-
"a",
|
|
1407
|
-
"b",
|
|
1408
|
-
"https://example.com/patch",
|
|
1409
|
-
byteSize = Long.MAX_VALUE,
|
|
1410
|
-
)
|
|
1411
|
-
} else {
|
|
1412
|
-
null
|
|
1413
|
-
},
|
|
1414
|
-
)
|
|
1415
|
-
}
|
|
1416
|
-
},
|
|
1417
|
-
CostCase(
|
|
1418
|
-
"overflow",
|
|
1419
|
-
mapOf("index.android.bundle" to 9_007_199_254_740_991, "assets/image.png" to 1),
|
|
1420
|
-
1,
|
|
1421
|
-
) { hashes ->
|
|
1422
|
-
hashes.mapValues { (path, hash) -> ChangedAssetDescriptor("https://example.com/$path", hash) }
|
|
1423
|
-
},
|
|
1424
|
-
)
|
|
1425
|
-
|
|
1426
|
-
for (case in cases) {
|
|
1427
|
-
val root = temporaryFolder.newFolder("archive-decline-${case.name}")
|
|
1428
|
-
val archiveBytes = Base64.getDecoder().decode(TarBrArchiveExtractorTest.VALID)
|
|
1429
|
-
val contents = mapOf("assets/image.png" to "image", "index.android.bundle" to "bundle")
|
|
1430
|
-
val hashes = contents.mapValues { sha256(root, it.value) }
|
|
1431
|
-
val manifest =
|
|
1432
|
-
manifestJson(
|
|
1433
|
-
bundleId = "target",
|
|
1434
|
-
assets = hashes,
|
|
1435
|
-
byteSizes = case.logicalSizes(contents),
|
|
1436
|
-
downloadByteSizes = case.downloadSizes,
|
|
1437
|
-
archive = archiveJson(root, archiveBytes, tarByteSize = case.tarSize).put("downloadByteSize", case.archiveSize),
|
|
1438
|
-
)
|
|
1439
|
-
val downloads =
|
|
1440
|
-
BinaryMappingDownloadService(
|
|
1441
|
-
mapOf(MANIFEST_URL to manifest.toByteArray()) +
|
|
1442
|
-
contents.mapKeys { "https://example.com/${it.key}" }.mapValues { it.value.toByteArray() },
|
|
1443
|
-
)
|
|
1444
|
-
val service =
|
|
1445
|
-
createService(root, downloadService = downloads, builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()))
|
|
1446
|
-
|
|
1447
|
-
service.updateBundle(
|
|
1448
|
-
bundleId = "target",
|
|
1449
|
-
manifestUrl = MANIFEST_URL,
|
|
1450
|
-
manifestFileHash = sha256(root, manifest),
|
|
1451
|
-
assets = case.descriptors(hashes),
|
|
1452
|
-
archiveUrl = ARCHIVE_URL,
|
|
1453
|
-
progressCallback = {},
|
|
1454
|
-
)
|
|
1455
|
-
|
|
1456
|
-
assertFalse("${case.name} must decline archive", downloads.calls.contains(ARCHIVE_URL))
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
@Test
|
|
1461
|
-
fun `download failures of the manifest or a file report network http or storage`() =
|
|
1462
|
-
runBlocking {
|
|
1463
|
-
val network = UpdateFailure.download(UpdateFailureReason.NETWORK)
|
|
1464
|
-
val cases =
|
|
1465
|
-
listOf(
|
|
1466
|
-
Triple(HttpStatusException(404, "Not Found"), "DOWNLOAD_FAILED", UpdateFailure.http(404)),
|
|
1467
|
-
Triple(
|
|
1468
|
-
HttpStatusException(403, "Forbidden", originCode = "AccessDenied"),
|
|
1469
|
-
"DOWNLOAD_FAILED",
|
|
1470
|
-
UpdateFailure.http(403, originCode = "AccessDenied"),
|
|
1471
|
-
),
|
|
1472
|
-
Triple(
|
|
1473
|
-
UnknownHostException("example.com"),
|
|
1474
|
-
"DOWNLOAD_FAILED",
|
|
1475
|
-
network.copy(transport = UpdateFailureTransport.DNS),
|
|
1476
|
-
),
|
|
1477
|
-
Triple(
|
|
1478
|
-
SocketTimeoutException("timeout"),
|
|
1479
|
-
"DOWNLOAD_FAILED",
|
|
1480
|
-
network.copy(transport = UpdateFailureTransport.TIMEOUT),
|
|
1481
|
-
),
|
|
1482
|
-
Triple(
|
|
1483
|
-
ConnectException("Connection refused"),
|
|
1484
|
-
"DOWNLOAD_FAILED",
|
|
1485
|
-
network.copy(transport = UpdateFailureTransport.CONNECTION),
|
|
1486
|
-
),
|
|
1487
|
-
Triple(
|
|
1488
|
-
SSLHandshakeException("Chain validation failed"),
|
|
1489
|
-
"DOWNLOAD_FAILED",
|
|
1490
|
-
network.copy(transport = UpdateFailureTransport.TLS),
|
|
1491
|
-
),
|
|
1492
|
-
Triple(IncompleteDownloadException(expectedSize = 10, actualSize = 4), "INCOMPLETE_DOWNLOAD", network),
|
|
1493
|
-
Triple(
|
|
1494
|
-
LocalStorageException(IOException("No space left on device")),
|
|
1495
|
-
"DOWNLOAD_FAILED",
|
|
1496
|
-
UpdateFailure.install(UpdateFailureReason.STORAGE),
|
|
1497
|
-
),
|
|
1498
|
-
)
|
|
1499
|
-
val resources = mapOf(MANIFEST_URL to UpdateFailureResource.MANIFEST, BUNDLE_URL to UpdateFailureResource.FILE)
|
|
1500
|
-
|
|
1501
|
-
cases.forEachIndexed { index, (error, code, failure) ->
|
|
1502
|
-
resources.forEach { (failingUrl, resource) ->
|
|
1503
|
-
val root = temporaryFolder.newFolder("download-failure-$index-${resource.value}")
|
|
1504
|
-
val result = runOneFileUpdate(root, failures = mapOf(failingUrl to error))
|
|
1505
|
-
|
|
1506
|
-
assertUpdateFailure(result, code, failure.copy(resource = resource))
|
|
1507
|
-
assertNull(loadMetadata(root)?.stagingBundleId)
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
@Test
|
|
1513
|
-
fun `verification failures report hash mismatch signature or invalid response`() =
|
|
1514
|
-
runBlocking {
|
|
1515
|
-
val hashMismatch = UpdateFailure.download(UpdateFailureReason.HASH_MISMATCH)
|
|
1516
|
-
val hashMismatchRoot = temporaryFolder.newFolder("manifest-hash-mismatch")
|
|
1517
|
-
assertUpdateFailure(
|
|
1518
|
-
runOneFileUpdate(hashMismatchRoot, manifestFileHash = sha256(hashMismatchRoot, "another manifest")),
|
|
1519
|
-
"SIGNATURE_VERIFICATION_FAILED",
|
|
1520
|
-
hashMismatch.copy(resource = UpdateFailureResource.MANIFEST),
|
|
1521
|
-
)
|
|
1522
|
-
|
|
1523
|
-
assertUpdateFailure(
|
|
1524
|
-
runOneFileUpdate(temporaryFolder.newFolder("file-hash-mismatch"), servedFile = "tampered".toByteArray()),
|
|
1525
|
-
"SIGNATURE_VERIFICATION_FAILED",
|
|
1526
|
-
hashMismatch.copy(resource = UpdateFailureResource.FILE),
|
|
1527
|
-
)
|
|
1528
|
-
|
|
1529
|
-
HotUpdaterConfig.publicKey = "configured-public-key"
|
|
1530
|
-
try {
|
|
1531
|
-
assertUpdateFailure(
|
|
1532
|
-
runOneFileUpdate(temporaryFolder.newFolder("unsigned-with-public-key")),
|
|
1533
|
-
"SIGNATURE_VERIFICATION_FAILED",
|
|
1534
|
-
UpdateFailure.download(UpdateFailureReason.SIGNATURE).copy(resource = UpdateFailureResource.MANIFEST),
|
|
1535
|
-
)
|
|
1536
|
-
} finally {
|
|
1537
|
-
HotUpdaterConfig.publicKey = null
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
val invalidManifest =
|
|
1541
|
-
UpdateFailure.download(UpdateFailureReason.INVALID_RESPONSE).copy(resource = UpdateFailureResource.MANIFEST)
|
|
1542
|
-
val otherBundleRoot = temporaryFolder.newFolder("manifest-for-other-bundle")
|
|
1543
|
-
assertUpdateFailure(
|
|
1544
|
-
runOneFileUpdate(
|
|
1545
|
-
otherBundleRoot,
|
|
1546
|
-
manifest = manifestJson("other-bundle", mapOf("index.android.bundle" to sha256(otherBundleRoot, BUNDLE_CONTENT))),
|
|
1547
|
-
),
|
|
1548
|
-
"INVALID_BUNDLE",
|
|
1549
|
-
invalidManifest,
|
|
1550
|
-
)
|
|
1551
|
-
assertUpdateFailure(
|
|
1552
|
-
runOneFileUpdate(temporaryFolder.newFolder("unparsable-manifest"), manifest = "not json"),
|
|
1553
|
-
"INVALID_BUNDLE",
|
|
1554
|
-
invalidManifest,
|
|
1555
|
-
)
|
|
1556
|
-
assertUpdateFailure(
|
|
1557
|
-
runOneFileUpdate(temporaryFolder.newFolder("missing-platform-bundle"), assetPath = "assets/image.png"),
|
|
1558
|
-
"INVALID_BUNDLE",
|
|
1559
|
-
invalidManifest,
|
|
1560
|
-
)
|
|
1561
|
-
val malformedUrlRoot = temporaryFolder.newFolder("malformed-file-url")
|
|
1562
|
-
assertUpdateFailure(
|
|
1563
|
-
runOneFileUpdate(
|
|
1564
|
-
malformedUrlRoot,
|
|
1565
|
-
descriptor = ChangedAssetDescriptor("not a url", sha256(malformedUrlRoot, BUNDLE_CONTENT)),
|
|
1566
|
-
),
|
|
1567
|
-
"UNKNOWN_ERROR",
|
|
1568
|
-
invalidManifest.copy(resource = UpdateFailureResource.FILE),
|
|
1569
|
-
)
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
@Test
|
|
1573
|
-
fun `install failures report extract or storage`() =
|
|
1574
|
-
runBlocking {
|
|
1575
|
-
val brotliRoot = temporaryFolder.newFolder("corrupt-brotli-file")
|
|
1576
|
-
val truncatedBrotli = Base64.getDecoder().decode(TarBrArchiveExtractorTest.VALID).let { it.copyOf(it.size / 2) }
|
|
1577
|
-
assertUpdateFailure(
|
|
1578
|
-
runOneFileUpdate(
|
|
1579
|
-
brotliRoot,
|
|
1580
|
-
descriptor = ChangedAssetDescriptor(BUNDLE_URL, sha256(brotliRoot, BUNDLE_CONTENT), fileCompression = "br"),
|
|
1581
|
-
servedFile = truncatedBrotli,
|
|
1582
|
-
),
|
|
1583
|
-
"DOWNLOAD_FAILED",
|
|
1584
|
-
UpdateFailure.install(UpdateFailureReason.EXTRACT).copy(resource = UpdateFailureResource.FILE),
|
|
1585
|
-
)
|
|
1586
|
-
|
|
1587
|
-
val stagingRoot = temporaryFolder.newFolder("staging-directory-blocked")
|
|
1588
|
-
File(bundleStoreDir(stagingRoot), "target.tmp").writeText("a file where the staging directory belongs")
|
|
1589
|
-
assertUpdateFailure(
|
|
1590
|
-
runOneFileUpdate(stagingRoot),
|
|
1591
|
-
"DIRECTORY_CREATION_FAILED",
|
|
1592
|
-
UpdateFailure.install(UpdateFailureReason.STORAGE),
|
|
1593
|
-
)
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
@Test
|
|
1597
|
-
fun `unanticipated failures report unknown in the stage they happened in`() =
|
|
1598
|
-
runBlocking {
|
|
1599
|
-
val downloadRoot = temporaryFolder.newFolder("unknown-download-failure")
|
|
1600
|
-
val downloadResult =
|
|
1601
|
-
runCatching {
|
|
1602
|
-
createService(downloadRoot).updateBundle("target", MANIFEST_URL, "hash", emptyMap()) {}
|
|
1603
|
-
}.exceptionOrNull()
|
|
1604
|
-
assertUpdateFailure(
|
|
1605
|
-
downloadResult,
|
|
1606
|
-
"UNKNOWN_ERROR",
|
|
1607
|
-
UpdateFailure.download(UpdateFailureReason.UNKNOWN).copy(resource = UpdateFailureResource.MANIFEST),
|
|
1608
|
-
)
|
|
1609
|
-
assertEquals("downloadFile should not be called in these tests", downloadResult?.message)
|
|
1610
|
-
|
|
1611
|
-
val installResult =
|
|
1612
|
-
runOneFileUpdate(
|
|
1613
|
-
temporaryFolder.newFolder("unknown-install-failure"),
|
|
1614
|
-
directoryRenamer = { source, destination ->
|
|
1615
|
-
if (source.name == "target.tmp") throw SecurityException("Promotion denied")
|
|
1616
|
-
source.renameTo(destination)
|
|
1617
|
-
},
|
|
1618
|
-
)
|
|
1619
|
-
assertUpdateFailure(installResult, "UNKNOWN_ERROR", UpdateFailure.install(UpdateFailureReason.UNKNOWN))
|
|
1620
|
-
assertEquals("Promotion denied", installResult?.message)
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
@Test
|
|
1624
|
-
fun `a bundle built from changed files reports manifest delivery`() =
|
|
1625
|
-
runBlocking {
|
|
1626
|
-
val root = temporaryFolder.newFolder("manifest-delivery")
|
|
1627
|
-
val hash = sha256(root, BUNDLE_CONTENT)
|
|
1628
|
-
val manifest = manifestJson("target", mapOf("index.android.bundle" to hash))
|
|
1629
|
-
val downloads =
|
|
1630
|
-
BinaryMappingDownloadService(mapOf(MANIFEST_URL to manifest.toByteArray(), BUNDLE_URL to BUNDLE_CONTENT.toByteArray()))
|
|
1631
|
-
val service = createService(root, downloadService = downloads, builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()))
|
|
1632
|
-
val assets = mapOf("index.android.bundle" to ChangedAssetDescriptor(BUNDLE_URL, hash))
|
|
1633
|
-
|
|
1634
|
-
val downloaded = service.updateBundle("target", MANIFEST_URL, sha256(root, manifest), assets) {}
|
|
1635
|
-
// Installing it again reuses the staged file, so nothing is downloaded.
|
|
1636
|
-
val reused = service.updateBundle("target", MANIFEST_URL, sha256(root, manifest), assets) {}
|
|
1637
|
-
|
|
1638
|
-
assertEquals(UpdateBundleResult(BundleDelivery.MANIFEST, patchFallback = false), downloaded)
|
|
1639
|
-
assertEquals(UpdateBundleResult(BundleDelivery.MANIFEST, patchFallback = false), reused)
|
|
1640
|
-
assertEquals(1, downloads.calls.count { it == BUNDLE_URL })
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
@Test
|
|
1644
|
-
fun `a bundle installed from the archive reports archive delivery`() =
|
|
1645
|
-
runBlocking {
|
|
1646
|
-
val root = temporaryFolder.newFolder("archive-delivery")
|
|
1647
|
-
val archiveBytes = Base64.getDecoder().decode(TarBrArchiveExtractorTest.VALID)
|
|
1648
|
-
val contents = mapOf("assets/image.png" to "image", "index.android.bundle" to "bundle")
|
|
1649
|
-
val hashes = contents.mapValues { sha256(root, it.value) }
|
|
1650
|
-
val manifest =
|
|
1651
|
-
manifestJson(
|
|
1652
|
-
bundleId = "target",
|
|
1653
|
-
assets = hashes,
|
|
1654
|
-
byteSizes = contents.mapValues { it.value.length.toLong() },
|
|
1655
|
-
downloadByteSizes = mapOf("assets/image.png" to 50, "index.android.bundle" to 55),
|
|
1656
|
-
archive = archiveJson(root, archiveBytes, tarByteSize = 3072),
|
|
1657
|
-
)
|
|
1658
|
-
val downloads =
|
|
1659
|
-
BinaryMappingDownloadService(mapOf(MANIFEST_URL to manifest.toByteArray(), ARCHIVE_URL to archiveBytes))
|
|
1660
|
-
|
|
1661
|
-
val result =
|
|
1662
|
-
createService(root, downloadService = downloads, builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()))
|
|
1663
|
-
.updateBundle(
|
|
1664
|
-
bundleId = "target",
|
|
1665
|
-
manifestUrl = MANIFEST_URL,
|
|
1666
|
-
manifestFileHash = sha256(root, manifest),
|
|
1667
|
-
assets = hashes.mapValues { (path, hash) -> ChangedAssetDescriptor("https://example.com/$path", hash) },
|
|
1668
|
-
archiveUrl = ARCHIVE_URL,
|
|
1669
|
-
progressCallback = {},
|
|
1670
|
-
)
|
|
1671
|
-
|
|
1672
|
-
assertEquals(UpdateBundleResult(BundleDelivery.ARCHIVE, patchFallback = false), result)
|
|
1673
|
-
assertEquals(listOf(MANIFEST_URL, ARCHIVE_URL), downloads.calls)
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
@Test
|
|
1677
|
-
fun `a file produced by a patch reports patch delivery`() =
|
|
1678
|
-
runBlocking {
|
|
1679
|
-
val patchUpdate = PatchUpdate(temporaryFolder.newFolder("patch-delivery"))
|
|
1680
|
-
|
|
1681
|
-
val result = patchUpdate.run(servedPatch = patchUpdate.patchBytes)
|
|
1682
|
-
|
|
1683
|
-
assertEquals(UpdateBundleResult(BundleDelivery.PATCH, patchFallback = false), result.getOrThrow())
|
|
1684
|
-
assertEquals(listOf(MANIFEST_URL, PATCH_URL), patchUpdate.downloads.calls)
|
|
1685
|
-
assertEquals(PATCHED_CONTENT, File(bundleStoreDir(patchUpdate.root), "target/index.android.bundle").readText())
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
@Test
|
|
1689
|
-
fun `a patch that cannot produce its file falls back to the file`() =
|
|
1690
|
-
runBlocking {
|
|
1691
|
-
val corruptPatch = PatchUpdate(temporaryFolder.newFolder("patch-fallback-corrupt"))
|
|
1692
|
-
assertEquals(
|
|
1693
|
-
UpdateBundleResult(BundleDelivery.MANIFEST, patchFallback = true),
|
|
1694
|
-
corruptPatch.run(servedPatch = "not a bsdiff patch".toByteArray()).getOrThrow(),
|
|
1695
|
-
)
|
|
1696
|
-
assertEquals(listOf(MANIFEST_URL, PATCH_URL, BUNDLE_URL), corruptPatch.downloads.calls)
|
|
1697
|
-
|
|
1698
|
-
val unreachablePatch = PatchUpdate(temporaryFolder.newFolder("patch-fallback-http"))
|
|
1699
|
-
assertEquals(
|
|
1700
|
-
UpdateBundleResult(BundleDelivery.MANIFEST, patchFallback = true),
|
|
1701
|
-
unreachablePatch.run(servedPatch = null).getOrThrow(),
|
|
1702
|
-
)
|
|
1703
|
-
|
|
1704
|
-
val corruptBase = PatchUpdate(temporaryFolder.newFolder("patch-fallback-base"))
|
|
1705
|
-
File(bundleStoreDir(corruptBase.root), "base-bundle/index.android.bundle").writeText("corrupt base")
|
|
1706
|
-
assertEquals(
|
|
1707
|
-
UpdateBundleResult(BundleDelivery.MANIFEST, patchFallback = true),
|
|
1708
|
-
corruptBase.run(servedPatch = corruptBase.patchBytes).getOrThrow(),
|
|
1709
|
-
)
|
|
1710
|
-
assertFalse(corruptBase.downloads.calls.contains(PATCH_URL))
|
|
1711
|
-
|
|
1712
|
-
// A patch for a bundle other than the running one is never attempted.
|
|
1713
|
-
val otherBase = PatchUpdate(temporaryFolder.newFolder("patch-for-other-base"))
|
|
1714
|
-
assertEquals(
|
|
1715
|
-
UpdateBundleResult(BundleDelivery.MANIFEST, patchFallback = false),
|
|
1716
|
-
otherBase.run(servedPatch = otherBase.patchBytes, patchBaseBundleId = "other-bundle").getOrThrow(),
|
|
1717
|
-
)
|
|
1718
|
-
assertEquals(listOf(MANIFEST_URL, BUNDLE_URL), otherBase.downloads.calls)
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
/**
|
|
1722
|
-
* Bundle "target" whose index.android.bundle can be produced by a bsdiff
|
|
1723
|
-
* patch against the running bundle "base-bundle", or downloaded whole.
|
|
1724
|
-
*/
|
|
1725
|
-
private inner class PatchUpdate(
|
|
1726
|
-
val root: File,
|
|
1727
|
-
) {
|
|
1728
|
-
val patchBytes: ByteArray = Base64.getDecoder().decode(BsdiffPatchTest.BSDIFF_PATCH_FIXTURE_BASE64)
|
|
1729
|
-
private val preferences = InMemoryPreferencesService()
|
|
1730
|
-
lateinit var downloads: BinaryMappingDownloadService
|
|
1731
|
-
|
|
1732
|
-
init {
|
|
1733
|
-
val base = createBundleDir(root, "base-bundle")
|
|
1734
|
-
val baseFile = writeFile(base, "index.android.bundle", BASE_CONTENT)
|
|
1735
|
-
File(base, "manifest.json").writeText(
|
|
1736
|
-
manifestJson("base-bundle", mapOf("index.android.bundle" to sha256(root, BASE_CONTENT))),
|
|
1737
|
-
)
|
|
1738
|
-
writeMetadata(root, BundleMetadata(isolationKey = TEST_ISOLATION_KEY, stagingBundleId = "base-bundle"))
|
|
1739
|
-
preferences.setItem("HotUpdaterBundleURL", baseFile.absolutePath)
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
/** Serves [servedPatch] with its hash, or a 404 for the patch when it is null. */
|
|
1743
|
-
suspend fun run(
|
|
1744
|
-
servedPatch: ByteArray?,
|
|
1745
|
-
patchBaseBundleId: String = "base-bundle",
|
|
1746
|
-
): Result<UpdateBundleResult> {
|
|
1747
|
-
val targetHash = sha256(root, PATCHED_CONTENT)
|
|
1748
|
-
val manifest = manifestJson("target", mapOf("index.android.bundle" to targetHash))
|
|
1749
|
-
downloads =
|
|
1750
|
-
BinaryMappingDownloadService(
|
|
1751
|
-
buildMap {
|
|
1752
|
-
put(MANIFEST_URL, manifest.toByteArray())
|
|
1753
|
-
put(BUNDLE_URL, PATCHED_CONTENT.toByteArray())
|
|
1754
|
-
servedPatch?.let { put(PATCH_URL, it) }
|
|
1755
|
-
},
|
|
1756
|
-
failures = if (servedPatch == null) mapOf(PATCH_URL to HttpStatusException(404, "Not Found")) else emptyMap(),
|
|
1757
|
-
)
|
|
1758
|
-
val service =
|
|
1759
|
-
createService(
|
|
1760
|
-
root,
|
|
1761
|
-
preferences = preferences,
|
|
1762
|
-
downloadService = downloads,
|
|
1763
|
-
builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()),
|
|
1764
|
-
)
|
|
1765
|
-
val descriptor =
|
|
1766
|
-
ChangedAssetDescriptor(
|
|
1767
|
-
fileUrl = BUNDLE_URL,
|
|
1768
|
-
fileHash = targetHash,
|
|
1769
|
-
patch =
|
|
1770
|
-
BsdiffPatchDescriptor(
|
|
1771
|
-
algorithm = "bsdiff",
|
|
1772
|
-
baseBundleId = patchBaseBundleId,
|
|
1773
|
-
baseFileHash = sha256(root, BASE_CONTENT),
|
|
1774
|
-
patchFileHash = sha256(root, servedPatch ?: ByteArray(0)),
|
|
1775
|
-
patchUrl = PATCH_URL,
|
|
1776
|
-
),
|
|
1777
|
-
)
|
|
1778
|
-
return runCatching {
|
|
1779
|
-
service.updateBundle("target", MANIFEST_URL, sha256(root, manifest), mapOf("index.android.bundle" to descriptor)) {}
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
@Test
|
|
1785
|
-
fun `rejections that are not update failures carry no classification`() =
|
|
1786
|
-
runBlocking {
|
|
1787
|
-
val crashedRoot = temporaryFolder.newFolder("crashed-bundle")
|
|
1788
|
-
assertTrue(
|
|
1789
|
-
CrashedHistory(mutableListOf(CrashedBundleEntry(bundleId = "target", crashedAt = 1)))
|
|
1790
|
-
.saveToFile(File(bundleStoreDir(crashedRoot), CrashedHistory.CRASHED_HISTORY_FILENAME)),
|
|
1791
|
-
)
|
|
1792
|
-
assertUpdateFailure(
|
|
1793
|
-
runOneFileUpdate(crashedRoot),
|
|
1794
|
-
"BUNDLE_IN_CRASHED_HISTORY",
|
|
1795
|
-
null,
|
|
1796
|
-
)
|
|
1797
|
-
|
|
1798
|
-
val staleRoot = temporaryFolder.newFolder("stale-selection")
|
|
1799
|
-
val hash = sha256(staleRoot, BUNDLE_CONTENT)
|
|
1800
|
-
val manifest = manifestJson("target", mapOf("index.android.bundle" to hash))
|
|
1801
|
-
val served =
|
|
1802
|
-
BinaryMappingDownloadService(
|
|
1803
|
-
mapOf(MANIFEST_URL to manifest.toByteArray(), BUNDLE_URL to BUNDLE_CONTENT.toByteArray()),
|
|
1804
|
-
)
|
|
1805
|
-
lateinit var service: BundleFileStorageService
|
|
1806
|
-
val downloads =
|
|
1807
|
-
object : DownloadService {
|
|
1808
|
-
override suspend fun downloadFile(
|
|
1809
|
-
fileUrl: URL,
|
|
1810
|
-
destination: File,
|
|
1811
|
-
fileSizeCallback: ((Long) -> Unit)?,
|
|
1812
|
-
progressCallback: (DownloadProgress) -> Unit,
|
|
1813
|
-
): DownloadResult {
|
|
1814
|
-
if (fileUrl.toString() == BUNDLE_URL) {
|
|
1815
|
-
// A newer catalog is accepted while the selected bundle downloads.
|
|
1816
|
-
service.acceptReleaseCatalog("project-a", "scope-production", 2, "hash-2", "production", "context-2")
|
|
1817
|
-
}
|
|
1818
|
-
return served.downloadFile(fileUrl, destination, fileSizeCallback, progressCallback)
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
service = createService(staleRoot, downloadService = downloads, builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()))
|
|
1822
|
-
assertTrue(service.acceptReleaseCatalog("project-a", "scope-production", 1, "hash-1", "production", "context-1"))
|
|
1823
|
-
assertTrue(service.stageReleaseSelection(releaseSelection("release-one", "target", 1, "hash-1", "context-1")))
|
|
1824
|
-
|
|
1825
|
-
val staleResult =
|
|
1826
|
-
runCatching {
|
|
1827
|
-
service.updateBundle(
|
|
1828
|
-
"target",
|
|
1829
|
-
MANIFEST_URL,
|
|
1830
|
-
sha256(staleRoot, manifest),
|
|
1831
|
-
mapOf("index.android.bundle" to ChangedAssetDescriptor(BUNDLE_URL, hash)),
|
|
1832
|
-
) {}
|
|
1833
|
-
}.exceptionOrNull()
|
|
1834
|
-
|
|
1835
|
-
assertTrue("Expected a stale selection, got $staleResult", staleResult is StaleReleaseSelectionException)
|
|
1836
|
-
assertNull(loadMetadata(staleRoot)?.stagingBundleId)
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
/** Installs bundle "target" with one file, returning the failure, if any. */
|
|
1840
|
-
private suspend fun runOneFileUpdate(
|
|
1841
|
-
root: File,
|
|
1842
|
-
assetPath: String = "index.android.bundle",
|
|
1843
|
-
manifest: String = manifestJson("target", mapOf(assetPath to sha256(root, BUNDLE_CONTENT))),
|
|
1844
|
-
manifestFileHash: String = sha256(root, manifest),
|
|
1845
|
-
descriptor: ChangedAssetDescriptor = ChangedAssetDescriptor(BUNDLE_URL, sha256(root, BUNDLE_CONTENT)),
|
|
1846
|
-
servedFile: ByteArray = BUNDLE_CONTENT.toByteArray(),
|
|
1847
|
-
failures: Map<String, Exception> = emptyMap(),
|
|
1848
|
-
directoryRenamer: (File, File) -> Boolean = { source, destination -> source.renameTo(destination) },
|
|
1849
|
-
): Throwable? {
|
|
1850
|
-
val service =
|
|
1851
|
-
createService(
|
|
1852
|
-
root,
|
|
1853
|
-
downloadService =
|
|
1854
|
-
BinaryMappingDownloadService(
|
|
1855
|
-
mapOf(MANIFEST_URL to manifest.toByteArray(), BUNDLE_URL to servedFile),
|
|
1856
|
-
failures,
|
|
1857
|
-
),
|
|
1858
|
-
builtInAssetResolver = MappingBuiltInAssetResolver(emptyMap()),
|
|
1859
|
-
directoryRenamer = directoryRenamer,
|
|
1860
|
-
)
|
|
1861
|
-
return runCatching {
|
|
1862
|
-
service.updateBundle("target", MANIFEST_URL, manifestFileHash, mapOf(assetPath to descriptor)) {}
|
|
1863
|
-
}.exceptionOrNull()
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
private fun assertUpdateFailure(
|
|
1867
|
-
error: Throwable?,
|
|
1868
|
-
code: String,
|
|
1869
|
-
failure: UpdateFailure?,
|
|
1870
|
-
) {
|
|
1871
|
-
assertTrue("Expected HotUpdaterException, got $error", error is HotUpdaterException)
|
|
1872
|
-
val updateError = error as HotUpdaterException
|
|
1873
|
-
assertEquals(code, updateError.code)
|
|
1874
|
-
assertEquals(failure, updateError.failure)
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
private fun createService(
|
|
1878
|
-
rootDir: File,
|
|
1879
|
-
preferences: InMemoryPreferencesService = InMemoryPreferencesService(),
|
|
1880
|
-
downloadService: DownloadService = UnusedDownloadService,
|
|
1881
|
-
builtInAssetResolver: BuiltInAssetResolver? = null,
|
|
1882
|
-
metadataWriter: (BundleMetadata, File) -> Boolean = { metadata, file -> metadata.saveToFile(file) },
|
|
1883
|
-
directoryRenamer: (File, File) -> Boolean = { source, destination -> source.renameTo(destination) },
|
|
1884
|
-
): BundleFileStorageService =
|
|
1885
|
-
BundleFileStorageService(
|
|
1886
|
-
context = ContextWrapper(null),
|
|
1887
|
-
fileSystem = TestFileSystemService(rootDir),
|
|
1888
|
-
downloadService = downloadService,
|
|
1889
|
-
preferences = preferences,
|
|
1890
|
-
isolationKey = TEST_ISOLATION_KEY,
|
|
1891
|
-
defaultChannelProvider = { "production" },
|
|
1892
|
-
builtInAssetResolver = builtInAssetResolver,
|
|
1893
|
-
metadataWriter = metadataWriter,
|
|
1894
|
-
directoryRenamer = directoryRenamer,
|
|
1895
|
-
)
|
|
1896
|
-
|
|
1897
|
-
private fun releaseSelection(
|
|
1898
|
-
releaseId: String,
|
|
1899
|
-
bundleId: String,
|
|
1900
|
-
generation: Long,
|
|
1901
|
-
catalogHash: String,
|
|
1902
|
-
selectionContextHash: String,
|
|
1903
|
-
): PersistedSelection =
|
|
1904
|
-
PersistedSelection(
|
|
1905
|
-
kind = "BUNDLE",
|
|
1906
|
-
releaseId = releaseId,
|
|
1907
|
-
bundleId = bundleId,
|
|
1908
|
-
catalogId = "project-a",
|
|
1909
|
-
scopeKey = "scope-production",
|
|
1910
|
-
generation = generation,
|
|
1911
|
-
catalogHash = catalogHash,
|
|
1912
|
-
channel = "production",
|
|
1913
|
-
selectionContextHash = selectionContextHash,
|
|
1914
|
-
)
|
|
1915
|
-
|
|
1916
|
-
private fun createBundleDir(
|
|
1917
|
-
rootDir: File,
|
|
1918
|
-
bundleId: String,
|
|
1919
|
-
): File = File(bundleStoreDir(rootDir), bundleId).apply { mkdirs() }
|
|
1920
|
-
|
|
1921
|
-
private fun createCompleteBundle(
|
|
1922
|
-
rootDir: File,
|
|
1923
|
-
directoryName: String,
|
|
1924
|
-
content: String,
|
|
1925
|
-
manifestBundleId: String = directoryName,
|
|
1926
|
-
): File {
|
|
1927
|
-
val directory = createBundleDir(rootDir, directoryName)
|
|
1928
|
-
writeFile(directory, "index.android.bundle", content)
|
|
1929
|
-
File(directory, "manifest.json").writeText(
|
|
1930
|
-
manifestJson(
|
|
1931
|
-
manifestBundleId,
|
|
1932
|
-
mapOf("index.android.bundle" to sha256(rootDir, content)),
|
|
1933
|
-
),
|
|
1934
|
-
)
|
|
1935
|
-
return directory
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
private fun writeManifest(
|
|
1939
|
-
bundleDir: File,
|
|
1940
|
-
assetPaths: List<String>,
|
|
1941
|
-
) {
|
|
1942
|
-
val assets =
|
|
1943
|
-
JSONObject().apply {
|
|
1944
|
-
assetPaths.forEach { assetPath ->
|
|
1945
|
-
put(assetPath, JSONObject().put("fileHash", "$assetPath-hash"))
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
File(bundleDir, "manifest.json").writeText(
|
|
1950
|
-
JSONObject()
|
|
1951
|
-
.put("bundleId", bundleDir.name)
|
|
1952
|
-
.put("assets", assets)
|
|
1953
|
-
.toString(),
|
|
1954
|
-
)
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
private fun writeMetadata(
|
|
1958
|
-
rootDir: File,
|
|
1959
|
-
metadata: BundleMetadata,
|
|
1960
|
-
) {
|
|
1961
|
-
assertTrue(metadata.saveToFile(File(bundleStoreDir(rootDir), BundleMetadata.METADATA_FILENAME)))
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
private fun loadMetadata(rootDir: File): BundleMetadata? =
|
|
1965
|
-
BundleMetadata.loadFromFile(
|
|
1966
|
-
File(bundleStoreDir(rootDir), BundleMetadata.METADATA_FILENAME),
|
|
1967
|
-
TEST_ISOLATION_KEY,
|
|
1968
|
-
)
|
|
1969
|
-
|
|
1970
|
-
private fun writeFile(
|
|
1971
|
-
rootDir: File,
|
|
1972
|
-
relativePath: String,
|
|
1973
|
-
content: String = "bundle-content",
|
|
1974
|
-
): File =
|
|
1975
|
-
File(rootDir, relativePath).apply {
|
|
1976
|
-
parentFile?.mkdirs()
|
|
1977
|
-
writeText(content)
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
private fun bundleStoreDir(rootDir: File): File = File(rootDir, "bundle-store").apply { mkdirs() }
|
|
1981
|
-
|
|
1982
|
-
private fun invokeResolveBundleFile(
|
|
1983
|
-
service: BundleFileStorageService,
|
|
1984
|
-
bundleDir: File,
|
|
1985
|
-
): File? {
|
|
1986
|
-
val method =
|
|
1987
|
-
BundleFileStorageService::class.java.getDeclaredMethod(
|
|
1988
|
-
"resolveBundleFile",
|
|
1989
|
-
File::class.java,
|
|
1990
|
-
String::class.java,
|
|
1991
|
-
)
|
|
1992
|
-
method.isAccessible = true
|
|
1993
|
-
return method.invoke(service, bundleDir, bundleDir.name) as File?
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
private fun assertResolvedBundlePath(
|
|
1997
|
-
service: BundleFileStorageService,
|
|
1998
|
-
bundleDir: File,
|
|
1999
|
-
expected: File,
|
|
2000
|
-
) {
|
|
2001
|
-
val resolved = invokeResolveBundleFile(service, bundleDir)
|
|
2002
|
-
|
|
2003
|
-
assertNotNull(resolved)
|
|
2004
|
-
assertEquals(expected.canonicalFile.absolutePath, resolved?.canonicalFile?.absolutePath)
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
|
-
private class TestFileSystemService(
|
|
2008
|
-
private val internalFilesDir: File,
|
|
2009
|
-
) : FileSystemService {
|
|
2010
|
-
override fun fileExists(path: String): Boolean = File(path).exists()
|
|
2011
|
-
|
|
2012
|
-
override fun createDirectory(path: String): Boolean = File(path).mkdirs()
|
|
2013
|
-
|
|
2014
|
-
override fun removeItem(path: String): Boolean = File(path).deleteRecursively()
|
|
2015
|
-
|
|
2016
|
-
override fun moveItem(
|
|
2017
|
-
sourcePath: String,
|
|
2018
|
-
destinationPath: String,
|
|
2019
|
-
): Boolean = File(sourcePath).renameTo(File(destinationPath))
|
|
2020
|
-
|
|
2021
|
-
override fun copyItem(
|
|
2022
|
-
sourcePath: String,
|
|
2023
|
-
destinationPath: String,
|
|
2024
|
-
): Boolean =
|
|
2025
|
-
try {
|
|
2026
|
-
File(sourcePath).copyRecursively(File(destinationPath), overwrite = true)
|
|
2027
|
-
} catch (_: Exception) {
|
|
2028
|
-
false
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
|
-
override fun contentsOfDirectory(path: String): List<String> = File(path).list()?.toList() ?: emptyList()
|
|
2032
|
-
|
|
2033
|
-
override fun getInternalFilesDir(): File = internalFilesDir
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
private class InMemoryPreferencesService(
|
|
2037
|
-
private val shouldFailWrite: (String, String?) -> Boolean = { _, _ -> false },
|
|
2038
|
-
) : PreferencesService {
|
|
2039
|
-
private val values = mutableMapOf<String, String?>()
|
|
2040
|
-
|
|
2041
|
-
override fun getItem(key: String): String? = values[key]
|
|
2042
|
-
|
|
2043
|
-
override fun setItem(
|
|
2044
|
-
key: String,
|
|
2045
|
-
value: String?,
|
|
2046
|
-
) {
|
|
2047
|
-
if (shouldFailWrite(key, value)) {
|
|
2048
|
-
throw IllegalStateException("Injected preference write failure")
|
|
2049
|
-
}
|
|
2050
|
-
if (value == null) {
|
|
2051
|
-
values.remove(key)
|
|
2052
|
-
} else {
|
|
2053
|
-
values[key] = value
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
private object UnusedDownloadService : DownloadService {
|
|
2059
|
-
override suspend fun downloadFile(
|
|
2060
|
-
fileUrl: URL,
|
|
2061
|
-
destination: File,
|
|
2062
|
-
fileSizeCallback: ((Long) -> Unit)?,
|
|
2063
|
-
progressCallback: (DownloadProgress) -> Unit,
|
|
2064
|
-
): DownloadResult = error("downloadFile should not be called in these tests")
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
private class RecordingFailedDownloadService : DownloadService {
|
|
2068
|
-
val calls = CopyOnWriteArrayList<Pair<String, String>>()
|
|
2069
|
-
|
|
2070
|
-
override suspend fun downloadFile(
|
|
2071
|
-
fileUrl: URL,
|
|
2072
|
-
destination: File,
|
|
2073
|
-
fileSizeCallback: ((Long) -> Unit)?,
|
|
2074
|
-
progressCallback: (DownloadProgress) -> Unit,
|
|
2075
|
-
): DownloadResult {
|
|
2076
|
-
calls += fileUrl.toString() to Thread.currentThread().name
|
|
2077
|
-
return DownloadResult.Error(IllegalStateException("expected download failure"))
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
|
-
private class MappingDownloadService(
|
|
2082
|
-
private val contents: Map<String, String>,
|
|
2083
|
-
) : DownloadService {
|
|
2084
|
-
val calls = CopyOnWriteArrayList<String>()
|
|
2085
|
-
|
|
2086
|
-
override suspend fun downloadFile(
|
|
2087
|
-
fileUrl: URL,
|
|
2088
|
-
destination: File,
|
|
2089
|
-
fileSizeCallback: ((Long) -> Unit)?,
|
|
2090
|
-
progressCallback: (DownloadProgress) -> Unit,
|
|
2091
|
-
): DownloadResult {
|
|
2092
|
-
calls += fileUrl.toString()
|
|
2093
|
-
val content =
|
|
2094
|
-
contents[fileUrl.toString()]
|
|
2095
|
-
?: return DownloadResult.Error(IllegalArgumentException("Unexpected URL: $fileUrl"))
|
|
2096
|
-
destination.parentFile?.mkdirs()
|
|
2097
|
-
destination.writeText(content)
|
|
2098
|
-
fileSizeCallback?.invoke(destination.length())
|
|
2099
|
-
progressCallback(DownloadProgress(1.0, destination.length(), destination.length()))
|
|
2100
|
-
return DownloadResult.Success(destination)
|
|
2101
|
-
}
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
private class BinaryMappingDownloadService(
|
|
2105
|
-
private val contents: Map<String, ByteArray>,
|
|
2106
|
-
private val failures: Map<String, Exception> = emptyMap(),
|
|
2107
|
-
) : DownloadService {
|
|
2108
|
-
val calls = CopyOnWriteArrayList<String>()
|
|
2109
|
-
|
|
2110
|
-
override suspend fun downloadFile(
|
|
2111
|
-
fileUrl: URL,
|
|
2112
|
-
destination: File,
|
|
2113
|
-
fileSizeCallback: ((Long) -> Unit)?,
|
|
2114
|
-
progressCallback: (DownloadProgress) -> Unit,
|
|
2115
|
-
): DownloadResult = download(fileUrl, destination, fileSizeCallback, progressCallback)
|
|
2116
|
-
|
|
2117
|
-
override suspend fun downloadFileOnce(
|
|
2118
|
-
fileUrl: URL,
|
|
2119
|
-
destination: File,
|
|
2120
|
-
fileSizeCallback: ((Long) -> Unit)?,
|
|
2121
|
-
progressCallback: (DownloadProgress) -> Unit,
|
|
2122
|
-
): DownloadResult = download(fileUrl, destination, fileSizeCallback, progressCallback)
|
|
2123
|
-
|
|
2124
|
-
private fun download(
|
|
2125
|
-
fileUrl: URL,
|
|
2126
|
-
destination: File,
|
|
2127
|
-
fileSizeCallback: ((Long) -> Unit)?,
|
|
2128
|
-
progressCallback: (DownloadProgress) -> Unit,
|
|
2129
|
-
): DownloadResult {
|
|
2130
|
-
calls += fileUrl.toString()
|
|
2131
|
-
failures[fileUrl.toString()]?.let { return DownloadResult.Error(it) }
|
|
2132
|
-
val bytes = contents[fileUrl.toString()] ?: return DownloadResult.Error(IllegalArgumentException("Unexpected URL: $fileUrl"))
|
|
2133
|
-
destination.parentFile?.mkdirs()
|
|
2134
|
-
destination.writeBytes(bytes)
|
|
2135
|
-
fileSizeCallback?.invoke(bytes.size.toLong())
|
|
2136
|
-
progressCallback(DownloadProgress(1.0, bytes.size.toLong(), bytes.size.toLong()))
|
|
2137
|
-
return DownloadResult.Success(destination)
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
private class MappingBuiltInAssetResolver(
|
|
2142
|
-
private val contents: Map<String, String>,
|
|
2143
|
-
) : BuiltInAssetResolver {
|
|
2144
|
-
override fun copyIfMatches(
|
|
2145
|
-
assetPath: String,
|
|
2146
|
-
expectedHash: String,
|
|
2147
|
-
destination: File,
|
|
2148
|
-
): Boolean {
|
|
2149
|
-
val content = contents[assetPath] ?: return false
|
|
2150
|
-
destination.parentFile?.mkdirs()
|
|
2151
|
-
destination.writeText(content)
|
|
2152
|
-
if (!HashUtils.verifyHash(destination, expectedHash)) {
|
|
2153
|
-
destination.delete()
|
|
2154
|
-
return false
|
|
2155
|
-
}
|
|
2156
|
-
return true
|
|
2157
|
-
}
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
private fun manifestJson(
|
|
2161
|
-
bundleId: String,
|
|
2162
|
-
assets: Map<String, String>,
|
|
2163
|
-
byteSizes: Map<String, Long> = emptyMap(),
|
|
2164
|
-
downloadByteSizes: Map<String, Long> = emptyMap(),
|
|
2165
|
-
archive: JSONObject? = null,
|
|
2166
|
-
): String =
|
|
2167
|
-
JSONObject()
|
|
2168
|
-
.put("bundleId", bundleId)
|
|
2169
|
-
.put(
|
|
2170
|
-
"assets",
|
|
2171
|
-
JSONObject().apply {
|
|
2172
|
-
assets.forEach { (path, hash) ->
|
|
2173
|
-
put(
|
|
2174
|
-
path,
|
|
2175
|
-
JSONObject()
|
|
2176
|
-
.put("fileHash", hash)
|
|
2177
|
-
.apply {
|
|
2178
|
-
byteSizes[path]?.let { put("byteSize", it) }
|
|
2179
|
-
downloadByteSizes[path]?.let { put("downloadByteSize", it) }
|
|
2180
|
-
},
|
|
2181
|
-
)
|
|
2182
|
-
}
|
|
2183
|
-
},
|
|
2184
|
-
).apply {
|
|
2185
|
-
archive?.let { put("archive", it) }
|
|
2186
|
-
}.toString()
|
|
2187
|
-
|
|
2188
|
-
private fun archiveJson(
|
|
2189
|
-
rootDir: File,
|
|
2190
|
-
bytes: ByteArray,
|
|
2191
|
-
tarByteSize: Long,
|
|
2192
|
-
): JSONObject =
|
|
2193
|
-
JSONObject()
|
|
2194
|
-
.put("downloadFileHash", sha256(rootDir, bytes))
|
|
2195
|
-
.put("downloadByteSize", bytes.size)
|
|
2196
|
-
.put("tarByteSize", tarByteSize)
|
|
2197
|
-
|
|
2198
|
-
private fun sha256(
|
|
2199
|
-
rootDir: File,
|
|
2200
|
-
content: String,
|
|
2201
|
-
): String {
|
|
2202
|
-
val file = File.createTempFile("hash-", null, rootDir)
|
|
2203
|
-
return try {
|
|
2204
|
-
file.writeText(content)
|
|
2205
|
-
HashUtils.calculateSHA256(file)
|
|
2206
|
-
} finally {
|
|
2207
|
-
file.delete()
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
|
-
private fun sha256(
|
|
2212
|
-
rootDir: File,
|
|
2213
|
-
bytes: ByteArray,
|
|
2214
|
-
): String {
|
|
2215
|
-
val file = File.createTempFile("hash-", null, rootDir)
|
|
2216
|
-
return try {
|
|
2217
|
-
file.writeBytes(bytes)
|
|
2218
|
-
HashUtils.calculateSHA256(file)
|
|
2219
|
-
} finally {
|
|
2220
|
-
file.delete()
|
|
2221
|
-
}
|
|
2222
|
-
}
|
|
2223
|
-
|
|
2224
|
-
companion object {
|
|
2225
|
-
private const val TEST_ISOLATION_KEY = "test-isolation-key"
|
|
2226
|
-
private const val MANIFEST_URL = "https://example.com/manifest.json"
|
|
2227
|
-
private const val ARCHIVE_URL = "https://example.com/bundle.tar.br"
|
|
2228
|
-
private const val BUNDLE_URL = "https://example.com/index.android.bundle"
|
|
2229
|
-
private const val BUNDLE_CONTENT = "bundle"
|
|
2230
|
-
private const val PATCH_URL = "https://example.com/index.android.bundle.bsdiff"
|
|
2231
|
-
|
|
2232
|
-
// The base and output of BsdiffPatchTest.BSDIFF_PATCH_FIXTURE_BASE64.
|
|
2233
|
-
private const val BASE_CONTENT = "console.log(\"base bundle\");\n"
|
|
2234
|
-
private const val PATCHED_CONTENT = "console.log(\"patched bundle\");\n"
|
|
2235
|
-
}
|
|
2236
|
-
}
|