@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.
Files changed (46) hide show
  1. package/CodePush.podspec +5 -0
  2. package/README.md +20 -145
  3. package/android/app/src/main/java/com/microsoft/codepush/react/ArchiveAttemptLog.java +181 -0
  4. package/android/app/src/main/java/com/microsoft/codepush/react/{BinaryPatchResult.java → ArchiveRestoreResult.java} +17 -7
  5. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +20 -20
  6. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +20 -1
  7. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushErrorCode.java +86 -0
  8. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushHttpException.java +29 -0
  9. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushIncompleteDownloadException.java +17 -0
  10. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +12 -7
  11. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +143 -43
  12. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +31 -31
  13. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushErrorCodeTest.java +90 -0
  14. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerBinaryPatchTest.java +159 -19
  15. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +304 -15
  16. package/cli/commands/createHistoryCommand/createReleaseHistory.test.ts +133 -0
  17. package/cli/commands/createHistoryCommand/createReleaseHistory.ts +3 -11
  18. package/cli/commands/releaseCommand/addToReleaseHistory.ts +3 -11
  19. package/cli/commands/releaseCommand/release.test.ts +55 -13
  20. package/cli/commands/releaseCommand/release.ts +39 -6
  21. package/cli/commands/updateHistoryCommand/updateReleaseHistory.ts +3 -11
  22. package/cli/dist/commands/createHistoryCommand/createReleaseHistory.js +2 -8
  23. package/cli/dist/commands/createHistoryCommand/createReleaseHistory.test.js +95 -0
  24. package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +2 -8
  25. package/cli/dist/commands/releaseCommand/release.js +23 -8
  26. package/cli/dist/commands/releaseCommand/release.test.js +37 -9
  27. package/cli/dist/commands/updateHistoryCommand/updateReleaseHistory.js +2 -8
  28. package/cli/dist/functions/resolveAssetDiffBases.js +7 -2
  29. package/cli/dist/functions/resolveAssetDiffBases.test.js +26 -12
  30. package/cli/dist/functions/stageReleaseHistoryFile.js +28 -0
  31. package/cli/functions/resolveAssetDiffBases.test.ts +28 -14
  32. package/cli/functions/resolveAssetDiffBases.ts +8 -1
  33. package/cli/functions/stageReleaseHistoryFile.ts +38 -0
  34. package/ios/CodePush/CodePush.h +6 -3
  35. package/ios/CodePush/CodePush.mm +10 -10
  36. package/ios/CodePush/CodePushBinaryPatch.h +18 -8
  37. package/ios/CodePush/CodePushBinaryPatch.m +23 -22
  38. package/ios/CodePush/CodePushDownloadHandler.m +2 -1
  39. package/ios/CodePush/CodePushErrorUtils.m +55 -0
  40. package/ios/CodePush/CodePushPackage.m +242 -71
  41. package/package.json +2 -1
  42. package/src/CodePush.js +28 -18
  43. package/src/CodePush.test.js +110 -55
  44. package/src/package-mixins.js +19 -10
  45. package/typings/react-native-code-push.d.ts +152 -41
  46. 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
- BinaryPatchResult result = restoreBundle(BASE_BUNDLE, applier);
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
- BinaryPatchResult result = new CodePushBinaryPatch(providerOf(BASE_BUNDLE), succeedingApplier())
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
- BinaryPatchResult result = new CodePushBinaryPatch(providerOf(BASE_BUNDLE), succeedingApplier())
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
- BinaryPatchResult result = restoreBundle(BASE_BUNDLE, succeedingApplier());
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
- BinaryPatchResult result = restoreBundle(BASE_BUNDLE,
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(BinaryPatchResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
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(BinaryPatchResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
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(BinaryPatchResult.REASON_UNSUPPORTED_FORMAT, restoreBundle(BASE_BUNDLE, succeedingApplier()));
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(BinaryPatchResult.REASON_UNSUPPORTED_FORMAT, restoreBundle(BASE_BUNDLE, succeedingApplier()));
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(BinaryPatchResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
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(BinaryPatchResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
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(BinaryPatchResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
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(BinaryPatchResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
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(BinaryPatchResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
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(BinaryPatchResult.REASON_INVALID_MANIFEST, restoreBundle(BASE_BUNDLE, succeedingApplier()));
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(BinaryPatchResult.REASON_BASE_BUNDLE_UNAVAILABLE, restoreBundle(binaryPatch));
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
- BinaryPatchResult result = restoreBundle(bytes("a bundle from another app build"), applier);
232
+ ArchiveRestoreResult result = restoreBundle(bytes("a bundle from another app build"), applier);
233
233
 
234
- assertFailure(BinaryPatchResult.REASON_BASE_HASH_MISMATCH, result);
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
- BinaryPatchResult result = restoreBundle(BASE_BUNDLE,
240
+ ArchiveRestoreResult result = restoreBundle(BASE_BUNDLE,
241
241
  new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_UNSUPPORTED_COMPRESSION, null));
242
242
 
243
- assertFailure(BinaryPatchResult.REASON_UNSUPPORTED_FORMAT, result);
243
+ assertFailure(ArchiveRestoreResult.REASON_UNSUPPORTED_FORMAT, result);
244
244
  }
245
245
 
246
246
  @Test
247
247
  public void reportsAFailedApplyWhenThePatchHeaderIsCorrupt() {
248
- BinaryPatchResult result = restoreBundle(BASE_BUNDLE,
248
+ ArchiveRestoreResult result = restoreBundle(BASE_BUNDLE,
249
249
  new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_INVALID_HEADER, null));
250
250
 
251
- assertFailure(BinaryPatchResult.REASON_PATCH_APPLY_FAILED, result);
251
+ assertFailure(ArchiveRestoreResult.REASON_PATCH_APPLY_FAILED, result);
252
252
  assertNoRestoredBundleInTheContents();
253
253
  }
254
254
 
255
255
  @Test
256
256
  public void reportsAFailedApplyWhenTheNativeLibraryIsMissing() {
257
- BinaryPatchResult result = restoreBundle(BASE_BUNDLE,
257
+ ArchiveRestoreResult result = restoreBundle(BASE_BUNDLE,
258
258
  new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_LIBRARY_UNAVAILABLE, null));
259
259
 
260
- assertFailure(BinaryPatchResult.REASON_PATCH_APPLY_FAILED, result);
260
+ assertFailure(ArchiveRestoreResult.REASON_PATCH_APPLY_FAILED, result);
261
261
  }
262
262
 
263
263
  @Test
264
264
  public void reportsAFailedVerificationWhenTheRestoredBundleHasAnotherSize() {
265
- BinaryPatchResult result = restoreBundle(BASE_BUNDLE,
265
+ ArchiveRestoreResult result = restoreBundle(BASE_BUNDLE,
266
266
  new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_OK, bytes("too short")));
267
267
 
268
- assertFailure(BinaryPatchResult.REASON_TARGET_VERIFICATION_FAILED, result);
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
- BinaryPatchResult result = restoreBundle(BASE_BUNDLE,
279
+ ArchiveRestoreResult result = restoreBundle(BASE_BUNDLE,
280
280
  new FakePatchApplier(CodePushBinaryPatch.PatchApplier.RESULT_OK, wrongBundle));
281
281
 
282
- assertFailure(BinaryPatchResult.REASON_TARGET_VERIFICATION_FAILED, result);
282
+ assertFailure(ArchiveRestoreResult.REASON_TARGET_VERIFICATION_FAILED, result);
283
283
  assertNoRestoredBundleInTheContents();
284
284
  }
285
285
 
286
- private BinaryPatchResult restoreBundle(byte[] baseBundle, CodePushBinaryPatch.PatchApplier applier) {
286
+ private ArchiveRestoreResult restoreBundle(byte[] baseBundle, CodePushBinaryPatch.PatchApplier applier) {
287
287
  return restoreBundle(new CodePushBinaryPatch(providerOf(baseBundle), applier));
288
288
  }
289
289
 
290
- private BinaryPatchResult restoreBundle(CodePushBinaryPatch binaryPatch) {
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, BinaryPatchResult result) {
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 archive an update is downloaded from, and how the one download that is allowed to
22
- * fail falls back to the other one.
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
- BinaryPatchResult.success());
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
- BinaryPatchResult.success());
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
- BinaryPatchResult.failure(BinaryPatchResult.REASON_BASE_HASH_MISMATCH));
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(BinaryPatchResult.REASON_BASE_HASH_MISMATCH, patchResult.optString("fallbackReason", null));
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
- BinaryPatchResult.failure(BinaryPatchResult.REASON_PATCH_APPLY_FAILED));
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("applyDurationMs", -1);
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<BinaryPatchResult> outcomes = Arrays.asList(BinaryPatchResult.success(),
122
- BinaryPatchResult.failure(BinaryPatchResult.REASON_TARGET_VERIFICATION_FAILED), null);
233
+ List<ArchiveRestoreResult> outcomes = Arrays.asList(ArchiveRestoreResult.success(),
234
+ ArchiveRestoreResult.failure(ArchiveRestoreResult.REASON_TARGET_VERIFICATION_FAILED), null);
123
235
 
124
- for (BinaryPatchResult outcome : outcomes) {
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 BinaryPatchResult mPatchOutcome;
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
- /** @param patchOutcome what the patch download ends in, or null when it cannot be downloaded */
175
- RecordingUpdateManager(String documentsDirectory, BinaryPatchResult patchOutcome) {
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
- BinaryPatchResult downloadAndInstallPackage(JSONObject updatePackage, String expectedBundleFileName,
310
+ ArchiveRestoreResult downloadAndInstallPackage(JSONObject updatePackage, String expectedBundleFileName,
182
311
  DownloadProgressCallback progressCallback,
183
312
  String downloadUrlString, boolean isBinaryPatchUpdate,
184
- BinaryPatchAttempt patchAttempt) throws IOException {
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 BinaryPatchResult.success();
322
+ return ArchiveRestoreResult.success();
191
323
  }
192
- if (mPatchOutcome == null) {
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 mPatchOutcome;
336
+ return outcome;
197
337
  }
198
338
 
199
339
  private static void sleep(long durationMs) {