Skip to content

Commit

Permalink
Merge pull request #200 from wix/app_logs
Browse files Browse the repository at this point in the history
App logs
  • Loading branch information
silyevsk authored Jul 17, 2017
2 parents d7b2c3d + b736d65 commit 308e3ae
Show file tree
Hide file tree
Showing 18 changed files with 370 additions and 12 deletions.
5 changes: 4 additions & 1 deletion detox/local-cli/detox-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ program
.option('-d, --debug-synchronization [value]',
'When an action/expectation takes a significant amount of time use this option to print device synchronization status. '
+ 'The status will be printed if the action takes more than [value]ms to complete')
.option('-a, --artifacts-location [path]', 'Artifacts destination path (currently will contain only logs). '
+ 'If the destination already exists, it will be removed first')
.parse(process.argv);

const config = require(path.join(process.cwd(), 'package.json')).detox;
Expand All @@ -23,6 +25,7 @@ const loglevel = program.loglevel ? `--loglevel ${program.loglevel}` : '';
const configuration = program.configuration ? `--configuration ${program.configuration}` : '';
const cleanup = program.cleanup ? `--cleanup` : '';
const reuse = program.reuse ? `--reuse` : '';
const artifactsLocation = program.artifactsLocation ? `--artifacts-location ${program.artifactsLocation}` : '';

if (typeof program.debugSynchronization === "boolean") {
program.debugSynchronization = 3000;
Expand All @@ -33,7 +36,7 @@ let debugSynchronization = program.debugSynchronization ? `--debug-synchronizati
let command;
switch (program.runner) {
case 'mocha':
command = `node_modules/.bin/${program.runner} ${testFolder} --opts ${testFolder}/${program.runnerConfig} ${configuration} ${loglevel} ${cleanup} ${reuse} ${debugSynchronization}`;
command = `node_modules/.bin/${program.runner} ${testFolder} --opts ${testFolder}/${program.runnerConfig} ${configuration} ${loglevel} ${cleanup} ${reuse} ${debugSynchronization} ${artifactsLocation}`;
break;
default:
throw new Error(`${program.runner} is not supported in detox cli tools. You can still run your tests with the runner's own cli tool`);
Expand Down
1 change: 1 addition & 0 deletions detox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"eslint-plugin-react-native": "^2.3.2",
"jest": "^18.1.0",
"minimist": "^1.2.0",
"mockdate": "^2.0.1",
"shelljs": "^0.7.3",
"ttab": "^0.3.1"
},
Expand Down
26 changes: 25 additions & 1 deletion detox/src/Detox.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const Client = require('./client/Client');
const DetoxServer = require('detox-server');
const URL = require('url').URL;
const _ = require('lodash');
const ArtifactsPathsProvider = require('./artifacts/ArtifactsPathsProvider');

log.level = argparse.getArgValue('loglevel') || 'info';
log.addLevel('wss', 999, {fg: 'blue', bg: 'black'}, 'wss');
Expand All @@ -30,6 +31,15 @@ class Detox {
this.userConfig = userConfig;
this.client = null;
this.device = null;
this._currentTestNumber = 0;
const artifactsLocation = argparse.getArgValue('artifacts-location');
if(artifactsLocation !== undefined) {
try {
this._artifactsPathsProvider = new ArtifactsPathsProvider(artifactsLocation);
} catch(ex) {
log.warn(ex);
}
}
}

async init() {
Expand Down Expand Up @@ -70,11 +80,25 @@ class Detox {
await this.client.cleanup();
}

if (argparse.getArgValue('cleanup')) {
if (argparse.getArgValue('cleanup') && this.device) {
await this.device.shutdown();
}
}

async beforeEach(...testNameComponents) {
this._currentTestNumber++;
if(this._artifactsPathsProvider !== undefined) {
const testArtifactsPath = this._artifactsPathsProvider.createPathForTest(this._currentTestNumber, ...testNameComponents)
this.device.setArtifactsDestination(testArtifactsPath);
}
}

async afterEach(suiteName, testName) {
if(this._artifactsPathsProvider !== undefined) {
await this.device.finalizeArtifacts();
}
}

async _chooseSession(deviceConfig) {
let session = deviceConfig.session;
let shouldStartServer = false;
Expand Down
42 changes: 42 additions & 0 deletions detox/src/Detox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe('Detox', () => {
jest.setMock(modulePath, FinalMock);
}

jest.mock('npmlog');
jest.mock('fs');
fs = require('fs');
jest.mock('minimist');
Expand Down Expand Up @@ -192,6 +193,47 @@ describe('Detox', () => {
expect(clientMockData.lastConstructorArguments[0]).toBe(expectedSession);
});

it(`beforeEach() - should set device artifacts destination`, async () => {
mockCommandLineArgs({'artifacts-location': '/tmp'});
Detox = require('./Detox');
detox = new Detox(schemes.validOneDeviceAndSession);
await detox.init();
await detox.beforeEach('a', 'b', 'c');
expect(device.setArtifactsDestination).toHaveBeenCalledTimes(1);
});

it(`beforeEach() - should not set device artifacts destination if artifacts not set in cli args`, async () => {
Detox = require('./Detox');
detox = new Detox(schemes.validOneDeviceAndSession);
await detox.init();
await detox.beforeEach('a', 'b', 'c');
expect(device.setArtifactsDestination).toHaveBeenCalledTimes(0);
});

it(`afterEach() - should call device.finalizeArtifacts`, async () => {
mockCommandLineArgs({'artifacts-location': '/tmp'});
Detox = require('./Detox');
detox = new Detox(schemes.validOneDeviceAndSession);
await detox.init();
await detox.afterEach();
expect(device.finalizeArtifacts).toHaveBeenCalledTimes(1);
});

it(`afterEach() - should not call device.finalizeArtifacts if artifacts not set in cli arg`, async () => {
Detox = require('./Detox');
detox = new Detox(schemes.validOneDeviceAndSession);
await detox.init();
await detox.afterEach();
expect(device.finalizeArtifacts).toHaveBeenCalledTimes(0);
});

it(`the constructor should catch exception from ArtifactsPathsProvider`, async () => {
mockCommandLineArgs({'artifacts-location': '/tmp'});
fs.mkdirSync = jest.fn(() => {throw 'Could not create artifacts root dir'});
Detox = require('./Detox');
detox = new Detox(schemes.validOneDeviceAndSession);
});

function mockCommandLineArgs(args) {
minimist.mockReturnValue(args);
}
Expand Down
56 changes: 56 additions & 0 deletions detox/src/artifacts/ArtifactsCopier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const log = require('npmlog');
const sh = require('../utils/sh')

class ArtifactsCopier {
constructor(deviceDriver) {
this._deviceDriver = deviceDriver;
this._currentLaunchNumber = 0;
this._currentTestArtifactsDestination = undefined;
}

prepare(deviceId) {
this._deviceId = deviceId;
}

setArtifactsDestination(artifactsDestination) {
this._currentTestArtifactsDestination = artifactsDestination;
this._currentLaunchNumber = 1;
}

async handleAppRelaunch() {
await this._copyArtifacts();
this._currentLaunchNumber++;
}

async finalizeArtifacts() {
await this._copyArtifacts();
}

async _copyArtifacts() {
const copy = async (sourcePath, destinationSuffix) => {
const destinationPath = `${this._currentTestArtifactsDestination}/${this._currentLaunchNumber}.${destinationSuffix}`;
const cpArgs = `"${sourcePath}" "${destinationPath}"`;
try {
await sh.cp(cpArgs);
} catch (ex) {
log.warn(`Couldn't copy (cp ${cpArgs})`);
}
}

if(this._currentTestArtifactsDestination === undefined) {
return;
}

const {stdout, stderr} = this._deviceDriver.getLogsPaths(this._deviceId);
const pathsMapping = [
[stdout, 'out.log'],
[stderr, 'err.log']
];
for (const [sourcePath, destinationSuffix] of pathsMapping) {
await copy(sourcePath, destinationSuffix);
}
}

}

module.exports = ArtifactsCopier;
36 changes: 36 additions & 0 deletions detox/src/artifacts/ArtifactsPathsProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const fs = require('fs');
const _ = require('lodash');
const log = require('npmlog');

class ArtifactsPathsProvider {
constructor(destinationParent) {
if(!destinationParent) {
throw new Error('destinationParent should not be undefined');
}
this._destinationRoot = `${destinationParent}/detox_artifacts.${new Date().toISOString()}`;
try {
fs.mkdirSync(this._destinationRoot);
} catch (ex) {
throw new Error(`Could not create artifacts root dir: ${this._destinationRoot}`);
}
}

createPathForTest(number, ...nameComponents) {
if(number !== parseInt(number) || number <= 0) {
throw new Error('The number should be a positive integer');
}

const lastPathComponent = [number].concat(nameComponents).join('.');
const pathForTest = `${this._destinationRoot}/${lastPathComponent}`;
try {
fs.mkdirSync(pathForTest);
} catch (ex) {
log.warn(`Could not create artifacts test dir: ${pathForTest}`);
return undefined;
}

return pathForTest;
}
}

module.exports = ArtifactsPathsProvider;
66 changes: 66 additions & 0 deletions detox/src/artifacts/ArtifactsPathsProvider.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const _ = require('lodash');

describe.only('ArtifactsPathsProvider', () => {
let ArtifactsPathsProvider;
let fs;
let mockedDateString = '2017-07-13T06:31:48.544Z';
let log;

beforeEach(() => {
jest.mock('fs');
fs = require('fs');
jest.mock('npmlog');
log = require('npmlog');

ArtifactsPathsProvider = require('./ArtifactsPathsProvider');
require('mockdate').set(new Date(mockedDateString));
});

it('constructor - should throw on undefined destinationRoot', () => {
expect(() => {
new ArtifactsPathsProvider();
}).toThrowError(/undefined/);
});

it('constructor - should throw if can\'t create run directory in the destination', () => {
fs.mkdirSync = jest.fn(() => {throw 'some'});
expect(() => {
new ArtifactsPathsProvider('/tmp');
}).toThrowError(/Could not create artifacts root dir/);
});

it('createPathForTest() - should throw on invalid number', () => {
function testForNumber(number) {
expect(() => {
(new ArtifactsPathsProvider('/tmp')).createPathForTest(number);
}).toThrowError(/should be a positive integer/);
}
testForNumber(undefined);
testForNumber('1');
testForNumber(-2);
testForNumber(0);
testForNumber('1.2');
});

it('createPathForTest() - should return proper path for no components', () => {
expect((new ArtifactsPathsProvider('/tmp')).createPathForTest(1)).
toEqual(`/tmp/detox_artifacts.${mockedDateString}/1`);
});

it('createPathForTest() - should return proper path for no 1 component', () => {
expect((new ArtifactsPathsProvider('/tmp')).createPathForTest(1, 'a')).
toEqual(`/tmp/detox_artifacts.${mockedDateString}/1.a`);
});

it('createPathForTest() - should return proper path for no 2 components', () => {
expect((new ArtifactsPathsProvider('/tmp')).createPathForTest(1, 'a', 'b')).
toEqual(`/tmp/detox_artifacts.${mockedDateString}/1.a.b`);
});

it('createPathsForTest() - should catch mkdirSync exception', () => {
const artifactsPathsProvider = new ArtifactsPathsProvider('/tmp');
fs.mkdirSync = jest.fn(() => {throw 'some'});
artifactsPathsProvider.createPathForTest(1);
expect(log.warn).toHaveBeenCalledWith('Could not create artifacts test dir: /tmp/detox_artifacts.2017-07-13T06:31:48.544Z/1');
});
});
15 changes: 15 additions & 0 deletions detox/src/devices/Device.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ const path = require('path');
const _ = require('lodash');
const argparse = require('../utils/argparse');
const configuration = require('../configuration');
const sh = require('../utils/sh');
const log = require('npmlog');
const ArtifactsCopier = require('../artifacts/ArtifactsCopier');

class Device {

constructor(deviceConfig, sessionConfig, deviceDriver) {
this._deviceConfig = deviceConfig;
this._sessionConfig = sessionConfig;
this.deviceDriver = deviceDriver;
this._artifactsCopier = new ArtifactsCopier(deviceDriver);

this.deviceDriver.validateDeviceConfig(deviceConfig);
}
Expand All @@ -18,12 +22,23 @@ class Device {
this._binaryPath = this._getAbsolutePath(this._deviceConfig.binaryPath);
this._deviceId = await this.deviceDriver.acquireFreeDevice(this._deviceConfig.name);
this._bundleId = await this.deviceDriver.getBundleIdFromBinary(this._binaryPath);
this._artifactsCopier.prepare(this._deviceId);

await this.deviceDriver.boot(this._deviceId);
await this.relaunchApp({delete: !argparse.getArgValue('reuse')});
}

setArtifactsDestination(testArtifactsPath) {
this._artifactsCopier.setArtifactsDestination(testArtifactsPath);
}

async finalizeArtifacts() {
await this._artifactsCopier.finalizeArtifacts();
}

async relaunchApp(params = {}, bundleId) {
await this._artifactsCopier.handleAppRelaunch();

if (params.url && params.userNotification) {
throw new Error(`detox can't understand this 'relaunchApp(${JSON.stringify(params)})' request, either request to launch with url or with userNotification, not both`);
}
Expand Down
31 changes: 31 additions & 0 deletions detox/src/devices/Device.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,20 @@ describe('Device', () => {
let Device;
let device;
let argparse;
let sh;

let Client;
let client;

beforeEach(async () => {
Device = require('./Device');

jest.mock('npmlog');

jest.mock('../utils/sh');
sh = require('../utils/sh');
sh.cp = jest.fn();

jest.mock('fs');
fs = require('fs');
jest.mock('../ios/expect');
Expand Down Expand Up @@ -323,4 +330,28 @@ describe('Device', () => {
}
});

it(`finalizeArtifacts() should call cp`, async () => {
device = validDevice();
device.deviceDriver.getLogsPaths = () => ({stdout: '/t1', stderr: '/t2'});
device.setArtifactsDestination('/tmp');
await device.finalizeArtifacts();
expect(sh.cp).toHaveBeenCalledTimes(2);
});

it(`finalizeArtifacts() should catch cp exception`, async () => {
device = validDevice();
device.deviceDriver.getLogsPaths = () => ({stdout: '/t1', stderr: '/t2'});
device.setArtifactsDestination('/tmp');
await device.relaunchApp();
sh.cp = jest.fn(() => {throw 'exception sent by mocked cp'});
await device.finalizeArtifacts();
});

it(`finalizeArtifacts() should not cp if setArtifactsDestination wasn't called`, async () => {
device = validDevice();
device.deviceDriver.getLogsPaths = () => ({stdout: '/t1', stderr: '/t2'});
await device.relaunchApp();
await device.finalizeArtifacts();
expect(sh.cp).toHaveBeenCalledTimes(0);
});
});
Loading

0 comments on commit 308e3ae

Please sign in to comment.