From a47ea31616a7020e377ddbfe669c0ea7ce848d63 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/services/karma-execution.ts | 15 +++++ lib/services/test-execution-service.ts | 79 +++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 lib/services/karma-execution.ts 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/test-execution-service.ts b/lib/services/test-execution-service.ts index 254d6f5eb5..666f0c96fc 100644 --- a/lib/services/test-execution-service.ts +++ b/lib/services/test-execution-service.ts @@ -32,11 +32,16 @@ class TestExecutionService implements ITestExecutionService { private $options: IOptions, private $pluginsService: IPluginsService, private $errors: IErrors, + private $androidDebugService:IDebugService, + private $iOSDebugService: IDebugService, private $devicesService: Mobile.IDevicesService) { } + 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 +55,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,12 +98,30 @@ 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'; + 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.$platformService.preparePlatform(platform).wait()) { + this.$errors.failWithoutHelp("Verify that listed files are well-formed and try again the operation."); } + + let platformData = this.$platformsData.getPlatformData(platform.toLowerCase()); + let projectDir = this.$projectData.projectDir; + this.$devicesService.initialize({ platform: platform, deviceId: this.$options.device }).wait(); + let projectFilesPath = path.join(platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); + + let liveSyncData: ILiveSyncData = { + platform: platform, + appIdentifier: this.$projectData.projectId, + projectFilesPath: projectFilesPath, + syncWorkingDirectory: path.join(projectDir, constants.APP_FOLDER_NAME), + canExecuteFastSync: false, // Always restart the application when change is detected, so tests will be rerun. + excludedProjectDirsAndFiles: ["**/*.js.map", "**/*.ts"] + }; + let karmaConfig: any = { browsers: [platform], configFile: path.join(this.$projectData.projectDir, 'karma.conf.js'), @@ -122,8 +145,36 @@ class TestExecutionService implements ITestExecutionService { if (this.$options.debugBrk) { karmaConfig.browserNoActivityTimeout = 1000000000; } + + karmaConfig.projectDir = this.$projectData.projectDir; this.$logger.debug(JSON.stringify(karmaConfig, null, 4)); - new KarmaServer(karmaConfig).start(); + + let karmaRunner = require("child_process").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://localhost:${port}/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.$liveSyncServiceBase.sync(liveSyncData).wait(); + } + }); + }); }).future()(); } @@ -138,8 +189,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 +204,16 @@ class TestExecutionService implements ITestExecutionService { return 'module.exports = ' + JSON.stringify(config); } + + private getDebugService(platform: string): IDebugService { + let lowerCasedPlatform = platform.toLowerCase(); + if(platform.toLowerCase() === 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}`); + } } $injector.register('testExecutionService', TestExecutionService);