react-native-update 10.43.2 → 10.44.0

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 (27) hide show
  1. package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +24 -12
  2. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +23 -8
  3. package/android/src/main/java/cn/reactnative/modules/update/ReactReloadManager.java +9 -1
  4. package/android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java +64 -0
  5. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +2 -2
  6. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +8 -8
  7. package/cpp/patch_core/archive_patch_core.cpp +16 -0
  8. package/cpp/patch_core/archive_patch_core.h +5 -0
  9. package/cpp/patch_core/jni_util.h +56 -0
  10. package/cpp/patch_core/patch_core_android.cpp +4 -37
  11. package/cpp/patch_core/state_ops.h +24 -0
  12. package/cpp/patch_core/tests/patch_core_test.cpp +32 -2
  13. package/cpp/patch_core/update_core_android.cpp +43 -69
  14. package/harmony/pushy/src/main/cpp/pushy.cpp +162 -35
  15. package/harmony/pushy/src/main/ets/DownloadTask.ts +59 -9
  16. package/harmony/pushy/src/main/ets/NativePatchCore.ts +2 -2
  17. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +52 -8
  18. package/harmony/pushy/src/main/ets/UpdateContext.ts +26 -9
  19. package/harmony/pushy.har +0 -0
  20. package/ios/RCTPushy/RCTPushyDownloader.mm +41 -2
  21. package/package.json +2 -1
  22. package/src/client.ts +130 -75
  23. package/src/provider.tsx +40 -26
  24. package/src/utils.ts +2 -2
  25. package/harmony/pushy/src/main/cpp/pushy.c +0 -117
  26. package/harmony/pushy/src/main/cpp/pushy.h +0 -8
  27. package/src/__tests__/setup.ts +0 -44
@@ -92,6 +92,10 @@ final class BundledResourceCopier {
92
92
  SafeZipFile baseZipFile = zipFileMap.get(context.getPackageResourcePath());
93
93
  HashMap<String, ArrayList<File>> remainingFiles =
94
94
  new HashMap<String, ArrayList<File>>(resToCopy);
95
+ // Track copies that were located but failed to write. These are real
96
+ // failures (disk full, corrupt archive) that must fail the update,
97
+ // not skips — otherwise the update activates with missing resources.
98
+ ArrayList<String> failedCopies = new ArrayList<String>();
95
99
 
96
100
  for (String fromPath : new ArrayList<String>(remainingFiles.keySet())) {
97
101
  ArrayList<File> targets = remainingFiles.get(fromPath);
@@ -168,23 +172,31 @@ final class BundledResourceCopier {
168
172
  }
169
173
  lastTarget = target;
170
174
  } catch (IOException e) {
171
- if (UpdateContext.DEBUG) {
172
- Log.w(
173
- UpdateContext.TAG,
174
- "Failed to copy resource "
175
- + actualSourcePath
176
- + " to "
177
- + target
178
- + ": "
179
- + e.getMessage()
180
- );
181
- }
175
+ // A located resource that fails to write is a hard
176
+ // failure, not a skip: record it and fail the update
177
+ // after the loop so a broken update is not activated.
178
+ Log.e(
179
+ UpdateContext.TAG,
180
+ "Failed to copy resource "
181
+ + actualSourcePath
182
+ + " to "
183
+ + target,
184
+ e
185
+ );
186
+ failedCopies.add(actualSourcePath + " -> " + target);
182
187
  }
183
188
  }
184
189
  remainingFiles.remove(fromPath);
185
190
  }
186
191
 
187
- if (!remainingFiles.isEmpty() && UpdateContext.DEBUG) {
192
+ if (!failedCopies.isEmpty()) {
193
+ throw new IOException(
194
+ "Failed to copy " + failedCopies.size()
195
+ + " bundled resource(s): " + failedCopies
196
+ );
197
+ }
198
+
199
+ if (!remainingFiles.isEmpty()) {
188
200
  Log.w(
189
201
  UpdateContext.TAG,
190
202
  "Skipped " + remainingFiles.size() + " missing bundled resources"
@@ -29,6 +29,10 @@ import org.json.JSONTokener;
29
29
 
30
30
  class DownloadTask implements Runnable {
31
31
  private static final int DOWNLOAD_CHUNK_SIZE = 4096;
32
+ // When the server does not report Content-Length we cannot key progress
33
+ // events on percentage change, so throttle by bytes to avoid flooding the
34
+ // bridge (e.g. a 20MB chunked download would otherwise emit ~5000 events).
35
+ private static final long PROGRESS_BYTES_THRESHOLD = 256 * 1024;
32
36
  private static final OkHttpClient HTTP_CLIENT = new OkHttpClient();
33
37
 
34
38
  static {
@@ -98,6 +102,7 @@ class DownloadTask implements Runnable {
98
102
  long bytesRead;
99
103
  long received = 0;
100
104
  int currentPercentage = 0;
105
+ long lastPostedBytes = 0;
101
106
 
102
107
  try (
103
108
  BufferedSource source = body.source();
@@ -113,7 +118,8 @@ class DownloadTask implements Runnable {
113
118
  currentPercentage = percentage;
114
119
  postProgress(received, contentLength);
115
120
  }
116
- } else {
121
+ } else if (received - lastPostedBytes >= PROGRESS_BYTES_THRESHOLD) {
122
+ lastPostedBytes = received;
117
123
  postProgress(received, contentLength);
118
124
  }
119
125
  }
@@ -394,19 +400,28 @@ class DownloadTask implements Runnable {
394
400
  default:
395
401
  break;
396
402
  }
397
-
398
- if (params.listener != null) {
399
- params.listener.onDownloadCompleted(params);
400
- }
401
403
  } catch (Throwable error) {
402
- if (UpdateContext.DEBUG) {
403
- Log.e(UpdateContext.TAG, "download task failed", error);
404
- }
404
+ Log.e(UpdateContext.TAG, "download task failed", error);
405
405
  cleanUpAfterFailure(taskType);
406
406
 
407
407
  if (params.listener != null) {
408
408
  params.listener.onDownloadFailed(error);
409
409
  }
410
+ return;
411
+ }
412
+
413
+ // The task itself succeeded. Run the completion callback outside the
414
+ // try/catch above so an exception thrown by the callback (e.g. a
415
+ // FileProvider misconfiguration during installApk) is not mistaken for
416
+ // a download failure that deletes the successfully downloaded file and
417
+ // settles the promise a second time.
418
+ if (params.listener != null) {
419
+ try {
420
+ params.listener.onDownloadCompleted(params);
421
+ } catch (Throwable error) {
422
+ Log.e(UpdateContext.TAG, "download completion callback failed", error);
423
+ params.listener.onDownloadFailed(error);
424
+ }
410
425
  }
411
426
  }
412
427
 
@@ -75,7 +75,13 @@ final class ReactReloadManager {
75
75
  currentReactHost,
76
76
  createBundleLoader(application, updateBundlePath, true)
77
77
  );
78
- } catch (Throwable ignored) {
78
+ } catch (Throwable reloadError) {
79
+ Log.e(
80
+ UpdateContext.TAG,
81
+ "Failed to reload via ReactHost, falling back to Activity.recreate() "
82
+ + "(this may load the previous bundle until next cold start)",
83
+ reloadError
84
+ );
79
85
  currentActivity.recreate();
80
86
  }
81
87
  }
@@ -172,6 +178,7 @@ final class ReactReloadManager {
172
178
  }
173
179
  }
174
180
  } catch (Throwable ignored) {
181
+ Log.w(UpdateContext.TAG, "getReactHost via ReactDelegate reflection failed", ignored);
175
182
  }
176
183
  }
177
184
 
@@ -179,6 +186,7 @@ final class ReactReloadManager {
179
186
  Method getReactHostMethod = application.getClass().getMethod("getReactHost");
180
187
  return getReactHostMethod.invoke(application);
181
188
  } catch (Throwable ignored) {
189
+ Log.w(UpdateContext.TAG, "getReactHost via Application.getReactHost() failed", ignored);
182
190
  }
183
191
 
184
192
  return null;
@@ -0,0 +1,64 @@
1
+ package cn.reactnative.modules.update;
2
+
3
+ import android.util.Log;
4
+ import androidx.annotation.Nullable;
5
+ import com.facebook.react.bridge.Promise;
6
+
7
+ import java.util.concurrent.Executor;
8
+ import java.util.concurrent.Executors;
9
+ import java.util.concurrent.ThreadFactory;
10
+
11
+ /**
12
+ * Runs state-persistence operations (switchVersion / markSuccess / setUuid /
13
+ * setLocalHashInfo) on a dedicated single background thread.
14
+ *
15
+ * These operations only read/modify SharedPreferences via a synchronous
16
+ * commit(); they were previously dispatched to the UI thread purely to
17
+ * serialize them. markSuccess in particular runs on every cold start, so doing
18
+ * its blocking disk write on the main thread caused jank/ANR on low-end
19
+ * devices. A single-thread executor preserves the same serialization guarantee
20
+ * while keeping the disk I/O off the UI thread.
21
+ *
22
+ * Note: reload/restart operations must still run on the UI thread and therefore
23
+ * keep using {@link UiThreadRunner}.
24
+ */
25
+ final class StateSerialRunner {
26
+ interface Operation {
27
+ void run() throws Throwable;
28
+ }
29
+
30
+ // Single worker thread -> operations stay serialized in submission order,
31
+ // matching the previous UI-thread behavior. The thread is named so it is
32
+ // identifiable in thread dumps / ANR traces when diagnosing persistence.
33
+ private static final Executor EXECUTOR = Executors.newSingleThreadExecutor(
34
+ new ThreadFactory() {
35
+ @Override
36
+ public Thread newThread(Runnable r) {
37
+ return new Thread(r, "pushy-state-serial");
38
+ }
39
+ });
40
+
41
+ private StateSerialRunner() {
42
+ }
43
+
44
+ static void run(
45
+ @Nullable final Promise promise,
46
+ final String operationName,
47
+ final Operation operation
48
+ ) {
49
+ EXECUTOR.execute(new Runnable() {
50
+ @Override
51
+ public void run() {
52
+ try {
53
+ operation.run();
54
+ } catch (Throwable error) {
55
+ if (promise != null) {
56
+ promise.reject(operationName + " failed", error);
57
+ } else {
58
+ Log.e(UpdateContext.TAG, operationName + " failed", error);
59
+ }
60
+ }
61
+ }
62
+ });
63
+ }
64
+ }
@@ -37,7 +37,7 @@ public class UpdateContext {
37
37
  private static final String KEY_FIRST_LOAD_MARKED = "firstLoadMarked";
38
38
 
39
39
  // Singleton instance
40
- private static UpdateContext sInstance;
40
+ private static volatile UpdateContext sInstance;
41
41
  private static final Object sLock = new Object();
42
42
  private static ReactInstanceManager pendingReactInstanceManager;
43
43
 
@@ -55,7 +55,7 @@ public class UpdateContext {
55
55
  boolean flagB
56
56
  );
57
57
 
58
- public UpdateContext(Context context) {
58
+ private UpdateContext(Context context) {
59
59
  this.context = context.getApplicationContext();
60
60
  this.executor = Executors.newSingleThreadExecutor();
61
61
 
@@ -149,7 +149,7 @@ public class UpdateModuleImpl {
149
149
  final Promise promise
150
150
  ) {
151
151
  final String hash = options.getString("hash");
152
- UiThreadRunner.run(promise, "switchVersionLater", new UiThreadRunner.Operation() {
152
+ StateSerialRunner.run(promise, "switchVersionLater", new StateSerialRunner.Operation() {
153
153
  @Override
154
154
  public void run() {
155
155
  setNeedUpdateInternal(updateContext, hash);
@@ -160,7 +160,7 @@ public class UpdateModuleImpl {
160
160
 
161
161
  public static void setNeedUpdate(final UpdateContext updateContext, final ReadableMap options) {
162
162
  final String hash = options.getString("hash");
163
- UiThreadRunner.run(null, "switchVersionLater", new UiThreadRunner.Operation() {
163
+ StateSerialRunner.run(null, "switchVersionLater", new StateSerialRunner.Operation() {
164
164
  @Override
165
165
  public void run() {
166
166
  setNeedUpdateInternal(updateContext, hash);
@@ -173,7 +173,7 @@ public class UpdateModuleImpl {
173
173
  }
174
174
 
175
175
  public static void markSuccess(final UpdateContext updateContext, final Promise promise) {
176
- UiThreadRunner.run(promise, "markSuccess", new UiThreadRunner.Operation() {
176
+ StateSerialRunner.run(promise, "markSuccess", new StateSerialRunner.Operation() {
177
177
  @Override
178
178
  public void run() {
179
179
  markSuccessInternal(updateContext);
@@ -183,7 +183,7 @@ public class UpdateModuleImpl {
183
183
  }
184
184
 
185
185
  public static void markSuccess(final UpdateContext updateContext) {
186
- UiThreadRunner.run(null, "markSuccess", new UiThreadRunner.Operation() {
186
+ StateSerialRunner.run(null, "markSuccess", new StateSerialRunner.Operation() {
187
187
  @Override
188
188
  public void run() {
189
189
  markSuccessInternal(updateContext);
@@ -200,7 +200,7 @@ public class UpdateModuleImpl {
200
200
  final String uuid,
201
201
  final Promise promise
202
202
  ) {
203
- UiThreadRunner.run(promise, "setUuid", new UiThreadRunner.Operation() {
203
+ StateSerialRunner.run(promise, "setUuid", new StateSerialRunner.Operation() {
204
204
  @Override
205
205
  public void run() {
206
206
  setUuidInternal(updateContext, uuid);
@@ -210,7 +210,7 @@ public class UpdateModuleImpl {
210
210
  }
211
211
 
212
212
  public static void setUuid(final UpdateContext updateContext, final String uuid) {
213
- UiThreadRunner.run(null, "setUuid", new UiThreadRunner.Operation() {
213
+ StateSerialRunner.run(null, "setUuid", new StateSerialRunner.Operation() {
214
214
  @Override
215
215
  public void run() {
216
216
  setUuidInternal(updateContext, uuid);
@@ -235,7 +235,7 @@ public class UpdateModuleImpl {
235
235
  final String info,
236
236
  final Promise promise
237
237
  ) {
238
- UiThreadRunner.run(promise, "setLocalHashInfo", new UiThreadRunner.Operation() {
238
+ StateSerialRunner.run(promise, "setLocalHashInfo", new StateSerialRunner.Operation() {
239
239
  @Override
240
240
  public void run() {
241
241
  setLocalHashInfoInternal(updateContext, hash, info);
@@ -249,7 +249,7 @@ public class UpdateModuleImpl {
249
249
  final String hash,
250
250
  final String info
251
251
  ) {
252
- UiThreadRunner.run(null, "setLocalHashInfo", new UiThreadRunner.Operation() {
252
+ StateSerialRunner.run(null, "setLocalHashInfo", new StateSerialRunner.Operation() {
253
253
  @Override
254
254
  public void run() {
255
255
  setLocalHashInfoInternal(updateContext, hash, info);
@@ -25,6 +25,22 @@ EntryAction ClassifyEntry(
25
25
  return entry_name == kManifestEntryName ? EntryAction::kSkip : EntryAction::kExtract;
26
26
  }
27
27
 
28
+ bool TryParseArchivePatchType(int value, ArchivePatchType* out) {
29
+ switch (value) {
30
+ case 1:
31
+ *out = ArchivePatchType::kFull;
32
+ return true;
33
+ case 2:
34
+ *out = ArchivePatchType::kPatchFromPackage;
35
+ return true;
36
+ case 3:
37
+ *out = ArchivePatchType::kPatchFromPpk;
38
+ return true;
39
+ default:
40
+ return false;
41
+ }
42
+ }
43
+
28
44
  patch::Status BuildArchivePatchPlan(
29
45
  ArchivePatchType type,
30
46
  const patch::PatchManifest& manifest,
@@ -35,6 +35,11 @@ EntryAction ClassifyEntry(
35
35
  ArchivePatchType type,
36
36
  const std::string& entry_name);
37
37
 
38
+ // Convert a platform-supplied integer to an ArchivePatchType. Returns false for
39
+ // unknown values so callers can fail loudly instead of silently treating an
40
+ // incremental patch as a full package (which would skip validation).
41
+ bool TryParseArchivePatchType(int value, ArchivePatchType* out);
42
+
38
43
  patch::Status BuildArchivePatchPlan(
39
44
  ArchivePatchType type,
40
45
  const patch::PatchManifest& manifest,
@@ -0,0 +1,56 @@
1
+ #pragma once
2
+
3
+ #include <jni.h>
4
+
5
+ #include <string>
6
+ #include <vector>
7
+
8
+ // Small JNI helpers shared by the Android glue translation units
9
+ // (patch_core_android.cpp and update_core_android.cpp). Header-only with inline
10
+ // linkage so both can include it without a separate compilation unit.
11
+
12
+ namespace pushy {
13
+ namespace jni_util {
14
+
15
+ inline std::string JStringToString(JNIEnv* env, jstring value) {
16
+ if (value == nullptr) {
17
+ return std::string();
18
+ }
19
+
20
+ const char* chars = env->GetStringUTFChars(value, nullptr);
21
+ if (chars == nullptr) {
22
+ return std::string();
23
+ }
24
+
25
+ std::string result(chars);
26
+ env->ReleaseStringUTFChars(value, chars);
27
+ return result;
28
+ }
29
+
30
+ inline std::vector<std::string> JArrayToVector(
31
+ JNIEnv* env,
32
+ jobjectArray values) {
33
+ std::vector<std::string> result;
34
+ if (values == nullptr) {
35
+ return result;
36
+ }
37
+
38
+ const jsize size = env->GetArrayLength(values);
39
+ result.reserve(static_cast<size_t>(size));
40
+ for (jsize index = 0; index < size; ++index) {
41
+ auto* item = static_cast<jstring>(env->GetObjectArrayElement(values, index));
42
+ result.push_back(JStringToString(env, item));
43
+ env->DeleteLocalRef(item);
44
+ }
45
+ return result;
46
+ }
47
+
48
+ inline void ThrowRuntimeException(JNIEnv* env, const std::string& message) {
49
+ jclass exception = env->FindClass("java/lang/RuntimeException");
50
+ if (exception != nullptr) {
51
+ env->ThrowNew(exception, message.c_str());
52
+ }
53
+ }
54
+
55
+ } // namespace jni_util
56
+ } // namespace pushy
@@ -3,47 +3,14 @@
3
3
  #include <string>
4
4
  #include <vector>
5
5
 
6
+ #include "jni_util.h"
6
7
  #include "patch_core.h"
7
8
 
8
9
  namespace {
9
10
 
10
- std::string JStringToString(JNIEnv* env, jstring value) {
11
- if (value == nullptr) {
12
- return std::string();
13
- }
14
-
15
- const char* chars = env->GetStringUTFChars(value, nullptr);
16
- if (chars == nullptr) {
17
- return std::string();
18
- }
19
-
20
- std::string result(chars);
21
- env->ReleaseStringUTFChars(value, chars);
22
- return result;
23
- }
24
-
25
- std::vector<std::string> JArrayToVector(JNIEnv* env, jobjectArray values) {
26
- std::vector<std::string> result;
27
- if (values == nullptr) {
28
- return result;
29
- }
30
-
31
- const jsize size = env->GetArrayLength(values);
32
- result.reserve(static_cast<size_t>(size));
33
- for (jsize i = 0; i < size; ++i) {
34
- auto* item = static_cast<jstring>(env->GetObjectArrayElement(values, i));
35
- result.push_back(JStringToString(env, item));
36
- env->DeleteLocalRef(item);
37
- }
38
- return result;
39
- }
40
-
41
- void ThrowRuntimeException(JNIEnv* env, const std::string& message) {
42
- jclass exception = env->FindClass("java/lang/RuntimeException");
43
- if (exception != nullptr) {
44
- env->ThrowNew(exception, message.c_str());
45
- }
46
- }
11
+ using pushy::jni_util::JArrayToVector;
12
+ using pushy::jni_util::JStringToString;
13
+ using pushy::jni_util::ThrowRuntimeException;
47
14
 
48
15
  } // namespace
49
16
 
@@ -0,0 +1,24 @@
1
+ #pragma once
2
+
3
+ // Single source of truth for the state-machine operation codes shared across
4
+ // the platform glue layers (Android JNI in update_core_android.cpp and
5
+ // HarmonyOS NAPI in pushy.cpp). The integer values MUST stay in sync with the
6
+ // callers on each platform:
7
+ // - Android: UpdateContext.java (STATE_OP_* constants)
8
+ // - HarmonyOS: UpdateContext.ts (StateOperation usage)
9
+ // Do not renumber existing entries; only append new ones.
10
+
11
+ namespace pushy {
12
+ namespace state_ops {
13
+
14
+ enum class StateOperation {
15
+ kSwitchVersion = 1,
16
+ kMarkSuccess = 2,
17
+ kRollback = 3,
18
+ kClearFirstTime = 4,
19
+ kClearRollbackMark = 5,
20
+ kResolveLaunch = 6,
21
+ };
22
+
23
+ } // namespace state_ops
24
+ } // namespace pushy
@@ -426,6 +426,28 @@ void TestArchivePatchCoreRejectsMissingEntries() {
426
426
  "manifest entry should be skipped");
427
427
  }
428
428
 
429
+ void TestTryParseArchivePatchType() {
430
+ pushy::archive_patch::ArchivePatchType type;
431
+ Expect(
432
+ pushy::archive_patch::TryParseArchivePatchType(1, &type) &&
433
+ type == pushy::archive_patch::ArchivePatchType::kFull,
434
+ "1 should parse to kFull");
435
+ Expect(
436
+ pushy::archive_patch::TryParseArchivePatchType(2, &type) &&
437
+ type == pushy::archive_patch::ArchivePatchType::kPatchFromPackage,
438
+ "2 should parse to kPatchFromPackage");
439
+ Expect(
440
+ pushy::archive_patch::TryParseArchivePatchType(3, &type) &&
441
+ type == pushy::archive_patch::ArchivePatchType::kPatchFromPpk,
442
+ "3 should parse to kPatchFromPpk");
443
+ Expect(
444
+ !pushy::archive_patch::TryParseArchivePatchType(0, &type),
445
+ "0 should be rejected");
446
+ Expect(
447
+ !pushy::archive_patch::TryParseArchivePatchType(4, &type),
448
+ "unknown type should be rejected, not silently coerced to kFull");
449
+ }
450
+
429
451
  void TestArchivePatchCoreSupportsCustomBundlePatchEntry() {
430
452
  PatchManifest manifest;
431
453
  manifest.copies.push_back(CopyOperation{"assets/a.png", "assets/a.png"});
@@ -524,6 +546,7 @@ int main() {
524
546
  {"StateCoreCanClearMarkers", TestStateCoreCanClearMarkers},
525
547
  {"ArchivePatchCoreBuildPlanAndCopyGroups", TestArchivePatchCoreBuildPlanAndCopyGroups},
526
548
  {"ArchivePatchCoreRejectsMissingEntries", TestArchivePatchCoreRejectsMissingEntries},
549
+ {"TryParseArchivePatchType", TestTryParseArchivePatchType},
527
550
  {"ArchivePatchCoreSupportsCustomBundlePatchEntry", TestArchivePatchCoreSupportsCustomBundlePatchEntry},
528
551
  {"ArchivePatchCoreHarmonyBundlePatchFromPackage", TestArchivePatchCoreHarmonyBundlePatchFromPackage},
529
552
  {"StateCoreRollbackToEmptyVersion", TestStateCoreRollbackToEmptyVersion},
@@ -531,15 +554,22 @@ int main() {
531
554
  {"StateCoreSwitchToSameVersion", TestStateCoreSwitchToSameVersion},
532
555
  };
533
556
 
557
+ int failures = 0;
534
558
  for (const auto& test : tests) {
535
559
  try {
536
560
  test.second();
537
561
  std::fprintf(stdout, "[PASS] %s\n", test.first.c_str());
538
562
  } catch (const std::exception& error) {
539
563
  std::fprintf(stderr, "[FAIL] %s: %s\n", test.first.c_str(), error.what());
540
- return 1;
564
+ ++failures;
541
565
  }
542
566
  }
543
567
 
544
- return 0;
568
+ std::fprintf(
569
+ stdout,
570
+ "\n%zu tests, %d passed, %d failed\n",
571
+ tests.size(),
572
+ static_cast<int>(tests.size()) - failures,
573
+ failures);
574
+ return failures == 0 ? 0 : 1;
545
575
  }