Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resolves #2149, #1257: Make TAP reporter conform to TAP13 #3452

Closed
wants to merge 31 commits into from

Conversation

mollstam
Copy link
Contributor

@mollstam mollstam commented Aug 8, 2018

Description of the Change

The TAP reporter has been made to conform with TAP13.

Changes to the actual reporter are small: a version string is emitted as the first line, and error message and stack are both emitted inside a YAML block to be parsed by any TAP Consumer.

A quite large integration test has been added that encodes the expectations of the TAP13 spec, and the existing TAP reporter unit tests have been updated to be compatible with the new spec.

Finally 'reporters.fixture.js' was added to have something to run that generates test results with varying properties.

Alternate Designs

It was considered to add either a new reporter (e.g. tap13) or hide the new spec under a reporter option but since there are no breaking changes in TAP13 this was deemed unnecessary.

Why should this be in core?

Mocha relies on a default selection of usable reporters. A bunch of TAP Consumers that I've surveyed are poorly written and expect TAP13 or will otherwise break thus rendering the current TAP reporter in mocha less useful.

Benefits

A larger ecosystem of tools that are only compatible with TAP13 are made available to use together with mocha. Taking a step back, benefits in the TAP13 spec vs v12 are inherited, such as being able to encode richer error information in the now allowed YAML block in the TAP report is a Good Thing.

Possible Drawbacks

Many TAP Consumers I've encountered are not as flexible as one might wish so it is possible they don't fully conform to the old v12 spec, thus not being compatible with the TAP13 spec resulting in undefined behaviour. For example, currently in mocha the error stack is simply inserted into the report following the failed test, something that is allowed by the v12 spec as an "unknown" line that shouldn't be an error and may be handled by the test harness in any way. If a test harness handles mocha's stacks by e.g. using regex it will probably break now that the stack is wrapped in what TAP13 calls a "YAML block".

Applicable issues

#2149
#1257

Note

I've decided to not address issue #2410 with this PR as it involves communicating with third party tools (e.g. tap-spec) over TAP13 in a new way which I feel is outside the scope of simply bringing the TAP reporter up to spec, whereas 2410 is more about establishing a new spec regarding the structure of the YAML blocks (unspecified by TAP13).

@mollstam
Copy link
Contributor Author

mollstam commented Aug 8, 2018

Hi y'all, thought I'd get the ball rolling on having mocha's tap reporter output v13 spec compliant reports since I need it. 😬

Any thoughts?

Technically v13 is not backwards compatible with v12 so this is a breaking change for any TAP Consumers that don't also support v13 but I'd defer to a maintainer to deem this being a patch or minor update.

(Kind of wished bots ignored WIP PRs)

@Bamieh Bamieh added area: reporters involving a specific reporter semver-major implementation requires increase of "major" version number; "breaking changes" pr-needs-work labels Aug 8, 2018
@coveralls
Copy link

coveralls commented Aug 8, 2018

Coverage Status

Coverage increased (+0.08%) to 90.32% when pulling 3dd336f on mollstam:reporter-tap-v13 into 2bb4124 on mochajs:master.

@Bamieh
Copy link
Contributor

Bamieh commented Aug 8, 2018

@mollstam thanks for taking your time to work on this here. Just a thought, we can provide the new version under a new flag, until we eventually deprecate the old one.

I looked at the v13 spec But it does not list/mention the breaking changes, can you provide the breaking changes to give us a better idea about what we're dealing with? Also if this gets pushed, we'll include those breaking changes in the release notes.

@mollstam
Copy link
Contributor Author

mollstam commented Aug 8, 2018

@Bamieh Actually you're right, the v12 is quite clear:

A TAP parser is required to not consider an unknown line as an error but may optionally choose to capture said line and hand it to the test harness, which may have custom behavior attached. This is to allow for forward compatability.

I thought the version string would confuse it but it seems I was wrong, good good.

I starting thinking I'd hide the 13 spec under a reporter option but that doesn't seem to be necessary then.

@mollstam
Copy link
Contributor Author

mollstam commented Aug 8, 2018

All right, I've at this point created a couple of integration tests to ensure that the output conforms to spec v13 and also some other Nice Things regarding the structure of the YAML block containing error message and stack. Only a few changes had to be made to tap.js to bring it up to spec.

The YAML test in itself is a bit hairy but I feel like it's good enough for now. I'd like to create a new fixture that generates a report with "a little bit of everything" so I can make sure that it looks good, and so that I can also stop using uncaught.fixture.js.

I'd like to also keep looking a bit at putting more data about errors into the YAML block for some nice reporting by tap-diff, tap-spec and other tape/node-tap related consumers. The TAP spec gives implementors free reign of content in YAML block would be nice to inventory what most consumers expect and see if we can provide it.

Finally I'm going to mull over issue #2410 a bit as well.

I'm off for today. 🐨

@mollstam mollstam changed the title [WIP] Make TAP reporter conform to v13 spec Make TAP reporter conform to v13 spec Aug 9, 2018
@mollstam mollstam changed the title Make TAP reporter conform to v13 spec resolves #2149 #1257 : Make TAP reporter conform to v13 spec Aug 9, 2018
@mollstam mollstam changed the title resolves #2149 #1257 : Make TAP reporter conform to v13 spec resolves #2149 #1257 : Make TAP reporter conform to TAP13 Aug 9, 2018
@mollstam mollstam changed the title resolves #2149 #1257 : Make TAP reporter conform to TAP13 resolves #2149, #1257: Make TAP reporter conform to TAP13 Aug 9, 2018
@mollstam
Copy link
Contributor Author

mollstam commented Aug 9, 2018

All right, I feel like I'm done here and I'd like to open up for review.

Also the semver-major label is no longer required.

@plroebuck
Copy link
Contributor

plroebuck commented Sep 14, 2018

Since there's no huge major difference between TAP12 and YAP13/14 output, any consideration given to providing the TAP version via reporterOptions - making the TAP producer (reporter) both forward and backward compatible?

Add TAPProducer(version) class instance to TAP reporter, and defer all writing of TAP output to it.

As TAP12 is apparently ancient nowadays, perhaps TAP13 should be unspecified default. Or leave the default "as is" and allow this to be added to a "semver-minor" Mocha release.

@plroebuck
Copy link
Contributor

Have these changes been run against the TAP 14 draft?

@plroebuck
Copy link
Contributor

Without personally really diving into TAP, does this reporter's output match that which would be produced by Node-based TAP producers node-tap and tape?

lib/reporters/tap.js Outdated Show resolved Hide resolved
@@ -32,6 +32,7 @@ function TAP(runner) {
var failures = 0;

runner.on('start', function() {
console.log('TAP version 13');
Copy link
Contributor

@plroebuck plroebuck Sep 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using self.write() and self.writeln() so that file-based output via reporterOptions could be a trivially implemented possibility.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xunit reporter has a write() and I find nothing about writeln(). Can you point me in the right direction? console.log is what most reporters seem to be doing. 😱

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, using functions from another proposed PR.
Until that's ready, you could implement those as:

/**
 * Write out the given string.
 *
 * @param {string} s
 */
TAP.prototype.write = function(s) {
  if (this.fileStream) {
    this.fileStream.write(s);
  } else {
    process.stdout.write(s);
  }
};

/**
 * Write out the given string and terminate the line.
 *
 * @param {string} s
 */
TAP.prototype.writeln = function(s) {
  this.write(s + '\n');
};

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glancing your console.log usage, will also need to add an sprintf() function to handle your string substitutions.

@plroebuck
Copy link
Contributor

As a (hopefully non-digressive) follow-on question, what TAP consumers were used to verify output from this PR? While I don't think these should be added to Mocha's build toolchain, any related test scripts you used for such can be preserved here as comment attachments.

}
if (err.stack) {
console.log(err.stack.replace(/^/gm, ' '));
var emitYamlBlock = err.message != null || err.stack != null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see "diff"-able Errors in Mocha codebase with actual and expected properties. Would they be considered "interesting" as output?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a great feature to have but it doesn't really have much to do with TAP13, rather a "while we're at it" thing and if I remember correctly I ran into some problems investigating it, but this was I while back. I think this feature shouldn't ride on this PR tbh.

Copy link
Contributor

@plroebuck plroebuck Sep 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But wasn't that exactly part of the tape output we should be attempting to match?

@plroebuck
Copy link
Contributor

plroebuck commented Sep 15, 2018

On suite end event, should Mocha emit a timing comment line?

A la this unbuffered subtest reference output.

TAP version 13
# Subtest: foo
    # Subtest: bar
        1..1
        ok 1 - this is fine
    ok 1 - bar # time=1.831ms

    1..1
ok 1 - foo # time=11.106ms

1..1
# time=19.039ms

@mollstam
Copy link
Contributor Author

Hello friend, thanks for having a look! :)

Since there's no huge major difference between TAP12 and YAP13/14 output, any consideration given to providing the TAP version via reporterOptions - making the TAP producer (reporter) both forward and backward compatible?

Add TAPProducer(version) class instance to TAP reporter, and defer all writing of TAP output to it.

As TAP12 is apparently ancient nowadays, perhaps TAP13 should be unspecified default. Or leave the default "as is" and allow this to be added to a "semver-minor" Mocha release.

As the TAP13 spec is both forward and backward compatible I'm not sure keeping the TAP12 output around is necessary, what's your thoughts?

Have these changes been run against the TAP 14 draft?

I had a look at the TAP14 draft back when implementing this and decided not to implement it since it is only a draft and I wanted to keep the footprint of the PR as small as possible.

Without personally really diving into TAP, does this reporter's output match that which would be produced by Node-based TAP producers node-tap and tape?

I did recreate the fixture in tape, mostly to test how they handle subtests/suites (as discussed in issue #2410). Here is a gist of the tape version and here is a diff from tape report to this version of the mocha tap reporter. The main difference is the tape report having more structured data (operator, expected, actual) in the YAML rather than simply "baking it" into the message property. I investigated reaching similar formatting but the footprint of the fix grew and I wanted to keep things simple.

As you can see the running theme is me wanting to make as little of change as possible to reach what I set out to do. This iteration of the TAP reporter is not by any means a "final form" but a lot of the future work relies on the YAML format being standardized (consumers vary wildly) and for that I think TAP14 needs to get ratified, or more effort to accomodate the popular consumers out there.

Let me know your thoughts and thanks again.

Was failing since it matched entire output array and first line
is now the version line per TAP v13 spec.
Only expect now is that version line is first line, more to come.
Removed the previously added unit test which now is superflous.
@plroebuck
Copy link
Contributor

plroebuck commented Sep 20, 2018

Plan:

  • Leave Mocha TAP12 as is (default).
  • Use this PR's format as Mocha TAP13 (via reporterOptions.tap.version).
  • Future TAP14 format would add support for expected/actual in YAML block.

@mollstam, what do you think?

@mollstam
Copy link
Contributor Author

Plan:

  • Leave Mocha TAP12 as is (default).
  • Use this PR's format as Mocha TAP13 (via reporterOptions.tap.version).
  • Future TAP14 format would add support for expected/actual in YAML block.

@mollstam, what do you think?

Sounds good! I named the option spec before reading this, would you prefer version? I'm indifferent. 🙂

@mollstam
Copy link
Contributor Author

@plroebuck I think the expected/actual stuff can be added to the TAP13 producer since it's just extra data in the YAML, we wouldn't destroy anything.

I would like to also clean up the stack property to not include the message, but I'll have a look at that when I try to reach parity with other popular Producers, which will have to wait for next week.

Base.call(this, runner);

var self = this;
var n = 1;
Copy link
Contributor

@plroebuck plroebuck Sep 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n is same as runner.stats.tests. Remove.

};

TAPProducer.prototype.writeEnd = function(runner) {
console.log('# tests ' + (runner.stats.passes + runner.stats.failures));
Copy link
Contributor

@plroebuck plroebuck Sep 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed this, but think this is possibly an unreported existing bug.
runner.stats.tests will not agree with this math if pending tests occurred.

console.log('# tests ' + (runner.stats.passes + runner.stats.failures));
console.log('# pass ' + runner.stats.passes);
console.log('# fail ' + runner.stats.failures);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this include a summary line for pending tests?

var passes = 0;
var failures = 0;

var tapSpec = '12';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be numeric instead?


runner.on('start', function() {
var total = runner.grepTotal(runner.suite);
console.log('%d..%d', 1, total);
self.producer.writeStart(runner);
});

runner.on('test end', function() {
++n;
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove test end event handler.

expectedString = string;
return expectedTotal;
};
TAP.call({}, runner);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TAP.call({}, runner, options);

it('should be able to survive global mass extinction events', function(done) {
throw new Error("How do we even test for this without causing one?")
done();
});
Copy link
Contributor

@plroebuck plroebuck Sep 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a pending test somewhere in this fixture?

it('just might one day');

console.log('# tests ' + (passes + failures));
console.log('# pass ' + passes);
console.log('# fail ' + failures);
self.producer.writeEnd(runner);
Copy link
Contributor

@plroebuck plroebuck Sep 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.producer.writeEnd(stats);

console.log('ok %d %s', n, title(test));
};

TAPProducer.prototype.writeEnd = function(runner) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TAPProducer.prototype.writeEnd = function(stats) {

this.producer = new TAP13Producer();
} else {
this.producer = new TAP12Producer();
}

runner.on('start', function() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  runner.on('start', function() {
    var ntests = runner.grepTotal(runner.suite);
    self.producer.writeStart(ntests);
  });

Base.call(this, runner);

var self = this;
Copy link
Contributor

@plroebuck plroebuck Sep 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add this line:
var stats = runner.stats;

});

runner.on('test end', function() {
++n;
});

runner.on('pending', function(test) {
console.log('ok %d %s # SKIP -', n, title(test));
self.producer.writePending(n, test);
Copy link
Contributor

@plroebuck plroebuck Sep 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    var n = stats.tests;
    self.producer.writePending(n, test);

@plroebuck
Copy link
Contributor

In "test/reporters/tap.spec.js", let's make use of this function:

  var showOutput = false;

  /**
   * Run reporter using stream reassignment to capture output.
   *
   * @param {Object} stubSelf - Reporter-like stub instance
   * @param {Runner} runner - Mock instance
   * @param {boolean} [tee=false] - If `true`, echo captured output to screen
   */
  function runReporter(stubSelf, runner, tee) {
    // Reassign stream in order to make a copy of all reporter output
    var stdoutWrite = process.stdout.write;
    process.stdout.write = function(string, enc, callback) {
      stdout.push(string);
      if (tee) {
        stdoutWrite.call(process.stdout, string, enc, callback);
      }
    };

    // Invoke reporter
    TAP.call(stubSelf, runner);

    // Revert stream reassignment here so reporter output
    // can't be corrupted if any test assertions throw
    process.stdout.write = stdoutWrite;
  }

@JoshuaKGoldberg JoshuaKGoldberg added status: waiting for author waiting on response from OP - more information needed and removed status: pr needs work labels Dec 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: reporters involving a specific reporter semver-major implementation requires increase of "major" version number; "breaking changes" status: waiting for author waiting on response from OP - more information needed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants