Skip to content

Commit dd45772

Browse files
Fix unit test runner
Fix the following issues in unit test runner: * Tests cannot start when application is not installed on device * Karma does not start when there are no .js files in app dir (initial state of TypeScript projects). * `tns test <platform> --watch` is not working for TypeScript projects * `tns test <platform> --debug-brk` does not debug. Change the way unit-tests are started. The current code was: 1) Start karma server 2) When start method of `karma-nativescript-launcher` is called, it spawns new CLI process (calls `tns dev-test` command with some arguments). 3) The new CLI process writes down some files in `node_modules/nativescript-unit-test-runner` 4) The new CLI process prepares the project. 5) The new CLI process changes the entry point of the application to point to nativescript-unit-test-runner's main-page. 6) The new CLI process calls livesync-base which should restart the application. 7) In case `--watch` option is used, karma launcher will listen for `file_list_modified` event and spawn new CLI process to run the tests. Problems were in all the steps. New way: 1) When `tns test <platform>` is called, first step is to prepare the project. 2) Initialize devices service, so we are sure all devices are detected. 3) Prepare livesync data - here we set canExecuteFastSync to false, so any change will restart the application and tests will be started again. 4) Fork new process, which should start karma server. 5) When `karma-nativescript-launcher`'s start method is called in the forked process, it will send required information to current CLI process. 6) CLI process receives the data and writes the required files in `node_modules/nativescript-unit-test-runner`. 7) CLI process calls livesync. In case --debug-brk is specified, debugService is called instead. 8) karma-nativescript-launcher no longer listens for `file-list-modified` event. CLI is alreday doing this (livesync logic). 9) Entry point of application is change by `nativescript-unit-test-runner` via after-prepare hook. The new behavior depends on the livesync - when app is not installed on the device, it will be installed. In case `--watch` is used, livesync will detect changes, prepare the project and restart the app - this will start the tests again. With this change `tns dev-test` command will stop working. As I consider it not-usable, I've deceided to skip its fixing for later.
1 parent 48a1a4e commit dd45772

File tree

6 files changed

+128
-42
lines changed

6 files changed

+128
-42
lines changed

lib/common

lib/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export let DEFAULT_APP_IDENTIFIER_PREFIX = "org.nativescript";
1414
export var LIVESYNC_EXCLUDED_DIRECTORIES = ["app_resources"];
1515
export var TESTING_FRAMEWORKS = ['jasmine', 'mocha', 'qunit'];
1616
export let TEST_RUNNER_NAME = "nativescript-unit-test-runner";
17+
export let LIVESYNC_EXCLUDED_FILE_PATTERNS = ["**/*.js.map", "**/*.ts"];
1718

1819
export class ReleaseType {
1920
static MAJOR = "major";

lib/services/karma-execution.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
///<reference path="../.d.ts"/>
2+
3+
"use strict";
4+
5+
import * as path from "path";
6+
7+
process.on("message", (data: any) => {
8+
if(data.karmaConfig) {
9+
let pathToKarma = path.join(data.karmaConfig.projectDir, 'node_modules/karma'),
10+
KarmaServer = require(path.join(pathToKarma, 'lib/server')),
11+
karma = new KarmaServer(data.karmaConfig);
12+
13+
karma.start();
14+
}
15+
});

lib/services/livesync/livesync-service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class LiveSyncService implements ILiveSyncService {
5858
appIdentifier: this.$projectData.projectId,
5959
projectFilesPath: path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME),
6060
syncWorkingDirectory: path.join(this.$projectData.projectDir, constants.APP_FOLDER_NAME),
61-
excludedProjectDirsAndFiles: ["**/*.js.map", "**/*.ts"]
61+
excludedProjectDirsAndFiles: constants.LIVESYNC_EXCLUDED_FILE_PATTERNS
6262
};
6363
this.$liveSyncServiceBase.sync(liveSyncData).wait();
6464
}).future<void>()();

lib/services/platform-service.ts

+2-7
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,6 @@ import Future = require("fibers/future");
1111
let clui = require("clui");
1212

1313
export class PlatformService implements IPlatformService {
14-
private static TNS_MODULES_FOLDER_NAME = "tns_modules";
15-
private static EXCLUDE_FILES_PATTERN = [
16-
"**/*.js.map",
17-
"**/*.ts"
18-
];
1914

2015
constructor(private $devicesService: Mobile.IDevicesService,
2116
private $errors: IErrors,
@@ -226,7 +221,7 @@ export class PlatformService implements IPlatformService {
226221
this.$xmlValidator.validateXmlFiles(sourceFiles).wait();
227222

228223
// Remove .ts and .js.map files
229-
PlatformService.EXCLUDE_FILES_PATTERN.forEach(pattern => sourceFiles = sourceFiles.filter(file => !minimatch(file, pattern, {nocase: true})));
224+
constants.LIVESYNC_EXCLUDED_FILE_PATTERNS.forEach(pattern => sourceFiles = sourceFiles.filter(file => !minimatch(file, pattern, {nocase: true})));
230225
let copyFileFutures = sourceFiles.map(source => {
231226
let destinationPath = path.join(appDestinationDirectoryPath, path.relative(appSourceDirectoryPath, source));
232227
if (this.$fs.getFsStats(source).wait().isDirectory()) {
@@ -250,7 +245,7 @@ export class PlatformService implements IPlatformService {
250245
// Process node_modules folder
251246
let appDir = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME);
252247
try {
253-
let tnsModulesDestinationPath = path.join(appDir, PlatformService.TNS_MODULES_FOLDER_NAME);
248+
let tnsModulesDestinationPath = path.join(appDir, constants.TNS_MODULES_FOLDER_NAME);
254249
this.$broccoliBuilder.prepareNodeModules(tnsModulesDestinationPath, platform, lastModifiedTime).wait();
255250
} catch(error) {
256251
this.$logger.debug(error);

lib/services/test-execution-service.ts

+108-33
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,17 @@ class TestExecutionService implements ITestExecutionService {
3232
private $options: IOptions,
3333
private $pluginsService: IPluginsService,
3434
private $errors: IErrors,
35-
private $devicesService: Mobile.IDevicesService) {
35+
private $androidDebugService:IDebugService,
36+
private $iOSDebugService: IDebugService,
37+
private $devicesService: Mobile.IDevicesService,
38+
private $childProcess: IChildProcess) {
3639
}
3740

41+
public platform: string;
42+
3843
public startTestRunner(platform: string) : IFuture<void> {
3944
return (() => {
45+
this.platform = platform;
4046
this.$options.justlaunch = true;
4147
let blockingOperationFuture = new Future<void>();
4248
process.on('message', (launcherConfig: any) => {
@@ -50,7 +56,7 @@ class TestExecutionService implements ITestExecutionService {
5056
let configOptions: IKarmaConfigOptions = JSON.parse(launcherConfig);
5157
this.$options.debugBrk = configOptions.debugBrk;
5258
this.$options.debugTransport = configOptions.debugTransport;
53-
let configJs = this.generateConfig(configOptions);
59+
let configJs = this.generateConfig(this.$options.port.toString(), configOptions);
5460
this.$fs.writeFile(path.join(projectDir, TestExecutionService.CONFIG_FILE_NAME), configJs).wait();
5561

5662
let socketIoJsUrl = `http://localhost:${this.$options.port}/socket.io/socket.io.js`;
@@ -93,37 +99,47 @@ class TestExecutionService implements ITestExecutionService {
9399
public startKarmaServer(platform: string): IFuture<void> {
94100
return (() => {
95101
platform = platform.toLowerCase();
96-
this.$pluginsService.ensureAllDependenciesAreInstalled().wait();
97-
let pathToKarma = path.join(this.$projectData.projectDir, 'node_modules/karma');
98-
let KarmaServer = require(path.join(pathToKarma, 'lib/server'));
99-
if (platform === 'ios' && this.$options.emulator) {
100-
platform = 'ios_simulator';
101-
}
102-
let karmaConfig: any = {
103-
browsers: [platform],
104-
configFile: path.join(this.$projectData.projectDir, 'karma.conf.js'),
105-
_NS: {
106-
log: this.$logger.getLevel(),
107-
path: this.$options.path,
108-
tns: process.argv[1],
109-
node: process.execPath,
110-
options: {
111-
debugTransport: this.$options.debugTransport,
112-
debugBrk: this.$options.debugBrk,
113-
}
114-
},
115-
};
116-
if (this.$config.DEBUG || this.$logger.getLevel() === 'TRACE') {
117-
karmaConfig.logLevel = 'DEBUG';
118-
}
119-
if (!this.$options.watch) {
120-
karmaConfig.singleRun = true;
102+
this.platform = platform;
103+
104+
if(this.$options.debugBrk && this.$options.watch) {
105+
this.$errors.failWithoutHelp("You cannot use --watch and --debug-brk simultaneously. Remove one of the flags and try again.");
121106
}
122-
if (this.$options.debugBrk) {
123-
karmaConfig.browserNoActivityTimeout = 1000000000;
107+
108+
if (!this.$platformService.preparePlatform(platform).wait()) {
109+
this.$errors.failWithoutHelp("Verify that listed files are well-formed and try again the operation.");
124110
}
125-
this.$logger.debug(JSON.stringify(karmaConfig, null, 4));
126-
new KarmaServer(karmaConfig).start();
111+
112+
let projectDir = this.$projectData.projectDir;
113+
this.$devicesService.initialize({ platform: platform, deviceId: this.$options.device }).wait();
114+
115+
let karmaConfig = this.getKarmaConfiguration(platform),
116+
karmaRunner = this.$childProcess.fork(path.join(__dirname, "karma-execution.js"));
117+
118+
karmaRunner.send({karmaConfig: karmaConfig});
119+
karmaRunner.on("message", (karmaData: any) => {
120+
fiberBootstrap.run(() => {
121+
this.$logger.trace("## Unit-testing: Parent process received message", karmaData);
122+
let port: string;
123+
if(karmaData.url) {
124+
port = karmaData.url.port;
125+
let socketIoJsUrl = `http://${karmaData.url.host}/socket.io/socket.io.js`;
126+
let socketIoJs = this.$httpClient.httpRequest(socketIoJsUrl).wait().body;
127+
this.$fs.writeFile(path.join(projectDir, TestExecutionService.SOCKETIO_JS_FILE_NAME), socketIoJs).wait();
128+
}
129+
130+
if(karmaData.launcherConfig) {
131+
let configOptions: IKarmaConfigOptions = JSON.parse(karmaData.launcherConfig);
132+
let configJs = this.generateConfig(port, configOptions);
133+
this.$fs.writeFile(path.join(projectDir, TestExecutionService.CONFIG_FILE_NAME), configJs).wait();
134+
}
135+
136+
if(this.$options.debugBrk) {
137+
this.getDebugService(platform).debug().wait();
138+
} else {
139+
this.liveSyncProject(platform).wait();
140+
}
141+
});
142+
});
127143
}).future<void>()();
128144
}
129145

@@ -138,8 +154,7 @@ class TestExecutionService implements ITestExecutionService {
138154
}).future<void>()();
139155
}
140156

141-
private generateConfig(options: any): string {
142-
let port = this.$options.port;
157+
private generateConfig(port: string, options: any): string {
143158
let nics = os.networkInterfaces();
144159
let ips = Object.keys(nics)
145160
.map(nicName => nics[nicName].filter((binding: any) => binding.family === 'IPv4' && !binding.internal)[0])
@@ -154,5 +169,65 @@ class TestExecutionService implements ITestExecutionService {
154169

155170
return 'module.exports = ' + JSON.stringify(config);
156171
}
172+
173+
private getDebugService(platform: string): IDebugService {
174+
let lowerCasedPlatform = platform.toLowerCase();
175+
if(lowerCasedPlatform === this.$devicePlatformsConstants.iOS.toLowerCase()) {
176+
return this.$iOSDebugService;
177+
} else if(lowerCasedPlatform === this.$devicePlatformsConstants.Android.toLowerCase()) {
178+
return this.$androidDebugService;
179+
}
180+
181+
throw new Error(`Invalid platform ${platform}. Valid platforms are ${this.$devicePlatformsConstants.iOS} and ${this.$devicePlatformsConstants.Android}`);
182+
}
183+
184+
private getKarmaConfiguration(platform: string): any {
185+
let karmaConfig: any = {
186+
browsers: [platform],
187+
configFile: path.join(this.$projectData.projectDir, 'karma.conf.js'),
188+
_NS: {
189+
log: this.$logger.getLevel(),
190+
path: this.$options.path,
191+
tns: process.argv[1],
192+
node: process.execPath,
193+
options: {
194+
debugTransport: this.$options.debugTransport,
195+
debugBrk: this.$options.debugBrk,
196+
}
197+
},
198+
};
199+
if (this.$config.DEBUG || this.$logger.getLevel() === 'TRACE') {
200+
karmaConfig.logLevel = 'DEBUG';
201+
}
202+
if (!this.$options.watch) {
203+
karmaConfig.singleRun = true;
204+
}
205+
if (this.$options.debugBrk) {
206+
karmaConfig.browserNoActivityTimeout = 1000000000;
207+
}
208+
209+
karmaConfig.projectDir = this.$projectData.projectDir;
210+
this.$logger.debug(JSON.stringify(karmaConfig, null, 4));
211+
212+
return karmaConfig;
213+
}
214+
215+
private liveSyncProject(platform: string): IFuture<void> {
216+
return (() => {
217+
let platformData = this.$platformsData.getPlatformData(platform.toLowerCase()),
218+
projectFilesPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME);
219+
220+
let liveSyncData: ILiveSyncData = {
221+
platform: platform,
222+
appIdentifier: this.$projectData.projectId,
223+
projectFilesPath: projectFilesPath,
224+
syncWorkingDirectory: path.join(this.$projectData.projectDir, constants.APP_FOLDER_NAME),
225+
canExecuteFastSync: false, // Always restart the application when change is detected, so tests will be rerun.
226+
excludedProjectDirsAndFiles: constants.LIVESYNC_EXCLUDED_FILE_PATTERNS
227+
};
228+
229+
this.$liveSyncServiceBase.sync(liveSyncData).wait();
230+
}).future<void>()();
231+
}
157232
}
158233
$injector.register('testExecutionService', TestExecutionService);

0 commit comments

Comments
 (0)