2121
2222mod_test = Blueprint ('test' , __name__ )
2323
24+ CCEXTRACTOR_WIN_BINARY = 'ccextractor.exe'
25+ CCEXTRACTOR_LINUX_BINARY = 'ccextractor'
26+
2427
2528@mod_test .before_app_request
2629def before_app_request () -> None :
@@ -367,6 +370,7 @@ def generate_diff(test_id: int, regression_test_id: int, output_id: int, to_view
367370
368371
369372@mod_test .route ('/log-files/<test_id>' )
373+ @login_required
370374def download_build_log_file (test_id ):
371375 """
372376 Serve download of build log.
@@ -464,18 +468,18 @@ def _artifact_redirect(blob_path, filename='artifact'):
464468
465469
466470@mod_test .route ('/<int:test_id>/binary' , methods = ['GET' ])
471+ @login_required
467472def download_binary (test_id ):
468- """Download the ccextractor binary used in a test (linux or windows)."""
469- from run import storage_client_bucket
470- # Try linux name first, then windows
471- for name in ['ccextractor' , 'ccextractor.exe' ]:
472- blob_path = f'test_artifacts/{ test_id } /{ name } '
473- if storage_client_bucket .blob (blob_path ).exists ():
474- return _artifact_redirect (blob_path , filename = name )
475- abort (404 )
473+ """Download the ccextractor binary used in a test."""
474+ test = Test .query .filter (Test .id == test_id ).first ()
475+ if test is None :
476+ abort (404 )
477+ name = CCEXTRACTOR_LINUX_BINARY if test .platform == TestPlatform .linux else CCEXTRACTOR_WIN_BINARY
478+ return _artifact_redirect (f'test_artifacts/{ test_id } /{ name } ' , filename = name )
476479
477480
478481@mod_test .route ('/<int:test_id>/coredump' , methods = ['GET' ])
482+ @login_required
479483def download_coredump (test_id ):
480484 """Download the coredump from a test, if one was produced."""
481485 return _artifact_redirect (
@@ -485,6 +489,7 @@ def download_coredump(test_id):
485489
486490
487491@mod_test .route ('/<int:test_id>/combined-stdout' , methods = ['GET' ])
492+ @login_required
488493def download_combined_stdout (test_id ):
489494 """Download the combined stdout/stderr log from all test invocations."""
490495 return _artifact_redirect (
@@ -494,6 +499,7 @@ def download_combined_stdout(test_id):
494499
495500
496501@mod_test .route ('/<int:test_id>/regression/<int:regression_test_id>/<int:output_id>/output-got' , methods = ['GET' ])
502+ @login_required
497503def download_output_got (test_id , regression_test_id , output_id ):
498504 """Download the actual output file from TestResults using DB hash."""
499505 rf = TestResultFile .query .filter (and_ (
@@ -511,6 +517,7 @@ def download_output_got(test_id, regression_test_id, output_id):
511517
512518
513519@mod_test .route ('/<int:test_id>/regression/<int:regression_test_id>/<int:output_id>/output-expected' , methods = ['GET' ])
520+ @login_required
514521def download_output_expected (test_id , regression_test_id , output_id ):
515522 """Download the expected output file from TestResults using DB hash."""
516523 rf = TestResultFile .query .filter (and_ (
@@ -526,8 +533,9 @@ def download_output_expected(test_id, regression_test_id, output_id):
526533 filename = f'output_expected_{ regression_test_id } _{ output_id } { ext } '
527534 )
528535@mod_test .route ('/<int:test_id>/sample/<int:sample_id>' , methods = ['GET' ])
536+ @login_required
529537def download_sample_ai (test_id , sample_id ):
530- """Download the sample file for a regression test (no auth required for AI workflow) ."""
538+ """Download the sample file for a regression test."""
531539 from mod_sample .models import Sample
532540 sample = Sample .query .filter (Sample .id == sample_id ).first ()
533541 if sample is None :
@@ -538,75 +546,69 @@ def download_sample_ai(test_id, sample_id):
538546 )
539547
540548
541- def _process_test_case (test_id , category_name , t_data ):
542- """Helper function to process a single test case."""
549+ def _build_output_entry (test_id , rt , expected_output , result_files ):
550+ """Build a single output entry dict for the ai.json response."""
551+ matched_rf = next (
552+ (rf for rf in result_files
553+ if rf .test_id != - 1 and rf .regression_test_output_id == expected_output .id ),
554+ None
555+ )
556+
557+ got_url = None
558+ diff_url = None
559+
560+ if matched_rf and matched_rf .got is not None :
561+ got_url = url_for (
562+ '.download_output_got' ,
563+ test_id = test_id ,
564+ regression_test_id = rt .id ,
565+ output_id = expected_output .id ,
566+ _external = True
567+ )
568+ diff_url = url_for (
569+ '.generate_diff' ,
570+ test_id = test_id ,
571+ regression_test_id = rt .id ,
572+ output_id = expected_output .id ,
573+ to_view = 0 ,
574+ _external = True
575+ )
576+
577+ return {
578+ 'output_id' : expected_output .id ,
579+ 'correct_extension' : expected_output .correct_extension ,
580+ 'expected_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+ 'got_url' : got_url ,
588+ 'diff_url' : diff_url ,
589+ }
590+
591+
592+ def _process_test_case (test , category_name , t_data ):
593+ """Build a structured dict for a single test case in the ai.json response."""
543594 rt = t_data ['test' ]
544595 result = t_data ['result' ]
545596 is_error = t_data .get ('error' , False )
546597 result_files = t_data ['files' ]
547598
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 )
599+ outputs = [
600+ _build_output_entry (test .id , rt , expected_output , result_files )
601+ for expected_output in rt .output_files
602+ if not expected_output .ignore
603+ ]
602604
603- return {
605+ response_dict = {
604606 'regression_test_id' : rt .id ,
605607 'category' : category_name ,
606608 'sample_filename' : rt .sample .original_name ,
607609 'sample_url' : url_for (
608610 '.download_sample_ai' ,
609- test_id = test_id ,
611+ test_id = test . id ,
610612 sample_id = rt .sample .id ,
611613 _external = True
612614 ),
@@ -616,11 +618,17 @@ def _process_test_case(test_id, category_name, t_data):
616618 'expected_exit_code' : result .expected_rc if result else None ,
617619 'runtime_ms' : result .runtime if result else None ,
618620 'outputs' : outputs ,
619- 'how_to_reproduce' : f'./ccextractor { rt .command } { rt .sample .original_name } ' ,
620621 }
621622
623+ # Format the reproduction command based on platform
624+ binary_name = f'./{ CCEXTRACTOR_LINUX_BINARY } ' if test .platform == TestPlatform .linux else CCEXTRACTOR_WIN_BINARY
625+ response_dict ['how_to_reproduce' ] = f'{ binary_name } { rt .command } { rt .sample .original_name } '
626+
627+ return response_dict
628+
622629
623630@mod_test .route ('/<int:test_id>/ai.json' , methods = ['GET' ])
631+ @login_required
624632def ai_json_endpoint (test_id ):
625633 """Structured JSON with download URLs for all artifacts — for AI agents."""
626634 from run import storage_client_bucket
@@ -632,10 +640,8 @@ def ai_json_endpoint(test_id):
632640 def blob_exists (path ):
633641 return storage_client_bucket .blob (path ).exists ()
634642
635- has_binary = (
636- blob_exists (f'test_artifacts/{ test_id } /ccextractor' ) or
637- blob_exists (f'test_artifacts/{ test_id } /ccextractor.exe' )
638- )
643+ binary_name = CCEXTRACTOR_LINUX_BINARY if test .platform == TestPlatform .linux else CCEXTRACTOR_WIN_BINARY
644+ has_binary = blob_exists (f'test_artifacts/{ test_id } /{ binary_name } ' )
639645 has_coredump = blob_exists (f'test_artifacts/{ test_id } /coredump' )
640646 has_combined_stdout = blob_exists (f'test_artifacts/{ test_id } /combined_stdout.log' )
641647
@@ -653,7 +659,7 @@ def blob_exists(path):
653659 else :
654660 passed += 1
655661
656- test_cases .append (_process_test_case (test_id , category ['category' ].name , t_data ))
662+ test_cases .append (_process_test_case (test , category ['category' ].name , t_data ))
657663
658664 report = {
659665 'test_id' : test .id ,
0 commit comments