@@ -445,7 +445,7 @@ def stop_test(test_id):
445445 return redirect (url_for ('.by_id' , test_id = test .id ))
446446
447447
448- def _artifact_redirect (test_id , blob_path , filename = 'artifact' ):
448+ def _artifact_redirect (blob_path , filename = 'artifact' ):
449449 """Generate a signed URL for a GCS artifact and redirect, or 404."""
450450 from datetime import timedelta
451451
@@ -463,39 +463,37 @@ def _artifact_redirect(test_id, blob_path, filename='artifact'):
463463 return redirect (url )
464464
465465
466- @mod_test .route ('/<int:test_id>/binary' )
466+ @mod_test .route ('/<int:test_id>/binary' , methods = [ 'GET' ] )
467467def download_binary (test_id ):
468468 """Download the ccextractor binary used in a test (linux or windows)."""
469469 from run import storage_client_bucket
470470 # Try linux name first, then windows
471471 for name in ['ccextractor' , 'ccextractor.exe' ]:
472472 blob_path = f'test_artifacts/{ test_id } /{ name } '
473473 if storage_client_bucket .blob (blob_path ).exists ():
474- return _artifact_redirect (test_id , blob_path , filename = name )
474+ return _artifact_redirect (blob_path , filename = name )
475475 abort (404 )
476476
477477
478- @mod_test .route ('/<int:test_id>/coredump' )
478+ @mod_test .route ('/<int:test_id>/coredump' , methods = [ 'GET' ] )
479479def download_coredump (test_id ):
480480 """Download the coredump from a test, if one was produced."""
481481 return _artifact_redirect (
482- test_id ,
483482 f'test_artifacts/{ test_id } /coredump' ,
484483 filename = f'coredump-{ test_id } '
485484 )
486485
487486
488- @mod_test .route ('/<int:test_id>/combined-stdout' )
487+ @mod_test .route ('/<int:test_id>/combined-stdout' , methods = [ 'GET' ] )
489488def download_combined_stdout (test_id ):
490489 """Download the combined stdout/stderr log from all test invocations."""
491490 return _artifact_redirect (
492- test_id ,
493491 f'test_artifacts/{ test_id } /combined_stdout.log' ,
494492 filename = f'combined_stdout-{ test_id } .log'
495493 )
496494
497495
498- @mod_test .route ('/<int:test_id>/regression/<int:regression_test_id>/<int:output_id>/output-got' )
496+ @mod_test .route ('/<int:test_id>/regression/<int:regression_test_id>/<int:output_id>/output-got' , methods = [ 'GET' ] )
499497def download_output_got (test_id , regression_test_id , output_id ):
500498 """Download the actual output file from TestResults using DB hash."""
501499 rf = TestResultFile .query .filter (and_ (
@@ -505,16 +503,14 @@ def download_output_got(test_id, regression_test_id, output_id):
505503 )).first ()
506504 if rf is None or rf .got is None :
507505 abort (404 )
508- import os
509506 ext = os .path .splitext (rf .regression_test_output .filename_correct )[1 ]
510507 return _artifact_redirect (
511- test_id ,
512508 f'TestResults/{ rf .got } { ext } ' ,
513509 filename = f'output_got_{ regression_test_id } _{ output_id } { ext } '
514510 )
515511
516512
517- @mod_test .route ('/<int:test_id>/regression/<int:regression_test_id>/<int:output_id>/output-expected' )
513+ @mod_test .route ('/<int:test_id>/regression/<int:regression_test_id>/<int:output_id>/output-expected' , methods = [ 'GET' ] )
518514def download_output_expected (test_id , regression_test_id , output_id ):
519515 """Download the expected output file from TestResults using DB hash."""
520516 rf = TestResultFile .query .filter (and_ (
@@ -524,28 +520,107 @@ def download_output_expected(test_id, regression_test_id, output_id):
524520 )).first ()
525521 if rf is None :
526522 abort (404 )
527- import os
528523 ext = os .path .splitext (rf .regression_test_output .filename_correct )[1 ]
529524 return _artifact_redirect (
530- test_id ,
531525 f'TestResults/{ rf .expected } { ext } ' ,
532526 filename = f'output_expected_{ regression_test_id } _{ output_id } { ext } '
533527 )
534- @mod_test .route ('/<int:test_id>/sample/<int:sample_id>' )
528+ @mod_test .route ('/<int:test_id>/sample/<int:sample_id>' , methods = [ 'GET' ] )
535529def download_sample_ai (test_id , sample_id ):
536530 """Download the sample file for a regression test (no auth required for AI workflow)."""
537531 from mod_sample .models import Sample
538532 sample = Sample .query .filter (Sample .id == sample_id ).first ()
539533 if sample is None :
540534 abort (404 )
541535 return _artifact_redirect (
542- test_id ,
543536 f'TestFiles/{ sample .filename } ' ,
544537 filename = sample .original_name
545538 )
546539
547540
548- @mod_test .route ('/<int:test_id>/ai.json' )
541+ def _process_test_case (test_id , category_name , t_data ):
542+ """Helper function to process a single test case."""
543+ rt = t_data ['test' ]
544+ result = t_data ['result' ]
545+ is_error = t_data .get ('error' , False )
546+ result_files = t_data ['files' ]
547+
548+ outputs = []
549+ for expected_output in rt .output_files :
550+ if expected_output .ignore :
551+ continue
552+
553+ matched_rf = None
554+ for rf in result_files :
555+ if rf .test_id != - 1 and rf .regression_test_output_id == expected_output .id :
556+ matched_rf = rf
557+ break
558+
559+ got_url = None
560+ diff_url = None
561+
562+ if matched_rf and matched_rf .got is not None :
563+ got_url = url_for (
564+ '.download_output_got' ,
565+ test_id = test_id ,
566+ regression_test_id = rt .id ,
567+ output_id = expected_output .id ,
568+ _external = True
569+ )
570+ diff_url = url_for (
571+ '.generate_diff' ,
572+ test_id = test_id ,
573+ regression_test_id = rt .id ,
574+ output_id = expected_output .id ,
575+ to_view = 0 ,
576+ _external = True
577+ )
578+ else :
579+ # If test passed, got and expected match exactly.
580+ got_url = url_for (
581+ '.download_output_expected' ,
582+ test_id = test_id ,
583+ regression_test_id = rt .id ,
584+ output_id = expected_output .id ,
585+ _external = True
586+ )
587+
588+ output_entry = {
589+ 'output_id' : expected_output .id ,
590+ 'correct_extension' : expected_output .correct_extension ,
591+ 'expected_url' : url_for (
592+ '.download_output_expected' ,
593+ test_id = test_id ,
594+ regression_test_id = rt .id ,
595+ output_id = expected_output .id ,
596+ _external = True
597+ ),
598+ 'got_url' : got_url ,
599+ 'diff_url' : diff_url ,
600+ }
601+ outputs .append (output_entry )
602+
603+ return {
604+ 'regression_test_id' : rt .id ,
605+ 'category' : category_name ,
606+ 'sample_filename' : rt .sample .original_name ,
607+ 'sample_url' : url_for (
608+ '.download_sample_ai' ,
609+ test_id = test_id ,
610+ sample_id = rt .sample .id ,
611+ _external = True
612+ ),
613+ 'arguments' : rt .command ,
614+ 'result' : 'Fail' if is_error else 'Pass' ,
615+ 'exit_code' : result .exit_code if result else None ,
616+ 'expected_exit_code' : result .expected_rc if result else None ,
617+ 'runtime_ms' : result .runtime if result else None ,
618+ 'outputs' : outputs ,
619+ 'how_to_reproduce' : f'./ccextractor { rt .command } { rt .sample .original_name } ' ,
620+ }
621+
622+
623+ @mod_test .route ('/<int:test_id>/ai.json' , methods = ['GET' ])
549624def ai_json_endpoint (test_id ):
550625 """Structured JSON with download URLs for all artifacts — for AI agents."""
551626 from run import storage_client_bucket
@@ -573,89 +648,12 @@ def blob_exists(path):
573648 for category in results :
574649 for t_data in category ['tests' ]:
575650 total += 1
576- rt = t_data ['test' ]
577- result = t_data ['result' ]
578- is_error = t_data .get ('error' , False )
579- result_files = t_data ['files' ]
580-
581- if is_error :
651+ if t_data .get ('error' , False ):
582652 failed += 1
583653 else :
584654 passed += 1
585655
586- outputs = []
587- for expected_output in rt .output_files :
588- if expected_output .ignore :
589- continue
590-
591- matched_rf = None
592- for rf in result_files :
593- if rf .test_id != - 1 and rf .regression_test_output_id == expected_output .id :
594- matched_rf = rf
595- break
596-
597- got_url = None
598- diff_url = None
599-
600- if matched_rf and matched_rf .got is not None :
601- got_url = url_for (
602- '.download_output_got' ,
603- test_id = test_id ,
604- regression_test_id = rt .id ,
605- output_id = expected_output .id ,
606- _external = True
607- )
608- diff_url = url_for (
609- '.generate_diff' ,
610- test_id = test_id ,
611- regression_test_id = rt .id ,
612- output_id = expected_output .id ,
613- to_view = 0 ,
614- _external = True
615- )
616- else :
617- # If test passed, got and expected match exactly.
618- got_url = url_for (
619- '.download_output_expected' ,
620- test_id = test_id ,
621- regression_test_id = rt .id ,
622- output_id = expected_output .id ,
623- _external = True
624- )
625-
626- output_entry = {
627- 'output_id' : expected_output .id ,
628- 'correct_extension' : expected_output .correct_extension ,
629- 'expected_url' : url_for (
630- '.download_output_expected' ,
631- test_id = test_id ,
632- regression_test_id = rt .id ,
633- output_id = expected_output .id ,
634- _external = True
635- ),
636- 'got_url' : got_url ,
637- 'diff_url' : diff_url ,
638- }
639- outputs .append (output_entry )
640-
641- test_cases .append ({
642- 'regression_test_id' : rt .id ,
643- 'category' : category ['category' ].name ,
644- 'sample_filename' : rt .sample .original_name ,
645- 'sample_url' : url_for (
646- '.download_sample_ai' ,
647- test_id = test_id ,
648- sample_id = rt .sample .id ,
649- _external = True
650- ),
651- 'arguments' : rt .command ,
652- 'result' : 'Fail' if is_error else 'Pass' ,
653- 'exit_code' : result .exit_code if result else None ,
654- 'expected_exit_code' : result .expected_rc if result else None ,
655- 'runtime_ms' : result .runtime if result else None ,
656- 'outputs' : outputs ,
657- 'how_to_reproduce' : f'./ccextractor { rt .command } { rt .sample .original_name } ' ,
658- })
656+ test_cases .append (_process_test_case (test_id , category ['category' ].name , t_data ))
659657
660658 report = {
661659 'test_id' : test .id ,
0 commit comments