@bravemobile/react-native-code-push 13.1.0-beta.3 → 13.1.0-beta.5
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/CodePush.podspec +5 -0
- package/README.md +20 -145
- package/android/app/src/main/java/com/microsoft/codepush/react/ArchiveAttemptLog.java +181 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/{BinaryPatchResult.java → ArchiveRestoreResult.java} +17 -7
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +20 -20
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +20 -1
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushErrorCode.java +86 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushHttpException.java +29 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushIncompleteDownloadException.java +17 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +12 -7
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +143 -43
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +31 -31
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushErrorCodeTest.java +90 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerBinaryPatchTest.java +159 -19
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +304 -15
- package/cli/commands/createHistoryCommand/createReleaseHistory.test.ts +133 -0
- package/cli/commands/createHistoryCommand/createReleaseHistory.ts +3 -11
- package/cli/commands/releaseCommand/addToReleaseHistory.ts +3 -11
- package/cli/commands/releaseCommand/release.test.ts +55 -13
- package/cli/commands/releaseCommand/release.ts +39 -6
- package/cli/commands/updateHistoryCommand/updateReleaseHistory.ts +3 -11
- package/cli/dist/commands/createHistoryCommand/createReleaseHistory.js +2 -8
- package/cli/dist/commands/createHistoryCommand/createReleaseHistory.test.js +95 -0
- package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +2 -8
- package/cli/dist/commands/releaseCommand/release.js +23 -8
- package/cli/dist/commands/releaseCommand/release.test.js +37 -9
- package/cli/dist/commands/updateHistoryCommand/updateReleaseHistory.js +2 -8
- package/cli/dist/functions/resolveAssetDiffBases.js +7 -2
- package/cli/dist/functions/resolveAssetDiffBases.test.js +26 -12
- package/cli/dist/functions/stageReleaseHistoryFile.js +28 -0
- package/cli/functions/resolveAssetDiffBases.test.ts +28 -14
- package/cli/functions/resolveAssetDiffBases.ts +8 -1
- package/cli/functions/stageReleaseHistoryFile.ts +38 -0
- package/ios/CodePush/CodePush.h +6 -3
- package/ios/CodePush/CodePush.mm +10 -10
- package/ios/CodePush/CodePushBinaryPatch.h +18 -8
- package/ios/CodePush/CodePushBinaryPatch.m +23 -22
- package/ios/CodePush/CodePushDownloadHandler.m +2 -1
- package/ios/CodePush/CodePushErrorUtils.m +55 -0
- package/ios/CodePush/CodePushPackage.m +242 -71
- package/package.json +2 -1
- package/src/CodePush.js +28 -18
- package/src/CodePush.test.js +110 -55
- package/src/package-mixins.js +19 -10
- package/typings/react-native-code-push.d.ts +152 -41
- package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +0 -92
|
@@ -55,7 +55,7 @@ public class CodePushBinaryPatchTest {
|
|
|
55
55
|
public void appliesAPatchAndLeavesTheContentsOfAFullArchiveBehind() throws IOException {
|
|
56
56
|
FakePatchApplier applier = new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_OK, TARGET_BUNDLE);
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
ArchiveRestoreResult result = restoreBundle(BASE_BUNDLE, applier);
|
|
59
59
|
|
|
60
60
|
assertTrue(result.succeeded());
|
|
61
61
|
assertArrayEquals(TARGET_BUNDLE, readFile(mBundleFile));
|
|
@@ -77,7 +77,7 @@ public class CodePushBinaryPatchTest {
|
|
|
77
77
|
assertTrue(mManifestFile.renameTo(new File(archiveRoot, CodePushConstants.BINARY_PATCH_MANIFEST_FILE_NAME)));
|
|
78
78
|
assertTrue(mPatchFile.renameTo(new File(archiveRoot, PATCH_FILE_NAME)));
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
ArchiveRestoreResult result = new CodePushBinaryPatch(providerOf(BASE_BUNDLE), succeedingApplier())
|
|
81
81
|
.restoreBundle(unzippedFolder.getAbsolutePath(), mWorkingFolder.getAbsolutePath(), BUNDLE_FILE_NAME);
|
|
82
82
|
|
|
83
83
|
assertTrue(result.succeeded());
|
|
@@ -101,7 +101,7 @@ public class CodePushBinaryPatchTest {
|
|
|
101
101
|
File assetFile = new File(contentsFolder, "logo.png");
|
|
102
102
|
writeFile(assetFile, bytes("an asset the diff brought along"));
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
ArchiveRestoreResult result = new CodePushBinaryPatch(providerOf(BASE_BUNDLE), succeedingApplier())
|
|
105
105
|
.restoreBundle(unzippedFolder.getAbsolutePath(), mWorkingFolder.getAbsolutePath(), BUNDLE_FILE_NAME);
|
|
106
106
|
|
|
107
107
|
assertTrue(result.succeeded());
|
|
@@ -117,7 +117,7 @@ public class CodePushBinaryPatchTest {
|
|
|
117
117
|
public void keepsARootHoldingLooseFilesBesidesTheDiffManifestAsItsOwnContentsRoot() throws IOException {
|
|
118
118
|
writeFile(new File(mContentsFolder, CodePushConstants.DIFF_MANIFEST_FILE_NAME), bytes("{\"deletedFiles\":[]}"));
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
ArchiveRestoreResult result = restoreBundle(BASE_BUNDLE, succeedingApplier());
|
|
121
121
|
|
|
122
122
|
assertTrue(result.succeeded());
|
|
123
123
|
assertArrayEquals(TARGET_BUNDLE, readFile(mBundleFile));
|
|
@@ -129,7 +129,7 @@ public class CodePushBinaryPatchTest {
|
|
|
129
129
|
assertTrue(mWorkingFolder.mkdirs());
|
|
130
130
|
writeFile(new File(mWorkingFolder, CodePushConstants.BINARY_PATCH_TARGET_FILE_NAME), bytes("half a bundle"));
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
ArchiveRestoreResult result = restoreBundle(BASE_BUNDLE,
|
|
133
133
|
new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_OK, TARGET_BUNDLE));
|
|
134
134
|
|
|
135
135
|
assertTrue(result.succeeded());
|
|
@@ -141,14 +141,14 @@ public class CodePushBinaryPatchTest {
|
|
|
141
141
|
public void reportsAnInvalidManifestWhenThereIsNone() {
|
|
142
142
|
assertTrue(mManifestFile.delete());
|
|
143
143
|
|
|
144
|
-
assertFailure(
|
|
144
|
+
assertFailure(ArchiveRestoreResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
@Test
|
|
148
148
|
public void reportsAnInvalidManifestWhenItIsNotJson() throws IOException {
|
|
149
149
|
writeFile(mManifestFile, bytes("not json at all"));
|
|
150
150
|
|
|
151
|
-
assertFailure(
|
|
151
|
+
assertFailure(ArchiveRestoreResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
@Test
|
|
@@ -156,14 +156,14 @@ public class CodePushBinaryPatchTest {
|
|
|
156
156
|
writeManifest(putValue(validManifest(), CodePushConstants.BINARY_PATCH_FORMAT_VERSION_KEY,
|
|
157
157
|
CodePushConstants.BINARY_PATCH_FORMAT_VERSION + 1));
|
|
158
158
|
|
|
159
|
-
assertFailure(
|
|
159
|
+
assertFailure(ArchiveRestoreResult.REASON_UNSUPPORTED_FORMAT, restoreBundle(BASE_BUNDLE, succeedingApplier()));
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
@Test
|
|
163
163
|
public void reportsAnUnsupportedFormatForAnotherAlgorithm() throws IOException {
|
|
164
164
|
writeManifest(putValue(validManifest(), CodePushConstants.BINARY_PATCH_ALGORITHM_KEY, "bsdiff-bz2"));
|
|
165
165
|
|
|
166
|
-
assertFailure(
|
|
166
|
+
assertFailure(ArchiveRestoreResult.REASON_UNSUPPORTED_FORMAT, restoreBundle(BASE_BUNDLE, succeedingApplier()));
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
@Test
|
|
@@ -171,7 +171,7 @@ public class CodePushBinaryPatchTest {
|
|
|
171
171
|
writeManifest(putValue(validManifest(), CodePushConstants.BINARY_PATCH_BUNDLE_PATH_KEY,
|
|
172
172
|
"../" + BUNDLE_FILE_NAME));
|
|
173
173
|
|
|
174
|
-
assertFailure(
|
|
174
|
+
assertFailure(ArchiveRestoreResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
|
|
175
175
|
assertFalse(new File(mTemporaryFolder.getRoot(), BUNDLE_FILE_NAME).exists());
|
|
176
176
|
}
|
|
177
177
|
|
|
@@ -180,7 +180,7 @@ public class CodePushBinaryPatchTest {
|
|
|
180
180
|
writeFile(new File(mTemporaryFolder.getRoot(), PATCH_FILE_NAME), PATCH);
|
|
181
181
|
writeManifest(putValue(validManifest(), CodePushConstants.BINARY_PATCH_FILE_KEY, "../" + PATCH_FILE_NAME));
|
|
182
182
|
|
|
183
|
-
assertFailure(
|
|
183
|
+
assertFailure(ArchiveRestoreResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
@Test
|
|
@@ -188,21 +188,21 @@ public class CodePushBinaryPatchTest {
|
|
|
188
188
|
writeManifest(putValue(validManifest(), CodePushConstants.BINARY_PATCH_BUNDLE_PATH_KEY,
|
|
189
189
|
new File(mTemporaryFolder.getRoot(), BUNDLE_FILE_NAME).getAbsolutePath()));
|
|
190
190
|
|
|
191
|
-
assertFailure(
|
|
191
|
+
assertFailure(ArchiveRestoreResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
@Test
|
|
195
195
|
public void reportsAnInvalidManifestWhenTheArchiveHasNoPatchFile() {
|
|
196
196
|
assertTrue(mPatchFile.delete());
|
|
197
197
|
|
|
198
|
-
assertFailure(
|
|
198
|
+
assertFailure(ArchiveRestoreResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
@Test
|
|
202
202
|
public void reportsAnInvalidManifestWhenTheTargetSizeIsEmpty() throws IOException {
|
|
203
203
|
writeManifest(putValue(validManifest(), CodePushConstants.BINARY_PATCH_TARGET_BUNDLE_SIZE_KEY, 0));
|
|
204
204
|
|
|
205
|
-
assertFailure(
|
|
205
|
+
assertFailure(ArchiveRestoreResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
@Test
|
|
@@ -210,7 +210,7 @@ public class CodePushBinaryPatchTest {
|
|
|
210
210
|
writeManifest(putValue(validManifest(), CodePushConstants.BINARY_PATCH_TARGET_BUNDLE_SIZE_KEY,
|
|
211
211
|
CodePushConstants.BINARY_PATCH_MAX_TARGET_BUNDLE_SIZE + 1));
|
|
212
212
|
|
|
213
|
-
assertFailure(
|
|
213
|
+
assertFailure(ArchiveRestoreResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
@Test
|
|
@@ -222,50 +222,50 @@ public class CodePushBinaryPatchTest {
|
|
|
222
222
|
}
|
|
223
223
|
}, succeedingApplier());
|
|
224
224
|
|
|
225
|
-
assertFailure(
|
|
225
|
+
assertFailure(ArchiveRestoreResult.REASON_BASE_BUNDLE_UNAVAILABLE, restoreBundle(binaryPatch));
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
@Test
|
|
229
229
|
public void reportsABaseHashMismatchWhenTheBinaryHoldsAnotherBundle() {
|
|
230
230
|
FakePatchApplier applier = new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_OK, TARGET_BUNDLE);
|
|
231
231
|
|
|
232
|
-
|
|
232
|
+
ArchiveRestoreResult result = restoreBundle(bytes("a bundle from another app build"), applier);
|
|
233
233
|
|
|
234
|
-
assertFailure(
|
|
234
|
+
assertFailure(ArchiveRestoreResult.REASON_BASE_HASH_MISMATCH, result);
|
|
235
235
|
assertEquals("a patch is not applied to a base it was not computed against", 0, applier.invocationCount);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
@Test
|
|
239
239
|
public void reportsAnUnsupportedFormatWhenTheApplierRejectsTheCodec() {
|
|
240
|
-
|
|
240
|
+
ArchiveRestoreResult result = restoreBundle(BASE_BUNDLE,
|
|
241
241
|
new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_UNSUPPORTED_COMPRESSION, null));
|
|
242
242
|
|
|
243
|
-
assertFailure(
|
|
243
|
+
assertFailure(ArchiveRestoreResult.REASON_UNSUPPORTED_FORMAT, result);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
@Test
|
|
247
247
|
public void reportsAFailedApplyWhenThePatchHeaderIsCorrupt() {
|
|
248
|
-
|
|
248
|
+
ArchiveRestoreResult result = restoreBundle(BASE_BUNDLE,
|
|
249
249
|
new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_INVALID_HEADER, null));
|
|
250
250
|
|
|
251
|
-
assertFailure(
|
|
251
|
+
assertFailure(ArchiveRestoreResult.REASON_PATCH_APPLY_FAILED, result);
|
|
252
252
|
assertNoRestoredBundleInTheContents();
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
@Test
|
|
256
256
|
public void reportsAFailedApplyWhenTheNativeLibraryIsMissing() {
|
|
257
|
-
|
|
257
|
+
ArchiveRestoreResult result = restoreBundle(BASE_BUNDLE,
|
|
258
258
|
new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_LIBRARY_UNAVAILABLE, null));
|
|
259
259
|
|
|
260
|
-
assertFailure(
|
|
260
|
+
assertFailure(ArchiveRestoreResult.REASON_PATCH_APPLY_FAILED, result);
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
@Test
|
|
264
264
|
public void reportsAFailedVerificationWhenTheRestoredBundleHasAnotherSize() {
|
|
265
|
-
|
|
265
|
+
ArchiveRestoreResult result = restoreBundle(BASE_BUNDLE,
|
|
266
266
|
new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_OK, bytes("too short")));
|
|
267
267
|
|
|
268
|
-
assertFailure(
|
|
268
|
+
assertFailure(ArchiveRestoreResult.REASON_TARGET_VERIFICATION_FAILED, result);
|
|
269
269
|
assertNoRestoredBundleInTheContents();
|
|
270
270
|
}
|
|
271
271
|
|
|
@@ -276,23 +276,23 @@ public class CodePushBinaryPatchTest {
|
|
|
276
276
|
byte[] wrongBundle = TARGET_BUNDLE.clone();
|
|
277
277
|
wrongBundle[0] = (byte) (wrongBundle[0] ^ 0xFF);
|
|
278
278
|
|
|
279
|
-
|
|
279
|
+
ArchiveRestoreResult result = restoreBundle(BASE_BUNDLE,
|
|
280
280
|
new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_OK, wrongBundle));
|
|
281
281
|
|
|
282
|
-
assertFailure(
|
|
282
|
+
assertFailure(ArchiveRestoreResult.REASON_TARGET_VERIFICATION_FAILED, result);
|
|
283
283
|
assertNoRestoredBundleInTheContents();
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
-
private
|
|
286
|
+
private ArchiveRestoreResult restoreBundle(byte[] baseBundle, CodePushBinaryPatch.PatchApplier applier) {
|
|
287
287
|
return restoreBundle(new CodePushBinaryPatch(providerOf(baseBundle), applier));
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
private
|
|
290
|
+
private ArchiveRestoreResult restoreBundle(CodePushBinaryPatch binaryPatch) {
|
|
291
291
|
return binaryPatch.restoreBundle(mContentsFolder.getAbsolutePath(), mWorkingFolder.getAbsolutePath(),
|
|
292
292
|
BUNDLE_FILE_NAME);
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
private void assertFailure(String expectedReason,
|
|
295
|
+
private void assertFailure(String expectedReason, ArchiveRestoreResult result) {
|
|
296
296
|
assertFalse(result.succeeded());
|
|
297
297
|
assertEquals(expectedReason, result.getFailureReason());
|
|
298
298
|
assertNoTemporaryFilesLeft();
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
package com.microsoft.codepush.react;
|
|
2
|
+
|
|
3
|
+
import static org.junit.Assert.assertEquals;
|
|
4
|
+
import static org.junit.Assert.assertFalse;
|
|
5
|
+
import static org.junit.Assert.assertTrue;
|
|
6
|
+
|
|
7
|
+
import org.junit.Test;
|
|
8
|
+
|
|
9
|
+
import java.io.IOException;
|
|
10
|
+
import java.net.ConnectException;
|
|
11
|
+
import java.net.MalformedURLException;
|
|
12
|
+
import java.net.SocketException;
|
|
13
|
+
import java.net.SocketTimeoutException;
|
|
14
|
+
import java.net.UnknownHostException;
|
|
15
|
+
|
|
16
|
+
import javax.net.ssl.SSLHandshakeException;
|
|
17
|
+
|
|
18
|
+
public class CodePushErrorCodeTest {
|
|
19
|
+
|
|
20
|
+
@Test
|
|
21
|
+
public void namesAConnectionThatDroppedAsANetworkFailure() {
|
|
22
|
+
// The message the reported majority of Android failures arrive with.
|
|
23
|
+
assertEquals(CodePushErrorCode.NETWORK,
|
|
24
|
+
CodePushErrorCode.of(new SocketException("Software caused connection abort")));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Test
|
|
28
|
+
public void namesAConnectionThatTimedOutAsANetworkFailure() {
|
|
29
|
+
assertEquals(CodePushErrorCode.NETWORK, CodePushErrorCode.of(new SocketTimeoutException("timeout")));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Test
|
|
33
|
+
public void namesAConnectionThatNeverOpenedAsANetworkFailure() {
|
|
34
|
+
assertEquals(CodePushErrorCode.NETWORK, CodePushErrorCode.of(new ConnectException("Connection refused")));
|
|
35
|
+
assertEquals(CodePushErrorCode.NETWORK, CodePushErrorCode.of(new UnknownHostException("cdn.example.test")));
|
|
36
|
+
assertEquals(CodePushErrorCode.NETWORK, CodePushErrorCode.of(new SSLHandshakeException("handshake failed")));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Test
|
|
40
|
+
public void namesADownloadThatStoppedShortAsANetworkFailure() {
|
|
41
|
+
// The socket raised nothing for it, so only the byte count says the body was cut off.
|
|
42
|
+
Throwable error = new CodePushIncompleteDownloadException(1024, 4096);
|
|
43
|
+
|
|
44
|
+
assertEquals(CodePushErrorCode.NETWORK, CodePushErrorCode.of(error));
|
|
45
|
+
assertTrue(CodePushErrorCode.isNetworkFailure(error));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test
|
|
49
|
+
public void namesAnErrorStatusAsAnHttpFailureRatherThanANetworkOne() {
|
|
50
|
+
Throwable error = new CodePushHttpException("https://cdn.example.test/full.zip", 503);
|
|
51
|
+
|
|
52
|
+
assertEquals(CodePushErrorCode.HTTP, CodePushErrorCode.of(error));
|
|
53
|
+
assertFalse("a server that answered is not a network that failed",
|
|
54
|
+
CodePushErrorCode.isNetworkFailure(error));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test
|
|
58
|
+
public void namesAnUpdateThatIsNotWhatItClaimedAsAnIntegrityFailure() {
|
|
59
|
+
assertEquals(CodePushErrorCode.INTEGRITY,
|
|
60
|
+
CodePushErrorCode.of(new CodePushInvalidUpdateException("The update contents failed the data integrity check.")));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Test
|
|
64
|
+
public void readsThroughTheWrapperADownloadCatchesItsFailuresIn() {
|
|
65
|
+
Throwable wrapped = new CodePushUnknownException("Error closing IO resources.",
|
|
66
|
+
new SocketException("Connection reset"));
|
|
67
|
+
|
|
68
|
+
assertEquals(CodePushErrorCode.NETWORK, CodePushErrorCode.of(wrapped));
|
|
69
|
+
assertTrue(CodePushErrorCode.isNetworkFailure(wrapped));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Test
|
|
73
|
+
public void namesTheFailuresThatUsedToEscapeTheDownloadUncaught() {
|
|
74
|
+
// Neither is an `IOException`, so the download's old catch let them past and left
|
|
75
|
+
// the promise waiting on it unsettled. They are classified like anything else now.
|
|
76
|
+
assertEquals(CodePushErrorCode.UNKNOWN,
|
|
77
|
+
CodePushErrorCode.of(new CodePushMalformedDataException("not a url", new MalformedURLException())));
|
|
78
|
+
assertEquals(CodePushErrorCode.UNKNOWN,
|
|
79
|
+
CodePushErrorCode.of(new IllegalStateException("File is outside extraction target directory.")));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Test
|
|
83
|
+
public void leavesAFailureNothingHasAWordForUnnamed() {
|
|
84
|
+
Throwable error = new IOException("the disk is full");
|
|
85
|
+
|
|
86
|
+
assertEquals(CodePushErrorCode.UNKNOWN, CodePushErrorCode.of(error));
|
|
87
|
+
assertFalse("an unnamed failure must not be retried as if the network caused it",
|
|
88
|
+
CodePushErrorCode.isNetworkFailure(error));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -15,19 +15,26 @@ import java.io.File;
|
|
|
15
15
|
import java.io.IOException;
|
|
16
16
|
import java.util.ArrayList;
|
|
17
17
|
import java.util.Arrays;
|
|
18
|
+
import java.util.HashMap;
|
|
19
|
+
import java.util.HashSet;
|
|
18
20
|
import java.util.List;
|
|
21
|
+
import java.util.Map;
|
|
22
|
+
import java.util.Set;
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
|
-
* Which
|
|
22
|
-
*
|
|
25
|
+
* Which archives an update is downloaded from, and how a failed one picks the next rung:
|
|
26
|
+
* the patch archive after an asset-side diff failure, the full archive after every other.
|
|
23
27
|
*/
|
|
24
28
|
public class CodePushUpdateManagerBinaryPatchTest {
|
|
25
29
|
|
|
26
30
|
private static final String BUNDLE_FILE_NAME = "index.android.bundle";
|
|
27
31
|
private static final String FULL_ARCHIVE_URL = "https://example.test/updates/full.zip";
|
|
28
32
|
private static final String PATCH_ARCHIVE_URL = "https://example.test/updates/full.zip-patch.zip";
|
|
33
|
+
private static final String DIFF_ARCHIVE_URL = "https://example.test/updates/diff-from-base.zip";
|
|
29
34
|
/** Long enough that a timing that wrongly included it could not be mistaken for one that did not. */
|
|
30
35
|
private static final long FULL_ARCHIVE_DOWNLOAD_DURATION_MS = 300;
|
|
36
|
+
/** Long enough that a total which dropped the attempt that installed could not be mistaken for one that kept it. */
|
|
37
|
+
private static final long PATCH_ARCHIVE_DOWNLOAD_DURATION_MS = 300;
|
|
31
38
|
|
|
32
39
|
@Rule
|
|
33
40
|
public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
|
|
@@ -46,7 +53,7 @@ public class CodePushUpdateManagerBinaryPatchTest {
|
|
|
46
53
|
@Test
|
|
47
54
|
public void downloadsTheFullArchiveWhenTheUpdateHasNoBinaryPatch() throws IOException {
|
|
48
55
|
RecordingUpdateManager updateManager = new RecordingUpdateManager(mDocumentsDirectory,
|
|
49
|
-
|
|
56
|
+
ArchiveRestoreResult.success());
|
|
50
57
|
|
|
51
58
|
JSONObject patchResult = updateManager.downloadPackage(updatePackage(null), BUNDLE_FILE_NAME, ignoreProgress());
|
|
52
59
|
|
|
@@ -58,7 +65,7 @@ public class CodePushUpdateManagerBinaryPatchTest {
|
|
|
58
65
|
@Test
|
|
59
66
|
public void downloadsOnlyTheBinaryPatchArchiveWhenThePatchApplies() throws IOException {
|
|
60
67
|
RecordingUpdateManager updateManager = new RecordingUpdateManager(mDocumentsDirectory,
|
|
61
|
-
|
|
68
|
+
ArchiveRestoreResult.success());
|
|
62
69
|
|
|
63
70
|
JSONObject patchResult = updateManager.downloadPackage(updatePackage(PATCH_ARCHIVE_URL), BUNDLE_FILE_NAME,
|
|
64
71
|
ignoreProgress());
|
|
@@ -71,7 +78,7 @@ public class CodePushUpdateManagerBinaryPatchTest {
|
|
|
71
78
|
@Test
|
|
72
79
|
public void fallsBackToTheFullArchiveOnceWhenThePatchCannotBeApplied() throws IOException {
|
|
73
80
|
RecordingUpdateManager updateManager = new RecordingUpdateManager(mDocumentsDirectory,
|
|
74
|
-
|
|
81
|
+
ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_BASE_HASH_MISMATCH));
|
|
75
82
|
|
|
76
83
|
JSONObject patchResult = updateManager.downloadPackage(updatePackage(PATCH_ARCHIVE_URL), BUNDLE_FILE_NAME,
|
|
77
84
|
ignoreProgress());
|
|
@@ -82,7 +89,7 @@ public class CodePushUpdateManagerBinaryPatchTest {
|
|
|
82
89
|
assertEquals(Arrays.asList(true, false), updateManager.patchAttempts);
|
|
83
90
|
assertEquals("fallback", patchResult.optString("status", null));
|
|
84
91
|
// The reason travels to the app exactly as the applier worded it.
|
|
85
|
-
assertEquals(
|
|
92
|
+
assertEquals(ArchiveRestoreResult.REASON_BASE_HASH_MISMATCH, patchResult.optString("fallbackReason", null));
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
@Test
|
|
@@ -100,10 +107,115 @@ public class CodePushUpdateManagerBinaryPatchTest {
|
|
|
100
107
|
assertFalse(patchResult.has("fallbackReason"));
|
|
101
108
|
}
|
|
102
109
|
|
|
110
|
+
@Test
|
|
111
|
+
public void downloadsOnlyTheAssetDiffArchiveWhenTheDiffApplies() throws IOException {
|
|
112
|
+
RecordingUpdateManager updateManager = new RecordingUpdateManager(mDocumentsDirectory,
|
|
113
|
+
ArchiveRestoreResult.success());
|
|
114
|
+
|
|
115
|
+
JSONObject patchResult = updateManager.downloadPackage(updatePackage(PATCH_ARCHIVE_URL, DIFF_ARCHIVE_URL),
|
|
116
|
+
BUNDLE_FILE_NAME, ignoreProgress());
|
|
117
|
+
|
|
118
|
+
assertEquals(Arrays.asList(DIFF_ARCHIVE_URL), updateManager.downloadedUrls);
|
|
119
|
+
assertEquals("applied", patchResult.optString("status", null));
|
|
120
|
+
assertEquals("asset-diff", patchResult.optString("archive", null));
|
|
121
|
+
assertEquals(1, patchResult.optJSONArray("attempts").length());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@Test
|
|
125
|
+
public void fallsBackToThePatchArchiveWhenTheDiffFailsAfterItsBundleWasRestored() throws IOException {
|
|
126
|
+
RecordingUpdateManager updateManager = new RecordingUpdateManager(mDocumentsDirectory,
|
|
127
|
+
ArchiveRestoreResult.success());
|
|
128
|
+
updateManager.urlsRestoringTheBundle.add(DIFF_ARCHIVE_URL);
|
|
129
|
+
updateManager.patchOutcomesByUrl.put(DIFF_ARCHIVE_URL,
|
|
130
|
+
ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_ASSET_MERGE_FAILED));
|
|
131
|
+
updateManager.downloadDurationMsByUrl.put(PATCH_ARCHIVE_URL, PATCH_ARCHIVE_DOWNLOAD_DURATION_MS);
|
|
132
|
+
|
|
133
|
+
JSONObject patchResult = updateManager.downloadPackage(updatePackage(PATCH_ARCHIVE_URL, DIFF_ARCHIVE_URL),
|
|
134
|
+
BUNDLE_FILE_NAME, ignoreProgress());
|
|
135
|
+
|
|
136
|
+
// A failure after the restore is on the asset side of the diff, which the patch
|
|
137
|
+
// archive does not share - so the patch is the next rung, not the full archive.
|
|
138
|
+
assertEquals(Arrays.asList(DIFF_ARCHIVE_URL, PATCH_ARCHIVE_URL), updateManager.downloadedUrls);
|
|
139
|
+
assertEquals("applied", patchResult.optString("status", null));
|
|
140
|
+
assertEquals("binary-patch", patchResult.optString("archive", null));
|
|
141
|
+
assertEquals(2, patchResult.optJSONArray("attempts").length());
|
|
142
|
+
JSONObject diffAttempt = patchResult.optJSONArray("attempts").optJSONObject(0);
|
|
143
|
+
assertEquals("asset-diff", diffAttempt.optString("archive", null));
|
|
144
|
+
assertEquals(ArchiveRestoreResult.REASON_ASSET_MERGE_FAILED, diffAttempt.optString("fallbackReason", null));
|
|
145
|
+
|
|
146
|
+
// The path runs to the end, not to the moment the diff was given up on: the attempt
|
|
147
|
+
// that installed is most of it, so a total frozen at the first fallback would be
|
|
148
|
+
// smaller than the attempt it is supposed to contain.
|
|
149
|
+
long totalDurationMs = patchResult.optLong("totalDurationMs", -1);
|
|
150
|
+
long patchAttemptDurationMs = patchResult.optJSONArray("attempts").optJSONObject(1)
|
|
151
|
+
.optLong("durationMs", -1);
|
|
152
|
+
assertTrue("the total covers the attempt that installed: " + totalDurationMs + " ms total against "
|
|
153
|
+
+ patchAttemptDurationMs + " ms in that attempt alone",
|
|
154
|
+
totalDurationMs >= patchAttemptDurationMs);
|
|
155
|
+
assertTrue("the total covers the download the attempt that installed spent: " + totalDurationMs + " ms",
|
|
156
|
+
totalDurationMs >= PATCH_ARCHIVE_DOWNLOAD_DURATION_MS);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@Test
|
|
160
|
+
public void skipsThePatchArchiveWhenTheDiffFailedInTheBundlePatchBothArchivesCarry() throws IOException {
|
|
161
|
+
RecordingUpdateManager updateManager = new RecordingUpdateManager(mDocumentsDirectory,
|
|
162
|
+
ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_BASE_HASH_MISMATCH));
|
|
163
|
+
|
|
164
|
+
JSONObject patchResult = updateManager.downloadPackage(updatePackage(PATCH_ARCHIVE_URL, DIFF_ARCHIVE_URL),
|
|
165
|
+
BUNDLE_FILE_NAME, ignoreProgress());
|
|
166
|
+
|
|
167
|
+
// Both archives carry the same bundle patch, so the patch archive would fail the
|
|
168
|
+
// same way and is passed over for the full one.
|
|
169
|
+
assertEquals(Arrays.asList(DIFF_ARCHIVE_URL, FULL_ARCHIVE_URL), updateManager.downloadedUrls);
|
|
170
|
+
assertEquals("fallback", patchResult.optString("status", null));
|
|
171
|
+
assertEquals("asset-diff", patchResult.optString("archive", null));
|
|
172
|
+
assertEquals(ArchiveRestoreResult.REASON_BASE_HASH_MISMATCH, patchResult.optString("fallbackReason", null));
|
|
173
|
+
assertEquals(1, patchResult.optJSONArray("attempts").length());
|
|
174
|
+
assertFalse("an attempt that never restored the bundle has no apply to report",
|
|
175
|
+
patchResult.optJSONArray("attempts").optJSONObject(0).has("applyDurationMs"));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@Test
|
|
179
|
+
public void fallsBackToTheFullArchiveWhenTheDiffAndThePatchBothFail() throws IOException {
|
|
180
|
+
RecordingUpdateManager updateManager = new RecordingUpdateManager(mDocumentsDirectory,
|
|
181
|
+
ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_TARGET_VERIFICATION_FAILED));
|
|
182
|
+
updateManager.urlsRestoringTheBundle.add(DIFF_ARCHIVE_URL);
|
|
183
|
+
updateManager.patchOutcomesByUrl.put(DIFF_ARCHIVE_URL,
|
|
184
|
+
ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_ASSET_MERGE_FAILED));
|
|
185
|
+
|
|
186
|
+
JSONObject patchResult = updateManager.downloadPackage(updatePackage(PATCH_ARCHIVE_URL, DIFF_ARCHIVE_URL),
|
|
187
|
+
BUNDLE_FILE_NAME, ignoreProgress());
|
|
188
|
+
|
|
189
|
+
assertEquals(Arrays.asList(DIFF_ARCHIVE_URL, PATCH_ARCHIVE_URL, FULL_ARCHIVE_URL),
|
|
190
|
+
updateManager.downloadedUrls);
|
|
191
|
+
assertEquals("fallback", patchResult.optString("status", null));
|
|
192
|
+
// The top of the result retells the last attempt; the chain holds them both.
|
|
193
|
+
assertEquals("binary-patch", patchResult.optString("archive", null));
|
|
194
|
+
assertEquals(ArchiveRestoreResult.REASON_TARGET_VERIFICATION_FAILED,
|
|
195
|
+
patchResult.optString("fallbackReason", null));
|
|
196
|
+
assertEquals(ArchiveRestoreResult.REASON_ASSET_MERGE_FAILED,
|
|
197
|
+
patchResult.optJSONArray("attempts").optJSONObject(0).optString("fallbackReason", null));
|
|
198
|
+
assertEquals(ArchiveRestoreResult.REASON_TARGET_VERIFICATION_FAILED,
|
|
199
|
+
patchResult.optJSONArray("attempts").optJSONObject(1).optString("fallbackReason", null));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@Test
|
|
203
|
+
public void fallsBackToTheFullArchiveWhenTheDiffFailsAndTheUpdateHasNoPatchArchive() throws IOException {
|
|
204
|
+
RecordingUpdateManager updateManager = new RecordingUpdateManager(mDocumentsDirectory,
|
|
205
|
+
ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_ASSET_MERGE_FAILED));
|
|
206
|
+
updateManager.urlsRestoringTheBundle.add(DIFF_ARCHIVE_URL);
|
|
207
|
+
|
|
208
|
+
JSONObject patchResult = updateManager.downloadPackage(updatePackage(null, DIFF_ARCHIVE_URL),
|
|
209
|
+
BUNDLE_FILE_NAME, ignoreProgress());
|
|
210
|
+
|
|
211
|
+
assertEquals(Arrays.asList(DIFF_ARCHIVE_URL, FULL_ARCHIVE_URL), updateManager.downloadedUrls);
|
|
212
|
+
assertEquals("fallback", patchResult.optString("status", null));
|
|
213
|
+
}
|
|
214
|
+
|
|
103
215
|
@Test
|
|
104
216
|
public void timesThePatchAttemptRatherThanTheDownloadThatFollowedIt() throws IOException {
|
|
105
217
|
RecordingUpdateManager updateManager = new RecordingUpdateManager(mDocumentsDirectory,
|
|
106
|
-
|
|
218
|
+
ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_PATCH_APPLY_FAILED));
|
|
107
219
|
updateManager.fullArchiveDownloadDurationMs = FULL_ARCHIVE_DOWNLOAD_DURATION_MS;
|
|
108
220
|
|
|
109
221
|
JSONObject patchResult = updateManager.downloadPackage(updatePackage(PATCH_ARCHIVE_URL), BUNDLE_FILE_NAME,
|
|
@@ -111,17 +223,17 @@ public class CodePushUpdateManagerBinaryPatchTest {
|
|
|
111
223
|
|
|
112
224
|
// The attempt was over before the full archive was even asked for, so what it reports
|
|
113
225
|
// is time the patch spent and nothing else.
|
|
114
|
-
long reportedDurationMs = patchResult.optLong("
|
|
226
|
+
long reportedDurationMs = patchResult.optLong("totalDurationMs", -1);
|
|
115
227
|
assertTrue("the fallback is timed over the download that followed it: " + reportedDurationMs + " ms",
|
|
116
228
|
reportedDurationMs >= 0 && reportedDurationMs < FULL_ARCHIVE_DOWNLOAD_DURATION_MS);
|
|
117
229
|
}
|
|
118
230
|
|
|
119
231
|
@Test
|
|
120
232
|
public void removesTheBinaryPatchWorkingDirectoryWhateverTheOutcomeIs() throws IOException {
|
|
121
|
-
List<
|
|
122
|
-
|
|
233
|
+
List<ArchiveRestoreResult> outcomes = Arrays.asList(ArchiveRestoreResult.success(),
|
|
234
|
+
ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_TARGET_VERIFICATION_FAILED), null);
|
|
123
235
|
|
|
124
|
-
for (
|
|
236
|
+
for (ArchiveRestoreResult outcome : outcomes) {
|
|
125
237
|
assertTrue(mBinaryPatchFolder.mkdirs());
|
|
126
238
|
assertTrue(new File(mBinaryPatchFolder, CodePushConstants.BINARY_PATCH_TARGET_FILE_NAME).createNewFile());
|
|
127
239
|
|
|
@@ -133,6 +245,10 @@ public class CodePushUpdateManagerBinaryPatchTest {
|
|
|
133
245
|
}
|
|
134
246
|
|
|
135
247
|
private static JSONObject updatePackage(String binaryPatchDownloadUrl) {
|
|
248
|
+
return updatePackage(binaryPatchDownloadUrl, null);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private static JSONObject updatePackage(String binaryPatchDownloadUrl, String assetDiffDownloadUrl) {
|
|
136
252
|
JSONObject updatePackage = new JSONObject();
|
|
137
253
|
CodePushUtils.setJSONValueForKey(updatePackage, CodePushConstants.PACKAGE_HASH_KEY, "package-hash");
|
|
138
254
|
CodePushUtils.setJSONValueForKey(updatePackage, CodePushConstants.DOWNLOAD_URL_KEY, FULL_ARCHIVE_URL);
|
|
@@ -140,6 +256,10 @@ public class CodePushUpdateManagerBinaryPatchTest {
|
|
|
140
256
|
CodePushUtils.setJSONValueForKey(updatePackage, CodePushConstants.BINARY_PATCH_DOWNLOAD_URL_KEY,
|
|
141
257
|
binaryPatchDownloadUrl);
|
|
142
258
|
}
|
|
259
|
+
if (assetDiffDownloadUrl != null) {
|
|
260
|
+
CodePushUtils.setJSONValueForKey(updatePackage, CodePushConstants.ASSET_DIFF_DOWNLOAD_URL_KEY,
|
|
261
|
+
assetDiffDownloadUrl);
|
|
262
|
+
}
|
|
143
263
|
|
|
144
264
|
return updatePackage;
|
|
145
265
|
}
|
|
@@ -163,37 +283,57 @@ public class CodePushUpdateManagerBinaryPatchTest {
|
|
|
163
283
|
*/
|
|
164
284
|
private static class RecordingUpdateManager extends CodePushUpdateManager {
|
|
165
285
|
|
|
166
|
-
private final
|
|
286
|
+
private final ArchiveRestoreResult mPatchOutcome;
|
|
167
287
|
|
|
168
288
|
final List<String> downloadedUrls = new ArrayList<>();
|
|
169
289
|
final List<Boolean> patchAttempts = new ArrayList<>();
|
|
170
290
|
|
|
291
|
+
/** Outcomes for one archive at a time; every other archive ends in the shared one. */
|
|
292
|
+
final Map<String, ArchiveRestoreResult> patchOutcomesByUrl = new HashMap<>();
|
|
293
|
+
|
|
294
|
+
/** Archives whose attempt restores the bundle before it ends, whichever way it ends. */
|
|
295
|
+
final Set<String> urlsRestoringTheBundle = new HashSet<>();
|
|
296
|
+
|
|
171
297
|
/** How long the full archive takes to download, for timings that must not include it. */
|
|
172
298
|
long fullArchiveDownloadDurationMs = 0;
|
|
173
299
|
|
|
174
|
-
/**
|
|
175
|
-
|
|
300
|
+
/** How long one archive takes to download, for timings that have to tell the attempts apart. */
|
|
301
|
+
final Map<String, Long> downloadDurationMsByUrl = new HashMap<>();
|
|
302
|
+
|
|
303
|
+
/** @param patchOutcome what a patch download ends in, or null when it cannot be downloaded */
|
|
304
|
+
RecordingUpdateManager(String documentsDirectory, ArchiveRestoreResult patchOutcome) {
|
|
176
305
|
super(documentsDirectory, (CodePushBinaryPatch) null);
|
|
177
306
|
mPatchOutcome = patchOutcome;
|
|
178
307
|
}
|
|
179
308
|
|
|
180
309
|
@Override
|
|
181
|
-
|
|
310
|
+
ArchiveRestoreResult downloadAndInstallPackage(JSONObject updatePackage, String expectedBundleFileName,
|
|
182
311
|
DownloadProgressCallback progressCallback,
|
|
183
312
|
String downloadUrlString, boolean isBinaryPatchUpdate,
|
|
184
|
-
|
|
313
|
+
ArchiveAttemptLog patchAttempt) throws IOException {
|
|
185
314
|
downloadedUrls.add(downloadUrlString);
|
|
186
315
|
patchAttempts.add(isBinaryPatchUpdate);
|
|
316
|
+
if (downloadDurationMsByUrl.containsKey(downloadUrlString)) {
|
|
317
|
+
sleep(downloadDurationMsByUrl.get(downloadUrlString));
|
|
318
|
+
}
|
|
187
319
|
|
|
188
320
|
if (!isBinaryPatchUpdate) {
|
|
189
321
|
sleep(fullArchiveDownloadDurationMs);
|
|
190
|
-
return
|
|
322
|
+
return ArchiveRestoreResult.success();
|
|
191
323
|
}
|
|
192
|
-
|
|
324
|
+
|
|
325
|
+
if (urlsRestoringTheBundle.contains(downloadUrlString)) {
|
|
326
|
+
patchAttempt.recordBundleRestored(1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
ArchiveRestoreResult outcome = patchOutcomesByUrl.containsKey(downloadUrlString)
|
|
330
|
+
? patchOutcomesByUrl.get(downloadUrlString)
|
|
331
|
+
: mPatchOutcome;
|
|
332
|
+
if (outcome == null) {
|
|
193
333
|
throw new IOException("the binary patch archive is not there");
|
|
194
334
|
}
|
|
195
335
|
|
|
196
|
-
return
|
|
336
|
+
return outcome;
|
|
197
337
|
}
|
|
198
338
|
|
|
199
339
|
private static void sleep(long durationMs) {
|