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

Show full stack on failure #330

Merged
merged 1 commit into from
Dec 4, 2016
Merged

Conversation

AnIrishDuck
Copy link

I rebased #265 and fixed all the tests. The easiest solution to fixing most tests was to strip out the stack trace parts that will change from machine to machine.

@devtristan @ljharb

@@ -157,8 +157,10 @@ function encodeResult (res, count) {
if (res.at) {
output += inner + 'at: ' + res.at + '\n';
}
if (res.operator === 'error' && res.actual && res.actual.stack) {
var lines = String(res.actual.stack).split('\n');
var error = (res.actual instanceof Error ? res.actual : res.error)
Copy link
Collaborator

Choose a reason for hiding this comment

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

instanceof will fail on cross-realm Error instances (like from an iframe, or the vm module.

if (res.operator === 'error' && res.actual && res.actual.stack) {
var lines = String(res.actual.stack).split('\n');
var error = (res.actual instanceof Error ? res.actual : res.error)
var stack = typeof error === 'object' ? error.stack : null
Copy link
Collaborator

Choose a reason for hiding this comment

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

missing semicolon

@@ -2,13 +2,15 @@ var tape = require('../');
var tap = require('tap');
var concat = require('concat-stream');

var stripFullStack = require('./common').stripFullStack
Copy link
Collaborator

Choose a reason for hiding this comment

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

semicolon

Copy link
Author

Choose a reason for hiding this comment

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

Ugh, sorry. This is what happens when you copy/paste. Will fix all cases.

}

module.exports.stripFullStack = function (output) {
return output.replace(/^\W+at.*:\d+:\d+.*$\W/gm, '');
Copy link
Collaborator

Choose a reason for hiding this comment

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

Rather than just stripping it, I'd prefer to try to normalize the output across engines - for example, use try/catch to get an actual stack trace at runtime, and then using that to figure out how stack traces should work?

Copy link
Author

Choose a reason for hiding this comment

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

So, here's the first-order problems that checking full stacks with every test would create:

  1. The file names will change depending on the absolute directory of the git clone.
  2. Any changes to line position (adding or removing things from a test or even potentially to a file that contains code as part of a test traceback) will cause tests to fail.

These are the two biggest problems, and could be easily fixed by stripping the relevant volatile part of the stack. The deeper issue that can't be resolved is the structure of the stack itself. Any refactoring that adds or removes a method to any "failing stack" will now fail the test.

As a result, many trivial refactors would then result in lots of pointless new test failures. It was annoying enough to go through all these tests myself once to fix this issue. I don't want to put that burden on every future author.

Does that make sense? Maybe I'm just not understanding what you're expecting to happen when normalizing output across engines. Is there some deeper property of the stacks we want to test everywhere?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Re 1, we can determine the filename with __filename and process.cwd() and similar.
Changes to line position are a fair point, and i'd be fine normalizing all of those (ie, all line numbers would be normalized to 0 - so that relative line numbers mattered but not absolute numbers).

I guess I'm mainly concerned (especially because this regex is pretty inscrutable) that there will be bugs in the stack traces, and we won't know.

Copy link
Author

Choose a reason for hiding this comment

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

How about I modify this to something like verifyAndStripTraces, which strips all stacks from the generated YAML and checks:

  1. That the expected test file shows up somewhere in the stack trace
  2. That every line of the stack trace "looks right". Honestly, the only thing I can think of here is matching against some basic regex (or regexes, which might make things more understandable).

For more clarification, here's an example error, with the stacks still fully in the test output:

    +++ found                                                                                 
    --- wanted                                                                                
         actual: |-                                                                           
           {}                                                                                 
         stack: |-                                                                            
           Error: should be equivalent                                                        
    +          at Test.assert [as _assert] (/Users/fmurphy/src/tape/lib/test.js:216:54)       
    +          at Test.bound [as _assert] (/Users/fmurphy/src/tape/lib/test.js:65:32)         
    +          at Test.deepEqual.Test.deepEquals.Test.isEquivalent.Test.same (/Users/fmurphy/src/tape/lib/test.js:384:10)
    +          at Test.bound [as deepEqual] (/Users/fmurphy/src/tape/lib/test.js:65:32)       
    +          at Test.<anonymous> (/Users/fmurphy/src/tape/test/undef.js:36:11)              
    +          at Test.bound [as _cb] (/Users/fmurphy/src/tape/lib/test.js:65:32)             
    +          at Test.run (/Users/fmurphy/src/tape/lib/test.js:84:10)                        
    +          at Test.bound [as run] (/Users/fmurphy/src/tape/lib/test.js:65:32)             
    +          at Immediate.next (/Users/fmurphy/src/tape/lib/results.js:71:15)               
    +          at runCallback (timers.js:574:20)                                              
    +          at tryOnImmediate (timers.js:554:5)                                            
    +          at processImmediate [as _immediateCallback] (timers.js:533:5)                  
       ...  

If any refactoring adds or removes a function, pulls something into an anonymous function, or moves things into a new file ... then this stack trace will change. Even if we do obvious things, like strip line numbers and character positions, and normalize file paths.

Completely checking stack correctness would require sophisticated code analysis, which seems like overkill. The challenge here is thus figuring out the right tradeoff between "smart" parsing and effort involved.

Copy link
Collaborator

Choose a reason for hiding this comment

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

So, I think that it can be simplified by replacing process.cwd() with the string "$PWD", for example - and line numbers replaced :[0-9]+:[0-9]+ with :#:# - what other normalization would be needed there?

Copy link
Author

Choose a reason for hiding this comment

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

The problem is, stacks change. All the time. Even running the same test in node version 0.10.0 (which, guessing by the test matrix, you guys want to support) gives a different stack:

    +++ found                                                                  
    --- wanted                                                                 
         actual: |-                                                            
           {}                                                                  
         stack: |-                                                             
           Error: should be equivalent                                         
    +          at Test.assert (/Users/fmurphy/src/tape/lib/test.js:216:54)     
    +          at Test.bound [as _assert] (/Users/fmurphy/src/tape/lib/test.js:65:32)
    +          at Test.deepEqual.Test.deepEquals.Test.isEquivalent.Test.same (/Users/fmurphy/src/tape/lib/test.js:384:10)
    +          at Test.bound [as deepEqual] (/Users/fmurphy/src/tape/lib/test.js:65:32)
    +          at Test.<anonymous> (/Users/fmurphy/src/tape/test/undef.js:36:11)
    +          at Test.bound [as _cb] (/Users/fmurphy/src/tape/lib/test.js:65:32)
    +          at Test.run (/Users/fmurphy/src/tape/lib/test.js:84:10)         
    +          at Test.bound [as run] (/Users/fmurphy/src/tape/lib/test.js:65:32)
    +          at Object.next [as _onImmediate] (/Users/fmurphy/src/tape/lib/results.js:71:15)
    +          at processImmediate [as _immediateCallback] (timers.js:309:15)  
       ...  

Because, at some point, someone refactored the timers core library in node (and added the runCallback and tryOnImmediate frames). What normalization method would reduce both stacks to the same value? One has more frames than the other...

Copy link
Author

@AnIrishDuck AnIrishDuck Oct 19, 2016

Choose a reason for hiding this comment

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

We could strip out all frames that don't involve the test/ directory. So the result would look like this:

           Error: should be equivalent                                         
              [... frames omitted ...]
              at Test.<anonymous> ($BASE/test/undef.js:$LINE:$COLUMN)
              [... frames omitted ...]

Would that be a good compromise?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that would probably suffice, thanks for investigating!

@@ -3,6 +3,8 @@ var path = require('path');
var spawn = require('child_process').spawn;
var concat = require('concat-stream');

var stripFullStack = require('./common').stripFullStack
Copy link
Collaborator

Choose a reason for hiding this comment

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

;

@@ -3,12 +3,14 @@ var tape = require('../');
var tap = require('tap');
var concat = require('concat-stream');

var stripFullStack = require('./common').stripFullStack
Copy link
Collaborator

Choose a reason for hiding this comment

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

;

Copy link
Collaborator

@ljharb ljharb left a comment

Choose a reason for hiding this comment

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

Seems like tests are failing on node <= 4

@@ -157,8 +157,10 @@ function encodeResult (res, count) {
if (res.at) {
output += inner + 'at: ' + res.at + '\n';
}
if (res.operator === 'error' && res.actual && res.actual.stack) {
var lines = String(res.actual.stack).split('\n');
var error = (res.actual instanceof Error ? res.actual : res.error);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd prefer not to use instanceof, since it's not reliable across realms. (same as #330 (comment))

Copy link
Author

Choose a reason for hiding this comment

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

Right, am still thinking about how to test / fix this. My current plan is to create a new test that throws an error from the node vm module. Does that sound like a good plan? Any suggestions on actually fixing this issue?

Copy link
Collaborator

Choose a reason for hiding this comment

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

As a test that's great as long as the test is skipped in non-node environments.

For actually fixing it, it's probably easier to just continue ducktyping the existence of a stack property.

if (res.operator === 'error' && res.actual && res.actual.stack) {
var lines = String(res.actual.stack).split('\n');
var error = (res.actual instanceof Error ? res.actual : res.error);
var stack = typeof error === 'object' ? error.stack : null;
Copy link
Collaborator

Choose a reason for hiding this comment

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

this won't catch when error is null

Copy link
Author

Choose a reason for hiding this comment

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

Will fix.

@AnIrishDuck
Copy link
Author

I'm hoping to get to the rest of the comments tomorrow, just was trying to be sure that Travis was happy. Checking errors on node 5 now.

@AnIrishDuck
Copy link
Author

@ljharb - I think it should be good to go now. I was never able to create a failing test with the vm module. For comparison functions i.e. vm.runInNewContext('t.equal(1,2)'), the result reporting would happen inside the vm realm, and the check would succeed.

For error-throw checks, I also tried doing:

t.throws(function () {
    vm.runInNewContext('throw new Error(\'CROSS\')');
}, /DOMAIN/);

The problem there is that every _assert for error testing sets actual and error to the same exception. Thus, the check was still harmless. Regardless, I think the new code is more foolproof, and doesn't implicate the same realm concerns.

Let me know if modifications are needed.


var actualStack = res.actual && res.actual.stack;
var errorStack = res.error && res.error.stack;
var stack = defined(actualStack, errorStack);
Copy link
Collaborator

Choose a reason for hiding this comment

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

why defined and not actualStack || errorStack? Are we worried about a stack trace that's falsy but not undefined?

Copy link
Author

Choose a reason for hiding this comment

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

Nah, it just seemed like the right thing to me. I don't think there will be any practical difference between the two, so I'll switch it if you want.

Copy link
Collaborator

Choose a reason for hiding this comment

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

nah, it's not adding a dep so i guess it's fine

@AnIrishDuck
Copy link
Author

@ljharb - anything needed from me to move this forward? I just checked back and I think I addressed all of your issues?

Copy link
Collaborator

@ljharb ljharb left a comment

Choose a reason for hiding this comment

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

LGTM - I'll test and merge this weekend.

@OliverJAsh
Copy link

I'm only seeing one line of the stack trace on the latest version of tape. How do I get this working?

@ljharb
Copy link
Collaborator

ljharb commented Apr 15, 2017

@OliverJAsh a version containing this change has not yet been released.

ljharb added a commit that referenced this pull request Jun 26, 2017
 - [Fix] fix spurious "test exited without ending" (#223)
 - [New] show full error stack on failure (#330)

 - [Deps] update `resolve`, `object-inspect`, `glob`
 - [Dev Deps] update `tap`, `concat-stream`, `js-yaml`

 - [Tests] fix stack differences on node 0.8
 - [Tests] npm v4.6+ breaks on node < v1, npm v5+ breaks on node < v4
 - [Tests] on `node` `v8`; no need for sudo; `v0.8` passes now; allow v5/v7/iojs to fail.
@holgerd77
Copy link

Hmm, is it actually possible to make this change optional? We have downgraded our tape dependency due to this change, since in our setup the stack trace is useless information and this extremely bloats the test output makes debugging much harder.

@ljharb
Copy link
Collaborator

ljharb commented Sep 22, 2017

While I wouldn't say a short stack trace is a part of the contract for tape, if you want to open a new issue, I would support an option that limits the number of lines of a stack trace shown.

@holgerd77
Copy link

@ljharb Thanks a lot, have done this: #397

if (res.operator === 'error' && res.actual && res.actual.stack) {
var lines = String(res.actual.stack).split('\n');

var actualStack = res.actual && res.actual.stack;
Copy link
Contributor

Choose a reason for hiding this comment

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants