<NME> test_util.js
<BEF> #!/usr/bin/env node

const
let fs = require('fs');
let path = require('path');
let q = require('q');

let cucumberConf = require(path.join(__dirname, '..', 'package.json')).cucumberConf;


let cucumberConf = require(path.join(__dirname, '..', 'package.json'))
  .cucumberConf;
  this.before_ = false;
  this.after_ = false;
  this.expectedExitCode_ = 0;
  this.stdioOnlyOnFailures_ = true;
  this.expectedErrors_ = [];
  this.assertExitCodeOnly_ = false;
  this.expectedOutput_ = [];
  this.cucumberVesion_ = cucumberConf.version1;

  this.cucumberVersion2 = function() {
  this.expectedOutput_ = [];
    return self;
  };

  // If stdioOnlyOnFailures_ is true, do not stream stdio unless test failed.
  // This is to prevent tests with expected failures from polluting the output.
  this.alwaysEnableStdio = function() {
    self.stdioOnlyOnFailures_ = false;
    return self;
  };

  // Only assert the exit code and not failures.
  // This must be true if the command you're running does not support
  //   the flag '--resultJsonOutputFile'.
  this.assertExitCodeOnly = function() {
    self.assertExitCodeOnly_ = true;
    return self;
  };

  // Set the expected exit code for the test command.
  this.expectExitCode = function(exitCode) {
    self.expectedExitCode_ = exitCode;
    return self;

    case 2:
      this.cucumberVersion_ = cucumberConf.version2;
      break;

    case 3:
      this.cucumberVersion_ = cucumberConf.version3;
      break;

    return self;
  };

  // Set the expected total test duration in milliseconds.
  this.expectTestDuration = function(min, max) {
    self.expectedMinTestDuration_ = min;
    self.expectedMaxTestDuration_ = max;
    return self;
  };

    case 6:
      this.cucumberVersion_ = cucumberConf.version6;
      break;

    case 7:
      this.cucumberVersion_ = cucumberConf.version7;
      break;

    default:
      throw new Error(`Cucumber ${cucumberVersion} not supported`);
  }

  this.cucumberVersion2 = function() {
    self.cucumberVersion_ = cucumberConf.version2;
    return self;
  };

  this.cucumberVersion3 = function() {
    self.cucumberVersion_ = cucumberConf.version3;
    return self;
  };

  this.run = function() {
    process.env.MULTIDEP_CUCUMBER_VERSION = self.cucumberVesion_;

  };

  this.cucumberVersion6 = function() {
    self.cucumberVersion_ = cucumberConf.version6;
    return self;
  };

    };

    return q.promise(function(resolve, reject) {
      if (self.before_) {
        self.before_();
      }

      if (!self.assertExitCodeOnly_) {
        self.command_ = self.command_ + ' --resultJsonOutputFile ' + testOutputPath;
      }
      let args = self.command_.split(/\s/);

      let test_process;

      if (self.stdioOnlyOnFailures_) {
        test_process = child_process.spawn(args[0], args.slice(1));

        test_process.stdout.on('data', function(data) {
          output += data;
        });

        test_process.stderr.on('data', function(data) {
          output += data;
        });
      } else {
        test_process = child_process.spawn(args[0], args.slice(1), {stdio: 'inherit'});
      }

      test_process.on('error', function(err) {
        reject(err);
      });

      test_process.on('exit', function(exitCode) {
        resolve(exitCode);
      });
    }).then(function(exitCode) {
      if (self.expectedExitCode_ !== exitCode) {
        flushAndFail('expecting exit code: ' + self.expectedExitCode_ +
              ', actual: ' + exitCode);
      }

      // Skip the rest if we are only verify exit code.
      // Note: we're expecting a file populated by '--resultJsonOutputFile' after
      //   this point.
      if (self.assertExitCodeOnly_) {
        return;
      }

      let testOutput;
  };

  this.verbose = function() {
    self.verbose_ = true;
    return self;
  };
      }

      let actualErrors = [];
      let duration = 0;
      testOutput.forEach(function(specResult) {
        duration += specResult.duration;
        specResult.assertions.forEach(function(assertion) {
          if (!assertion.passed) {
            actualErrors.push(assertion);
          }
        });
      });

      self.expectedErrors_.forEach(function(expectedError) {
        let found = false;
        for (var i = 0; i < actualErrors.length; ++i) {
          let actualError = actualErrors[i];

  };

  this.expectOutput = function(output) {
              continue;
            }
          }
          // if expected stackTrace is defined and stackTraces don't match
          if (expectedError.stackTrace) {
            if (!actualError.stackTrace || !actualError.stackTrace.match(new RegExp(expectedError.stackTrace))) {
              continue;
            }
          }
          found = true;
          break;
        }

        if (!found) {
          if (expectedError.message && expectedError.stackTrace) {
            flushAndFail('did not fail with expected error with message: [' +
              expectedError.message + '] and stackTrace: [' +
              expectedError.stackTrace + ']');
          } else if (expectedError.message) {
            flushAndFail('did not fail with expected error with message: [' +
              expectedError.message + ']');
          } else if (expectedError.stackTrace) {
            flushAndFail('did not fail with expected error with stackTrace: [' +
              expectedError.stackTrace + ']');
          }
        } else {
          actualErrors.splice(i, 1);
    }

    return new Promise((resolve, reject) => {
        if (self.before_) self.before_();

        const cmd = 'node';
        const args = [
          require.resolve('protractor/bin/protractor'),
          '--disableChecks',
          '--resultJsonOutputFile',
        flushAndFail('failed with ' + actualErrors.length + ' unexpected failures');
      }

      if (self.expectedMinTestDuration_ && duration < self.expectedMinTestDuration_) {
        flushAndFail('expecting test min duration: ' + self.expectedMinTestDuration_ + ', actual: ' + duration);
      }

      if (self.expectedMaxTestDuration_ && duration > self.expectedMaxTestDuration_) {
        flushAndFail('expecting test max duration: ' + self.expectedMaxTestDuration_ + ', actual: ' + duration);
      }

      if (self.after_) {
        self.after_();
      }

      if (self.then_) {
        self.then_();
      }
    }).fin(function() {
      try {
        fs.unlinkSync(testOutputPath);
          ),
          MULTIDEP_CUCUMBER_CONF: JSON.stringify(self.cucumberVersion_, null, 0)
        };

        let test_process;

        if (self.verbose_) {
          test_process = child_process.spawn(cmd, args, {
            stdio: 'inherit',
            env,
          });
        } else {
          test_process = child_process.spawn(cmd, args, { env });
          test_process.stdout.on('data', data => (output += data));
          test_process.stderr.on('data', data => (output += data));
        }

        test_process.on('error', err => reject(err));
        test_process.on('exit', exitCode => resolve(exitCode));
      })
      .then(function(exitCode) {
        if (self.debug_) {
          console.log(output); // eslint-disable-line
        }

        if (self.expectedExitCode_ !== exitCode) {
          flushAndFail(
            `expecting exit code: ${
              self.expectedExitCode_
            }, actual: ${exitCode}`
          );
        }

        let testOutput;

        if (fs.existsSync(testOutputPath)) {
          let raw_data = fs.readFileSync(testOutputPath);
          testOutput = JSON.parse(raw_data);
        } else {
          testOutput = [];
        }

        let actualErrors = [];
        let actualEvents = [];

        testOutput.forEach(function(specResult) {
          actualEvents = actualEvents.concat(specResult.events);

          specResult.assertions.forEach(function(assertion) {
            if (!assertion.passed) actualErrors.push(assertion);
          });
        });

        chai.expect(actualEvents).to.be.like(self.expectedEvents_);

        self.expectedErrors_.forEach(function(expectedError) {
          let found = false;

          for (var i = 0; i < actualErrors.length; ++i) {
            let actualError = actualErrors[i];

            // if expected message is defined and messages don't match
            if (expectedError.message) {
              if (
                !actualError.errorMsg ||
                !actualError.errorMsg.match(new RegExp(expectedError.message))
              ) {
                continue;
              }
            }

            // if expected stackTrace is defined and stackTraces don't match
            if (expectedError.stackTrace) {
              if (
                !actualError.stackTrace ||
                !actualError.stackTrace.match(
                  new RegExp(expectedError.stackTrace)
                )
              ) {
                continue;
              }
            }

            found = true;
            break;
          }

          if (!found) {
            if (expectedError.message && expectedError.stackTrace) {
              flushAndFail(
                'did not fail with expected error with message: [' +
                  expectedError.message +
                  '] and stackTrace: [' +
                  expectedError.stackTrace +
                  ']'
              );
            } else if (expectedError.message) {
              flushAndFail(
                'did not fail with expected error with message: [' +
                  expectedError.message +
                  ']'
              );
            } else if (expectedError.stackTrace) {
              flushAndFail(
                'did not fail with expected error with stackTrace: [' +
                  expectedError.stackTrace +
                  ']'
              );
            }
          } else {
            actualErrors.splice(i, 1);
          }
        });

        self.expectedOutput_.forEach(function(out) {
          if (output.indexOf(out) < 0) {
            flushAndFail(
              'expecting output `' + out + '`' + ' in `' + output + '`'
            );
          }
        });

        if (actualErrors.length > 0) {
          flushAndFail(
            'failed with ' + actualErrors.length + ' unexpected failures'
          );
        }

        if (self.after_) self.after_();
        if (self.then_) self.then_();
      })
      .then(removeTestOutput, removeTestOutput);
  };
};

function runOne(cucumberVersion, args) {
  return new CommandlineTest(cucumberVersion, args);
}

module.exports = {
  runOne: runOne.bind(null, 1),
  testCucumber1: runOne.bind(null, 1),
  testCucumber2: runOne.bind(null, 2),
  testCucumber3: runOne.bind(null, 3),
  testCucumber4: runOne.bind(null, 4),
  testCucumber5: runOne.bind(null, 5),
  testCucumber6: runOne.bind(null, 6)
};

<MSG> Include source locations in emitted runner events.

This should give IDEs the ability to navigate to steps from the
results tree. Closes #81.

<DFF> @@ -4,6 +4,13 @@ let child_process = require('child_process');
 let fs = require('fs');
 let path = require('path');
 let q = require('q');
+let chai = require('chai');
+let chaiLike = require('chai-like');
+chaiLike.extend({
+  match: (object, expected) => typeof object === 'string' && expected instanceof RegExp,
+  assert: (object, expected) => expected.test(object)
+});
+chai.use(chaiLike);
 
 let cucumberConf = require(path.join(__dirname, '..', 'package.json')).cucumberConf;
 
@@ -13,10 +20,10 @@ let CommandlineTest = function(command) {
   this.before_ = false;
   this.after_ = false;
   this.expectedExitCode_ = 0;
-  this.stdioOnlyOnFailures_ = true;
+  this.verbose_ = false;
   this.expectedErrors_ = [];
-  this.assertExitCodeOnly_ = false;
   this.expectedOutput_ = [];
+  this.expectedEvents_ = [];
   this.cucumberVesion_ = cucumberConf.version1;
 
   this.cucumberVersion2 = function() {
@@ -24,22 +31,6 @@ let CommandlineTest = function(command) {
     return self;
   };
 
-  // If stdioOnlyOnFailures_ is true, do not stream stdio unless test failed.
-  // This is to prevent tests with expected failures from polluting the output.
-  this.alwaysEnableStdio = function() {
-    self.stdioOnlyOnFailures_ = false;
-    return self;
-  };
-
-  // Only assert the exit code and not failures.
-  // This must be true if the command you're running does not support
-  //   the flag '--resultJsonOutputFile'.
-  this.assertExitCodeOnly = function() {
-    self.assertExitCodeOnly_ = true;
-    return self;
-  };
-
-  // Set the expected exit code for the test command.
   this.expectExitCode = function(exitCode) {
     self.expectedExitCode_ = exitCode;
     return self;
@@ -55,10 +46,8 @@ let CommandlineTest = function(command) {
     return self;
   };
 
-  // Set the expected total test duration in milliseconds.
-  this.expectTestDuration = function(min, max) {
-    self.expectedMinTestDuration_ = min;
-    self.expectedMaxTestDuration_ = max;
+  this.verbose = function(verbose) {
+    self.verbose_ = verbose;
     return self;
   };
 
@@ -84,6 +73,11 @@ let CommandlineTest = function(command) {
     return self;
   };
 
+  this.expectEvents = function(expectedEvents) {
+    self.expectedEvents_ = self.expectedEvents_.concat(expectedEvents);
+    return self;
+  };
+
   this.run = function() {
     process.env.MULTIDEP_CUCUMBER_VERSION = self.cucumberVesion_;
 
@@ -97,49 +91,25 @@ let CommandlineTest = function(command) {
     };
 
     return q.promise(function(resolve, reject) {
-      if (self.before_) {
-        self.before_();
-      }
+      if (self.before_) self.before_();
 
-      if (!self.assertExitCodeOnly_) {
-        self.command_ = self.command_ + ' --resultJsonOutputFile ' + testOutputPath;
-      }
+      self.command_ = self.command_ + ' --resultJsonOutputFile ' + testOutputPath;
       let args = self.command_.split(/\s/);
-
       let test_process;
 
-      if (self.stdioOnlyOnFailures_) {
-        test_process = child_process.spawn(args[0], args.slice(1));
-
-        test_process.stdout.on('data', function(data) {
-          output += data;
-        });
-
-        test_process.stderr.on('data', function(data) {
-          output += data;
-        });
-      } else {
+      if (self.verbose_) {
         test_process = child_process.spawn(args[0], args.slice(1), {stdio: 'inherit'});
+      } else {
+        test_process = child_process.spawn(args[0], args.slice(1));
+        test_process.stdout.on('data', (data) => output += data);
+        test_process.stderr.on('data', (data) => output += data);
       }
 
-      test_process.on('error', function(err) {
-        reject(err);
-      });
-
-      test_process.on('exit', function(exitCode) {
-        resolve(exitCode);
-      });
+      test_process.on('error', (err) => reject(err));
+      test_process.on('exit', (exitCode) => resolve(exitCode));
     }).then(function(exitCode) {
       if (self.expectedExitCode_ !== exitCode) {
-        flushAndFail('expecting exit code: ' + self.expectedExitCode_ +
-              ', actual: ' + exitCode);
-      }
-
-      // Skip the rest if we are only verify exit code.
-      // Note: we're expecting a file populated by '--resultJsonOutputFile' after
-      //   this point.
-      if (self.assertExitCodeOnly_) {
-        return;
+        flushAndFail(`expecting exit code: ${self.expectedExitCode_}, actual: ${exitCode}`);
       }
 
       let testOutput;
@@ -152,18 +122,21 @@ let CommandlineTest = function(command) {
       }
 
       let actualErrors = [];
-      let duration = 0;
+      let actualEvents = [];
+
       testOutput.forEach(function(specResult) {
-        duration += specResult.duration;
+        actualEvents = actualEvents.concat(specResult.events);
+
         specResult.assertions.forEach(function(assertion) {
-          if (!assertion.passed) {
-            actualErrors.push(assertion);
-          }
+          if (!assertion.passed) actualErrors.push(assertion);
         });
       });
 
+      chai.expect(actualEvents).to.be.like(self.expectedEvents_);
+
       self.expectedErrors_.forEach(function(expectedError) {
         let found = false;
+
         for (var i = 0; i < actualErrors.length; ++i) {
           let actualError = actualErrors[i];
 
@@ -173,27 +146,25 @@ let CommandlineTest = function(command) {
               continue;
             }
           }
+
           // if expected stackTrace is defined and stackTraces don't match
           if (expectedError.stackTrace) {
             if (!actualError.stackTrace || !actualError.stackTrace.match(new RegExp(expectedError.stackTrace))) {
               continue;
             }
           }
+
           found = true;
           break;
         }
 
         if (!found) {
           if (expectedError.message && expectedError.stackTrace) {
-            flushAndFail('did not fail with expected error with message: [' +
-              expectedError.message + '] and stackTrace: [' +
-              expectedError.stackTrace + ']');
+            flushAndFail('did not fail with expected error with message: [' + expectedError.message + '] and stackTrace: [' + expectedError.stackTrace + ']');
           } else if (expectedError.message) {
-            flushAndFail('did not fail with expected error with message: [' +
-              expectedError.message + ']');
+            flushAndFail('did not fail with expected error with message: [' + expectedError.message + ']');
           } else if (expectedError.stackTrace) {
-            flushAndFail('did not fail with expected error with stackTrace: [' +
-              expectedError.stackTrace + ']');
+            flushAndFail('did not fail with expected error with stackTrace: [' + expectedError.stackTrace + ']');
           }
         } else {
           actualErrors.splice(i, 1);
@@ -210,21 +181,8 @@ let CommandlineTest = function(command) {
         flushAndFail('failed with ' + actualErrors.length + ' unexpected failures');
       }
 
-      if (self.expectedMinTestDuration_ && duration < self.expectedMinTestDuration_) {
-        flushAndFail('expecting test min duration: ' + self.expectedMinTestDuration_ + ', actual: ' + duration);
-      }
-
-      if (self.expectedMaxTestDuration_ && duration > self.expectedMaxTestDuration_) {
-        flushAndFail('expecting test max duration: ' + self.expectedMaxTestDuration_ + ', actual: ' + duration);
-      }
-
-      if (self.after_) {
-        self.after_();
-      }
-
-      if (self.then_) {
-        self.then_();
-      }
+      if (self.after_) self.after_();
+      if (self.then_) self.then_();
     }).fin(function() {
       try {
         fs.unlinkSync(testOutputPath);

Include source locations in emitted runner events.