From 8a5d082ec0905c478bea476e878e8e19d0d207a3 Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Thu, 25 Feb 2016 13:27:31 +0200 Subject: [PATCH] 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 --watch` is not working for TypeScript projects * `tns test --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 ` 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. --- lib/common | 2 +- lib/constants.ts | 1 + lib/services/karma-execution.ts | 15 +++ lib/services/livesync/livesync-service.ts | 2 +- lib/services/platform-service.ts | 9 +- lib/services/test-execution-service.ts | 141 +++++++++++++++++----- 6 files changed, 128 insertions(+), 42 deletions(-) create mode 100644 lib/services/karma-execution.ts diff --git a/lib/common b/lib/common index 9ea72d51ec..db060b6471 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 9ea72d51ec24537f15bd6d8f72de0bc0eb20d0cc +Subproject commit db060b647161fc2cf368be86576d2ff2052c627e diff --git a/lib/constants.ts b/lib/constants.ts index d2824b15e2..73ffa7610c 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -14,6 +14,7 @@ export let DEFAULT_APP_IDENTIFIER_PREFIX = "org.nativescript"; export var LIVESYNC_EXCLUDED_DIRECTORIES = ["app_resources"]; export var TESTING_FRAMEWORKS = ['jasmine', 'mocha', 'qunit']; export let TEST_RUNNER_NAME = "nativescript-unit-test-runner"; +export let LIVESYNC_EXCLUDED_FILE_PATTERNS = ["**/*.js.map", "**/*.ts"]; export class ReleaseType { static MAJOR = "major"; diff --git a/lib/services/karma-execution.ts b/lib/services/karma-execution.ts new file mode 100644 index 0000000000..23ba20f8d1 --- /dev/null +++ b/lib/services/karma-execution.ts @@ -0,0 +1,15 @@ +/// + +"use strict"; + +import * as path from "path"; + +process.on("message", (data: any) => { + if(data.karmaConfig) { + let pathToKarma = path.join(data.karmaConfig.projectDir, 'node_modules/karma'), + KarmaServer = require(path.join(pathToKarma, 'lib/server')), + karma = new KarmaServer(data.karmaConfig); + + karma.start(); + } +}); diff --git a/lib/services/livesync/livesync-service.ts b/lib/services/livesync/livesync-service.ts index 6a81215557..9795c809b7 100644 --- a/lib/services/livesync/livesync-service.ts +++ b/lib/services/livesync/livesync-service.ts @@ -58,7 +58,7 @@ class LiveSyncService implements ILiveSyncService { appIdentifier: this.$projectData.projectId, projectFilesPath: path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME), syncWorkingDirectory: path.join(this.$projectData.projectDir, constants.APP_FOLDER_NAME), - excludedProjectDirsAndFiles: ["**/*.js.map", "**/*.ts"] + excludedProjectDirsAndFiles: constants.LIVESYNC_EXCLUDED_FILE_PATTERNS }; this.$liveSyncServiceBase.sync(liveSyncData).wait(); }).future()(); diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index b7d2b33c2e..abdbcdafcc 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -11,11 +11,6 @@ import Future = require("fibers/future"); let clui = require("clui"); export class PlatformService implements IPlatformService { - private static TNS_MODULES_FOLDER_NAME = "tns_modules"; - private static EXCLUDE_FILES_PATTERN = [ - "**/*.js.map", - "**/*.ts" - ]; constructor(private $devicesService: Mobile.IDevicesService, private $errors: IErrors, @@ -226,7 +221,7 @@ export class PlatformService implements IPlatformService { this.$xmlValidator.validateXmlFiles(sourceFiles).wait(); // Remove .ts and .js.map files - PlatformService.EXCLUDE_FILES_PATTERN.forEach(pattern => sourceFiles = sourceFiles.filter(file => !minimatch(file, pattern, {nocase: true}))); + constants.LIVESYNC_EXCLUDED_FILE_PATTERNS.forEach(pattern => sourceFiles = sourceFiles.filter(file => !minimatch(file, pattern, {nocase: true}))); let copyFileFutures = sourceFiles.map(source => { let destinationPath = path.join(appDestinationDirectoryPath, path.relative(appSourceDirectoryPath, source)); if (this.$fs.getFsStats(source).wait().isDirectory()) { @@ -250,7 +245,7 @@ export class PlatformService implements IPlatformService { // Process node_modules folder let appDir = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); try { - let tnsModulesDestinationPath = path.join(appDir, PlatformService.TNS_MODULES_FOLDER_NAME); + let tnsModulesDestinationPath = path.join(appDir, constants.TNS_MODULES_FOLDER_NAME); this.$broccoliBuilder.prepareNodeModules(tnsModulesDestinationPath, platform, lastModifiedTime).wait(); } catch(error) { this.$logger.debug(error); diff --git a/lib/services/test-execution-service.ts b/lib/services/test-execution-service.ts index 254d6f5eb5..a782816ac3 100644 --- a/lib/services/test-execution-service.ts +++ b/lib/services/test-execution-service.ts @@ -32,11 +32,17 @@ class TestExecutionService implements ITestExecutionService { private $options: IOptions, private $pluginsService: IPluginsService, private $errors: IErrors, - private $devicesService: Mobile.IDevicesService) { + private $androidDebugService:IDebugService, + private $iOSDebugService: IDebugService, + private $devicesService: Mobile.IDevicesService, + private $childProcess: IChildProcess) { } + public platform: string; + public startTestRunner(platform: string) : IFuture { return (() => { + this.platform = platform; this.$options.justlaunch = true; let blockingOperationFuture = new Future(); process.on('message', (launcherConfig: any) => { @@ -50,7 +56,7 @@ class TestExecutionService implements ITestExecutionService { let configOptions: IKarmaConfigOptions = JSON.parse(launcherConfig); this.$options.debugBrk = configOptions.debugBrk; this.$options.debugTransport = configOptions.debugTransport; - let configJs = this.generateConfig(configOptions); + let configJs = this.generateConfig(this.$options.port.toString(), configOptions); this.$fs.writeFile(path.join(projectDir, TestExecutionService.CONFIG_FILE_NAME), configJs).wait(); let socketIoJsUrl = `http://localhost:${this.$options.port}/socket.io/socket.io.js`; @@ -93,37 +99,47 @@ class TestExecutionService implements ITestExecutionService { public startKarmaServer(platform: string): IFuture { return (() => { platform = platform.toLowerCase(); - this.$pluginsService.ensureAllDependenciesAreInstalled().wait(); - let pathToKarma = path.join(this.$projectData.projectDir, 'node_modules/karma'); - let KarmaServer = require(path.join(pathToKarma, 'lib/server')); - if (platform === 'ios' && this.$options.emulator) { - platform = 'ios_simulator'; - } - let karmaConfig: any = { - browsers: [platform], - configFile: path.join(this.$projectData.projectDir, 'karma.conf.js'), - _NS: { - log: this.$logger.getLevel(), - path: this.$options.path, - tns: process.argv[1], - node: process.execPath, - options: { - debugTransport: this.$options.debugTransport, - debugBrk: this.$options.debugBrk, - } - }, - }; - if (this.$config.DEBUG || this.$logger.getLevel() === 'TRACE') { - karmaConfig.logLevel = 'DEBUG'; - } - if (!this.$options.watch) { - karmaConfig.singleRun = true; + this.platform = platform; + + if(this.$options.debugBrk && this.$options.watch) { + this.$errors.failWithoutHelp("You cannot use --watch and --debug-brk simultaneously. Remove one of the flags and try again."); } - if (this.$options.debugBrk) { - karmaConfig.browserNoActivityTimeout = 1000000000; + + if (!this.$platformService.preparePlatform(platform).wait()) { + this.$errors.failWithoutHelp("Verify that listed files are well-formed and try again the operation."); } - this.$logger.debug(JSON.stringify(karmaConfig, null, 4)); - new KarmaServer(karmaConfig).start(); + + let projectDir = this.$projectData.projectDir; + this.$devicesService.initialize({ platform: platform, deviceId: this.$options.device }).wait(); + + let karmaConfig = this.getKarmaConfiguration(platform), + karmaRunner = this.$childProcess.fork(path.join(__dirname, "karma-execution.js")); + + karmaRunner.send({karmaConfig: karmaConfig}); + karmaRunner.on("message", (karmaData: any) => { + fiberBootstrap.run(() => { + this.$logger.trace("## Unit-testing: Parent process received message", karmaData); + let port: string; + if(karmaData.url) { + port = karmaData.url.port; + let socketIoJsUrl = `http://${karmaData.url.host}/socket.io/socket.io.js`; + let socketIoJs = this.$httpClient.httpRequest(socketIoJsUrl).wait().body; + this.$fs.writeFile(path.join(projectDir, TestExecutionService.SOCKETIO_JS_FILE_NAME), socketIoJs).wait(); + } + + if(karmaData.launcherConfig) { + let configOptions: IKarmaConfigOptions = JSON.parse(karmaData.launcherConfig); + let configJs = this.generateConfig(port, configOptions); + this.$fs.writeFile(path.join(projectDir, TestExecutionService.CONFIG_FILE_NAME), configJs).wait(); + } + + if(this.$options.debugBrk) { + this.getDebugService(platform).debug().wait(); + } else { + this.liveSyncProject(platform).wait(); + } + }); + }); }).future()(); } @@ -138,8 +154,7 @@ class TestExecutionService implements ITestExecutionService { }).future()(); } - private generateConfig(options: any): string { - let port = this.$options.port; + private generateConfig(port: string, options: any): string { let nics = os.networkInterfaces(); let ips = Object.keys(nics) .map(nicName => nics[nicName].filter((binding: any) => binding.family === 'IPv4' && !binding.internal)[0]) @@ -154,5 +169,65 @@ class TestExecutionService implements ITestExecutionService { return 'module.exports = ' + JSON.stringify(config); } + + private getDebugService(platform: string): IDebugService { + let lowerCasedPlatform = platform.toLowerCase(); + if(lowerCasedPlatform === this.$devicePlatformsConstants.iOS.toLowerCase()) { + return this.$iOSDebugService; + } else if(lowerCasedPlatform === this.$devicePlatformsConstants.Android.toLowerCase()) { + return this.$androidDebugService; + } + + throw new Error(`Invalid platform ${platform}. Valid platforms are ${this.$devicePlatformsConstants.iOS} and ${this.$devicePlatformsConstants.Android}`); + } + + private getKarmaConfiguration(platform: string): any { + let karmaConfig: any = { + browsers: [platform], + configFile: path.join(this.$projectData.projectDir, 'karma.conf.js'), + _NS: { + log: this.$logger.getLevel(), + path: this.$options.path, + tns: process.argv[1], + node: process.execPath, + options: { + debugTransport: this.$options.debugTransport, + debugBrk: this.$options.debugBrk, + } + }, + }; + if (this.$config.DEBUG || this.$logger.getLevel() === 'TRACE') { + karmaConfig.logLevel = 'DEBUG'; + } + if (!this.$options.watch) { + karmaConfig.singleRun = true; + } + if (this.$options.debugBrk) { + karmaConfig.browserNoActivityTimeout = 1000000000; + } + + karmaConfig.projectDir = this.$projectData.projectDir; + this.$logger.debug(JSON.stringify(karmaConfig, null, 4)); + + return karmaConfig; + } + + private liveSyncProject(platform: string): IFuture { + return (() => { + let platformData = this.$platformsData.getPlatformData(platform.toLowerCase()), + projectFilesPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); + + let liveSyncData: ILiveSyncData = { + platform: platform, + appIdentifier: this.$projectData.projectId, + projectFilesPath: projectFilesPath, + syncWorkingDirectory: path.join(this.$projectData.projectDir, constants.APP_FOLDER_NAME), + canExecuteFastSync: false, // Always restart the application when change is detected, so tests will be rerun. + excludedProjectDirsAndFiles: constants.LIVESYNC_EXCLUDED_FILE_PATTERNS + }; + + this.$liveSyncServiceBase.sync(liveSyncData).wait(); + }).future()(); + } } $injector.register('testExecutionService', TestExecutionService);