Skip to content

Commit b048eae

Browse files
avivkellertargos
authored andcommitted
test_runner: reimplement assert.ok to allow stack parsing
PR-URL: #54776 Refs: nodejs/help#4461 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
1 parent a983726 commit b048eae

4 files changed

Lines changed: 303 additions & 269 deletions

File tree

‎lib/assert.js‎

Lines changed: 1 addition & 268 deletions
Original file line numberDiff line numberDiff line change
@@ -24,31 +24,21 @@ const {
2424
ArrayPrototypeIndexOf,
2525
ArrayPrototypeJoin,
2626
ArrayPrototypePush,
27-
ArrayPrototypeShift,
2827
ArrayPrototypeSlice,
2928
Error,
30-
ErrorCaptureStackTrace,
31-
FunctionPrototypeBind,
3229
NumberIsNaN,
3330
ObjectAssign,
3431
ObjectIs,
3532
ObjectKeys,
3633
ObjectPrototypeIsPrototypeOf,
3734
ReflectApply,
3835
RegExpPrototypeExec,
39-
RegExpPrototypeSymbolReplace,
40-
SafeMap,
4136
String,
42-
StringPrototypeCharCodeAt,
43-
StringPrototypeIncludes,
4437
StringPrototypeIndexOf,
45-
StringPrototypeReplace,
4638
StringPrototypeSlice,
4739
StringPrototypeSplit,
48-
StringPrototypeStartsWith,
4940
}=primordials;
5041

51-
const{ Buffer }=require('buffer');
5242
const{
5343
codes: {
5444
ERR_AMBIGUOUS_ARGUMENT,
@@ -57,53 +47,27 @@ const {
5747
ERR_INVALID_RETURN_VALUE,
5848
ERR_MISSING_ARGS,
5949
},
60-
isErrorStackTraceLimitWritable,
61-
overrideStackTrace,
6250
}=require('internal/errors');
6351
constAssertionError=require('internal/assert/assertion_error');
64-
const{ openSync, closeSync, readSync }=require('fs');
6552
const{ inspect }=require('internal/util/inspect');
6653
const{ isPromise, isRegExp }=require('internal/util/types');
67-
const{EOL}=require('internal/constants');
68-
const{ BuiltinModule }=require('internal/bootstrap/realm');
6954
const{ isError, deprecate }=require('internal/util');
55+
const{ innerOk }=require('internal/assert/utils');
7056

71-
consterrorCache=newSafeMap();
7257
constCallTracker=require('internal/assert/calltracker');
7358
const{
7459
validateFunction,
7560
}=require('internal/validators');
76-
const{ fileURLToPath }=require('internal/url');
7761

7862
letisDeepEqual;
7963
letisDeepStrictEqual;
80-
letparseExpressionAt;
81-
letfindNodeAround;
82-
lettokenizer;
83-
letdecoder;
8464

8565
functionlazyLoadComparison(){
8666
constcomparison=require('internal/util/comparisons');
8767
isDeepEqual=comparison.isDeepEqual;
8868
isDeepStrictEqual=comparison.isDeepStrictEqual;
8969
}
9070

91-
// Escape control characters but not \n and \t to keep the line breaks and
92-
// indentation intact.
93-
// eslint-disable-next-line no-control-regex
94-
constescapeSequencesRegExp=/[\x00-\x08\x0b\x0c\x0e-\x1f]/g;
95-
constmeta=[
96-
'\\u0000','\\u0001','\\u0002','\\u0003','\\u0004',
97-
'\\u0005','\\u0006','\\u0007','\\b','',
98-
'','\\u000b','\\f','','\\u000e',
99-
'\\u000f','\\u0010','\\u0011','\\u0012','\\u0013',
100-
'\\u0014','\\u0015','\\u0016','\\u0017','\\u0018',
101-
'\\u0019','\\u001a','\\u001b','\\u001c','\\u001d',
102-
'\\u001e','\\u001f',
103-
];
104-
105-
constescapeFn=(str)=>meta[StringPrototypeCharCodeAt(str,0)];
106-
10771
letwarned=false;
10872

10973
// The assert module provides functions that throw
@@ -178,237 +142,6 @@ assert.fail = fail;
178142
// The AssertionError is defined in internal/error.
179143
assert.AssertionError=AssertionError;
180144

181-
functionfindColumn(fd,column,code){
182-
if(code.length>column+100){
183-
try{
184-
returnparseCode(code,column);
185-
}catch{
186-
// End recursion in case no code could be parsed. The expression should
187-
// have been found after 2500 characters, so stop trying.
188-
if(code.length-column>2500){
189-
// eslint-disable-next-line no-throw-literal
190-
thrownull;
191-
}
192-
}
193-
}
194-
// Read up to 2500 bytes more than necessary in columns. That way we address
195-
// multi byte characters and read enough data to parse the code.
196-
constbytesToRead=column-code.length+2500;
197-
constbuffer=Buffer.allocUnsafe(bytesToRead);
198-
constbytesRead=readSync(fd,buffer,0,bytesToRead);
199-
code+=decoder.write(buffer.slice(0,bytesRead));
200-
// EOF: fast path.
201-
if(bytesRead<bytesToRead){
202-
returnparseCode(code,column);
203-
}
204-
// Read potentially missing code.
205-
returnfindColumn(fd,column,code);
206-
}
207-
208-
functiongetCode(fd,line,column){
209-
letbytesRead=0;
210-
if(line===0){
211-
// Special handle line number one. This is more efficient and simplifies the
212-
// rest of the algorithm. Read more than the regular column number in bytes
213-
// to prevent multiple reads in case multi byte characters are used.
214-
returnfindColumn(fd,column,'');
215-
}
216-
letlines=0;
217-
// Prevent blocking the event loop by limiting the maximum amount of
218-
// data that may be read.
219-
letmaxReads=32;// bytesPerRead * maxReads = 512 KiB
220-
constbytesPerRead=16384;
221-
// Use a single buffer up front that is reused until the call site is found.
222-
letbuffer=Buffer.allocUnsafe(bytesPerRead);
223-
while(maxReads--!==0){
224-
// Only allocate a new buffer in case the needed line is found. All data
225-
// before that can be discarded.
226-
buffer=lines<line ? buffer : Buffer.allocUnsafe(bytesPerRead);
227-
bytesRead=readSync(fd,buffer,0,bytesPerRead);
228-
// Read the buffer until the required code line is found.
229-
for(leti=0;i<bytesRead;i++){
230-
if(buffer[i]===10&&++lines===line){
231-
// If the end of file is reached, directly parse the code and return.
232-
if(bytesRead<bytesPerRead){
233-
returnparseCode(buffer.toString('utf8',i+1,bytesRead),column);
234-
}
235-
// Check if the read code is sufficient or read more until the whole
236-
// expression is read. Make sure multi byte characters are preserved
237-
// properly by using the decoder.
238-
constcode=decoder.write(buffer.slice(i+1,bytesRead));
239-
returnfindColumn(fd,column,code);
240-
}
241-
}
242-
}
243-
}
244-
245-
functionparseCode(code,offset){
246-
// Lazy load acorn.
247-
if(parseExpressionAt===undefined){
248-
constParser=require('internal/deps/acorn/acorn/dist/acorn').Parser;
249-
({ findNodeAround }=require('internal/deps/acorn/acorn-walk/dist/walk'));
250-
251-
parseExpressionAt=FunctionPrototypeBind(Parser.parseExpressionAt,Parser);
252-
tokenizer=FunctionPrototypeBind(Parser.tokenizer,Parser);
253-
}
254-
letnode;
255-
letstart;
256-
// Parse the read code until the correct expression is found.
257-
for(consttokenoftokenizer(code,{ecmaVersion: 'latest'})){
258-
start=token.start;
259-
if(start>offset){
260-
// No matching expression found. This could happen if the assert
261-
// expression is bigger than the provided buffer.
262-
break;
263-
}
264-
try{
265-
node=parseExpressionAt(code,start,{ecmaVersion: 'latest'});
266-
// Find the CallExpression in the tree.
267-
node=findNodeAround(node,offset,'CallExpression');
268-
if(node?.node.end>=offset){
269-
return[
270-
node.node.start,
271-
StringPrototypeReplace(StringPrototypeSlice(code,
272-
node.node.start,node.node.end),
273-
escapeSequencesRegExp,escapeFn),
274-
];
275-
}
276-
// eslint-disable-next-line no-unused-vars
277-
}catch(err){
278-
continue;
279-
}
280-
}
281-
// eslint-disable-next-line no-throw-literal
282-
thrownull;
283-
}
284-
285-
functiongetErrMessage(message,fn){
286-
consttmpLimit=Error.stackTraceLimit;
287-
consterrorStackTraceLimitIsWritable=isErrorStackTraceLimitWritable();
288-
// Make sure the limit is set to 1. Otherwise it could fail (<= 0) or it
289-
// does to much work.
290-
if(errorStackTraceLimitIsWritable)Error.stackTraceLimit=1;
291-
// We only need the stack trace. To minimize the overhead use an object
292-
// instead of an error.
293-
consterr={};
294-
ErrorCaptureStackTrace(err,fn);
295-
if(errorStackTraceLimitIsWritable)Error.stackTraceLimit=tmpLimit;
296-
297-
overrideStackTrace.set(err,(_,stack)=>stack);
298-
constcall=err.stack[0];
299-
300-
letfilename=call.getFileName();
301-
constline=call.getLineNumber()-1;
302-
letcolumn=call.getColumnNumber()-1;
303-
letidentifier;
304-
letcode;
305-
306-
if(filename){
307-
identifier=`${filename}${line}${column}`;
308-
309-
// Skip Node.js modules!
310-
if(StringPrototypeStartsWith(filename,'node:')&&
311-
BuiltinModule.exists(StringPrototypeSlice(filename,5))){
312-
errorCache.set(identifier,undefined);
313-
return;
314-
}
315-
}else{
316-
returnmessage;
317-
}
318-
319-
if(errorCache.has(identifier)){
320-
returnerrorCache.get(identifier);
321-
}
322-
323-
letfd;
324-
try{
325-
// Set the stack trace limit to zero. This makes sure unexpected token
326-
// errors are handled faster.
327-
if(errorStackTraceLimitIsWritable)Error.stackTraceLimit=0;
328-
329-
if(filename){
330-
if(decoder===undefined){
331-
const{ StringDecoder }=require('string_decoder');
332-
decoder=newStringDecoder('utf8');
333-
}
334-
335-
// ESM file prop is a file proto. Convert that to path.
336-
// This ensure opensync will not throw ENOENT for ESM files.
337-
constfileProtoPrefix='file://';
338-
if(StringPrototypeStartsWith(filename,fileProtoPrefix)){
339-
filename=fileURLToPath(filename);
340-
}
341-
342-
fd=openSync(filename,'r',0o666);
343-
// Reset column and message.
344-
({0: column,1: message}=getCode(fd,line,column));
345-
// Flush unfinished multi byte characters.
346-
decoder.end();
347-
}else{
348-
for(leti=0;i<line;i++){
349-
code=StringPrototypeSlice(code,
350-
StringPrototypeIndexOf(code,'\n')+1);
351-
}
352-
({0: column,1: message}=parseCode(code,column));
353-
}
354-
// Always normalize indentation, otherwise the message could look weird.
355-
if(StringPrototypeIncludes(message,'\n')){
356-
if(EOL==='\r\n'){
357-
message=RegExpPrototypeSymbolReplace(/\r\n/g,message,'\n');
358-
}
359-
constframes=StringPrototypeSplit(message,'\n');
360-
message=ArrayPrototypeShift(frames);
361-
for(constframeofframes){
362-
letpos=0;
363-
while(pos<column&&(frame[pos]===' '||frame[pos]==='\t')){
364-
pos++;
365-
}
366-
message+=`\n ${StringPrototypeSlice(frame,pos)}`;
367-
}
368-
}
369-
message=`The expression evaluated to a falsy value:\n\n ${message}\n`;
370-
// Make sure to always set the cache! No matter if the message is
371-
// undefined or not
372-
errorCache.set(identifier,message);
373-
374-
returnmessage;
375-
}catch{
376-
// Invalidate cache to prevent trying to read this part again.
377-
errorCache.set(identifier,undefined);
378-
}finally{
379-
// Reset limit.
380-
if(errorStackTraceLimitIsWritable)Error.stackTraceLimit=tmpLimit;
381-
if(fd!==undefined)
382-
closeSync(fd);
383-
}
384-
}
385-
386-
functioninnerOk(fn,argLen,value,message){
387-
if(!value){
388-
letgeneratedMessage=false;
389-
390-
if(argLen===0){
391-
generatedMessage=true;
392-
message='No value argument passed to `assert.ok()`';
393-
}elseif(message==null){
394-
generatedMessage=true;
395-
message=getErrMessage(message,fn);
396-
}elseif(isError(message)){
397-
throwmessage;
398-
}
399-
400-
consterr=newAssertionError({
401-
actual: value,
402-
expected: true,
403-
message,
404-
operator: '==',
405-
stackStartFn: fn,
406-
});
407-
err.generatedMessage=generatedMessage;
408-
throwerr;
409-
}
410-
}
411-
412145
/**
413146
* Pure assertion tests whether a value is truthy, as determined
414147
* by !!value.

0 commit comments

Comments
 (0)