pylynqa 0.1.0a1__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.
- pylynqa/__init__.py +30 -0
- pylynqa/atest/test_api.py +222 -0
- pylynqa/client.py +490 -0
- pylynqa/models.py +227 -0
- pylynqa/test/__init__.py +50 -0
- pylynqa/test/conftest.py +12 -0
- pylynqa/test/data_set.py +153 -0
- pylynqa/test/screenshot.png +0 -0
- pylynqa/test/test_client.py +644 -0
- pylynqa-0.1.0a1.dist-info/METADATA +14 -0
- pylynqa-0.1.0a1.dist-info/RECORD +13 -0
- pylynqa-0.1.0a1.dist-info/WHEEL +5 -0
- pylynqa-0.1.0a1.dist-info/top_level.txt +1 -0
pylynqa/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Python client package for the Lynqa API."""
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"BASE_URL",
|
|
5
|
+
"Attachment",
|
|
6
|
+
"CreateAttachment",
|
|
7
|
+
"CreateTestStep",
|
|
8
|
+
"LynqaClient",
|
|
9
|
+
"LynqaClientError",
|
|
10
|
+
"TestData",
|
|
11
|
+
"TestRunContext",
|
|
12
|
+
"TestRunsFilter",
|
|
13
|
+
"TestStep",
|
|
14
|
+
"TextualData",
|
|
15
|
+
"TimePeriod",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
from .client import BASE_URL, LynqaClient
|
|
19
|
+
from .models import (
|
|
20
|
+
Attachment,
|
|
21
|
+
CreateAttachment,
|
|
22
|
+
CreateTestStep,
|
|
23
|
+
LynqaClientError,
|
|
24
|
+
TestData,
|
|
25
|
+
TestRunContext,
|
|
26
|
+
TestRunsFilter,
|
|
27
|
+
TestStep,
|
|
28
|
+
TextualData,
|
|
29
|
+
TimePeriod,
|
|
30
|
+
)
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Acceptance tests for the Lynqa API — no mocking, real HTTP calls.
|
|
2
|
+
|
|
3
|
+
Requires the environment variable ``LYNQA_API_KEY`` to be set.
|
|
4
|
+
|
|
5
|
+
Run with::
|
|
6
|
+
|
|
7
|
+
LYNQA_API_KEY=lq_live_xxx pytest projects/pylynqa/src/pylynqa/atest -v
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# ruff: file-ignore[undocumented-public-method,no-self-use]
|
|
11
|
+
import os
|
|
12
|
+
import time
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
import responses
|
|
17
|
+
|
|
18
|
+
import pylynqa
|
|
19
|
+
from pylynqa import LynqaClient, TestRunsFilter, TimePeriod
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Constants
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
POLL_TIMEOUT_S = 300
|
|
26
|
+
TERMINAL_STATUSES = {"success", "failed", "error", "stopped", "not_run"}
|
|
27
|
+
|
|
28
|
+
TEST_RUN_NAME = f"Automatic test — Google search for lynqa - {datetime.now(tz=timezone.utc).isoformat()}"
|
|
29
|
+
GHERKIN_SCENARIO = (
|
|
30
|
+
"Given go to https://www.google.com/\n"
|
|
31
|
+
"When I look at the search input\n"
|
|
32
|
+
"Then the search input exists\n"
|
|
33
|
+
"When I search for 'lynqa'\n"
|
|
34
|
+
"Then several results are displayed"
|
|
35
|
+
"Then the first results display 'Smartesting'"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Fixtures
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture(scope="module")
|
|
45
|
+
def client() -> LynqaClient:
|
|
46
|
+
"""Instantiate a LynqaClient object."""
|
|
47
|
+
api_key = os.environ.get("LYNQA_API_KEY")
|
|
48
|
+
if not api_key:
|
|
49
|
+
pytest.skip("LYNQA_API_KEY environment variable is not set")
|
|
50
|
+
return LynqaClient(api_key=api_key)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Helpers
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _wait_for_completion(client: LynqaClient, run_id: str) -> None:
|
|
59
|
+
"""Poll status until the run reaches a terminal state or the timeout expires."""
|
|
60
|
+
deadline = time.monotonic() + POLL_TIMEOUT_S
|
|
61
|
+
while time.monotonic() < deadline:
|
|
62
|
+
result = client.get_test_run_status(run_id)
|
|
63
|
+
if result["status"] in TERMINAL_STATUSES:
|
|
64
|
+
return
|
|
65
|
+
time.sleep(5)
|
|
66
|
+
pytest.fail(f"Test run {run_id!r} did not complete within {POLL_TIMEOUT_S}s")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Test scenario
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TestApiScenario:
|
|
75
|
+
"""End-to-end scenario exercising all major API operations in order."""
|
|
76
|
+
|
|
77
|
+
test_run_id: str = ""
|
|
78
|
+
|
|
79
|
+
_INDEPENDENT_TESTS = (
|
|
80
|
+
"test_health_live",
|
|
81
|
+
"test_health_ready",
|
|
82
|
+
"test_get_account_credits",
|
|
83
|
+
"test_get_purchases",
|
|
84
|
+
"test_get_credit_ledger",
|
|
85
|
+
"test_create_gherkin_test_run",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@pytest.fixture(autouse=True) # ruff: ignore[pytest-fixture-autouse]
|
|
89
|
+
def setup(self, request):
|
|
90
|
+
"""Set up all tests."""
|
|
91
|
+
# Disable HTTP mocking for all requests to the real API base URL.
|
|
92
|
+
responses.add_passthru(pylynqa.BASE_URL)
|
|
93
|
+
# Skip certain tests if test run not created
|
|
94
|
+
if request.node.name not in self._INDEPENDENT_TESTS and not TestApiScenario.test_run_id:
|
|
95
|
+
pytest.skip("skipped: test_create_gherkin_test_run did not produce a test_run_id")
|
|
96
|
+
|
|
97
|
+
def test_health_live(self, client):
|
|
98
|
+
# Act
|
|
99
|
+
result = client.health_live()
|
|
100
|
+
# Assert
|
|
101
|
+
assert result["status"] == "ok"
|
|
102
|
+
|
|
103
|
+
def test_health_ready(self, client):
|
|
104
|
+
# Act
|
|
105
|
+
result = client.health_ready()
|
|
106
|
+
# Assert
|
|
107
|
+
assert result["ready"]
|
|
108
|
+
|
|
109
|
+
def test_get_account_credits(self, client):
|
|
110
|
+
# Act
|
|
111
|
+
available_credits = client.get_test_execution_credits()
|
|
112
|
+
# Assert
|
|
113
|
+
assert isinstance(available_credits, int)
|
|
114
|
+
assert available_credits >= 0
|
|
115
|
+
|
|
116
|
+
def test_get_purchases(self, client):
|
|
117
|
+
# Act
|
|
118
|
+
purchases = client.get_purchases()
|
|
119
|
+
# Assert
|
|
120
|
+
assert isinstance(purchases, list)
|
|
121
|
+
|
|
122
|
+
def test_get_credit_ledger(self, client):
|
|
123
|
+
# Act
|
|
124
|
+
ledger = client.get_credit_ledger()
|
|
125
|
+
# Assert
|
|
126
|
+
assert isinstance(ledger, list)
|
|
127
|
+
assert len(ledger) > 0
|
|
128
|
+
assert ledger[0]["balanceAfter"] != 0, "Should have at least one credit ledger"
|
|
129
|
+
|
|
130
|
+
def test_create_gherkin_test_run(self, client):
|
|
131
|
+
# Act
|
|
132
|
+
TestApiScenario.test_run_id = client.add_gherkin_test_run(
|
|
133
|
+
url="https://www.google.com/",
|
|
134
|
+
scenario=GHERKIN_SCENARIO,
|
|
135
|
+
name=TEST_RUN_NAME,
|
|
136
|
+
)
|
|
137
|
+
# Assert
|
|
138
|
+
assert TestApiScenario.test_run_id is not None
|
|
139
|
+
if not isinstance(TestApiScenario.test_run_id, str):
|
|
140
|
+
pytest.warns(UserWarning, match="test_run_id is not a str")
|
|
141
|
+
else:
|
|
142
|
+
assert len(TestApiScenario.test_run_id) > 0
|
|
143
|
+
|
|
144
|
+
def test_get_test_run_status(self, client):
|
|
145
|
+
# Act
|
|
146
|
+
result = client.get_test_run_status(TestApiScenario.test_run_id)
|
|
147
|
+
# Assert
|
|
148
|
+
assert "status" in result
|
|
149
|
+
assert result["status"] in {"waiting", "running"} | TERMINAL_STATUSES
|
|
150
|
+
|
|
151
|
+
def test_get_test_run_full_status(self, client):
|
|
152
|
+
# Arrange
|
|
153
|
+
_wait_for_completion(client, TestApiScenario.test_run_id)
|
|
154
|
+
# Act
|
|
155
|
+
result = client.get_test_run_full_status(TestApiScenario.test_run_id)
|
|
156
|
+
# Assert
|
|
157
|
+
assert "status" in result
|
|
158
|
+
assert result["status"] in TERMINAL_STATUSES
|
|
159
|
+
assert "stepStatuses" in result
|
|
160
|
+
assert "steps" in result
|
|
161
|
+
assert len(result["stepStatuses"]) == len(result["steps"])
|
|
162
|
+
assert len(result["stepStatuses"]) > 0
|
|
163
|
+
assert result["type"] == "gherkin"
|
|
164
|
+
|
|
165
|
+
def test_get_test_run(self, client):
|
|
166
|
+
# Act
|
|
167
|
+
result = client.get_test_run(TestApiScenario.test_run_id)
|
|
168
|
+
# Assert
|
|
169
|
+
assert result["name"] == TEST_RUN_NAME
|
|
170
|
+
assert result["url"] == "https://www.google.com/"
|
|
171
|
+
assert result["type"] == "gherkin"
|
|
172
|
+
assert "steps" in result
|
|
173
|
+
|
|
174
|
+
def test_get_step_info(self, client):
|
|
175
|
+
# Arrange
|
|
176
|
+
status_result = client.get_test_run_full_status(TestApiScenario.test_run_id)
|
|
177
|
+
if not status_result.get("stepStatuses"):
|
|
178
|
+
pytest.skip("No steps available in full status")
|
|
179
|
+
# Act
|
|
180
|
+
result = client.get_test_run_step_status(TestApiScenario.test_run_id, 0)
|
|
181
|
+
# Assert
|
|
182
|
+
assert "status" in result
|
|
183
|
+
assert "commands" in result
|
|
184
|
+
assert "assertionsReport" in result
|
|
185
|
+
|
|
186
|
+
def test_get_screenshot_info(self, client):
|
|
187
|
+
# Arrange
|
|
188
|
+
status = client.get_test_run_status(TestApiScenario.test_run_id)
|
|
189
|
+
screenshot_id = status.get("initialReport", {}).get("screenshot")
|
|
190
|
+
if not screenshot_id:
|
|
191
|
+
pytest.skip("No initial screenshot available")
|
|
192
|
+
# Act
|
|
193
|
+
data = client.get_screenshot(TestApiScenario.test_run_id, screenshot_id)
|
|
194
|
+
# Assert
|
|
195
|
+
assert isinstance(data, str)
|
|
196
|
+
assert len(data) > 0
|
|
197
|
+
|
|
198
|
+
def test_query_test_runs(self, client):
|
|
199
|
+
# Act
|
|
200
|
+
result = client.query_test_runs(filters=TestRunsFilter(relative_period=TimePeriod(count=5, unit="m")))
|
|
201
|
+
# Assert
|
|
202
|
+
assert "testRuns" in result
|
|
203
|
+
assert len(result["testRuns"]) > 0
|
|
204
|
+
if not isinstance(TestApiScenario.test_run_id, str):
|
|
205
|
+
pytest.warns(UserWarning, match="test_run_id is not a str")
|
|
206
|
+
assert result["testRuns"][-1].get("id", 0) == str(
|
|
207
|
+
TestApiScenario.test_run_id
|
|
208
|
+
) # Remove str casting when API ready
|
|
209
|
+
|
|
210
|
+
def test_stop_test_runs(self, client):
|
|
211
|
+
# Act
|
|
212
|
+
result = client.stop_test_runs([TestApiScenario.test_run_id])
|
|
213
|
+
# Assert
|
|
214
|
+
assert "stoppedTestRunIds" in result
|
|
215
|
+
assert isinstance(result["stoppedTestRunIds"], list)
|
|
216
|
+
|
|
217
|
+
def test_delete_test_run(self, client):
|
|
218
|
+
# Act
|
|
219
|
+
client.delete_test_run(TestApiScenario.test_run_id)
|
|
220
|
+
# Assert
|
|
221
|
+
with pytest.raises(pylynqa.LynqaClientError):
|
|
222
|
+
client.get_test_run_status(TestApiScenario.test_run_id)
|