zackliqcom commited on
Commit
6ec1d3e
·
verified ·
1 Parent(s): 39dbdce

Upload folder using huggingface_hub

Browse files
conftest.py CHANGED
@@ -12,14 +12,8 @@ from appium import webdriver
12
  from utils import options, write_qdc_log
13
 
14
 
15
- @pytest.fixture(scope="session")
16
  def driver():
17
- """Appium WebDriver fixture (only used by scorecard tests).
18
-
19
- For most Linux tests, direct SSH commands are used instead.
20
- This fixture is not auto-initialized to avoid connection errors
21
- when the Appium server isn't needed.
22
- """
23
  return webdriver.Remote(command_executor="http://127.0.0.1:4723/wd/hub", options=options)
24
 
25
 
 
12
  from utils import options, write_qdc_log
13
 
14
 
15
+ @pytest.fixture(scope="session", autouse=True)
16
  def driver():
 
 
 
 
 
 
17
  return webdriver.Remote(command_executor="http://127.0.0.1:4723/wd/hub", options=options)
18
 
19
 
requirements.txt ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Appium-Python-Client==5.2.4
2
+ attrs==25.4.0
3
+ certifi==2025.10.5
4
+ exceptiongroup==1.3.0
5
+ h11==0.16.0
6
+ idna==3.11
7
+ iniconfig==2.1.0
8
+ outcome==1.3.0.post0
9
+ packaging==25.0
10
+ pluggy==1.6.0
11
+ PySocks==1.7.1
12
+ pytest==8.4.2
13
+ selenium==4.36.0
14
+ sniffio==1.3.1
15
+ sortedcontainers==2.4.0
16
+ tomli==2.3.0
17
+ trio==0.31.0
18
+ trio-websocket==0.12.2
19
+ typing_extensions==4.15.0
20
+ urllib3==2.5.0
21
+ websocket-client==1.9.0
22
+ wsproto==1.2.0
run_backend_ops_posix.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ---------------------------------------------------------------------
2
+ # Copyright (c) 2025 Qualcomm Technologies, Inc. and/or its subsidiaries.
3
+ # SPDX-License-Identifier: BSD-3-Clause
4
+ # ---------------------------------------------------------------------
5
+ """
6
+ On-device test-backend-ops runner for llama.cpp (HTP0 backend).
7
+
8
+ Executed by QDC's Appium test framework on the QDC runner.
9
+ The runner has ADB access to the allocated device.
10
+ """
11
+
12
+ import os
13
+ import sys
14
+
15
+ import pytest
16
+
17
+ from utils import BIN_PATH, CMD_PREFIX, push_bundle_if_needed, run_adb_command, write_qdc_log
18
+
19
+
20
+ @pytest.fixture(scope="session", autouse=True)
21
+ def install(driver):
22
+ push_bundle_if_needed(f"{BIN_PATH}/test-backend-ops")
23
+
24
+
25
+ @pytest.mark.parametrize("type_a", ["mxfp4", "fp16", "q4_0"])
26
+ def test_backend_ops_htp0(type_a):
27
+ cmd = f"{CMD_PREFIX} GGML_HEXAGON_HOSTBUF=0 GGML_HEXAGON_EXPERIMENTAL=1 {BIN_PATH}/test-backend-ops -b HTP0 -o MUL_MAT"
28
+ if type_a == "q4_0":
29
+ cmd += r' -p "^(?=.*type_a=q4_0)(?!.*type_b=f32,m=576,n=512,k=576).*$"'
30
+ else:
31
+ cmd += f" -p type_a={type_a}"
32
+ result = run_adb_command(
33
+ cmd,
34
+ check=False,
35
+ )
36
+ write_qdc_log(f"backend_ops_{type_a}.log", result.stdout or "")
37
+ assert result.returncode == 0, f"test-backend-ops type_a={type_a} failed (exit {result.returncode})"
38
+
39
+
40
+ if __name__ == "__main__":
41
+ ret = pytest.main(["-s", "--junitxml=results.xml", os.path.realpath(__file__)])
42
+ if os.path.exists("results.xml"):
43
+ with open("results.xml") as f:
44
+ write_qdc_log("results.xml", f.read())
45
+ sys.exit(ret)
run_bench_tests_posix.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ---------------------------------------------------------------------
2
+ # Copyright (c) 2025 Qualcomm Technologies, Inc. and/or its subsidiaries.
3
+ # SPDX-License-Identifier: BSD-3-Clause
4
+ # ---------------------------------------------------------------------
5
+ """
6
+ On-device bench and completion test runner for llama.cpp (CPU, GPU, NPU backends).
7
+
8
+ Executed by QDC's Appium test framework on the QDC runner.
9
+ The runner has ADB access to the allocated device.
10
+
11
+ Placeholders replaced at artifact creation time by run_qdc_jobs.py:
12
+ <<MODEL_URL>> Direct URL to the GGUF model file (downloaded on-device via curl)
13
+ """
14
+
15
+ import os
16
+ import subprocess
17
+ import sys
18
+
19
+ import pytest
20
+
21
+ from utils import BIN_PATH, CMD_PREFIX, push_bundle_if_needed, run_adb_command, write_qdc_log
22
+
23
+ MODEL_PATH = "/data/local/tmp/model.gguf"
24
+ PROMPT = "What is the capital of France?"
25
+ CLI_OPTS = "--batch-size 128 -n 128 -no-cnv --seed 42"
26
+
27
+
28
+ @pytest.fixture(scope="session", autouse=True)
29
+ def install(driver):
30
+ push_bundle_if_needed(f"{BIN_PATH}/llama-cli")
31
+
32
+ # Skip model download if already present
33
+ check = subprocess.run(
34
+ ["adb", "shell", f"ls {MODEL_PATH}"],
35
+ text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
36
+ )
37
+ if check.returncode != 0:
38
+ run_adb_command(f'curl -L -J --output {MODEL_PATH} "<<MODEL_URL>>"')
39
+
40
+
41
+ @pytest.mark.parametrize("device,extra_flags", [
42
+ pytest.param("none", "-ctk q8_0 -ctv q8_0", id="cpu"),
43
+ pytest.param("GPUOpenCL", "", id="gpu"),
44
+ pytest.param("HTP0", "-ctk q8_0 -ctv q8_0", id="npu"),
45
+ ])
46
+ def test_llama_completion(device, extra_flags):
47
+ result = run_adb_command(
48
+ f'{CMD_PREFIX} {BIN_PATH}/llama-completion'
49
+ f' -m {MODEL_PATH} --device {device} -ngl 99 -t 4 {CLI_OPTS} {extra_flags} -fa on'
50
+ f' -p "{PROMPT}"',
51
+ check=False,
52
+ )
53
+ write_qdc_log(f"llama_completion_{device}.log", result.stdout or "")
54
+ assert result.returncode == 0, f"llama-completion {device} failed (exit {result.returncode})"
55
+
56
+
57
+ _DEVICE_LOG_NAME = {"none": "cpu", "GPUOpenCL": "gpu", "HTP0": "htp"}
58
+
59
+
60
+ @pytest.mark.parametrize("device", [
61
+ pytest.param("none", id="cpu"),
62
+ pytest.param("GPUOpenCL", id="gpu"),
63
+ pytest.param("HTP0", id="npu"),
64
+ ])
65
+ def test_llama_bench(device):
66
+ result = run_adb_command(
67
+ f"{CMD_PREFIX} {BIN_PATH}/llama-bench"
68
+ f" -m {MODEL_PATH} --device {device} -ngl 99 --batch-size 128 -t 4 -p 128 -n 32",
69
+ check=False,
70
+ )
71
+ write_qdc_log(f"llama_bench_{_DEVICE_LOG_NAME[device]}.log", result.stdout or "")
72
+ assert result.returncode == 0, f"llama-bench {device} failed (exit {result.returncode})"
73
+
74
+
75
+ if __name__ == "__main__":
76
+ ret = pytest.main(["-s", "--junitxml=results.xml", os.path.realpath(__file__)])
77
+ if os.path.exists("results.xml"):
78
+ with open("results.xml") as f:
79
+ write_qdc_log("results.xml", f.read())
80
+ sys.exit(ret)
run_scorecard_posix.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ---------------------------------------------------------------------
2
+ # Copyright (c) 2025 Qualcomm Technologies, Inc. and/or its subsidiaries.
3
+ # SPDX-License-Identifier: BSD-3-Clause
4
+ # ---------------------------------------------------------------------
5
+ """
6
+ Scorecard benchmark script for llama.cpp on Android devices via Appium.
7
+
8
+ This script runs comprehensive benchmarks using the run-*.sh scripts from
9
+ llama.cpp/scripts/snapdragon/adb/:
10
+ 1. Performance benchmarks (CPU/GPU/HTP x 3 context lengths)
11
+ 2. Fallback ops detection (SCHED=1)
12
+ 3. Perplexity (WikiText-2)
13
+
14
+ Placeholders are replaced at artifact creation time:
15
+ - <<MODEL_URL>>: URL to download the model
16
+ """
17
+
18
+ import os
19
+ import subprocess
20
+ import sys
21
+
22
+ import pytest
23
+ from appium import webdriver
24
+ from appium.options.common import AppiumOptions
25
+
26
+ options = AppiumOptions()
27
+ options.set_capability("automationName", "UiAutomator2")
28
+ options.set_capability("platformName", "Android")
29
+ options.set_capability("deviceName", os.getenv("ANDROID_DEVICE_VERSION"))
30
+
31
+ # Context lengths to benchmark
32
+ CONTEXT_LENGTHS = [128, 1024, 4096]
33
+
34
+ # System prompt for completion benchmarks
35
+ SYSTEM_PROMPT = "You are a helpful assistant. Be helpful but brief."
36
+
37
+
38
+ class TestScorecard:
39
+ @pytest.fixture
40
+ def driver(self) -> webdriver.Remote:
41
+ return webdriver.Remote(
42
+ command_executor="http://127.0.0.1:4723/wd/hub", options=options
43
+ )
44
+
45
+ def test_scorecard(self, driver: webdriver.Remote) -> None:
46
+ """Run comprehensive llama.cpp scorecard benchmarks."""
47
+ model_url = "<<MODEL_URL>>"
48
+ num_htps = "<<NUM_HTPS>>"
49
+
50
+ # On-device paths (matching llama.cpp scripts/snapdragon/adb conventions)
51
+ basedir = "/data/local/tmp/llama.cpp"
52
+ model_path = "/data/local/tmp/gguf/model.gguf"
53
+ log_file = "/data/local/tmp/QDC_logs/scorecard.log"
54
+
55
+ scorecard_script = f"""
56
+ cd /data/local/tmp/llama_cpp_bundle
57
+
58
+ export LD_LIBRARY_PATH=/data/local/tmp/llama_cpp_bundle/lib:$LD_LIBRARY_PATH
59
+ export ADSP_LIBRARY_PATH="/data/local/tmp/llama_cpp_bundle/lib;/system/lib/rfsa/adsp;/system/vendor/lib/rfsa/adsp;/dsp"
60
+ chmod +x /data/local/tmp/llama_cpp_bundle/bin/*
61
+
62
+ BASEDIR=/data/local/tmp/llama_cpp_bundle
63
+ MODEL={model_path}
64
+ LOG_FILE={log_file}
65
+ NUM_HTPS={num_htps}
66
+ HTP_FLAGS="--no-mmap --poll 1000 -t 6 --cpu-mask 0xfc --cpu-strict 1 -fa on -ngl 99"
67
+
68
+ mkdir -p /data/local/tmp/gguf /data/local/tmp/QDC_logs
69
+
70
+ echo "Downloading model from {model_url}..."
71
+ curl -L -J --output $MODEL "{model_url}"
72
+
73
+ echo "============================================================" > $LOG_FILE
74
+ echo "LLAMA.CPP SCORECARD" >> $LOG_FILE
75
+ echo "Date: $(date)" >> $LOG_FILE
76
+ echo "Model: {model_url}" >> $LOG_FILE
77
+ echo "============================================================" >> $LOG_FILE
78
+
79
+ # --- Helper: derive NDEV and extra flags from compute unit ---
80
+ device_flags() {{
81
+ local DEVICE="$1"
82
+ NDEV=0; EXTRA_FLAGS=""
83
+ case "$DEVICE" in
84
+ CPU) EXTRA_FLAGS="--n-gpu-layers 0" ;;
85
+ GPU) EXTRA_FLAGS="-fa off" ;;
86
+ HTP) NDEV=$NUM_HTPS
87
+ EXTRA_FLAGS="$HTP_FLAGS --device HTP0 -ctk f16 -ctv f16 --batch-size 128" ;;
88
+ esac
89
+ }}
90
+
91
+ #############################################
92
+ # SECTION 1: PERFORMANCE BENCHMARKS
93
+ #############################################
94
+ echo "" >> $LOG_FILE
95
+ echo "########################################################" >> $LOG_FILE
96
+ echo "# SECTION 1: PERFORMANCE BENCHMARKS" >> $LOG_FILE
97
+ echo "########################################################" >> $LOG_FILE
98
+
99
+ for COMPUTE in CPU GPU HTP; do
100
+ device_flags "$COMPUTE"
101
+ for CTX_LEN in 128 1024 4096; do
102
+ echo "" >> $LOG_FILE
103
+ echo "=== $COMPUTE | CTX=$CTX_LEN ===" >> $LOG_FILE
104
+ GGML_HEXAGON_NDEV=$NDEV $BASEDIR/bin/llama-completion \\
105
+ --model $MODEL \\
106
+ --n-predict -1 \\
107
+ --ctx-size $CTX_LEN \\
108
+ --system-prompt "{SYSTEM_PROMPT}" \\
109
+ --file "$BASEDIR/sample_prompt_${{CTX_LEN}}.txt" \\
110
+ --seed 1 --single-turn --no-display-prompt \\
111
+ $EXTRA_FLAGS \\
112
+ 2>&1 | tee -a $LOG_FILE
113
+ done
114
+ done
115
+
116
+ #############################################
117
+ # SECTION 2: FALLBACK OPS DETECTION
118
+ #############################################
119
+ echo "" >> $LOG_FILE
120
+ echo "########################################################" >> $LOG_FILE
121
+ echo "# SECTION 2: FALLBACK OPS (GGML_SCHED_DEBUG=2)" >> $LOG_FILE
122
+ echo "########################################################" >> $LOG_FILE
123
+
124
+ for DEVICE in GPU HTP; do
125
+ device_flags "$DEVICE"
126
+ echo "" >> $LOG_FILE
127
+ echo "=== FALLBACK_OPS | $DEVICE ===" >> $LOG_FILE
128
+ GGML_SCHED_DEBUG=2 GGML_HEXAGON_NDEV=$NDEV $BASEDIR/bin/llama-completion \\
129
+ --model $MODEL \\
130
+ --n-predict 256 --ctx-size 128 \\
131
+ -p "Hello world" \\
132
+ --seed 1 --single-turn --no-display-prompt \\
133
+ $EXTRA_FLAGS -v \\
134
+ 2>&1 | tee -a $LOG_FILE
135
+ done
136
+
137
+ #############################################
138
+ # SECTION 3: PERPLEXITY (WikiText-2)
139
+ #############################################
140
+ echo "" >> $LOG_FILE
141
+ echo "########################################################" >> $LOG_FILE
142
+ echo "# SECTION 3: PERPLEXITY (WikiText-2)" >> $LOG_FILE
143
+ echo "########################################################" >> $LOG_FILE
144
+
145
+ for DEVICE in CPU GPU HTP; do
146
+ device_flags "$DEVICE"
147
+ echo "" >> $LOG_FILE
148
+ echo "=== PERPLEXITY | $DEVICE ===" >> $LOG_FILE
149
+ GGML_HEXAGON_NDEV=$NDEV $BASEDIR/bin/llama-perplexity \\
150
+ -m $MODEL \\
151
+ -f $BASEDIR/wiki.test.raw \\
152
+ --ctx-size 2048 --chunks 10 \\
153
+ $EXTRA_FLAGS \\
154
+ 2>&1 | tee -a $LOG_FILE
155
+ done
156
+
157
+ #############################################
158
+ # SECTION 4: QUALITY CHECKS (Q&A Validation)
159
+ #############################################
160
+ echo "" >> $LOG_FILE
161
+ echo "########################################################" >> $LOG_FILE
162
+ echo "# SECTION 4: QUALITY CHECKS" >> $LOG_FILE
163
+ echo "########################################################" >> $LOG_FILE
164
+
165
+ run_quality_check() {{
166
+ local DEVICE="$1" QUESTION="$2" EXPECTED="$3"
167
+ device_flags "$DEVICE"
168
+
169
+ echo "" >> $LOG_FILE
170
+ echo "--- Quality Check ($DEVICE) ---" >> $LOG_FILE
171
+ echo "Question: $QUESTION" >> $LOG_FILE
172
+ echo "Expected to contain: $EXPECTED" >> $LOG_FILE
173
+
174
+ RAW_RESPONSE=$(GGML_HEXAGON_NDEV=$NDEV $BASEDIR/bin/llama-completion \\
175
+ --model $MODEL \\
176
+ -p "$QUESTION" \\
177
+ --n-predict 256 --ctx-size 512 \\
178
+ --seed 1 --single-turn --no-display-prompt \\
179
+ $EXTRA_FLAGS \\
180
+ 2>/dev/null)
181
+
182
+ RESPONSE=$(echo "$RAW_RESPONSE" | sed 's/[^[:print:][:space:]]//g' | sed '/^[[:space:]]*$/d' | head -20)
183
+
184
+ echo "RESPONSE_START" >> $LOG_FILE
185
+ echo "$RESPONSE" >> $LOG_FILE
186
+ echo "RESPONSE_END" >> $LOG_FILE
187
+
188
+ if echo "$RAW_RESPONSE" | grep -qi "$EXPECTED"; then
189
+ echo "QUALITY_CHECK: $DEVICE | $QUESTION | $EXPECTED | PASS" >> $LOG_FILE
190
+ else
191
+ echo "QUALITY_CHECK: $DEVICE | $QUESTION | $EXPECTED | FAIL" >> $LOG_FILE
192
+ fi
193
+ }}
194
+
195
+ for DEVICE in CPU GPU HTP; do
196
+ echo "" >> $LOG_FILE
197
+ echo "=== QUALITY_CHECKS | $DEVICE ===" >> $LOG_FILE
198
+ run_quality_check "$DEVICE" "The capital of France is" "Paris"
199
+ run_quality_check "$DEVICE" "2 + 2 =" "4"
200
+ run_quality_check "$DEVICE" "The planet closest to the Sun is" "Mercury"
201
+ done
202
+
203
+ echo "" >> $LOG_FILE
204
+ echo "============================================================" >> $LOG_FILE
205
+ echo "=== SCORECARD COMPLETE ===" >> $LOG_FILE
206
+ echo "============================================================" >> $LOG_FILE
207
+ """
208
+
209
+ # Push the bundle to the device
210
+ subprocess.run(
211
+ ["adb", "push", "/qdc/appium/llama_cpp_bundle/", "/data/local/tmp"],
212
+ capture_output=True,
213
+ encoding="utf-8",
214
+ errors="replace",
215
+ check=True,
216
+ )
217
+
218
+ # Run the scorecard script
219
+ result = subprocess.run(
220
+ [
221
+ "adb",
222
+ "shell",
223
+ "sh",
224
+ "-c",
225
+ scorecard_script,
226
+ ],
227
+ capture_output=True,
228
+ encoding="utf-8",
229
+ errors="replace",
230
+ check=True,
231
+ )
232
+ print(result.stdout)
233
+ print(result.stderr)
234
+
235
+
236
+ if __name__ == "__main__":
237
+ sys.exit(pytest.main(["-s", "--junitxml=results.xml", os.path.realpath(__file__)]))
utils.py CHANGED
@@ -2,12 +2,7 @@
2
  # Copyright (c) 2025 Qualcomm Technologies, Inc. and/or its subsidiaries.
3
  # SPDX-License-Identifier: BSD-3-Clause
4
  # ---------------------------------------------------------------------
5
- """Shared helpers for QDC on-device test runners (Linux/IoT devices).
6
-
7
- Unlike Android devices which use ADB, Linux IoT devices are accessed via
8
- SSH through QDC's infrastructure. The test scripts run directly on the
9
- device through the QDC Appium session.
10
- """
11
 
12
  import os
13
  import subprocess
@@ -16,131 +11,42 @@ import tempfile
16
  from appium.options.common import AppiumOptions
17
 
18
  # ---------------------------------------------------------------------------
19
- # On-device paths (Linux IoT)
20
  # ---------------------------------------------------------------------------
21
 
22
- BUNDLE_PATH = "/tmp/llama_cpp_bundle"
23
- QDC_LOGS_PATH = "/tmp/QDC_logs"
24
- LIB_PATH = f"{BUNDLE_PATH}/lib"
25
- BIN_PATH = f"{BUNDLE_PATH}/bin"
26
- ENV_PREFIX = (
27
- f"export LD_LIBRARY_PATH={LIB_PATH}:$LD_LIBRARY_PATH && "
28
  f"export ADSP_LIBRARY_PATH={LIB_PATH} && "
29
  f"chmod +x {BIN_PATH}/* &&"
30
  )
31
- CMD_PREFIX = f"cd {BUNDLE_PATH} && {ENV_PREFIX}"
32
 
33
  # ---------------------------------------------------------------------------
34
- # Appium session options (Linux/IoT)
35
  # ---------------------------------------------------------------------------
36
 
37
  options = AppiumOptions()
38
- options.set_capability("automationName", "QDCLinux")
39
- options.set_capability("platformName", "Linux")
40
- options.set_capability("deviceName", os.getenv("QDC_DEVICE_NAME", "QCS9075M"))
41
 
42
  # ---------------------------------------------------------------------------
43
- # SSH/Shell helpers for Linux devices
44
  # ---------------------------------------------------------------------------
45
 
46
-
47
- def verify_binary_exists(binary_path: str) -> bool:
48
- """Verify that a binary exists and is executable.
49
-
50
- Args:
51
- binary_path: Full path to the binary to check
52
-
53
- Returns:
54
- True if binary exists and is executable, False otherwise
55
- """
56
- print(f"[DEBUG] Verifying binary: {binary_path}")
57
-
58
- # Check if file exists
59
- result = run_shell_command(f"test -f {binary_path}", check=False)
60
- if result.returncode != 0:
61
- print(f"[ERROR] Binary does not exist: {binary_path}")
62
- print(f"[ERROR] Expected location: {binary_path}")
63
- print(f"[ERROR] Bundle should be at: {BUNDLE_PATH}")
64
- print(f"[ERROR] Check if binaries were pushed correctly")
65
- return False
66
-
67
- # Check if executable
68
- result = run_shell_command(f"test -x {binary_path}", check=False)
69
- if result.returncode != 0:
70
- print(f"[WARNING] Binary exists but is not executable: {binary_path}")
71
- print(f"[DEBUG] Attempting to set executable permissions")
72
- chmod_result = run_shell_command(f"chmod +x {binary_path}", check=False)
73
- if chmod_result.returncode != 0:
74
- print(f"[ERROR] Failed to set executable permissions on {binary_path}")
75
- return False
76
- print(f"[DEBUG] Successfully set executable permissions")
77
-
78
- # Get file info for debugging
79
- ls_result = run_shell_command(f"ls -lh {binary_path}", check=False)
80
- if ls_result.returncode == 0:
81
- print(f"[DEBUG] Binary info: {ls_result.stdout.strip()}")
82
-
83
- print(f"[DEBUG] Binary verified: {binary_path}")
84
- return True
85
-
86
-
87
- def run_shell_command(cmd: str, *, check: bool = True) -> subprocess.CompletedProcess:
88
- """Run a shell command on the Linux device.
89
-
90
- For QDC Linux devices, commands are executed through the QDC infrastructure
91
- which provides SSH access to the device. The QDC Appium driver handles the
92
- SSH tunneling transparently.
93
-
94
- When running directly on-device (QDC_DEVICE_HOST=localhost), executes
95
- commands locally via shell to avoid SSH password prompts.
96
- """
97
- device_host = os.getenv("QDC_DEVICE_HOST", "localhost")
98
-
99
- print(f"[DEBUG] Running command on device_host='{device_host}'")
100
- print(f"[DEBUG] Command: {cmd[:200]}{'...' if len(cmd) > 200 else ''}")
101
-
102
- try:
103
- # If localhost, run directly via shell (avoids SSH password prompt for on-device testing)
104
- if device_host == "localhost":
105
- raw = subprocess.run(
106
- ["/bin/sh", "-c", f"{cmd}; echo __RC__:$?"],
107
- text=True,
108
- stdout=subprocess.PIPE,
109
- stderr=subprocess.STDOUT,
110
- timeout=300,
111
- )
112
- else:
113
- # Remote device: use SSH
114
- print(f"[DEBUG] Using SSH to connect to {device_host}")
115
- raw = subprocess.run(
116
- ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10",
117
- device_host, f"{cmd}; echo __RC__:$?"],
118
- text=True,
119
- stdout=subprocess.PIPE,
120
- stderr=subprocess.STDOUT,
121
- timeout=300,
122
- )
123
-
124
- # Check for SSH authentication failures
125
- if raw.returncode != 0 and raw.stdout:
126
- if "Permission denied" in raw.stdout:
127
- print("[ERROR] SSH authentication failed! Password required or key not set up.")
128
- print("[ERROR] To fix: Set up passwordless SSH or set QDC_DEVICE_HOST=localhost if on-device")
129
- elif "Connection refused" in raw.stdout:
130
- print(f"[ERROR] SSH connection refused to {device_host}. Is SSH server running?")
131
- elif "Host key verification failed" in raw.stdout:
132
- print(f"[ERROR] SSH host key verification failed for {device_host}")
133
- print("[ERROR] To fix: ssh-keyscan {device_host} >> ~/.ssh/known_hosts")
134
-
135
- except subprocess.TimeoutExpired as e:
136
- print(f"[ERROR] Command timed out after 300 seconds")
137
- print(f"[ERROR] Command was: {cmd[:200]}")
138
- raise
139
-
140
  stdout = raw.stdout
141
  returncode = raw.returncode
142
-
143
- # Parse exit code from __RC__: sentinel
144
  if stdout:
145
  lines = stdout.rstrip("\n").split("\n")
146
  if lines and lines[-1].startswith("__RC__:"):
@@ -148,155 +54,40 @@ def run_shell_command(cmd: str, *, check: bool = True) -> subprocess.CompletedPr
148
  returncode = int(lines[-1][7:])
149
  stdout = "\n".join(lines[:-1]) + "\n"
150
  except ValueError:
151
- print(f"[WARNING] Failed to parse exit code from: {lines[-1]}")
152
-
153
  print(stdout)
154
-
155
- if returncode != 0:
156
- print(f"[ERROR] Command failed with exit code {returncode}")
157
-
158
- # Try to provide helpful context for common errors
159
- if "No such file or directory" in stdout:
160
- print("[ERROR] File or directory not found. Check if binaries were pushed to device.")
161
- print(f"[ERROR] Expected bundle path: {BUNDLE_PATH}")
162
- elif "Permission denied" in stdout and device_host == "localhost":
163
- print("[ERROR] Permission denied. Check file permissions or run with appropriate privileges.")
164
- elif "not found" in stdout.lower() or "command not found" in stdout.lower():
165
- print("[ERROR] Command not found. Check if binary exists and is in PATH or use full path.")
166
-
167
  result = subprocess.CompletedProcess(raw.args, returncode, stdout=stdout)
168
  if check:
169
- assert returncode == 0, f"Command failed (exit {returncode})\nCommand: {cmd[:200]}\nOutput: {stdout[:500]}"
170
  return result
171
 
172
 
173
  def write_qdc_log(filename: str, content: str) -> None:
174
- """Write content as a log file to QDC_LOGS_PATH on the device for QDC log collection."""
175
- print(f"[DEBUG] Writing QDC log: {filename} ({len(content)} bytes)")
176
-
177
- # Ensure log directory exists
178
- mkdir_result = run_shell_command(f"mkdir -p {QDC_LOGS_PATH}", check=False)
179
- if mkdir_result.returncode != 0:
180
- print(f"[WARNING] Failed to create log directory {QDC_LOGS_PATH}: {mkdir_result.stdout}")
181
-
182
- device_host = os.getenv("QDC_DEVICE_HOST", "localhost")
183
-
184
  try:
185
- if device_host == "localhost":
186
- # Running on-device: write directly to filesystem
187
- log_path = f"{QDC_LOGS_PATH}/{filename}"
188
- with open(log_path, "w") as f:
189
- f.write(content)
190
- print(f"[DEBUG] Successfully wrote log to {log_path}")
191
- else:
192
- # Remote device: use SCP
193
- with tempfile.NamedTemporaryFile(mode="w", suffix=".log", delete=False) as f:
194
- f.write(content)
195
- tmp_path = f.name
196
-
197
- print(f"[DEBUG] Using SCP to transfer log to {device_host}")
198
- result = subprocess.run(
199
- ["scp", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10",
200
- tmp_path, f"{device_host}:{QDC_LOGS_PATH}/{filename}"],
201
- stdout=subprocess.PIPE,
202
- stderr=subprocess.STDOUT,
203
- timeout=60,
204
- text=True,
205
- )
206
-
207
- if result.returncode != 0:
208
- print(f"[ERROR] SCP failed with exit code {result.returncode}")
209
- print(f"[ERROR] Output: {result.stdout}")
210
- else:
211
- print(f"[DEBUG] Successfully transferred log to {device_host}:{QDC_LOGS_PATH}/{filename}")
212
-
213
- os.unlink(tmp_path)
214
-
215
- except Exception as e:
216
- print(f"[ERROR] Failed to write QDC log {filename}: {e}")
217
- raise
218
 
219
 
220
  def push_bundle_if_needed(check_binary: str) -> None:
221
  """Push llama_cpp_bundle to the device if check_binary is not already present."""
222
- print(f"[DEBUG] Checking if binary exists: {check_binary}")
223
-
224
- result = run_shell_command(f"ls {check_binary}", check=False)
225
-
226
- if result.returncode == 0:
227
- print(f"[DEBUG] Binary already exists on device: {check_binary}")
228
- return
229
-
230
- print(f"[WARNING] Binary not found: {check_binary}")
231
- print(f"[DEBUG] Will attempt to push bundle from /qdc/appium/llama_cpp_bundle/ to {BUNDLE_PATH}")
232
-
233
- device_host = os.getenv("QDC_DEVICE_HOST", "localhost")
234
- source_path = "/qdc/appium/llama_cpp_bundle/"
235
-
236
- try:
237
- if device_host == "localhost":
238
- # Running on-device: copy locally (if source exists)
239
- if not os.path.exists(source_path):
240
- print(f"[ERROR] Source bundle not found at {source_path}")
241
- print(f"[ERROR] You may need to manually copy binaries to {BUNDLE_PATH}")
242
- print(f"[ERROR] Expected structure: {BUNDLE_PATH}/{{bin,lib}}/")
243
- return
244
-
245
- print(f"[DEBUG] Copying bundle from {source_path} to /tmp/")
246
- result = subprocess.run(
247
- ["cp", "-r", source_path, "/tmp/"],
248
- text=True,
249
- stdout=subprocess.PIPE,
250
- stderr=subprocess.STDOUT,
251
- timeout=120,
252
- )
253
-
254
- if result.returncode != 0:
255
- print(f"[ERROR] Failed to copy bundle: {result.stdout}")
256
- else:
257
- print(f"[DEBUG] Successfully copied bundle to {BUNDLE_PATH}")
258
-
259
- # Verify the copy succeeded
260
- verify = run_shell_command(f"ls {check_binary}", check=False)
261
- if verify.returncode != 0:
262
- print(f"[ERROR] Bundle copied but binary still not found: {check_binary}")
263
- else:
264
- print(f"[DEBUG] Verified binary exists after copy: {check_binary}")
265
- else:
266
- # Remote device: use SCP
267
- print(f"[DEBUG] Using SCP to transfer bundle to {device_host}:/tmp/")
268
-
269
- if not os.path.exists(source_path):
270
- print(f"[ERROR] Source bundle not found at {source_path}")
271
- print(f"[ERROR] Cannot push to remote device")
272
- return
273
-
274
- result = subprocess.run(
275
- ["scp", "-r", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10",
276
- source_path, f"{device_host}:/tmp/"],
277
- text=True,
278
- stdout=subprocess.PIPE,
279
- stderr=subprocess.STDOUT,
280
- timeout=120,
281
- )
282
-
283
- if result.returncode != 0:
284
- print(f"[ERROR] SCP failed: {result.stdout}")
285
- if "Permission denied" in result.stdout:
286
- print("[ERROR] SSH authentication failed. Set up passwordless SSH.")
287
- else:
288
- print(f"[DEBUG] Successfully transferred bundle to {device_host}:{BUNDLE_PATH}")
289
-
290
- # Verify the transfer succeeded
291
- verify = run_shell_command(f"ls {check_binary}", check=False)
292
- if verify.returncode != 0:
293
- print(f"[ERROR] Bundle transferred but binary still not found: {check_binary}")
294
- else:
295
- print(f"[DEBUG] Verified binary exists after transfer: {check_binary}")
296
-
297
- except subprocess.TimeoutExpired:
298
- print(f"[ERROR] Timeout while pushing bundle (exceeded 120 seconds)")
299
- raise
300
- except Exception as e:
301
- print(f"[ERROR] Unexpected error while pushing bundle: {e}")
302
- raise
 
2
  # Copyright (c) 2025 Qualcomm Technologies, Inc. and/or its subsidiaries.
3
  # SPDX-License-Identifier: BSD-3-Clause
4
  # ---------------------------------------------------------------------
5
+ """Shared helpers for QDC on-device test runners."""
 
 
 
 
 
6
 
7
  import os
8
  import subprocess
 
11
  from appium.options.common import AppiumOptions
12
 
13
  # ---------------------------------------------------------------------------
14
+ # On-device paths
15
  # ---------------------------------------------------------------------------
16
 
17
+ BUNDLE_PATH = "/data/local/tmp/llama_cpp_bundle"
18
+ QDC_LOGS_PATH = "/data/local/tmp/QDC_logs"
19
+ LIB_PATH = f"{BUNDLE_PATH}/lib"
20
+ BIN_PATH = f"{BUNDLE_PATH}/bin"
21
+ ENV_PREFIX = (
22
+ f"export LD_LIBRARY_PATH={LIB_PATH} && "
23
  f"export ADSP_LIBRARY_PATH={LIB_PATH} && "
24
  f"chmod +x {BIN_PATH}/* &&"
25
  )
26
+ CMD_PREFIX = f"cd {BUNDLE_PATH} && {ENV_PREFIX}"
27
 
28
  # ---------------------------------------------------------------------------
29
+ # Appium session options
30
  # ---------------------------------------------------------------------------
31
 
32
  options = AppiumOptions()
33
+ options.set_capability("automationName", "UiAutomator2")
34
+ options.set_capability("platformName", "Android")
35
+ options.set_capability("deviceName", os.getenv("ANDROID_DEVICE_VERSION"))
36
 
37
  # ---------------------------------------------------------------------------
38
+ # ADB helpers
39
  # ---------------------------------------------------------------------------
40
 
41
+ def run_adb_command(cmd: str, *, check: bool = True) -> subprocess.CompletedProcess:
42
+ # Append exit-code sentinel because `adb shell` doesn't reliably propagate
43
+ # the on-device exit code (older ADB versions always return 0).
44
+ raw = subprocess.run(
45
+ ["adb", "shell", f"{cmd}; echo __RC__:$?"],
46
+ text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
47
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  stdout = raw.stdout
49
  returncode = raw.returncode
 
 
50
  if stdout:
51
  lines = stdout.rstrip("\n").split("\n")
52
  if lines and lines[-1].startswith("__RC__:"):
 
54
  returncode = int(lines[-1][7:])
55
  stdout = "\n".join(lines[:-1]) + "\n"
56
  except ValueError:
57
+ pass
 
58
  print(stdout)
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  result = subprocess.CompletedProcess(raw.args, returncode, stdout=stdout)
60
  if check:
61
+ assert returncode == 0, f"Command failed (exit {returncode})"
62
  return result
63
 
64
 
65
  def write_qdc_log(filename: str, content: str) -> None:
66
+ """Push content as a log file to QDC_LOGS_PATH on the device for QDC log collection."""
67
+ subprocess.run(
68
+ ["adb", "shell", f"mkdir -p {QDC_LOGS_PATH}"],
69
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
70
+ )
71
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".log", delete=False) as f:
72
+ f.write(content)
73
+ tmp_path = f.name
 
 
74
  try:
75
+ subprocess.run(
76
+ ["adb", "push", tmp_path, f"{QDC_LOGS_PATH}/{filename}"],
77
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
78
+ )
79
+ finally:
80
+ os.unlink(tmp_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
 
83
  def push_bundle_if_needed(check_binary: str) -> None:
84
  """Push llama_cpp_bundle to the device if check_binary is not already present."""
85
+ result = subprocess.run(
86
+ ["adb", "shell", f"ls {check_binary}"],
87
+ text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
88
+ )
89
+ if result.returncode != 0:
90
+ subprocess.run(
91
+ ["adb", "push", "/qdc/appium/llama_cpp_bundle/", "/data/local/tmp"],
92
+ text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
93
+ )