unitysvc-services 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl

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.
@@ -0,0 +1,796 @@
1
+ """Test command group - test code examples with upstream credentials."""
2
+
3
+ import fnmatch
4
+ import json
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import tempfile
9
+ import tomllib
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import typer
14
+ from rich.console import Console
15
+ from rich.table import Table
16
+
17
+ from .models.base import DocumentCategoryEnum
18
+ from .utils import find_files_by_schema, render_template_file
19
+
20
+ app = typer.Typer(help="Test code examples with upstream credentials")
21
+ console = Console()
22
+
23
+
24
+ def extract_service_directory_name(listing_file: Path) -> str | None:
25
+ """Extract service directory name from listing file path.
26
+
27
+ The service directory is the directory immediately after "services" directory.
28
+ For example: .../services/llama-3-1-405b-instruct/listing-svcreseller.json
29
+ Returns: "llama-3-1-405b-instruct"
30
+
31
+ Args:
32
+ listing_file: Path to the listing file
33
+
34
+ Returns:
35
+ Service directory name or None if not found
36
+ """
37
+ parts = listing_file.parts
38
+ try:
39
+ services_idx = parts.index("services")
40
+ # Service directory is immediately after "services"
41
+ if services_idx + 1 < len(parts):
42
+ return parts[services_idx + 1]
43
+ except (ValueError, IndexError):
44
+ pass
45
+ return None
46
+
47
+
48
+ def extract_code_examples_from_listing(listing_data: dict[str, Any], listing_file: Path) -> list[dict[str, Any]]:
49
+ """Extract code example documents from a listing file.
50
+
51
+ Args:
52
+ listing_data: Parsed listing data
53
+ listing_file: Path to the listing file for resolving relative paths
54
+
55
+ Returns:
56
+ List of code example documents with resolved file paths
57
+ """
58
+ code_examples = []
59
+
60
+ # Get service name for display - use directory name as fallback
61
+ service_name = listing_data.get("service_name")
62
+ if not service_name:
63
+ # Use service directory name as fallback
64
+ service_name = extract_service_directory_name(listing_file) or "unknown"
65
+
66
+ # Check user_access_interfaces
67
+ interfaces = listing_data.get("user_access_interfaces", [])
68
+
69
+ for interface in interfaces:
70
+ documents = interface.get("documents", [])
71
+
72
+ for doc in documents:
73
+ # Match both "code_example" and "code_examples"
74
+ category = doc.get("category", "")
75
+ if category == DocumentCategoryEnum.code_examples:
76
+ # Resolve file path relative to listing file
77
+ file_path = doc.get("file_path")
78
+ if file_path:
79
+ # Resolve relative path
80
+ absolute_path = (listing_file.parent / file_path).resolve()
81
+
82
+ # Extract meta fields for code examples (expect, requirements, etc.)
83
+ meta = doc.get("meta", {}) or {}
84
+
85
+ code_example = {
86
+ "service_name": service_name,
87
+ "title": doc.get("title", "Untitled"),
88
+ "mime_type": doc.get("mime_type", "python"),
89
+ "file_path": str(absolute_path),
90
+ "listing_data": listing_data, # Full listing data for templates
91
+ "listing_file": listing_file, # Path to listing file for loading related data
92
+ "expect": meta.get("expect"), # Expected output substring for validation (from meta)
93
+ "requirements": meta.get("requirements"), # Required packages (from meta)
94
+ }
95
+ code_examples.append(code_example)
96
+
97
+ return code_examples
98
+
99
+
100
+ def load_related_data(listing_file: Path) -> dict[str, Any]:
101
+ """Load offering, provider, and seller data related to a listing file.
102
+
103
+ Args:
104
+ listing_file: Path to the listing file
105
+
106
+ Returns:
107
+ Dictionary with offering, provider, and seller data (may be empty dicts if not found)
108
+ """
109
+ result: dict[str, Any] = {
110
+ "offering": {},
111
+ "provider": {},
112
+ "seller": {},
113
+ }
114
+
115
+ try:
116
+ # Find offering file (service.json in same directory as listing) using find_files_by_schema
117
+ offering_results = find_files_by_schema(listing_file.parent, "service_v1")
118
+ if offering_results:
119
+ # Unpack tuple: (file_path, format, data)
120
+ # Data is already loaded by find_files_by_schema
121
+ _file_path, _format, offering_data = offering_results[0]
122
+ result["offering"] = offering_data
123
+ else:
124
+ console.print(f"[yellow]Warning: No service_v1 file found in {listing_file.parent}[/yellow]")
125
+
126
+ # Find provider file using find_files_by_schema
127
+ # Structure: data/{provider}/services/{service}/listing.json
128
+ # Go up to provider directory (2 levels up from listing)
129
+ provider_dir = listing_file.parent.parent.parent
130
+ provider_results = find_files_by_schema(provider_dir, "provider_v1")
131
+ if provider_results:
132
+ # Unpack tuple: (file_path, format, data)
133
+ # Data is already loaded by find_files_by_schema
134
+ _file_path, _format, provider_data = provider_results[0]
135
+ result["provider"] = provider_data
136
+ else:
137
+ console.print(f"[yellow]Warning: No provider_v1 file found in {provider_dir}[/yellow]")
138
+
139
+ # Find seller file using find_files_by_schema
140
+ # Go up to data directory (3 levels up from listing)
141
+ data_dir = listing_file.parent.parent.parent.parent
142
+ seller_results = find_files_by_schema(data_dir, "seller_v1")
143
+ if seller_results:
144
+ # Unpack tuple: (file_path, format, data)
145
+ # Data is already loaded by find_files_by_schema
146
+ _file_path, _format, seller_data = seller_results[0]
147
+ result["seller"] = seller_data
148
+ else:
149
+ console.print(f"[yellow]Warning: No seller_v1 file found in {data_dir}[/yellow]")
150
+
151
+ except Exception as e:
152
+ console.print(f"[yellow]Warning: Failed to load related data: {e}[/yellow]")
153
+
154
+ return result
155
+
156
+
157
+ def load_provider_credentials(provider_file: Path) -> dict[str, str] | None:
158
+ """Load API key and endpoint from provider file.
159
+
160
+ Args:
161
+ provider_file: Path to provider.toml or provider.json
162
+
163
+ Returns:
164
+ Dictionary with api_key and api_endpoint, or None if not found
165
+ """
166
+ try:
167
+ if provider_file.suffix == ".toml":
168
+ with open(provider_file, "rb") as f:
169
+ provider_data = tomllib.load(f)
170
+ else:
171
+ with open(provider_file) as f:
172
+ provider_data = json.load(f)
173
+
174
+ access_info = provider_data.get("provider_access_info", {})
175
+ api_key = access_info.get("api_key") or access_info.get("FIREWORKS_API_KEY")
176
+ api_endpoint = access_info.get("api_endpoint") or access_info.get("FIREWORKS_API_BASE_URL")
177
+
178
+ if api_key and api_endpoint:
179
+ return {
180
+ "api_key": str(api_key),
181
+ "api_endpoint": str(api_endpoint),
182
+ }
183
+ except Exception as e:
184
+ console.print(f"[yellow]Warning: Failed to load provider credentials: {e}[/yellow]")
185
+
186
+ return None
187
+
188
+
189
+ def execute_code_example(code_example: dict[str, Any], credentials: dict[str, str]) -> dict[str, Any]:
190
+ """Execute a code example script with upstream credentials.
191
+
192
+ Args:
193
+ code_example: Code example metadata with file_path and listing_data
194
+ credentials: Dictionary with api_key and api_endpoint
195
+
196
+ Returns:
197
+ Result dictionary with success, exit_code, stdout, stderr, rendered_content, file_suffix
198
+ """
199
+ result: dict[str, Any] = {
200
+ "success": False,
201
+ "exit_code": None,
202
+ "error": None,
203
+ "stdout": None,
204
+ "stderr": None,
205
+ "rendered_content": None,
206
+ "file_suffix": None,
207
+ "listing_file": None,
208
+ "actual_filename": None,
209
+ }
210
+
211
+ file_path = code_example.get("file_path")
212
+ if not file_path or not Path(file_path).exists():
213
+ result["error"] = f"File not found: {file_path}"
214
+ return result
215
+
216
+ try:
217
+ # Get original file extension
218
+ original_path = Path(file_path)
219
+
220
+ # Load related data for template rendering (if needed)
221
+ listing_data = code_example.get("listing_data", {})
222
+ listing_file = code_example.get("listing_file")
223
+ related_data = {}
224
+ if listing_file:
225
+ related_data = load_related_data(Path(listing_file))
226
+
227
+ # Render template if applicable (handles both .j2 and non-.j2 files)
228
+ try:
229
+ file_content, actual_filename = render_template_file(
230
+ original_path,
231
+ listing=listing_data,
232
+ offering=related_data.get("offering", {}),
233
+ provider=related_data.get("provider", {}),
234
+ seller=related_data.get("seller", {}),
235
+ )
236
+ except Exception as e:
237
+ result["error"] = f"Template rendering failed: {str(e)}"
238
+ return result
239
+
240
+ # Get file suffix from the actual filename (after .j2 stripping if applicable)
241
+ file_suffix = Path(actual_filename).suffix or ".txt"
242
+
243
+ # Store rendered content and file suffix for later use (e.g., writing failed tests)
244
+ result["rendered_content"] = file_content
245
+ result["file_suffix"] = file_suffix
246
+ result["listing_file"] = listing_file
247
+ result["actual_filename"] = actual_filename
248
+
249
+ # Determine interpreter to use
250
+ lines = file_content.split("\n")
251
+ interpreter_cmd = None
252
+
253
+ # First, try to parse shebang
254
+ if lines and lines[0].startswith("#!"):
255
+ shebang = lines[0][2:].strip()
256
+ if "/env " in shebang:
257
+ # e.g., #!/usr/bin/env python3
258
+ interpreter_cmd = shebang.split("/env ", 1)[1].strip().split()[0]
259
+ else:
260
+ # e.g., #!/usr/bin/python3
261
+ interpreter_cmd = shebang.split("/")[-1].split()[0]
262
+
263
+ # If no shebang found, determine interpreter based on file extension
264
+ if not interpreter_cmd:
265
+ if file_suffix == ".py":
266
+ # Try python3 first, fallback to python
267
+ if shutil.which("python3"):
268
+ interpreter_cmd = "python3"
269
+ elif shutil.which("python"):
270
+ interpreter_cmd = "python"
271
+ else:
272
+ result["error"] = "Neither 'python3' nor 'python' found. Please install Python to run this test."
273
+ return result
274
+ elif file_suffix == ".js":
275
+ # JavaScript files need Node.js
276
+ if shutil.which("node"):
277
+ interpreter_cmd = "node"
278
+ else:
279
+ result["error"] = "'node' not found. Please install Node.js to run JavaScript tests."
280
+ return result
281
+ elif file_suffix == ".sh":
282
+ # Shell scripts use bash
283
+ if shutil.which("bash"):
284
+ interpreter_cmd = "bash"
285
+ else:
286
+ result["error"] = "'bash' not found. Please install bash to run shell script tests."
287
+ return result
288
+ else:
289
+ # Unknown file type - try python3/python as fallback
290
+ if shutil.which("python3"):
291
+ interpreter_cmd = "python3"
292
+ elif shutil.which("python"):
293
+ interpreter_cmd = "python"
294
+ else:
295
+ result["error"] = f"Unknown file type '{file_suffix}' and no Python interpreter found."
296
+ return result
297
+ else:
298
+ # Shebang was found - verify the interpreter exists
299
+ if not shutil.which(interpreter_cmd):
300
+ result["error"] = (
301
+ f"Interpreter '{interpreter_cmd}' from shebang not found. Please install it to run this test."
302
+ )
303
+ return result
304
+
305
+ # Prepare environment variables
306
+ env = os.environ.copy()
307
+ env["API_KEY"] = credentials["api_key"]
308
+ env["API_ENDPOINT"] = credentials["api_endpoint"]
309
+
310
+ # Write script to temporary file with original extension
311
+ with tempfile.NamedTemporaryFile(mode="w", suffix=file_suffix, delete=False) as temp_file:
312
+ temp_file.write(file_content)
313
+ temp_file_path = temp_file.name
314
+
315
+ try:
316
+ # Execute the script (interpreter availability already verified)
317
+ process = subprocess.run(
318
+ [interpreter_cmd, temp_file_path],
319
+ env=env,
320
+ capture_output=True,
321
+ text=True,
322
+ timeout=30,
323
+ )
324
+
325
+ result["exit_code"] = process.returncode
326
+ result["stdout"] = process.stdout
327
+ result["stderr"] = process.stderr
328
+
329
+ # Determine if test passed
330
+ # Test passes if: exit_code == 0 AND (expect is None OR expect in stdout)
331
+ expected_output = code_example.get("expect")
332
+
333
+ if process.returncode != 0:
334
+ # Failed: non-zero exit code
335
+ result["success"] = False
336
+ result["error"] = f"Script exited with code {process.returncode}. stderr: {process.stderr[:200]}"
337
+ elif expected_output and expected_output not in process.stdout:
338
+ # Failed: exit code is 0 but expected string not found in output
339
+ result["success"] = False
340
+ result["error"] = (
341
+ f"Output validation failed: expected substring '{expected_output}' "
342
+ f"not found in stdout. stdout: {process.stdout[:200]}"
343
+ )
344
+ else:
345
+ # Passed: exit code is 0 AND (no expect field OR expected string found)
346
+ result["success"] = True
347
+
348
+ finally:
349
+ try:
350
+ os.unlink(temp_file_path)
351
+ except Exception:
352
+ pass
353
+
354
+ except subprocess.TimeoutExpired:
355
+ result["error"] = "Script execution timeout (30 seconds)"
356
+ except Exception as e:
357
+ result["error"] = f"Error executing script: {str(e)}"
358
+
359
+ return result
360
+
361
+
362
+ @app.command("list")
363
+ def list_code_examples(
364
+ data_dir: Path | None = typer.Argument(
365
+ None,
366
+ help="Directory containing provider data files (default: current directory)",
367
+ ),
368
+ provider_name: str | None = typer.Option(
369
+ None,
370
+ "--provider",
371
+ "-p",
372
+ help="Only list code examples for a specific provider",
373
+ ),
374
+ services: str | None = typer.Option(
375
+ None,
376
+ "--services",
377
+ "-s",
378
+ help="Comma-separated list of service patterns (supports wildcards, e.g., 'llama*,gpt-4*')",
379
+ ),
380
+ ):
381
+ """List available code examples without running them.
382
+
383
+ This command scans for code examples in listing files and displays them in a table
384
+ with file paths shown relative to the data directory.
385
+
386
+ Useful for exploring available examples before running tests.
387
+
388
+ Examples:
389
+ # List all code examples
390
+ usvc test list
391
+
392
+ # List for specific provider
393
+ usvc test list --provider fireworks
394
+
395
+ # List for specific services
396
+ usvc test list --services "llama*,gpt-4*"
397
+ """
398
+ # Set data directory
399
+ if data_dir is None:
400
+ data_dir = Path.cwd()
401
+
402
+ if not data_dir.is_absolute():
403
+ data_dir = Path.cwd() / data_dir
404
+
405
+ if not data_dir.exists():
406
+ console.print(
407
+ f"[red]✗[/red] Data directory not found: {data_dir}",
408
+ style="bold red",
409
+ )
410
+ raise typer.Exit(code=1)
411
+
412
+ # Parse service patterns if provided
413
+ service_patterns: list[str] = []
414
+ if services:
415
+ service_patterns = [s.strip() for s in services.split(",") if s.strip()]
416
+
417
+ console.print(f"[blue]Scanning for code examples in:[/blue] {data_dir}\n")
418
+
419
+ # Find all provider files
420
+ provider_results = find_files_by_schema(data_dir, "provider_v1")
421
+ provider_names: set[str] = set()
422
+
423
+ for _provider_file, _format, provider_data in provider_results:
424
+ prov_name = provider_data.get("name", "unknown")
425
+ if not provider_name or prov_name == provider_name:
426
+ provider_names.add(prov_name)
427
+
428
+ if not provider_names:
429
+ console.print("[yellow]No providers found.[/yellow]")
430
+ raise typer.Exit(code=0)
431
+
432
+ # Find all listing files
433
+ listing_results = find_files_by_schema(data_dir, "listing_v1")
434
+
435
+ if not listing_results:
436
+ console.print("[yellow]No listing files found.[/yellow]")
437
+ raise typer.Exit(code=0)
438
+
439
+ # Extract code examples from all listings
440
+ all_code_examples: list[tuple[dict[str, Any], str, str]] = []
441
+
442
+ for listing_file, _format, listing_data in listing_results:
443
+ # Determine provider for this listing
444
+ parts = listing_file.parts
445
+ prov_name = "unknown"
446
+
447
+ try:
448
+ services_idx = parts.index("services")
449
+ if services_idx > 0:
450
+ prov_name = parts[services_idx - 1]
451
+ except (ValueError, IndexError):
452
+ pass
453
+
454
+ # Skip if provider filter is set and doesn't match
455
+ if provider_name and prov_name != provider_name:
456
+ continue
457
+
458
+ # Skip if provider not in our list
459
+ if prov_name not in provider_names:
460
+ continue
461
+
462
+ # Filter by service directory name if patterns are provided
463
+ if service_patterns:
464
+ service_dir = extract_service_directory_name(listing_file)
465
+ if not service_dir:
466
+ continue
467
+
468
+ # Check if service matches any of the patterns
469
+ matches = any(fnmatch.fnmatch(service_dir, pattern) for pattern in service_patterns)
470
+ if not matches:
471
+ continue
472
+
473
+ code_examples = extract_code_examples_from_listing(listing_data, listing_file)
474
+
475
+ for example in code_examples:
476
+ # Get file extension
477
+ file_path = example.get("file_path", "")
478
+ file_ext = Path(file_path).suffix or "unknown"
479
+ all_code_examples.append((example, prov_name, file_ext))
480
+
481
+ if not all_code_examples:
482
+ console.print("[yellow]No code examples found.[/yellow]")
483
+ raise typer.Exit(code=0)
484
+
485
+ # Display results in table
486
+ table = Table(title="Available Code Examples")
487
+ table.add_column("Service", style="cyan")
488
+ table.add_column("Provider", style="blue")
489
+ table.add_column("Title", style="white")
490
+ table.add_column("Type", style="magenta")
491
+ table.add_column("File Path", style="dim")
492
+
493
+ for example, prov_name, file_ext in all_code_examples:
494
+ file_path = example.get("file_path", "N/A")
495
+
496
+ # Show path relative to data directory
497
+ if file_path != "N/A":
498
+ try:
499
+ abs_path = Path(file_path).resolve()
500
+ rel_path = abs_path.relative_to(data_dir.resolve())
501
+ file_path = str(rel_path)
502
+ except ValueError:
503
+ # If relative_to fails, just show the path as-is
504
+ file_path = str(file_path)
505
+
506
+ row = [
507
+ example["service_name"],
508
+ prov_name,
509
+ example["title"],
510
+ file_ext,
511
+ file_path,
512
+ ]
513
+
514
+ table.add_row(*row)
515
+
516
+ console.print(table)
517
+ console.print(f"\n[green]Total:[/green] {len(all_code_examples)} code example(s)")
518
+
519
+
520
+ @app.command()
521
+ def run(
522
+ data_dir: Path | None = typer.Argument(
523
+ None,
524
+ help="Directory containing provider data files (default: current directory)",
525
+ ),
526
+ provider_name: str | None = typer.Option(
527
+ None,
528
+ "--provider",
529
+ "-p",
530
+ help="Only test code examples for a specific provider",
531
+ ),
532
+ services: str | None = typer.Option(
533
+ None,
534
+ "--services",
535
+ "-s",
536
+ help="Comma-separated list of service patterns (supports wildcards, e.g., 'llama*,gpt-4*')",
537
+ ),
538
+ verbose: bool = typer.Option(
539
+ False,
540
+ "--verbose",
541
+ "-v",
542
+ help="Show detailed output including stdout/stderr from scripts",
543
+ ),
544
+ ):
545
+ """Test code examples with upstream API credentials.
546
+
547
+ This command:
548
+ 1. Scans for all listing files (schema: listing_v1)
549
+ 2. Extracts code example documents
550
+ 3. Loads provider credentials from provider.toml
551
+ 4. Executes each code example with API_KEY and API_ENDPOINT set to upstream values
552
+ 5. Displays test results
553
+
554
+ Examples:
555
+ # Test all code examples
556
+ unitysvc_services test run
557
+
558
+ # Test specific provider
559
+ unitysvc_services test run --provider fireworks
560
+
561
+ # Test specific services (with wildcards)
562
+ unitysvc_services test run --services "llama*,gpt-4*"
563
+
564
+ # Test single service
565
+ unitysvc_services test run --services "llama-3-1-405b-instruct"
566
+
567
+ # Combine filters
568
+ unitysvc_services test run --provider fireworks --services "llama*"
569
+
570
+ # Show detailed output
571
+ unitysvc_services test run --verbose
572
+ """
573
+ # Set data directory
574
+ if data_dir is None:
575
+ data_dir = Path.cwd()
576
+
577
+ if not data_dir.is_absolute():
578
+ data_dir = Path.cwd() / data_dir
579
+
580
+ if not data_dir.exists():
581
+ console.print(
582
+ f"[red]✗[/red] Data directory not found: {data_dir}",
583
+ style="bold red",
584
+ )
585
+ raise typer.Exit(code=1)
586
+
587
+ # Parse service patterns if provided
588
+ service_patterns: list[str] = []
589
+ if services:
590
+ service_patterns = [s.strip() for s in services.split(",") if s.strip()]
591
+ console.print(f"[blue]Service filter patterns:[/blue] {', '.join(service_patterns)}\n")
592
+
593
+ console.print(f"[blue]Scanning for listing files in:[/blue] {data_dir}\n")
594
+
595
+ # Find all provider files first to get credentials
596
+ provider_results = find_files_by_schema(data_dir, "provider_v1")
597
+ provider_credentials: dict[str, dict[str, str]] = {}
598
+
599
+ for provider_file, _format, provider_data in provider_results:
600
+ prov_name = provider_data.get("name", "unknown")
601
+
602
+ # Skip if provider filter is set and doesn't match
603
+ if provider_name and prov_name != provider_name:
604
+ continue
605
+
606
+ credentials = load_provider_credentials(provider_file)
607
+ if credentials:
608
+ provider_credentials[prov_name] = credentials
609
+ console.print(f"[green]✓[/green] Loaded credentials for provider: {prov_name}")
610
+
611
+ if not provider_credentials:
612
+ console.print("[yellow]No provider credentials found.[/yellow]")
613
+ raise typer.Exit(code=0)
614
+
615
+ console.print()
616
+
617
+ # Find all listing files
618
+ listing_results = find_files_by_schema(data_dir, "listing_v1")
619
+
620
+ if not listing_results:
621
+ console.print("[yellow]No listing files found.[/yellow]")
622
+ raise typer.Exit(code=0)
623
+
624
+ console.print(f"[cyan]Found {len(listing_results)} listing file(s)[/cyan]\n")
625
+
626
+ # Extract code examples from all listings
627
+ all_code_examples: list[tuple[dict[str, Any], str]] = []
628
+
629
+ for listing_file, _format, listing_data in listing_results:
630
+ # Determine provider for this listing
631
+ # Provider is the directory name before "services"
632
+ parts = listing_file.parts
633
+ prov_name = "unknown"
634
+
635
+ try:
636
+ services_idx = parts.index("services")
637
+ if services_idx > 0:
638
+ prov_name = parts[services_idx - 1]
639
+ except (ValueError, IndexError):
640
+ pass
641
+
642
+ # Skip if provider filter is set and doesn't match
643
+ if provider_name and prov_name != provider_name:
644
+ continue
645
+
646
+ # Skip if we don't have credentials for this provider
647
+ if prov_name not in provider_credentials:
648
+ continue
649
+
650
+ # Filter by service directory name if patterns are provided
651
+ if service_patterns:
652
+ service_dir = extract_service_directory_name(listing_file)
653
+ if not service_dir:
654
+ continue
655
+
656
+ # Check if service matches any of the patterns
657
+ matches = any(fnmatch.fnmatch(service_dir, pattern) for pattern in service_patterns)
658
+ if not matches:
659
+ continue
660
+
661
+ code_examples = extract_code_examples_from_listing(listing_data, listing_file)
662
+
663
+ for example in code_examples:
664
+ all_code_examples.append((example, prov_name))
665
+
666
+ if not all_code_examples:
667
+ console.print("[yellow]No code examples found in listings.[/yellow]")
668
+ raise typer.Exit(code=0)
669
+
670
+ console.print(f"[cyan]Found {len(all_code_examples)} code example(s)[/cyan]\n")
671
+
672
+ # Execute each code example
673
+ results = []
674
+
675
+ for example, prov_name in all_code_examples:
676
+ service_name = example["service_name"]
677
+ title = example["title"]
678
+
679
+ console.print(f"[bold]Testing:[/bold] {service_name} - {title}")
680
+
681
+ credentials = provider_credentials[prov_name]
682
+ result = execute_code_example(example, credentials)
683
+
684
+ results.append(
685
+ {
686
+ "service_name": service_name,
687
+ "provider": prov_name,
688
+ "title": title,
689
+ "result": result,
690
+ }
691
+ )
692
+
693
+ if result["success"]:
694
+ console.print(f" [green]✓ Success[/green] (exit code: {result['exit_code']})")
695
+ if verbose and result["stdout"]:
696
+ console.print(f" [dim]stdout:[/dim] {result['stdout'][:200]}")
697
+
698
+ # Save successful test output to .out file
699
+ if result.get("stdout") and result.get("listing_file") and result.get("actual_filename"):
700
+ listing_file = Path(result["listing_file"])
701
+ actual_filename = result["actual_filename"]
702
+
703
+ # Create filename: {listing_stem}_{actual_filename}.out
704
+ # e.g., "svclisting_test.py.out" for svclisting.json and test.py
705
+ listing_stem = listing_file.stem
706
+ output_filename = f"{listing_stem}_{actual_filename}.out"
707
+
708
+ # Save to listing directory
709
+ output_path = listing_file.parent / output_filename
710
+
711
+ # Write stdout to .out file
712
+ try:
713
+ with open(output_path, "w", encoding="utf-8") as f:
714
+ f.write(result["stdout"])
715
+ console.print(f" [dim]→ Output saved to:[/dim] {output_path}")
716
+ except Exception as e:
717
+ console.print(f" [yellow]⚠ Failed to save output: {e}[/yellow]")
718
+ else:
719
+ console.print(f" [red]✗ Failed[/red] - {result['error']}")
720
+ if verbose:
721
+ if result["stdout"]:
722
+ console.print(f" [dim]stdout:[/dim] {result['stdout'][:200]}")
723
+ if result["stderr"]:
724
+ console.print(f" [dim]stderr:[/dim] {result['stderr'][:200]}")
725
+
726
+ # Write failed test content to current directory
727
+ if result.get("rendered_content") and result.get("listing_file") and result.get("actual_filename"):
728
+ listing_file = Path(result["listing_file"])
729
+ actual_filename = result["actual_filename"]
730
+ listing_stem = listing_file.stem
731
+
732
+ # Create filename: failed_{listing_stem}_{actual_filename}
733
+ failed_filename = f"failed_{listing_stem}_{actual_filename}"
734
+
735
+ # Prepare content with environment variables as header comments
736
+ content_with_env = result["rendered_content"]
737
+
738
+ # Add environment variables as comments at the top
739
+ env_header = (
740
+ "# Environment variables used for this test:\n"
741
+ f"# API_KEY={credentials['api_key']}\n"
742
+ f"# API_ENDPOINT={credentials['api_endpoint']}\n"
743
+ "#\n"
744
+ "# To reproduce this test, export these variables:\n"
745
+ f"# export API_KEY='{credentials['api_key']}'\n"
746
+ f"# export API_ENDPOINT='{credentials['api_endpoint']}'\n"
747
+ "#\n\n"
748
+ )
749
+
750
+ content_with_env = env_header + content_with_env
751
+
752
+ # Write to current directory
753
+ try:
754
+ with open(failed_filename, "w", encoding="utf-8") as f:
755
+ f.write(content_with_env)
756
+ console.print(f" [yellow]→ Test content saved to:[/yellow] {failed_filename}")
757
+ console.print(" [dim] (includes environment variables for reproduction)[/dim]")
758
+ except Exception as e:
759
+ console.print(f" [yellow]⚠ Failed to save test content: {e}[/yellow]")
760
+
761
+ console.print()
762
+
763
+ # Print summary table
764
+ console.print("\n" + "=" * 70)
765
+ console.print("[bold]Test Results Summary:[/bold]\n")
766
+
767
+ table = Table(title="Code Example Tests")
768
+ table.add_column("Service", style="cyan")
769
+ table.add_column("Provider", style="blue")
770
+ table.add_column("Example", style="white")
771
+ table.add_column("Status", style="green")
772
+ table.add_column("Exit Code", style="white")
773
+
774
+ total_tests = len(results)
775
+ passed = sum(1 for r in results if r["result"]["success"])
776
+ failed = total_tests - passed
777
+
778
+ for test in results:
779
+ status = "[green]✓ Pass[/green]" if test["result"]["success"] else "[red]✗ Fail[/red]"
780
+ # Use 'is not None' to properly handle exit_code of 0 (success)
781
+ exit_code = str(test["result"]["exit_code"]) if test["result"]["exit_code"] is not None else "N/A"
782
+
783
+ table.add_row(
784
+ test["service_name"],
785
+ test["provider"],
786
+ test["title"],
787
+ status,
788
+ exit_code,
789
+ )
790
+
791
+ console.print(table)
792
+ console.print(f"\n[green]✓ Passed: {passed}/{total_tests}[/green]")
793
+ console.print(f"[red]✗ Failed: {failed}/{total_tests}[/red]")
794
+
795
+ if failed > 0:
796
+ raise typer.Exit(code=1)