unitysvc-services 0.1.24__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.
- unitysvc_services/__init__.py +4 -0
- unitysvc_services/api.py +421 -0
- unitysvc_services/cli.py +23 -0
- unitysvc_services/format_data.py +140 -0
- unitysvc_services/interactive_prompt.py +1132 -0
- unitysvc_services/list.py +216 -0
- unitysvc_services/models/__init__.py +71 -0
- unitysvc_services/models/base.py +1375 -0
- unitysvc_services/models/listing_data.py +118 -0
- unitysvc_services/models/listing_v1.py +56 -0
- unitysvc_services/models/provider_data.py +79 -0
- unitysvc_services/models/provider_v1.py +54 -0
- unitysvc_services/models/seller_data.py +120 -0
- unitysvc_services/models/seller_v1.py +42 -0
- unitysvc_services/models/service_data.py +114 -0
- unitysvc_services/models/service_v1.py +81 -0
- unitysvc_services/populate.py +207 -0
- unitysvc_services/publisher.py +1628 -0
- unitysvc_services/py.typed +0 -0
- unitysvc_services/query.py +688 -0
- unitysvc_services/scaffold.py +1103 -0
- unitysvc_services/schema/base.json +777 -0
- unitysvc_services/schema/listing_v1.json +1286 -0
- unitysvc_services/schema/provider_v1.json +952 -0
- unitysvc_services/schema/seller_v1.json +379 -0
- unitysvc_services/schema/service_v1.json +1306 -0
- unitysvc_services/test.py +965 -0
- unitysvc_services/unpublisher.py +505 -0
- unitysvc_services/update.py +287 -0
- unitysvc_services/utils.py +533 -0
- unitysvc_services/validator.py +731 -0
- unitysvc_services-0.1.24.dist-info/METADATA +184 -0
- unitysvc_services-0.1.24.dist-info/RECORD +37 -0
- unitysvc_services-0.1.24.dist-info/WHEEL +5 -0
- unitysvc_services-0.1.24.dist-info/entry_points.txt +3 -0
- unitysvc_services-0.1.24.dist-info/licenses/LICENSE +21 -0
- unitysvc_services-0.1.24.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,965 @@
|
|
|
1
|
+
"""Test command group - test code examples with upstream credentials."""
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from .models.base import DocumentCategoryEnum, UpstreamStatusEnum
|
|
15
|
+
from .utils import determine_interpreter, find_files_by_schema, render_template_file
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(help="Test code examples with upstream credentials")
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def extract_service_directory_name(listing_file: Path) -> str | None:
|
|
22
|
+
"""Extract service directory name from listing file path.
|
|
23
|
+
|
|
24
|
+
The service directory is the directory immediately after "services" directory.
|
|
25
|
+
For example: .../services/llama-3-1-405b-instruct/listing-svcreseller.json
|
|
26
|
+
Returns: "llama-3-1-405b-instruct"
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
listing_file: Path to the listing file
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Service directory name or None if not found
|
|
33
|
+
"""
|
|
34
|
+
parts = listing_file.parts
|
|
35
|
+
try:
|
|
36
|
+
services_idx = parts.index("services")
|
|
37
|
+
# Service directory is immediately after "services"
|
|
38
|
+
if services_idx + 1 < len(parts):
|
|
39
|
+
return parts[services_idx + 1]
|
|
40
|
+
except (ValueError, IndexError):
|
|
41
|
+
pass
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def extract_code_examples_from_listing(listing_data: dict[str, Any], listing_file: Path) -> list[dict[str, Any]]:
|
|
46
|
+
"""Extract code example documents from a listing file.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
listing_data: Parsed listing data
|
|
50
|
+
listing_file: Path to the listing file for resolving relative paths
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List of code example documents with resolved file paths
|
|
54
|
+
"""
|
|
55
|
+
code_examples = []
|
|
56
|
+
|
|
57
|
+
# Get service name for display - use directory name as fallback
|
|
58
|
+
service_name = listing_data.get("service_name")
|
|
59
|
+
if not service_name:
|
|
60
|
+
# Use service directory name as fallback
|
|
61
|
+
service_name = extract_service_directory_name(listing_file) or "unknown"
|
|
62
|
+
|
|
63
|
+
# Check user_access_interfaces
|
|
64
|
+
interfaces = listing_data.get("user_access_interfaces", [])
|
|
65
|
+
|
|
66
|
+
for interface in interfaces:
|
|
67
|
+
documents = interface.get("documents", [])
|
|
68
|
+
|
|
69
|
+
for doc in documents:
|
|
70
|
+
# Check if this is a code example document
|
|
71
|
+
category = doc.get("category", "")
|
|
72
|
+
if category == DocumentCategoryEnum.code_example:
|
|
73
|
+
# Resolve file path relative to listing file
|
|
74
|
+
file_path = doc.get("file_path")
|
|
75
|
+
if file_path:
|
|
76
|
+
# Resolve relative path
|
|
77
|
+
absolute_path = (listing_file.parent / file_path).resolve()
|
|
78
|
+
|
|
79
|
+
# Extract meta fields for code examples (expect, requirements, etc.)
|
|
80
|
+
meta = doc.get("meta", {}) or {}
|
|
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
|
+
"interface": interface, # Interface data for templates (base_url, routing_key, etc.)
|
|
90
|
+
"expect": meta.get("expect"), # Expected output substring for validation (from meta)
|
|
91
|
+
"requirements": meta.get("requirements"), # Required packages (from meta)
|
|
92
|
+
}
|
|
93
|
+
code_examples.append(code_example)
|
|
94
|
+
|
|
95
|
+
return code_examples
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def load_related_data(listing_file: Path) -> dict[str, Any]:
|
|
99
|
+
"""Load offering, provider, and seller data related to a listing file.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
listing_file: Path to the listing file
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Dictionary with offering, provider, and seller data (may be empty dicts if not found)
|
|
106
|
+
"""
|
|
107
|
+
result: dict[str, Any] = {
|
|
108
|
+
"offering": {},
|
|
109
|
+
"provider": {},
|
|
110
|
+
"seller": {},
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
# Find offering file (service.json in same directory as listing) using find_files_by_schema
|
|
115
|
+
offering_results = find_files_by_schema(listing_file.parent, "service_v1")
|
|
116
|
+
if offering_results:
|
|
117
|
+
# Unpack tuple: (file_path, format, data)
|
|
118
|
+
# Data is already loaded by find_files_by_schema
|
|
119
|
+
_file_path, _format, offering_data = offering_results[0]
|
|
120
|
+
result["offering"] = offering_data
|
|
121
|
+
else:
|
|
122
|
+
console.print(f"[yellow]Warning: No service_v1 file found in {listing_file.parent}[/yellow]")
|
|
123
|
+
|
|
124
|
+
# Find provider file using find_files_by_schema
|
|
125
|
+
# Structure: data/{provider}/services/{service}/listing.json
|
|
126
|
+
# Go up to provider directory (2 levels up from listing)
|
|
127
|
+
provider_dir = listing_file.parent.parent.parent
|
|
128
|
+
provider_results = find_files_by_schema(provider_dir, "provider_v1")
|
|
129
|
+
if provider_results:
|
|
130
|
+
# Unpack tuple: (file_path, format, data)
|
|
131
|
+
# Data is already loaded by find_files_by_schema
|
|
132
|
+
_file_path, _format, provider_data = provider_results[0]
|
|
133
|
+
result["provider"] = provider_data
|
|
134
|
+
else:
|
|
135
|
+
console.print(f"[yellow]Warning: No provider_v1 file found in {provider_dir}[/yellow]")
|
|
136
|
+
|
|
137
|
+
# Find seller file using find_files_by_schema
|
|
138
|
+
# Go up to data directory (3 levels up from listing)
|
|
139
|
+
data_dir = listing_file.parent.parent.parent.parent
|
|
140
|
+
seller_results = find_files_by_schema(data_dir, "seller_v1")
|
|
141
|
+
if seller_results:
|
|
142
|
+
# Unpack tuple: (file_path, format, data)
|
|
143
|
+
# Data is already loaded by find_files_by_schema
|
|
144
|
+
_file_path, _format, seller_data = seller_results[0]
|
|
145
|
+
result["seller"] = seller_data
|
|
146
|
+
else:
|
|
147
|
+
console.print(f"[yellow]Warning: No seller_v1 file found in {data_dir}[/yellow]")
|
|
148
|
+
|
|
149
|
+
except Exception as e:
|
|
150
|
+
console.print(f"[yellow]Warning: Failed to load related data: {e}[/yellow]")
|
|
151
|
+
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def load_provider_credentials(listing_file: Path) -> dict[str, str] | None:
|
|
156
|
+
"""Load API key and endpoint from service offering file.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
listing_file: Path to the listing file (used to locate the service offering)
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Dictionary with api_key and base_url, or None if not found
|
|
163
|
+
"""
|
|
164
|
+
try:
|
|
165
|
+
# Load related data including the offering
|
|
166
|
+
related_data = load_related_data(listing_file)
|
|
167
|
+
offering = related_data.get("offering", {})
|
|
168
|
+
|
|
169
|
+
if not offering:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
# Extract credentials from upstream_access_interface
|
|
173
|
+
upstream_access = offering.get("upstream_access_interface", {})
|
|
174
|
+
api_key = upstream_access.get("api_key")
|
|
175
|
+
base_url = upstream_access.get("base_url")
|
|
176
|
+
|
|
177
|
+
if api_key and base_url:
|
|
178
|
+
return {
|
|
179
|
+
"api_key": str(api_key),
|
|
180
|
+
"base_url": str(base_url),
|
|
181
|
+
}
|
|
182
|
+
except Exception as e:
|
|
183
|
+
console.print(f"[yellow]Warning: Failed to load service credentials: {e}[/yellow]")
|
|
184
|
+
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def execute_code_example(code_example: dict[str, Any], credentials: dict[str, str]) -> dict[str, Any]:
|
|
189
|
+
"""Execute a code example script with upstream credentials.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
code_example: Code example metadata with file_path and listing_data
|
|
193
|
+
credentials: Dictionary with api_key and base_url
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Result dictionary with success, exit_code, stdout, stderr, rendered_content, file_suffix
|
|
197
|
+
"""
|
|
198
|
+
result: dict[str, Any] = {
|
|
199
|
+
"success": False,
|
|
200
|
+
"exit_code": None,
|
|
201
|
+
"error": None,
|
|
202
|
+
"stdout": None,
|
|
203
|
+
"stderr": None,
|
|
204
|
+
"rendered_content": None,
|
|
205
|
+
"file_suffix": None,
|
|
206
|
+
"listing_file": None,
|
|
207
|
+
"actual_filename": None,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
file_path = code_example.get("file_path")
|
|
211
|
+
if not file_path or not Path(file_path).exists():
|
|
212
|
+
result["error"] = f"File not found: {file_path}"
|
|
213
|
+
return result
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
# Get original file extension
|
|
217
|
+
original_path = Path(file_path)
|
|
218
|
+
|
|
219
|
+
# Load related data for template rendering (if needed)
|
|
220
|
+
listing_data = code_example.get("listing_data", {})
|
|
221
|
+
listing_file = code_example.get("listing_file")
|
|
222
|
+
related_data = {}
|
|
223
|
+
if listing_file:
|
|
224
|
+
related_data = load_related_data(Path(listing_file))
|
|
225
|
+
|
|
226
|
+
# Render template if applicable (handles both .j2 and non-.j2 files)
|
|
227
|
+
try:
|
|
228
|
+
file_content, actual_filename = render_template_file(
|
|
229
|
+
original_path,
|
|
230
|
+
listing=listing_data,
|
|
231
|
+
offering=related_data.get("offering", {}),
|
|
232
|
+
provider=related_data.get("provider", {}),
|
|
233
|
+
seller=related_data.get("seller", {}),
|
|
234
|
+
interface=code_example.get("interface", {}),
|
|
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 (using shared utility function)
|
|
250
|
+
interpreter_cmd, error = determine_interpreter(file_content, file_suffix)
|
|
251
|
+
if error:
|
|
252
|
+
result["error"] = error
|
|
253
|
+
return result
|
|
254
|
+
|
|
255
|
+
# At this point, interpreter_cmd is guaranteed to be a string (error check above)
|
|
256
|
+
assert interpreter_cmd is not None, "interpreter_cmd should not be None after error check"
|
|
257
|
+
|
|
258
|
+
# Prepare environment variables
|
|
259
|
+
env = os.environ.copy()
|
|
260
|
+
env["API_KEY"] = credentials["api_key"]
|
|
261
|
+
env["BASE_URL"] = credentials["base_url"]
|
|
262
|
+
|
|
263
|
+
# Write script to temporary file with original extension
|
|
264
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=file_suffix, delete=False) as temp_file:
|
|
265
|
+
temp_file.write(file_content)
|
|
266
|
+
temp_file_path = temp_file.name
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
# Execute the script (interpreter availability already verified)
|
|
270
|
+
process = subprocess.run(
|
|
271
|
+
[interpreter_cmd, temp_file_path],
|
|
272
|
+
env=env,
|
|
273
|
+
capture_output=True,
|
|
274
|
+
text=True,
|
|
275
|
+
timeout=30,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
result["exit_code"] = process.returncode
|
|
279
|
+
result["stdout"] = process.stdout
|
|
280
|
+
result["stderr"] = process.stderr
|
|
281
|
+
|
|
282
|
+
# Determine if test passed
|
|
283
|
+
# Test passes if: exit_code == 0 AND (expect is None OR expect in stdout)
|
|
284
|
+
expected_output = code_example.get("expect")
|
|
285
|
+
|
|
286
|
+
if process.returncode != 0:
|
|
287
|
+
# Failed: non-zero exit code
|
|
288
|
+
result["success"] = False
|
|
289
|
+
result["error"] = f"Script exited with code {process.returncode}. stderr: {process.stderr[:200]}"
|
|
290
|
+
elif expected_output and expected_output not in process.stdout:
|
|
291
|
+
# Failed: exit code is 0 but expected string not found in output
|
|
292
|
+
result["success"] = False
|
|
293
|
+
result["error"] = (
|
|
294
|
+
f"Output validation failed: expected substring '{expected_output}' "
|
|
295
|
+
f"not found in stdout. stdout: {process.stdout[:200]}"
|
|
296
|
+
)
|
|
297
|
+
else:
|
|
298
|
+
# Passed: exit code is 0 AND (no expect field OR expected string found)
|
|
299
|
+
result["success"] = True
|
|
300
|
+
|
|
301
|
+
finally:
|
|
302
|
+
try:
|
|
303
|
+
os.unlink(temp_file_path)
|
|
304
|
+
except Exception:
|
|
305
|
+
pass
|
|
306
|
+
|
|
307
|
+
except subprocess.TimeoutExpired:
|
|
308
|
+
result["error"] = "Script execution timeout (30 seconds)"
|
|
309
|
+
except Exception as e:
|
|
310
|
+
result["error"] = f"Error executing script: {str(e)}"
|
|
311
|
+
|
|
312
|
+
return result
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def update_offering_override_status(listing_file: Path, status: UpstreamStatusEnum | None) -> None:
|
|
316
|
+
"""Update or remove the status field in the offering override file.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
listing_file: Path to the listing file (offering is in same directory)
|
|
320
|
+
status: Status to set (e.g., UpstreamStatusEnum.deprecated), or None to remove status field
|
|
321
|
+
"""
|
|
322
|
+
import json
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
# Find the offering file (service.json) in the same directory
|
|
326
|
+
offering_results = find_files_by_schema(listing_file.parent, "service_v1")
|
|
327
|
+
if not offering_results:
|
|
328
|
+
console.print(f"[yellow]⚠ No service offering file found in {listing_file.parent}[/yellow]")
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
# Get the base offering file path
|
|
332
|
+
offering_file_path, offering_format, _offering_data = offering_results[0]
|
|
333
|
+
|
|
334
|
+
# Construct override file path
|
|
335
|
+
override_path = offering_file_path.with_stem(f"{offering_file_path.stem}.override")
|
|
336
|
+
|
|
337
|
+
# Load existing override file if it exists
|
|
338
|
+
if override_path.exists():
|
|
339
|
+
try:
|
|
340
|
+
with open(override_path, encoding="utf-8") as f:
|
|
341
|
+
if offering_format == "json":
|
|
342
|
+
override_data = json.load(f)
|
|
343
|
+
else: # toml
|
|
344
|
+
import tomli
|
|
345
|
+
|
|
346
|
+
override_data = tomli.loads(f.read())
|
|
347
|
+
except Exception as e:
|
|
348
|
+
console.print(f"[yellow]⚠ Failed to read override file {override_path}: {e}[/yellow]")
|
|
349
|
+
override_data = {}
|
|
350
|
+
else:
|
|
351
|
+
override_data = {}
|
|
352
|
+
|
|
353
|
+
# Update or remove upstream_status field
|
|
354
|
+
if status is None:
|
|
355
|
+
# Remove upstream_status field if it exists and equals deprecated
|
|
356
|
+
if override_data.get("upstream_status") == UpstreamStatusEnum.deprecated:
|
|
357
|
+
del override_data["upstream_status"]
|
|
358
|
+
console.print(" [dim]→ Removed deprecated upstream_status from override file[/dim]")
|
|
359
|
+
else:
|
|
360
|
+
# Set upstream_status field
|
|
361
|
+
override_data["upstream_status"] = status.value
|
|
362
|
+
console.print(f" [dim]→ Set upstream_status to {status.value} in override file[/dim]")
|
|
363
|
+
|
|
364
|
+
# Write override file (or delete if empty)
|
|
365
|
+
if override_data:
|
|
366
|
+
# Write the override file
|
|
367
|
+
with open(override_path, "w", encoding="utf-8") as f:
|
|
368
|
+
if offering_format == "json":
|
|
369
|
+
json.dump(override_data, f, indent=2)
|
|
370
|
+
f.write("\n") # Add trailing newline
|
|
371
|
+
else: # toml
|
|
372
|
+
import tomli_w
|
|
373
|
+
|
|
374
|
+
f.write(tomli_w.dumps(override_data))
|
|
375
|
+
console.print(f" [dim]→ Updated override file: {override_path}[/dim]")
|
|
376
|
+
else:
|
|
377
|
+
# Delete override file if it's now empty
|
|
378
|
+
if override_path.exists():
|
|
379
|
+
override_path.unlink()
|
|
380
|
+
console.print(f" [dim]→ Removed empty override file: {override_path}[/dim]")
|
|
381
|
+
|
|
382
|
+
except Exception as e:
|
|
383
|
+
console.print(f"[yellow]⚠ Failed to update override file: {e}[/yellow]")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@app.command("list")
|
|
387
|
+
def list_code_examples(
|
|
388
|
+
data_dir: Path | None = typer.Argument(
|
|
389
|
+
None,
|
|
390
|
+
help="Directory containing provider data files (default: current directory)",
|
|
391
|
+
),
|
|
392
|
+
provider_name: str | None = typer.Option(
|
|
393
|
+
None,
|
|
394
|
+
"--provider",
|
|
395
|
+
"-p",
|
|
396
|
+
help="Only list code examples for a specific provider",
|
|
397
|
+
),
|
|
398
|
+
services: str | None = typer.Option(
|
|
399
|
+
None,
|
|
400
|
+
"--services",
|
|
401
|
+
"-s",
|
|
402
|
+
help="Comma-separated list of service patterns (supports wildcards, e.g., 'llama*,gpt-4*')",
|
|
403
|
+
),
|
|
404
|
+
):
|
|
405
|
+
"""List available code examples without running them.
|
|
406
|
+
|
|
407
|
+
This command scans for code examples in listing files and displays them in a table
|
|
408
|
+
with file paths shown relative to the data directory.
|
|
409
|
+
|
|
410
|
+
Useful for exploring available examples before running tests.
|
|
411
|
+
|
|
412
|
+
Examples:
|
|
413
|
+
# List all code examples
|
|
414
|
+
usvc test list
|
|
415
|
+
|
|
416
|
+
# List for specific provider
|
|
417
|
+
usvc test list --provider fireworks
|
|
418
|
+
|
|
419
|
+
# List for specific services
|
|
420
|
+
usvc test list --services "llama*,gpt-4*"
|
|
421
|
+
"""
|
|
422
|
+
# Set data directory
|
|
423
|
+
if data_dir is None:
|
|
424
|
+
data_dir = Path.cwd()
|
|
425
|
+
|
|
426
|
+
if not data_dir.is_absolute():
|
|
427
|
+
data_dir = Path.cwd() / data_dir
|
|
428
|
+
|
|
429
|
+
if not data_dir.exists():
|
|
430
|
+
console.print(
|
|
431
|
+
f"[red]✗[/red] Data directory not found: {data_dir}",
|
|
432
|
+
style="bold red",
|
|
433
|
+
)
|
|
434
|
+
raise typer.Exit(code=1)
|
|
435
|
+
|
|
436
|
+
# Parse service patterns if provided
|
|
437
|
+
service_patterns: list[str] = []
|
|
438
|
+
if services:
|
|
439
|
+
service_patterns = [s.strip() for s in services.split(",") if s.strip()]
|
|
440
|
+
|
|
441
|
+
console.print(f"[blue]Scanning for code examples in:[/blue] {data_dir}\n")
|
|
442
|
+
|
|
443
|
+
# Find all provider files
|
|
444
|
+
provider_results = find_files_by_schema(data_dir, "provider_v1")
|
|
445
|
+
provider_names: set[str] = set()
|
|
446
|
+
|
|
447
|
+
for _provider_file, _format, provider_data in provider_results:
|
|
448
|
+
prov_name = provider_data.get("name", "unknown")
|
|
449
|
+
if not provider_name or prov_name == provider_name:
|
|
450
|
+
provider_names.add(prov_name)
|
|
451
|
+
|
|
452
|
+
if not provider_names:
|
|
453
|
+
console.print("[yellow]No providers found.[/yellow]")
|
|
454
|
+
raise typer.Exit(code=0)
|
|
455
|
+
|
|
456
|
+
# Find all listing files
|
|
457
|
+
listing_results = find_files_by_schema(data_dir, "listing_v1")
|
|
458
|
+
|
|
459
|
+
if not listing_results:
|
|
460
|
+
console.print("[yellow]No listing files found.[/yellow]")
|
|
461
|
+
raise typer.Exit(code=0)
|
|
462
|
+
|
|
463
|
+
# Extract code examples from all listings
|
|
464
|
+
all_code_examples: list[tuple[dict[str, Any], str, str]] = []
|
|
465
|
+
|
|
466
|
+
for listing_file, _format, listing_data in listing_results:
|
|
467
|
+
# Determine provider for this listing
|
|
468
|
+
parts = listing_file.parts
|
|
469
|
+
prov_name = "unknown"
|
|
470
|
+
|
|
471
|
+
try:
|
|
472
|
+
services_idx = parts.index("services")
|
|
473
|
+
if services_idx > 0:
|
|
474
|
+
prov_name = parts[services_idx - 1]
|
|
475
|
+
except (ValueError, IndexError):
|
|
476
|
+
pass
|
|
477
|
+
|
|
478
|
+
# Skip if provider filter is set and doesn't match
|
|
479
|
+
if provider_name and prov_name != provider_name:
|
|
480
|
+
continue
|
|
481
|
+
|
|
482
|
+
# Skip if provider not in our list
|
|
483
|
+
if prov_name not in provider_names:
|
|
484
|
+
continue
|
|
485
|
+
|
|
486
|
+
# Filter by service directory name if patterns are provided
|
|
487
|
+
if service_patterns:
|
|
488
|
+
service_dir = extract_service_directory_name(listing_file)
|
|
489
|
+
if not service_dir:
|
|
490
|
+
continue
|
|
491
|
+
|
|
492
|
+
# Check if service matches any of the patterns
|
|
493
|
+
matches = any(fnmatch.fnmatch(service_dir, pattern) for pattern in service_patterns)
|
|
494
|
+
if not matches:
|
|
495
|
+
continue
|
|
496
|
+
|
|
497
|
+
code_examples = extract_code_examples_from_listing(listing_data, listing_file)
|
|
498
|
+
|
|
499
|
+
for example in code_examples:
|
|
500
|
+
# Get file extension (strip .j2 if present to show actual type)
|
|
501
|
+
file_path = example.get("file_path", "")
|
|
502
|
+
path = Path(file_path)
|
|
503
|
+
# If it's a .j2 template, get the extension before .j2
|
|
504
|
+
if path.suffix == ".j2":
|
|
505
|
+
file_ext = Path(path.stem).suffix or "unknown"
|
|
506
|
+
else:
|
|
507
|
+
file_ext = path.suffix or "unknown"
|
|
508
|
+
all_code_examples.append((example, prov_name, file_ext))
|
|
509
|
+
|
|
510
|
+
if not all_code_examples:
|
|
511
|
+
console.print("[yellow]No code examples found.[/yellow]")
|
|
512
|
+
raise typer.Exit(code=0)
|
|
513
|
+
|
|
514
|
+
# Display results in table
|
|
515
|
+
table = Table(title="Available Code Examples")
|
|
516
|
+
table.add_column("Service", style="cyan")
|
|
517
|
+
table.add_column("Provider", style="blue")
|
|
518
|
+
table.add_column("Title", style="white")
|
|
519
|
+
table.add_column("Type", style="magenta")
|
|
520
|
+
table.add_column("File Path", style="dim")
|
|
521
|
+
|
|
522
|
+
for example, prov_name, file_ext in all_code_examples:
|
|
523
|
+
file_path = example.get("file_path", "N/A")
|
|
524
|
+
|
|
525
|
+
# Show path relative to data directory
|
|
526
|
+
if file_path != "N/A":
|
|
527
|
+
try:
|
|
528
|
+
abs_path = Path(file_path).resolve()
|
|
529
|
+
rel_path = abs_path.relative_to(data_dir.resolve())
|
|
530
|
+
file_path = str(rel_path)
|
|
531
|
+
except ValueError:
|
|
532
|
+
# If relative_to fails, just show the path as-is
|
|
533
|
+
file_path = str(file_path)
|
|
534
|
+
|
|
535
|
+
row = [
|
|
536
|
+
example["service_name"],
|
|
537
|
+
prov_name,
|
|
538
|
+
example["title"],
|
|
539
|
+
file_ext,
|
|
540
|
+
file_path,
|
|
541
|
+
]
|
|
542
|
+
|
|
543
|
+
table.add_row(*row)
|
|
544
|
+
|
|
545
|
+
console.print(table)
|
|
546
|
+
console.print(f"\n[green]Total:[/green] {len(all_code_examples)} code example(s)")
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@app.command()
|
|
550
|
+
def run(
|
|
551
|
+
data_dir: Path | None = typer.Argument(
|
|
552
|
+
None,
|
|
553
|
+
help="Directory containing provider data files (default: current directory)",
|
|
554
|
+
),
|
|
555
|
+
provider_name: str | None = typer.Option(
|
|
556
|
+
None,
|
|
557
|
+
"--provider",
|
|
558
|
+
"-p",
|
|
559
|
+
help="Only test code examples for a specific provider",
|
|
560
|
+
),
|
|
561
|
+
services: str | None = typer.Option(
|
|
562
|
+
None,
|
|
563
|
+
"--services",
|
|
564
|
+
"-s",
|
|
565
|
+
help="Comma-separated list of service patterns (supports wildcards, e.g., 'llama*,gpt-4*')",
|
|
566
|
+
),
|
|
567
|
+
test_file: str | None = typer.Option(
|
|
568
|
+
None,
|
|
569
|
+
"--test-file",
|
|
570
|
+
"-t",
|
|
571
|
+
help="Only run a specific test file by filename (e.g., 'code-example.py.j2')",
|
|
572
|
+
),
|
|
573
|
+
verbose: bool = typer.Option(
|
|
574
|
+
False,
|
|
575
|
+
"--verbose",
|
|
576
|
+
"-v",
|
|
577
|
+
help="Show detailed output including stdout/stderr from scripts",
|
|
578
|
+
),
|
|
579
|
+
force: bool = typer.Option(
|
|
580
|
+
False,
|
|
581
|
+
"--force",
|
|
582
|
+
"-f",
|
|
583
|
+
help="Force rerun all tests, ignoring existing .out and .err files",
|
|
584
|
+
),
|
|
585
|
+
fail_fast: bool = typer.Option(
|
|
586
|
+
False,
|
|
587
|
+
"--fail-fast",
|
|
588
|
+
"-x",
|
|
589
|
+
help="Stop testing on first failure",
|
|
590
|
+
),
|
|
591
|
+
):
|
|
592
|
+
"""Test code examples with upstream API credentials.
|
|
593
|
+
|
|
594
|
+
This command:
|
|
595
|
+
1. Scans for all listing files (schema: listing_v1)
|
|
596
|
+
2. Extracts code example documents
|
|
597
|
+
3. Loads provider credentials from provider.toml
|
|
598
|
+
4. Skips tests that have existing .out and .err files (unless --force is used)
|
|
599
|
+
5. Executes each code example with API_KEY and BASE_URL set to upstream values
|
|
600
|
+
6. Displays test results
|
|
601
|
+
|
|
602
|
+
Examples:
|
|
603
|
+
# Test all code examples
|
|
604
|
+
unitysvc_services test run
|
|
605
|
+
|
|
606
|
+
# Test specific provider
|
|
607
|
+
unitysvc_services test run --provider fireworks
|
|
608
|
+
|
|
609
|
+
# Test specific services (with wildcards)
|
|
610
|
+
unitysvc_services test run --services "llama*,gpt-4*"
|
|
611
|
+
|
|
612
|
+
# Test single service
|
|
613
|
+
unitysvc_services test run --services "llama-3-1-405b-instruct"
|
|
614
|
+
|
|
615
|
+
# Test specific file
|
|
616
|
+
unitysvc_services test run --test-file "code-example.py.j2"
|
|
617
|
+
|
|
618
|
+
# Combine filters
|
|
619
|
+
unitysvc_services test run --provider fireworks --services "llama*"
|
|
620
|
+
|
|
621
|
+
# Show detailed output
|
|
622
|
+
unitysvc_services test run --verbose
|
|
623
|
+
|
|
624
|
+
# Force rerun all tests (ignore existing results)
|
|
625
|
+
unitysvc_services test run --force
|
|
626
|
+
|
|
627
|
+
# Stop on first failure
|
|
628
|
+
unitysvc_services test run --fail-fast
|
|
629
|
+
"""
|
|
630
|
+
# Set data directory
|
|
631
|
+
if data_dir is None:
|
|
632
|
+
data_dir = Path.cwd()
|
|
633
|
+
|
|
634
|
+
if not data_dir.is_absolute():
|
|
635
|
+
data_dir = Path.cwd() / data_dir
|
|
636
|
+
|
|
637
|
+
if not data_dir.exists():
|
|
638
|
+
console.print(
|
|
639
|
+
f"[red]✗[/red] Data directory not found: {data_dir}",
|
|
640
|
+
style="bold red",
|
|
641
|
+
)
|
|
642
|
+
raise typer.Exit(code=1)
|
|
643
|
+
|
|
644
|
+
# Parse service patterns if provided
|
|
645
|
+
service_patterns: list[str] = []
|
|
646
|
+
if services:
|
|
647
|
+
service_patterns = [s.strip() for s in services.split(",") if s.strip()]
|
|
648
|
+
console.print(f"[blue]Service filter patterns:[/blue] {', '.join(service_patterns)}\n")
|
|
649
|
+
|
|
650
|
+
# Display test file filter if provided
|
|
651
|
+
if test_file:
|
|
652
|
+
console.print(f"[blue]Test file filter:[/blue] {test_file}\n")
|
|
653
|
+
|
|
654
|
+
console.print(f"[blue]Scanning for listing files in:[/blue] {data_dir}\n")
|
|
655
|
+
|
|
656
|
+
# Find all listing files
|
|
657
|
+
listing_results = find_files_by_schema(data_dir, "listing_v1")
|
|
658
|
+
|
|
659
|
+
if not listing_results:
|
|
660
|
+
console.print("[yellow]No listing files found.[/yellow]")
|
|
661
|
+
raise typer.Exit(code=0)
|
|
662
|
+
|
|
663
|
+
console.print(f"[cyan]Found {len(listing_results)} listing file(s)[/cyan]\n")
|
|
664
|
+
|
|
665
|
+
# Extract code examples from all listings
|
|
666
|
+
all_code_examples: list[tuple[dict[str, Any], str, dict[str, str]]] = []
|
|
667
|
+
|
|
668
|
+
for listing_file, _format, listing_data in listing_results:
|
|
669
|
+
# Determine provider for this listing
|
|
670
|
+
# Provider is the directory name before "services"
|
|
671
|
+
parts = listing_file.parts
|
|
672
|
+
prov_name = "unknown"
|
|
673
|
+
|
|
674
|
+
try:
|
|
675
|
+
services_idx = parts.index("services")
|
|
676
|
+
if services_idx > 0:
|
|
677
|
+
prov_name = parts[services_idx - 1]
|
|
678
|
+
except (ValueError, IndexError):
|
|
679
|
+
pass
|
|
680
|
+
|
|
681
|
+
# Skip if provider filter is set and doesn't match
|
|
682
|
+
if provider_name and prov_name != provider_name:
|
|
683
|
+
continue
|
|
684
|
+
|
|
685
|
+
# Load credentials from service offering for this listing
|
|
686
|
+
credentials = load_provider_credentials(listing_file)
|
|
687
|
+
if not credentials:
|
|
688
|
+
console.print(f"[yellow]⚠ No credentials found for listing: {listing_file}[/yellow]")
|
|
689
|
+
continue
|
|
690
|
+
|
|
691
|
+
# Filter by service directory name if patterns are provided
|
|
692
|
+
if service_patterns:
|
|
693
|
+
service_dir = extract_service_directory_name(listing_file)
|
|
694
|
+
if not service_dir:
|
|
695
|
+
continue
|
|
696
|
+
|
|
697
|
+
# Check if service matches any of the patterns
|
|
698
|
+
matches = any(fnmatch.fnmatch(service_dir, pattern) for pattern in service_patterns)
|
|
699
|
+
if not matches:
|
|
700
|
+
continue
|
|
701
|
+
|
|
702
|
+
code_examples = extract_code_examples_from_listing(listing_data, listing_file)
|
|
703
|
+
|
|
704
|
+
for example in code_examples:
|
|
705
|
+
# Filter by test file name if provided
|
|
706
|
+
if test_file:
|
|
707
|
+
file_path = example.get("file_path", "")
|
|
708
|
+
# Check if the file path ends with the test file name
|
|
709
|
+
if not file_path.endswith(test_file):
|
|
710
|
+
continue
|
|
711
|
+
|
|
712
|
+
all_code_examples.append((example, prov_name, credentials))
|
|
713
|
+
|
|
714
|
+
if not all_code_examples:
|
|
715
|
+
console.print("[yellow]No code examples found in listings.[/yellow]")
|
|
716
|
+
raise typer.Exit(code=0)
|
|
717
|
+
|
|
718
|
+
console.print(f"[cyan]Found {len(all_code_examples)} code example(s)[/cyan]\n")
|
|
719
|
+
|
|
720
|
+
# Execute each code example
|
|
721
|
+
results = []
|
|
722
|
+
skipped_count = 0
|
|
723
|
+
|
|
724
|
+
# Track test results per service offering (for status updates)
|
|
725
|
+
# Key: offering directory path, Value: {"passed": int, "failed": int, "listing_file": Path}
|
|
726
|
+
offering_test_results: dict[str, dict[str, Any]] = {}
|
|
727
|
+
|
|
728
|
+
for example, prov_name, credentials in all_code_examples:
|
|
729
|
+
service_name = example["service_name"]
|
|
730
|
+
title = example["title"]
|
|
731
|
+
example_listing_file = example.get("listing_file")
|
|
732
|
+
|
|
733
|
+
# Determine actual filename (strip .j2 if it's a template)
|
|
734
|
+
file_path = example.get("file_path")
|
|
735
|
+
if file_path:
|
|
736
|
+
original_path = Path(file_path)
|
|
737
|
+
# If it's a .j2 template, the actual filename is without .j2
|
|
738
|
+
if original_path.suffix == ".j2":
|
|
739
|
+
actual_filename = original_path.stem
|
|
740
|
+
else:
|
|
741
|
+
actual_filename = original_path.name
|
|
742
|
+
else:
|
|
743
|
+
actual_filename = None
|
|
744
|
+
|
|
745
|
+
# Prepare output file paths if we have the necessary information
|
|
746
|
+
out_path = None
|
|
747
|
+
err_path = None
|
|
748
|
+
if example_listing_file and actual_filename:
|
|
749
|
+
listing_path = Path(example_listing_file)
|
|
750
|
+
listing_stem = listing_path.stem
|
|
751
|
+
|
|
752
|
+
# Create filename pattern: {service_name}_{listing_stem}_{actual_filename}.out/.err
|
|
753
|
+
# e.g., "llama-3-1-405b-instruct_svclisting_test.py.out"
|
|
754
|
+
base_filename = f"{service_name}_{listing_stem}_{actual_filename}"
|
|
755
|
+
out_filename = f"{base_filename}.out"
|
|
756
|
+
err_filename = f"{base_filename}.err"
|
|
757
|
+
|
|
758
|
+
# Output paths in the listing directory
|
|
759
|
+
out_path = listing_path.parent / out_filename
|
|
760
|
+
err_path = listing_path.parent / err_filename
|
|
761
|
+
|
|
762
|
+
# Check if test results already exist (skip if not forcing)
|
|
763
|
+
if not force and out_path and err_path and out_path.exists() and err_path.exists():
|
|
764
|
+
console.print(f"[bold]Testing:[/bold] {service_name} - {title}")
|
|
765
|
+
console.print(" [yellow]⊘ Skipped[/yellow] (results already exist)")
|
|
766
|
+
console.print()
|
|
767
|
+
skipped_count += 1
|
|
768
|
+
# Add a skipped result for the summary
|
|
769
|
+
results.append(
|
|
770
|
+
{
|
|
771
|
+
"service_name": service_name,
|
|
772
|
+
"provider": prov_name,
|
|
773
|
+
"title": title,
|
|
774
|
+
"result": {
|
|
775
|
+
"success": True,
|
|
776
|
+
"exit_code": None,
|
|
777
|
+
"skipped": True,
|
|
778
|
+
},
|
|
779
|
+
}
|
|
780
|
+
)
|
|
781
|
+
continue
|
|
782
|
+
|
|
783
|
+
console.print(f"[bold]Testing:[/bold] {service_name} - {title}")
|
|
784
|
+
|
|
785
|
+
result = execute_code_example(example, credentials)
|
|
786
|
+
result["skipped"] = False
|
|
787
|
+
|
|
788
|
+
results.append(
|
|
789
|
+
{
|
|
790
|
+
"service_name": service_name,
|
|
791
|
+
"provider": prov_name,
|
|
792
|
+
"title": title,
|
|
793
|
+
"result": result,
|
|
794
|
+
}
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
if result["success"]:
|
|
798
|
+
console.print(f" [green]✓ Success[/green] (exit code: {result['exit_code']})")
|
|
799
|
+
if verbose and result["stdout"]:
|
|
800
|
+
console.print(f" [dim]stdout:[/dim] {result['stdout'][:200]}")
|
|
801
|
+
|
|
802
|
+
# Save successful test output to .out and .err files (if paths were determined)
|
|
803
|
+
if out_path and err_path:
|
|
804
|
+
# Write stdout to .out file
|
|
805
|
+
try:
|
|
806
|
+
with open(out_path, "w", encoding="utf-8") as f:
|
|
807
|
+
f.write(result["stdout"] or "")
|
|
808
|
+
console.print(f" [dim]→ Output saved to:[/dim] {out_path}")
|
|
809
|
+
except Exception as e:
|
|
810
|
+
console.print(f" [yellow]⚠ Failed to save output: {e}[/yellow]")
|
|
811
|
+
|
|
812
|
+
# Write stderr to .err file
|
|
813
|
+
try:
|
|
814
|
+
with open(err_path, "w", encoding="utf-8") as f:
|
|
815
|
+
f.write(result["stderr"] or "")
|
|
816
|
+
if result["stderr"]:
|
|
817
|
+
console.print(f" [dim]→ Error output saved to:[/dim] {err_path}")
|
|
818
|
+
except Exception as e:
|
|
819
|
+
console.print(f" [yellow]⚠ Failed to save error output: {e}[/yellow]")
|
|
820
|
+
|
|
821
|
+
# Track test result for this offering (don't update status yet)
|
|
822
|
+
if example_listing_file and not test_file:
|
|
823
|
+
offering_dir = str(example_listing_file.parent)
|
|
824
|
+
if offering_dir not in offering_test_results:
|
|
825
|
+
offering_test_results[offering_dir] = {
|
|
826
|
+
"passed": 0,
|
|
827
|
+
"failed": 0,
|
|
828
|
+
"listing_file": example_listing_file,
|
|
829
|
+
}
|
|
830
|
+
offering_test_results[offering_dir]["passed"] += 1
|
|
831
|
+
else:
|
|
832
|
+
console.print(f" [red]✗ Failed[/red] - {result['error']}")
|
|
833
|
+
if verbose:
|
|
834
|
+
if result["stdout"]:
|
|
835
|
+
console.print(f" [dim]stdout:[/dim] {result['stdout'][:200]}")
|
|
836
|
+
if result["stderr"]:
|
|
837
|
+
console.print(f" [dim]stderr:[/dim] {result['stderr'][:200]}")
|
|
838
|
+
|
|
839
|
+
# Write failed test outputs and script to current directory
|
|
840
|
+
# Use actual_filename from result in case template rendering modified it
|
|
841
|
+
if result.get("listing_file") and result.get("actual_filename"):
|
|
842
|
+
result_listing_file = Path(result["listing_file"])
|
|
843
|
+
result_actual_filename = result["actual_filename"]
|
|
844
|
+
result_listing_stem = result_listing_file.stem
|
|
845
|
+
|
|
846
|
+
# Create filename: failed_{service_name}_{listing_stem}_{actual_filename}
|
|
847
|
+
# This will be the base name for .out, .err, and the script file
|
|
848
|
+
failed_filename = f"failed_{service_name}_{result_listing_stem}_{result_actual_filename}"
|
|
849
|
+
|
|
850
|
+
# Write stdout to .out file in current directory
|
|
851
|
+
out_filename = f"{failed_filename}.out"
|
|
852
|
+
try:
|
|
853
|
+
with open(out_filename, "w", encoding="utf-8") as f:
|
|
854
|
+
f.write(result["stdout"] or "")
|
|
855
|
+
console.print(f" [yellow]→ Output saved to:[/yellow] {out_filename}")
|
|
856
|
+
except Exception as e:
|
|
857
|
+
console.print(f" [yellow]⚠ Failed to save output: {e}[/yellow]")
|
|
858
|
+
|
|
859
|
+
# Write stderr to .err file in current directory
|
|
860
|
+
err_filename = f"{failed_filename}.err"
|
|
861
|
+
try:
|
|
862
|
+
with open(err_filename, "w", encoding="utf-8") as f:
|
|
863
|
+
f.write(result["stderr"] or "")
|
|
864
|
+
console.print(f" [yellow]→ Error output saved to:[/yellow] {err_filename}")
|
|
865
|
+
except Exception as e:
|
|
866
|
+
console.print(f" [yellow]⚠ Failed to save error output: {e}[/yellow]")
|
|
867
|
+
|
|
868
|
+
# Write failed test script content to current directory (for debugging)
|
|
869
|
+
# rendered_content is always set if we got here (set during template rendering)
|
|
870
|
+
try:
|
|
871
|
+
with open(failed_filename, "w", encoding="utf-8") as f:
|
|
872
|
+
f.write(result["rendered_content"])
|
|
873
|
+
console.print(f" [yellow]→ Test script saved to:[/yellow] {failed_filename}")
|
|
874
|
+
except Exception as e:
|
|
875
|
+
console.print(f" [yellow]⚠ Failed to save test script: {e}[/yellow]")
|
|
876
|
+
|
|
877
|
+
# Write environment variables to .env file
|
|
878
|
+
env_filename = f"{failed_filename}.env"
|
|
879
|
+
try:
|
|
880
|
+
with open(env_filename, "w", encoding="utf-8") as f:
|
|
881
|
+
f.write(f"API_KEY={credentials['api_key']}\n")
|
|
882
|
+
f.write(f"BASE_URL={credentials['base_url']}\n")
|
|
883
|
+
console.print(f" [yellow]→ Environment variables saved to:[/yellow] {env_filename}")
|
|
884
|
+
console.print(f" [dim] (source this file to reproduce: source {env_filename})[/dim]")
|
|
885
|
+
except Exception as e:
|
|
886
|
+
console.print(f" [yellow]⚠ Failed to save environment file: {e}[/yellow]")
|
|
887
|
+
|
|
888
|
+
# Track test result for this offering (don't update status yet)
|
|
889
|
+
if example_listing_file and not test_file:
|
|
890
|
+
offering_dir = str(example_listing_file.parent)
|
|
891
|
+
if offering_dir not in offering_test_results:
|
|
892
|
+
offering_test_results[offering_dir] = {
|
|
893
|
+
"passed": 0,
|
|
894
|
+
"failed": 0,
|
|
895
|
+
"listing_file": example_listing_file,
|
|
896
|
+
}
|
|
897
|
+
offering_test_results[offering_dir]["failed"] += 1
|
|
898
|
+
|
|
899
|
+
# Stop testing if fail-fast is enabled
|
|
900
|
+
if fail_fast:
|
|
901
|
+
console.print()
|
|
902
|
+
console.print("[yellow]⚠ Stopping tests due to --fail-fast[/yellow]")
|
|
903
|
+
break
|
|
904
|
+
|
|
905
|
+
console.print()
|
|
906
|
+
|
|
907
|
+
# Update offering status based on test results (only if not using --test-file)
|
|
908
|
+
if not test_file and offering_test_results:
|
|
909
|
+
console.print("\n[cyan]Updating service offering status...[/cyan]")
|
|
910
|
+
for _offering_dir, test_stats in offering_test_results.items():
|
|
911
|
+
listing_file = test_stats["listing_file"]
|
|
912
|
+
passed = test_stats["passed"]
|
|
913
|
+
failed = test_stats["failed"]
|
|
914
|
+
|
|
915
|
+
# If any test failed, set to deprecated
|
|
916
|
+
if failed > 0:
|
|
917
|
+
update_offering_override_status(listing_file, UpstreamStatusEnum.deprecated)
|
|
918
|
+
# If all tests passed, remove deprecated status
|
|
919
|
+
elif passed > 0 and failed == 0:
|
|
920
|
+
update_offering_override_status(listing_file, None)
|
|
921
|
+
|
|
922
|
+
# Print summary table
|
|
923
|
+
console.print("\n" + "=" * 70)
|
|
924
|
+
console.print("[bold]Test Results Summary:[/bold]\n")
|
|
925
|
+
|
|
926
|
+
table = Table(title="Code Example Tests")
|
|
927
|
+
table.add_column("Service", style="cyan")
|
|
928
|
+
table.add_column("Provider", style="blue")
|
|
929
|
+
table.add_column("Example", style="white")
|
|
930
|
+
table.add_column("Status", style="green")
|
|
931
|
+
table.add_column("Exit Code", style="white")
|
|
932
|
+
|
|
933
|
+
total_tests = len(results)
|
|
934
|
+
skipped = sum(1 for r in results if r["result"].get("skipped", False))
|
|
935
|
+
passed = sum(1 for r in results if r["result"]["success"] and not r["result"].get("skipped", False))
|
|
936
|
+
failed = total_tests - passed - skipped
|
|
937
|
+
|
|
938
|
+
for test in results:
|
|
939
|
+
result = test["result"]
|
|
940
|
+
if result.get("skipped", False):
|
|
941
|
+
status = "[yellow]⊘ Skipped[/yellow]"
|
|
942
|
+
elif result["success"]:
|
|
943
|
+
status = "[green]✓ Pass[/green]"
|
|
944
|
+
else:
|
|
945
|
+
status = "[red]✗ Fail[/red]"
|
|
946
|
+
|
|
947
|
+
# Use 'is not None' to properly handle exit_code of 0 (success)
|
|
948
|
+
exit_code = str(result["exit_code"]) if result["exit_code"] is not None else "N/A"
|
|
949
|
+
|
|
950
|
+
table.add_row(
|
|
951
|
+
test["service_name"],
|
|
952
|
+
test["provider"],
|
|
953
|
+
test["title"],
|
|
954
|
+
status,
|
|
955
|
+
exit_code,
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
console.print(table)
|
|
959
|
+
console.print(f"\n[green]✓ Passed: {passed}/{total_tests}[/green]")
|
|
960
|
+
if skipped > 0:
|
|
961
|
+
console.print(f"[yellow]⊘ Skipped: {skipped}/{total_tests}[/yellow]")
|
|
962
|
+
console.print(f"[red]✗ Failed: {failed}/{total_tests}[/red]")
|
|
963
|
+
|
|
964
|
+
if failed > 0:
|
|
965
|
+
raise typer.Exit(code=1)
|