Skip to content

Commit 040603c

Browse files
committed
Address review: SP-mediated uploads, login_required, platform binary, code smell fix
1 parent 1baab37 commit 040603c

3 files changed

Lines changed: 140 additions & 97 deletions

File tree

install/ci-vm/ci-linux/ci/runCI

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,11 @@ if [ -e "${dstDir}/ccextractor" ]; then
139139
#!/bin/bash
140140
COMBINED_LOG="/tmp/combined_stdout.log"
141141
REAL_BINARY="PLACEHOLDER_BINARY"
142-
EXIT_CODE_FILE="/tmp/.wrapper_exit_code"
142+
EXIT_CODE_FILE=$(mktemp)
143143
echo "=== TEST INVOCATION: $@ ===" >> "$COMBINED_LOG"
144144
{ "$REAL_BINARY" "$@" 2>&1; echo $? > "$EXIT_CODE_FILE"; } | tee -a "$COMBINED_LOG"
145145
exit_code=$(cat "$EXIT_CODE_FILE")
146+
rm -f "$EXIT_CODE_FILE"
146147
echo "=== EXIT CODE: ${exit_code} ===" >> "$COMBINED_LOG"
147148
echo "" >> "$COMBINED_LOG"
148149
exit $exit_code
@@ -153,37 +154,33 @@ WRAPPER_EOF
153154
executeCommand cd ${suiteDstDir}
154155
executeCommand ${tester} --debug --entries "${testFile}" --executable "${wrapper_path}" --tempfolder "${tempFolder}" --timeout 600 --reportfolder "${reportFolder}" --resultfolder "${resultFolder}" --samplefolder "${sampleFolder}" --method Server --url "${reportURL}"
155156

156-
# Upload AI artifacts to GCS
157-
gcs_bucket=$(curl -s "http://metadata/computeMetadata/v1/instance/attributes/bucket" -H "Metadata-Flavor: Google")
158-
test_id=$(curl -s "http://metadata/computeMetadata/v1/instance/attributes/testID" -H "Metadata-Flavor: Google")
159-
token=$(curl -s "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" -H "Metadata-Flavor: Google" | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
160-
157+
# Upload artifacts through the Sample Platform server
161158
upload_artifact() {
162159
local file_path="$1"
163-
local dest_path="$2"
160+
local artifact_name="$2"
164161
if [ -f "$file_path" ]; then
165162
local http_code
166-
http_code=$(curl -s -X POST --data-binary @"$file_path" \
167-
-H "Authorization: Bearer $token" \
168-
-H "Content-Type: application/octet-stream" \
169-
-w "%{http_code}" \
170-
-o /dev/null \
171-
"https://storage.googleapis.com/upload/storage/v1/b/${gcs_bucket}/o?uploadType=media&name=${dest_path}")
172-
if [ -z "$http_code" ] || [ "$http_code" -ne 200 ]; then
173-
echo "GCS upload failed for ${dest_path}: HTTP ${http_code:-no_response}" >> "${logFile}"
163+
http_code=$(curl -s -A "${userAgent}" \
164+
--form "type=artifact" \
165+
--form "name=${artifact_name}" \
166+
--form "file=@${file_path}" \
167+
-w "%{http_code}" -o /dev/null \
168+
"${reportURL}" 2>/dev/null)
169+
if [ -z "$http_code" ] || [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
170+
echo "Artifact upload failed for ${artifact_name}: HTTP ${http_code:-no_response}" >> "${logFile}"
174171
fi
175172
fi
176173
}
177174

178-
upload_artifact "$ccextractor_path" "test_artifacts/${test_id}/ccextractor"
175+
upload_artifact "$ccextractor_path" "ccextractor"
179176

180177
# Upload combined stdout log
181-
upload_artifact "${combined_stdout}" "test_artifacts/${test_id}/combined_stdout.log"
178+
upload_artifact "${combined_stdout}" "combined_stdout.log"
182179

183180
# Upload coredumps if any
184181
for core_file in /tmp/coredumps/core.*; do
185182
if [ -f "$core_file" ]; then
186-
upload_artifact "$core_file" "test_artifacts/${test_id}/coredump"
183+
upload_artifact "$core_file" "coredump"
187184
break
188185
fi
189186
done

mod_ci/controllers.py

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,9 +1194,7 @@ def create_instance(compute, project, zone, test, reportURL) -> Dict:
11941194
startup_script = f.read()
11951195
metadata_items = [
11961196
{'key': 'startup-script', 'value': startup_script},
1197-
{'key': 'reportURL', 'value': reportURL},
1198-
{'key': 'bucket', 'value': config.get('GCS_BUCKET_NAME', '')},
1199-
{'key': 'testID', 'value': str(test.id)}
1197+
{'key': 'reportURL', 'value': reportURL}
12001198
]
12011199
elif test.platform == TestPlatform.windows:
12021200
image_response = compute.images().getFromFamily(project=config.get('WINDOWS_INSTANCE_PROJECT_NAME', ''),
@@ -1217,9 +1215,7 @@ def create_instance(compute, project, zone, test, reportURL) -> Dict:
12171215
{'key': 'windows-startup-script-ps1', 'value': startup_script},
12181216
{'key': 'service_account', 'value': service_account},
12191217
{'key': 'rclone_conf', 'value': rclone_conf},
1220-
{'key': 'reportURL', 'value': reportURL},
1221-
{'key': 'bucket', 'value': config.get('GCS_BUCKET_NAME', '')},
1222-
{'key': 'testID', 'value': str(test.id)}
1218+
{'key': 'reportURL', 'value': reportURL}
12231219
]
12241220
source_disk_image = image_response['selfLink']
12251221

@@ -2346,6 +2342,11 @@ def progress_reporter(test_id, token):
23462342
if not upload_type_request(log, test_id, repo_folder, test, request):
23472343
return "EMPTY"
23482344

2345+
elif request.form['type'] == 'artifact':
2346+
log.info(f'[PROGRESS_REPORTER][Test: {test_id}] Artifact upload')
2347+
if not artifact_upload_request(log, test_id, request):
2348+
return "EMPTY"
2349+
23492350
elif request.form['type'] == 'finish':
23502351
log.info(f'[PROGRESS_REPORTER][Test: {test_id}] Test finished')
23512352
finish_type_request(log, test_id, test, request)
@@ -2695,6 +2696,45 @@ def upload_type_request(log, test_id, repo_folder, test, request) -> bool:
26952696
return False
26962697

26972698

2699+
# Allowed artifact names that the VM can upload
2700+
ALLOWED_ARTIFACT_NAMES = {'ccextractor', 'ccextractor.exe', 'combined_stdout.log', 'coredump'}
2701+
2702+
2703+
def artifact_upload_request(log, test_id, request) -> bool:
2704+
"""
2705+
Handle artifact upload from the CI VM.
2706+
2707+
Validates the artifact name against an allow-list, then uploads
2708+
the file to GCS under test_artifacts/{test_id}/{name}.
2709+
2710+
:param log: logger
2711+
:type log: Logger
2712+
:param test_id: The id of the test to update.
2713+
:type test_id: int
2714+
:param request: Request parameters
2715+
:type request: Request
2716+
:return: True if upload succeeded, False otherwise.
2717+
:rtype: bool
2718+
"""
2719+
from run import storage_client_bucket
2720+
2721+
artifact_name = request.form.get('name', '')
2722+
if artifact_name not in ALLOWED_ARTIFACT_NAMES:
2723+
log.warning(f"[Test: {test_id}] Rejected artifact upload with disallowed name: {artifact_name}")
2724+
return False
2725+
2726+
if 'file' not in request.files:
2727+
log.warning(f"[Test: {test_id}] Artifact upload missing file")
2728+
return False
2729+
2730+
uploaded_file = request.files['file']
2731+
blob_path = f'test_artifacts/{test_id}/{artifact_name}'
2732+
blob = storage_client_bucket.blob(blob_path)
2733+
blob.upload_from_file(uploaded_file.stream)
2734+
log.info(f"[Test: {test_id}] Artifact '{artifact_name}' uploaded to {blob_path}")
2735+
return True
2736+
2737+
26982738
def finish_type_request(log, test_id, test, request):
26992739
"""
27002740
Handle finish request type for progress reporter.

mod_test/controllers.py

Lines changed: 79 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121

2222
mod_test = Blueprint('test', __name__)
2323

24+
CCEXTRACTOR_WIN_BINARY = 'ccextractor.exe'
25+
CCEXTRACTOR_LINUX_BINARY = 'ccextractor'
26+
2427

2528
@mod_test.before_app_request
2629
def 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
370374
def 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
467472
def 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
479483
def 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
488493
def 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
497503
def 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
514521
def 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
529537
def 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
624632
def 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

Comments
 (0)