char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
d30500b83
char/aliceai-t5-moe
1"""Run llama.cpp Hexagon tests in a single QDC job. 2 3Bundles test scripts into one artifact and submits a single QDC job: 4 51. run_bench_tests_posix.py — llama-cli and llama-bench on CPU / GPU / NPU 6(from scripts/snapdragon/qdc/) 7 8Results are written to $GITHUB_STEP_SUMMARY when set (GitHub Actions). 9 10Prerequisites: 11pip install /path/to/qualcomm_device_cloud_sdk*.whl 12 13Platform is inferred from --device: 14android Appium + pytest (Android phones: SM8750 / SM8650 / SM8850) 15linux BASH (Linux IoT: QCS9075M) 16 17Required environment variables: 18QDC_API_KEY API key from QDC UI -> Users -> Settings -> API Keys 19 20Usage: 21python run_qdc_jobs.py \\ 22--pkg-dir pkg-snapdragon/llama.cpp \\ 23--model-url https://.../Llama-3.2-1B-Instruct-Q4_0.gguf \\ 24--device SM8750 25""" 26 27from __future__import annotations 28 29import argparse 30import enum 31import logging 32import os 33import re 34import shutil 35import sys 36import tempfile 37import time 38import xml .etree .ElementTree as ET 39from dataclasses import dataclass ,field 40from pathlib import Path 41from typing import Callable 42 43from qualcomm_device_cloud_sdk .api import qdc_api 44from qualcomm_device_cloud_sdk .logging import configure_logging 45from qualcomm_device_cloud_sdk .models import ( 46ArtifactType , 47JobMode , 48JobState , 49JobSubmissionParameter , 50JobType , 51TestFramework , 52) 53 54# configure_logging only sets up the SDK logger; basicConfig is needed for 55# our own log.info to reach stdout. 56logging .basicConfig ( 57level = logging .INFO , 58format = "%(asctime)s %(name)s %(levelname)s - %(message)s" , 59handlers = [logging .StreamHandler ()], 60) 61configure_logging (level = logging .INFO ,handlers = [logging .StreamHandler ()]) 62# Silence per-poll GET/status spam from the SDK and its HTTP client. 63logging .getLogger ("qualcomm_device_cloud" ).setLevel (logging .WARNING ) 64logging .getLogger ("httpx" ).setLevel (logging .WARNING ) 65logging .getLogger ("httpcore" ).setLevel (logging .WARNING ) 66log = logging .getLogger (__name__ ) 67 68POLL_INTERVAL = 30 69JOB_TIMEOUT = 3600 70LOG_UPLOAD_TIMEOUT = 600 71CAPACITY_TIMEOUT = 1800 72CAPACITY_POLL = 60 73MAX_CONCURRENT_JOBS = 5 74DEFAULT_RETRIES = 0 75RETRY_DELAY = 300 76TERMINAL_STATES = {JobState .COMPLETED ,JobState .CANCELED } 77NON_TERMINAL_STATES = {JobState .DISPATCHED ,JobState .RUNNING ,JobState .SETUP ,JobState .SUBMITTED } 78 79 80class DeviceUnavailableError (Exception ): 81"""Raised when the QDC device resource is not available (retryable).""" 82 83 84_SCRIPTS_DIR = Path (__file__ ).parent 85_TESTS_DIR = _SCRIPTS_DIR / "tests" 86 87# --- Shared test assets ------------------------------------------------------- 88_UTILS = _TESTS_DIR / "utils.py" 89_CONFTEST = _TESTS_DIR / "conftest.py" 90_PYTEST_LINE_RE = re .compile ( 91r"(?:[\w/]+\.py::)?(?:\w+::)?([\w\[\].-]+)\s+(PASSED|FAILED|ERROR|SKIPPED)" 92) 93_EXCLUDED_LOGS = { 94"qdc_android_whole_host-000.log" , 95"qdc_kernel_host-000.log" , 96"qdc_LE_whole_host-000.log" , 97"qdc_LE_kernel_host-000.log" , 98"script.log" , 99} 100_NON_TERMINAL_STATE_VALUES = {s .value for s in NON_TERMINAL_STATES } 101 102# --- Android (Appium + pytest) assets ---------------------------------------- 103_RUN_BENCH = _TESTS_DIR / "run_bench_tests_posix.py" 104_RUN_BACKEND_OPS = _TESTS_DIR / "run_backend_ops_posix.py" 105_REQUIREMENTS = _SCRIPTS_DIR / "requirements.txt" 106_RUN_PY = _SCRIPTS_DIR .parent / "run.py" 107 108# --- Linux (BASH) assets ------------------------------------------------------ 109_RUN_LINUX_TEMPLATE = _TESTS_DIR / "linux" / "run_linux.sh" 110_LINUX_ENTRY_SCRIPT = "/bin/bash /data/local/tmp/TestContent/run_linux.sh" 111 112# ============================================================================= 113# Artifact builders (per platform) 114# ============================================================================= 115 116 117@dataclass 118class JobResult : 119passed :bool 120tests :dict [str ,bool ]= field (default_factory = dict ) 121raw_logs :dict [str ,str ]= field (default_factory = dict ) 122failure_details :dict [str ,str ]= field (default_factory = dict ) 123 124 125def _write_lf (path :Path ,content :str )-> None : 126"""Write text with LF line endings (required by /bin/bash on Linux).""" 127with open (path ,"w" ,encoding = "utf-8" ,newline = "\n" )as f : 128f .write (content ) 129 130 131def _build_android_artifact ( 132pkg_dir :Path , 133stage_dir :Path , 134test_mode :str , 135model_url :str | None , 136)-> Path : 137"""Android zip (Appium/pytest). Extracted by QDC under /qdc/appium/. 138 139Zip structure: 140llama_cpp_bundle/ installed package (adb pushed to /data/local/tmp/) 141run.py Snapdragon runner 142tests/ 143utils.py shared adb helpers 144conftest.py Appium pytest fixtures 145test_bench_posix.py bench + cli tests (for --test bench or all) 146test_backend_ops_posix.py test-backend-ops on HTP0 147requirements.txt 148pytest.ini addopts = --junitxml=results.xml 149""" 150bundle_dir = stage_dir / "llama_cpp_bundle" 151shutil .copytree (pkg_dir ,bundle_dir ) 152 153dest = stage_dir / "run.py" 154shutil .copy (_RUN_PY ,dest ) 155dest .chmod (0o755 ) 156 157tests_dir = stage_dir / "tests" 158tests_dir .mkdir () 159 160shutil .copy (_UTILS ,tests_dir / "utils.py" ) 161shutil .copy (_CONFTEST ,tests_dir / "conftest.py" ) 162 163if test_mode in ("bench" ,"all" ): 164assert model_url is not None 165 (tests_dir / "test_bench_posix.py" ).write_text ( 166_RUN_BENCH .read_text ().replace ("<<MODEL_URL>>" ,model_url ) 167 ) 168if test_mode in ("backend-ops" ,"all" ): 169shutil .copy (_RUN_BACKEND_OPS ,tests_dir / "test_backend_ops_posix.py" ) 170 171shutil .copy (_REQUIREMENTS ,stage_dir / "requirements.txt" ) 172 (stage_dir / "pytest.ini" ).write_text ( 173"[pytest]\naddopts = --junitxml=results.xml\n" 174 ) 175 176zip_base = str (stage_dir / "artifact" ) 177shutil .make_archive (zip_base ,"zip" ,stage_dir ) 178return Path (f" { zip_base } .zip" ) 179 180 181def _build_linux_artifact ( 182pkg_dir :Path , 183stage_dir :Path , 184test_mode :str , 185model_url :str | None , 186)-> Path : 187"""Linux IoT zip (BASH framework). Extracted by QDC to /data/local/tmp/TestContent/. 188 189Zip structure: 190run_linux.sh entry script (placeholder-substituted, LF line endings) 191llama_cpp_bundle/ installed package 192""" 193bundle_dir = stage_dir / "llama_cpp_bundle" 194shutil .copytree (pkg_dir ,bundle_dir ) 195 196template = _RUN_LINUX_TEMPLATE .read_text (encoding = "utf-8" ) 197rendered = template .replace ("{MODEL_URL}" ,model_url or "" ).replace ( 198"{TEST_MODE}" ,test_mode 199 ) 200script_path = stage_dir / "run_linux.sh" 201_write_lf (script_path ,rendered ) 202script_path .chmod (0o755 ) 203 204zip_base = str (stage_dir / "artifact" ) 205shutil .make_archive (zip_base ,"zip" ,stage_dir ) 206return Path (f" { zip_base } .zip" ) 207 208 209# ============================================================================= 210# Platform enum + strategy table 211# ============================================================================= 212 213 214class Platform (enum .Enum ): 215ANDROID = "android" 216LINUX = "linux" 217 218 219@dataclass( frozen = True ) 220class PlatformSpec : 221test_framework :TestFramework 222entry_script :str | None 223build_artifact :Callable [[Path ,Path ,str ,str | None ],Path ] 224job_name_fmt :str 225 226 227PLATFORM_SPECS :dict [Platform ,PlatformSpec ]= { 228Platform .ANDROID :PlatformSpec ( 229test_framework = TestFramework .APPIUM , 230entry_script = None , 231build_artifact = _build_android_artifact , 232job_name_fmt = "{base}" , 233 ), 234Platform .LINUX :PlatformSpec ( 235test_framework = TestFramework .BASH , 236entry_script = _LINUX_ENTRY_SCRIPT , 237build_artifact = _build_linux_artifact , 238job_name_fmt = "{base} (Linux)" , 239 ), 240} 241 242DEVICE_PLATFORM :dict [str ,Platform ]= { 243"SM8750" :Platform .ANDROID , 244"SM8650" :Platform .ANDROID , 245"SM8850" :Platform .ANDROID , 246"QCS9075M" :Platform .LINUX , 247} 248 249 250# ============================================================================= 251# Shared QDC job plumbing 252# ============================================================================= 253 254 255def wait_for_job (client ,job_id :str ,timeout :int )-> str : 256elapsed = 0 257last_state = None 258consecutive_errors = 0 259max_consecutive_errors = 5 260while elapsed < timeout : 261try : 262raw = qdc_api .get_job_status (client ,job_id ) 263consecutive_errors = 0 264except Exception as e : 265consecutive_errors += 1 266log .warning ( 267"Transient error polling job %s (%d/%d): %s" , 268job_id , 269consecutive_errors , 270max_consecutive_errors , 271e , 272 ) 273if consecutive_errors >= max_consecutive_errors : 274raise 275time .sleep (POLL_INTERVAL ) 276elapsed += POLL_INTERVAL 277continue 278try : 279status = JobState (raw ) 280except ValueError : 281status = raw 282if status in TERMINAL_STATES : 283return raw .lower () 284if raw != last_state : 285log .info ("Job %s: %s" ,job_id ,raw ) 286last_state = raw 287time .sleep (POLL_INTERVAL ) 288elapsed += POLL_INTERVAL 289# Abort to free the QDC concurrency slot instead of leaking it. 290try : 291qdc_api .abort_job (client ,job_id ) 292log .warning ("Aborted job %s after timeout to free concurrency slot" ,job_id ) 293except Exception as e : 294log .warning ("Failed to abort job %s: %s" ,job_id ,e ) 295raise TimeoutError (f"Job { job_id } did not finish within { timeout } s" ) 296 297 298def wait_for_log_upload (client ,job_id :str )-> None : 299elapsed = 0 300while elapsed <= LOG_UPLOAD_TIMEOUT : 301try : 302status = (qdc_api .get_job_log_upload_status (client ,job_id )or "" ).lower () 303except Exception as e : 304log .warning ("get_job_log_upload_status failed: %s — will retry" ,e ) 305status = "" 306if status in {"completed" ,"failed" }: 307return 308log .info ("Waiting for log upload (status=%s) ..." ,status ) 309time .sleep (POLL_INTERVAL ) 310elapsed += POLL_INTERVAL 311log .warning ("Timed out waiting for log upload after %ds" ,LOG_UPLOAD_TIMEOUT ) 312 313 314def wait_for_capacity (client ,max_jobs :int = MAX_CONCURRENT_JOBS )-> None : 315"""Block until the user's active (non-terminal) QDC job count is below max_jobs.""" 316elapsed = 0 317while elapsed < CAPACITY_TIMEOUT : 318jobs_page = qdc_api .get_jobs_list (client ,page_number = 0 ,page_size = 50 ) 319if jobs_page is None : 320log .warning ( 321"Could not retrieve job list; proceeding without capacity check" 322 ) 323return 324items = getattr (jobs_page ,"data" , [])or [] 325active = sum ( 3261 for j in items if getattr (j ,"state" ,None )in _NON_TERMINAL_STATE_VALUES 327 ) 328if active < max_jobs : 329log .info ("Active QDC jobs: %d / %d — proceeding" ,active ,max_jobs ) 330return 331log .info ( 332"Active QDC jobs: %d / %d — waiting %ds ..." , 333active , 334max_jobs , 335CAPACITY_POLL , 336 ) 337time .sleep (CAPACITY_POLL ) 338elapsed += CAPACITY_POLL 339raise TimeoutError ( 340f"Capacity wait timed out after { CAPACITY_TIMEOUT } s" 341 ) 342 343 344# --------------------------------------------------------------------------- 345# Log parsing helpers 346# --------------------------------------------------------------------------- 347 348 349def _parse_junit_xml (content :str )-> tuple [dict [str ,bool ],dict [str ,str ]]: 350try : 351root = ET .fromstring (content ) 352except ET .ParseError : 353return {}, {} 354results :dict [str ,bool ]= {} 355failures :dict [str ,str ]= {} 356for tc in root .iter ("testcase" ): 357name = tc .get ("name" ,"" ) 358if classname := tc .get ("classname" ,"" ): 359name = f" { classname } . { name } " 360failure_el = tc .find ("failure" ) 361if failure_el is None : 362failure_el = tc .find ("error" ) 363results [name ]= failure_el is None 364if failure_el is not None : 365parts = [failure_el .get ("message" ,"" ),failure_el .text or "" ] 366failures [name ]= "\n" .join (p for p in parts if p ).strip () 367return results ,failures 368 369 370def _parse_pytest_output (content :str )-> dict [str ,bool ]: 371results :dict [str ,bool ]= {} 372for m in _PYTEST_LINE_RE .finditer (content ): 373results [m .group (1 )]= m .group (2 )== "PASSED" 374return results 375 376 377def fetch_logs_and_parse_tests ( 378client ,job_id :str ,max_retries :int = 5 ,retry_delay :int = 30 379)-> tuple [dict [str ,bool ],dict [str ,str ],dict [str ,str ]]: 380"""Returns (test_results, raw_logs, failure_details).""" 381log_files = None 382for attempt in range (1 ,max_retries + 1 ): 383try : 384log_files = qdc_api .get_job_log_files (client ,job_id ) 385break 386except Exception as e : 387if attempt < max_retries : 388log .warning ( 389"get_job_log_files failed (attempt %d/%d): %s — retrying in %ds" , 390attempt ,max_retries ,e ,retry_delay , 391 ) 392time .sleep (retry_delay ) 393else : 394log .error ( 395"get_job_log_files failed after %d attempts: %s" ,max_retries ,e 396 ) 397return {}, {}, {} 398if not log_files : 399log .warning ("No log files returned for job %s" ,job_id ) 400return {}, {}, {} 401 402test_results :dict [str ,bool ]= {} 403pytest_fallback :dict [str ,bool ]= {} 404raw_logs :dict [str ,str ]= {} 405failure_details :dict [str ,str ]= {} 406 407with tempfile .TemporaryDirectory ()as tmpdir : 408for lf in log_files : 409zip_path = os .path .join (tmpdir ,"log.zip" ) 410log .info ("Downloading log file: %s" ,lf .filename ) 411qdc_api .download_job_log_files (client ,lf .filename ,zip_path ) 412try : 413shutil .unpack_archive (zip_path ,tmpdir ,"zip" ) 414except Exception as e : 415log .warning ("Could not unpack %s as zip: %s" ,lf .filename ,e ) 416 417for root_dir ,_ ,files in os .walk (tmpdir ): 418for fname in sorted (files ): 419fpath = os .path .join (root_dir ,fname ) 420content = Path (fpath ).read_text (errors = "replace" ) 421if fname .endswith (".xml" ): 422results ,failures = _parse_junit_xml (content ) 423test_results .update (results ) 424failure_details .update (failures ) 425elif fname .endswith (".log" ): 426if fname in _EXCLUDED_LOGS : 427continue 428log .info ("--- %s ---\n%s" ,fname ,content ) 429raw_logs [fname ]= content 430pytest_fallback .update (_parse_pytest_output (content )) 431 432return ( 433 (test_results if test_results else pytest_fallback ), 434raw_logs , 435failure_details , 436 ) 437 438 439def write_summary (result :JobResult ,title :str = "QDC Test Results" )-> None : 440summary_path = os .environ .get ("GITHUB_STEP_SUMMARY" ) 441if not summary_path : 442return 443 444icon = "✅" if result .passed else "❌" 445 446lines = [ 447f"## { title } \n" , 448f"Overall: { icon } { 'PASSED' if result . passed else 'FAILED' } \n" , 449 ] 450reportable = {n :ok for n ,ok in result .tests .items ()if "test_install" not in n } 451if reportable : 452lines += ["| Test | Result |" ,"| ---- | ------ |" ] 453for name ,ok in reportable .items (): 454lines .append (f"| ` { name } ` | { '✅' if ok else '❌' } |" ) 455passed_n = sum (1 for v in reportable .values ()if v ) 456failed_n = sum (1 for v in reportable .values ()if not v ) 457lines += ["" ,f"** { passed_n } passed, { failed_n } failed**" ] 458else : 459lines .append ("_No per-test data available._" ) 460 461failed_names = [n for n ,ok in reportable .items ()if not ok ] 462if failed_names : 463lines += ["" ,"### Failures" ] 464for name in failed_names : 465detail = result .failure_details .get (name ) 466if detail : 467lines += [ 468f"<details><summary><code> { name } </code></summary>" , 469"" , 470"```" , 471detail , 472"```" , 473"" , 474"</details>" , 475 ] 476 477if result .raw_logs : 478lines += ["" ,"### Raw Logs" ] 479for fname ,content in sorted (result .raw_logs .items ()): 480lines += [ 481f"<details><summary> { fname } </summary>" , 482"" , 483"```" , 484content .rstrip (), 485"```" , 486"" , 487"</details>" , 488 ] 489 490with open (summary_path ,"a" )as f : 491f .write ("\n" .join (lines )+ "\n" ) 492 493 494# ============================================================================= 495# CLI + main 496# ============================================================================= 497 498def parse_args ()-> argparse .Namespace : 499p = argparse .ArgumentParser ( 500description = __doc__ , 501formatter_class = argparse .RawDescriptionHelpFormatter , 502 ) 503p .add_argument ("--pkg-dir" ,required = True ,type = Path , 504help = "Installed llama.cpp package directory (contains bin/ and lib/)" ) 505p .add_argument ("--model-url" , 506help = "Direct URL to the GGUF model file (required for --test bench)" ) 507p .add_argument ("--device" ,required = True , 508help = "QDC chipset name, e.g. SM8750" ) 509p .add_argument ("--test" ,choices = ["bench" ,"backend-ops" ,"all" ],default = "bench" , 510help = "Test suite to run (default: bench)" ) 511p .add_argument ("--job-timeout" ,type = int ,default = JOB_TIMEOUT ,metavar = "SECONDS" , 512help = f"Max seconds to wait for job completion (default: { JOB_TIMEOUT } )" ) 513p .add_argument ("--retries" ,type = int ,default = DEFAULT_RETRIES ,metavar = "N" , 514help = "Number of retries when device is unavailable (default: 0)" ) 515p .add_argument ("--retry-delay" ,type = int ,default = RETRY_DELAY ,metavar = "SECONDS" , 516help = f"Seconds to wait between retries (default: { RETRY_DELAY } )" ) 517args = p .parse_args () 518if args .test in ("bench" ,"all" )and not args .model_url : 519p .error ("--model-url is required when --test bench or --test all" ) 520return args 521 522 523def _submit_and_run_job (client ,args ,spec ,target_id ,artifact_id )-> JobResult : 524"""Submit a QDC job and wait for results. 525 526Raises DeviceUnavailableError for transient device/resource issues that 527are worth retrying. Returns JobResult for definitive outcomes (pass or 528test failure). 529""" 530try : 531wait_for_capacity (client ) 532except TimeoutError : 533raise DeviceUnavailableError ("Capacity wait timed out — device busy" ) 534 535job_name = spec .job_name_fmt .format (base = "llama.cpp Hexagon tests" ) 536 537job_id = qdc_api .submit_job ( 538public_api_client = client , 539target_id = target_id , 540job_name = job_name , 541external_job_id = None , 542job_type = JobType .AUTOMATED , 543job_mode = JobMode .APPLICATION , 544timeout = max (1 ,args .job_timeout // 60 ), 545test_framework = spec .test_framework , 546entry_script = spec .entry_script , 547job_artifacts = [artifact_id ], 548monkey_events = None , 549monkey_session_timeout = None , 550job_parameters = [JobSubmissionParameter .WIFIENABLED ], 551 ) 552if job_id is None : 553raise DeviceUnavailableError ("Job submission failed — device may be unavailable" ) 554log .info ("Job submitted: %s (device=%s)" ,job_id ,args .device ) 555 556try : 557job_status = wait_for_job (client ,job_id ,timeout = args .job_timeout ) 558except TimeoutError as e : 559raise DeviceUnavailableError (str (e )) 560log .info ("Job %s finished: %s" ,job_id ,job_status ) 561 562wait_for_log_upload (client ,job_id ) 563tests ,raw_logs ,failure_details = fetch_logs_and_parse_tests (client ,job_id ) 564 565job_ok = job_status == JobState .COMPLETED .value .lower () 566 567if not job_ok and not tests : 568raise DeviceUnavailableError ( 569f"Job did not complete (status= { job_status } ) and produced no test results" 570 ) 571 572passed = job_ok and all (tests .values ())if tests else job_ok 573if spec .test_framework == TestFramework .BASH and not tests : 574log .error ("No test results recovered (state=%s). Script likely never ran." ,job_status ) 575passed = False 576if not passed : 577log .error ("Job did not complete successfully or tests failed (status=%s)" ,job_status ) 578 579return JobResult (passed = passed ,tests = tests ,raw_logs = raw_logs ,failure_details = failure_details ) 580 581 582def main ()-> int : 583args = parse_args () 584 585platform = DEVICE_PLATFORM .get (args .device ) 586if platform is None : 587log .error ( 588"Unknown device %r. Known: %s" , 589args .device ,", " .join (sorted (DEVICE_PLATFORM .keys ())), 590 ) 591return 1 592spec = PLATFORM_SPECS [platform ] 593 594api_key = os .environ .get ("QDC_API_KEY" ) 595if not api_key : 596log .error ("QDC_API_KEY environment variable must be set" ) 597return 1 598if not args .pkg_dir .is_dir (): 599log .error ("--pkg-dir %s does not exist" ,args .pkg_dir ) 600return 1 601 602client = qdc_api .get_public_api_client_using_api_key ( 603api_key_header = api_key , 604app_name_header = "llama-cpp-ci" , 605on_behalf_of_header = "llama-cpp-ci" , 606client_type_header = "Python" , 607 ) 608 609target_id = qdc_api .get_target_id (client ,args .device ) 610if target_id is None : 611log .error ("Could not find QDC target for device %r" ,args .device ) 612return 1 613 614with tempfile .TemporaryDirectory ()as tmpdir : 615log .info ("Building %s artifact (test=%s) ..." ,platform .value ,args .test ) 616zip_path = spec .build_artifact ( 617args .pkg_dir ,Path (tmpdir ),args .test ,args .model_url 618 ) 619log .info ("Uploading artifact (%d MB) ..." ,zip_path .stat ().st_size // 1_000_000 ) 620artifact_id = qdc_api .upload_file (client ,str (zip_path ),ArtifactType .TESTSCRIPT ) 621 622if artifact_id is None : 623log .error ("Artifact upload failed" ) 624return 1 625 626max_attempts = 1 + args .retries 627for attempt in range (1 ,max_attempts + 1 ): 628try : 629result = _submit_and_run_job (client ,args ,spec ,target_id ,artifact_id ) 630break 631except DeviceUnavailableError as e : 632if attempt < max_attempts : 633log .warning ( 634"Attempt %d/%d failed (device unavailable): %s — retrying in %ds" , 635attempt ,max_attempts ,e ,args .retry_delay , 636 ) 637time .sleep (args .retry_delay ) 638else : 639log .error ( 640"Attempt %d/%d failed (device unavailable): %s — no retries left" , 641attempt ,max_attempts ,e , 642 ) 643write_summary ( 644JobResult (passed = False ,tests = {}), 645title = f"QDC Device Unavailable ( { args . device } )" , 646 ) 647return 1 648else : 649return 1 650 651if args .test == "backend-ops" : 652title = f"Backend Ops — HTP0 ( { args . device } )" 653elif args .test == "all" : 654title = f"QDC Tests ( { args . device } )" 655else : 656title = f"QDC Test Results ( { args . device } )" 657write_summary (result ,title = title ) 658 659return 0 if result .passed else 1 660 661 662if __name__ == "__main__" : 663sys .exit (main ())