Skip to content

Commit 3a8b6f6

Browse files
author
Datata1
committed
add integration test
1 parent 15f000a commit 3a8b6f6

5 files changed

Lines changed: 309 additions & 6 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import asyncio
2+
3+
from codesphere import CodesphereSDK
4+
from codesphere.resources.workspace import WorkspaceCreate
5+
from codesphere.resources.workspace.landscape import ProfileBuilder
6+
7+
TEAM_ID = 123 # Replace with your actual team ID
8+
9+
10+
async def main():
11+
async with CodesphereSDK() as sdk:
12+
plans = await sdk.metadata.list_plans()
13+
plan_id = next(
14+
(p for p in plans if p.title == "Micro" and not p.deprecated), None
15+
).id
16+
17+
payload = WorkspaceCreate(
18+
plan_id,
19+
team_id=TEAM_ID,
20+
name=f"my-unique-landscape-demo-{int(asyncio.time())}",
21+
)
22+
23+
print("\nCreating workspace...")
24+
workspace = await sdk.workspaces.create(payload)
25+
print(f"Created workspace: {workspace.name} (ID: {workspace.id})")
26+
27+
try:
28+
print("Waiting for workspace to be running...")
29+
await workspace.wait_until_running(timeout=300.0, poll_interval=5.0)
30+
print("Workspace is now running!")
31+
32+
print("\nCreating landscape profile...")
33+
profile = (
34+
ProfileBuilder()
35+
.prepare()
36+
.add_step("npm install", name="Install dependencies")
37+
.done()
38+
.add_reactive_service("web")
39+
.plan(plan_id)
40+
.add_step("npm start")
41+
.add_port(3000, public=True)
42+
.add_path("/", port=3000)
43+
.replicas(1)
44+
.env("NODE_ENV", "production")
45+
.done()
46+
.build()
47+
)
48+
49+
profile_name = "production"
50+
await workspace.landscape.save_profile(profile_name, profile)
51+
print(f"Saved profile: {profile_name}")
52+
53+
profiles = await workspace.landscape.list_profiles()
54+
print(f"Available profiles: {[p.name for p in profiles]}")
55+
56+
yaml_content = await workspace.landscape.get_profile(profile_name)
57+
print(f"\nGenerated profile YAML:\n{yaml_content}")
58+
59+
print("\nDeploying landscape...")
60+
await workspace.landscape.deploy(profile=profile_name)
61+
print("Deployment started!")
62+
63+
finally:
64+
# Cleanup: Delete the workspace
65+
# print("\nCleaning up...")
66+
# await workspace.delete()
67+
# print(f"Deleted workspace: {workspace.name}")
68+
pass
69+
70+
71+
if __name__ == "__main__":
72+
asyncio.run(main())

src/codesphere/resources/workspace/landscape/schemas.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ class ReactiveServiceConfig(CamelModel):
4747
"""Configuration for a reactive (custom) service in the run stage."""
4848

4949
steps: List[Step] = Field(default_factory=list)
50-
env: Optional[Dict[str, str]] = None
51-
plan: Optional[int] = None
50+
plan: int # Required - workspace plan ID
5251
replicas: int = 1
52+
env: Optional[Dict[str, str]] = None
5353
base_image: Optional[str] = None
5454
run_as_user: Optional[int] = Field(default=None, ge=0, le=65534)
5555
run_as_group: Optional[int] = Field(default=None, ge=0, le=65534)
@@ -289,16 +289,25 @@ def build(self) -> tuple[str, ReactiveServiceConfig]:
289289
290290
Returns:
291291
Tuple of (service_name, ReactiveServiceConfig).
292+
293+
Raises:
294+
ValueError: If plan is not set.
292295
"""
296+
if self._plan is None:
297+
raise ValueError(
298+
f"Service '{self._name}' requires a plan ID. "
299+
"Use .plan(plan_id) to set it."
300+
)
301+
293302
network = None
294303
if self._ports or self._paths:
295304
network = NetworkConfig(ports=self._ports, paths=self._paths)
296305

297306
config = ReactiveServiceConfig(
298307
steps=self._steps,
299-
env=self._env if self._env else None,
300308
plan=self._plan,
301309
replicas=self._replicas,
310+
env=self._env if self._env else None,
302311
base_image=self._base_image,
303312
run_as_user=self._run_as_user,
304313
run_as_group=self._run_as_group,

src/codesphere/resources/workspace/schemas.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import asyncio
34
import logging
45
from functools import cached_property
56
from typing import Dict, List, Optional
@@ -96,6 +97,39 @@ async def get_status(self) -> WorkspaceStatus:
9697

9798
return await self._execute_operation(_GET_STATUS_OP)
9899

100+
async def wait_until_running(
101+
self,
102+
*,
103+
timeout: float = 300.0,
104+
poll_interval: float = 5.0,
105+
) -> None:
106+
"""Wait until the workspace is in a running state.
107+
108+
Args:
109+
timeout: Maximum time to wait in seconds (default: 300s / 5 minutes).
110+
poll_interval: Time between status checks in seconds (default: 5s).
111+
112+
Raises:
113+
TimeoutError: If the workspace is not running within the timeout period.
114+
"""
115+
elapsed = 0.0
116+
while elapsed < timeout:
117+
status = await self.get_status()
118+
if status.is_running:
119+
log.debug(f"Workspace {self.id} is now running.")
120+
return
121+
122+
log.debug(
123+
f"Workspace {self.id} not running yet, "
124+
f"waiting {poll_interval}s... (elapsed: {elapsed:.1f}s)"
125+
)
126+
await asyncio.sleep(poll_interval)
127+
elapsed += poll_interval
128+
129+
raise TimeoutError(
130+
f"Workspace {self.id} did not reach running state within {timeout} seconds."
131+
)
132+
99133
async def execute_command(
100134
self, command: str, env: Optional[Dict[str, str]] = None
101135
) -> CommandOutput:

tests/integration/test_landscape.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ async def test_save_profile_with_builder(
185185
self,
186186
sdk_client: CodesphereSDK,
187187
test_workspace: Workspace,
188+
test_plan_id: int,
188189
):
189190
"""save_profile should create a profile file using ProfileBuilder."""
190191
workspace = await sdk_client.workspaces.get(workspace_id=test_workspace.id)
@@ -196,6 +197,7 @@ async def test_save_profile_with_builder(
196197
.add_step("echo 'Installing dependencies'")
197198
.done()
198199
.add_reactive_service("web")
200+
.plan(test_plan_id)
199201
.add_step("echo 'Starting server'")
200202
.add_port(3000, public=True)
201203
.replicas(1)
@@ -280,6 +282,7 @@ async def test_get_profile_returns_yaml_content(
280282
self,
281283
sdk_client: CodesphereSDK,
282284
test_workspace: Workspace,
285+
test_plan_id: int,
283286
):
284287
"""get_profile should return the YAML content of a saved profile."""
285288
workspace = await sdk_client.workspaces.get(workspace_id=test_workspace.id)
@@ -291,6 +294,7 @@ async def test_get_profile_returns_yaml_content(
291294
.add_step("npm install")
292295
.done()
293296
.add_reactive_service("api")
297+
.plan(test_plan_id)
294298
.add_step("npm start")
295299
.add_port(8080)
296300
.env("NODE_ENV", "production")
@@ -357,6 +361,7 @@ async def test_complex_profile_roundtrip(
357361
self,
358362
sdk_client: CodesphereSDK,
359363
test_workspace: Workspace,
364+
test_plan_id: int,
360365
):
361366
"""A complex profile should survive save and retrieve."""
362367
workspace = await sdk_client.workspaces.get(workspace_id=test_workspace.id)
@@ -372,6 +377,7 @@ async def test_complex_profile_roundtrip(
372377
.add_step("npm test")
373378
.done()
374379
.add_reactive_service("frontend")
380+
.plan(test_plan_id)
375381
.add_step("npm run serve")
376382
.add_port(3000, public=True)
377383
.add_path("/", port=3000)
@@ -380,6 +386,7 @@ async def test_complex_profile_roundtrip(
380386
.health_endpoint("/health")
381387
.done()
382388
.add_reactive_service("backend")
389+
.plan(test_plan_id)
383390
.add_step("python -m uvicorn main:app")
384391
.add_port(8000)
385392
.add_path("/api", port=8000, strip_path=True)
@@ -412,6 +419,7 @@ async def test_profile_with_special_characters_in_env(
412419
self,
413420
sdk_client: CodesphereSDK,
414421
test_workspace: Workspace,
422+
test_plan_id: int,
415423
):
416424
"""Profile with special characters in env values should work."""
417425
workspace = await sdk_client.workspaces.get(workspace_id=test_workspace.id)
@@ -420,6 +428,7 @@ async def test_profile_with_special_characters_in_env(
420428
profile = (
421429
ProfileBuilder()
422430
.add_reactive_service("app")
431+
.plan(test_plan_id)
423432
.add_step("npm start")
424433
.add_port(3000)
425434
.env("DATABASE_URL", "postgres://user:p@ss=word@localhost:5432/db")
@@ -437,3 +446,148 @@ async def test_profile_with_special_characters_in_env(
437446

438447
finally:
439448
await workspace.landscape.delete_profile(profile_name)
449+
450+
451+
class TestLandscapeDeploymentWorkflow:
452+
"""Integration tests for the complete landscape deployment workflow."""
453+
454+
async def test_full_landscape_workflow_deploy_teardown_delete(
455+
self,
456+
sdk_client: CodesphereSDK,
457+
test_workspace: Workspace,
458+
test_plan_id: int,
459+
):
460+
"""Test the complete workflow: create profile, deploy, teardown, delete profile."""
461+
workspace = await sdk_client.workspaces.get(workspace_id=test_workspace.id)
462+
profile_name = "sdk-workflow-test"
463+
464+
# Step 1: Create a valid profile with ProfileBuilder
465+
profile = (
466+
ProfileBuilder()
467+
.prepare()
468+
.add_step("echo 'Preparing...'", name="Prepare")
469+
.done()
470+
.add_reactive_service("web")
471+
.plan(test_plan_id)
472+
.add_step("echo 'Starting web service' && sleep infinity")
473+
.add_port(3000, public=True)
474+
.add_path("/", port=3000)
475+
.replicas(1)
476+
.env("NODE_ENV", "production")
477+
.done()
478+
.build()
479+
)
480+
481+
try:
482+
# Step 2: Save the profile
483+
await workspace.landscape.save_profile(profile_name, profile)
484+
485+
# Verify profile exists
486+
profiles = await workspace.landscape.list_profiles()
487+
profile_names = [p.name for p in profiles]
488+
assert profile_name in profile_names, "Profile should exist after saving"
489+
490+
# Verify profile content
491+
content = await workspace.landscape.get_profile(profile_name)
492+
assert "schemaVersion: v0.2" in content
493+
assert "web:" in content
494+
assert f"plan: {test_plan_id}" in content
495+
496+
# Step 3: Deploy the landscape
497+
await workspace.landscape.deploy(profile=profile_name)
498+
499+
# Step 4: Teardown the landscape
500+
await workspace.landscape.teardown()
501+
502+
finally:
503+
# Step 5: Delete the profile
504+
await workspace.landscape.delete_profile(profile_name)
505+
506+
# Verify profile is deleted
507+
profiles = await workspace.landscape.list_profiles()
508+
profile_names = [p.name for p in profiles]
509+
assert profile_name not in profile_names, (
510+
"Profile should not exist after deletion"
511+
)
512+
513+
async def test_deploy_and_teardown_only(
514+
self,
515+
sdk_client: CodesphereSDK,
516+
test_workspace: Workspace,
517+
test_plan_id: int,
518+
):
519+
"""Test deploy and teardown without profile deletion."""
520+
workspace = await sdk_client.workspaces.get(workspace_id=test_workspace.id)
521+
profile_name = "sdk-deploy-teardown-test"
522+
523+
profile = (
524+
ProfileBuilder()
525+
.prepare()
526+
.add_step("echo 'Setup complete'")
527+
.done()
528+
.add_reactive_service("api")
529+
.plan(test_plan_id)
530+
.add_step("echo 'API running' && sleep infinity")
531+
.add_port(8080)
532+
.add_path("/api", port=8080)
533+
.replicas(1)
534+
.done()
535+
.build()
536+
)
537+
538+
try:
539+
# Save profile
540+
await workspace.landscape.save_profile(profile_name, profile)
541+
542+
# Deploy
543+
await workspace.landscape.deploy(profile=profile_name)
544+
545+
# Teardown
546+
await workspace.landscape.teardown()
547+
548+
# Profile should still exist after teardown
549+
profiles = await workspace.landscape.list_profiles()
550+
profile_names = [p.name for p in profiles]
551+
assert profile_name in profile_names, (
552+
"Profile should still exist after teardown"
553+
)
554+
555+
finally:
556+
# Cleanup
557+
await workspace.landscape.delete_profile(profile_name)
558+
559+
async def test_profile_deletion_removes_from_list(
560+
self,
561+
sdk_client: CodesphereSDK,
562+
test_workspace: Workspace,
563+
test_plan_id: int,
564+
):
565+
"""Verify that deleting a profile removes it from the profile list."""
566+
workspace = await sdk_client.workspaces.get(workspace_id=test_workspace.id)
567+
profile_name = "sdk-deletion-verify-test"
568+
569+
profile = (
570+
ProfileBuilder()
571+
.add_reactive_service("service")
572+
.plan(test_plan_id)
573+
.add_step("echo 'running'")
574+
.add_port(3000)
575+
.done()
576+
.build()
577+
)
578+
579+
# Create profile
580+
await workspace.landscape.save_profile(profile_name, profile)
581+
582+
# Verify it exists
583+
profiles_before = await workspace.landscape.list_profiles()
584+
assert profile_name in [p.name for p in profiles_before]
585+
586+
# Delete profile
587+
await workspace.landscape.delete_profile(profile_name)
588+
589+
# Verify it's gone
590+
profiles_after = await workspace.landscape.list_profiles()
591+
assert profile_name not in [p.name for p in profiles_after], (
592+
f"Profile '{profile_name}' should be removed after deletion"
593+
)

0 commit comments

Comments
 (0)