1111# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212# See the License for the specific language governing permissions and
1313# limitations under the License.
14- """Post-run audit for CBTS Layer C per-test coverage data quality.
14+ r """Post-run audit for CBTS Layer C per-test coverage data quality.
1515
1616Non-blocking report mode (Stage 1 of the rollout): emits a per-test CSV/JSON
1717report plus a summary, and only exits non-zero on *hard* pipeline-plumbing
8585 --out-csv ${JOB_WORKSPACE}/cbts_audit.csv \\
8686 --out-json ${JOB_WORKSPACE}/cbts_audit.json
8787"""
88+
8889import argparse
8990import ast
9091import csv
@@ -139,28 +140,31 @@ def _is_integration_test(test_id: str) -> bool:
139140@dataclass
140141class TestRecord :
141142 test_id : str
142- junit_status : str # passed / failed / error / skipped / NO_JUNIT_MATCH / unknown
143- cov_status : str # OK / MAIN_ONLY / EMPTY / MISSING
144- n_files : int # total product files (source filter already restricts to tensorrt_llm/)
145- n_engine_files : int # subset under _torch/pyexecutor or _torch/models -- the worker-side signal
146- n_engine_body_lines : int # lines in engine files MINUS def/class/decorator/import lines
147- # (shell-style hits get covered just by importing; this metric
148- # tells us whether engine function bodies were actually executed)
149- context_key : str # matched coverage context (may differ from test_id)
143+ junit_status : str # passed / failed / error / skipped / NO_JUNIT_MATCH / unknown
144+ cov_status : str # OK / MAIN_ONLY / EMPTY / MISSING
145+ n_files : int # total product files (source filter already restricts to tensorrt_llm/)
146+ n_engine_files : int # subset under _torch/pyexecutor or _torch/models -- the worker-side signal
147+ n_engine_body_lines : int # lines in engine files MINUS def/class/decorator/import lines
148+ # (shell-style hits get covered just by importing; this metric
149+ # tells us whether engine function bodies were actually executed)
150+ context_key : str # matched coverage context (may differ from test_id)
150151
151152
152153# --- junit -----------------------------------------------------------------
153154
155+
154156def _junit_class_part (classname : str , file_attr : str ) -> str :
155- """Strip the dotted module prefix from a junit ``classname`` when the
156- ``file`` attribute is present."""
157+ """Strip the dotted module prefix from a junit ``classname``.
158+
159+ Only does this when the ``file`` attribute is present.
160+ """
157161 if not classname or not file_attr :
158162 return ""
159163 mod_dotted = file_attr .replace ("/" , "." ).rsplit (".py" , 1 )[0 ]
160164 if classname == mod_dotted :
161165 return ""
162166 if classname .startswith (mod_dotted + "." ):
163- return classname [len (mod_dotted ) + 1 :]
167+ return classname [len (mod_dotted ) + 1 :]
164168 return classname
165169
166170
@@ -189,9 +193,7 @@ def _junit_to_nodeid(classname: str, name: str, file_attr: str) -> str:
189193 if not classname :
190194 return name
191195 parts = classname .split ("." )
192- first_class_idx = next (
193- (i for i , p in enumerate (parts ) if p [:1 ].isupper ()), None
194- )
196+ first_class_idx = next ((i for i , p in enumerate (parts ) if p [:1 ].isupper ()), None )
195197 if first_class_idx is None :
196198 file_path = "/" .join (parts ) + ".py"
197199 return f"{ file_path } ::{ name } "
@@ -230,15 +232,17 @@ def load_junit(path: Path) -> dict:
230232
231233# --- coverage db -----------------------------------------------------------
232234
235+
233236@functools .lru_cache (maxsize = None )
234237def _decl_import_lines (file_path : str ) -> frozenset :
235- """Lines that are def/class/decorator/import -- 'shell' code that's
236- counted as covered just by importing the module.
238+ """Lines that are def/class/decorator/import -- 'shell' code.
239+
240+ These count as covered just by importing the module.
237241
238242 For any file we can't parse (missing, syntax error, encoding), return
239243 an empty frozenset, which means we'll count ALL covered lines as
240244 body -- conservative for audit purposes (we'd rather over-count body
241- coverage than spuriously deflate it for unparseable files).
245+ coverage than spuriously deflate it for unparsable files).
242246 """
243247 try :
244248 with open (file_path ) as f :
@@ -294,6 +298,7 @@ def load_contexts(db_path: Path) -> dict:
294298
295299# --- matching --------------------------------------------------------------
296300
301+
297302def match_test_to_context (test_id : str , contexts : dict ) -> str :
298303 """Find the context for ``test_id``. Returns ``""`` if no match."""
299304 if test_id in contexts :
@@ -348,8 +353,7 @@ def classify(ctx_info, has_sentinel, junit_status="unknown", test_id="") -> str:
348353 return "TEST_FAILED"
349354 if ctx_info ["n_engine" ] == 0 :
350355 return "MAIN_ONLY"
351- if (_is_integration_test (test_id )
352- and not ctx_info .get ("hit_main_executor" , False )):
356+ if _is_integration_test (test_id ) and not ctx_info .get ("hit_main_executor" , False ):
353357 return "SUBPROCESS_LOST"
354358 return "OK"
355359
@@ -372,6 +376,7 @@ def _nodeid_sentinel_name(nodeid: str) -> str:
372376
373377# --- save error scan -------------------------------------------------------
374378
379+
375380def count_save_errors (log_path : Path ) -> int :
376381 count = 0
377382 with open (log_path , errors = "replace" ) as f :
@@ -383,18 +388,33 @@ def count_save_errors(log_path: Path) -> int:
383388
384389# --- outputs ---------------------------------------------------------------
385390
391+
386392def write_csv (path : Path , records ):
387393 with open (path , "w" , newline = "" ) as f :
388394 w = csv .writer (f )
389- w .writerow ([
390- "test_id" , "junit_status" , "cov_status" , "n_files" ,
391- "n_engine_files" , "n_engine_body_lines" , "context_key" ,
392- ])
395+ w .writerow (
396+ [
397+ "test_id" ,
398+ "junit_status" ,
399+ "cov_status" ,
400+ "n_files" ,
401+ "n_engine_files" ,
402+ "n_engine_body_lines" ,
403+ "context_key" ,
404+ ]
405+ )
393406 for r in records :
394- w .writerow ([
395- r .test_id , r .junit_status , r .cov_status , r .n_files ,
396- r .n_engine_files , r .n_engine_body_lines , r .context_key ,
397- ])
407+ w .writerow (
408+ [
409+ r .test_id ,
410+ r .junit_status ,
411+ r .cov_status ,
412+ r .n_files ,
413+ r .n_engine_files ,
414+ r .n_engine_body_lines ,
415+ r .context_key ,
416+ ]
417+ )
398418
399419
400420def write_json (path : Path , records , save_errs , db_path ):
@@ -413,10 +433,16 @@ def write_json(path: Path, records, save_errs, db_path):
413433# every run; the rest are only shown when non-zero, to keep the summary
414434# uncluttered when the relevant signal isn't present in this run.
415435_REPORTED_STATUSES = (
416- "OK" , "SUBPROCESS_LOST" , "MAIN_ONLY" , "EMPTY" ,
436+ "OK" ,
437+ "SUBPROCESS_LOST" ,
438+ "MAIN_ONLY" ,
439+ "EMPTY" ,
417440 "TEST_FAILED" ,
418- "NO_PRODUCT_CODE" , "FAILED_EARLY" ,
419- "MISSING" , "MISSING_LOST" , "MISSING_NO_PLUGIN" ,
441+ "NO_PRODUCT_CODE" ,
442+ "FAILED_EARLY" ,
443+ "MISSING" ,
444+ "MISSING_LOST" ,
445+ "MISSING_NO_PLUGIN" ,
420446)
421447_ALWAYS_SHOW = frozenset ({"OK" , "SUBPROCESS_LOST" , "MAIN_ONLY" , "EMPTY" })
422448
@@ -441,38 +467,41 @@ def print_summary(records, save_errs):
441467 if not examples :
442468 continue
443469 n_show = min (5 , len (examples ))
444- print (f"\n First { n_show } { status } tests "
445- f"(of { len (examples )} ):" )
470+ print (f"\n First { n_show } { status } tests (of { len (examples )} ):" )
446471 for r in examples [:n_show ]:
447- print (f" { r .test_id } "
448- f"(files={ r .n_files } , engine={ r .n_engine_files } , "
449- f"body_lines={ r .n_engine_body_lines } )" )
472+ print (
473+ f" { r .test_id } "
474+ f"(files={ r .n_files } , engine={ r .n_engine_files } , "
475+ f"body_lines={ r .n_engine_body_lines } )"
476+ )
450477
451478
452479# --- main ------------------------------------------------------------------
453480
481+
454482def main (argv = None ):
455483 p = argparse .ArgumentParser (description = __doc__ .splitlines ()[0 ])
456- p .add_argument ("--db" , type = Path , required = True ,
457- help = "combined .coverage.<stage> DB" )
458- p .add_argument ("--junit" , type = Path ,
459- help = "pytest junit.xml (enables MISSING detection)" )
460- p .add_argument ("--pytest-log" , type = Path ,
461- help = "pytest stdout/stderr log (scanned for save errors)" )
462- p .add_argument ("--sentinel-dir" , type = Path ,
463- help = "directory of per-test sentinel files written by "
464- "cbts_plugin (enables MISSING_LOST vs "
465- "MISSING_NO_PLUGIN split)" )
466- p .add_argument ("--out-csv" , type = Path ,
467- help = "per-test CSV report (recommended for triage)" )
468- p .add_argument ("--out-json" , type = Path ,
469- help = "per-test JSON report (recommended for trend metrics)" )
484+ p .add_argument ("--db" , type = Path , required = True , help = "combined .coverage.<stage> DB" )
485+ p .add_argument ("--junit" , type = Path , help = "pytest junit.xml (enables MISSING detection)" )
486+ p .add_argument (
487+ "--pytest-log" , type = Path , help = "pytest stdout/stderr log (scanned for save errors)"
488+ )
489+ p .add_argument (
490+ "--sentinel-dir" ,
491+ type = Path ,
492+ help = "directory of per-test sentinel files written by "
493+ "cbts_plugin (enables MISSING_LOST vs "
494+ "MISSING_NO_PLUGIN split)" ,
495+ )
496+ p .add_argument ("--out-csv" , type = Path , help = "per-test CSV report (recommended for triage)" )
497+ p .add_argument (
498+ "--out-json" , type = Path , help = "per-test JSON report (recommended for trend metrics)"
499+ )
470500 args = p .parse_args (argv )
471501
472502 # ---- Hard signals on the DB ----
473503 if not args .db .exists () or args .db .stat ().st_size == 0 :
474- print (f"FATAL: coverage DB { args .db } missing or empty" ,
475- file = sys .stderr )
504+ print (f"FATAL: coverage DB { args .db } missing or empty" , file = sys .stderr )
476505 return 2
477506
478507 contexts = load_contexts (args .db )
@@ -507,22 +536,20 @@ def main(argv=None):
507536 continue # NOT_RUN -- not a coverage gap
508537 ctx_name = match_test_to_context (test_id , contexts )
509538 ctx_info = contexts .get (ctx_name ) if ctx_name else None
510- has_sentinel = (
511- _nodeid_sentinel_name (test_id ) in sentinels
512- if sentinels_in_use else None
513- )
539+ has_sentinel = _nodeid_sentinel_name (test_id ) in sentinels if sentinels_in_use else None
514540 if ctx_name :
515541 matched_contexts .add (ctx_name )
516- records .append (TestRecord (
517- test_id = test_id ,
518- junit_status = status ,
519- cov_status = classify (ctx_info , has_sentinel ,
520- junit_status = status , test_id = test_id ),
521- n_files = ctx_info ["n_files" ] if ctx_info else 0 ,
522- n_engine_files = ctx_info ["n_engine" ] if ctx_info else 0 ,
523- n_engine_body_lines = ctx_info ["n_engine_body" ] if ctx_info else 0 ,
524- context_key = ctx_name ,
525- ))
542+ records .append (
543+ TestRecord (
544+ test_id = test_id ,
545+ junit_status = status ,
546+ cov_status = classify (ctx_info , has_sentinel , junit_status = status , test_id = test_id ),
547+ n_files = ctx_info ["n_files" ] if ctx_info else 0 ,
548+ n_engine_files = ctx_info ["n_engine" ] if ctx_info else 0 ,
549+ n_engine_body_lines = ctx_info ["n_engine_body" ] if ctx_info else 0 ,
550+ context_key = ctx_name ,
551+ )
552+ )
526553
527554 # Contexts with no matching junit row: still report them. Parametrized
528555 # tests, multi-call hooks, and fixture-level captures can land here.
@@ -531,15 +558,17 @@ def main(argv=None):
531558 for ctx_name , ctx_info in contexts .items ():
532559 if ctx_name in matched_contexts :
533560 continue
534- records .append (TestRecord (
535- test_id = ctx_name ,
536- junit_status = "NO_JUNIT_MATCH" if junit_tests else "unknown" ,
537- cov_status = classify (ctx_info , has_sentinel = None , test_id = ctx_name ),
538- n_files = ctx_info ["n_files" ],
539- n_engine_files = ctx_info ["n_engine" ],
540- n_engine_body_lines = ctx_info ["n_engine_body" ],
541- context_key = ctx_name ,
542- ))
561+ records .append (
562+ TestRecord (
563+ test_id = ctx_name ,
564+ junit_status = "NO_JUNIT_MATCH" if junit_tests else "unknown" ,
565+ cov_status = classify (ctx_info , has_sentinel = None , test_id = ctx_name ),
566+ n_files = ctx_info ["n_files" ],
567+ n_engine_files = ctx_info ["n_engine" ],
568+ n_engine_body_lines = ctx_info ["n_engine_body" ],
569+ context_key = ctx_name ,
570+ )
571+ )
543572
544573 # ---- Outputs ----
545574 if args .out_csv :
0 commit comments