jac-scale 0.1.1__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.
- jac_scale/__init__.py +0 -0
- jac_scale/abstractions/config/app_config.jac +30 -0
- jac_scale/abstractions/config/base_config.jac +26 -0
- jac_scale/abstractions/database_provider.jac +51 -0
- jac_scale/abstractions/deployment_target.jac +64 -0
- jac_scale/abstractions/image_registry.jac +54 -0
- jac_scale/abstractions/logger.jac +20 -0
- jac_scale/abstractions/models/deployment_result.jac +27 -0
- jac_scale/abstractions/models/resource_status.jac +38 -0
- jac_scale/config_loader.jac +31 -0
- jac_scale/context.jac +14 -0
- jac_scale/factories/database_factory.jac +43 -0
- jac_scale/factories/deployment_factory.jac +43 -0
- jac_scale/factories/registry_factory.jac +32 -0
- jac_scale/factories/utility_factory.jac +34 -0
- jac_scale/impl/config_loader.impl.jac +131 -0
- jac_scale/impl/context.impl.jac +24 -0
- jac_scale/impl/memory_hierarchy.main.impl.jac +63 -0
- jac_scale/impl/memory_hierarchy.mongo.impl.jac +239 -0
- jac_scale/impl/memory_hierarchy.redis.impl.jac +186 -0
- jac_scale/impl/serve.impl.jac +1785 -0
- jac_scale/jserver/__init__.py +0 -0
- jac_scale/jserver/impl/jfast_api.impl.jac +731 -0
- jac_scale/jserver/impl/jserver.impl.jac +79 -0
- jac_scale/jserver/jfast_api.jac +162 -0
- jac_scale/jserver/jserver.jac +101 -0
- jac_scale/memory_hierarchy.jac +138 -0
- jac_scale/plugin.jac +218 -0
- jac_scale/plugin_config.jac +175 -0
- jac_scale/providers/database/kubernetes_mongo.jac +137 -0
- jac_scale/providers/database/kubernetes_redis.jac +110 -0
- jac_scale/providers/registry/dockerhub.jac +64 -0
- jac_scale/serve.jac +118 -0
- jac_scale/targets/kubernetes/kubernetes_config.jac +215 -0
- jac_scale/targets/kubernetes/kubernetes_target.jac +841 -0
- jac_scale/targets/kubernetes/utils/kubernetes_utils.impl.jac +519 -0
- jac_scale/targets/kubernetes/utils/kubernetes_utils.jac +85 -0
- jac_scale/tests/__init__.py +0 -0
- jac_scale/tests/conftest.py +29 -0
- jac_scale/tests/fixtures/test_api.jac +159 -0
- jac_scale/tests/fixtures/todo_app.jac +68 -0
- jac_scale/tests/test_abstractions.py +88 -0
- jac_scale/tests/test_deploy_k8s.py +265 -0
- jac_scale/tests/test_examples.py +484 -0
- jac_scale/tests/test_factories.py +149 -0
- jac_scale/tests/test_file_upload.py +444 -0
- jac_scale/tests/test_k8s_utils.py +156 -0
- jac_scale/tests/test_memory_hierarchy.py +247 -0
- jac_scale/tests/test_serve.py +1835 -0
- jac_scale/tests/test_sso.py +711 -0
- jac_scale/utilities/loggers/standard_logger.jac +40 -0
- jac_scale/utils.jac +16 -0
- jac_scale-0.1.1.dist-info/METADATA +658 -0
- jac_scale-0.1.1.dist-info/RECORD +57 -0
- jac_scale-0.1.1.dist-info/WHEEL +5 -0
- jac_scale-0.1.1.dist-info/entry_points.txt +3 -0
- jac_scale-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"""Test for running jac-scale examples and testing their APIs."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import gc
|
|
5
|
+
import socket
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
from jaclang.project.config import find_project_root
|
|
14
|
+
|
|
15
|
+
JacClientExamples = (
|
|
16
|
+
Path(__file__).parent.parent.parent.parent
|
|
17
|
+
/ "jac-client"
|
|
18
|
+
/ "jac_client"
|
|
19
|
+
/ "examples"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_free_port() -> int:
|
|
24
|
+
"""Get a free port by binding to port 0 and releasing it."""
|
|
25
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
26
|
+
s.bind(("", 0))
|
|
27
|
+
s.listen(1)
|
|
28
|
+
port = s.getsockname()[1]
|
|
29
|
+
return port
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class JacScaleTestRunner:
|
|
33
|
+
"""Helper class to run jac-scale examples and test their APIs."""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self, example_file: Path, session_name: str = "test", setup_npm: bool = True
|
|
37
|
+
):
|
|
38
|
+
"""Initialize the test runner.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
example_file: Path to the .jac file to serve
|
|
42
|
+
session_name: Name for the session file (default: "test")
|
|
43
|
+
setup_npm: Whether to run npm install and setup src directory (default: True)
|
|
44
|
+
"""
|
|
45
|
+
self.example_file = example_file
|
|
46
|
+
self.port = get_free_port()
|
|
47
|
+
self.base_url = f"http://localhost:{self.port}"
|
|
48
|
+
self.session_file = example_file.parent / f"{session_name}_{self.port}.session"
|
|
49
|
+
self.server_process: subprocess.Popen[str] | None = None
|
|
50
|
+
self.token: str | None = None
|
|
51
|
+
self.root_id: str | None = None
|
|
52
|
+
self.setup_npm = setup_npm
|
|
53
|
+
|
|
54
|
+
def start_server(self, timeout: int = 30) -> None:
|
|
55
|
+
"""Start the jac-scale server.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
timeout: Maximum time to wait for server to start (in seconds)
|
|
59
|
+
"""
|
|
60
|
+
# Find project root (where jac.toml is) using jaclang's find_project_root
|
|
61
|
+
project_root_result = find_project_root(self.example_file.parent)
|
|
62
|
+
if project_root_result:
|
|
63
|
+
example_dir, _ = project_root_result
|
|
64
|
+
else:
|
|
65
|
+
example_dir = self.example_file.parent
|
|
66
|
+
|
|
67
|
+
# Clean up directories before starting (don't clean src - it contains source files)
|
|
68
|
+
dirs_to_clean = ["build", "dist", "node_modules", ".jac"]
|
|
69
|
+
for dir_name in dirs_to_clean:
|
|
70
|
+
dir_path = example_dir / dir_name
|
|
71
|
+
if dir_path.exists():
|
|
72
|
+
subprocess.run(
|
|
73
|
+
["rm", "-rf", dir_name],
|
|
74
|
+
cwd=example_dir,
|
|
75
|
+
check=False,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Setup npm dependencies if needed
|
|
79
|
+
if self.setup_npm:
|
|
80
|
+
print(f"Setting up example directory: {example_dir}")
|
|
81
|
+
|
|
82
|
+
# Run npm install
|
|
83
|
+
npm_install = subprocess.run(
|
|
84
|
+
["jac", "add", "--npm"],
|
|
85
|
+
cwd=example_dir,
|
|
86
|
+
capture_output=True,
|
|
87
|
+
text=True,
|
|
88
|
+
)
|
|
89
|
+
if npm_install.returncode != 0:
|
|
90
|
+
print(f"npm install warning: {npm_install.stderr}")
|
|
91
|
+
|
|
92
|
+
print("Example directory setup complete")
|
|
93
|
+
|
|
94
|
+
# Get the jac executable from the same directory as the current Python interpreter
|
|
95
|
+
import sys
|
|
96
|
+
from pathlib import Path
|
|
97
|
+
|
|
98
|
+
jac_executable = Path(sys.executable).parent / "jac"
|
|
99
|
+
|
|
100
|
+
cmd = [
|
|
101
|
+
str(jac_executable),
|
|
102
|
+
"start",
|
|
103
|
+
str(self.example_file),
|
|
104
|
+
# "--session",
|
|
105
|
+
# str(self.session_file),
|
|
106
|
+
"--port",
|
|
107
|
+
str(self.port),
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
self.server_process = subprocess.Popen(
|
|
111
|
+
cmd,
|
|
112
|
+
stdout=subprocess.PIPE,
|
|
113
|
+
stderr=subprocess.PIPE,
|
|
114
|
+
text=True,
|
|
115
|
+
cwd=example_dir, # Run from example directory
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Wait for server to be ready
|
|
119
|
+
max_attempts = timeout * 5 # Check every 0.2 seconds
|
|
120
|
+
server_ready = False
|
|
121
|
+
|
|
122
|
+
for _ in range(max_attempts):
|
|
123
|
+
# Check if process has died
|
|
124
|
+
if self.server_process.poll() is not None:
|
|
125
|
+
stdout, stderr = self.server_process.communicate()
|
|
126
|
+
raise RuntimeError(
|
|
127
|
+
f"Server process terminated unexpectedly.\n"
|
|
128
|
+
f"STDOUT: {stdout}\nSTDERR: {stderr}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
response = requests.get(f"{self.base_url}/docs", timeout=2)
|
|
133
|
+
if response.status_code in (200, 404):
|
|
134
|
+
print(f"Server started on port {self.port}")
|
|
135
|
+
server_ready = True
|
|
136
|
+
break
|
|
137
|
+
except (requests.ConnectionError, requests.Timeout):
|
|
138
|
+
time.sleep(0.2)
|
|
139
|
+
|
|
140
|
+
if not server_ready:
|
|
141
|
+
stdout, stderr = self.server_process.communicate(timeout=5)
|
|
142
|
+
raise RuntimeError(
|
|
143
|
+
f"Server failed to become ready.\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def stop_server(self) -> None:
|
|
147
|
+
"""Stop the jac-scale server and clean up session files."""
|
|
148
|
+
if self.server_process:
|
|
149
|
+
self.server_process.terminate()
|
|
150
|
+
try:
|
|
151
|
+
self.server_process.wait(timeout=5)
|
|
152
|
+
except subprocess.TimeoutExpired:
|
|
153
|
+
self.server_process.kill()
|
|
154
|
+
self.server_process.wait()
|
|
155
|
+
|
|
156
|
+
# Close the pipes to avoid ResourceWarning
|
|
157
|
+
if self.server_process.stdout:
|
|
158
|
+
self.server_process.stdout.close()
|
|
159
|
+
if self.server_process.stderr:
|
|
160
|
+
self.server_process.stderr.close()
|
|
161
|
+
|
|
162
|
+
# Run garbage collection to clean up lingering socket objects
|
|
163
|
+
gc.collect()
|
|
164
|
+
|
|
165
|
+
# Clean up session files
|
|
166
|
+
if self.session_file.exists():
|
|
167
|
+
session_dir = self.session_file.parent
|
|
168
|
+
prefix = self.session_file.name
|
|
169
|
+
|
|
170
|
+
for file in session_dir.iterdir():
|
|
171
|
+
if file.name.startswith(prefix):
|
|
172
|
+
with contextlib.suppress(Exception):
|
|
173
|
+
file.unlink()
|
|
174
|
+
|
|
175
|
+
# Clean up directories after stopping (don't clean src - it contains source files)
|
|
176
|
+
project_root_result = find_project_root(self.example_file.parent)
|
|
177
|
+
if project_root_result:
|
|
178
|
+
example_dir, _ = project_root_result
|
|
179
|
+
else:
|
|
180
|
+
example_dir = self.example_file.parent
|
|
181
|
+
dirs_to_clean = [
|
|
182
|
+
"build",
|
|
183
|
+
"dist",
|
|
184
|
+
"node_modules",
|
|
185
|
+
".jac",
|
|
186
|
+
"package-lock.json",
|
|
187
|
+
]
|
|
188
|
+
for dir_name in dirs_to_clean:
|
|
189
|
+
dir_path = example_dir / dir_name
|
|
190
|
+
if dir_path.exists():
|
|
191
|
+
subprocess.run(
|
|
192
|
+
["rm", "-rf", dir_name],
|
|
193
|
+
cwd=example_dir,
|
|
194
|
+
check=False,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def create_user(self, username: str, password: str) -> dict[str, Any]:
|
|
198
|
+
"""Create a new user and store the token.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
username: Username for the new user
|
|
202
|
+
password: Password for the new user
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
User creation response
|
|
206
|
+
"""
|
|
207
|
+
result = self.request(
|
|
208
|
+
"POST", "/user/create", data={"username": username, "password": password}
|
|
209
|
+
)
|
|
210
|
+
self.token = result.get("token")
|
|
211
|
+
self.root_id = result.get("root_id")
|
|
212
|
+
return result
|
|
213
|
+
|
|
214
|
+
def login(self, username: str, password: str) -> dict[str, Any]:
|
|
215
|
+
"""Login as an existing user and store the token.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
username: Username
|
|
219
|
+
password: Password
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Login response
|
|
223
|
+
"""
|
|
224
|
+
result = self.request(
|
|
225
|
+
"POST", "/user/login", data={"username": username, "password": password}
|
|
226
|
+
)
|
|
227
|
+
self.token = result.get("token")
|
|
228
|
+
self.root_id = result.get("root_id")
|
|
229
|
+
return result
|
|
230
|
+
|
|
231
|
+
def request(
|
|
232
|
+
self,
|
|
233
|
+
method: str,
|
|
234
|
+
path: str,
|
|
235
|
+
data: dict[str, Any] | None = None,
|
|
236
|
+
use_token: bool = False,
|
|
237
|
+
timeout: int = 5,
|
|
238
|
+
max_retries: int = 60,
|
|
239
|
+
retry_interval: float = 2.0,
|
|
240
|
+
) -> dict[str, Any]:
|
|
241
|
+
"""Make an HTTP request to the server.
|
|
242
|
+
|
|
243
|
+
Retries on 503 Service Unavailable responses.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
method: HTTP method (GET, POST, etc.)
|
|
247
|
+
path: API path (e.g., "/walker/CreateTask")
|
|
248
|
+
data: Request body data
|
|
249
|
+
use_token: Whether to include authentication token
|
|
250
|
+
timeout: Request timeout in seconds
|
|
251
|
+
max_retries: Maximum number of retries for 503 responses
|
|
252
|
+
retry_interval: Time to wait between retries in seconds
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Response JSON data
|
|
256
|
+
"""
|
|
257
|
+
url = f"{self.base_url}{path}"
|
|
258
|
+
headers = {"Content-Type": "application/json"}
|
|
259
|
+
|
|
260
|
+
if use_token and self.token:
|
|
261
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
262
|
+
|
|
263
|
+
response = None
|
|
264
|
+
for attempt in range(max_retries):
|
|
265
|
+
response = requests.request(
|
|
266
|
+
method=method,
|
|
267
|
+
url=url,
|
|
268
|
+
json=data,
|
|
269
|
+
headers=headers,
|
|
270
|
+
timeout=timeout,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if response.status_code == 503:
|
|
274
|
+
print(
|
|
275
|
+
f"[DEBUG] {path} returned 503, retrying ({attempt + 1}/{max_retries})..."
|
|
276
|
+
)
|
|
277
|
+
time.sleep(retry_interval)
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
break
|
|
281
|
+
|
|
282
|
+
assert response is not None, "No response received"
|
|
283
|
+
json_response: Any = response.json()
|
|
284
|
+
|
|
285
|
+
# Handle jac-scale's tuple response format [status, body]
|
|
286
|
+
if isinstance(json_response, list) and len(json_response) == 2:
|
|
287
|
+
json_response = json_response[1]
|
|
288
|
+
|
|
289
|
+
# Handle TransportResponse envelope format
|
|
290
|
+
if (
|
|
291
|
+
isinstance(json_response, dict)
|
|
292
|
+
and "ok" in json_response
|
|
293
|
+
and "data" in json_response
|
|
294
|
+
):
|
|
295
|
+
if json_response.get("ok") and json_response.get("data") is not None:
|
|
296
|
+
# Success case: return the data field
|
|
297
|
+
return json_response["data"]
|
|
298
|
+
elif not json_response.get("ok") and json_response.get("error"):
|
|
299
|
+
# Error case: return error info
|
|
300
|
+
error_info = json_response["error"]
|
|
301
|
+
result: dict[str, Any] = {
|
|
302
|
+
"error": error_info.get("message", "Unknown error")
|
|
303
|
+
}
|
|
304
|
+
if "code" in error_info:
|
|
305
|
+
result["error_code"] = error_info["code"]
|
|
306
|
+
if "details" in error_info:
|
|
307
|
+
result["error_details"] = error_info["details"]
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
# FastAPI validation errors (422) have "detail" field - return as-is
|
|
311
|
+
return json_response # type: ignore[return-value]
|
|
312
|
+
|
|
313
|
+
def request_raw(
|
|
314
|
+
self,
|
|
315
|
+
method: str,
|
|
316
|
+
path: str,
|
|
317
|
+
data: dict[str, Any] | None = None,
|
|
318
|
+
use_token: bool = False,
|
|
319
|
+
timeout: int = 120,
|
|
320
|
+
max_retries: int = 60,
|
|
321
|
+
retry_interval: float = 2.0,
|
|
322
|
+
) -> str:
|
|
323
|
+
"""Make a raw HTTP request to the server.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
method: HTTP method (GET, POST, etc.)
|
|
327
|
+
path: API path (e.g., "/walker/CreateTask")
|
|
328
|
+
data: Request body data
|
|
329
|
+
use_token: Whether to include authentication token
|
|
330
|
+
timeout: Request timeout in seconds
|
|
331
|
+
max_retries: Maximum number of retries for 503 responses and timeouts
|
|
332
|
+
retry_interval: Time to wait between retries in seconds
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Response text
|
|
336
|
+
"""
|
|
337
|
+
url = f"{self.base_url}{path}"
|
|
338
|
+
headers = {"Content-Type": "application/json"}
|
|
339
|
+
|
|
340
|
+
if use_token and self.token:
|
|
341
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
342
|
+
|
|
343
|
+
response = None
|
|
344
|
+
for attempt in range(max_retries):
|
|
345
|
+
try:
|
|
346
|
+
response = requests.request(
|
|
347
|
+
method=method,
|
|
348
|
+
url=url,
|
|
349
|
+
json=data,
|
|
350
|
+
headers=headers,
|
|
351
|
+
timeout=timeout,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
if response.status_code == 503:
|
|
355
|
+
print(
|
|
356
|
+
f"[DEBUG] {path} returned 503, retrying ({attempt + 1}/{max_retries})..."
|
|
357
|
+
)
|
|
358
|
+
time.sleep(retry_interval)
|
|
359
|
+
continue
|
|
360
|
+
|
|
361
|
+
return response.text
|
|
362
|
+
except requests.exceptions.Timeout:
|
|
363
|
+
print(
|
|
364
|
+
f"[DEBUG] {path} timed out, retrying ({attempt + 1}/{max_retries})..."
|
|
365
|
+
)
|
|
366
|
+
time.sleep(retry_interval)
|
|
367
|
+
continue
|
|
368
|
+
|
|
369
|
+
# Return last response text even if it was 503, or error message if all timeouts
|
|
370
|
+
if response is not None:
|
|
371
|
+
return response.text
|
|
372
|
+
return f"Request failed after {max_retries} retries (all timeouts)"
|
|
373
|
+
|
|
374
|
+
def spawn_walker(
|
|
375
|
+
self, walker_name: str, **kwargs: dict[str, Any]
|
|
376
|
+
) -> dict[str, Any]:
|
|
377
|
+
"""Spawn a walker with the given parameters.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
walker_name: Name of the walker to spawn
|
|
381
|
+
**kwargs: Walker field values
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Walker execution response
|
|
385
|
+
"""
|
|
386
|
+
return self.request(
|
|
387
|
+
"POST", f"/walker/{walker_name}", data=kwargs, use_token=True
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
def call_function(
|
|
391
|
+
self, function_name: str, **kwargs: dict[str, Any]
|
|
392
|
+
) -> dict[str, Any]:
|
|
393
|
+
"""Call a function with the given parameters.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
function_name: Name of the function to call
|
|
397
|
+
**kwargs: Function parameter values
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Function result
|
|
401
|
+
"""
|
|
402
|
+
# Build query string from kwargs
|
|
403
|
+
query_params = "&".join(f"{k}={v}" for k, v in kwargs.items())
|
|
404
|
+
path = f"/function/{function_name}"
|
|
405
|
+
if query_params:
|
|
406
|
+
path += f"?{query_params}"
|
|
407
|
+
|
|
408
|
+
return self.request("GET", path, use_token=True)
|
|
409
|
+
|
|
410
|
+
def __enter__(self) -> "JacScaleTestRunner":
|
|
411
|
+
"""Context manager entry."""
|
|
412
|
+
self.start_server()
|
|
413
|
+
return self
|
|
414
|
+
|
|
415
|
+
def __exit__(
|
|
416
|
+
self,
|
|
417
|
+
exc_type: type[BaseException] | None,
|
|
418
|
+
exc_val: BaseException | None,
|
|
419
|
+
exc_tb: object | None,
|
|
420
|
+
) -> None:
|
|
421
|
+
"""Context manager exit."""
|
|
422
|
+
self.stop_server()
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class TestJacClientExamples:
|
|
426
|
+
"""Template for testing custom examples."""
|
|
427
|
+
|
|
428
|
+
def test_all_in_one(self) -> None:
|
|
429
|
+
"""Test a custom example file."""
|
|
430
|
+
# Point to your example file
|
|
431
|
+
example_file = JacClientExamples / "all-in-one" / "main.jac"
|
|
432
|
+
with JacScaleTestRunner(
|
|
433
|
+
example_file, session_name="custom_test", setup_npm=True
|
|
434
|
+
) as runner:
|
|
435
|
+
assert "background-image" in runner.request_raw("GET", "/styles/styles.css")
|
|
436
|
+
assert "PNG" in runner.request_raw("GET", "/static/assets/burger.png")
|
|
437
|
+
assert "/static/client.js" in runner.request_raw("GET", "/cl/app")
|
|
438
|
+
assert (
|
|
439
|
+
runner.request_raw("GET", "/static/client.js")
|
|
440
|
+
!= "Static file not found"
|
|
441
|
+
)
|
|
442
|
+
assert (
|
|
443
|
+
runner.request_raw("GET", "/static/client.jss")
|
|
444
|
+
== "Static file not found"
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
def test_js_styling(self) -> None:
|
|
448
|
+
"""Test JS and styling example file."""
|
|
449
|
+
# Point to your example file
|
|
450
|
+
example_file = JacClientExamples / "css-styling" / "js-styling" / "main.jac"
|
|
451
|
+
with JacScaleTestRunner(
|
|
452
|
+
example_file, session_name="js_styling_test", setup_npm=True
|
|
453
|
+
) as runner:
|
|
454
|
+
assert "const countDisplay" in runner.request_raw("GET", "/styles.js")
|
|
455
|
+
assert "/static/client.js" in runner.request_raw("GET", "/cl/app")
|
|
456
|
+
|
|
457
|
+
def test_material_ui(self) -> None:
|
|
458
|
+
"""Test Material-UI styling example."""
|
|
459
|
+
example_file = JacClientExamples / "css-styling" / "material-ui" / "main.jac"
|
|
460
|
+
with JacScaleTestRunner(
|
|
461
|
+
example_file, session_name="material_ui_test", setup_npm=True
|
|
462
|
+
) as runner:
|
|
463
|
+
assert "/static/client.js" in runner.request_raw("GET", "/cl/app")
|
|
464
|
+
|
|
465
|
+
def test_pure_css(self) -> None:
|
|
466
|
+
"""Test Pure CSS example."""
|
|
467
|
+
example_file = JacClientExamples / "css-styling" / "pure-css" / "main.jac"
|
|
468
|
+
with JacScaleTestRunner(
|
|
469
|
+
example_file, session_name="pure_css_test", setup_npm=True
|
|
470
|
+
) as runner:
|
|
471
|
+
page_content = runner.request_raw("GET", "/cl/app")
|
|
472
|
+
assert "/static/client.js" in page_content
|
|
473
|
+
assert ".container {" in runner.request_raw("GET", "/styles.css")
|
|
474
|
+
|
|
475
|
+
def test_styled_components(self) -> None:
|
|
476
|
+
"""Test Styled Components example."""
|
|
477
|
+
example_file = (
|
|
478
|
+
JacClientExamples / "css-styling" / "styled-components" / "main.jac"
|
|
479
|
+
)
|
|
480
|
+
with JacScaleTestRunner(
|
|
481
|
+
example_file, session_name="styled_components_test", setup_npm=True
|
|
482
|
+
) as runner:
|
|
483
|
+
assert "/static/client.js" in runner.request_raw("GET", "/cl/app")
|
|
484
|
+
assert "import styled from" in runner.request_raw("GET", "/styled.js")
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Tests for the new factory-based architecture."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
# Note: These imports will work once the Jac files are compiled to Python
|
|
8
|
+
# For now, we'll use relative imports that match the structure
|
|
9
|
+
try:
|
|
10
|
+
from ..abstractions.config.app_config import AppConfig
|
|
11
|
+
from ..factories.deployment_factory import DeploymentTargetFactory
|
|
12
|
+
from ..factories.registry_factory import ImageRegistryFactory
|
|
13
|
+
from ..factories.utility_factory import UtilityFactory
|
|
14
|
+
from ..targets.kubernetes.kubernetes_config import KubernetesConfig
|
|
15
|
+
except ImportError:
|
|
16
|
+
# If Jac files aren't compiled yet, skip these tests
|
|
17
|
+
pytest.skip("Jac modules not compiled", allow_module_level=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_deployment_target_factory_creates_kubernetes_target():
|
|
21
|
+
"""Test that factory creates KubernetesTarget for 'kubernetes' type."""
|
|
22
|
+
config = {
|
|
23
|
+
"app_name": "test-app",
|
|
24
|
+
"namespace": "default",
|
|
25
|
+
"container_port": 8000,
|
|
26
|
+
"node_port": 30001,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
target = DeploymentTargetFactory.create("kubernetes", config)
|
|
30
|
+
|
|
31
|
+
assert target is not None
|
|
32
|
+
assert hasattr(target, "deploy")
|
|
33
|
+
assert hasattr(target, "destroy")
|
|
34
|
+
assert hasattr(target, "get_status")
|
|
35
|
+
assert hasattr(target, "scale")
|
|
36
|
+
assert target.k8s_config.app_name == "test-app"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_deployment_target_factory_raises_for_unsupported_target():
|
|
40
|
+
"""Test that factory raises ValueError for unsupported target."""
|
|
41
|
+
config = {"app_name": "test-app"}
|
|
42
|
+
|
|
43
|
+
with pytest.raises(ValueError, match="Unsupported deployment target"):
|
|
44
|
+
DeploymentTargetFactory.create("unsupported", config)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_deployment_target_factory_raises_for_not_implemented_target():
|
|
48
|
+
"""Test that factory raises NotImplementedError for future targets."""
|
|
49
|
+
config = {"app_name": "test-app"}
|
|
50
|
+
|
|
51
|
+
with pytest.raises(NotImplementedError, match="not yet implemented"):
|
|
52
|
+
DeploymentTargetFactory.create("aws", config)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_image_registry_factory_creates_dockerhub():
|
|
56
|
+
"""Test that factory creates DockerHubRegistry for 'dockerhub' type."""
|
|
57
|
+
config = {
|
|
58
|
+
"app_name": "test-app",
|
|
59
|
+
"docker_username": "testuser",
|
|
60
|
+
"docker_password": "testpass",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
registry = ImageRegistryFactory.create("dockerhub", config)
|
|
64
|
+
|
|
65
|
+
assert registry is not None
|
|
66
|
+
assert hasattr(registry, "build_image")
|
|
67
|
+
assert hasattr(registry, "push_image")
|
|
68
|
+
assert hasattr(registry, "get_image_url")
|
|
69
|
+
assert registry.docker_username == "testuser"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_image_registry_factory_raises_for_unsupported_registry():
|
|
73
|
+
"""Test that factory raises ValueError for unsupported registry."""
|
|
74
|
+
config = {"app_name": "test-app"}
|
|
75
|
+
|
|
76
|
+
with pytest.raises(ValueError, match="Unsupported image registry"):
|
|
77
|
+
ImageRegistryFactory.create("unsupported", config)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_utility_factory_creates_standard_logger():
|
|
81
|
+
"""Test that factory creates StandardLogger for 'standard' type."""
|
|
82
|
+
logger = UtilityFactory.create_logger("standard")
|
|
83
|
+
|
|
84
|
+
assert logger is not None
|
|
85
|
+
assert hasattr(logger, "info")
|
|
86
|
+
assert hasattr(logger, "error")
|
|
87
|
+
assert hasattr(logger, "warn")
|
|
88
|
+
assert hasattr(logger, "debug")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_utility_factory_defaults_to_standard_logger():
|
|
92
|
+
"""Test that factory defaults to standard logger when no type specified."""
|
|
93
|
+
logger = UtilityFactory.create_logger()
|
|
94
|
+
|
|
95
|
+
assert logger is not None
|
|
96
|
+
assert hasattr(logger, "info")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_kubernetes_config_from_dict():
|
|
100
|
+
"""Test KubernetesConfig creation from dictionary."""
|
|
101
|
+
config_dict = {
|
|
102
|
+
"app_name": "my-app",
|
|
103
|
+
"namespace": "test-ns",
|
|
104
|
+
"container_port": 9000,
|
|
105
|
+
"node_port": 30002,
|
|
106
|
+
"mongodb_enabled": False,
|
|
107
|
+
"redis_enabled": False,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
config = KubernetesConfig.from_dict(config_dict)
|
|
111
|
+
|
|
112
|
+
assert config.app_name == "my-app"
|
|
113
|
+
assert config.namespace == "test-ns"
|
|
114
|
+
assert config.container_port == 9000
|
|
115
|
+
assert config.node_port == 30002
|
|
116
|
+
assert config.mongodb_enabled is False
|
|
117
|
+
assert config.redis_enabled is False
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_app_config_creation():
|
|
121
|
+
"""Test AppConfig creation."""
|
|
122
|
+
app_config = AppConfig(
|
|
123
|
+
code_folder="/path/to/code",
|
|
124
|
+
file_name="main.jac",
|
|
125
|
+
build=True,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
assert app_config.code_folder == "/path/to/code"
|
|
129
|
+
assert app_config.file_name == "main.jac"
|
|
130
|
+
assert app_config.build is True
|
|
131
|
+
assert app_config.testing is False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@patch("jac_scale.factories.deployment_factory.KubernetesTarget")
|
|
135
|
+
def test_deployment_target_with_logger(mock_kubernetes_target: MagicMock):
|
|
136
|
+
"""Test that deployment target is created with logger."""
|
|
137
|
+
mock_target = MagicMock()
|
|
138
|
+
mock_kubernetes_target.return_value = mock_target
|
|
139
|
+
|
|
140
|
+
config = {"app_name": "test-app", "namespace": "default"}
|
|
141
|
+
logger = UtilityFactory.create_logger("standard")
|
|
142
|
+
|
|
143
|
+
DeploymentTargetFactory.create("kubernetes", config, logger)
|
|
144
|
+
|
|
145
|
+
# Verify logger was passed to target
|
|
146
|
+
mock_kubernetes_target.assert_called_once()
|
|
147
|
+
call_kwargs = mock_kubernetes_target.call_args[1]
|
|
148
|
+
assert "logger" in call_kwargs
|
|
149
|
+
assert call_kwargs["logger"] == logger
|