wafer-cli 0.2.9__tar.gz → 0.2.11__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/PKG-INFO +1 -1
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/README.md +41 -2
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/pyproject.toml +2 -1
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/tests/test_cli_coverage.py +125 -33
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/tests/test_cli_parity_integration.py +1 -1
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/tests/test_kernel_scope_cli.py +171 -18
- wafer_cli-0.2.11/tests/test_nsys_analyze.py +293 -0
- wafer_cli-0.2.11/tests/test_nsys_profile.py +157 -0
- wafer_cli-0.2.11/tests/test_output.py +263 -0
- wafer_cli-0.2.11/tests/test_skill_commands.py +231 -0
- wafer_cli-0.2.11/tests/test_targets_ops.py +206 -0
- wafer_cli-0.2.11/tests/test_wevin_cli.py +660 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/GUIDE.md +18 -7
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/api_client.py +4 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/cli.py +1177 -278
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/corpus.py +158 -32
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/evaluate.py +75 -6
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/kernel_scope.py +132 -31
- wafer_cli-0.2.11/wafer/nsys_analyze.py +1042 -0
- wafer_cli-0.2.11/wafer/nsys_profile.py +511 -0
- wafer_cli-0.2.11/wafer/output.py +241 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/skills/wafer-guide/SKILL.md +13 -0
- wafer_cli-0.2.11/wafer/ssh_keys.py +261 -0
- wafer_cli-0.2.11/wafer/targets_ops.py +718 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/wevin_cli.py +127 -18
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/workspaces.py +232 -184
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer_cli.egg-info/PKG-INFO +1 -1
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer_cli.egg-info/SOURCES.txt +9 -1
- wafer_cli-0.2.9/tests/test_isa_cli.py +0 -212
- wafer_cli-0.2.9/tests/test_wevin_cli.py +0 -68
- wafer_cli-0.2.9/wafer/nsys_analyze.py +0 -212
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/setup.cfg +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/tests/test_analytics.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/tests/test_billing.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/tests/test_config_integration.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/tests/test_file_operations_integration.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/tests/test_rocprof_compute_integration.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/tests/test_ssh_integration.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/tests/test_workflow_integration.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/__init__.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/analytics.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/auth.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/autotuner.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/billing.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/config.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/global_config.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/gpu_run.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/inference.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/ncu_analyze.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/problems.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/rocprof_compute.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/rocprof_sdk.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/rocprof_systems.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/target_lock.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/targets.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/templates/__init__.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/templates/ask_docs.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/templates/optimize_kernel.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/templates/trace_analyze.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer/tracelens.py +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer_cli.egg-info/dependency_links.txt +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer_cli.egg-info/entry_points.txt +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer_cli.egg-info/requires.txt +0 -0
- {wafer_cli-0.2.9 → wafer_cli-0.2.11}/wafer_cli.egg-info/top_level.txt +0 -0
|
@@ -43,10 +43,16 @@ wafer remote-run --upload-dir ./my_code -- python3 train.py
|
|
|
43
43
|
|
|
44
44
|
Create and manage persistent GPU environments.
|
|
45
45
|
|
|
46
|
+
**Available GPUs:**
|
|
47
|
+
|
|
48
|
+
- `MI300X` - AMD Instinct MI300X (192GB HBM3, ROCm)
|
|
49
|
+
- `B200` - NVIDIA Blackwell B200 (180GB HBM3e, CUDA) - default
|
|
50
|
+
|
|
46
51
|
```bash
|
|
47
52
|
wafer workspaces list
|
|
48
|
-
wafer workspaces create my-workspace
|
|
49
|
-
wafer workspaces
|
|
53
|
+
wafer workspaces create my-workspace --gpu B200 --wait # NVIDIA B200
|
|
54
|
+
wafer workspaces create amd-dev --gpu MI300X # AMD MI300X
|
|
55
|
+
wafer workspaces ssh <workspace-id>
|
|
50
56
|
wafer workspaces delete <workspace-id>
|
|
51
57
|
```
|
|
52
58
|
|
|
@@ -197,6 +203,39 @@ Now you can tab-complete:
|
|
|
197
203
|
|
|
198
204
|
---
|
|
199
205
|
|
|
206
|
+
## AI Assistant Skills
|
|
207
|
+
|
|
208
|
+
Install the Wafer CLI skill to make wafer commands discoverable by your AI coding assistant:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# Install for all supported tools (Claude Code, Codex CLI, Cursor)
|
|
212
|
+
wafer skill install
|
|
213
|
+
|
|
214
|
+
# Install for a specific tool
|
|
215
|
+
wafer skill install -t cursor # Cursor
|
|
216
|
+
wafer skill install -t claude # Claude Code
|
|
217
|
+
wafer skill install -t codex # Codex CLI
|
|
218
|
+
|
|
219
|
+
# Check installation status
|
|
220
|
+
wafer skill status
|
|
221
|
+
|
|
222
|
+
# Uninstall
|
|
223
|
+
wafer skill uninstall
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Installing from GitHub (Cursor)
|
|
227
|
+
|
|
228
|
+
You can also install the skill directly from GitHub in Cursor:
|
|
229
|
+
|
|
230
|
+
1. Open Cursor Settings (Cmd+Shift+J / Ctrl+Shift+J)
|
|
231
|
+
2. Navigate to **Rules** → **Add Rule** → **Remote Rule (Github)**
|
|
232
|
+
3. Enter: `https://github.com/wafer-ai/skills`
|
|
233
|
+
4. Cursor will automatically discover skills in `.cursor/skills/`
|
|
234
|
+
|
|
235
|
+
The skill provides comprehensive guidance for GPU kernel development, including documentation lookup, trace analysis, kernel evaluation, and optimization workflows.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
200
239
|
## Requirements
|
|
201
240
|
|
|
202
241
|
- Python 3.10+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "wafer-cli"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.11"
|
|
4
4
|
description = "CLI tool for running commands on remote GPUs and GPU kernel optimization agent"
|
|
5
5
|
requires-python = ">=3.11"
|
|
6
6
|
dependencies = [
|
|
@@ -78,6 +78,7 @@ ignore = [
|
|
|
78
78
|
[tool.ruff.lint.per-file-ignores]
|
|
79
79
|
"tests/**/*.py" = ["ANN201"] # Don't require return type annotations in tests
|
|
80
80
|
"wafer/evaluate.py" = ["PLR0915", "PLR1702", "E402"] # complex deployment flows - TODO: refactor
|
|
81
|
+
"wafer/output.py" = ["ANN401"] # Output collector uses **kwargs for flexible event data
|
|
81
82
|
|
|
82
83
|
[tool.ruff.lint.pylint]
|
|
83
84
|
max-args = 7 # Max function arguments (Tiger Style: few parameters)
|
|
@@ -61,7 +61,7 @@ def _cleanup_test_workspaces() -> None:
|
|
|
61
61
|
return
|
|
62
62
|
for ws in workspaces:
|
|
63
63
|
if ws.get("name", "").startswith("test-"):
|
|
64
|
-
runner.invoke(app, ["workspaces", "delete", ws["id"]])
|
|
64
|
+
runner.invoke(app, ["workspaces", "delete", ws["id"], "-y"])
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
@pytest.fixture(scope="session", autouse=True)
|
|
@@ -213,7 +213,7 @@ class TestWorkspacesCommands:
|
|
|
213
213
|
finally:
|
|
214
214
|
# Delete
|
|
215
215
|
if ws_id:
|
|
216
|
-
result = runner.invoke(app, ["workspaces", "delete", ws_id])
|
|
216
|
+
result = runner.invoke(app, ["workspaces", "delete", ws_id, "-y"])
|
|
217
217
|
|
|
218
218
|
def test_workspaces_create_pretty(self) -> None:
|
|
219
219
|
"""Create workspace with pretty output."""
|
|
@@ -228,53 +228,43 @@ class TestWorkspacesCommands:
|
|
|
228
228
|
return
|
|
229
229
|
|
|
230
230
|
# Extract ID from output
|
|
231
|
-
# Output format: "
|
|
231
|
+
# Output format: "Creating workspace: <name> (<uuid>)"
|
|
232
232
|
match = re.search(r"\(([a-f0-9-]{36})\)", result.output)
|
|
233
233
|
if match:
|
|
234
234
|
ws_id = match.group(1)
|
|
235
|
+
assert "Creating workspace:" in result.output
|
|
235
236
|
assert ws_id is not None, f"Could not extract workspace ID from: {result.output}"
|
|
236
237
|
|
|
237
238
|
finally:
|
|
238
239
|
if ws_id:
|
|
239
|
-
runner.invoke(app, ["workspaces", "delete", ws_id])
|
|
240
|
+
runner.invoke(app, ["workspaces", "delete", ws_id, "-y"])
|
|
240
241
|
|
|
241
|
-
def
|
|
242
|
-
"""
|
|
243
|
-
ws_name = f"test-
|
|
242
|
+
def test_workspaces_create_wait_json(self) -> None:
|
|
243
|
+
"""Create workspace with --wait JSON output."""
|
|
244
|
+
ws_name = f"test-create-wait-{os.getpid()}"
|
|
244
245
|
ws_id = None
|
|
245
246
|
|
|
246
247
|
try:
|
|
247
|
-
|
|
248
|
-
|
|
248
|
+
result = runner.invoke(
|
|
249
|
+
app,
|
|
250
|
+
["workspaces", "create", ws_name, "--wait", "--json"],
|
|
251
|
+
)
|
|
249
252
|
if result.exit_code != 0:
|
|
250
253
|
if "auth" in result.output.lower() or "not authenticated" in result.output.lower():
|
|
251
254
|
pytest.skip("Not authenticated")
|
|
255
|
+
assert "error" in result.output.lower() or "no gpu" in result.output.lower()
|
|
252
256
|
return
|
|
253
257
|
|
|
254
258
|
data = json.loads(result.stdout)
|
|
255
|
-
ws_id = data.get("
|
|
259
|
+
ws_id = data.get("workspace_id")
|
|
256
260
|
assert ws_id is not None
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if result.exit_code == 0:
|
|
261
|
-
attach_data = json.loads(result.stdout)
|
|
262
|
-
# Should have SSH connection info
|
|
263
|
-
assert "ssh_host" in attach_data
|
|
264
|
-
assert "ssh_port" in attach_data
|
|
265
|
-
assert "ssh_user" in attach_data
|
|
266
|
-
assert "private_key_pem" in attach_data
|
|
267
|
-
else:
|
|
268
|
-
# May fail with 503 if no GPU available - that's ok
|
|
269
|
-
assert (
|
|
270
|
-
"503" in result.output
|
|
271
|
-
or "No GPU" in result.output
|
|
272
|
-
or "error" in result.output.lower()
|
|
273
|
-
)
|
|
261
|
+
assert "ssh_host" in data
|
|
262
|
+
assert "ssh_port" in data
|
|
263
|
+
assert "ssh_user" in data
|
|
274
264
|
|
|
275
265
|
finally:
|
|
276
266
|
if ws_id:
|
|
277
|
-
runner.invoke(app, ["workspaces", "delete", ws_id])
|
|
267
|
+
runner.invoke(app, ["workspaces", "delete", ws_id, "-y"])
|
|
278
268
|
|
|
279
269
|
def test_workspaces_show_not_found(self) -> None:
|
|
280
270
|
"""Show workspace with invalid ID returns 404."""
|
|
@@ -288,10 +278,10 @@ class TestWorkspacesCommands:
|
|
|
288
278
|
or "auth" in result.output.lower()
|
|
289
279
|
)
|
|
290
280
|
|
|
291
|
-
def
|
|
292
|
-
"""
|
|
281
|
+
def test_workspaces_ssh_not_found(self) -> None:
|
|
282
|
+
"""SSH to workspace with invalid ID returns 404."""
|
|
293
283
|
fake_id = "00000000-0000-0000-0000-000000000000"
|
|
294
|
-
result = runner.invoke(app, ["workspaces", "
|
|
284
|
+
result = runner.invoke(app, ["workspaces", "ssh", fake_id])
|
|
295
285
|
if result.exit_code != 0:
|
|
296
286
|
# Should be 404 or auth error
|
|
297
287
|
assert (
|
|
@@ -300,6 +290,15 @@ class TestWorkspacesCommands:
|
|
|
300
290
|
or "auth" in result.output.lower()
|
|
301
291
|
)
|
|
302
292
|
|
|
293
|
+
def test_workspaces_exec_option_order(self) -> None:
|
|
294
|
+
"""Exec should reject options after workspace name."""
|
|
295
|
+
result = runner.invoke(
|
|
296
|
+
app,
|
|
297
|
+
["workspaces", "exec", "dev", "--timeout", "10", "--", "echo", "hi"],
|
|
298
|
+
)
|
|
299
|
+
assert result.exit_code != 0
|
|
300
|
+
assert "options must come before the workspace name" in result.output.lower()
|
|
301
|
+
|
|
303
302
|
|
|
304
303
|
class TestNsysAnalyzeCommand:
|
|
305
304
|
"""Test wafer nvidia nsys analyze command."""
|
|
@@ -333,7 +332,24 @@ class TestNsysAnalyzeCommand:
|
|
|
333
332
|
result = runner.invoke(
|
|
334
333
|
app, ["nvidia", "nsys", "analyze", str(real_nsys_file), "--remote", "--json"]
|
|
335
334
|
)
|
|
336
|
-
|
|
335
|
+
if result.exit_code != 0:
|
|
336
|
+
combined = result.output.lower()
|
|
337
|
+
if "auth" in combined or "login" in combined or "401" in combined or "403" in combined:
|
|
338
|
+
pytest.skip("Not authenticated")
|
|
339
|
+
if (
|
|
340
|
+
"no gpu" in combined
|
|
341
|
+
or "no targets" in combined
|
|
342
|
+
or "unavailable" in combined
|
|
343
|
+
or "timed out" in combined
|
|
344
|
+
or "timeout" in combined
|
|
345
|
+
or "connect" in combined
|
|
346
|
+
or "service" in combined
|
|
347
|
+
or "billing" in combined
|
|
348
|
+
or "spend limit" in combined
|
|
349
|
+
or "402" in combined
|
|
350
|
+
):
|
|
351
|
+
pytest.skip(f"Remote nsys unavailable: {result.output.strip()}")
|
|
352
|
+
assert result.exit_code == 0
|
|
337
353
|
data = json.loads(result.stdout)
|
|
338
354
|
assert "kernels" in data or "timeline" in data or "summary" in data
|
|
339
355
|
|
|
@@ -344,7 +360,24 @@ class TestNsysAnalyzeCommand:
|
|
|
344
360
|
pytest.skip("No real .nsys-rep fixture available")
|
|
345
361
|
|
|
346
362
|
result = runner.invoke(app, ["nvidia", "nsys", "analyze", str(real_nsys_file), "--remote"])
|
|
347
|
-
|
|
363
|
+
if result.exit_code != 0:
|
|
364
|
+
combined = result.output.lower()
|
|
365
|
+
if "auth" in combined or "login" in combined or "401" in combined or "403" in combined:
|
|
366
|
+
pytest.skip("Not authenticated")
|
|
367
|
+
if (
|
|
368
|
+
"no gpu" in combined
|
|
369
|
+
or "no targets" in combined
|
|
370
|
+
or "unavailable" in combined
|
|
371
|
+
or "timed out" in combined
|
|
372
|
+
or "timeout" in combined
|
|
373
|
+
or "connect" in combined
|
|
374
|
+
or "service" in combined
|
|
375
|
+
or "billing" in combined
|
|
376
|
+
or "spend limit" in combined
|
|
377
|
+
or "402" in combined
|
|
378
|
+
):
|
|
379
|
+
pytest.skip(f"Remote nsys unavailable: {result.output.strip()}")
|
|
380
|
+
assert result.exit_code == 0
|
|
348
381
|
assert "NSYS" in result.output or "Kernel" in result.output or "Profile" in result.output
|
|
349
382
|
|
|
350
383
|
def test_nsys_analyze_local(self, real_nsys_file: Path | None) -> None:
|
|
@@ -627,3 +660,62 @@ class TestEvaluateCommand:
|
|
|
627
660
|
or "error" in combined
|
|
628
661
|
or result.exit_code in (0, 1)
|
|
629
662
|
)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
class TestWorkspacesExecFlagPassthrough:
|
|
666
|
+
"""Test that flags after -- are passed through to the command, not intercepted."""
|
|
667
|
+
|
|
668
|
+
def test_exec_passes_dash_i_flag(self) -> None:
|
|
669
|
+
"""The -i flag should pass through, not be intercepted as --image."""
|
|
670
|
+
result = runner.invoke(app, [
|
|
671
|
+
"workspaces", "exec", "test-ws", "--", "echo", "-i", "test"
|
|
672
|
+
])
|
|
673
|
+
# Should NOT fail with "no such option" or similar parse error
|
|
674
|
+
assert "no such option" not in result.output.lower()
|
|
675
|
+
assert "unrecognized" not in result.output.lower()
|
|
676
|
+
# May fail with workspace not found - that's fine, parsing succeeded
|
|
677
|
+
|
|
678
|
+
def test_exec_passes_dash_v_flag(self) -> None:
|
|
679
|
+
"""The -v flag should pass through, not be intercepted as --verbose."""
|
|
680
|
+
result = runner.invoke(app, [
|
|
681
|
+
"workspaces", "exec", "test-ws", "--", "python", "-v", "-c", "print(1)"
|
|
682
|
+
])
|
|
683
|
+
assert "no such option" not in result.output.lower()
|
|
684
|
+
|
|
685
|
+
def test_exec_passes_double_dash_help(self) -> None:
|
|
686
|
+
"""--help after -- should pass to command, not show exec help."""
|
|
687
|
+
result = runner.invoke(app, [
|
|
688
|
+
"workspaces", "exec", "test-ws", "--", "python", "--help"
|
|
689
|
+
])
|
|
690
|
+
# Should NOT show the exec command's help text
|
|
691
|
+
assert "Execute a command in workspace" not in result.output
|
|
692
|
+
|
|
693
|
+
def test_exec_passes_multiple_flags(self) -> None:
|
|
694
|
+
"""Multiple flags like -i -v -n should all pass through."""
|
|
695
|
+
result = runner.invoke(app, [
|
|
696
|
+
"workspaces", "exec", "test-ws", "--", "grep", "-i", "-v", "-n", "pattern"
|
|
697
|
+
])
|
|
698
|
+
assert "no such option" not in result.output.lower()
|
|
699
|
+
|
|
700
|
+
def test_exec_no_command_shows_error(self) -> None:
|
|
701
|
+
"""Missing command after -- should show helpful error."""
|
|
702
|
+
result = runner.invoke(app, [
|
|
703
|
+
"workspaces", "exec", "test-ws", "--"
|
|
704
|
+
])
|
|
705
|
+
assert result.exit_code != 0
|
|
706
|
+
assert "no command" in result.output.lower() or "error" in result.output.lower()
|
|
707
|
+
|
|
708
|
+
def test_exec_preserves_flag_order(self) -> None:
|
|
709
|
+
"""Flags should be preserved in order they were given."""
|
|
710
|
+
# This tests the shlex.join behavior
|
|
711
|
+
result = runner.invoke(app, [
|
|
712
|
+
"workspaces", "exec", "test-ws", "--", "cmd", "-a", "1", "-b", "2"
|
|
713
|
+
])
|
|
714
|
+
assert "no such option" not in result.output.lower()
|
|
715
|
+
|
|
716
|
+
def test_exec_with_equals_syntax(self) -> None:
|
|
717
|
+
"""Flags with --flag=value syntax should pass through."""
|
|
718
|
+
result = runner.invoke(app, [
|
|
719
|
+
"workspaces", "exec", "test-ws", "--", "cmd", "--output=/tmp/out"
|
|
720
|
+
])
|
|
721
|
+
assert "no such option" not in result.output.lower()
|
|
@@ -217,15 +217,15 @@ class TestTargetsCommandFunction:
|
|
|
217
217
|
|
|
218
218
|
|
|
219
219
|
# ============================================================================
|
|
220
|
-
# CLI Integration Tests
|
|
220
|
+
# CLI Integration Tests (using unified wafer amd isa command)
|
|
221
221
|
# ============================================================================
|
|
222
222
|
|
|
223
|
-
class
|
|
224
|
-
"""Tests for wafer amd
|
|
223
|
+
class TestISAAnalyzerCliCommands:
|
|
224
|
+
"""Tests for wafer amd isa CLI commands (unified ISA Analyzer)."""
|
|
225
225
|
|
|
226
226
|
def test_analyze_command_help(self) -> None:
|
|
227
227
|
"""Should display help for analyze command."""
|
|
228
|
-
result = runner.invoke(app, ["amd", "
|
|
228
|
+
result = runner.invoke(app, ["amd", "isa", "analyze", "--help"])
|
|
229
229
|
|
|
230
230
|
assert result.exit_code == 0
|
|
231
231
|
output = strip_ansi(result.stdout)
|
|
@@ -237,7 +237,7 @@ class TestKernelScopeCliCommands:
|
|
|
237
237
|
isa_file.write_text(SAMPLE_ISA)
|
|
238
238
|
|
|
239
239
|
result = runner.invoke(app, [
|
|
240
|
-
"amd", "
|
|
240
|
+
"amd", "isa", "analyze", str(isa_file)
|
|
241
241
|
])
|
|
242
242
|
|
|
243
243
|
assert result.exit_code == 0, f"Failed: {result.output}"
|
|
@@ -249,7 +249,7 @@ class TestKernelScopeCliCommands:
|
|
|
249
249
|
isa_file.write_text(SAMPLE_ISA)
|
|
250
250
|
|
|
251
251
|
result = runner.invoke(app, [
|
|
252
|
-
"amd", "
|
|
252
|
+
"amd", "isa", "analyze", str(isa_file), "--json"
|
|
253
253
|
])
|
|
254
254
|
|
|
255
255
|
assert result.exit_code == 0, f"Failed: {result.output}"
|
|
@@ -262,7 +262,7 @@ class TestKernelScopeCliCommands:
|
|
|
262
262
|
isa_file.write_text(SAMPLE_ISA)
|
|
263
263
|
|
|
264
264
|
result = runner.invoke(app, [
|
|
265
|
-
"amd", "
|
|
265
|
+
"amd", "isa", "analyze", str(isa_file), "--csv"
|
|
266
266
|
])
|
|
267
267
|
|
|
268
268
|
assert result.exit_code == 0, f"Failed: {result.output}"
|
|
@@ -271,32 +271,32 @@ class TestKernelScopeCliCommands:
|
|
|
271
271
|
def test_analyze_missing_file_via_cli(self, tmp_path: Path) -> None:
|
|
272
272
|
"""Should fail for missing file."""
|
|
273
273
|
result = runner.invoke(app, [
|
|
274
|
-
"amd", "
|
|
274
|
+
"amd", "isa", "analyze", str(tmp_path / "missing.s")
|
|
275
275
|
])
|
|
276
276
|
|
|
277
277
|
assert result.exit_code != 0
|
|
278
278
|
|
|
279
279
|
def test_metrics_via_cli(self) -> None:
|
|
280
280
|
"""Should list metrics via CLI."""
|
|
281
|
-
result = runner.invoke(app, ["amd", "
|
|
281
|
+
result = runner.invoke(app, ["amd", "isa", "metrics"])
|
|
282
282
|
|
|
283
283
|
assert result.exit_code == 0
|
|
284
284
|
assert "vgpr_count" in result.stdout
|
|
285
285
|
|
|
286
286
|
def test_targets_via_cli(self) -> None:
|
|
287
287
|
"""Should list targets via CLI."""
|
|
288
|
-
result = runner.invoke(app, ["amd", "
|
|
288
|
+
result = runner.invoke(app, ["amd", "isa", "targets"])
|
|
289
289
|
|
|
290
290
|
assert result.exit_code == 0
|
|
291
291
|
assert "gfx90a" in result.stdout or "gfx942" in result.stdout
|
|
292
292
|
|
|
293
293
|
|
|
294
|
-
class
|
|
295
|
-
"""Tests for
|
|
294
|
+
class TestISAAnalyzerCliHelp:
|
|
295
|
+
"""Tests for ISA Analyzer command help text."""
|
|
296
296
|
|
|
297
|
-
def
|
|
298
|
-
"""Should display help for
|
|
299
|
-
result = runner.invoke(app, ["amd", "
|
|
297
|
+
def test_isa_help(self) -> None:
|
|
298
|
+
"""Should display help for isa command group."""
|
|
299
|
+
result = runner.invoke(app, ["amd", "isa", "--help"])
|
|
300
300
|
|
|
301
301
|
assert result.exit_code == 0
|
|
302
302
|
output = strip_ansi(result.stdout)
|
|
@@ -304,12 +304,12 @@ class TestKernelScopeCliHelp:
|
|
|
304
304
|
assert "metrics" in output.lower()
|
|
305
305
|
assert "targets" in output.lower()
|
|
306
306
|
|
|
307
|
-
def
|
|
308
|
-
"""AMD help should mention
|
|
307
|
+
def test_amd_help_includes_isa(self) -> None:
|
|
308
|
+
"""AMD help should mention ISA analyzer."""
|
|
309
309
|
result = runner.invoke(app, ["amd", "--help"])
|
|
310
310
|
|
|
311
311
|
assert result.exit_code == 0
|
|
312
|
-
assert "
|
|
312
|
+
assert "isa" in result.stdout.lower()
|
|
313
313
|
|
|
314
314
|
|
|
315
315
|
# ============================================================================
|
|
@@ -465,3 +465,156 @@ class TestEdgeCases:
|
|
|
465
465
|
output = analyze_command(str(tmp_path), recursive=True)
|
|
466
466
|
|
|
467
467
|
assert "1 files" in output or "test_kernel" in output
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# ============================================================================
|
|
471
|
+
# Unified ISA Analyzer CLI Tests (supports .co, .s, .ll, .ttgir)
|
|
472
|
+
# ============================================================================
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class TestUnifiedISAAnalyzerCLI:
|
|
476
|
+
"""Tests for unified ISA analyzer CLI command."""
|
|
477
|
+
|
|
478
|
+
def test_cli_wafer_amd_isa_analyze_help(self) -> None:
|
|
479
|
+
"""wafer amd isa analyze --help should show help."""
|
|
480
|
+
result = runner.invoke(app, ["amd", "isa", "analyze", "--help"])
|
|
481
|
+
|
|
482
|
+
assert result.exit_code == 0
|
|
483
|
+
assert "AMD GPU ISA" in result.output or "analyze" in result.output.lower()
|
|
484
|
+
|
|
485
|
+
def test_cli_wafer_amd_isa_metrics(self) -> None:
|
|
486
|
+
"""wafer amd isa metrics should list available metrics."""
|
|
487
|
+
result = runner.invoke(app, ["amd", "isa", "metrics"])
|
|
488
|
+
|
|
489
|
+
assert result.exit_code == 0
|
|
490
|
+
output = strip_ansi(result.output)
|
|
491
|
+
assert "vgpr_count" in output
|
|
492
|
+
assert "spill_count" in output
|
|
493
|
+
assert "mfma_count" in output
|
|
494
|
+
|
|
495
|
+
def test_cli_wafer_amd_isa_targets(self) -> None:
|
|
496
|
+
"""wafer amd isa targets should list supported GPU targets."""
|
|
497
|
+
result = runner.invoke(app, ["amd", "isa", "targets"])
|
|
498
|
+
|
|
499
|
+
assert result.exit_code == 0
|
|
500
|
+
output = strip_ansi(result.output)
|
|
501
|
+
assert "gfx90a" in output
|
|
502
|
+
assert "gfx942" in output
|
|
503
|
+
|
|
504
|
+
def test_cli_wafer_amd_isa_analyze_isa_file(self, tmp_path: Path) -> None:
|
|
505
|
+
"""wafer amd isa analyze should analyze .s files."""
|
|
506
|
+
isa_file = tmp_path / "kernel.s"
|
|
507
|
+
isa_file.write_text(SAMPLE_ISA)
|
|
508
|
+
|
|
509
|
+
result = runner.invoke(app, ["amd", "isa", "analyze", str(isa_file)])
|
|
510
|
+
|
|
511
|
+
assert result.exit_code == 0
|
|
512
|
+
output = strip_ansi(result.output)
|
|
513
|
+
assert "test_kernel" in output
|
|
514
|
+
assert "gfx90a" in output
|
|
515
|
+
|
|
516
|
+
def test_cli_wafer_amd_isa_analyze_json_output(self, tmp_path: Path) -> None:
|
|
517
|
+
"""wafer amd isa analyze --json should output JSON."""
|
|
518
|
+
isa_file = tmp_path / "kernel.s"
|
|
519
|
+
isa_file.write_text(SAMPLE_ISA)
|
|
520
|
+
|
|
521
|
+
result = runner.invoke(app, ["amd", "isa", "analyze", str(isa_file), "--json"])
|
|
522
|
+
|
|
523
|
+
assert result.exit_code == 0
|
|
524
|
+
data = json.loads(result.output)
|
|
525
|
+
assert data["success"] is True
|
|
526
|
+
assert data["file_type"] == "isa"
|
|
527
|
+
assert data["isa_analysis"]["kernel_name"] == "test_kernel"
|
|
528
|
+
|
|
529
|
+
def test_cli_wafer_amd_isa_analyze_csv_output(self, tmp_path: Path) -> None:
|
|
530
|
+
"""wafer amd isa analyze --csv should output CSV."""
|
|
531
|
+
isa_file = tmp_path / "kernel.s"
|
|
532
|
+
isa_file.write_text(SAMPLE_ISA)
|
|
533
|
+
|
|
534
|
+
result = runner.invoke(app, ["amd", "isa", "analyze", str(isa_file), "--csv"])
|
|
535
|
+
|
|
536
|
+
assert result.exit_code == 0
|
|
537
|
+
lines = result.output.strip().split("\n")
|
|
538
|
+
assert len(lines) >= 2 # Header + data
|
|
539
|
+
assert "kernel_name" in lines[0]
|
|
540
|
+
assert "test_kernel" in lines[1]
|
|
541
|
+
|
|
542
|
+
def test_cli_wafer_amd_isa_analyze_co_uses_api(self, tmp_path: Path) -> None:
|
|
543
|
+
"""wafer amd isa analyze on .co should attempt API call."""
|
|
544
|
+
co_file = tmp_path / "kernel.co"
|
|
545
|
+
co_file.write_bytes(b"fake code object")
|
|
546
|
+
|
|
547
|
+
result = runner.invoke(app, ["amd", "isa", "analyze", str(co_file)])
|
|
548
|
+
|
|
549
|
+
# Should exit with error (API call will fail in test environment)
|
|
550
|
+
# The error could be auth-related or API unavailable
|
|
551
|
+
assert result.exit_code != 0
|
|
552
|
+
output = result.output.lower()
|
|
553
|
+
assert "error" in output or "api" in output or "failed" in output
|
|
554
|
+
|
|
555
|
+
def test_cli_wafer_amd_isa_analyze_nonexistent_file(self) -> None:
|
|
556
|
+
"""wafer amd isa analyze should error on nonexistent file."""
|
|
557
|
+
result = runner.invoke(app, ["amd", "isa", "analyze", "/nonexistent/path.s"])
|
|
558
|
+
|
|
559
|
+
assert result.exit_code != 0
|
|
560
|
+
assert "not found" in result.output.lower() or "error" in result.output.lower()
|
|
561
|
+
|
|
562
|
+
def test_cli_wafer_amd_isa_analyze_directory(self, tmp_path: Path) -> None:
|
|
563
|
+
"""wafer amd isa analyze should analyze directories."""
|
|
564
|
+
(tmp_path / "kernel1.s").write_text(SAMPLE_ISA)
|
|
565
|
+
(tmp_path / "kernel2.s").write_text(SAMPLE_ISA_WITH_SPILLS)
|
|
566
|
+
|
|
567
|
+
result = runner.invoke(app, ["amd", "isa", "analyze", str(tmp_path)])
|
|
568
|
+
|
|
569
|
+
assert result.exit_code == 0
|
|
570
|
+
output = strip_ansi(result.output)
|
|
571
|
+
assert "2 files" in output
|
|
572
|
+
assert "Successful" in output or "successful" in output
|
|
573
|
+
|
|
574
|
+
def test_cli_wafer_amd_isa_analyze_with_filter(self, tmp_path: Path) -> None:
|
|
575
|
+
"""wafer amd isa analyze --filter should filter results."""
|
|
576
|
+
(tmp_path / "no_spills.s").write_text(SAMPLE_ISA)
|
|
577
|
+
(tmp_path / "has_spills.s").write_text(SAMPLE_ISA_WITH_SPILLS)
|
|
578
|
+
|
|
579
|
+
result = runner.invoke(
|
|
580
|
+
app,
|
|
581
|
+
["amd", "isa", "analyze", str(tmp_path), "--filter", "spills > 0"]
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
assert result.exit_code == 0
|
|
585
|
+
output = strip_ansi(result.output)
|
|
586
|
+
# Should only show the file with spills
|
|
587
|
+
assert "1 files" in output or "spilling_kernel" in output
|
|
588
|
+
|
|
589
|
+
def test_cli_wafer_amd_isa_analyze_output_to_file(self, tmp_path: Path) -> None:
|
|
590
|
+
"""wafer amd isa analyze --output should write to file."""
|
|
591
|
+
isa_file = tmp_path / "kernel.s"
|
|
592
|
+
isa_file.write_text(SAMPLE_ISA)
|
|
593
|
+
output_file = tmp_path / "output.json"
|
|
594
|
+
|
|
595
|
+
result = runner.invoke(
|
|
596
|
+
app,
|
|
597
|
+
["amd", "isa", "analyze", str(isa_file), "--json", "-o", str(output_file)]
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
assert result.exit_code == 0
|
|
601
|
+
assert output_file.exists()
|
|
602
|
+
data = json.loads(output_file.read_text())
|
|
603
|
+
assert data["success"] is True
|
|
604
|
+
|
|
605
|
+
def test_analyze_command_with_api_params(self, tmp_path: Path) -> None:
|
|
606
|
+
"""analyze_command should accept API params for .co files."""
|
|
607
|
+
co_file = tmp_path / "kernel.co"
|
|
608
|
+
co_file.write_bytes(b"fake code object")
|
|
609
|
+
|
|
610
|
+
# Even with fake API params, should attempt to call API
|
|
611
|
+
# (will fail because .co file doesn't exist at API, but params are passed)
|
|
612
|
+
with pytest.raises(RuntimeError) as exc_info:
|
|
613
|
+
analyze_command(
|
|
614
|
+
str(co_file),
|
|
615
|
+
api_url="https://fake.api.wafer.dev",
|
|
616
|
+
auth_headers={"Authorization": "Bearer fake"}
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
# Should have tried to make API call (not just reject due to missing params)
|
|
620
|
+
assert "API error" in str(exc_info.value) or "Request failed" in str(exc_info.value)
|