Skip to content

Commit 39a8c04

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

3 files changed

Lines changed: 137 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: 76 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ def generate_diff(test_id: int, regression_test_id: int, output_id: int, to_view
367367

368368

369369
@mod_test.route('/log-files/<test_id>')
370+
@login_required
370371
def download_build_log_file(test_id):
371372
"""
372373
Serve download of build log.
@@ -464,18 +465,18 @@ def _artifact_redirect(blob_path, filename='artifact'):
464465

465466

466467
@mod_test.route('/<int:test_id>/binary', methods=['GET'])
468+
@login_required
467469
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)
470+
"""Download the ccextractor binary used in a test."""
471+
test = Test.query.filter(Test.id == test_id).first()
472+
if test is None:
473+
abort(404)
474+
name = 'ccextractor' if test.platform == TestPlatform.linux else 'ccextractor.exe'
475+
return _artifact_redirect(f'test_artifacts/{test_id}/{name}', filename=name)
476476

477477

478478
@mod_test.route('/<int:test_id>/coredump', methods=['GET'])
479+
@login_required
479480
def download_coredump(test_id):
480481
"""Download the coredump from a test, if one was produced."""
481482
return _artifact_redirect(
@@ -485,6 +486,7 @@ def download_coredump(test_id):
485486

486487

487488
@mod_test.route('/<int:test_id>/combined-stdout', methods=['GET'])
489+
@login_required
488490
def download_combined_stdout(test_id):
489491
"""Download the combined stdout/stderr log from all test invocations."""
490492
return _artifact_redirect(
@@ -494,6 +496,7 @@ def download_combined_stdout(test_id):
494496

495497

496498
@mod_test.route('/<int:test_id>/regression/<int:regression_test_id>/<int:output_id>/output-got', methods=['GET'])
499+
@login_required
497500
def download_output_got(test_id, regression_test_id, output_id):
498501
"""Download the actual output file from TestResults using DB hash."""
499502
rf = TestResultFile.query.filter(and_(
@@ -511,6 +514,7 @@ def download_output_got(test_id, regression_test_id, output_id):
511514

512515

513516
@mod_test.route('/<int:test_id>/regression/<int:regression_test_id>/<int:output_id>/output-expected', methods=['GET'])
517+
@login_required
514518
def download_output_expected(test_id, regression_test_id, output_id):
515519
"""Download the expected output file from TestResults using DB hash."""
516520
rf = TestResultFile.query.filter(and_(
@@ -526,8 +530,9 @@ def download_output_expected(test_id, regression_test_id, output_id):
526530
filename=f'output_expected_{regression_test_id}_{output_id}{ext}'
527531
)
528532
@mod_test.route('/<int:test_id>/sample/<int:sample_id>', methods=['GET'])
533+
@login_required
529534
def download_sample_ai(test_id, sample_id):
530-
"""Download the sample file for a regression test (no auth required for AI workflow)."""
535+
"""Download the sample file for a regression test."""
531536
from mod_sample.models import Sample
532537
sample = Sample.query.filter(Sample.id == sample_id).first()
533538
if sample is None:
@@ -538,75 +543,69 @@ def download_sample_ai(test_id, sample_id):
538543
)
539544

540545

541-
def _process_test_case(test_id, category_name, t_data):
542-
"""Helper function to process a single test case."""
546+
def _build_output_entry(test_id, rt, expected_output, result_files):
547+
"""Build a single output entry dict for the ai.json response."""
548+
matched_rf = next(
549+
(rf for rf in result_files
550+
if rf.test_id != -1 and rf.regression_test_output_id == expected_output.id),
551+
None
552+
)
553+
554+
got_url = None
555+
diff_url = None
556+
557+
if matched_rf and matched_rf.got is not None:
558+
got_url = url_for(
559+
'.download_output_got',
560+
test_id=test_id,
561+
regression_test_id=rt.id,
562+
output_id=expected_output.id,
563+
_external=True
564+
)
565+
diff_url = url_for(
566+
'.generate_diff',
567+
test_id=test_id,
568+
regression_test_id=rt.id,
569+
output_id=expected_output.id,
570+
to_view=0,
571+
_external=True
572+
)
573+
574+
return {
575+
'output_id': expected_output.id,
576+
'correct_extension': expected_output.correct_extension,
577+
'expected_url': url_for(
578+
'.download_output_expected',
579+
test_id=test_id,
580+
regression_test_id=rt.id,
581+
output_id=expected_output.id,
582+
_external=True
583+
),
584+
'got_url': got_url,
585+
'diff_url': diff_url,
586+
}
587+
588+
589+
def _process_test_case(test, category_name, t_data):
590+
"""Build a structured dict for a single test case in the ai.json response."""
543591
rt = t_data['test']
544592
result = t_data['result']
545593
is_error = t_data.get('error', False)
546594
result_files = t_data['files']
547595

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)
596+
outputs = [
597+
_build_output_entry(test.id, rt, expected_output, result_files)
598+
for expected_output in rt.output_files
599+
if not expected_output.ignore
600+
]
602601

603-
return {
602+
response_dict = {
604603
'regression_test_id': rt.id,
605604
'category': category_name,
606605
'sample_filename': rt.sample.original_name,
607606
'sample_url': url_for(
608607
'.download_sample_ai',
609-
test_id=test_id,
608+
test_id=test.id,
610609
sample_id=rt.sample.id,
611610
_external=True
612611
),
@@ -616,11 +615,17 @@ def _process_test_case(test_id, category_name, t_data):
616615
'expected_exit_code': result.expected_rc if result else None,
617616
'runtime_ms': result.runtime if result else None,
618617
'outputs': outputs,
619-
'how_to_reproduce': f'./ccextractor {rt.command} {rt.sample.original_name}',
620618
}
621619

620+
# Format the reproduction command based on platform
621+
binary_name = './ccextractor' if test.platform == TestPlatform.linux else 'ccextractor.exe'
622+
response_dict['how_to_reproduce'] = f'{binary_name} {rt.command} {rt.sample.original_name}'
623+
624+
return response_dict
625+
622626

623627
@mod_test.route('/<int:test_id>/ai.json', methods=['GET'])
628+
@login_required
624629
def ai_json_endpoint(test_id):
625630
"""Structured JSON with download URLs for all artifacts — for AI agents."""
626631
from run import storage_client_bucket
@@ -632,10 +637,8 @@ def ai_json_endpoint(test_id):
632637
def blob_exists(path):
633638
return storage_client_bucket.blob(path).exists()
634639

635-
has_binary = (
636-
blob_exists(f'test_artifacts/{test_id}/ccextractor') or
637-
blob_exists(f'test_artifacts/{test_id}/ccextractor.exe')
638-
)
640+
binary_name = 'ccextractor' if test.platform == TestPlatform.linux else 'ccextractor.exe'
641+
has_binary = blob_exists(f'test_artifacts/{test_id}/{binary_name}')
639642
has_coredump = blob_exists(f'test_artifacts/{test_id}/coredump')
640643
has_combined_stdout = blob_exists(f'test_artifacts/{test_id}/combined_stdout.log')
641644

@@ -653,7 +656,7 @@ def blob_exists(path):
653656
else:
654657
passed += 1
655658

656-
test_cases.append(_process_test_case(test_id, category['category'].name, t_data))
659+
test_cases.append(_process_test_case(test, category['category'].name, t_data))
657660

658661
report = {
659662
'test_id': test.id,

0 commit comments

Comments
 (0)