Repository: wix/Detox
Pull request #2434 (2020-10-27 15:12:00)

Expand retry() API and improve exec() retry-logging
- [x] This is a small change 
- [ ] This change has been discussed in issue #<?> and the solution has been agreed upon with maintainers.

---

**Description:**
Associated with the ongoing work on Genymotion-Cloud integration (#2429), where logging of command failures, and better-retry control, are required.

In particular, in the case where a command is retried due to repeated failures, instead of having an obscure log - simply showing the command and an overdue failure, as is today (in trace level):
```
detox[18311] DEBUG: [exec.js/EXEC_CMD, #2] "gmsaas" --format compactjson instances start --stop-when-inactive 5974e92c-5ec6-4576-a93e-2ceec4338f0b "Detox-1603890039526.4004"
(log stuck for ~1.5m... :-/)
detox[18311] ERROR: [exec.js/EXEC_FAIL, #2] ""gmsaas" --format compactjson instances start --stop-when-inactive 5974e92c-5ec6-4576-a93e-2ceec4338f0b "Detox-1603890039526.4004"" failed with code = 4, stdout and stderr:
detox[18311] ERROR: [exec.js/EXEC_FAIL, #2]
detox[18311] ERROR: [exec.js/EXEC_FAIL, #2] {"error": {"message": "API return unexpected code: 403. Error: {\"code\":\"TOO_MANY_RUNNING_VDS\",\"message\":\"Too many running virtual devices\"}", "details": ""}, "exit_code": 4, "exit_code_desc": "API_ERROR"}
```

With a proper argument to `exec` (i.e. `statusLog.retrying: true`), all retries and errors in between would be debug/trace-logged: (*)

```
detox[18311] DEBUG: [exec.js/EXEC_CMD, #2] "gmsaas" --format compactjson instances start --stop-when-inactive 5974e92c-5ec6-4576-a93e-2ceec4338f0b "Detox-1603890039526.4004"
detox[18311] TRACE: [exec.js/EXEC_TRY_FAIL, #2] {"error": {"message": "API return unexpected code: 403. Error: {\"code\":\"TOO_MANY_RUNNING_VDS\",\"message\":\"Too many running virtual devices\"}", "details": ""}, "exit_code": 4, "exit_code_desc": "API_ERROR"}

detox[18311] DEBUG: [exec.js/EXEC_RETRY, #2] (Retry #1) "gmsaas" --format compactjson instances start --stop-when-inactive 5974e92c-5ec6-4576-a93e-2ceec4338f0b "Detox-1603890039526.4004"
detox[18311] TRACE: [exec.js/EXEC_TRY_FAIL, #2] {"error": {"message": "API return unexpected code: 403. Error: {\"code\":\"TOO_MANY_RUNNING_VDS\",\"message\":\"Too many running virtual devices\"}", "details": ""}, "exit_code": 4, "exit_code_desc": "API_ERROR"}

detox[18311] DEBUG: [exec.js/EXEC_RETRY, #2] (Retry #2) "gmsaas" --format compactjson instances start --stop-when-inactive 5974e92c-5ec6-4576-a93e-2ceec4338f0b "Detox-1603890039526.4004"
detox[18311] TRACE: [exec.js/EXEC_TRY_FAIL, #2] {"error": {"message": "API return unexpected code: 403. Error: {\"code\":\"TOO_MANY_RUNNING_VDS\",\"message\":\"Too many running virtual devices\"}", "details": ""}, "exit_code": 4, "exit_code_desc": "API_ERROR"}

detox[18311] DEBUG: [exec.js/EXEC_RETRY, #2] (Retry #3) "gmsaas" --format compactjson instances start --stop-when-inactive 5974e92c-5ec6-4576-a93e-2ceec4338f0b "Detox-1603890039526.4004"
detox[18311] TRACE: [exec.js/EXEC_TRY_FAIL, #2] {"error": {"message": "API return unexpected code: 403. Error: {\"code\":\"TOO_MANY_RUNNING_VDS\",\"message\":\"Too many running virtual devices\"}", "details": ""}, "exit_code": 4, "exit_code_desc": "API_ERROR"}

detox[18311] DEBUG: [exec.js/EXEC_RETRY, #2] (Retry #4) "gmsaas" --format compactjson instances start --stop-when-inactive 5974e92c-5ec6-4576-a93e-2ceec4338f0b "Detox-1603890039526.4004"
detox[18311] TRACE: [exec.js/EXEC_TRY_FAIL, #2] {"error": {"message": "API return unexpected code: 403. Error: {\"code\":\"TOO_MANY_RUNNING_VDS\",\"message\":\"Too many running virtual devices\"}", "details": ""}, "exit_code": 4, "exit_code_desc": "API_ERROR"}

detox[18311] DEBUG: [exec.js/EXEC_RETRY, #2] (Retry #5) "gmsaas" --format compactjson instances start --stop-when-inactive 5974e92c-5ec6-4576-a93e-2ceec4338f0b "Detox-1603890039526.4004"
detox[18311] TRACE: [exec.js/EXEC_TRY_FAIL, #2] {"error": {"message": "API return unexpected code: 403. Error: {\"code\":\"TOO_MANY_RUNNING_VDS\",\"message\":\"Too many running virtual devices\"}", "details": ""}, "exit_code": 4, "exit_code_desc": "API_ERROR"}

detox[18311] DEBUG: [exec.js/EXEC_RETRY, #2] (Retry #6) "gmsaas" --format compactjson instances start --stop-when-inactive 5974e92c-5ec6-4576-a93e-2ceec4338f0b "Detox-1603890039526.4004"
detox[18311] TRACE: [exec.js/EXEC_TRY_FAIL, #2] {"error": {"message": "API return unexpected code: 403. Error: {\"code\":\"TOO_MANY_RUNNING_VDS\",\"message\":\"Too many running virtual devices\"}", "details": ""}, "exit_code": 4, "exit_code_desc": "API_ERROR"}

detox[18311] DEBUG: [exec.js/EXEC_RETRY, #2] (Retry #7) "gmsaas" --format compactjson instances start --stop-when-inactive 5974e92c-5ec6-4576-a93e-2ceec4338f0b "Detox-1603890039526.4004"
detox[18311] TRACE: [exec.js/EXEC_TRY_FAIL, #2] {"error": {"message": "API return unexpected code: 403. Error: {\"code\":\"TOO_MANY_RUNNING_VDS\",\"message\":\"Too many running virtual devices\"}", "details": ""}, "exit_code": 4, "exit_code_desc": "API_ERROR"}

detox[18311] DEBUG: [exec.js/EXEC_RETRY, #2] (Retry #8) "gmsaas" --format compactjson instances start --stop-when-inactive 5974e92c-5ec6-4576-a93e-2ceec4338f0b "Detox-1603890039526.4004"
detox[18311] TRACE: [exec.js/EXEC_TRY_FAIL, #2] {"error": {"message": "API return unexpected code: 403. Error: {\"code\":\"TOO_MANY_RUNNING_VDS\",\"message\":\"Too many running virtual devices\"}", "details": ""}, "exit_code": 4, "exit_code_desc": "API_ERROR"}

detox[18311] DEBUG: [exec.js/EXEC_RETRY, #2] (Retry #9) "gmsaas" --format compactjson instances start --stop-when-inactive 5974e92c-5ec6-4576-a93e-2ceec4338f0b "Detox-1603890039526.4004"
detox[18311] ERROR: [exec.js/EXEC_FAIL, #2] ""gmsaas" --format compactjson instances start --stop-when-inactive 5974e92c-5ec6-4576-a93e-2ceec4338f0b "Detox-1603890039526.4004"" failed with code = 4, stdout and stderr:

detox[18311] ERROR: [exec.js/EXEC_FAIL, #2]
detox[18311] ERROR: [exec.js/EXEC_FAIL, #2] {"error": {"message": "API return unexpected code: 403. Error: {\"code\":\"TOO_MANY_RUNNING_VDS\",\"message\":\"Too many running virtual devices\"}", "details": ""}, "exit_code": 4, "exit_code_desc": "API_ERROR"}
```

_* Note: there is already a supported argument that does something similar - `statusLogs.trying`. I found it unsuitable, however, because it logs some semantic, user-induced content that is hard to associate with the original command. Also, it is always logged, even if the original command succeeds, whcih is redundant. I have left it intact, nevertheless, for backwards compatibility._

d4vidi: will reopen soon

Patch:
diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/UiAutomatorHelper.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/UiAutomatorHelper.java
index d12f163d9..c424a1b38 100644
--- a/detox/android/detox/src/main/java/com/wix/detox/espresso/UiAutomatorHelper.java
+++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/UiAutomatorHelper.java
@@ -28,6 +28,7 @@ public class UiAutomatorHelper {
     private static final String LOG_TAG = "detox";
 
     private static final String METHOD_LOOP_UNTIL_IDLE = "loopMainThreadUntilIdle";
+    private static final String METHOD_LOOP_AT_LEAST = "loopMainThreadForAtLeast";
 
     /**
      * This triggers a full Espresso sync. It's intended use is to sync UIAutomator calls.
@@ -61,7 +62,7 @@ public class UiAutomatorHelper {
             @Override
             public void run() {
                 try {
-                    Reflect.on(UiControllerUtils.getUiController()).call(METHOD_LOOP_UNTIL_IDLE, millis);
+                    Reflect.on(UiControllerUtils.getUiController()).call(METHOD_LOOP_AT_LEAST, millis);
                 } catch (ReflectException e) {
                     Log.e(LOG_TAG, "Failed to sync Espresso manually.", e.getCause());
                 }
diff --git a/detox/package.json b/detox/package.json
index 6581d55c1..4f80b7bcc 100644
--- a/detox/package.json
+++ b/detox/package.json
@@ -1,7 +1,7 @@
 {
   "name": "detox",
   "description": "E2E tests and automation for mobile",
-  "version": "17.10.3",
+  "version": "17.10.4",
   "bin": {
     "detox": "local-cli/cli.js"
   },
diff --git a/detox/src/utils/exec.js b/detox/src/utils/exec.js
index d57270312..9fb5d9ec7 100644
--- a/detox/src/utils/exec.js
+++ b/detox/src/utils/exec.js
@@ -7,21 +7,23 @@ const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
 
 let _operationCounter = 0;
 
-async function execWithRetriesAndLogs(bin, options, statusLogs, retries = 9, interval = 1000) {
+async function execWithRetriesAndLogs(bin, options, statusLogs = {}, retries = 9, interval = 1000) {
   const trackingId = _operationCounter++;
   const cmd = _composeCommand(bin, options);
   const execTimeout = _.get(options, 'timeout', 0);
   const log = execLogger.child({ fn: 'execWithRetriesAndLogs', cmd, trackingId });
   const verbosity = _.get(options, 'verbosity', 'normal');
-  log.debug({ event: 'EXEC_CMD' }, `${cmd}`);
 
   let result;
   try {
-    await retry({retries, interval}, async (retryNumber) => {
-      if (statusLogs && statusLogs.trying) {
-        log.debug({ event: 'EXEC_TRY', retryNumber }, statusLogs.trying);
-      }
+    log.debug({ event: 'EXEC_CMD' }, `${cmd}`);
 
+    await retry({retries, interval}, async (retryNumber, lastError) => {
+      if (statusLogs.trying) {
+        _logTrying(log, statusLogs.trying, retryNumber, lastError);
+      } else if (statusLogs.retrying) {
+        _logRetrying(log, cmd, retryNumber, lastError);
+      }
       result = await exec(cmd, { timeout: execTimeout });
     });
   } catch (err) {
@@ -45,7 +47,7 @@ async function execWithRetriesAndLogs(bin, options, statusLogs, retries = 9, int
 
   _logExecOutput(log, result, verbosity === 'high' ? 'debug' : 'trace');
 
-  if (statusLogs && statusLogs.successful) {
+  if (statusLogs.successful) {
     log.debug({ event: 'EXEC_SUCCESS' }, statusLogs.successful);
   }
 
@@ -88,6 +90,20 @@ function _logExecOutput(log, process, level) {
   }
 }
 
+function _logTrying(log, message, retryNumber, lastError) {
+  if (lastError && lastError.stderr) {
+    log.trace({ event: 'EXEC_TRY_FAIL' }, lastError.stderr);
+  }
+  log.debug({ event: 'EXEC_TRY', retryNumber }, message);
+}
+
+function _logRetrying(log, message, retryNumber, lastError) {
+  if (retryNumber > 1) {
+    log.trace({ event: 'EXEC_TRY_FAIL' }, lastError.stderr);
+    log.debug({ event: 'EXEC_RETRY', retryNumber }, `(Retry #${retryNumber - 1})`, message);
+  }
+}
+
 function _composeCommand(bin, options) {
   if (!options) {
     return bin;
diff --git a/detox/src/utils/retry.js b/detox/src/utils/retry.js
index 308ed119b..b34bde997 100644
--- a/detox/src/utils/retry.js
+++ b/detox/src/utils/retry.js
@@ -1,3 +1,4 @@
+const _ = require('lodash');
 const sleep = require('./sleep');
 
 const DEFAULT_RETRIES = 9;
@@ -5,13 +6,14 @@ const DEFAULT_INTERVAL = 500;
 const DEFAULT_CONDITION_FN = () => true;
 const DEFAULT_BACKOFF_MODE = 'linear';
 const backoffModes = {
-  'linear': () => ({ interval, totalRetries }) => totalRetries * interval,
+  'linear': () => ({ interval, totalTries }) => totalTries * interval,
   'none': () => ({ interval }) => interval,
 };
 
-async function retry(options, func) {
-  if (typeof options === 'function') {
-    func = options;
+async function retry(optionsOrFunc, func) {
+  let options = optionsOrFunc;
+  if (typeof optionsOrFunc === 'function') {
+    func = optionsOrFunc;
     options = {};
   }
 
@@ -24,14 +26,16 @@ async function retry(options, func) {
 
   const backoffFn = backoffModes[backoff]();
 
-  for (let totalRetries = 1; true; totalRetries++) {
+  for (let totalTries = 1, lastError = null; true; totalTries++) {
     try {
-      return await func(totalRetries);
+      return await func(totalTries, lastError);
     } catch (e) {
-      if (!conditionFn(e) || (totalRetries > retries)) {
+      lastError = e;
+
+      if (!conditionFn(e) || (totalTries > retries)) {
         throw e;
       }
-      await sleep(backoffFn({ interval, totalRetries }));
+      await sleep(backoffFn({ interval, totalTries }));
     }
   }
 }
diff --git a/examples/demo-native-android/package.json b/examples/demo-native-android/package.json
index 5efa5da58..51d35e53f 100644
--- a/examples/demo-native-android/package.json
+++ b/examples/demo-native-android/package.json
@@ -1,6 +1,6 @@
 {
   "name": "detox-demo-native-android",
-  "version": "17.10.3",
+  "version": "17.10.4",
   "private": true,
   "scripts": {
     "packager": "react-native start",
@@ -8,7 +8,7 @@
     "e2e": "mocha e2e --opts ./e2e/mocha.opts"
   },
   "devDependencies": {
-    "detox": "^17.10.3",
+    "detox": "^17.10.4",
     "mocha": "^6.1.3"
   },
   "detox": {}
diff --git a/examples/demo-native-ios/package.json b/examples/demo-native-ios/package.json
index a3e2cd328..26c5da396 100644
--- a/examples/demo-native-ios/package.json
+++ b/examples/demo-native-ios/package.json
@@ -1,9 +1,9 @@
 {
   "name": "detox-demo-native-ios",
-  "version": "17.10.3",
+  "version": "17.10.4",
   "private": true,
   "devDependencies": {
-    "detox": "^17.10.3",
+    "detox": "^17.10.4",
     "mocha": "^6.1.3"
   },
   "detox": {
diff --git a/examples/demo-plugin/package.json b/examples/demo-plugin/package.json
index 57bbcc454..ec5529c0c 100644
--- a/examples/demo-plugin/package.json
+++ b/examples/demo-plugin/package.json
@@ -1,12 +1,12 @@
 {
   "name": "demo-plugin",
-  "version": "17.10.3",
+  "version": "17.10.4",
   "private": true,
   "scripts": {
     "test:plugin": "detox test --configuration plugin -l verbose"
   },
   "devDependencies": {
-    "detox": "^17.10.3",
+    "detox": "^17.10.4",
     "jest": "^26.5.0"
   },
   "detox": {
diff --git a/examples/demo-react-native-detox-instruments/package.json b/examples/demo-react-native-detox-instruments/package.json
index 6f4bd69a7..061928788 100644
--- a/examples/demo-react-native-detox-instruments/package.json
+++ b/examples/demo-react-native-detox-instruments/package.json
@@ -1,10 +1,10 @@
 {
   "name": "demo-react-native-detox-instruments",
-  "version": "17.10.3",
+  "version": "17.10.4",
   "private": true,
   "scripts": {},
   "devDependencies": {
-    "detox": "^17.10.3",
+    "detox": "^17.10.4",
     "mocha": "6.x.x"
   },
   "detox": {
diff --git a/examples/demo-react-native-jest/package.json b/examples/demo-react-native-jest/package.json
index 4fecdeab2..5a48f17d3 100644
--- a/examples/demo-react-native-jest/package.json
+++ b/examples/demo-react-native-jest/package.json
@@ -1,6 +1,6 @@
 {
   "name": "demo-react-native-jest",
-  "version": "17.10.3",
+  "version": "17.10.4",
   "private": true,
   "scripts": {
     "test:ios-release": "detox test --configuration ios.sim.release -l verbose",
@@ -9,7 +9,7 @@
     "test:android-release-ci": "detox test --configuration android.emu.release -l verbose --workers 2 --headless --record-logs all --take-screenshots all"
   },
   "devDependencies": {
-    "detox": "^17.10.3",
+    "detox": "^17.10.4",
     "jest": "^26.5.0",
     "jest-circus": "^26.5.2",
     "sanitize-filename": "^1.6.1"
diff --git a/examples/demo-react-native/package.json b/examples/demo-react-native/package.json
index 15a7b70e5..6a23f78e8 100644
--- a/examples/demo-react-native/package.json
+++ b/examples/demo-react-native/package.json
@@ -1,6 +1,6 @@
 {
   "name": "example",
-  "version": "17.10.3",
+  "version": "17.10.4",
   "private": true,
   "scripts": {
     "start": "react-native start",
@@ -22,7 +22,7 @@
     "react-native": "0.62.x"
   },
   "devDependencies": {
-    "detox": "^17.10.3",
+    "detox": "^17.10.4",
     "mocha": "^6.1.3"
   }
 }
diff --git a/lerna.json b/lerna.json
index b8dbd06ee..18e4a0f55 100644
--- a/lerna.json
+++ b/lerna.json
@@ -13,7 +13,7 @@
     "generation",
     "."
   ],
-  "version": "17.10.3",
+  "version": "17.10.4",
   "npmClient": "npm",
   "command": {
     "publish": {
diff --git a/package.json b/package.json
index 14d335c3e..b6c8094fd 100644
--- a/package.json
+++ b/package.json
@@ -18,5 +18,5 @@
     "semver": "5.x.x",
     "shell-utils": "1.x.x"
   },
-  "version": "17.10.3"
+  "version": "17.10.4"
 }


Test patch:
diff --git a/detox/src/utils/exec.test.js b/detox/src/utils/exec.test.js
index a3f4cd3eb..263ddd0f3 100644
--- a/detox/src/utils/exec.test.js
+++ b/detox/src/utils/exec.test.js
@@ -1,5 +1,3 @@
-const _ = require('lodash');
-
 describe('exec', () => {
   let logger;
   let exec;
@@ -7,9 +5,11 @@ describe('exec', () => {
 
   beforeEach(() => {
     jest.mock('./logger');
-    jest.mock('child-process-promise');
     logger = require('./logger');
+
+    jest.mock('child-process-promise');
     cpp = require('child-process-promise');
+
     exec = require('./exec');
   });
 
@@ -20,9 +20,7 @@ describe('exec', () => {
   });
 
   it(`exec command with no arguments successfully`, async () => {
-    const successfulResult = returnSuccessfulNoValue();
-    const resolvedPromise = Promise.resolve(successfulResult);
-    cpp.exec.mockReturnValueOnce(resolvedPromise);
+    mockCppSuccessful(cpp);
     await exec.execWithRetriesAndLogs('bin');
     expect(cpp.exec).toHaveBeenCalledWith(`bin`, { timeout: 0 });
   });
@@ -48,17 +46,39 @@ describe('exec', () => {
     expect(cpp.exec).toHaveBeenCalledWith(`export MY_PREFIX && bin --argument 123`, { timeout: 0 });
   });
 
-  it(`exec command with arguments and status logs successfully`, async () => {
-    mockCppSuccessful(cpp);
+  it(`exec command with arguments and try-based status logs successfully, with status logging`, async () => {
+    cpp.exec
+      .mockRejectedValueOnce(returnErrorWithValue('error result'))
+      .mockResolvedValueOnce(returnSuccessfulWithValue('successful result'));
+
+    const options = {args: `--argument 123`};
+    const statusLogs = {
+      trying: 'trying status log',
+      successful: 'successful status log',
+    };
+    await exec.execWithRetriesAndLogs('bin', options, statusLogs);
+
+    expect(cpp.exec).toHaveBeenCalledWith(`bin --argument 123`, { timeout: 0 });
+    expect(logger.debug).toHaveBeenCalledWith({ event: 'EXEC_TRY', retryNumber: 1}, statusLogs.trying);
+    expect(logger.debug).toHaveBeenCalledWith({ event: 'EXEC_TRY', retryNumber: 2}, statusLogs.trying);
+    expect(logger.trace).toHaveBeenCalledWith({ event: 'EXEC_TRY_FAIL' }, 'error result');
+  });
+
+  it(`exec command with arguments and retry-based status logs successfully, with status logging`, async () => {
+    cpp.exec
+      .mockRejectedValueOnce(returnErrorWithValue('error result'))
+      .mockResolvedValueOnce(returnSuccessfulWithValue('successful result'));
 
     const options = {args: `--argument 123`};
     const statusLogs = {
-      trying: `trying status log`,
-      successful: `successful status log`
+      retrying: true,
     };
     await exec.execWithRetriesAndLogs('bin', options, statusLogs);
 
     expect(cpp.exec).toHaveBeenCalledWith(`bin --argument 123`, { timeout: 0 });
+    expect(logger.debug).toHaveBeenCalledWith({ event: 'EXEC_RETRY', retryNumber: 2}, '(Retry #1)', 'bin --argument 123');
+    expect(logger.debug).not.toHaveBeenCalledWith({ event: 'EXEC_RETRY', retryNumber: 1}, expect.any(String), expect.any(String));
+    expect(logger.trace).toHaveBeenCalledWith({ event: 'EXEC_TRY_FAIL' }, 'error result');
   });
 
   it(`exec command should output success and err logs`, async () => {
@@ -75,7 +95,7 @@ describe('exec', () => {
         exitCode: 0
       }
     };
-    cpp.exec.mockReturnValueOnce(Promise.resolve(cppResult));
+    cpp.exec.mockResolvedValueOnce(cppResult);
 
     await exec.execWithRetriesAndLogs('bin');
 
@@ -103,9 +123,7 @@ describe('exec', () => {
   });
 
   it(`exec command and fail with error code`, async () => {
-    const errorResult = returnErrorWithValue('error result');
-    const rejectedPromise = Promise.reject(errorResult);
-    cpp.exec.mockReturnValueOnce(rejectedPromise);
+    mockCppFailure(cpp);
 
     try {
       await exec.execWithRetriesAndLogs('bin', null, '', 0, 1);
@@ -118,9 +136,7 @@ describe('exec', () => {
   });
 
   it(`exec command and fail with error code, report only to debug log if verbosity is low`, async () => {
-    const errorResult = returnErrorWithValue('error result');
-    const rejectedPromise = Promise.reject(errorResult);
-    cpp.exec.mockReturnValueOnce(rejectedPromise);
+    mockCppFailure(cpp);
 
     try {
       await exec.execWithRetriesAndLogs('bin', { verbosity: 'low' }, '', 0, 1);
@@ -133,9 +149,7 @@ describe('exec', () => {
   });
 
   it(`exec command and fail with timeout`, async () => {
-    const errorResult = returnErrorWithValue('error result');
-    const rejectedPromise = Promise.reject(errorResult);
-    cpp.exec.mockReturnValueOnce(rejectedPromise);
+    mockCppFailure(cpp);
 
     try {
       await exec.execWithRetriesAndLogs('bin', { timeout: 1 }, '', 0, 1);
@@ -148,13 +162,14 @@ describe('exec', () => {
 
   it(`exec command with multiple failures`, async () => {
     const errorResult = returnErrorWithValue('error result');
-    const rejectedPromise = Promise.reject(errorResult);
-    cpp.exec.mockReturnValueOnce(rejectedPromise)
-       .mockReturnValueOnce(rejectedPromise)
-       .mockReturnValueOnce(rejectedPromise)
-       .mockReturnValueOnce(rejectedPromise)
-       .mockReturnValueOnce(rejectedPromise)
-       .mockReturnValueOnce(rejectedPromise);
+    cpp.exec
+      .mockRejectedValueOnce(errorResult)
+      .mockRejectedValueOnce(errorResult)
+      .mockRejectedValueOnce(errorResult)
+      .mockRejectedValueOnce(errorResult)
+      .mockRejectedValueOnce(errorResult)
+      .mockRejectedValueOnce(errorResult);
+
     try {
       await exec.execWithRetriesAndLogs('bin', null, '', 5, 1);
       fail('expected execWithRetriesAndLogs() to throw');
@@ -167,16 +182,15 @@ describe('exec', () => {
 
   it(`exec command with multiple failures and then a success`, async () => {
     const errorResult = returnErrorWithValue('error result');
-    const rejectedPromise = Promise.reject(errorResult);
     const successfulResult = returnSuccessfulWithValue('successful result');
-    const resolvedPromise = Promise.resolve(successfulResult);
 
-    cpp.exec.mockReturnValueOnce(rejectedPromise)
-       .mockReturnValueOnce(rejectedPromise)
-       .mockReturnValueOnce(rejectedPromise)
-       .mockReturnValueOnce(rejectedPromise)
-       .mockReturnValueOnce(rejectedPromise)
-       .mockReturnValueOnce(resolvedPromise);
+    cpp.exec
+      .mockRejectedValueOnce(errorResult)
+      .mockRejectedValueOnce(errorResult)
+      .mockRejectedValueOnce(errorResult)
+      .mockRejectedValueOnce(errorResult)
+      .mockRejectedValueOnce(errorResult)
+      .mockResolvedValueOnce(successfulResult);
 
     await exec.execWithRetriesAndLogs('bin', null, '', 6, 1);
     expect(cpp.exec).toHaveBeenCalledWith(`bin`, { timeout: 0 });
@@ -298,42 +312,36 @@ function toStream(string) {
   return stream;
 }
 
-function returnSuccessfulWithValue(value) {
-  const result = {
+const returnSuccessfulWithValue = (value) => ({
     stdout: JSON.stringify(value),
     stderr: "err",
     childProcess: {
       exitCode: 0
     }
-  };
-  return result;
-}
+  });
 
-function returnErrorWithValue(value) {
-  const result = {
+const returnErrorWithValue = (value) => ({
     stdout: "out",
     stderr: value,
     childProcess: {
       exitCode: 1
     }
-  };
-  return result;
-}
+  });
 
-function returnSuccessfulNoValue() {
-  const result = {
+const returnSuccessfulNoValue = () => ({
     childProcess: {
       exitCode: 0
     }
-  };
-  return result;
-}
+  });
 
 function mockCppSuccessful(cpp) {
   const successfulResult = returnSuccessfulWithValue('successful result');
-  const resolvedPromise = Promise.resolve(successfulResult);
-  cpp.exec.mockReturnValueOnce(resolvedPromise);
-
+  cpp.exec.mockResolvedValueOnce(successfulResult);
   return successfulResult;
 }
 
+function mockCppFailure(cpp) {
+  const errorResult = returnErrorWithValue('error result');
+  cpp.exec.mockRejectedValueOnce(errorResult);
+  return errorResult;
+}
diff --git a/detox/src/utils/retry.test.js b/detox/src/utils/retry.test.js
index 6e49f7b98..b7591e5bc 100644
--- a/detox/src/utils/retry.test.js
+++ b/detox/src/utils/retry.test.js
@@ -7,8 +7,8 @@ describe('retry', () => {
     .mockReturnValueOnce(Promise.reject())
     .mockReturnValueOnce(Promise.resolve());
   const mockFailingTwiceUserFn = () => jest.fn()
-    .mockReturnValueOnce(Promise.reject('once'))
-    .mockReturnValueOnce(Promise.reject('twice'))
+    .mockReturnValueOnce(Promise.reject(new Error('once')))
+    .mockReturnValueOnce(Promise.reject(new Error('twice')))
     .mockReturnValueOnce(Promise.resolve());
 
   beforeEach(() => {
@@ -36,6 +36,14 @@ describe('retry', () => {
     expect(mockFn).toHaveBeenCalledTimes(3);
   });
 
+  it('should provide error info in each failure', async () => {
+    const mockFn = mockFailingTwiceUserFn();
+    await retry({retries: 999, interval: 0}, mockFn);
+    expect(mockFn).toHaveBeenCalledWith(1, null);
+    expect(mockFn).toHaveBeenCalledWith(2, new Error('once'));
+    expect(mockFn).toHaveBeenCalledWith(3, new Error('twice'));
+  });
+
   it('should adhere to retries parameter', async () => {
     const mockFn = mockFailingUserFn();
     try {
diff --git a/detox/test/package.json b/detox/test/package.json
index cef1dd2db..ad80b26da 100644
--- a/detox/test/package.json
+++ b/detox/test/package.json
@@ -1,6 +1,6 @@
 {
   "name": "detox-test",
-  "version": "17.10.3",
+  "version": "17.10.4",
   "private": true,
   "engines": {
     "node": ">=8.3.0"
@@ -31,7 +31,7 @@
   },
   "devDependencies": {
     "@babel/core": "^7.8.7",
-    "detox": "^17.10.3",
+    "detox": "^17.10.4",
     "express": "^4.15.3",
     "jest": "^26.5.0",
     "jest-circus": "^26.5.2",