11"""Asynchronous SDK smoke tests for AsyncBenchmark operations.
22
33These tests validate the AsyncBenchmark class against the real API.
4- Until AsyncBenchmarkOps is available (PR3), we use the raw async API client
5- to find or create benchmarks for testing .
4+ We create a dedicated smoketest benchmark and scenarios with consistent names
5+ so that resources are reused across test runs (since there's no delete endpoint) .
66"""
77
88from __future__ import annotations
99
10+ from typing import List , Tuple
11+
1012import pytest
1113
12- from runloop_api_client .sdk import AsyncBenchmark , AsyncRunloopSDK , AsyncScenarioRun , AsyncBenchmarkRun
14+ from runloop_api_client import AsyncRunloopSDK
15+ from runloop_api_client .sdk import AsyncScenario , AsyncBenchmark , AsyncScenarioRun , AsyncBenchmarkRun
1316
1417pytestmark = [pytest .mark .smoketest ]
1518
1619TWO_MINUTE_TIMEOUT = 120
1720
18-
19- class TestAsyncBenchmarkRetrieval :
20- """Test AsyncBenchmark retrieval operations."""
21-
22- @pytest .mark .timeout (TWO_MINUTE_TIMEOUT )
23- async def test_benchmark_from_existing (self , async_sdk_client : AsyncRunloopSDK ) -> None :
24- """Test creating AsyncBenchmark from existing benchmark.
25-
26- This test:
27- 1. Lists benchmarks via raw async API
28- 2. Creates an AsyncBenchmark wrapper
29- 3. Validates get_info returns correct data
30- """
31- # List existing benchmarks via raw API
32- benchmarks_page = await async_sdk_client .api .benchmarks .list_public (limit = 1 )
33- benchmarks = benchmarks_page .benchmarks
34-
35- if not benchmarks :
36- raise Exception ("No benchmarks available to test" )
37-
38- benchmark_data = benchmarks [0 ]
39-
40- # Create AsyncBenchmark wrapper
41- # TODO: use AsyncBenchmarkOps to create a benchmark once implemented
42- benchmark = AsyncBenchmark (
43- client = async_sdk_client .api ,
44- benchmark_id = benchmark_data .id ,
45- )
46-
47- assert benchmark .id == benchmark_data .id
48-
49- # Test get_info
50- info = await benchmark .get_info ()
51- assert info .id == benchmark_data .id
52- assert info .name == benchmark_data .name
21+ # Consistent names for smoketest resources
22+ SMOKETEST_BENCHMARK_NAME = "sdk-smoketest-benchmark"
23+ SMOKETEST_SCENARIO_1_NAME = "sdk-smoketest-scenario-1"
24+ SMOKETEST_SCENARIO_2_NAME = "sdk-smoketest-scenario-2"
25+
26+
27+ async def get_or_create_scenario (
28+ async_sdk_client : AsyncRunloopSDK ,
29+ name : str ,
30+ problem_statement : str ,
31+ ) -> AsyncScenario :
32+ """Get an existing scenario by name or create a new one."""
33+ # Check if scenario already exists
34+ scenarios = await async_sdk_client .scenario .list (name = name , limit = 1 )
35+ for scenario in scenarios :
36+ # Return the first matching scenario
37+ return scenario
38+
39+ # Create a new scenario using the SDK builder
40+ return await (
41+ async_sdk_client .scenario .builder (name )
42+ .with_problem_statement (problem_statement )
43+ .add_shell_command_scorer ("pass-scorer" , command = "exit 0" )
44+ .push ()
45+ )
46+
47+
48+ async def get_or_create_benchmark (
49+ async_sdk_client : AsyncRunloopSDK ,
50+ name : str ,
51+ scenario_ids : List [str ],
52+ ) -> AsyncBenchmark :
53+ """Get an existing benchmark by name or create a new one."""
54+ # Check if benchmark already exists
55+ benchmarks_page = await async_sdk_client .api .benchmarks .list (name = name , limit = 1 )
56+ for benchmark in benchmarks_page .benchmarks :
57+ # Return the first matching benchmark
58+ return AsyncBenchmark (async_sdk_client .api , benchmark .id )
59+
60+ # Create a new benchmark
61+ return AsyncBenchmark (
62+ async_sdk_client .api ,
63+ (
64+ await async_sdk_client .api .benchmarks .create (
65+ name = name ,
66+ scenario_ids = scenario_ids ,
67+ description = "Smoketest benchmark for SDK testing" ,
68+ )
69+ ).id ,
70+ )
71+
72+
73+ @pytest .fixture (scope = "module" )
74+ async def smoketest_benchmark (
75+ async_sdk_client : AsyncRunloopSDK ,
76+ ) -> Tuple [AsyncBenchmark , List [str ]]:
77+ """Create or retrieve the smoketest benchmark and scenario IDs."""
78+ # Create or get scenarios
79+ scenario_1 = await get_or_create_scenario (
80+ async_sdk_client ,
81+ SMOKETEST_SCENARIO_1_NAME ,
82+ "Smoketest scenario 1 - basic validation" ,
83+ )
84+ scenario_2 = await get_or_create_scenario (
85+ async_sdk_client ,
86+ SMOKETEST_SCENARIO_2_NAME ,
87+ "Smoketest scenario 2 - basic validation" ,
88+ )
89+
90+ scenario_ids = [scenario_1 .id , scenario_2 .id ]
91+
92+ # Create or get benchmark
93+ benchmark = await get_or_create_benchmark (
94+ async_sdk_client ,
95+ SMOKETEST_BENCHMARK_NAME ,
96+ scenario_ids ,
97+ )
98+
99+ return benchmark , scenario_ids
53100
54101
55102class TestAsyncBenchmarkRun :
56103 """Test AsyncBenchmark run operations."""
57104
58105 @pytest .mark .timeout (TWO_MINUTE_TIMEOUT )
59- async def test_benchmark_run_and_cancel (self , async_sdk_client : AsyncRunloopSDK ) -> None :
106+ async def test_benchmark_run_and_cancel (
107+ self ,
108+ async_sdk_client : AsyncRunloopSDK ,
109+ smoketest_benchmark : Tuple [AsyncBenchmark , List [str ]],
110+ ) -> None :
60111 """Test starting and canceling a benchmark run.
61112
62113 This test:
63- 1. Finds an existing benchmark
114+ 1. Uses the smoketest benchmark fixture
64115 2. Starts a new benchmark run via the AsyncBenchmark class
65116 3. Validates the run object
66117 4. Cancels the run
67118 """
68- # Find an existing benchmark via raw API
69- benchmarks_page = await async_sdk_client .api .benchmarks .list_public (limit = 1 )
70- benchmarks = benchmarks_page .benchmarks
71-
72- if not benchmarks :
73- raise Exception ("No benchmarks available to test" )
74-
75- benchmark_data = benchmarks [0 ]
76- if not benchmark_data .scenario_ids :
77- raise Exception ("No scenarios available to test" )
78-
79- # Create AsyncBenchmark wrapper
80- benchmark = AsyncBenchmark (
81- client = async_sdk_client .api ,
82- benchmark_id = benchmark_data .id ,
83- )
119+ benchmark , scenario_ids = smoketest_benchmark
84120
85121 # Start a run
86122 run = await benchmark .start_run (run_name = "sdk-smoketest-async-benchmark-run" )
87- scenario = async_sdk_client .scenario .from_id (benchmark_data .scenario_ids [0 ])
88- scenario_run = None
123+ scenario_runs : List [AsyncScenarioRun ] = []
89124
90125 try :
91126 assert isinstance (run , AsyncBenchmarkRun )
@@ -95,61 +130,60 @@ async def test_benchmark_run_and_cancel(self, async_sdk_client: AsyncRunloopSDK)
95130 # Get run info
96131 info = await run .get_info ()
97132 assert info .id == run .id
98- assert info .state in ["running" , "completed" , "canceled" ]
99-
100- # Start a scenario run
101- scenario_run = await scenario .run (
102- benchmark_run_id = run .id , run_name = "sdk-smoketest-async-benchmark-run-scenario"
103- )
104- scenario_runs = await run .list_scenario_runs ()
105- assert isinstance (scenario_runs , list )
106- assert len (scenario_runs ) == 1
107- assert isinstance (scenario_runs [0 ], AsyncScenarioRun )
108- assert scenario_runs [0 ].id == scenario_run .id
109- assert scenario_runs [0 ].devbox_id == scenario_run .devbox_id
133+ assert info .state == "running"
134+
135+ # Run the scenarios
136+ for scenario_id in scenario_ids :
137+ scenario = async_sdk_client .scenario .from_id (scenario_id )
138+ scenario_runs .append (
139+ await scenario .run_async (
140+ benchmark_run_id = run .id , run_name = "sdk-smoketest-async-benchmark-run-scenario"
141+ )
142+ )
143+
144+ benchmark_scenario_runs = await run .list_scenario_runs ()
145+ assert isinstance (benchmark_scenario_runs , list )
146+ assert len (benchmark_scenario_runs ) == len (scenario_runs )
147+ for scenario_run in benchmark_scenario_runs :
148+ assert isinstance (scenario_run , AsyncScenarioRun )
149+ assert any (
150+ scenario_run .id == scenario_run .id and scenario_run .devbox_id == scenario_run .devbox_id
151+ for scenario_run in scenario_runs
152+ )
110153
111154 # Cancel the scenario run
112- scenario_result = await scenario_run .cancel ()
113- assert scenario_result .state in ["canceled" , "completed" ]
155+ for scenario_run in scenario_runs :
156+ scenario_result = await scenario_run .cancel ()
157+ assert scenario_result .state in ["canceled" , "completed" ]
114158
115159 # Cancel the benchmark run
116160 result = await run .cancel ()
117161 assert result .state in ["canceled" , "completed" ]
118162
119163 except Exception :
120164 # Ensure cleanup on any error
121- if scenario_run :
122- await async_sdk_client . api . scenarios . runs . cancel (scenario_run . id )
123- await async_sdk_client . api . benchmarks . runs . cancel (run . id )
165+ for scenario_run in scenario_runs :
166+ await scenario_run . cancel ()
167+ await run . cancel ()
124168 raise
125169
126170
127171class TestAsyncBenchmarkListRuns :
128172 """Test AsyncBenchmark list_runs operations."""
129173
130174 @pytest .mark .timeout (TWO_MINUTE_TIMEOUT )
131- async def test_list_runs (self , async_sdk_client : AsyncRunloopSDK ) -> None :
175+ async def test_list_runs (
176+ self ,
177+ smoketest_benchmark : Tuple [AsyncBenchmark , List [str ]],
178+ ) -> None :
132179 """Test listing benchmark runs.
133180
134181 This test:
135- 1. Finds an existing benchmark
182+ 1. Uses the smoketest benchmark fixture
136183 2. Lists its runs
137184 3. Validates returned objects are AsyncBenchmarkRun instances
138185 """
139- # Find an existing benchmark via raw API
140- benchmarks_page = await async_sdk_client .api .benchmarks .list_public (limit = 1 )
141- benchmarks = benchmarks_page .benchmarks
142-
143- if not benchmarks :
144- raise Exception ("No benchmarks available to test" )
145-
146- benchmark_data = benchmarks [0 ]
147-
148- # Create AsyncBenchmark wrapper
149- benchmark = AsyncBenchmark (
150- client = async_sdk_client .api ,
151- benchmark_id = benchmark_data .id ,
152- )
186+ benchmark , _ = smoketest_benchmark
153187
154188 runs = await benchmark .list_runs ()
155189 assert isinstance (runs , list )
0 commit comments