qa-insight 1.0.0__tar.gz
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.
- qa_insight-1.0.0/LICENSE +21 -0
- qa_insight-1.0.0/PKG-INFO +205 -0
- qa_insight-1.0.0/README.md +163 -0
- qa_insight-1.0.0/pyproject.toml +72 -0
- qa_insight-1.0.0/setup.cfg +4 -0
- qa_insight-1.0.0/src/qa_insight/__init__.py +18 -0
- qa_insight-1.0.0/src/qa_insight/behave_env.py +68 -0
- qa_insight-1.0.0/src/qa_insight/ci.py +68 -0
- qa_insight-1.0.0/src/qa_insight/client.py +172 -0
- qa_insight-1.0.0/src/qa_insight/config.py +75 -0
- qa_insight-1.0.0/src/qa_insight/dotenv.py +33 -0
- qa_insight-1.0.0/src/qa_insight/git.py +28 -0
- qa_insight-1.0.0/src/qa_insight/insight.py +233 -0
- qa_insight-1.0.0/src/qa_insight/junit_importer.py +115 -0
- qa_insight-1.0.0/src/qa_insight/pytest_plugin.py +119 -0
- qa_insight-1.0.0/src/qa_insight/redact.py +43 -0
- qa_insight-1.0.0/src/qa_insight/robot_library.py +43 -0
- qa_insight-1.0.0/src/qa_insight/robot_listener.py +70 -0
- qa_insight-1.0.0/src/qa_insight/synthesize.py +71 -0
- qa_insight-1.0.0/src/qa_insight/types.py +19 -0
- qa_insight-1.0.0/src/qa_insight.egg-info/PKG-INFO +205 -0
- qa_insight-1.0.0/src/qa_insight.egg-info/SOURCES.txt +33 -0
- qa_insight-1.0.0/src/qa_insight.egg-info/dependency_links.txt +1 -0
- qa_insight-1.0.0/src/qa_insight.egg-info/entry_points.txt +6 -0
- qa_insight-1.0.0/src/qa_insight.egg-info/requires.txt +22 -0
- qa_insight-1.0.0/src/qa_insight.egg-info/top_level.txt +1 -0
- qa_insight-1.0.0/tests/test_ci.py +54 -0
- qa_insight-1.0.0/tests/test_client.py +88 -0
- qa_insight-1.0.0/tests/test_config.py +45 -0
- qa_insight-1.0.0/tests/test_contract.py +78 -0
- qa_insight-1.0.0/tests/test_dotenv.py +43 -0
- qa_insight-1.0.0/tests/test_full_run.py +55 -0
- qa_insight-1.0.0/tests/test_golden.py +84 -0
- qa_insight-1.0.0/tests/test_integration.py +271 -0
- qa_insight-1.0.0/tests/test_redact.py +31 -0
qa_insight-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 QA Insight
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qa-insight
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official QA Insight SDK for Python. Sends test run events (pytest, Robot Framework, behave) to the QA Insight Collector without changing your tests.
|
|
5
|
+
Author-email: QA Insight <qainsight.io@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/QA-Insight/qa-insight
|
|
8
|
+
Project-URL: Repository, https://github.com/QA-Insight/qa-insight
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/QA-Insight/qa-insight/issues
|
|
10
|
+
Keywords: qa,testing,playwright,selenium,pytest,robot,behave,observability
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Provides-Extra: pytest
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == "pytest"
|
|
26
|
+
Provides-Extra: robot
|
|
27
|
+
Requires-Dist: robotframework>=6.0; extra == "robot"
|
|
28
|
+
Provides-Extra: behave
|
|
29
|
+
Requires-Dist: behave>=1.2.6; extra == "behave"
|
|
30
|
+
Provides-Extra: examples
|
|
31
|
+
Requires-Dist: pytest>=7.0; extra == "examples"
|
|
32
|
+
Requires-Dist: pytest-playwright>=0.4; extra == "examples"
|
|
33
|
+
Requires-Dist: playwright>=1.30; extra == "examples"
|
|
34
|
+
Requires-Dist: selenium>=4.10; extra == "examples"
|
|
35
|
+
Requires-Dist: robotframework>=6.0; extra == "examples"
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
38
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
39
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
40
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# qa-insight (Python SDK)
|
|
44
|
+
|
|
45
|
+
SDK oficial de [QA Insight](https://github.com/QA-Insight/qa-insight) para **Python**.
|
|
46
|
+
|
|
47
|
+
Envía automáticamente el ciclo de vida de tus pruebas (inicio/fin de ejecución y de tests) al **Collector** de QA Insight, sin modificar tus tests. Compatible con **pytest** (incluido Selenium WebDriver 4 y Playwright), **Robot Framework**, **behave** (BDD) y un **importador de JUnit XML** post-run.
|
|
48
|
+
|
|
49
|
+
- **Cero dependencias runtime** (solo stdlib: `urllib`, `threading`, `queue`, `subprocess`, `re`). Las integraciones (pytest, robotframework, behave) son dependencias opcionales.
|
|
50
|
+
- Python **3.9+**.
|
|
51
|
+
- Nunca altera el resultado de tus pruebas; si el Collector no responde, registra una advertencia y continúa.
|
|
52
|
+
|
|
53
|
+
## Instalación
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install -e . # desde el repo (desarrollo)
|
|
57
|
+
pip install -e ".[examples]" # con pytest + Playwright + Selenium + Robot para los ejemplos
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Configuración por variables de entorno
|
|
61
|
+
|
|
62
|
+
| Variable | Default | Descripción |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `QAI_ENDPOINT` | `http://localhost:3000/collector/v1/events` | URL del Collector de QA Insight |
|
|
65
|
+
| `QAI_API_KEY` | — | API key de QA Insight (proyecto + ambiente) |
|
|
66
|
+
| `QAI_ENABLED` | `true` | `false` desactiva el envío |
|
|
67
|
+
| `QAI_TIMEOUT_MS` | `2000` | Timeout por request HTTP |
|
|
68
|
+
| `QAI_RETRY_MAX` | `3` | Reintentos para errores transitorios (5xx, 429, red) |
|
|
69
|
+
| `QAI_FLUSH_TIMEOUT_MS` | `5000` | Máximo de espera del flush al terminar |
|
|
70
|
+
| `QAI_RUN_NAME` | `python-run-<ts>` | Nombre de la ejecución |
|
|
71
|
+
| `QAI_RUN_ID` | UUID generado | `externalRunId` (idempotencia del run) |
|
|
72
|
+
| `QAI_FRAMEWORK` | `pytest`/`robot`/`behave`/`junit` | Framework reportado a la columna framework |
|
|
73
|
+
| `QAI_BROWSER` | — | Browser/proyecto (chromium, firefox, webkit, chrome, ...) |
|
|
74
|
+
|
|
75
|
+
## pytest
|
|
76
|
+
|
|
77
|
+
El plugin se auto-registra (entry point `pytest11`). Solo necesitas las variables de entorno:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
export QAI_ENDPOINT=http://localhost:3000/collector/v1/events
|
|
81
|
+
export QAI_API_KEY=qai_...
|
|
82
|
+
export QAI_FRAMEWORK=playwright # o selenium, o pytest
|
|
83
|
+
pytest
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- `pytest_sessionstart` → `RUN_STARTED`
|
|
87
|
+
- Cada test → `TEST_STARTED` / `TEST_FINISHED` (con `retryIndex` si usás `pytest-rerunfailures`)
|
|
88
|
+
- `pytest_sessionfinish` → `RUN_FINISHED`
|
|
89
|
+
- Los tests parametrizados generan nodeids únicos → no colapsan en el backend.
|
|
90
|
+
|
|
91
|
+
Para reportar el browser por test (badges de browser en la UI), setealos en un `conftest.py`:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# conftest.py (Playwright)
|
|
95
|
+
import pytest
|
|
96
|
+
from qa_insight import QaiContext
|
|
97
|
+
|
|
98
|
+
@pytest.fixture(autouse=True)
|
|
99
|
+
def qai_browser(browser):
|
|
100
|
+
QaiContext.set_browser(browser.browser_type.name)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
# conftest.py (Selenium 4)
|
|
105
|
+
import pytest
|
|
106
|
+
from qa_insight import QaiContext
|
|
107
|
+
|
|
108
|
+
@pytest.fixture
|
|
109
|
+
def driver():
|
|
110
|
+
from selenium import webdriver
|
|
111
|
+
d = webdriver.Chrome() # Selenium Manager descarga el driver automáticamente
|
|
112
|
+
QaiContext.set_webdriver(d)
|
|
113
|
+
yield d
|
|
114
|
+
d.quit()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
El plugin resuelve el browser en este orden: `QaiContext.browser` → `QAI_BROWSER` → capabilities del `QaiContext.webdriver`.
|
|
118
|
+
|
|
119
|
+
### Cross-browser
|
|
120
|
+
|
|
121
|
+
- **Playwright**: `pytest --browser chromium --browser firefox --browser webkit` (cada browser genera su ejecución con sus badges).
|
|
122
|
+
- **Selenium 4**: `pytest --selenium-browser chrome` / `pytest --selenium-browser firefox` (ver ejemplo).
|
|
123
|
+
- **Robot Framework**: exporta `QAI_BROWSER=chrome|firefox` al correr con `--variable BROWSER:...`.
|
|
124
|
+
|
|
125
|
+
## Robot Framework
|
|
126
|
+
|
|
127
|
+
El SDK incluye un **library listener** (`QaiInsightLibrary`) que se auto-registra, de modo que no hace falta
|
|
128
|
+
pasar `--listener` en la línea de comandos. Solo importá la librería (idealmente en un resource compartido):
|
|
129
|
+
|
|
130
|
+
```robotframework
|
|
131
|
+
*** Settings ***
|
|
132
|
+
Library qa_insight.robot_library.QaiInsightLibrary
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Al importarse, la librería carga el `.env` del proyecto, emite `RUN_STARTED`, registra el listener para
|
|
136
|
+
toda la ejecución (tags, suites, tests específicos o archivos individuales) y, al terminar, emite
|
|
137
|
+
`RUN_FINISHED` de forma garantizada vía su método `close()`.
|
|
138
|
+
|
|
139
|
+
Configuración vía `.env` (sin necesidad de `export`):
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
QAI_API_KEY=qai_...
|
|
143
|
+
QAI_ENDPOINT=http://localhost:3000/collector/v1/events
|
|
144
|
+
QAI_FRAMEWORK=robot
|
|
145
|
+
QAI_BROWSER=chrome # opcional: badges de browser + Open Browser de SeleniumLibrary
|
|
146
|
+
HEADLESS=True # opcional: modo headless/visible del navegador
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Ejecutá tus tests como siempre:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
robot tests/ # todo
|
|
153
|
+
robot --include smoke tests/ # por tags
|
|
154
|
+
robot --suite my_suite tests/ # por suite
|
|
155
|
+
robot --test "my test" tests/ # por test
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
> Alternativa (CLI listener): `robot --listener qa_insight.robot_listener.QaiInsightListener tests/`
|
|
159
|
+
|
|
160
|
+
## behave (BDD)
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
# features/environment.py
|
|
164
|
+
from qa_insight.behave_env import * # noqa
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Importador de JUnit XML (post-run)
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
export QAI_API_KEY=qai_...
|
|
171
|
+
qa-insight-import junit.xml --framework pytest
|
|
172
|
+
# o para cualquier framework que emita JUnit XML
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Uso manual / sin runner
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from qa_insight import QaiInsight
|
|
179
|
+
|
|
180
|
+
insight = QaiInsight({"api_key": "qai_...", "framework": "selenium"})
|
|
181
|
+
insight.start_run()
|
|
182
|
+
insight.start_test("test_login", suite="tests/test_auth.py")
|
|
183
|
+
insight.finish_test("test_login", suite="tests/test_auth.py", status="PASSED", duration_ms=1200)
|
|
184
|
+
insight.finish_run()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Comportamiento ante fallos de red
|
|
188
|
+
|
|
189
|
+
- Buffer no bloqueante en un hilo de fondo + flush acotado (`QAI_FLUSH_TIMEOUT_MS`) al terminar.
|
|
190
|
+
- Errores transitorios (timeout, red, 5xx, 429) se reintentan con backoff exponencial (50 ms·2^n, cap 1 s).
|
|
191
|
+
- Errores permanentes (4xx) no se reintentan.
|
|
192
|
+
- La API key **nunca** se imprime ni se incluye en los payloads; `errorMessage`/`stacktrace` se redactan antes de enviar (authorization, bearer, cookie, password, secret, api-key, token, `qai_`, `xox*` + literales custom).
|
|
193
|
+
- Payload limitado a 100 KB (se trunca el stacktrace si excede).
|
|
194
|
+
|
|
195
|
+
## Desarrollo
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
pip install -e ".[dev]"
|
|
199
|
+
ruff check src tests
|
|
200
|
+
pytest
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Licencia
|
|
204
|
+
|
|
205
|
+
MIT
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# qa-insight (Python SDK)
|
|
2
|
+
|
|
3
|
+
SDK oficial de [QA Insight](https://github.com/QA-Insight/qa-insight) para **Python**.
|
|
4
|
+
|
|
5
|
+
Envía automáticamente el ciclo de vida de tus pruebas (inicio/fin de ejecución y de tests) al **Collector** de QA Insight, sin modificar tus tests. Compatible con **pytest** (incluido Selenium WebDriver 4 y Playwright), **Robot Framework**, **behave** (BDD) y un **importador de JUnit XML** post-run.
|
|
6
|
+
|
|
7
|
+
- **Cero dependencias runtime** (solo stdlib: `urllib`, `threading`, `queue`, `subprocess`, `re`). Las integraciones (pytest, robotframework, behave) son dependencias opcionales.
|
|
8
|
+
- Python **3.9+**.
|
|
9
|
+
- Nunca altera el resultado de tus pruebas; si el Collector no responde, registra una advertencia y continúa.
|
|
10
|
+
|
|
11
|
+
## Instalación
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install -e . # desde el repo (desarrollo)
|
|
15
|
+
pip install -e ".[examples]" # con pytest + Playwright + Selenium + Robot para los ejemplos
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Configuración por variables de entorno
|
|
19
|
+
|
|
20
|
+
| Variable | Default | Descripción |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `QAI_ENDPOINT` | `http://localhost:3000/collector/v1/events` | URL del Collector de QA Insight |
|
|
23
|
+
| `QAI_API_KEY` | — | API key de QA Insight (proyecto + ambiente) |
|
|
24
|
+
| `QAI_ENABLED` | `true` | `false` desactiva el envío |
|
|
25
|
+
| `QAI_TIMEOUT_MS` | `2000` | Timeout por request HTTP |
|
|
26
|
+
| `QAI_RETRY_MAX` | `3` | Reintentos para errores transitorios (5xx, 429, red) |
|
|
27
|
+
| `QAI_FLUSH_TIMEOUT_MS` | `5000` | Máximo de espera del flush al terminar |
|
|
28
|
+
| `QAI_RUN_NAME` | `python-run-<ts>` | Nombre de la ejecución |
|
|
29
|
+
| `QAI_RUN_ID` | UUID generado | `externalRunId` (idempotencia del run) |
|
|
30
|
+
| `QAI_FRAMEWORK` | `pytest`/`robot`/`behave`/`junit` | Framework reportado a la columna framework |
|
|
31
|
+
| `QAI_BROWSER` | — | Browser/proyecto (chromium, firefox, webkit, chrome, ...) |
|
|
32
|
+
|
|
33
|
+
## pytest
|
|
34
|
+
|
|
35
|
+
El plugin se auto-registra (entry point `pytest11`). Solo necesitas las variables de entorno:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
export QAI_ENDPOINT=http://localhost:3000/collector/v1/events
|
|
39
|
+
export QAI_API_KEY=qai_...
|
|
40
|
+
export QAI_FRAMEWORK=playwright # o selenium, o pytest
|
|
41
|
+
pytest
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- `pytest_sessionstart` → `RUN_STARTED`
|
|
45
|
+
- Cada test → `TEST_STARTED` / `TEST_FINISHED` (con `retryIndex` si usás `pytest-rerunfailures`)
|
|
46
|
+
- `pytest_sessionfinish` → `RUN_FINISHED`
|
|
47
|
+
- Los tests parametrizados generan nodeids únicos → no colapsan en el backend.
|
|
48
|
+
|
|
49
|
+
Para reportar el browser por test (badges de browser en la UI), setealos en un `conftest.py`:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
# conftest.py (Playwright)
|
|
53
|
+
import pytest
|
|
54
|
+
from qa_insight import QaiContext
|
|
55
|
+
|
|
56
|
+
@pytest.fixture(autouse=True)
|
|
57
|
+
def qai_browser(browser):
|
|
58
|
+
QaiContext.set_browser(browser.browser_type.name)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
# conftest.py (Selenium 4)
|
|
63
|
+
import pytest
|
|
64
|
+
from qa_insight import QaiContext
|
|
65
|
+
|
|
66
|
+
@pytest.fixture
|
|
67
|
+
def driver():
|
|
68
|
+
from selenium import webdriver
|
|
69
|
+
d = webdriver.Chrome() # Selenium Manager descarga el driver automáticamente
|
|
70
|
+
QaiContext.set_webdriver(d)
|
|
71
|
+
yield d
|
|
72
|
+
d.quit()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
El plugin resuelve el browser en este orden: `QaiContext.browser` → `QAI_BROWSER` → capabilities del `QaiContext.webdriver`.
|
|
76
|
+
|
|
77
|
+
### Cross-browser
|
|
78
|
+
|
|
79
|
+
- **Playwright**: `pytest --browser chromium --browser firefox --browser webkit` (cada browser genera su ejecución con sus badges).
|
|
80
|
+
- **Selenium 4**: `pytest --selenium-browser chrome` / `pytest --selenium-browser firefox` (ver ejemplo).
|
|
81
|
+
- **Robot Framework**: exporta `QAI_BROWSER=chrome|firefox` al correr con `--variable BROWSER:...`.
|
|
82
|
+
|
|
83
|
+
## Robot Framework
|
|
84
|
+
|
|
85
|
+
El SDK incluye un **library listener** (`QaiInsightLibrary`) que se auto-registra, de modo que no hace falta
|
|
86
|
+
pasar `--listener` en la línea de comandos. Solo importá la librería (idealmente en un resource compartido):
|
|
87
|
+
|
|
88
|
+
```robotframework
|
|
89
|
+
*** Settings ***
|
|
90
|
+
Library qa_insight.robot_library.QaiInsightLibrary
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Al importarse, la librería carga el `.env` del proyecto, emite `RUN_STARTED`, registra el listener para
|
|
94
|
+
toda la ejecución (tags, suites, tests específicos o archivos individuales) y, al terminar, emite
|
|
95
|
+
`RUN_FINISHED` de forma garantizada vía su método `close()`.
|
|
96
|
+
|
|
97
|
+
Configuración vía `.env` (sin necesidad de `export`):
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
QAI_API_KEY=qai_...
|
|
101
|
+
QAI_ENDPOINT=http://localhost:3000/collector/v1/events
|
|
102
|
+
QAI_FRAMEWORK=robot
|
|
103
|
+
QAI_BROWSER=chrome # opcional: badges de browser + Open Browser de SeleniumLibrary
|
|
104
|
+
HEADLESS=True # opcional: modo headless/visible del navegador
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Ejecutá tus tests como siempre:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
robot tests/ # todo
|
|
111
|
+
robot --include smoke tests/ # por tags
|
|
112
|
+
robot --suite my_suite tests/ # por suite
|
|
113
|
+
robot --test "my test" tests/ # por test
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
> Alternativa (CLI listener): `robot --listener qa_insight.robot_listener.QaiInsightListener tests/`
|
|
117
|
+
|
|
118
|
+
## behave (BDD)
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
# features/environment.py
|
|
122
|
+
from qa_insight.behave_env import * # noqa
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Importador de JUnit XML (post-run)
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
export QAI_API_KEY=qai_...
|
|
129
|
+
qa-insight-import junit.xml --framework pytest
|
|
130
|
+
# o para cualquier framework que emita JUnit XML
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Uso manual / sin runner
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from qa_insight import QaiInsight
|
|
137
|
+
|
|
138
|
+
insight = QaiInsight({"api_key": "qai_...", "framework": "selenium"})
|
|
139
|
+
insight.start_run()
|
|
140
|
+
insight.start_test("test_login", suite="tests/test_auth.py")
|
|
141
|
+
insight.finish_test("test_login", suite="tests/test_auth.py", status="PASSED", duration_ms=1200)
|
|
142
|
+
insight.finish_run()
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Comportamiento ante fallos de red
|
|
146
|
+
|
|
147
|
+
- Buffer no bloqueante en un hilo de fondo + flush acotado (`QAI_FLUSH_TIMEOUT_MS`) al terminar.
|
|
148
|
+
- Errores transitorios (timeout, red, 5xx, 429) se reintentan con backoff exponencial (50 ms·2^n, cap 1 s).
|
|
149
|
+
- Errores permanentes (4xx) no se reintentan.
|
|
150
|
+
- La API key **nunca** se imprime ni se incluye en los payloads; `errorMessage`/`stacktrace` se redactan antes de enviar (authorization, bearer, cookie, password, secret, api-key, token, `qai_`, `xox*` + literales custom).
|
|
151
|
+
- Payload limitado a 100 KB (se trunca el stacktrace si excede).
|
|
152
|
+
|
|
153
|
+
## Desarrollo
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
pip install -e ".[dev]"
|
|
157
|
+
ruff check src tests
|
|
158
|
+
pytest
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Licencia
|
|
162
|
+
|
|
163
|
+
MIT
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "qa-insight"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Official QA Insight SDK for Python. Sends test run events (pytest, Robot Framework, behave) to the QA Insight Collector without changing your tests."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "QA Insight", email = "qainsight.io@gmail.com" }]
|
|
14
|
+
keywords = ["qa", "testing", "playwright", "selenium", "pytest", "robot", "behave", "observability"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
25
|
+
"Topic :: Software Development :: Testing",
|
|
26
|
+
]
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
pytest = ["pytest>=7.0"]
|
|
31
|
+
robot = ["robotframework>=6.0"]
|
|
32
|
+
behave = ["behave>=1.2.6"]
|
|
33
|
+
examples = [
|
|
34
|
+
"pytest>=7.0",
|
|
35
|
+
"pytest-playwright>=0.4",
|
|
36
|
+
"playwright>=1.30",
|
|
37
|
+
"selenium>=4.10",
|
|
38
|
+
"robotframework>=6.0",
|
|
39
|
+
]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest>=7.0",
|
|
42
|
+
"ruff>=0.4",
|
|
43
|
+
"build>=1.0",
|
|
44
|
+
"twine>=5.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
Homepage = "https://github.com/QA-Insight/qa-insight"
|
|
49
|
+
Repository = "https://github.com/QA-Insight/qa-insight"
|
|
50
|
+
"Bug Tracker" = "https://github.com/QA-Insight/qa-insight/issues"
|
|
51
|
+
|
|
52
|
+
[project.entry-points.pytest11]
|
|
53
|
+
qa-insight = "qa_insight.pytest_plugin"
|
|
54
|
+
|
|
55
|
+
[project.scripts]
|
|
56
|
+
qa-insight-import = "qa_insight.junit_importer:main"
|
|
57
|
+
qa-insight-synthesize = "qa_insight.synthesize:main"
|
|
58
|
+
|
|
59
|
+
[tool.setuptools.packages.find]
|
|
60
|
+
where = ["src"]
|
|
61
|
+
|
|
62
|
+
[tool.ruff]
|
|
63
|
+
line-length = 110
|
|
64
|
+
target-version = "py39"
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint]
|
|
67
|
+
select = ["E", "F", "W", "I", "UP"]
|
|
68
|
+
ignore = ["E501"]
|
|
69
|
+
|
|
70
|
+
[tool.pytest.ini_options]
|
|
71
|
+
testpaths = ["tests"]
|
|
72
|
+
addopts = "-q"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .client import QaiClient
|
|
2
|
+
from .config import QaiConfig, resolve_config
|
|
3
|
+
from .dotenv import load_dotenv
|
|
4
|
+
from .insight import QaiContext, QaiInsight
|
|
5
|
+
from .redact import build_redactor
|
|
6
|
+
from .robot_library import QaiInsightLibrary
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"QaiClient",
|
|
10
|
+
"QaiConfig",
|
|
11
|
+
"QaiContext",
|
|
12
|
+
"QaiInsight",
|
|
13
|
+
"QaiInsightLibrary",
|
|
14
|
+
"build_redactor",
|
|
15
|
+
"load_dotenv",
|
|
16
|
+
"resolve_config",
|
|
17
|
+
]
|
|
18
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""behave hooks.
|
|
2
|
+
|
|
3
|
+
Import this module from your ``environment.py``:
|
|
4
|
+
|
|
5
|
+
from qa_insight.behave_env import * # noqa
|
|
6
|
+
|
|
7
|
+
or copy the hooks into your own ``environment.py``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
from .insight import QaiInsight
|
|
15
|
+
from .types import QaiTestStatus
|
|
16
|
+
|
|
17
|
+
_DEFAULT_FRAMEWORK = "behave"
|
|
18
|
+
|
|
19
|
+
_insight: QaiInsight | None = None
|
|
20
|
+
|
|
21
|
+
_STATUS = {
|
|
22
|
+
"passed": QaiTestStatus.PASSED.value,
|
|
23
|
+
"failed": QaiTestStatus.FAILED.value,
|
|
24
|
+
"skipped": QaiTestStatus.SKIPPED.value,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_started_at: float | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def before_all(context) -> None:
|
|
31
|
+
global _insight
|
|
32
|
+
_insight = QaiInsight({"framework": _DEFAULT_FRAMEWORK})
|
|
33
|
+
_insight.start_run()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def before_scenario(context, scenario) -> None:
|
|
37
|
+
global _insight, _started_at
|
|
38
|
+
if _insight is None:
|
|
39
|
+
return
|
|
40
|
+
feature = getattr(scenario, "feature", None)
|
|
41
|
+
_started_at = time.time()
|
|
42
|
+
_insight.start_test(scenario.name, suite=getattr(feature, "name", None) or None, started_at=_started_at)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def after_scenario(context, scenario) -> None:
|
|
46
|
+
global _insight
|
|
47
|
+
if _insight is None:
|
|
48
|
+
return
|
|
49
|
+
feature = getattr(scenario, "feature", None)
|
|
50
|
+
duration_ms = int((getattr(scenario, "duration", 0) or 0) * 1000)
|
|
51
|
+
# behave >= 1.3 exposes scenario.status as an IntEnum (Status.passed);
|
|
52
|
+
# fall back to a plain string for older versions.
|
|
53
|
+
status_value = getattr(scenario.status, "name", None) or scenario.status
|
|
54
|
+
status = _STATUS.get(status_value, QaiTestStatus.ABORTED.value)
|
|
55
|
+
_insight.finish_test(
|
|
56
|
+
scenario.name,
|
|
57
|
+
suite=getattr(feature, "name", None) or None,
|
|
58
|
+
status=status,
|
|
59
|
+
duration_ms=duration_ms,
|
|
60
|
+
started_at=_started_at,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def after_all(context) -> None:
|
|
65
|
+
global _insight
|
|
66
|
+
if _insight is not None:
|
|
67
|
+
_insight.finish_run()
|
|
68
|
+
_insight = None
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _env(name: str) -> str | None:
|
|
7
|
+
value = os.environ.get(name)
|
|
8
|
+
return value if value and len(value) > 0 else None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _github_actions() -> dict[str, str | None]:
|
|
12
|
+
server = _env("GITHUB_SERVER_URL") or "https://github.com"
|
|
13
|
+
repo = _env("GITHUB_REPOSITORY")
|
|
14
|
+
run_id = _env("GITHUB_RUN_ID")
|
|
15
|
+
pipeline_url = f"{server}/{repo}/actions/runs/{run_id}" if repo and run_id else None
|
|
16
|
+
return {
|
|
17
|
+
"provider": "github-actions",
|
|
18
|
+
"branch": _env("GITHUB_HEAD_REF") or _env("GITHUB_REF_NAME"),
|
|
19
|
+
"commitSha": _env("GITHUB_SHA"),
|
|
20
|
+
"pipelineUrl": pipeline_url,
|
|
21
|
+
"runId": run_id,
|
|
22
|
+
"buildId": _env("GITHUB_RUN_ATTEMPT"),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _gitlab_ci() -> dict[str, str | None]:
|
|
27
|
+
return {
|
|
28
|
+
"provider": "gitlab-ci",
|
|
29
|
+
"branch": _env("CI_COMMIT_BRANCH") or _env("CI_COMMIT_REF_NAME"),
|
|
30
|
+
"commitSha": _env("CI_COMMIT_SHA"),
|
|
31
|
+
"pipelineUrl": _env("CI_PIPELINE_URL") or _env("CI_JOB_URL"),
|
|
32
|
+
"runId": _env("CI_PIPELINE_ID") or _env("CI_JOB_ID"),
|
|
33
|
+
"buildId": _env("CI_JOB_ID"),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _jenkins() -> dict[str, str | None]:
|
|
38
|
+
return {
|
|
39
|
+
"provider": "jenkins",
|
|
40
|
+
"branch": _env("BRANCH_NAME") or _env("GIT_BRANCH"),
|
|
41
|
+
"commitSha": _env("GIT_COMMIT"),
|
|
42
|
+
"pipelineUrl": _env("BUILD_URL"),
|
|
43
|
+
"runId": _env("BUILD_NUMBER"),
|
|
44
|
+
"buildId": _env("BUILD_ID") or _env("BUILD_NUMBER"),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _azure_devops() -> dict[str, str | None]:
|
|
49
|
+
return {
|
|
50
|
+
"provider": "azure-pipelines",
|
|
51
|
+
"branch": _env("BUILD_SOURCEBRANCH"),
|
|
52
|
+
"commitSha": _env("BUILD_SOURCEVERSION"),
|
|
53
|
+
"pipelineUrl": _env("BUILD_BUILDURI"),
|
|
54
|
+
"runId": _env("BUILD_BUILDID"),
|
|
55
|
+
"buildId": _env("BUILD_BUILDID"),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def detect_ci() -> dict[str, str | None]:
|
|
60
|
+
if _env("GITHUB_ACTIONS") == "true":
|
|
61
|
+
return _github_actions()
|
|
62
|
+
if _env("GITLAB_CI") == "true":
|
|
63
|
+
return _gitlab_ci()
|
|
64
|
+
if _env("JENKINS_URL") or _env("JENKINS_HOME"):
|
|
65
|
+
return _jenkins()
|
|
66
|
+
if _env("TF_BUILD") in ("True", "true"):
|
|
67
|
+
return _azure_devops()
|
|
68
|
+
return {}
|