@@ -13,31 +13,43 @@ var expect = require('expect.js'),
1313// `start()` with stub binaries that reproduce each output shape and assert the
1414// callback fires exactly once with an error, and that nothing throws.
1515//
16+ // Throws are observed via process.on('uncaughtExceptionMonitor'), which sees
17+ // every uncaught exception WITHOUT detaching mocha's own handler: a regression
18+ // fails the test with the real thrown error instead of a bare timeout, and no
19+ // unrelated error is ever swallowed.
20+ //
1621// Stubs are shell scripts, so these are skipped on Windows.
1722describe ( 'Local.start output handling' , function ( ) {
18- var stubDir , bsLocal , uncaught , mochaListeners ;
23+ var stubDir , bsLocal , uncaught , monitorListener ;
1924
2025 function stub ( name , body ) {
2126 var stubPath = path . join ( stubDir , name ) ;
2227 fs . writeFileSync ( stubPath , '#!/bin/sh\n' + body + '\n' , { mode : 0o755 } ) ;
2328 return stubPath ;
2429 }
2530
26- // Drives start() with the given stub and collects every callback invocation.
27- // The callback, any second (fall-through) invocation and any throw all come
28- // out of the same synchronous execFile exithandler, so settling two ticks
29- // after the first callback observes all of them deterministically — no
30- // fixed sleep. The guard timer only fires if the callback never does (the
31- // exact regression this suite exists to catch), so that failure mode shows
32- // up as an assertion on calls.length instead of a mocha timeout.
33- function run ( stubPath , done ) {
31+ // Drives start() with the given stub, then hands every callback invocation
32+ // plus the uncaught array to `assert`. The callback, any second
33+ // (fall-through) invocation and any throw all come out of the same
34+ // synchronous execFile exithandler, so settling two ticks after the first
35+ // callback observes all of them deterministically — no fixed sleep. The
36+ // guard timer only fires if the callback never does, so that failure mode
37+ // shows up as an assertion on calls.length instead of a mocha timeout.
38+ // Assertion failures are routed to mocha's done, never left to throw
39+ // asynchronously.
40+ function run ( stubPath , done , assert ) {
3441 var calls = [ ] , finished = false ;
3542
3643 function finish ( ) {
3744 if ( finished ) return ;
3845 finished = true ;
3946 clearTimeout ( guard ) ;
40- done ( calls , uncaught ) ;
47+ try {
48+ assert ( calls , uncaught ) ;
49+ done ( ) ;
50+ } catch ( assertionError ) {
51+ done ( assertionError ) ;
52+ }
4153 }
4254
4355 var guard = setTimeout ( finish , 5000 ) ;
@@ -67,18 +79,13 @@ describe('Local.start output handling', function () {
6779 // executes the real binary from the network.
6880 bsLocal . retriesLeft = 0 ;
6981
70- // Capture uncaughtExceptions for the duration of each test. Mocha's own
71- // handler is snapshotted here and restored in afterEach, so restoration
72- // survives a throwing test body.
7382 uncaught = [ ] ;
74- mochaListeners = process . listeners ( 'uncaughtException' ) ;
75- process . removeAllListeners ( 'uncaughtException' ) ;
76- process . on ( 'uncaughtException' , function ( err ) { uncaught . push ( err ) ; } ) ;
83+ monitorListener = function ( err ) { uncaught . push ( err ) ; } ;
84+ process . on ( 'uncaughtExceptionMonitor' , monitorListener ) ;
7785 } ) ;
7886
7987 afterEach ( function ( ) {
80- process . removeAllListeners ( 'uncaughtException' ) ;
81- mochaListeners . forEach ( function ( listener ) { process . on ( 'uncaughtException' , listener ) ; } ) ;
88+ process . removeListener ( 'uncaughtExceptionMonitor' , monitorListener ) ;
8289 } ) ;
8390
8491 if ( os . platform ( ) . match ( / w i n 3 2 / i) ) {
@@ -88,73 +95,88 @@ describe('Local.start output handling', function () {
8895
8996 it ( 'reports an error exactly once when the binary exits with no output' , function ( done ) {
9097 this . timeout ( 10000 ) ;
91- run ( stub ( 'empty-output.sh' , 'exit 0' ) , function ( calls , uncaught ) {
98+ run ( stub ( 'empty-output.sh' , 'exit 0' ) , done , function ( calls , uncaught ) {
9299 expect ( uncaught ) . to . eql ( [ ] ) ;
93100 expect ( calls . length ) . to . equal ( 1 ) ;
94101 expect ( calls [ 0 ] ) . to . be . an ( 'object' ) ;
95102 expect ( calls [ 0 ] . message ) . to . equal ( 'No output received' ) ;
96- done ( ) ;
97103 } ) ;
98104 } ) ;
99105
100106 it ( 'reports an error exactly once when the binary emits non-JSON output' , function ( done ) {
101107 this . timeout ( 10000 ) ;
102- run ( stub ( 'garbage-output.sh' , 'echo "segmentation fault"; exit 0' ) , function ( calls , uncaught ) {
108+ run ( stub ( 'garbage-output.sh' , 'echo "segmentation fault"; exit 0' ) , done , function ( calls , uncaught ) {
103109 expect ( uncaught ) . to . eql ( [ ] ) ;
104110 expect ( calls . length ) . to . equal ( 1 ) ;
105111 expect ( calls [ 0 ] . message ) . to . match ( / ^ I n v a l i d o u t p u t r e c e i v e d : / ) ;
106112 expect ( calls [ 0 ] . extra ) . to . match ( / s e g m e n t a t i o n f a u l t / ) ;
107- done ( ) ;
108113 } ) ;
109114 } ) ;
110115
111116 it ( 'reports an error exactly once when the binary emits literal null' , function ( done ) {
112117 this . timeout ( 10000 ) ;
113- run ( stub ( 'null-output.sh' , 'echo "null"; exit 0' ) , function ( calls , uncaught ) {
118+ run ( stub ( 'null-output.sh' , 'echo "null"; exit 0' ) , done , function ( calls , uncaught ) {
114119 expect ( uncaught ) . to . eql ( [ ] ) ;
115120 expect ( calls . length ) . to . equal ( 1 ) ;
116121 expect ( calls [ 0 ] . message ) . to . match ( / ^ I n v a l i d o u t p u t r e c e i v e d : / ) ;
117- done ( ) ;
118122 } ) ;
119123 } ) ;
120124
121125 it ( 'reports a fallback message when a non-connected payload has no message key' , function ( done ) {
122126 this . timeout ( 10000 ) ;
123- run ( stub ( 'no-message-key.sh' , 'echo \'{"state":"disconnected"}\'; exit 0' ) , function ( calls , uncaught ) {
127+ run ( stub ( 'no-message-key.sh' , 'echo \'{"state":"disconnected"}\'; exit 0' ) , done , function ( calls , uncaught ) {
124128 expect ( uncaught ) . to . eql ( [ ] ) ;
125129 expect ( calls . length ) . to . equal ( 1 ) ;
126130 expect ( calls [ 0 ] . message ) . to . equal ( 'Failed to start BrowserStack Local' ) ;
127- done ( ) ;
128131 } ) ;
129132 } ) ;
130133
131134 it ( 'reports a fallback message when the payload message is not a string' , function ( done ) {
132135 this . timeout ( 10000 ) ;
133- run ( stub ( 'non-string-message.sh' , 'echo \'{"state":"disconnected","message":42}\'; exit 0' ) , function ( calls , uncaught ) {
136+ run ( stub ( 'non-string-message.sh' , 'echo \'{"state":"disconnected","message":42}\'; exit 0' ) , done , function ( calls , uncaught ) {
134137 expect ( uncaught ) . to . eql ( [ ] ) ;
135138 expect ( calls . length ) . to . equal ( 1 ) ;
136139 expect ( calls [ 0 ] . message ) . to . equal ( 'Failed to start BrowserStack Local' ) ;
137- done ( ) ;
138140 } ) ;
139141 } ) ;
140142
141143 it ( 'surfaces the binary message when a non-connected payload carries one' , function ( done ) {
142144 this . timeout ( 10000 ) ;
143- run ( stub ( 'with-message.sh' , 'echo \'{"state":"disconnected","message":{"message":"Invalid key"}}\'; exit 0' ) , function ( calls , uncaught ) {
145+ run ( stub ( 'with-message.sh' , 'echo \'{"state":"disconnected","message":{"message":"Invalid key"}}\'; exit 0' ) , done , function ( calls , uncaught ) {
144146 expect ( uncaught ) . to . eql ( [ ] ) ;
145147 expect ( calls . length ) . to . equal ( 1 ) ;
146148 expect ( calls [ 0 ] . message ) . to . equal ( 'Invalid key' ) ;
147- done ( ) ;
148149 } ) ;
149150 } ) ;
150151
151152 it ( 'surfaces the JSON diagnostic when the binary exits non-zero with a payload' , function ( done ) {
152153 this . timeout ( 10000 ) ;
153- run ( stub ( 'nonzero-with-payload.sh' , 'echo \'{"state":"disconnected","message":{"message":"Invalid key"}}\'; exit 1' ) , function ( calls , uncaught ) {
154+ run ( stub ( 'nonzero-with-payload.sh' , 'echo \'{"state":"disconnected","message":{"message":"Invalid key"}}\'; exit 1' ) , done , function ( calls , uncaught ) {
154155 expect ( uncaught ) . to . eql ( [ ] ) ;
155156 expect ( calls . length ) . to . equal ( 1 ) ;
156157 expect ( calls [ 0 ] . message ) . to . equal ( 'Invalid key' ) ;
157- done ( ) ;
158+ } ) ;
159+ } ) ;
160+
161+ it ( 'keeps crash output as extra when the binary exits non-zero with non-JSON output' , function ( done ) {
162+ this . timeout ( 10000 ) ;
163+ run ( stub ( 'nonzero-garbage.sh' , 'echo "segmentation fault"; exit 139' ) , done , function ( calls , uncaught ) {
164+ expect ( uncaught ) . to . eql ( [ ] ) ;
165+ expect ( calls . length ) . to . equal ( 1 ) ;
166+ expect ( calls [ 0 ] . message ) . to . match ( / C o m m a n d f a i l e d / ) ;
167+ expect ( calls [ 0 ] . extra ) . to . match ( / s e g m e n t a t i o n f a u l t / ) ;
168+ } ) ;
169+ } ) ;
170+
171+ it ( 'records the daemon pid when a connected payload precedes a non-zero exit' , function ( done ) {
172+ this . timeout ( 10000 ) ;
173+ run ( stub ( 'connected-then-fail.sh' , 'echo \'{"state":"connected","pid":12345}\'; exit 1' ) , done , function ( calls , uncaught ) {
174+ expect ( uncaught ) . to . eql ( [ ] ) ;
175+ expect ( calls . length ) . to . equal ( 1 ) ;
176+ expect ( calls [ 0 ] ) . to . be . an ( 'object' ) ;
177+ // The daemon is up even though the foreground process errored;
178+ // stop() must still be able to reach it.
179+ expect ( bsLocal . pid ) . to . equal ( 12345 ) ;
158180 } ) ;
159181 } ) ;
160182
@@ -180,13 +202,12 @@ describe('Local.start output handling', function () {
180202 it ( 'truncates oversized non-JSON output attached to the error' , function ( done ) {
181203 this . timeout ( 10000 ) ;
182204 // ~64KB of garbage; extra should be capped at 1KB plus a truncation note.
183- run ( stub ( 'huge-output.sh' , 'head -c 65536 /dev/zero | tr "\\0" "x"; exit 0' ) , function ( calls , uncaught ) {
205+ run ( stub ( 'huge-output.sh' , 'head -c 65536 /dev/zero | tr "\\0" "x"; exit 0' ) , done , function ( calls , uncaught ) {
184206 expect ( uncaught ) . to . eql ( [ ] ) ;
185207 expect ( calls . length ) . to . equal ( 1 ) ;
186208 expect ( calls [ 0 ] . message ) . to . match ( / ^ I n v a l i d o u t p u t r e c e i v e d : / ) ;
187209 expect ( calls [ 0 ] . extra . length ) . to . be . below ( 1100 ) ;
188210 expect ( calls [ 0 ] . extra ) . to . match ( / \[ t r u n c a t e d \d + b y t e s \] $ / ) ;
189- done ( ) ;
190211 } ) ;
191212 } ) ;
192213} ) ;
0 commit comments