detox
Advanced tools
Comparing version 20.11.0 to 20.13.1
@@ -116,4 +116,8 @@ /// <reference types="node" /> | ||
}): Promise<void>; | ||
/** | ||
* Workaround for Jest exiting abruptly in --bail mode. | ||
* Makes sure that all workers and their test environments are properly torn down. | ||
*/ | ||
unsafe_conductEarlyTeardown(): Promise<void>; | ||
/** | ||
* Reports to Detox CLI about passed and failed test files. | ||
@@ -220,2 +224,7 @@ * The failed test files might be re-run again if | ||
/** | ||
* Signalizes that the test session is being torn down. | ||
* Experimental feature for Jest --bail mode. | ||
*/ | ||
unsafe_earlyTeardown?: boolean; | ||
/** | ||
* Results of test file executions. Primarily used for Detox CLI retry mechanism. | ||
@@ -222,0 +231,0 @@ */ |
const { log } = require('../internals'); | ||
const DeviceRegistry = require('../src/devices/DeviceRegistry'); | ||
const { getDetoxLibraryRootPath } = require('../src/utils/environment'); | ||
const DeviceRegistry = require('../src/devices/allocation/DeviceRegistry'); | ||
module.exports.command = 'reset-lock-file'; | ||
module.exports.desc = 'Resets all Detox lock files. Useful when you need to run multiple `detox test` commands in parallel with --keepLockFile.'; | ||
module.exports.desc = 'Resets Detox lock file completely - all devices are marked as available after that.'; | ||
module.exports.handler = async function resetLockFile() { | ||
await Promise.all([ | ||
DeviceRegistry.forIOS().reset(), | ||
DeviceRegistry.forAndroid().reset(), | ||
]); | ||
const registry = new DeviceRegistry(); | ||
await registry.reset(); | ||
log.info(`Cleaned lock files from: ${getDetoxLibraryRootPath()}`); | ||
log.info(`Cleaned lock file at: ${registry.lockFilePath}`); | ||
}; |
103
package.json
{ | ||
"name": "detox", | ||
"description": "E2E tests and automation for mobile", | ||
"version": "20.11.0", | ||
"version": "20.13.1", | ||
"bin": { | ||
@@ -54,3 +54,3 @@ "detox": "local-cli/cli.js" | ||
"jest": "^28.1.3", | ||
"jest-allure2-reporter": "^1.2.1", | ||
"jest-allure2-reporter": "2.0.0-alpha.3", | ||
"mockdate": "^2.0.1", | ||
@@ -110,103 +110,6 @@ "prettier": "^2.4.1", | ||
}, | ||
"jest": { | ||
"setupFiles": [ | ||
"<rootDir>/__tests__/setupJest.js" | ||
], | ||
"testEnvironment": "node", | ||
"testRunner": "jest-circus/runner", | ||
"roots": [ | ||
"node_modules", | ||
"local-cli", | ||
"src", | ||
"runners" | ||
], | ||
"testPathIgnorePatterns": [ | ||
"/node_modules/", | ||
"local-cli/test.js" | ||
], | ||
"coveragePathIgnorePatterns": [ | ||
"/node_modules/", | ||
"__tests__", | ||
".test.js$", | ||
".mock.js$", | ||
"index.js", | ||
"internals.js", | ||
"local-cli/utils", | ||
"src/environmentFactory", | ||
"src/android/espressoapi", | ||
"src/artifacts/factories/index.js", | ||
"src/artifacts/providers/index.js", | ||
"src/artifacts/log", | ||
"src/artifacts/screenshot", | ||
"src/artifacts/video", | ||
"src/devices/allocation/drivers/AllocationDriverBase.js", | ||
"src/devices/allocation/drivers/android/emulator/launchEmulatorProcess.js", | ||
"src/devices/allocation/drivers/android/emulator/patchAvdSkinConfig.js", | ||
"src/devices/allocation/drivers/ios", | ||
"src/devices/allocation/factories", | ||
"src/devices/allocation/factories/drivers", | ||
"src/devices/cookies", | ||
"src/devices/common/drivers/android/exec/ADB.js", | ||
"src/devices/common/drivers/android/emulator/exec/EmulatorExec.js", | ||
"src/devices/common/drivers/android/tools/EmulatorTelnet.js", | ||
"src/devices/common/drivers/ios/tools", | ||
"src/devices/runtime/drivers/android/AndroidDriver.js", | ||
"src/devices/runtime/drivers/android/emulator/EmulatorDriver.js", | ||
"src/devices/runtime/drivers/DeviceDriverBase.js", | ||
"src/devices/runtime/drivers/ios", | ||
"src/devices/runtime/factories", | ||
"src/devices/runtime/factories/drivers", | ||
"src/matchers/factories", | ||
"src/validation/EnvironmentValidatorBase.js", | ||
"src/validation/factories", | ||
"src/validation/ios/IosSimulatorEnvValidator", | ||
"src/utils/appdatapath.js", | ||
"src/utils/debug.js", | ||
"src/utils/environment.js", | ||
"src/utils/logger.js", | ||
"src/utils/pipeCommands.js", | ||
"src/utils/pressAnyKey.js", | ||
"src/utils/shellUtils.js", | ||
"runners/jest/reporters", | ||
"runners/jest/testEnvironment", | ||
"src/DetoxWorker.js", | ||
"src/logger/utils/streamUtils.js", | ||
"src/realms" | ||
], | ||
"resetMocks": true, | ||
"resetModules": true, | ||
"reporters": [ | ||
"default", | ||
[ | ||
"jest-allure2-reporter", | ||
{ | ||
"getEnvironmentInfo": false | ||
} | ||
] | ||
], | ||
"coverageReporters": [ | ||
"html", | ||
"json", | ||
"text", | ||
"clover", | ||
[ | ||
"lcov", | ||
{ | ||
"projectRoot": ".." | ||
} | ||
] | ||
], | ||
"coverageThreshold": { | ||
"global": { | ||
"statements": 100, | ||
"branches": 100, | ||
"functions": 100, | ||
"lines": 100 | ||
} | ||
} | ||
}, | ||
"browserslist": [ | ||
"node 14" | ||
], | ||
"gitHead": "e132a02242f1a4ebcf1775ba4ceb782d5b1a855c" | ||
"gitHead": "a6fdfe461358d7b00d65af7230911ef7f9cc9366" | ||
} |
@@ -1,1 +0,21 @@ | ||
module.exports = require('./reporters/DetoxReporter'); | ||
/** @typedef {import('@jest/reporters').Reporter} Reporter */ | ||
const { | ||
DetoxIPCReporter, | ||
DetoxReporterDispatcher, | ||
DetoxSummaryReporter, | ||
DetoxVerboseReporter, | ||
} = require('./reporters'); | ||
/** @implements {Reporter} */ | ||
class DetoxReporter extends DetoxReporterDispatcher { | ||
constructor(globalConfig) { | ||
super(globalConfig, { | ||
DetoxSummaryReporter, | ||
DetoxVerboseReporter, | ||
DetoxIPCReporter, | ||
}); | ||
} | ||
} | ||
module.exports = DetoxReporter; |
@@ -73,3 +73,8 @@ const path = require('path'); | ||
// @ts-expect-error TS2425 | ||
async handleTestEvent(event, state) { | ||
if (detox.session.unsafe_earlyTeardown) { | ||
throw new Error('Detox halted test execution due to an early teardown request'); | ||
} | ||
this._timer.schedule(state.testTimeout != null ? state.testTimeout : this.setupTimeout); | ||
@@ -76,0 +81,0 @@ |
@@ -18,8 +18,7 @@ const path = require('path'); | ||
this._emitter = emitter; | ||
this._originalMatcher = matcher; | ||
this._selectElementWithMatcher(this._originalMatcher); | ||
this._matcher = matcher; | ||
} | ||
_selectElementWithMatcher(matcher) { | ||
this._call = invoke.call(invoke.Espresso, 'onView', matcher._call); | ||
get _call() { | ||
return invoke.call(invoke.Espresso, 'onView', this._matcher._call); | ||
} | ||
@@ -29,6 +28,5 @@ | ||
if (typeof index !== 'number') throw new DetoxRuntimeError({ message: `Element atIndex argument must be a number, got ${typeof index}` }); | ||
const matcher = this._originalMatcher; | ||
this._originalMatcher._call = invoke.callDirectly(DetoxMatcherApi.matcherForAtIndex(index, matcher._call.value)); | ||
const matcher = this._matcher; | ||
this._matcher._call = invoke.callDirectly(DetoxMatcherApi.matcherForAtIndex(index, matcher._call.value)); | ||
this._selectElementWithMatcher(this._originalMatcher); | ||
return this; | ||
@@ -40,3 +38,3 @@ } | ||
const traceDescription = actionDescription.tapAtPoint(value); | ||
return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); | ||
return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute(); | ||
} | ||
@@ -47,3 +45,3 @@ | ||
const traceDescription = actionDescription.tapAtPoint(value); | ||
return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); | ||
return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute(); | ||
} | ||
@@ -54,3 +52,3 @@ | ||
const traceDescription = actionDescription.longPress(); | ||
return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); | ||
return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute(); | ||
} | ||
@@ -64,3 +62,3 @@ | ||
const traceDescription = actionDescription.multiTap(times); | ||
return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); | ||
return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute(); | ||
} | ||
@@ -71,3 +69,3 @@ | ||
const traceDescription = actionDescription.tapBackspaceKey(); | ||
return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); | ||
return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute(); | ||
} | ||
@@ -78,3 +76,3 @@ | ||
const traceDescription = actionDescription.tapReturnKey(); | ||
return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); | ||
return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute(); | ||
} | ||
@@ -85,3 +83,3 @@ | ||
const traceDescription = actionDescription.typeText(value); | ||
return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); | ||
return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute(); | ||
} | ||
@@ -92,3 +90,3 @@ | ||
const traceDescription = actionDescription.replaceText(value); | ||
return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); | ||
return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute(); | ||
} | ||
@@ -99,3 +97,3 @@ | ||
const traceDescription = actionDescription.clearText(); | ||
return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); | ||
return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute(); | ||
} | ||
@@ -106,3 +104,3 @@ | ||
const traceDescription = actionDescription.scroll(amount, direction, startPositionX, startPositionY); | ||
return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); | ||
return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute(); | ||
} | ||
@@ -112,7 +110,7 @@ | ||
// override the user's element selection with an extended matcher that looks for UIScrollView children | ||
this._selectElementWithMatcher(this._originalMatcher._extendToDescendantScrollViews()); | ||
this._matcher = this._matcher._extendToDescendantScrollViews(); | ||
const action = new actions.ScrollEdgeAction(edge); | ||
const traceDescription = actionDescription.scrollTo(edge); | ||
return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); | ||
return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute(); | ||
} | ||
@@ -122,7 +120,7 @@ | ||
// override the user's element selection with an extended matcher that looks for UIScrollView children | ||
this._selectElementWithMatcher(this._originalMatcher._extendToDescendantScrollViews()); | ||
this._matcher = this._matcher._extendToDescendantScrollViews(); | ||
const action = new actions.ScrollToIndex(index); | ||
const traceDescription = actionDescription.scrollToIndex(index); | ||
return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); | ||
return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute(); | ||
} | ||
@@ -137,3 +135,3 @@ | ||
const traceDescription = actionDescription.setDatePickerDate(dateString, formatString); | ||
return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); | ||
return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute(); | ||
} | ||
@@ -152,7 +150,7 @@ | ||
// override the user's element selection with an extended matcher that avoids RN issues with RCTScrollView | ||
this._selectElementWithMatcher(this._originalMatcher._avoidProblematicReactNativeElements()); | ||
this._matcher = this._matcher._avoidProblematicReactNativeElements(); | ||
const action = new actions.SwipeAction(direction, speed, normalizedSwipeOffset, normalizedStartingPointX, normalizedStartingPointY); | ||
const traceDescription = actionDescription.swipe(direction, speed, normalizedSwipeOffset, normalizedStartingPointX, normalizedStartingPointY); | ||
return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); | ||
return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute(); | ||
} | ||
@@ -164,3 +162,3 @@ | ||
const traceDescription = actionDescription.takeScreenshot(screenshotName); | ||
const resultBase64 = await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); | ||
const resultBase64 = await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute(); | ||
const filePath = tempfile('detox.element-screenshot.png'); | ||
@@ -180,4 +178,3 @@ await fs.writeFile(filePath, resultBase64, 'base64'); | ||
const traceDescription = actionDescription.getAttributes(); | ||
const result = await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); | ||
return JSON.parse(result); | ||
return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute(); | ||
} | ||
@@ -188,3 +185,3 @@ | ||
const traceDescription = actionDescription.adjustSliderToPosition(newPosition); | ||
return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute(); | ||
return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute(); | ||
} | ||
@@ -194,3 +191,3 @@ | ||
const traceDescription = actionDescription.performAccessibilityAction(actionName); | ||
return await new ActionInteraction(this._invocationManager, this, new actions.AccessibilityActionAction(actionName), traceDescription).execute(); | ||
return await new ActionInteraction(this._invocationManager, this._matcher, new actions.AccessibilityActionAction(actionName), traceDescription).execute(); | ||
} | ||
@@ -197,0 +194,0 @@ } |
const DetoxRuntimeError = require('../../errors/DetoxRuntimeError'); | ||
const invoke = require('../../invoke'); | ||
const assertIsFunction = require('../../utils/assertIsFunction'); | ||
const isArrowFunction = require('../../utils/isArrowFunction'); | ||
const actions = require('../actions/web'); | ||
@@ -76,8 +78,10 @@ const EspressoWebDetoxApi = require('../espressoapi/web/EspressoWebDetox'); | ||
async runScript(script) { | ||
return await new ActionInteraction(this[_invocationManager], new actions.WebRunScriptAction(this, script)).execute(); | ||
} | ||
async runScript(maybeFunction, args) { | ||
const script = stringifyScript(maybeFunction); | ||
async runScriptWithArgs(script, args) { | ||
return await new ActionInteraction(this[_invocationManager], new actions.WebRunScriptWithArgsAction(this, script, args)).execute(); | ||
if (args) { | ||
return await new ActionInteraction(this[_invocationManager], new actions.WebRunScriptWithArgsAction(this, script, args)).execute(); | ||
} else { | ||
return await new ActionInteraction(this[_invocationManager], new actions.WebRunScriptAction(this, script)).execute(); | ||
} | ||
} | ||
@@ -124,2 +128,16 @@ | ||
function stringifyScript(maybeFunction) { | ||
if (typeof maybeFunction !== 'string' && typeof maybeFunction !== 'function') { | ||
return maybeFunction; | ||
} | ||
const script = (typeof maybeFunction === 'function' ? maybeFunction.toString() : assertIsFunction(maybeFunction)).trim(); | ||
// WebElement interactions don't support arrow functions for some reason. | ||
if (isArrowFunction(script)) { | ||
return `function arrowWorkaround() { return (${script}).apply(this, arguments); }`; | ||
} | ||
return script; | ||
} | ||
module.exports = { | ||
@@ -126,0 +144,0 @@ WebElement, |
@@ -17,3 +17,3 @@ /** | ||
class DetoxAssertion { | ||
static assertMatcher(i, m) { | ||
static assertMatcher(viewInteraction, viewMatcher) { | ||
return { | ||
@@ -27,6 +27,6 @@ target: { | ||
type: "Invocation", | ||
value: i | ||
value: viewInteraction | ||
}, { | ||
type: "Invocation", | ||
value: sanitize_matcher(m) | ||
value: sanitize_matcher(viewMatcher) | ||
}] | ||
@@ -36,3 +36,3 @@ }; | ||
static assertNotVisible(i) { | ||
static assertNotVisible(viewInteraction) { | ||
return { | ||
@@ -46,3 +46,3 @@ target: { | ||
type: "Invocation", | ||
value: i | ||
value: viewInteraction | ||
}] | ||
@@ -52,3 +52,3 @@ }; | ||
static assertNotExists(i) { | ||
static assertNotExists(viewInteraction) { | ||
return { | ||
@@ -62,3 +62,3 @@ target: { | ||
type: "Invocation", | ||
value: i | ||
value: viewInteraction | ||
}] | ||
@@ -68,3 +68,3 @@ }; | ||
static waitForAssertMatcher(i, m, timeoutSeconds) { | ||
static waitForAssertMatcher(viewInteraction, viewMatcher, timeoutSeconds) { | ||
if (typeof timeoutSeconds !== "number") throw new Error("timeoutSeconds should be a number, but got " + (timeoutSeconds + (" (" + (typeof timeoutSeconds + ")")))); | ||
@@ -79,6 +79,6 @@ return { | ||
type: "Invocation", | ||
value: i | ||
value: viewInteraction | ||
}, { | ||
type: "Invocation", | ||
value: sanitize_matcher(m) | ||
value: sanitize_matcher(viewMatcher) | ||
}, { | ||
@@ -91,3 +91,4 @@ type: "Double", | ||
static waitForAssertMatcherWithSearchAction(i, vm, searchAction, searchMatcher) { | ||
static waitForAssertMatcherWithSearchAction(viewInteraction, viewMatcher, searchAction, searchMatcher | ||
) { | ||
return { | ||
@@ -101,9 +102,10 @@ target: { | ||
type: "Invocation", | ||
value: i | ||
value: viewInteraction | ||
}, { | ||
type: "Invocation", | ||
value: sanitize_matcher(vm) | ||
value: sanitize_matcher(viewMatcher) | ||
}, searchAction, { | ||
type: "Invocation", | ||
value: sanitize_matcher(searchMatcher) | ||
value: sanitize_matcher(searchMatcher | ||
) | ||
}] | ||
@@ -110,0 +112,0 @@ }; |
@@ -8,5 +8,12 @@ /** | ||
function sanitize_matcher(matcher) { | ||
if (!matcher._call) { | ||
return matcher; | ||
} | ||
const originalMatcher = typeof matcher._call === 'function' ? matcher._call() : matcher._call; | ||
return originalMatcher.type ? originalMatcher.value : originalMatcher; | ||
} | ||
class EspressoDetox { | ||
static perform(interaction, action) { | ||
static perform(matcher, action) { | ||
return { | ||
@@ -20,3 +27,3 @@ target: { | ||
type: "Invocation", | ||
value: interaction | ||
value: sanitize_matcher(matcher) | ||
}, action] | ||
@@ -23,0 +30,0 @@ }; |
@@ -79,6 +79,3 @@ /** | ||
method: "runScriptWithArgs", | ||
args: [script, { | ||
type: "ArrayList<Object>", | ||
value: args | ||
}] | ||
args: [script, args] | ||
}; | ||
@@ -85,0 +82,0 @@ } |
@@ -28,5 +28,5 @@ const DetoxRuntimeError = require('../../errors/DetoxRuntimeError'); | ||
class ActionInteraction extends Interaction { | ||
constructor(invocationManager, element, action, traceDescription) { | ||
constructor(invocationManager, matcher, action, traceDescription) { | ||
super(invocationManager, traceDescription); | ||
this._call = EspressoDetoxApi.perform(call(element._call), action._call); | ||
this._call = EspressoDetoxApi.perform(matcher, action._call); | ||
// TODO: move this.execute() here from the caller | ||
@@ -52,3 +52,2 @@ } | ||
this._assertionMatcher = assertionMatcher; | ||
this._element._selectElementWithMatcher(this._element._originalMatcher); | ||
} | ||
@@ -55,0 +54,0 @@ |
@@ -7,3 +7,3 @@ class ArtifactPluginsProvider { | ||
declareArtifactPlugins({ client }) { | ||
const serviceLocator = require('../../servicelocator/android'); | ||
const serviceLocator = require('../../devices/servicelocator/android'); | ||
const adb = serviceLocator.adb; | ||
@@ -38,4 +38,4 @@ const devicePathBuilder = serviceLocator.devicePathBuilder; | ||
declareArtifactPlugins({ client }) { | ||
const serviceLocator = require('../../servicelocator/ios'); | ||
const appleSimUtils = serviceLocator.appleSimUtils; | ||
const AppleSimUtils = require('../../devices/common/drivers/ios/tools/AppleSimUtils'); | ||
const appleSimUtils = new AppleSimUtils(); | ||
@@ -42,0 +42,0 @@ const SimulatorInstrumentsPlugin = require('../instruments/ios/SimulatorInstrumentsPlugin'); |
@@ -25,19 +25,2 @@ const path = require('path'); | ||
async onBootDevice(event) { | ||
await super.onBootDevice(event); | ||
if (this.enabled && event.coldBoot) { | ||
await this.appleSimUtils.takeScreenshot(event.deviceId, '/dev/null').catch(() => { | ||
log.debug({}, ` | ||
NOTE: For an unknown yet reason, taking the first screenshot is apt | ||
to fail when booting iOS Simulator in a hidden window mode (or on CI). | ||
Detox applies a workaround by taking a dummy screenshot to ensure | ||
that the future ones are going to work fine. This screenshot is not | ||
saved anywhere, and the error above is suppressed for all log levels | ||
except for "debug" and "trace." | ||
`.trim()); | ||
}); | ||
} | ||
} | ||
async onBeforeUninstallApp(event) { | ||
@@ -44,0 +27,0 @@ await this.api.requestIdleCallback(async () => { |
@@ -35,2 +35,3 @@ /* eslint @typescript-eslint/no-unused-vars: ["error", { "args": "none" }] */ | ||
return _.merge(acc, { | ||
// @ts-ignore-line | ||
options: typeof options === 'function' ? options(acc) : options | ||
@@ -37,0 +38,0 @@ }); |
@@ -36,3 +36,2 @@ const CAF = require('caf'); | ||
/** @type {DetoxInternals.RuntimeConfig['apps']} */ | ||
@@ -64,3 +63,2 @@ this._appsConfig = null; | ||
this._deviceAllocator = null; | ||
this._deviceCookie = null; | ||
@@ -120,3 +118,2 @@ | ||
envValidatorFactory, | ||
deviceAllocatorFactory, | ||
// @ts-ignore | ||
@@ -128,3 +125,3 @@ artifactsManagerFactory, | ||
runtimeDeviceFactory, | ||
} = environmentFactory.createFactories(this._deviceConfig); | ||
} = environmentFactory.createFactories(deviceConfig); | ||
@@ -142,7 +139,4 @@ const envValidator = envValidatorFactory.createValidator(); | ||
this._artifactsManager = artifactsManagerFactory.createArtifactsManager(this._artifactsConfig, commonDeps); | ||
this._deviceAllocator = deviceAllocatorFactory.createDeviceAllocator(commonDeps); | ||
this._deviceCookie = yield this._deviceAllocator.allocate(this._deviceConfig); | ||
this._deviceCookie = yield this._context[symbols.allocateDevice](); | ||
yield this._deviceAllocator.postAllocate(this._deviceCookie); | ||
this.device = runtimeDeviceFactory.createRuntimeDevice( | ||
@@ -165,2 +159,4 @@ this._deviceCookie, | ||
yield this._eventEmitter.emit('bootDevice', { deviceId: this.device.id }); | ||
if (behaviorConfig.init.exposeGlobals) { | ||
@@ -216,7 +212,5 @@ const injectedGlobals = { | ||
if (this._deviceCookie) { | ||
const shutdown = this._behaviorConfig ? this._behaviorConfig.cleanup.shutdownDevice : false; | ||
await this._deviceAllocator.free(this._deviceCookie, { shutdown }); | ||
await this._context[symbols.deallocateDevice](this._deviceCookie); | ||
} | ||
this._deviceAllocator = null; | ||
this._deviceCookie = null; | ||
@@ -223,0 +217,0 @@ this.device = null; |
@@ -1,3 +0,8 @@ | ||
// @ts-nocheck | ||
const log = require('../../utils/logger').child({ cat: 'device' }); | ||
/** | ||
* @typedef {import('./drivers/AllocationDriverBase').AllocationDriverBase} AllocationDriverBase | ||
* @typedef {import('./drivers/AllocationDriverBase').DeallocOptions} DeallocOptions | ||
* @typedef {import('../common/drivers/DeviceCookie').DeviceCookie} DeviceCookie | ||
*/ | ||
const log = require('../../utils/logger').child({ cat: 'device,device-allocation' }); | ||
const traceMethods = require('../../utils/traceMethods'); | ||
@@ -7,39 +12,80 @@ | ||
/** | ||
* @param allocationDriver { AllocationDriverBase } | ||
* @param {AllocationDriverBase} allocationDriver | ||
*/ | ||
constructor(allocationDriver) { | ||
this._driver = allocationDriver; | ||
traceMethods(log, this, ['allocate', 'postAllocate', 'free']); | ||
this._counter = 0; | ||
this._ids = new Map(); | ||
traceMethods(log, this, ['init', 'cleanup', 'emergencyCleanup']); | ||
} | ||
/** | ||
* @param deviceConfig { Object } | ||
* @return {Promise<DeviceCookie>} | ||
* @returns {Promise<void>} | ||
*/ | ||
allocate(deviceConfig) { | ||
return this._driver.allocate(deviceConfig); | ||
async init() { | ||
if (typeof this._driver.init === 'function') { | ||
await this._driver.init(); | ||
} | ||
} | ||
/** | ||
* @param {DeviceCookie} deviceCookie | ||
* @return {Promise<unknown>} | ||
* @param {Detox.DetoxDeviceConfig} deviceConfig | ||
* @returns {Promise<DeviceCookie>} | ||
*/ | ||
postAllocate(deviceCookie) { | ||
if (typeof this._driver.postAllocate !== 'function') { | ||
return Promise.resolve(); | ||
} | ||
async allocate(deviceConfig) { | ||
const tid = this._counter++; | ||
return await log.trace.complete({ data: deviceConfig, id: tid }, 'allocate', async () => { | ||
const cookie = await this._driver.allocate(deviceConfig); | ||
log.debug({ data: cookie }, `settled on ${cookie.name || cookie.id}`); | ||
this._ids.set(cookie.id, tid); | ||
return cookie; | ||
}); | ||
} | ||
return this._driver.postAllocate(deviceCookie); | ||
/** | ||
* @param {DeviceCookie} cookie | ||
* @returns {Promise<DeviceCookie>} | ||
*/ | ||
async postAllocate(cookie) { | ||
const tid = this._ids.get(cookie.id); | ||
return await log.trace.complete({ data: cookie, id: tid }, `post-allocate: ${cookie.id}`, async () => { | ||
const updatedCookie = typeof this._driver.postAllocate === 'function' | ||
? await this._driver.postAllocate(cookie) | ||
: undefined; | ||
return updatedCookie || cookie; | ||
}); | ||
} | ||
/** | ||
* @param cookie { DeviceCookie } | ||
* @param options { DeallocOptions } | ||
* @return {Promise<void>} | ||
* @param {DeviceCookie} cookie | ||
* @param {DeallocOptions} options | ||
* @returns {Promise<void>} | ||
*/ | ||
free(cookie, options) { | ||
return this._driver.free(cookie, options); | ||
async free(cookie, options = {}) { | ||
const tid = this._ids.get(cookie.id); | ||
await log.trace.complete({ data: options, id: tid }, `free: ${cookie.id}`, async () => { | ||
await this._driver.free(cookie, options); | ||
}); | ||
} | ||
/** | ||
* @returns {Promise<void>} | ||
*/ | ||
async cleanup() { | ||
if (typeof this._driver.cleanup === 'function') { | ||
await this._driver.cleanup(); | ||
} | ||
} | ||
/** | ||
* @returns {void} | ||
*/ | ||
emergencyCleanup() { | ||
if (typeof this._driver.emergencyCleanup === 'function') { | ||
this._driver.emergencyCleanup(); | ||
} | ||
} | ||
} | ||
module.exports = DeviceAllocator; |
@@ -1,20 +0,26 @@ | ||
// @ts-nocheck | ||
const AttachedAndroidDeviceCookie = require('../../../../cookies/AttachedAndroidDeviceCookie'); | ||
const AllocationDriverBase = require('../../AllocationDriverBase'); | ||
/** | ||
* @typedef {import('../../AllocationDriverBase').AllocationDriverBase} AllocationDriverBase | ||
* @typedef {import('../../../../common/drivers/android/cookies').AndroidDeviceCookie} AndroidDeviceCookie | ||
*/ | ||
class AttachedAndroidAllocDriver extends AllocationDriverBase { | ||
/** | ||
* @implements {AllocationDriverBase} | ||
*/ | ||
class AttachedAndroidAllocDriver { | ||
/** | ||
* @param adb { ADB } | ||
* @param deviceRegistry { DeviceRegistry } | ||
* @param freeDeviceFinder { FreeDeviceFinder } | ||
* @param attachedAndroidLauncher { AttachedAndroidLauncher } | ||
* @param {object} options | ||
* @param {import('../../../../common/drivers/android/exec/ADB')} options.adb | ||
* @param {import('../../../DeviceRegistry')} options.deviceRegistry | ||
* @param {import('../FreeDeviceFinder')} options.freeDeviceFinder | ||
*/ | ||
constructor({ adb, deviceRegistry, freeDeviceFinder, attachedAndroidLauncher }) { | ||
super(); | ||
constructor({ adb, deviceRegistry, freeDeviceFinder }) { | ||
this._adb = adb; | ||
this._deviceRegistry = deviceRegistry; | ||
this._freeDeviceFinder = freeDeviceFinder; | ||
this._attachedAndroidLauncher = attachedAndroidLauncher; | ||
} | ||
async init() { | ||
await this._deviceRegistry.unregisterZombieDevices(); | ||
} | ||
/** | ||
@@ -26,9 +32,9 @@ * @param deviceConfig | ||
const adbNamePattern = deviceConfig.device.adbName; | ||
const adbName = await this._deviceRegistry.allocateDevice(() => this._freeDeviceFinder.findFreeDevice(adbNamePattern)); | ||
const adbName = await this._deviceRegistry.registerDevice(() => this._freeDeviceFinder.findFreeDevice(adbNamePattern)); | ||
return new AttachedAndroidDeviceCookie(adbName); | ||
return { id: adbName, adbName }; | ||
} | ||
/** | ||
* @param {AttachedAndroidDeviceCookie} deviceCookie | ||
* @param {AndroidDeviceCookie} deviceCookie | ||
* @returns {Promise<void>} | ||
@@ -42,7 +48,6 @@ */ | ||
await this._adb.unlockScreen(adbName); | ||
await this._attachedAndroidLauncher.notifyLaunchCompleted(adbName); | ||
} | ||
/** | ||
* @param cookie { AttachedAndroidDeviceCookie } | ||
* @param cookie { AndroidDeviceCookie } | ||
* @return {Promise<void>} | ||
@@ -52,3 +57,3 @@ */ | ||
const { adbName } = cookie; | ||
await this._deviceRegistry.disposeDevice(adbName); | ||
await this._deviceRegistry.unregisterDevice(adbName); | ||
} | ||
@@ -55,0 +60,0 @@ } |
@@ -1,30 +0,56 @@ | ||
// @ts-nocheck | ||
/** | ||
* @typedef {import('../../AllocationDriverBase').AllocationDriverBase} AllocationDriverBase | ||
* @typedef {import('../../../../common/drivers/android/cookies').AndroidDeviceCookie} AndroidDeviceCookie | ||
*/ | ||
const _ = require('lodash'); | ||
const AndroidEmulatorCookie = require('../../../../cookies/AndroidEmulatorCookie'); | ||
const AllocationDriverBase = require('../../AllocationDriverBase'); | ||
const Deferred = require('../../../../../utils/Deferred'); | ||
const log = require('../../../../../utils/logger').child({ cat: 'device,device-allocation' }); | ||
const { patchAvdSkinConfig } = require('./patchAvdSkinConfig'); | ||
class EmulatorAllocDriver extends AllocationDriverBase { | ||
/** | ||
* @implements {AllocationDriverBase} | ||
*/ | ||
class EmulatorAllocDriver { | ||
/** | ||
* @param adb { ADB } | ||
* @param avdValidator { AVDValidator } | ||
* @param emulatorVersionResolver { EmulatorVersionResolver } | ||
* @param emulatorLauncher { EmulatorLauncher } | ||
* @param allocationHelper { EmulatorAllocationHelper } | ||
* @param {object} options | ||
* @param {import('../../../../common/drivers/android/exec/ADB')} options.adb | ||
* @param {import('./AVDValidator')} options.avdValidator | ||
* @param {DetoxInternals.RuntimeConfig} options.detoxConfig | ||
* @param {import('../../../DeviceRegistry')} options.deviceRegistry | ||
* @param {import('./FreeEmulatorFinder')} options.freeDeviceFinder | ||
* @param {import('./FreePortFinder')} options.freePortFinder | ||
* @param {import('./EmulatorLauncher')} options.emulatorLauncher | ||
* @param {import('./EmulatorVersionResolver')} options.emulatorVersionResolver | ||
*/ | ||
constructor({ adb, avdValidator, emulatorVersionResolver, emulatorLauncher, allocationHelper }) { | ||
super(); | ||
constructor({ | ||
adb, | ||
avdValidator, | ||
detoxConfig, | ||
deviceRegistry, | ||
freeDeviceFinder, | ||
freePortFinder, | ||
emulatorVersionResolver, | ||
emulatorLauncher | ||
}) { | ||
this._adb = adb; | ||
this._avdValidator = avdValidator; | ||
this._deviceRegistry = deviceRegistry; | ||
this._emulatorVersionResolver = emulatorVersionResolver; | ||
this._emulatorLauncher = emulatorLauncher; | ||
this._allocationHelper = allocationHelper; | ||
this._launchInfo = {}; | ||
this._freeDeviceFinder = freeDeviceFinder; | ||
this._freePortFinder = freePortFinder; | ||
this._shouldShutdown = detoxConfig.behavior.cleanup.shutdownDevice; | ||
this._fixAvdConfigIniSkinNameIfNeeded = _.memoize(this._fixAvdConfigIniSkinNameIfNeeded.bind(this)); | ||
} | ||
async init() { | ||
await this._deviceRegistry.unregisterZombieDevices(); | ||
} | ||
/** | ||
* @param deviceConfig | ||
* @returns {Promise<AndroidEmulatorCookie>} | ||
* @returns {Promise<AndroidDeviceCookie>} | ||
*/ | ||
@@ -37,29 +63,36 @@ async allocate(deviceConfig) { | ||
const allocResult = await this._allocationHelper.allocateDevice(avdName); | ||
const { adbName } = allocResult; | ||
const adbName = await this._deviceRegistry.registerDevice(async () => { | ||
let adbName = await this._freeDeviceFinder.findFreeDevice(avdName); | ||
if (!adbName) { | ||
const port = await this._freePortFinder.findFreePort(); | ||
adbName = `emulator-${port}`; | ||
this._launchInfo[adbName] = { | ||
avdName, | ||
isRunning: allocResult.isRunning, | ||
launchOptions: { | ||
bootArgs: deviceConfig.bootArgs, | ||
gpuMode: deviceConfig.gpuMode, | ||
headless: deviceConfig.headless, | ||
readonly: deviceConfig.readonly, | ||
port: allocResult.placeholderPort, | ||
}, | ||
await this._emulatorLauncher.launch({ | ||
bootArgs: deviceConfig.bootArgs, | ||
gpuMode: deviceConfig.gpuMode, | ||
headless: deviceConfig.headless, | ||
readonly: deviceConfig.readonly, | ||
avdName, | ||
adbName, | ||
port, | ||
}); | ||
} | ||
return adbName; | ||
}); | ||
return { | ||
id: adbName, | ||
adbName, | ||
name: `${adbName} (${avdName})`, | ||
}; | ||
return new AndroidEmulatorCookie(adbName); | ||
} | ||
/** | ||
* @param {AndroidEmulatorCookie} deviceCookie | ||
* @returns {Promise<void>} | ||
* @param {AndroidDeviceCookie} deviceCookie | ||
*/ | ||
async postAllocate(deviceCookie) { | ||
const { adbName } = deviceCookie; | ||
const { avdName, isRunning, launchOptions } = this._launchInfo[adbName]; | ||
await this._emulatorLauncher.launch(avdName, adbName, isRunning, launchOptions); | ||
await this._emulatorLauncher.awaitEmulatorBoot(adbName); | ||
await this._adb.apiLevel(adbName); | ||
@@ -71,5 +104,5 @@ await this._adb.disableAndroidAnimations(adbName); | ||
/** | ||
* @param cookie { AndroidEmulatorCookie } | ||
* @param options { DeallocOptions } | ||
* @return { Promise<void> } | ||
* @param cookie {AndroidDeviceCookie} | ||
* @param options {Partial<import('../../AllocationDriverBase').DeallocOptions>} | ||
* @return {Promise<void>} | ||
*/ | ||
@@ -79,6 +112,32 @@ async free(cookie, options = {}) { | ||
await this._allocationHelper.deallocateDevice(adbName); | ||
if (options.shutdown) { | ||
await this._doShutdown(adbName); | ||
await this._deviceRegistry.unregisterDevice(adbName); | ||
} else { | ||
await this._deviceRegistry.releaseDevice(adbName); | ||
} | ||
} | ||
if (options.shutdown) { | ||
async cleanup() { | ||
if (this._shouldShutdown) { | ||
const { devices } = await this._adb.devices(); | ||
const actualEmulators = devices.map((device) => device.adbName); | ||
const sessionDevices = await this._deviceRegistry.readSessionDevices(); | ||
const emulatorsToShutdown = _.intersection(sessionDevices.getIds(), actualEmulators); | ||
const shutdownPromises = emulatorsToShutdown.map((adbName) => this._doShutdown(adbName)); | ||
await Promise.all(shutdownPromises); | ||
} | ||
await this._deviceRegistry.unregisterSessionDevices(); | ||
} | ||
/** | ||
* @param {string} adbName | ||
* @return {Promise<void>} | ||
*/ | ||
async _doShutdown(adbName) { | ||
try { | ||
await this._emulatorLauncher.shutdown(adbName); | ||
} catch (err) { | ||
log.warn({ err }, `Failed to shutdown emulator ${adbName}`); | ||
} | ||
@@ -85,0 +144,0 @@ } |
// @ts-nocheck | ||
const { DetoxRuntimeError } = require('../../../../../errors'); | ||
const log = require('../../../../../utils/logger').child({ cat: 'device' }); | ||
const retry = require('../../../../../utils/retry'); | ||
const traceMethods = require('../../../../../utils/traceMethods'); | ||
const DeviceLauncher = require('../../../../common/drivers/DeviceLauncher'); | ||
const { LaunchCommand } = require('../../../../common/drivers/android/emulator/exec/EmulatorExec'); | ||
@@ -13,36 +10,43 @@ | ||
class EmulatorLauncher extends DeviceLauncher { | ||
constructor({ adb, emulatorExec, eventEmitter }) { | ||
super(eventEmitter); | ||
class EmulatorLauncher { | ||
constructor({ adb, emulatorExec }) { | ||
this._adb = adb; | ||
this._emulatorExec = emulatorExec; | ||
traceMethods(log, this, ['_awaitEmulatorBoot']); | ||
} | ||
/** | ||
* @param avdName { String } | ||
* @param adbName { String } | ||
* @param isRunning { Boolean } | ||
* @param options { Object } | ||
* @param options.port { Number | undefined } | ||
* @param options.bootArgs { String | undefined } | ||
* @param options.gpuMode { String | undefined } | ||
* @param options.headless { Boolean } | ||
* @param options.readonly { Boolean } | ||
* @param {object} options | ||
* @param {string} options.avdName | ||
* @param {string} options.adbName | ||
* @param {number} options.port | ||
* @param {string | undefined} options.bootArgs | ||
* @param {string | undefined} options.gpuMode | ||
* @param {boolean} options.headless | ||
* @param {boolean} options.readonly | ||
*/ | ||
async launch(avdName, adbName, isRunning, options = { port: undefined }) { | ||
if (!isRunning) { | ||
const launchCommand = new LaunchCommand(avdName, options); | ||
await retry({ | ||
retries: 2, | ||
interval: 100, | ||
conditionFn: isUnknownEmulatorError, | ||
}, () => this._launchEmulator(avdName, launchCommand, adbName)); | ||
} | ||
await this._awaitEmulatorBoot(adbName); | ||
await this._notifyBootEvent(adbName, avdName, !isRunning); | ||
async launch(options) { | ||
const launchCommand = new LaunchCommand(options); | ||
await retry({ | ||
retries: 2, | ||
interval: 100, | ||
conditionFn: isUnknownEmulatorError, | ||
}, () => launchEmulatorProcess(this._emulatorExec, this._adb, launchCommand)); | ||
} | ||
/** | ||
* @param {string} adbName | ||
*/ | ||
async awaitEmulatorBoot(adbName) { | ||
await retry({ retries: 240, interval: 2500, shouldUnref: true }, async () => { | ||
const isBootComplete = await this._adb.isBootComplete(adbName); | ||
if (!isBootComplete) { | ||
throw new DetoxRuntimeError({ | ||
message: `Waited for ${adbName} to complete booting for too long!`, | ||
}); | ||
} | ||
}); | ||
} | ||
async shutdown(adbName) { | ||
await this._notifyPreShutdown(adbName); | ||
await this._adb.emuKill(adbName); | ||
@@ -61,22 +65,5 @@ await retry({ | ||
}); | ||
await this._notifyShutdownCompleted(adbName); | ||
} | ||
_launchEmulator(emulatorName, launchCommand, adbName) { | ||
return launchEmulatorProcess(emulatorName, this._emulatorExec, launchCommand, this._adb, adbName); | ||
} | ||
async _awaitEmulatorBoot(adbName) { | ||
await retry({ retries: 240, interval: 2500, shouldUnref: true }, async () => { | ||
const isBootComplete = await this._adb.isBootComplete(adbName); | ||
if (!isBootComplete) { | ||
throw new DetoxRuntimeError({ | ||
message: `Waited for ${adbName} to complete booting for too long!`, | ||
}); | ||
} | ||
}); | ||
} | ||
} | ||
module.exports = EmulatorLauncher; |
@@ -1,2 +0,2 @@ | ||
const FreeDeviceFinder = require('../../../../common/drivers/android/tools/FreeDeviceFinder'); | ||
const FreeDeviceFinder = require('../FreeDeviceFinder'); | ||
@@ -3,0 +3,0 @@ class FreeEmulatorFinder extends FreeDeviceFinder { |
@@ -7,6 +7,6 @@ const fs = require('fs'); | ||
function launchEmulatorProcess(emulatorName, emulatorExec, emulatorLaunchCommand, adb, adbName) { | ||
function launchEmulatorProcess(emulatorExec, adb, emulatorLaunchCommand) { | ||
let childProcessOutput; | ||
const portName = emulatorLaunchCommand.port ? `-${emulatorLaunchCommand.port}` : ''; | ||
const tempLog = `./${emulatorName}${portName}.log`; | ||
const tempLog = `./${emulatorLaunchCommand.avdName}${portName}.log`; | ||
const stdout = fs.openSync(tempLog, 'a'); | ||
@@ -35,3 +35,3 @@ const stderr = fs.openSync(tempLog, 'a'); | ||
adb.waitForDevice(adbName).then(() => childProcessPromise._cpResolve()); | ||
adb.waitForDevice(emulatorLaunchCommand.adbName).then(() => childProcessPromise._cpResolve()); | ||
@@ -38,0 +38,0 @@ return childProcessPromise.then(() => true).catch((err) => { |
@@ -0,23 +1,41 @@ | ||
/** | ||
* @typedef {import('../../AllocationDriverBase').AllocationDriverBase} AllocationDriverBase | ||
* @typedef {import('../../../../common/drivers/android/cookies').GenycloudEmulatorCookie} GenycloudEmulatorCookie | ||
*/ | ||
const { DetoxRuntimeError } = require('../../../../../errors'); | ||
const Timer = require('../../../../../utils/Timer'); | ||
const GenycloudEmulatorCookie = require('../../../../cookies/GenycloudEmulatorCookie'); | ||
const AllocationDriverBase = require('../../AllocationDriverBase'); | ||
const log = require('../../../../../utils/logger').child({ cat: 'device' }); | ||
class GenyAllocDriver extends AllocationDriverBase { | ||
const GenyRegistry = require('./GenyRegistry'); | ||
const events = { | ||
GENYCLOUD_TEARDOWN: { event: 'GENYCLOUD_TEARDOWN' }, | ||
}; | ||
/** | ||
* @implements {AllocationDriverBase} | ||
*/ | ||
class GenyAllocDriver { | ||
/** | ||
* @param {object} options | ||
* @param {import('../../../../common/drivers/android/exec/ADB')} options.adb | ||
* @param {DetoxInternals.SessionState} options.detoxSession | ||
* @param {import('./GenyRegistry')} options.genyRegistry | ||
* @param {import('./GenyInstanceLauncher')} options.instanceLauncher | ||
* @param {import('./GenyRecipeQuerying')} options.recipeQuerying | ||
* @param {import('./GenyInstanceAllocationHelper')} options.allocationHelper | ||
* @param {import('./GenyInstanceLauncher')} options.instanceLauncher | ||
*/ | ||
constructor({ adb, recipeQuerying, allocationHelper, instanceLauncher }) { | ||
super(); | ||
constructor({ | ||
adb, | ||
detoxSession, | ||
genyRegistry = new GenyRegistry(), | ||
instanceLauncher, | ||
recipeQuerying, | ||
}) { | ||
this._adb = adb; | ||
this._detoxSessionId = detoxSession.id; | ||
this._genyRegistry = genyRegistry; | ||
this._instanceLauncher = instanceLauncher; | ||
this._recipeQuerying = recipeQuerying; | ||
this._instanceLauncher = instanceLauncher; | ||
this._instanceAllocationHelper = allocationHelper; | ||
this._launchInfo = {}; | ||
this._instanceCounter = 0; | ||
} | ||
@@ -30,2 +48,3 @@ | ||
async allocate(deviceConfig) { | ||
await new Promise((resolve) => setTimeout(resolve, 10000)); | ||
const deviceQuery = deviceConfig.device; | ||
@@ -35,5 +54,15 @@ const recipe = await this._recipeQuerying.getRecipeFromQuery(deviceQuery); | ||
const { instance, isNew } = await this._instanceAllocationHelper.allocateDevice(recipe); | ||
this._launchInfo[instance.uuid] = { isNew }; | ||
return new GenycloudEmulatorCookie(instance); | ||
let instance = this._genyRegistry.findFreeInstance(recipe); | ||
if (!instance) { | ||
const instanceName = `Detox.${this._detoxSessionId}.${this._instanceCounter++}`; | ||
instance = await this._instanceLauncher.launch(recipe, instanceName); | ||
this._genyRegistry.addInstance(instance, recipe); | ||
} | ||
return { | ||
id: instance.uuid, | ||
adbName: instance.adbName, | ||
name: instance.name, | ||
instance, | ||
}; | ||
} | ||
@@ -43,32 +72,57 @@ | ||
* @param {GenycloudEmulatorCookie} cookie | ||
* @returns {Promise<void>} | ||
*/ | ||
async postAllocate(cookie) { | ||
const { instance } = cookie; | ||
const { isNew } = this._launchInfo[instance.uuid]; | ||
const readyInstance = cookie.instance = await this._instanceLauncher.launch(instance, isNew); | ||
const instance = await this._instanceLauncher.connect(cookie.instance); | ||
this._genyRegistry.updateInstance(instance); | ||
const { adbName } = readyInstance; | ||
await Timer.run(20000, 'waiting for device to respond', async () => { | ||
await this._adb.disableAndroidAnimations(adbName); | ||
await this._adb.setWiFiToggle(adbName, true); | ||
await this._adb.apiLevel(adbName); | ||
}); | ||
if (this._genyRegistry.pollNewInstance(instance.uuid)) { | ||
const { adbName } = instance; | ||
await Timer.run(20000, 'waiting for device to respond', async () => { | ||
await this._adb.disableAndroidAnimations(adbName); | ||
await this._adb.setWiFiToggle(adbName, true); | ||
await this._adb.apiLevel(adbName); | ||
}); | ||
} | ||
return { | ||
...cookie, | ||
adbName: instance.adbName, | ||
}; | ||
} | ||
/** | ||
* @param cookie { GenycloudEmulatorCookie } | ||
* @param options { DeallocOptions } | ||
* @param cookie {Omit<GenycloudEmulatorCookie, 'instance'>} | ||
* @param options {Partial<import('../../AllocationDriverBase').DeallocOptions>} | ||
* @return {Promise<void>} | ||
*/ | ||
async free(cookie, options = {}) { | ||
const { instance } = cookie; | ||
await this._instanceAllocationHelper.deallocateDevice(instance.uuid); | ||
// Known issue: cookie won't have a proper 'instance' field due to (de)serialization | ||
if (options.shutdown) { | ||
await this._instanceLauncher.shutdown(instance); | ||
this._genyRegistry.removeInstance(cookie.id); | ||
await this._instanceLauncher.shutdown(cookie.id); | ||
} else { | ||
this._genyRegistry.markAsFree(cookie.id); | ||
} | ||
} | ||
async cleanup() { | ||
log.info(events.GENYCLOUD_TEARDOWN, 'Initiating Genymotion SaaS instances teardown...'); | ||
const killPromises = this._genyRegistry.getInstances().map((instance) => { | ||
this._genyRegistry.markAsBusy(instance.uuid); | ||
const onSuccess = () => this._genyRegistry.removeInstance(instance.uuid); | ||
const onError = (error) => ({ ...instance, error }); | ||
return this._instanceLauncher.shutdown(instance.uuid).then(onSuccess, onError); | ||
}); | ||
const deletionLeaks = (await Promise.all(killPromises)).filter(Boolean); | ||
this._reportGlobalCleanupSummary(deletionLeaks); | ||
} | ||
emergencyCleanup() { | ||
const instances = this._genyRegistry.getInstances(); | ||
this._reportGlobalCleanupSummary(instances); | ||
} | ||
_assertRecipe(deviceQuery, recipe) { | ||
@@ -82,4 +136,22 @@ if (!recipe) { | ||
} | ||
_reportGlobalCleanupSummary(deletionLeaks) { | ||
if (deletionLeaks.length) { | ||
log.warn(events.GENYCLOUD_TEARDOWN, 'WARNING! Detected a Genymotion SaaS instance leakage, for the following instances:'); | ||
deletionLeaks.forEach(({ uuid, name, error }) => { | ||
log.warn(events.GENYCLOUD_TEARDOWN, [ | ||
`Instance ${name} (${uuid})${error ? `: ${error}` : ''}`, | ||
` Kill it by visiting https://cloud.geny.io/instance/${uuid}, or by running:`, | ||
` gmsaas instances stop ${uuid}`, | ||
].join('\n')); | ||
}); | ||
log.info(events.GENYCLOUD_TEARDOWN, 'Instances teardown completed with warnings'); | ||
} else { | ||
log.info(events.GENYCLOUD_TEARDOWN, 'Instances teardown completed successfully'); | ||
} | ||
} | ||
} | ||
module.exports = GenyAllocDriver; |
@@ -1,44 +0,49 @@ | ||
// @ts-nocheck | ||
const DetoxRuntimeError = require('../../../../../errors/DetoxRuntimeError'); | ||
const logger = require('../../../../../utils/logger').child({ cat: 'device' }); | ||
const retry = require('../../../../../utils/retry'); | ||
const DeviceLauncher = require('../../../../common/drivers/DeviceLauncher'); | ||
class GenyInstanceLauncher extends DeviceLauncher { | ||
constructor({ instanceLifecycleService, instanceLookupService, deviceCleanupRegistry, eventEmitter }) { | ||
super(eventEmitter); | ||
const GenyInstance = require('./services/dto/GenyInstance'); | ||
const events = { | ||
CREATE_DEVICE: { event: 'CREATE_DEVICE' }, | ||
}; | ||
class GenyInstanceLauncher { | ||
constructor({ genyCloudExec, instanceLifecycleService }) { | ||
this._genyCloudExec = genyCloudExec; | ||
this._instanceLifecycleService = instanceLifecycleService; | ||
this._instanceLookupService = instanceLookupService; | ||
this._deviceCleanupRegistry = deviceCleanupRegistry; | ||
} | ||
/** | ||
* Note: | ||
* In the context of Genymotion-cloud (as opposed to local emulators), emulators are | ||
* not launched per-se, as with local emulators. Rather, we just need to sync-up with | ||
* them and connect, if needed. | ||
* | ||
* @param instance {GenyInstance} The freshly allocated cloud-instance. | ||
* @param isNew { boolean } | ||
* @param {import('./services/dto/GenyRecipe')} recipe | ||
* @param {string} instanceName | ||
* @returns {Promise<GenyInstance>} | ||
*/ | ||
async launch(instance, isNew = true) { | ||
if (isNew) { | ||
await this._deviceCleanupRegistry.allocateDevice(instance.uuid, { name: instance.name }); | ||
} | ||
instance = await this._waitForInstanceBoot(instance); | ||
instance = await this._adbConnectIfNeeded(instance); | ||
await this._notifyBootEvent(instance.adbName, instance.recipeName, isNew); | ||
async launch(recipe, instanceName) { | ||
logger.debug(events.CREATE_DEVICE, `Trying to create a device based on "${recipe}"`); | ||
const instance = await this._instanceLifecycleService.createInstance(recipe.uuid, instanceName); | ||
const { name, uuid } = instance; | ||
logger.info(events.CREATE_DEVICE, `Allocating Genymotion Cloud instance ${name} for testing. To access it via a browser, go to: https://cloud.geny.io/instance/${uuid}`); | ||
return instance; | ||
} | ||
async shutdown(instance) { | ||
const { uuid } = instance; | ||
/** | ||
* @param {GenyInstance} instance The freshly allocated cloud-instance. | ||
* @returns {Promise<GenyInstance>} | ||
*/ | ||
async connect(instance) { | ||
const bootedInstance = await this._waitForInstanceBoot(instance); | ||
const connectedInstance = await this._adbConnectIfNeeded(bootedInstance); | ||
await this._notifyPreShutdown(uuid); | ||
await this._instanceLifecycleService.deleteInstance(uuid); | ||
await this._deviceCleanupRegistry.disposeDevice(uuid); | ||
await this._notifyShutdownCompleted(uuid); | ||
return connectedInstance; | ||
} | ||
/** | ||
* @param {string} instanceId | ||
*/ | ||
async shutdown(instanceId) { | ||
await this._instanceLifecycleService.deleteInstance(instanceId); | ||
} | ||
async _waitForInstanceBoot(instance) { | ||
@@ -51,13 +56,17 @@ if (instance.isOnline()) { | ||
backoff: 'none', | ||
retries: 25, | ||
retries: 20, | ||
interval: 5000, | ||
initialSleep: 45000, | ||
shouldUnref: true, | ||
}; | ||
return await retry(options, async () => { | ||
const _instance = await this._instanceLookupService.getInstance(instance.uuid); | ||
if (!_instance.isOnline()) { | ||
const { instance: _instance } = await this._genyCloudExec.getInstance(instance.uuid); | ||
const anInstance = new GenyInstance(_instance); | ||
if (!anInstance.isOnline()) { | ||
throw new DetoxRuntimeError(`Timeout waiting for instance ${instance.uuid} to be ready`); | ||
} | ||
return _instance; | ||
return anInstance; | ||
}); | ||
@@ -64,0 +73,0 @@ } |
@@ -1,22 +0,35 @@ | ||
// @ts-nocheck | ||
/** | ||
* @typedef {import('../AllocationDriverBase').AllocationDriverBase} AllocationDriverBase | ||
* @typedef {import('../AllocationDriverBase').DeallocOptions} DeallocOptions | ||
* @typedef {import('../../../common/drivers/ios/cookies').IosSimulatorCookie} IosSimulatorCookie | ||
*/ | ||
const _ = require('lodash'); | ||
const DetoxRuntimeError = require('../../../../errors/DetoxRuntimeError'); | ||
const IosSimulatorCookie = require('../../../cookies/IosSimulatorCookie'); | ||
const AllocationDriverBase = require('../AllocationDriverBase'); | ||
const { DetoxRuntimeError } = require('../../../../errors'); | ||
const log = require('../../../../utils/logger').child({ cat: 'device,device-allocation' }); | ||
class SimulatorAllocDriver extends AllocationDriverBase { | ||
const SimulatorQuery = require('./SimulatorQuery'); | ||
/** | ||
* @implements {AllocationDriverBase} | ||
*/ | ||
class SimulatorAllocDriver { | ||
/** | ||
* @param deviceRegistry { DeviceRegistry } | ||
* @param applesimutils { AppleSimUtils } | ||
* @param simulatorLauncher { SimulatorLauncher } | ||
* @param {object} options | ||
* @param {import('../../DeviceRegistry')} options.deviceRegistry | ||
* @param {DetoxInternals.RuntimeConfig} options.detoxConfig | ||
* @param {import('../../../common/drivers/ios/tools/AppleSimUtils')} options.applesimutils | ||
*/ | ||
constructor({ deviceRegistry, applesimutils, simulatorLauncher }) { | ||
super(); | ||
constructor({ detoxConfig, deviceRegistry, applesimutils }) { | ||
this._deviceRegistry = deviceRegistry; | ||
this._applesimutils = applesimutils; | ||
this._simulatorLauncher = simulatorLauncher; | ||
this._launchInfo = {}; | ||
this._shouldShutdown = detoxConfig.behavior.cleanup.shutdownDevice; | ||
} | ||
async init() { | ||
await this._deviceRegistry.unregisterZombieDevices(); | ||
} | ||
/** | ||
@@ -27,16 +40,15 @@ * @param deviceConfig { Object } | ||
async allocate(deviceConfig) { | ||
const deviceQuery = this._adaptQuery(deviceConfig.device); | ||
const deviceQuery = new SimulatorQuery(deviceConfig.device); | ||
// TODO Delegate this onto a well tested allocator class | ||
const udid = await this._deviceRegistry.allocateDevice(async () => { | ||
const udid = await this._deviceRegistry.registerDevice(async () => { | ||
return await this._findOrCreateDevice(deviceQuery); | ||
}); | ||
const deviceComment = this._commentDevice(deviceQuery); | ||
if (!udid) { | ||
throw new DetoxRuntimeError(`Failed to find device matching ${deviceComment}`); | ||
throw new DetoxRuntimeError(`Failed to find device matching ${deviceQuery.getDeviceComment()}`); | ||
} | ||
this._launchInfo[udid] = { deviceConfig }; | ||
return new IosSimulatorCookie(udid); | ||
return { id: udid, udid }; | ||
} | ||
@@ -46,3 +58,3 @@ | ||
* @param {IosSimulatorCookie} deviceCookie | ||
* @returns {Promise<void>} | ||
* @returns {Promise<IosSimulatorCookie>} | ||
*/ | ||
@@ -52,3 +64,11 @@ async postAllocate(deviceCookie) { | ||
const { deviceConfig } = this._launchInfo[udid]; | ||
await this._simulatorLauncher.launch(udid, deviceConfig.type, deviceConfig.bootArgs, deviceConfig.headless); | ||
await this._applesimutils.boot(udid, deviceConfig.bootArgs, deviceConfig.headless); | ||
return { | ||
id: udid, | ||
udid, | ||
type: deviceConfig.type, | ||
bootArgs: deviceConfig.bootArgs, | ||
headless: deviceConfig.headless, | ||
}; | ||
} | ||
@@ -64,17 +84,36 @@ | ||
await this._deviceRegistry.disposeDevice(udid); | ||
if (options.shutdown) { | ||
await this._simulatorLauncher.shutdown(udid); | ||
await this._doShutdown(udid); | ||
await this._deviceRegistry.unregisterDevice(udid); | ||
} else { | ||
await this._deviceRegistry.releaseDevice(udid); | ||
} | ||
} | ||
async cleanup() { | ||
if (this._shouldShutdown) { | ||
const sessionDevices = await this._deviceRegistry.readSessionDevices(); | ||
const shutdownPromises = sessionDevices.getIds().map((udid) => this._doShutdown(udid)); | ||
await Promise.all(shutdownPromises); | ||
} | ||
await this._deviceRegistry.unregisterSessionDevices(); | ||
} | ||
/** | ||
* @param {string} udid | ||
* @returns {Promise<void>} | ||
* @private | ||
*/ | ||
async _doShutdown(udid) { | ||
try { | ||
await this._applesimutils.shutdown(udid); | ||
} catch (err) { | ||
log.warn({ err }, `Failed to shutdown simulator ${udid}`); | ||
} | ||
} | ||
/*** | ||
* @private | ||
* @param deviceQuery {{ | ||
* byId?: string; | ||
* byName?: string; | ||
* byType?: string; | ||
* byOS?: string; | ||
* }} | ||
* @param {SimulatorQuery} deviceQuery | ||
* @returns {Promise<String>} | ||
@@ -90,2 +129,3 @@ */ | ||
udid = this._applesimutils.create(prototypeDevice); | ||
await this._runScreenshotWorkaround(udid); | ||
} else { | ||
@@ -98,8 +138,27 @@ udid = free[0].udid; | ||
async _runScreenshotWorkaround(udid) { | ||
await this._applesimutils.takeScreenshot(udid, '/dev/null').catch(() => { | ||
log.debug({}, ` | ||
NOTE: For an unknown yet reason, taking the first screenshot is apt | ||
to fail when booting iOS Simulator in a hidden window mode (or on CI). | ||
Detox applies a workaround by taking a dummy screenshot to ensure | ||
that the future ones are going to work fine. This screenshot is not | ||
saved anywhere, and the error above is suppressed for all log levels | ||
except for "debug" and "trace." | ||
`.trim()); | ||
}); | ||
} | ||
/** | ||
* @private | ||
* @param {SimulatorQuery} deviceQuery | ||
*/ | ||
async _groupDevicesByStatus(deviceQuery) { | ||
const searchResults = await this._queryDevices(deviceQuery); | ||
const { rawDevices: takenDevices } = this._deviceRegistry.getRegisteredDevices(); | ||
const takenUDIDs = new Set(_.map(takenDevices, 'id')); | ||
const { taken, free } = _.groupBy(searchResults, ({ udid }) => takenUDIDs.has(udid) ? 'taken' : 'free'); | ||
const takenDevices = this._deviceRegistry.getTakenDevicesSync(); | ||
const { taken, free } = _.groupBy(searchResults, ({ udid }) => { | ||
return takenDevices.includes(udid) ? 'taken' : 'free'; | ||
}); | ||
const targetOS = _.get(taken, '0.os.identifier'); | ||
@@ -114,6 +173,10 @@ const isMatching = targetOS && { os: { identifier: targetOS } }; | ||
/** | ||
* @private | ||
* @param {SimulatorQuery} deviceQuery | ||
*/ | ||
async _queryDevices(deviceQuery) { | ||
const result = await this._applesimutils.list( | ||
deviceQuery, | ||
`Searching for device ${this._commentQuery(deviceQuery)} ...` | ||
`Searching for device ${deviceQuery} ...` | ||
); | ||
@@ -123,3 +186,3 @@ | ||
throw new DetoxRuntimeError({ | ||
message: `Failed to find a device ${this._commentQuery(deviceQuery)}`, | ||
message: `Failed to find a device ${deviceQuery}`, | ||
hint: `Run 'applesimutils --list' to list your supported devices. ` + | ||
@@ -131,26 +194,4 @@ `It is advised only to specify a device type, e.g., "iPhone Xʀ" and avoid explicit search by OS version.` | ||
} | ||
_adaptQuery({ id, name, os, type }) { | ||
return _.omitBy({ | ||
byId: id, | ||
byName: name, | ||
byOS: os, | ||
byType: type, | ||
}, _.isUndefined); | ||
} | ||
_commentQuery({ byId, byName, byOS, byType }) { | ||
return _.compact([ | ||
byId && `by UDID = ${JSON.stringify(byId)}`, | ||
byName && `by name = ${JSON.stringify(byName)}`, | ||
byType && `by type = ${JSON.stringify(byType)}`, | ||
byOS && `by OS = ${JSON.stringify(byOS)}`, | ||
]).join(' and '); | ||
} | ||
_commentDevice({ byId, byName, byOS, byType }) { | ||
return byId || _.compact([byName, byType, byOS]).join(', '); | ||
} | ||
} | ||
module.exports = SimulatorAllocDriver; |
@@ -5,7 +5,8 @@ // @ts-nocheck | ||
class AndroidEmulator extends DeviceAllocatorFactory { | ||
_createDriver({ eventEmitter }) { | ||
const serviceLocator = require('../../../servicelocator/android'); | ||
_createDriver({ detoxSession, detoxConfig }) { | ||
const serviceLocator = require('../../servicelocator/android'); | ||
const adb = serviceLocator.adb; | ||
const emulatorExec = serviceLocator.emulator.exec; | ||
const deviceRegistry = serviceLocator.deviceRegistry; | ||
const DeviceRegistry = require('../../allocation/DeviceRegistry'); | ||
const deviceRegistry = new DeviceRegistry({ sessionId: detoxSession.id }); | ||
@@ -24,8 +25,8 @@ const AVDsResolver = require('../drivers/android/emulator/AVDsResolver'); | ||
const FreePortFinder = require('../drivers/android/emulator/FreePortFinder'); | ||
const freePortFinder = new FreePortFinder(); | ||
const EmulatorLauncher = require('../drivers/android/emulator/EmulatorLauncher'); | ||
const emulatorLauncher = new EmulatorLauncher({ adb, emulatorExec, eventEmitter }); | ||
const emulatorLauncher = new EmulatorLauncher({ adb, emulatorExec }); | ||
const EmulatorAllocationHelper = require('../drivers/android/emulator/EmulatorAllocationHelper'); | ||
const allocationHelper = new EmulatorAllocationHelper(deviceRegistry, freeEmulatorFinder); | ||
const EmulatorAllocDriver = require('../drivers/android/emulator/EmulatorAllocDriver'); | ||
@@ -35,5 +36,8 @@ return new EmulatorAllocDriver({ | ||
avdValidator, | ||
detoxConfig, | ||
deviceRegistry, | ||
emulatorVersionResolver, | ||
emulatorLauncher, | ||
allocationHelper, | ||
freeDeviceFinder: freeEmulatorFinder, | ||
freePortFinder, | ||
}); | ||
@@ -44,15 +48,13 @@ } | ||
class AndroidAttached extends DeviceAllocatorFactory { | ||
_createDriver({ eventEmitter }) { | ||
const serviceLocator = require('../../../servicelocator/android'); | ||
_createDriver({ detoxSession, detoxConfig }) { | ||
const serviceLocator = require('../../servicelocator/android'); | ||
const adb = serviceLocator.adb; | ||
const deviceRegistry = serviceLocator.deviceRegistry; | ||
const DeviceRegistry = require('../../allocation/DeviceRegistry'); | ||
const deviceRegistry = new DeviceRegistry({ sessionId: detoxSession.id }); | ||
const FreeDeviceFinder = require('../../common/drivers/android/tools/FreeDeviceFinder'); | ||
const FreeDeviceFinder = require('../drivers/android/FreeDeviceFinder'); | ||
const freeDeviceFinder = new FreeDeviceFinder(adb, deviceRegistry); | ||
const AttachedAndroidLauncher = require('../drivers/android/attached/AttachedAndroidLauncher'); | ||
const attachedAndroidLauncher = new AttachedAndroidLauncher(eventEmitter); | ||
const AttachedAndroidAllocDriver = require('../drivers/android/attached/AttachedAndroidAllocDriver'); | ||
return new AttachedAndroidAllocDriver({ adb, deviceRegistry, freeDeviceFinder, attachedAndroidLauncher }); | ||
return new AttachedAndroidAllocDriver({ adb, deviceRegistry, freeDeviceFinder }); | ||
} | ||
@@ -62,35 +64,27 @@ } | ||
class Genycloud extends DeviceAllocatorFactory { | ||
_createDriver({ eventEmitter }) { | ||
const serviceLocator = require('../../../servicelocator/android'); | ||
_createDriver(deps) { | ||
const serviceLocator = require('../../servicelocator/android'); | ||
const adb = serviceLocator.adb; | ||
const exec = serviceLocator.genycloud.exec; | ||
const deviceRegistry = serviceLocator.genycloud.runtimeDeviceRegistry; | ||
const deviceCleanupRegistry = serviceLocator.genycloud.cleanupDeviceRegistry; | ||
const InstanceNaming = require('../../common/drivers/android/genycloud/services/GenyInstanceNaming'); | ||
const instanceNaming = new InstanceNaming(); // TODO should consider a permissive impl for debug/dev mode. Maybe even a custom arg in package.json (Detox > ... > genycloud > sharedAccount: false) | ||
const RecipesService = require('../../common/drivers/android/genycloud/services/GenyRecipesService'); | ||
const RecipesService = require('../drivers/android/genycloud/services/GenyRecipesService'); | ||
const recipeService = new RecipesService(exec); | ||
const InstanceLookupService = require('../../common/drivers/android/genycloud/services/GenyInstanceLookupService'); | ||
const instanceLookupService = new InstanceLookupService(exec, instanceNaming, deviceRegistry); | ||
const InstanceLifecycleService = require('../drivers/android/genycloud/services/GenyInstanceLifecycleService'); | ||
const instanceLifecycleService = new InstanceLifecycleService(exec); | ||
const InstanceLifecycleService = require('../../common/drivers/android/genycloud/services/GenyInstanceLifecycleService'); | ||
const instanceLifecycleService = new InstanceLifecycleService(exec, instanceNaming); | ||
const RecipeQuerying = require('../drivers/android/genycloud/GenyRecipeQuerying'); | ||
const recipeQuerying = new RecipeQuerying(recipeService); | ||
const InstanceAllocationHelper = require('../drivers/android/genycloud/GenyInstanceAllocationHelper'); | ||
const allocationHelper = new InstanceAllocationHelper({ deviceRegistry, instanceLookupService, instanceLifecycleService }); | ||
const InstanceLauncher = require('../drivers/android/genycloud/GenyInstanceLauncher'); | ||
const instanceLauncher = new InstanceLauncher({ genyCloudExec: exec, instanceLifecycleService }); | ||
const InstanceLauncher = require('../drivers/android/genycloud/GenyInstanceLauncher'); | ||
const GenyAllocDriver = require('../drivers/android/genycloud/GenyAllocDriver'); | ||
const instanceLauncher = new InstanceLauncher({ instanceLifecycleService, instanceLookupService, deviceCleanupRegistry, eventEmitter }); | ||
return new GenyAllocDriver({ | ||
adb, | ||
instanceLauncher, | ||
instanceLifecycleService, | ||
recipeQuerying, | ||
allocationHelper, | ||
instanceLauncher, | ||
...deps, | ||
}); | ||
@@ -97,0 +91,0 @@ } |
@@ -5,12 +5,11 @@ // @ts-nocheck | ||
class IosSimulator extends DeviceAllocatorFactory { | ||
_createDriver({ eventEmitter }) { | ||
const serviceLocator = require('../../../servicelocator/ios'); | ||
const applesimutils = serviceLocator.appleSimUtils; | ||
const deviceRegistry = serviceLocator.deviceRegistry; | ||
_createDriver({ detoxConfig, detoxSession, eventEmitter }) { | ||
const AppleSimUtils = require('../../../devices/common/drivers/ios/tools/AppleSimUtils'); | ||
const applesimutils = new AppleSimUtils(); | ||
const SimulatorLauncher = require('../drivers/ios/SimulatorLauncher'); | ||
const simulatorLauncher = new SimulatorLauncher({ applesimutils, eventEmitter }); | ||
const DeviceRegistry = require('../../../devices/allocation/DeviceRegistry'); | ||
const deviceRegistry = new DeviceRegistry({ sessionId: detoxSession.id }); | ||
const SimulatorAllocDriver = require('../drivers/ios/SimulatorAllocDriver'); | ||
return new SimulatorAllocDriver({ deviceRegistry, applesimutils, simulatorLauncher }); | ||
return new SimulatorAllocDriver({ detoxConfig, deviceRegistry, applesimutils }); | ||
} | ||
@@ -17,0 +16,0 @@ } |
@@ -35,9 +35,20 @@ const os = require('os'); | ||
class LaunchCommand extends ExecCommand { | ||
constructor(emulatorName, options) { | ||
constructor(options) { | ||
super(); | ||
this._options = options; | ||
this._args = this._getEmulatorArgs(emulatorName); | ||
this.port = options.port; | ||
this._args = this._getEmulatorArgs(); | ||
} | ||
get adbName() { | ||
return this._options.adbName; | ||
} | ||
get avdName() { | ||
return this._options.avdName; | ||
} | ||
get port() { | ||
return this._options.port; | ||
} | ||
_getArgs() { | ||
@@ -47,4 +58,5 @@ return this._args; | ||
_getEmulatorArgs(emulatorName) { | ||
_getEmulatorArgs() { | ||
const { | ||
avdName, | ||
bootArgs, | ||
@@ -69,3 +81,3 @@ gpuMode = this._getDefaultGPUMode(), | ||
...deviceBootArgs, | ||
`@${emulatorName}` | ||
`@${avdName}` | ||
]); | ||
@@ -72,0 +84,0 @@ |
@@ -21,2 +21,3 @@ // @ts-nocheck | ||
const { stdout } = await this.adbCmd('', 'devices', { verbosity: 'high' }); | ||
/** @type {DeviceHandle[]} */ | ||
const devices = _.chain(stdout) | ||
@@ -23,0 +24,0 @@ .trim() |
@@ -300,3 +300,3 @@ // @ts-nocheck | ||
(err.stderr.includes(`the app is not currently running`) || | ||
err.stderr.includes(`The operation couldn’t be completed. found nothing to terminate`))) { | ||
err.stderr.includes(`found nothing to terminate`))) { | ||
return; | ||
@@ -459,2 +459,4 @@ } | ||
overrides.push(`--cellularBars "${flags.cellularBars}"`); | ||
if (flags.operatorName) | ||
overrides.push(`--operatorName "${flags.operatorName}"`); | ||
if (flags.batteryState) | ||
@@ -461,0 +463,0 @@ overrides.push(`--batteryState "${flags.batteryState}"`); |
@@ -11,3 +11,3 @@ // @ts-nocheck | ||
* @typedef GenycloudDriverProps | ||
* @property instance { GenyInstance } The DTO associated with the cloud instance | ||
* @property adbName { GenyInstance } The DTO associated with the cloud instance | ||
*/ | ||
@@ -18,11 +18,12 @@ | ||
* @param deps { GenycloudDriverDeps } | ||
* @param props { GenycloudDriverProps } | ||
* @param props { GenycloudEmulatorCookie } | ||
*/ | ||
constructor(deps, { instance }) { | ||
super(deps, { adbName: instance.adbName }); | ||
this.instance = instance; | ||
constructor(deps, { adbName, name }) { | ||
super(deps, { adbName }); | ||
this._instanceName = name; | ||
} | ||
getDeviceName() { | ||
return this.instance.toString(); | ||
return this._instanceName; | ||
} | ||
@@ -29,0 +30,0 @@ |
@@ -17,3 +17,2 @@ // @ts-nocheck | ||
* @typedef SimulatorDriverDeps { DeviceDriverDeps } | ||
* @property simulatorLauncher { SimulatorLauncher } | ||
* @property applesimutils { AppleSimUtils } | ||
@@ -42,3 +41,2 @@ */ | ||
this._deviceName = `${udid} (${this._type})`; | ||
this._simulatorLauncher = deps.simulatorLauncher; | ||
this._applesimutils = deps.applesimutils; | ||
@@ -155,5 +153,8 @@ } | ||
async resetContentAndSettings() { | ||
await this._simulatorLauncher.shutdown(this.udid); | ||
await this.emitter.emit('beforeShutdownDevice', { deviceId: this.udid }); | ||
await this._applesimutils.shutdown(this.udid); | ||
await this.emitter.emit('shutdownDevice', { deviceId: this.udid }); | ||
await this._applesimutils.resetContentAndSettings(this.udid); | ||
await this._simulatorLauncher.launch(this.udid, this._type, this._bootArgs, this._headless); | ||
await this._applesimutils.boot(this.udid, this._bootArgs, this._headless); | ||
await this.emitter.emit('bootDevice', { deviceId: this.udid }); | ||
} | ||
@@ -160,0 +161,0 @@ |
@@ -6,3 +6,3 @@ /* eslint @typescript-eslint/no-unused-vars: ["error", { "args": "none" }] */ | ||
_createDriverDependencies(commonDeps) { | ||
const serviceLocator = require('../../../servicelocator/android'); | ||
const serviceLocator = require('../../servicelocator/android'); | ||
const adb = serviceLocator.adb; | ||
@@ -47,8 +47,4 @@ const aapt = serviceLocator.aapt; | ||
_createDriver(deviceCookie, deps, configs) { | ||
const props = { | ||
adbName: deviceCookie.adbName, | ||
}; | ||
const { AttachedAndroidRuntimeDriver } = require('../drivers'); | ||
return new AttachedAndroidRuntimeDriver(deps, props); | ||
return new AttachedAndroidRuntimeDriver(deps, deviceCookie); | ||
} | ||
@@ -59,8 +55,4 @@ } | ||
_createDriver(deviceCookie, deps, configs) { | ||
const props = { | ||
instance: deviceCookie.instance, | ||
}; | ||
const { GenycloudRuntimeDriver } = require('../drivers'); | ||
return new GenycloudRuntimeDriver(deps, props); | ||
return new GenycloudRuntimeDriver(deps, deviceCookie); | ||
} | ||
@@ -67,0 +59,0 @@ } |
@@ -5,11 +5,10 @@ const RuntimeDeviceFactory = require('./base'); | ||
_createDriverDependencies(commonDeps) { | ||
const serviceLocator = require('../../../servicelocator/ios'); | ||
const applesimutils = serviceLocator.appleSimUtils; | ||
const { eventEmitter } = commonDeps; | ||
const SimulatorLauncher = require('../../allocation/drivers/ios/SimulatorLauncher'); | ||
const AppleSimUtils = require('../../../devices/common/drivers/ios/tools/AppleSimUtils'); | ||
const applesimutils = new AppleSimUtils(); | ||
return { | ||
...commonDeps, | ||
applesimutils, | ||
simulatorLauncher: new SimulatorLauncher({ applesimutils, eventEmitter }), | ||
}; | ||
@@ -16,0 +15,0 @@ } |
@@ -5,5 +5,5 @@ // @ts-nocheck | ||
const runtimeDeviceFactories = require('./devices/runtime/factories'); | ||
const envValidationFactories = require('./devices/validation/factories'); | ||
const matchersFactories = require('./matchers/factories'); | ||
const resolveModuleFromPath = require('./utils/resolveModuleFromPath'); | ||
const envValidationFactories = require('./validation/factories'); | ||
@@ -40,11 +40,2 @@ function validateConfig(deviceConfig) { | ||
function createGlobalLifecycleHandler(deviceConfig) { | ||
if (deviceConfig.type === 'android.genycloud') { | ||
const FactoryClass = require('./devices/lifecycle/factories/GenyGlobalLifecycleHandlerFactory'); | ||
const factory = new FactoryClass(); | ||
return factory.createHandler(); | ||
} | ||
return null; | ||
} | ||
function _getFactoryClasses(deviceConfig) { | ||
@@ -120,3 +111,2 @@ let envValidatorFactoryClass; | ||
createFactories, | ||
createGlobalLifecycleHandler, | ||
}; |
const { IPC } = require('node-ipc'); | ||
const { DetoxInternalError } = require('../errors'); | ||
const { serializeObjectWithError } = require('../utils/errorUtils'); | ||
const { serializeObjectWithError, deserializeObjectWithError } = require('../utils/errorUtils'); | ||
@@ -63,2 +63,18 @@ class IPCClient { | ||
async allocateDevice() { | ||
const { deviceCookie, error } = deserializeObjectWithError(await this._emit('allocateDevice', {})); | ||
if (error) { | ||
throw error; | ||
} | ||
return deviceCookie; | ||
} | ||
async deallocateDevice(deviceCookie) { | ||
const { error } = deserializeObjectWithError(await this._emit('deallocateDevice', { deviceCookie })); | ||
if (error) { | ||
throw error; | ||
} | ||
} | ||
/** | ||
@@ -74,2 +90,7 @@ * @param {DetoxInternals.DetoxTestFileReport[]} testResults | ||
async conductEarlyTeardown() { | ||
const sessionState = await this._emit('conductEarlyTeardown', {}); | ||
this._sessionState.patch(sessionState); | ||
} | ||
async _connectToServer() { | ||
@@ -76,0 +97,0 @@ const serverId = this.serverId; |
@@ -11,7 +11,11 @@ const { uniqBy } = require('lodash'); | ||
* @param {Detox.Logger} options.logger | ||
* @param {object} options.callbacks | ||
* @param {() => Promise<any>} options.callbacks.onAllocateDevice | ||
* @param {(cookie: any) => Promise<void>} options.callbacks.onDeallocateDevice | ||
*/ | ||
constructor({ sessionState, logger }) { | ||
constructor({ sessionState, logger, callbacks }) { | ||
this._sessionState = sessionState; | ||
this._logger = logger.child({ cat: 'ipc,ipc-server' }); | ||
this._ipc = null; | ||
this._callbacks = callbacks; | ||
this._workers = new Set(); | ||
@@ -42,5 +46,8 @@ this._contexts = new Set(); | ||
this._ipc.serve(() => resolve()); | ||
this._ipc.server.on('conductEarlyTeardown', this.onConductEarlyTeardown.bind(this)); | ||
this._ipc.server.on('registerContext', this.onRegisterContext.bind(this)); | ||
this._ipc.server.on('registerWorker', this.onRegisterWorker.bind(this)); | ||
this._ipc.server.on('reportTestResults', this.onReportTestResults.bind(this)); | ||
this._ipc.server.on('allocateDevice', this.onAllocateDevice.bind(this)); | ||
this._ipc.server.on('deallocateDevice', this.onDeallocateDevice.bind(this)); | ||
this._ipc.server.start(); | ||
@@ -88,2 +95,34 @@ }); | ||
onConductEarlyTeardown(_data = null, socket = null) { | ||
// Note that we don't save `unsafe_earlyTeardown` in the primary session state | ||
// because it's transient and needed only to make the workers quit early. | ||
const newState = { unsafe_earlyTeardown: true }; | ||
if (socket) { | ||
this._ipc.server.emit(socket, 'conductEarlyTeardownDone', newState); | ||
} | ||
this._ipc.server.broadcast('sessionStateUpdate', newState); | ||
} | ||
async onAllocateDevice(_payload, socket) { | ||
let deviceCookie; | ||
try { | ||
deviceCookie = await this._callbacks.onAllocateDevice(); | ||
this._ipc.server.emit(socket, 'allocateDeviceDone', { deviceCookie }); | ||
} catch (error) { | ||
this._ipc.server.emit(socket, 'allocateDeviceDone', serializeObjectWithError({ error })); | ||
} | ||
} | ||
async onDeallocateDevice({ deviceCookie }, socket) { | ||
try { | ||
await this._callbacks.onDeallocateDevice(deviceCookie); | ||
this._ipc.server.emit(socket, 'deallocateDeviceDone', {}); | ||
} catch (error) { | ||
this._ipc.server.emit(socket, 'deallocateDeviceDone', serializeObjectWithError({ error })); | ||
} | ||
} | ||
onReportTestResults({ testResults }, socket = null) { | ||
@@ -90,0 +129,0 @@ const merged = uniqBy([ |
@@ -23,2 +23,3 @@ const vm = require('vm'); | ||
this.testSessionIndex = testSessionIndex; | ||
this.unsafe_earlyTeardown = undefined; | ||
this.workersCount = workersCount; | ||
@@ -25,0 +26,0 @@ } |
@@ -252,3 +252,3 @@ const path = require('path'); | ||
const args = maybeAction === action ? [maybeContext, maybeMessage] : [maybeContext]; | ||
const { context, msg } = this._parseArgs(null, args); | ||
const { context, msg } = this._parseArgs({ ph: 'B' }, args); | ||
const end = (ctx) => this[level].end({ | ||
@@ -261,3 +261,3 @@ id: context.id, | ||
let result; | ||
this._beginInternal(level, { ...context, ph: 'B' }, msg); | ||
this._beginInternal(level, context, msg); | ||
try { | ||
@@ -264,0 +264,0 @@ result = typeof action === 'function' |
@@ -104,2 +104,4 @@ const funpermaproxy = require('funpermaproxy'); | ||
[symbols.reportTestResults](_testResults) {} | ||
/** @abstract */ | ||
[symbols.conductEarlyTeardown]() {} | ||
/** | ||
@@ -153,2 +155,8 @@ * @abstract | ||
/** @abstract */ | ||
async [symbols.allocateDevice]() {} | ||
/** @abstract */ | ||
async [symbols.deallocateDevice]() {} | ||
async [symbols.uninstallWorker]() { | ||
@@ -155,0 +163,0 @@ try { |
@@ -27,2 +27,3 @@ const funpermaproxy = require('funpermaproxy'); | ||
this.tracing = context[symbols.tracing]; | ||
this.unsafe_conductEarlyTeardown = context[symbols.conductEarlyTeardown]; | ||
this.worker = funpermaproxy(() => context[symbols.worker]); | ||
@@ -29,0 +30,0 @@ } |
@@ -19,5 +19,3 @@ const { URL } = require('url'); | ||
//#region Private symbols | ||
const _globalLifecycleHandler = Symbol('globalLifecycleHandler'); | ||
const _ipcServer = Symbol('ipcServer'); | ||
const _resetLockFile = Symbol('resetLockFile'); | ||
const _wss = Symbol('wss'); | ||
@@ -29,2 +27,3 @@ const _dirty = Symbol('dirty'); | ||
const _logFinalError = Symbol('logFinalError'); | ||
const _deviceAllocator = Symbol('deviceAllocator'); | ||
//#endregion | ||
@@ -38,3 +37,4 @@ | ||
this[_wss] = null; | ||
this[_globalLifecycleHandler] = null; | ||
this[_deviceAllocator] = null; | ||
/** Path to file where the initial session object is serialized */ | ||
@@ -57,2 +57,8 @@ this[_sessionFile] = ''; | ||
[symbols.conductEarlyTeardown] = async () => { | ||
if (this[_ipcServer]) { | ||
await this[_ipcServer].onConductEarlyTeardown(); | ||
} | ||
}; | ||
async [symbols.resolveConfig](opts = {}) { | ||
@@ -86,3 +92,2 @@ const session = this[$sessionState]; | ||
const { | ||
behavior: behaviorConfig, | ||
device: deviceConfig, | ||
@@ -104,2 +109,6 @@ logger: loggerConfig, | ||
logger: this[symbols.logger], | ||
callbacks: { | ||
onAllocateDevice: this[symbols.allocateDevice].bind(this), | ||
onDeallocateDevice: this[symbols.deallocateDevice].bind(this), | ||
}, | ||
}); | ||
@@ -110,11 +119,10 @@ | ||
const environmentFactory = require('../environmentFactory'); | ||
this[_globalLifecycleHandler] = await environmentFactory.createGlobalLifecycleHandler(deviceConfig); | ||
if (this[_globalLifecycleHandler]) { | ||
await this[_globalLifecycleHandler].globalInit(); | ||
} | ||
const { deviceAllocatorFactory } = environmentFactory.createFactories(deviceConfig); | ||
this[_deviceAllocator] = deviceAllocatorFactory.createDeviceAllocator({ | ||
detoxConfig, | ||
detoxSession: this[$sessionState], | ||
}); | ||
if (!behaviorConfig.init.keepLockFile) { | ||
await this[_resetLockFile](); | ||
} | ||
await this[_deviceAllocator].init(); | ||
@@ -164,2 +172,25 @@ // TODO: Detox-server creation ought to be delegated to a generator/factory. | ||
/** @override */ | ||
async [symbols.allocateDevice]() { | ||
const { device } = this[$sessionState].detoxConfig; | ||
const deviceCookie = await this[_deviceAllocator].allocate(device); | ||
try { | ||
return await this[_deviceAllocator].postAllocate(deviceCookie); | ||
} catch (e) { | ||
try { | ||
await this[_deviceAllocator].free(deviceCookie, { shutdown: true }); | ||
} catch (e2) { | ||
this[symbols.logger].error({ cat: 'device', err: e2 }, `Failed to free ${deviceCookie.name || deviceCookie.id} after a failed allocation`); | ||
} | ||
throw e; | ||
} | ||
} | ||
/** @override */ | ||
async [symbols.deallocateDevice](cookie) { | ||
await this[_deviceAllocator].free(cookie); | ||
} | ||
/** @override */ | ||
async [symbols.cleanup]() { | ||
@@ -171,5 +202,5 @@ try { | ||
} finally { | ||
if (this[_globalLifecycleHandler]) { | ||
await this[_globalLifecycleHandler].globalCleanup(); | ||
this[_globalLifecycleHandler] = null; | ||
if (this[_deviceAllocator]) { | ||
await this[_deviceAllocator].cleanup(); | ||
this[_deviceAllocator] = null; | ||
} | ||
@@ -208,5 +239,5 @@ | ||
if (this[_globalLifecycleHandler]) { | ||
this[_globalLifecycleHandler].emergencyCleanup(); | ||
this[_globalLifecycleHandler] = null; | ||
if (this[_deviceAllocator]) { | ||
this[_deviceAllocator].emergencyCleanup(); | ||
this[_deviceAllocator] = null; | ||
} | ||
@@ -254,29 +285,4 @@ | ||
//#endregion | ||
//#region Private members | ||
async[_resetLockFile]() { | ||
const DeviceRegistry = require('../devices/DeviceRegistry'); | ||
const deviceType = this[symbols.config].device.type; | ||
switch (deviceType) { | ||
case 'ios.none': | ||
case 'ios.simulator': | ||
await DeviceRegistry.forIOS().reset(); | ||
break; | ||
case 'android.attached': | ||
case 'android.emulator': | ||
case 'android.genycloud': | ||
await DeviceRegistry.forAndroid().reset(); | ||
break; | ||
} | ||
if (deviceType === 'android.genycloud') { | ||
const GenyDeviceRegistryFactory = require('../devices/allocation/drivers/android/genycloud/GenyDeviceRegistryFactory'); | ||
await GenyDeviceRegistryFactory.forGlobalShutdown().reset(); | ||
} | ||
} | ||
//#endregion | ||
} | ||
module.exports = DetoxPrimaryContext; |
@@ -36,2 +36,10 @@ const fs = require('fs-extra'); | ||
[symbols.conductEarlyTeardown] = async () => { | ||
if (this[_ipcClient]) { | ||
await this[_ipcClient].conductEarlyTeardown(); | ||
} else { | ||
throw new DetoxInternalError('Detected an attempt to report early teardown using a non-initialized context.'); | ||
} | ||
}; | ||
async [symbols.resolveConfig]() { | ||
@@ -59,2 +67,21 @@ return this[symbols.config]; | ||
/** @override */ | ||
async [symbols.allocateDevice]() { | ||
if (this[_ipcClient]) { | ||
const deviceCookie = await this[_ipcClient].allocateDevice(); | ||
return deviceCookie; | ||
} else { | ||
throw new DetoxInternalError('Detected an attempt to allocate a device using a non-initialized context.'); | ||
} | ||
} | ||
/** @override */ | ||
async [symbols.deallocateDevice](deviceCookie) { | ||
if (this[_ipcClient]) { | ||
await this[_ipcClient].deallocateDevice(deviceCookie); | ||
} else { | ||
throw new DetoxInternalError('Detected an attempt to allocate a device using a non-initialized context.'); | ||
} | ||
} | ||
/** @override */ | ||
async [symbols.cleanup]() { | ||
@@ -61,0 +88,0 @@ try { |
@@ -8,2 +8,4 @@ /** | ||
* readonly installWorker: unique symbol; | ||
* readonly allocateDevice: unique symbol; | ||
* readonly deallocateDevice: unique symbol; | ||
* readonly logger: unique symbol; | ||
@@ -16,2 +18,3 @@ * readonly onHookFailure: unique symbol; | ||
* readonly onTestStart: unique symbol; | ||
* readonly conductEarlyTeardown: unique symbol; | ||
* readonly reportTestResults: unique symbol; | ||
@@ -37,2 +40,5 @@ * readonly resolveConfig: unique symbol; | ||
reportTestResults: Symbol('reportTestResults'), | ||
conductEarlyTeardown: Symbol('conductEarlyTeardown'), | ||
allocateDevice: Symbol('allocateDevice'), | ||
deallocateDevice: Symbol('deallocateDevice'), | ||
//#endregion | ||
@@ -39,0 +45,0 @@ |
@@ -22,5 +22,4 @@ const fs = require('fs'); | ||
Go to https://developer.android.com/studio/command-line/variables.html for more details`; | ||
const DEVICE_LOCK_FILE_PATH_IOS = path.join(DETOX_LIBRARY_ROOT_PATH, 'device.registry.state.lock'); | ||
const DEVICE_LOCK_FILE_PATH_ANDROID = path.join(DETOX_LIBRARY_ROOT_PATH, 'android-device.registry.state.lock'); | ||
const GENYCLOUD_GLOBAL_CLEANUP_FILE_PATH = path.join(DETOX_LIBRARY_ROOT_PATH, 'genycloud-cleanup.lock'); | ||
const DETOX_LOCK_FILE_PATH = path.join(DETOX_LIBRARY_ROOT_PATH, 'global-context.json'); | ||
const DEVICE_REGISTRY_PATH = path.join(DETOX_LIBRARY_ROOT_PATH, 'device.registry.json'); | ||
const LAST_FAILED_TESTS_PATH = path.join(DETOX_LIBRARY_ROOT_PATH, 'last-failed.txt'); | ||
@@ -199,15 +198,10 @@ | ||
function getDeviceLockFilePathIOS() { | ||
return DEVICE_LOCK_FILE_PATH_IOS; | ||
function getDetoxLockFilePath() { | ||
return DETOX_LOCK_FILE_PATH; | ||
} | ||
// TODO This can probably be merged with IOS' by now | ||
function getDeviceLockFilePathAndroid() { | ||
return DEVICE_LOCK_FILE_PATH_ANDROID; | ||
function getDeviceRegistryPath() { | ||
return DEVICE_REGISTRY_PATH; | ||
} | ||
function getGenyCloudGlobalCleanupFilePath() { | ||
return GENYCLOUD_GLOBAL_CLEANUP_FILE_PATH; | ||
} | ||
function getLastFailedTestsPath() { | ||
@@ -234,7 +228,6 @@ return LAST_FAILED_TESTS_PATH; | ||
getDetoxLibraryRootPath, | ||
getDeviceLockFilePathIOS, | ||
getDeviceLockFilePathAndroid, | ||
getGenyCloudGlobalCleanupFilePath, | ||
getDetoxLockFilePath, | ||
getDeviceRegistryPath, | ||
getLastFailedTestsPath, | ||
getHomeDir, | ||
}; |
@@ -43,4 +43,4 @@ const { isError } = require('lodash'); | ||
function serializeObjectWithError(obj, errorKey) { | ||
if (obj[errorKey]) { | ||
function serializeObjectWithError(obj, errorKey = 'error') { | ||
if (obj[errorKey] instanceof Error) { | ||
return { ...obj, [errorKey]: serializeError(obj[errorKey]) }; | ||
@@ -52,3 +52,3 @@ } | ||
function deserializeObjectWithError(obj, errorKey) { | ||
function deserializeObjectWithError(obj, errorKey = 'error') { | ||
if (typeof obj[errorKey] === 'object' && !(obj[errorKey] instanceof Error)) { | ||
@@ -55,0 +55,0 @@ return { ...obj, [errorKey]: deserializeError(obj[errorKey]) }; |
{ | ||
"compilerOptions": { | ||
"module": "commonjs", | ||
"lib": ["es2018"], | ||
"target": "ES2018", | ||
"lib": ["es2022"], | ||
"target": "ES2022", | ||
"allowJs": true, | ||
"checkJs": true, | ||
"moduleResolution": "node", | ||
"moduleResolution": "node16", | ||
"resolveJsonModule": true, | ||
@@ -23,2 +23,4 @@ "esModuleInterop": true, | ||
"android", | ||
"allure-report", | ||
"allure-results", | ||
"coverage", | ||
@@ -25,0 +27,0 @@ "ios", |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses eval() which is a dangerous function. This prevents the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
8342809
563
22517
76
8