Skip to content

Commit 41127c7

Browse files
authored
Merge pull request lightspeed-core#970 from tisnik/lcore-1141-better-comments-in-e2e-tests
LCORE-1141: better comments in e2e tests
2 parents 2adb747 + d368216 commit 41127c7

5 files changed

Lines changed: 224 additions & 21 deletions

File tree

tests/e2e/features/environment.py

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,25 @@ def _fetch_models_from_service() -> dict:
5050

5151

5252
def before_all(context: Context) -> None:
53-
"""Run before and after the whole shooting match."""
53+
"""Run before and after the whole shooting match.
54+
55+
Initialize global test environment before the test suite runs.
56+
57+
Sets context.deployment_mode from the E2E_DEPLOYMENT_MODE environment
58+
variable (default "server") and context.is_library_mode accordingly.
59+
60+
Attempts to detect a default LLM model and provider via
61+
_fetch_models_from_service() and stores results in context.default_model
62+
and context.default_provider; if detection fails, falls back to
63+
"gpt-4-turbo" and "openai".
64+
65+
Parameters:
66+
context (Context): Behave context into which this function writes:
67+
- deployment_mode (str): "server" or "library".
68+
- is_library_mode (bool): True when deployment_mode is "library".
69+
- default_model (str): Detected model id or fallback model.
70+
- default_provider (str): Detected provider id or fallback provider.
71+
"""
5472
# Detect deployment mode from environment variable
5573
context.deployment_mode = os.getenv("E2E_DEPLOYMENT_MODE", "server").lower()
5674
context.is_library_mode = context.deployment_mode == "library"
@@ -76,7 +94,18 @@ def before_all(context: Context) -> None:
7694

7795

7896
def before_scenario(context: Context, scenario: Scenario) -> None:
79-
"""Run before each scenario is run."""
97+
"""Run before each scenario is run.
98+
99+
Prepare scenario execution by skipping scenarios based on tags and
100+
selecting a scenario-specific configuration.
101+
102+
Skips the scenario if it has the `skip` tag, if it has the `local` tag
103+
while the test run is not in local mode, or if it has
104+
`skip-in-library-mode` when running in library mode. When the scenario is
105+
tagged with `InvalidFeedbackStorageConfig` or `NoCacheConfig`, sets
106+
`context.scenario_config` to the appropriate configuration file path for
107+
the current deployment mode (library-mode or server-mode).
108+
"""
80109
if "skip" in scenario.effective_tags:
81110
scenario.skip("Marked with @skip")
82111
return
@@ -100,7 +129,31 @@ def before_scenario(context: Context, scenario: Scenario) -> None:
100129

101130

102131
def after_scenario(context: Context, scenario: Scenario) -> None:
103-
"""Run after each scenario is run."""
132+
"""Run after each scenario is run.
133+
134+
Perform per-scenario teardown: restore scenario-specific configuration and,
135+
in server mode, attempt to restart and verify the Llama Stack container if
136+
it was previously running.
137+
138+
If the scenario used an alternate feedback storage or no-cache
139+
configuration, the original feature configuration is restored and the
140+
lightspeed-stack container is restarted. When not running in library mode
141+
and the context indicates the Llama Stack was running before the scenario,
142+
this function attempts to start the llama-stack container and polls its
143+
health endpoint until it becomes healthy or a timeout is reached.
144+
145+
Parameters:
146+
context (Context): Behave test context. Expected attributes used here include:
147+
- feature_config: path to the feature-level configuration to restore.
148+
- is_library_mode (bool): whether tests run in library mode.
149+
- llama_stack_was_running (bool, optional): whether llama-stack was
150+
running before the scenario.
151+
- hostname_llama, port_llama (str/int, optional): host and port
152+
used for the llama-stack health check.
153+
scenario (Scenario): Behave scenario whose tags determine which
154+
scenario-specific teardown actions to run (e.g.,
155+
"InvalidFeedbackStorageConfig", "NoCacheConfig").
156+
"""
104157
if "InvalidFeedbackStorageConfig" in scenario.effective_tags:
105158
switch_config(context.feature_config)
106159
restart_container("lightspeed-stack")
@@ -161,7 +214,10 @@ def after_scenario(context: Context, scenario: Scenario) -> None:
161214

162215

163216
def before_feature(context: Context, feature: Feature) -> None:
164-
"""Run before each feature file is exercised."""
217+
"""Run before each feature file is exercised.
218+
219+
Prepare per-feature test environment and apply feature-specific configuration.
220+
"""
165221
if "Authorized" in feature.tags:
166222
mode_dir = "library-mode" if context.is_library_mode else "server-mode"
167223
context.feature_config = (
@@ -178,7 +234,11 @@ def before_feature(context: Context, feature: Feature) -> None:
178234

179235

180236
def after_feature(context: Context, feature: Feature) -> None:
181-
"""Run after each feature file is exercised."""
237+
"""Run after each feature file is exercised.
238+
239+
Perform feature-level teardown: restore any modified configuration and
240+
clean up feedback conversations.
241+
"""
182242
if "Authorized" in feature.tags:
183243
switch_config(context.default_config_backup)
184244
restart_container("lightspeed-stack")

tests/e2e/features/steps/auth.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88

99
@given("I set the Authorization header to {header_value}")
1010
def set_authorization_header_custom(context: Context, header_value: str) -> None:
11-
"""Set a custom Authorization header value."""
11+
"""Set a custom Authorization header value.
12+
13+
Parameters:
14+
header_value (str): The value to set for the `Authorization` header.
15+
"""
1216
if not hasattr(context, "auth_headers"):
1317
context.auth_headers = {}
1418
context.auth_headers["Authorization"] = header_value
@@ -29,6 +33,10 @@ def access_rest_api_endpoint_post(
2933
"""Send POST HTTP request with payload in the endpoint as parameter to tested service.
3034
3135
The response is stored in `context.response` attribute.
36+
37+
Parameters:
38+
endpoint (str): Endpoint path to call; will be normalized.
39+
user_id (str): Value used for the `user_id` query parameter (surrounding quotes are removed).
3240
"""
3341
endpoint = normalize_endpoint(endpoint)
3442
user_id = user_id.replace('"', "")

tests/e2e/features/steps/common.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@
77

88
@given("The service is started locally")
99
def service_is_started_locally(context: Context) -> None:
10-
"""Check the service status."""
10+
"""Check the service status.
11+
12+
Populate the Behave context with local service endpoint values read from
13+
environment variables.
14+
15+
Parameters:
16+
context (Context): Behave context object to receive the endpoint attributes.
17+
"""
1118
assert context is not None
1219
context.hostname = os.getenv("E2E_LSC_HOSTNAME", "localhost")
1320
context.port = os.getenv("E2E_LSC_PORT", "8080")
@@ -17,5 +24,15 @@ def service_is_started_locally(context: Context) -> None:
1724

1825
@given("The system is in default state")
1926
def system_in_default_state(context: Context) -> None:
20-
"""Check the default system state."""
27+
"""Check the default system state.
28+
29+
Ensure the Behave test context is present for steps that assume the system
30+
is in its default state.
31+
32+
Parameters:
33+
context (Context): Behave Context instance used to store and share test state.
34+
35+
Raises:
36+
AssertionError: If `context` is None.
37+
"""
2138
assert context is not None

tests/e2e/features/steps/health.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,22 @@
88

99
@given("The llama-stack connection is disrupted")
1010
def llama_stack_connection_broken(context: Context) -> None:
11-
"""Break llama_stack connection by stopping the container."""
11+
"""Break llama_stack connection by stopping the container.
12+
13+
Disrupts the Llama Stack service by stopping its Docker container and
14+
records whether it was running.
15+
16+
Checks whether the Docker container named "llama-stack" is running; if it
17+
is, stops the container, waits briefly for the disruption to take effect,
18+
and sets `context.llama_stack_was_running` to True so callers can restore
19+
state later. If the container is not running, the flag remains False. On
20+
failure to run Docker commands, prints a warning message describing the
21+
error.
22+
23+
Parameters:
24+
context (behave.runner.Context): Behave context used to store
25+
`llama_stack_was_running` and share state between steps.
26+
"""
1227
# Store original state for restoration
1328
context.llama_stack_was_running = False
1429

@@ -39,6 +54,12 @@ def llama_stack_connection_broken(context: Context) -> None:
3954

4055
@given("the service is stopped")
4156
def stop_service(context: Context) -> None:
42-
"""Stop service."""
57+
"""Stop service.
58+
59+
Stop a service used by the current test scenario.
60+
61+
Parameters:
62+
context (Context): Behave step context carrying scenario state and configuration.
63+
"""
4364
# TODO: add step implementation
4465
assert context is not None

tests/e2e/utils/utils.py

Lines changed: 108 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,42 @@
1010

1111

1212
def normalize_endpoint(endpoint: str) -> str:
13-
"""Normalize endpoint to be added into the URL."""
13+
"""Normalize endpoint to be added into the URL.
14+
15+
Ensure an endpoint string is suitable for inclusion in a URL.
16+
17+
Removes any double-quote characters and prepends a leading slash if one is
18+
not already present.
19+
20+
Parameters:
21+
endpoint (str): The endpoint string to normalize.
22+
23+
Returns:
24+
str: The normalized endpoint starting with '/' and containing no double-quote characters.
25+
"""
1426
endpoint = endpoint.replace('"', "")
1527
if not endpoint.startswith("/"):
1628
endpoint = "/" + endpoint
1729
return endpoint
1830

1931

2032
def validate_json(message: Any, schema: Any) -> None:
21-
"""Check the JSON message with the given schema."""
33+
"""Check the JSON message with the given schema.
34+
35+
Validate a JSON-like object against a jsonschema-compatible schema.
36+
37+
Parameters:
38+
message (Any): The JSON-like instance to validate (typically a dict or list).
39+
schema (Any): A jsonschema-compatible schema describing the expected structure.
40+
41+
Returns:
42+
None
43+
44+
Raises:
45+
AssertionError: If the instance does not conform to the schema or if
46+
the schema itself is invalid; the assertion message contains the
47+
underlying jsonschema error.
48+
"""
2249
try:
2350
jsonschema.validate(
2451
instance=message,
@@ -33,7 +60,23 @@ def validate_json(message: Any, schema: Any) -> None:
3360

3461

3562
def wait_for_container_health(container_name: str, max_attempts: int = 3) -> None:
36-
"""Wait for container to be healthy."""
63+
"""Wait for container to be healthy.
64+
65+
Polls a Docker container until its health status becomes `healthy` or the
66+
attempt limit is reached.
67+
68+
Checks the container's `Health.Status` using `docker inspect` up to
69+
`max_attempts`, printing progress and final status messages. Transient
70+
inspect errors or timeouts are ignored and retried; the function returns
71+
after the container is observed healthy or after all attempts complete.
72+
73+
Returns:
74+
None
75+
76+
Parameters:
77+
container_name (str): Docker container name or ID to check.
78+
max_attempts (int): Maximum number of health check attempts (default 3).
79+
"""
3780
for attempt in range(max_attempts):
3881
try:
3982
result = subprocess.run(
@@ -71,6 +114,13 @@ def validate_json_partially(actual: Any, expected: Any) -> None:
71114
"""Recursively validate that `actual` JSON contains all keys and values specified in `expected`.
72115
73116
Extra elements/keys are ignored. Raises AssertionError if validation fails.
117+
118+
Returns:
119+
None
120+
121+
Raises:
122+
AssertionError: If a required key is missing, no list element matches
123+
an expected item, or a value does not equal the expected value.
74124
"""
75125
if isinstance(expected, dict):
76126
for key, expected_value in expected.items():
@@ -98,7 +148,24 @@ def validate_json_partially(actual: Any, expected: Any) -> None:
98148
def switch_config(
99149
source_path: str, destination_path: str = "lightspeed-stack.yaml"
100150
) -> None:
101-
"""Overwrite the config in `destination_path` by `source_path`."""
151+
"""Overwrite the config in `destination_path` by `source_path`.
152+
153+
Replace the destination configuration file with the file at source_path.
154+
155+
Parameters:
156+
source_path (str): Path to the replacement configuration file.
157+
destination_path (str): Path to the configuration file to be
158+
overwritten (defaults to "lightspeed-stack.yaml").
159+
160+
Returns:
161+
None
162+
163+
Raises:
164+
FileNotFoundError: If source_path does not exist.
165+
PermissionError: If the file cannot be read or destination cannot be
166+
written due to permissions.
167+
OSError: For other OS-related failures during the copy operation.
168+
"""
102169
try:
103170
shutil.copy(source_path, destination_path)
104171
except (FileNotFoundError, PermissionError, OSError) as e:
@@ -107,7 +174,19 @@ def switch_config(
107174

108175

109176
def create_config_backup(config_path: str) -> str:
110-
"""Create a backup of `config_path` if it does not already exist."""
177+
"""Create a backup of `config_path` if it does not already exist.
178+
179+
Ensure a backup of the given configuration file exists by creating a
180+
`.backup` copy if it is missing.
181+
182+
Returns:
183+
str: Path to the backup file (original path with `.backup` appended).
184+
185+
Raises:
186+
FileNotFoundError: If the source config file does not exist.
187+
PermissionError: If the process lacks permission to read or write the files.
188+
OSError: For other OS-level errors encountered while copying.
189+
"""
111190
backup_file = f"{config_path}.backup"
112191
if not os.path.exists(backup_file):
113192
try:
@@ -119,7 +198,18 @@ def create_config_backup(config_path: str) -> str:
119198

120199

121200
def remove_config_backup(backup_path: str) -> None:
122-
"""Delete the backup file at `backup_path` if it exists."""
201+
"""Delete the backup file at `backup_path` if it exists.
202+
203+
Remove the backup file at the given path if it exists.
204+
205+
If the file is present, attempts to delete it; on failure prints a warning with the error.
206+
207+
Returns:
208+
None
209+
210+
Parameters:
211+
backup_path (str): Filesystem path to the backup file to remove.
212+
"""
123213
if os.path.exists(backup_path):
124214
try:
125215
os.remove(backup_path)
@@ -128,7 +218,15 @@ def remove_config_backup(backup_path: str) -> None:
128218

129219

130220
def restart_container(container_name: str) -> None:
131-
"""Restart a Docker container by name and wait until it is healthy."""
221+
"""Restart a Docker container by name and wait until it is healthy.
222+
223+
Returns:
224+
None
225+
226+
Raises:
227+
subprocess.CalledProcessError: if the `docker restart` command fails.
228+
subprocess.TimeoutExpired: if the `docker restart` command times out.
229+
"""
132230
try:
133231
subprocess.run(
134232
["docker", "restart", container_name],
@@ -147,13 +245,12 @@ def restart_container(container_name: str) -> None:
147245
def replace_placeholders(context: Context, text: str) -> str:
148246
"""Replace {MODEL} and {PROVIDER} placeholders with actual values from context.
149247
150-
Args:
151-
context: Behave context containing default_model and default_provider
152-
text: String that may contain {MODEL} and {PROVIDER} placeholders
248+
Parameters:
249+
context (Context): Behave context containing default_model and default_provider
250+
text (str): String that may contain {MODEL} and {PROVIDER} placeholders
153251
154252
Returns:
155253
String with placeholders replaced by actual values
156-
157254
"""
158255
result = text.replace("{MODEL}", context.default_model)
159256
result = result.replace("{PROVIDER}", context.default_provider)

0 commit comments

Comments
 (0)