Skip to content

Commit

Permalink
Properly handle instrumentation args passed in as launch-args to devi…
Browse files Browse the repository at this point in the history
…ce.launchApp()
  • Loading branch information
d4vidi committed Dec 24, 2019
1 parent 7102c5c commit 65d0ad7
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 10 deletions.
12 changes: 9 additions & 3 deletions detox/android/detox/src/main/java/com/wix/detox/Detox.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
import androidx.annotation.NonNull;
import android.util.Base64;

import java.util.Arrays;
import java.util.List;

import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;

Expand Down Expand Up @@ -71,6 +74,7 @@ public final class Detox {
private static final String LAUNCH_ARGS_KEY = "launchArgs";
private static final String DETOX_URL_OVERRIDE_ARG = "detoxURLOverride";
private static final long ACTIVITY_LAUNCH_TIMEOUT = 10000L;
private static final List<String> RESERVED_INSTRUMENTATION_ARGS = Arrays.asList("class", "package", "func", "unit", "size", "perf", "debug", "log", "emma", "coverageFile");

private static ActivityTestRule sActivityTestRule;

Expand Down Expand Up @@ -234,11 +238,13 @@ private static void launchActivitySync(Intent intent) {
}

private static Bundle readLaunchArgs() {
final Bundle instrumArgs = InstrumentationRegistry.getArguments();
final Bundle instrumentationArgs = InstrumentationRegistry.getArguments();
final Bundle launchArgs = new Bundle();

for (String arg : instrumArgs.keySet()) {
launchArgs.putString(arg, decodeLaunchArgValue(arg, instrumArgs));
for (String arg : instrumentationArgs.keySet()) {
if (!RESERVED_INSTRUMENTATION_ARGS.contains(arg)) {
launchArgs.putString(arg, decodeLaunchArgValue(arg, instrumentationArgs));
}
}
return launchArgs;
}
Expand Down
31 changes: 26 additions & 5 deletions detox/src/devices/drivers/AndroidDriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ const fs = require('fs');
const URL = require('url').URL;
const _ = require('lodash');
const { encodeBase64 } = require('../../utils/encoding');
const log = require('../../utils/logger').child({ __filename });
const logger = require('../../utils/logger');
const log = logger.child({ __filename });
const invoke = require('../../invoke');
const InvocationManager = invoke.InvocationManager;
const ADB = require('../android/ADB');
Expand All @@ -22,6 +23,9 @@ const retry = require('../../utils/retry');
const { interruptProcess, spawnAndLog } = require('../../utils/exec');
const AndroidExpect = require('../../android/expect');

const reservedInstrumentationArgs = ['class', 'package', 'func', 'unit', 'size', 'perf', 'debug', 'log', 'emma', 'coverageFile'];
const isReservedInstrumentationArg = (arg) => reservedInstrumentationArgs.includes(arg);

class AndroidDriver extends DeviceDriverBase {
constructor(config) {
super(config);
Expand Down Expand Up @@ -202,7 +206,7 @@ class AndroidDriver extends DeviceDriverBase {
}

async _launchInstrumentationProcess(deviceId, bundleId, rawLaunchArgs) {
const launchArgs = this._prepareLaunchArgs(rawLaunchArgs);
const launchArgs = this._prepareLaunchArgs(rawLaunchArgs, true);
const additionalLaunchArgs = this._prepareLaunchArgs({debug: false});
const serverPort = new URL(this.client.configuration.server).port;
await this.adb.reverse(deviceId, serverPort);
Expand Down Expand Up @@ -252,13 +256,30 @@ class AndroidDriver extends DeviceDriverBase {
return this.invocationManager.execute(DetoxApi.launchMainActivity());
}

_prepareLaunchArgs(launchArgs) {
return _.reduce(launchArgs, (result, value, key) => {
_prepareLaunchArgs(launchArgs, verbose = false) {
const usedReservedArgs = [];
const preparedLaunchArgs = _.reduce(launchArgs, (result, value, key) => {
const valueAsString = _.isString(value) ? value : JSON.stringify(value);
const valueEncoded = (key.startsWith('detox')) ? valueAsString : encodeBase64(valueAsString);

let valueEncoded = valueAsString;
if (isReservedInstrumentationArg(key)) {
usedReservedArgs.push(key);
} else if (!key.startsWith('detox')) {
valueEncoded = encodeBase64(valueAsString);
}

result.push('-e', key, valueEncoded);
return result;
}, []);

if (verbose && usedReservedArgs.length) {
logger.warn([`Arguments [${usedReservedArgs}] were passed in as launchArgs to device.launchApp() `,
'but are reserved to Android\'s test-instrumentation and will not be passed into the app. ',
'Ignore this message if this is what you meant to do. Refer to ',
'https://developer.android.com/studio/test/command-line#AMOptionsSyntax for ',
'further details.'].join(''));
}
return preparedLaunchArgs;
}
}

Expand Down
73 changes: 71 additions & 2 deletions detox/src/devices/drivers/AndroidDriver.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ describe('Android driver', () => {
const deviceId = 'device-id-mock';
const bundleId = 'bundle-id-mock';

let logger;
let exec;
beforeEach(() => {
jest.mock('../../utils/encoding', () => ({
Expand All @@ -14,6 +15,15 @@ describe('Android driver', () => {
jest.mock('../../utils/sleep', () => jest.fn().mockResolvedValue(''));
jest.mock('../../utils/retry', () => jest.fn().mockResolvedValue(''));

const mockLogger = {
warn: jest.fn(),
};
jest.mock('../../utils/logger', () => ({
child: () => mockLogger,
...mockLogger,
}));
logger = require('../../utils/logger');

jest.mock('../../utils/exec', () => ({
spawnAndLog: jest.fn().mockReturnValue({
childProcess: {
Expand Down Expand Up @@ -43,6 +53,7 @@ describe('Android driver', () => {
expect(spawnedFlags[index]).toEqual('-e');
expect(spawnedFlags[index + 1]).toEqual(key);
expect(spawnedFlags[index + 2]).toEqual(value);
return index + 3;
}
}),
});
Expand All @@ -62,15 +73,73 @@ describe('Android driver', () => {
const spawnArgs = exec.spawnAndLog.mock.calls[0];
const spawnedFlags = spawnArgs[1];

expectSpawnedFlag(spawnedFlags).startingIndex(7).toBe({
let index = 7;
index = expectSpawnedFlag(spawnedFlags).startingIndex(index).toBe({
key: 'object-arg',
value: 'base64({"such":"wow","much":"amaze","very":111})'
});
expectSpawnedFlag(spawnedFlags).startingIndex(10).toBe({
expectSpawnedFlag(spawnedFlags).startingIndex(index).toBe({
key: 'string-arg',
value: 'base64(text, with commas-and-dashes,)'
});
});

// Ref: https://developer.android.com/studio/test/command-line#AMOptionsSyntax
it('should whitelist reserved instrumentation args with respect to base64 encoding', async () => {
const launchArgs = {
// Free arg
'user-arg': 'merry christ-nukah',

// Reserved instrumentation args
'class': 'class-value',
'package': 'package-value',
'func': 'func-value',
'unit': 'unit-value',
'size': 'size-value',
'perf': 'perf-value',
'debug': 'debug-value',
'log': 'log-value',
'emma': 'emma-value',
'coverageFile': 'coverageFile-value',
};

await uut.launchApp(deviceId, bundleId, launchArgs, '');

const spawnArgs = exec.spawnAndLog.mock.calls[0];
const spawnedFlags = spawnArgs[1];

let index = 10;
index = expectSpawnedFlag(spawnedFlags).startingIndex(index).toBe({ key: 'class', value: 'class-value' });
index = expectSpawnedFlag(spawnedFlags).startingIndex(index).toBe({ key: 'package', value: 'package-value' });
index = expectSpawnedFlag(spawnedFlags).startingIndex(index).toBe({ key: 'func', value: 'func-value' });
index = expectSpawnedFlag(spawnedFlags).startingIndex(index).toBe({ key: 'unit', value: 'unit-value' });
index = expectSpawnedFlag(spawnedFlags).startingIndex(index).toBe({ key: 'size', value: 'size-value' });
index = expectSpawnedFlag(spawnedFlags).startingIndex(index).toBe({ key: 'perf', value: 'perf-value' });
index = expectSpawnedFlag(spawnedFlags).startingIndex(index).toBe({ key: 'debug', value: 'debug-value' });
index = expectSpawnedFlag(spawnedFlags).startingIndex(index).toBe({ key: 'log', value: 'log-value' });
index = expectSpawnedFlag(spawnedFlags).startingIndex(index).toBe({ key: 'emma', value: 'emma-value' });
expectSpawnedFlag(spawnedFlags).startingIndex(index).toBe({ key: 'coverageFile', value: 'coverageFile-value' });
});

it('should log reserved instrumentation args usage warning, if such have been used', async () => {
const launchArgs = {
'class': 'class-value',
};

await uut.launchApp(deviceId, bundleId, launchArgs, '');

expect(logger.warn).toHaveBeenCalled();
});

it('should NOT log instrumentation args usage warning, if none used', async () => {
const launchArgs = {
'user-arg': 'merry christ-nukah',
};

await uut.launchApp(deviceId, bundleId, launchArgs, '');

expect(logger.warn).not.toHaveBeenCalled();
});
});

describe('net-port reversing', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ describe(':android: Launch arguments', () => {
await expect(element(by.id(`launchArg-${key}.value`))).toHaveText(expectedValue);
}

async function assertNoLaunchArg(launchArgKey) {
await expect(element(by.id(`launchArg-${launchArgKey}.name`))).toBeNotVisible();
}

it('should handle primitive args', async () => {
const launchArgs = {
hello: 'world',
Expand Down Expand Up @@ -40,4 +44,22 @@ describe(':android: Launch arguments', () => {
await assertLaunchArg(launchArgs, 'complex', JSON.stringify(launchArgs.complex));
await assertLaunchArg(launchArgs, 'complexlist', JSON.stringify(launchArgs.complexlist));
});

// Ref: https://developer.android.com/studio/test/command-line#AMOptionsSyntax
it('should not pass android instrumentation args through', async () => {
const launchArgs = {
hello: 'world',
debug: false,
log: false,
size: 'large',
};

await device.launchApp({newInstance: true, launchArgs});

await element(by.text('Launch Args')).tap();
await assertLaunchArg(launchArgs, 'hello', 'world');
await assertNoLaunchArg('debug');
await assertNoLaunchArg('log');
await assertNoLaunchArg('size');
});
});

0 comments on commit 65d0ad7

Please sign in to comment.