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