node.jstypescriptgulprun-sequence

run-sequence synchronous task never completes


I'm almost certainly going about this in the wrong way, so first up my high-level requirement.

I'm using the angular2-seed and want to run Protractor tests in a headless mode by using Xvfb. I don't want an Xvfb server running at all times (this is a build server) so instead I'd like to spin up an Xvfb service, have Protractor do it's thing, and then "gracefully" shut down Xvfb. In isolation these tasks are working fine, however I've hit a wall when it comes to adding them into the gulp build setup.

Here's the task in the gulpfile:

gulp.task('e2e.headless', (done: any) =>
  runSequence('start.xvfb',
              'protractor',
              'stop.xvfb',
              done));

The tasks themselves are loaded in through individual typescript task files, i.e:

import {runProtractor} from '../../utils';

export = runProtractor

And here are my (latest) utility files themselves.

protractor.ts

import * as util from 'gulp-util';
import {normalize, join} from 'path';
import {ChildProcess} from 'child_process';

function reportError(message: string) {
  console.error(require('chalk').white.bgRed.bold(message));
  process.exit(1);
}

function promiseFromChildProcess(child: ChildProcess) {
  return new Promise(function (resolve: () => void, reject: () => void) {
    child.on('close', (code: any) => {
      util.log('Exited with code: ', code);
      resolve();
    });
    child.stdout.on('data', (data: any) => {
      util.log(`stdout: ${data}`);
    });

    child.stderr.on('data', (data: any) => {
      util.log(`stderr: ${data}`);
      reject();
    });
  });
}

export function runProtractor(): (done: () => void) => void {
  return done => {
    const root = normalize(join(__dirname, '..', '..', '..'));
    const exec = require('child_process').exec;

    // Our Xvfb instance is running on :99
    // TODO: Pass this in instead of hard-coding
    process.env.DISPLAY=':99';
    util.log('cwd:', root);

    let child = exec('protractor', { cwd: root, env: process.env},
      function (error: Error, stdout: NodeBuffer, stderr: NodeBuffer) {
        if (error !== null) {
          reportError('Protractor error: ' + error + stderr);
        }
      });
    promiseFromChildProcess(child).then(() => done());
  };
}

xvfb_tools.ts

import * as util from 'gulp-util';

const exec = require('child_process').exec;

function reportError(message: string) {
  console.error(require('chalk').white.bgRed.bold(message));
  process.exit(1);
}

export function stopXvfb() {
    return exec('pkill -c -n Xvfb',
        function (error: NodeJS.ErrnoException, stdout: NodeBuffer, stderr: NodeBuffer) {
            if (error !== null) {
                reportError('Failed to kill Xvfb.  Not really sure why...');
            } else if (stdout.toString() === '0') {
                reportError('No known Xvfb instance.  Is it running?');
            } else {
                util.log('Xvfb terminated');
            }
        });
}

export function startXvfb() {
    return exec('Xvfb :99 -ac -screen 0 1600x1200x24',
        function (error: NodeJS.ErrnoException, stdout: NodeBuffer, stderr: NodeBuffer) {
            if (error !== null && error.code !== null) {
                reportError('Xvfb failed to start.  Err: ' + error.code + ', ' + error + ', ' + stderr);
            }
        });
}

I feel as though I'm probably going around the houses in creating a promise from my exec child_process, however earlier interations of the code didn't do it, so... Note that the debug logging which should be output in runProtractor() displaying the root directory never gets called, so I'm quite sure that there is an async issue at play here. Here is the output from the task:

[00:47:49] Starting 'e2e.headless'...
[00:47:49] Starting 'start.xvfb'...
[00:47:49] Finished 'start.xvfb' after 12 ms
[00:47:49] Starting 'protractor'...
[00:47:49] Finished 'protractor' after 5.74 ms
[00:47:49] Starting 'stop.xvfb'...
[00:47:49] Finished 'stop.xvfb' after 11 ms
[00:47:49] Finished 'e2e.headless' after 38 ms
[00:47:49] Xvfb terminated

Can someone set me straight/push me in the right direction please??


Solution

  • Thanks to Ludovic from the angular2-seed team!

    The mistake was in not calling the runProtractor function from the wrapper class, i.e. export = runProtractor(). Once that was noted, I could then strip out the un-necessary wrapper function as well as the promiseFromChildProcess, which were distractions.

    The final task was just an anonymous function that takes the gulp callback "done" which is called when exiting:

    function reportError(message: string) {
      console.error(require('chalk').white.bgRed.bold(message));
      process.exit(1);
    }
    
    export = (done: any) => {
        const root = normalize(join(__dirname, '..', '..', '..'));
        const exec = require('child_process').exec;
    
        process.env.DISPLAY=':99';
        util.log('cwd:', root);
    
        exec('protractor', { cwd: root, env: process.env},
          function (error: Error, stdout: NodeBuffer, stderr: NodeBuffer) {
            if (error !== null) {
              reportError('Protractor error: ' + error + stderr);
            } else {
              done();
            }
          });
    }