1
1
// This module tests that the classes generated in out.ts match the CSL API
2
2
// at the method level
3
3
import fs from "node:fs" ;
4
- import { ClassInfo , ClassRename , MethodInfo , ParamInfo , SomeType } from "../test_types" ;
4
+ import { ClassInfo , ClassRename , MethodComparisonResult , MethodInfo , ParamInfo , SomeType , TypeComparisonResult } from "../test_types" ;
5
5
import grammar , { TypeDefsSemantics } from "./grammar.ohm-bundle"
6
6
import { describe , test } from "@jest/globals" ;
7
7
@@ -190,6 +190,7 @@ const cdlClasses: Array<ClassInfo> = semantics(cdlMatch).classes();
190
190
let cdlClassesMap : Map < string , MethodInfo [ ] > = new Map ( cdlClasses . map ( ( cls ) => [ cls . name , cls . methods ] ) )
191
191
console . log ( "Extracting renaming exports from CDL type definitions..." ) ;
192
192
const classRenames : Array < ClassRename > = semantics ( cdlMatch ) . renames ( ) ;
193
+ const classRenamesMap : Map < string , string > = new Map ( classRenames . map ( ( rename ) => [ rename . originalName , rename . newName ] ) )
193
194
194
195
// We trace all the classes as parsed
195
196
if ( traceClassInfos ) {
@@ -201,11 +202,10 @@ if (traceClassInfos) {
201
202
}
202
203
}
203
204
204
- // Before filtering, we rename all classes that are re-exported with a different name
205
+ // Before filtering, we add all classes that are re-exported with a different name
205
206
for ( const rename of classRenames ) {
206
207
const clsValue = cdlClassesMap . get ( rename . originalName )
207
208
if ( clsValue ) {
208
- cdlClassesMap . delete ( rename . originalName ) ;
209
209
cdlClassesMap . set ( rename . newName , clsValue ) ;
210
210
}
211
211
}
@@ -277,9 +277,12 @@ for (const cls of cdlClasses) {
277
277
console . log ( "compareToCslTests.length: " , compareToCslTests . length )
278
278
console . log ( "compareToCdlTests.length: " , compareToCdlTests . length )
279
279
280
+ // We open a report file to write down each method comparison failure as we find it
281
+ let methodFailuresFile = fs . openSync ( "tests/reports/api_failing_methods.csv" , "w" ) ;
282
+ fs . writeFileSync ( methodFailuresFile , "Affected class,Method,Failure reason,Failure message\n" ) ;
283
+
280
284
// Used for debugging
281
- // 984
282
- const testN = 984 ;
285
+ const testN = 2752 ;
283
286
const testNConfig = { testTable : compareToCdlTests , srcMap : cslClassesMap } ;
284
287
test . skip ( `Test N. ${ testN } ` , ( ) => {
285
288
const { testTable, srcMap } = testNConfig ;
@@ -288,25 +291,19 @@ test.skip(`Test N. ${testN}`, () => {
288
291
} ) ;
289
292
290
293
// Tests
291
- describe ( "API coverage tests" , ( ) => {
292
- test ( "There should be no missing classes" , ( ) => {
293
- expect ( missingClasses ) . toHaveLength ( 0 ) ;
294
- } )
295
- test ( "There should be no missing methods" , ( ) => {
296
- expect ( missingMethods ) . toHaveLength ( 0 ) ;
297
- } )
298
- } )
299
- describe ( "Compare each CSL class/method to its CDL counterpart" , ( ) => {
300
- test . each ( compareToCslTests ) ( "($n) Comparing CDL's $class . $comparedToMethod.name to CSL's" , ( params ) => {
301
- compareToClass ( cdlClassesMap , params . class , params . comparedToMethod ) ;
302
- } )
303
- } ) ;
304
-
305
- describe ( "Compare each CDL class/method to its CSL counterpart" , ( ) => {
306
- test . each ( compareToCdlTests ) ( "($n) Comparing CSL's $class . $comparedToMethod.name to CDL's" , ( params ) => {
307
- compareToClass ( cslClassesMap , params . class , params . comparedToMethod ) ;
308
- } )
309
- } ) ;
294
+ describe ( "API coverage tests" , ( ) => {
295
+ test ( "There should be no missing classes" , ( ) => {
296
+ expect ( missingClasses ) . toHaveLength ( 0 ) ;
297
+ } )
298
+ test ( "There should be no missing methods" , ( ) => {
299
+ expect ( missingMethods ) . toHaveLength ( 0 ) ;
300
+ } )
301
+ } )
302
+ describe ( "Compare each CDL class/method to its CSL counterpart" , ( ) => {
303
+ test . each ( compareToCdlTests ) ( "($n) Comparing CSL's $class . $comparedToMethod.name to CDL's" , ( params ) => {
304
+ compareToClass ( cslClassesMap , params . class , params . comparedToMethod ) ;
305
+ } )
306
+ } ) ;
310
307
311
308
// Test Utils
312
309
function prettyClassInfo ( cls : ClassInfo ) : void {
@@ -320,36 +317,82 @@ function prettyClassInfo(cls: ClassInfo): void {
320
317
console . log ( msg ) ;
321
318
}
322
319
320
+ function prettyType ( ty : SomeType ) : string {
321
+ switch ( ty . tag ) {
322
+ case "simple" :
323
+ return ty . ident ;
324
+ case "array" :
325
+ return `${ prettyType ( ty . type ) } []` ;
326
+ case "tuple" :
327
+ let tyStrs = ty . types . map ( ( ty ) => prettyType ( ty ) ) ;
328
+ return `[${ tyStrs . join ( ", " ) } ]` ;
329
+ case "nullable" :
330
+ return `(${ prettyType ( ty . type ) } | undefined)`
331
+ case "object" : {
332
+ let msg = "{" ;
333
+ for ( const [ attrName , attrType ] of ty . attrMap . entries ( ) ) {
334
+ msg = `${ msg } ${ attrName } : ${ prettyType ( attrType ) } ;` ;
335
+ }
336
+ msg = `${ msg } }` ;
337
+ return msg ;
338
+ }
339
+ }
340
+ }
341
+
323
342
function compareToClass ( clss : Map < string , MethodInfo [ ] > , cls : string , comparedToMethod : MethodInfo ) {
324
343
const methods = clss . get ( cls ) ;
325
344
if ( methods ) {
326
345
const method = methods . find ( ( info ) => info . name == comparedToMethod . name )
327
- compareToMethod ( method , comparedToMethod ) ;
346
+ if ( method ) {
347
+ const cmpResult = compareToMethod ( method , comparedToMethod ) ;
348
+ // Before testing, we write the result in the report file
349
+ if ( cmpResult != "success" ) {
350
+ fs . writeFileSync ( methodFailuresFile , `${ cls } ,${ method . name } ,${ cmpResult . reason } ,${ cmpResult . msg } \n` ) ;
351
+ }
352
+ expect ( cmpResult ) . toStrictEqual ( "success" ) ;
353
+ }
328
354
}
329
355
}
330
356
331
- function compareToMethod ( method1 : MethodInfo | undefined , method2 : MethodInfo ) {
332
- if ( method1 ) {
333
- // Method names should match
334
- expect ( method1 . name ) . toEqual ( method2 . name ) ;
335
- // Return types should match
336
- compareTypes ( method1 . returnType , method2 . returnType ) ;
337
- // Static qualifiers should match
338
- expect ( method1 . static ) . toEqual ( method2 . static ) ;
339
- // Methods should have the same number of parameters
340
- expect ( method1 . params . length ) . toEqual ( method2 . params . length ) ;
341
- // All parameter types should match (names do not, however)
342
- for ( let i = 0 ; i < method1 . params . length ; i ++ ) {
343
- const param1 = method1 . params [ i ] ;
344
- const param2 = method2 . params [ i ] ;
345
- // Types should match
346
- compareTypes ( param1 . type , param2 . type ) ;
357
+ function compareToMethod ( method1 : MethodInfo , method2 : MethodInfo ) : MethodComparisonResult {
358
+ // Method names should match
359
+ if ( method1 . name != method2 . name ) {
360
+ return { reason : "method_names_dont_match" , msg : `Expected ${ method2 . name } , received ${ method1 . name } ` } ;
361
+ }
362
+ // Return types should match
363
+ const returnCmp = compareTypes ( method1 . returnType , method2 . returnType ) ;
364
+ if ( returnCmp != "success" ) {
365
+ return { reason : "return_types_dont_match" , msg : `Expected ${ prettyType ( returnCmp . expected ) } , received ${ prettyType ( returnCmp . received ) } ` } ;
366
+ }
367
+ // Static qualifiers should match
368
+ if ( method1 . static != method2 . static ) {
369
+ return { reason : "static_qualifiers_dont_match" , msg : `Expected ${ method2 . static } , received ${ method1 . static } ` } ;
370
+ }
371
+ // Methods should have the same number of parameters
372
+ if ( method1 . params . length != method2 . params . length ) {
373
+ return { reason : "number_of_parameters_doesnt_match" , msg : `Expected ${ method2 . params . length } , received ${ method1 . params . length } ` }
374
+ }
375
+ // All parameter types should match (names do not, however)
376
+ let paramErrMsgs : Array < string > = [ ] ;
377
+ for ( let i = 0 ; i < method1 . params . length ; i ++ ) {
378
+ const param1 = method1 . params [ i ] ;
379
+ const param2 = method2 . params [ i ] ;
380
+ // Types should match
381
+ const paramCmp = compareTypes ( param1 . type , param2 . type ) ;
382
+ if ( paramCmp != "success" ) {
383
+ paramErrMsgs . push ( `Expected ${ param1 . name } to have type ${ prettyType ( paramCmp . expected ) } , instead it has type ${ prettyType ( paramCmp . received ) } ` ) ;
347
384
}
348
385
}
386
+ if ( paramErrMsgs . length > 0 ) {
387
+ return { reason : "parameter_types_dont_match" , msg :paramErrMsgs . join ( " | " ) }
388
+ }
389
+
390
+ return "success" ;
349
391
}
350
392
351
393
// We normalize types. At runtime, `undefined` and `T` are equivalent, and so are `undefined | T`
352
394
// and `T`. For this reason, we strip the `undefined`s to make type comparisons simpler.
395
+ // We also take into account classes that are exported under a different name.
353
396
function normalizeType ( ty : SomeType ) : SomeType {
354
397
switch ( ty . tag ) {
355
398
case "nullable" : {
@@ -369,22 +412,74 @@ function normalizeType(ty: SomeType): SomeType {
369
412
return normalized ;
370
413
}
371
414
case "simple" : {
415
+ const renamedTo = classRenamesMap . get ( ty . ident ) ;
372
416
if ( ty . ident == "BigNum" ) {
373
417
return { tag : "simple" , ident : "bigint" }
418
+ } else if ( renamedTo ) {
419
+ return { tag : "simple" , ident : renamedTo } ;
374
420
} else {
375
421
return ty ;
376
422
}
377
423
}
378
424
}
379
425
}
380
426
381
- function compareTypes ( ty1 : SomeType , ty2 : SomeType ) {
427
+ function compareTypes ( ty1 : SomeType , ty2 : SomeType ) : TypeComparisonResult {
382
428
const nty1 = normalizeType ( ty1 ) ;
383
429
const nty2 = normalizeType ( ty2 ) ;
384
430
// if one of the types is undefined, then the types are equivalent
385
431
if ( ( nty1 . tag == "simple" && nty1 . ident == "undefined" ) || ( nty2 . tag == "simple" && nty2 . ident == "undefined" ) ) {
386
- expect ( true ) . toBeTruthy ( ) ;
432
+ return "success" ;
433
+ // otherwise, check for strict equality
434
+ } else if ( ! someTypeEquals ( nty1 , nty2 ) ) {
435
+ return { expected : nty2 , received : nty1 }
387
436
} else {
388
- expect ( nty1 ) . toEqual ( nty2 ) ;
437
+ return "success" ;
389
438
}
390
439
}
440
+
441
+ // strict SomeType equality
442
+ function someTypeEquals ( ty1 : SomeType , ty2 : SomeType ) {
443
+ if ( ty1 . tag == "simple" && ty2 . tag == "simple" ) {
444
+ // console.log("comparing simples")
445
+ return ty1 . ident === ty2 . ident ;
446
+ }
447
+
448
+ if ( ty1 . tag == "nullable" && ty2 . tag == "nullable" ) {
449
+ // console.log("comparing nullables")
450
+ return someTypeEquals ( ty1 . type , ty2 . type ) ;
451
+ }
452
+
453
+ if ( ty1 . tag == "array" && ty2 . tag == "array" ) {
454
+ // console.log("comparing arrays")
455
+ return someTypeEquals ( ty1 . type , ty2 . type ) ;
456
+ }
457
+
458
+ if ( ty1 . tag == "tuple" && ty2 . tag == "tuple" ) {
459
+ // console.log("comparing tuples")
460
+ if ( ty1 . types . length != ty2 . types . length ) {
461
+ return false ;
462
+ }
463
+ for ( let i = 0 ; i < ty1 . types . length ; i ++ ) {
464
+ if ( ! someTypeEquals ( ty1 . types [ i ] , ty2 . types [ i ] ) ) {
465
+ return false ;
466
+ }
467
+ }
468
+ return true ;
469
+ }
470
+
471
+ if ( ty1 . tag == "object" && ty2 . tag == "object" ) {
472
+ // console.log("comparing objects")
473
+ for ( const [ attrName , attrType1 ] of ty1 . attrMap . entries ( ) ) {
474
+ const attrType2 = ty2 . attrMap . get ( attrName ) ;
475
+ if ( ! attrType2 || ! someTypeEquals ( attrType1 , attrType2 ) ) {
476
+ return false ;
477
+ }
478
+ }
479
+ return true ;
480
+ }
481
+
482
+ // tags don't match
483
+ // console.log("tags don't match")
484
+ return false ;
485
+ }
0 commit comments