Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,5 @@ pip-wheel-metadata/
pytestdebug.log

.vs/
*-examples/
*-examples/
.user/
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ Supported test frameworks:
3. [RobotFramework](https://github.com/testit-tms/adapters-python/tree/main/testit-adapter-robotframework)
4. [Nose](https://github.com/testit-tms/adapters-python/tree/main/testit-adapter-nose)

## Internal documentation

Adapter behaviour specs (export contract, mode 0, real-time import): [docs/README.md](docs/README.md).

# 🚀 Warning
Since 3.0.0 version:
- If the externalId annotation is not specified, then its contents will be a hash of a fully qualified method name.
Expand Down
17 changes: 17 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Adapter documentation (Python)

Supplementary docs for behaviour that is not covered in adapter READMEs.

| Document | Topic |
|----------|--------|
| [test-result-export-contract.md](./test-result-export-contract.md) | **Canonical:** `sendTestResults` vs PUT; PUT only for fixture setup/teardown |
| [mode0-duplicate-results-bulk-after-realtime.md](./mode0-duplicate-results-bulk-after-realtime.md) | Mode 0 duplicate Passed (TP + orphan): no second bulk send |
| [mode0-orphan-testpoint-inprogress.md](./mode0-orphan-testpoint-inprogress.md) | Mode 0 + InProgress slots: create path, Sync Storage |
| [test-run-tags-and-links.md](./test-run-tags-and-links.md) | Test run tags & links on create / early merge |

Commons (real-time import, nested steps):

| Document | Topic |
|----------|--------|
| [../testit-python-commons/realtime-import-spec.md](../testit-python-commons/realtime-import-spec.md) | `importRealtime=true`, nested steps, session PUT |
| [../testit-python-commons/sync-storage-interaction-spec.md](../testit-python-commons/sync-storage-interaction-spec.md) | Sync Storage protocol |
62 changes: 62 additions & 0 deletions docs/mode0-duplicate-results-bulk-after-realtime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Mode 0: duplicate test results (TP-bound + orphan) — Python adapter

**Related:** [mode0-orphan-testpoint-inprogress.md](./mode0-orphan-testpoint-inprogress.md), [test-result-export-contract.md](./test-result-export-contract.md)
**Typical setup:** pytest + Sync Storage, `adapterMode=0`, fixed `testRunId`, `importRealtime=false` (default)

Cross-adapter parity: Java [PR #278](https://github.com/testit-tms/adapters-java/pull/278).

---

## Symptom (historical bug)

One autotest from a test plan produced **two** Passed rows in the same run for the same `externalId`:

| Row | `testPointId` | Origin |
|-----|---------------|--------|
| Older | Real plan point UUID | Created by TMS when the run started |
| Newer | Missing / `00000000-…` | **Second** `POST …/testRuns/{id}/test-results` from the adapter |

Root cause: the adapter finalized the test twice — once at **test finish**, again at **run finish** (bulk).

---

## Current behaviour (fixed)

**Rule:** final status goes only through **`sendTestResults`**.
**PUT** is **not** used to finalize; see [test-result-export-contract.md](./test-result-export-contract.md).

### End of test — Sync Storage master (`importRealtime=false`)

When Sync Storage accepts the cut (`on_master_no_already_in_progress` → true):

1. Cut model to Sync Storage (coordination only).
2. `_write_test_realtime_internal` → autotest update + **`sendTestResults`** with final status and full steps.
3. Store `externalId → resultId` in `AdapterManager.__test_result_map`.

### End of run — bulk (`write_tests_after_all`)

- If `externalId` is already in `__test_result_map` → **skip** `sendTestResults`; refresh autotest metadata if needed.
- Otherwise → bulk `sendTestResults` once (Sync Storage off, or test not sent at finish).

---

## Flow (mode 0 + Sync Storage + `importRealtime=false`)

```text
Plan start → TMS creates TP-bound InProgress
stopTestCase → SyncStorage cut + sendTestResults (Passed/Failed + full payload)
sessionfinish → bulk skips sendTestResults for tests already in __test_result_map
```

Expected: **one** finalized result row per autotest in the run, with steps from the create payload.

---

## Regression checklist

1. Mode 0, one autotest, Sync Storage on, `importRealtime=false`.
2. Log: `Finalized test result via sendTestResults` at test end.
3. Log: `Bulk import: skip sendTestResults …` at run end.
4. `testResults/search`: one hit per `externalId`.

**Bad signs:** two Passed rows for the same `externalId`; bulk `sendTestResults` without skip for an already finalized test.
90 changes: 31 additions & 59 deletions docs/mode0-orphan-testpoint-inprogress.md
Original file line number Diff line number Diff line change
@@ -1,89 +1,61 @@
# Mode 0: InProgress matching (orphan TP + parametrized)
# Mode 0: InProgress slots and test plan runs

How the Python adapter finds and **updates** an existing InProgress test result
instead of creating a new one when `adapterMode=0` (test plan / webhook / filter).
How the Python adapter exports results when `adapterMode=0` (test plan / webhook / filter) and TMS already created **InProgress** rows bound to test points.

Related: Java/sync-storage contract in
`adapters-java/docs/mode0-orphan-testpoint-inprogress.md`.
**Related:** [test-result-export-contract.md](./test-result-export-contract.md), Java `adapters-java/docs/mode0-orphan-testpoint-inprogress.md`.

---

## Problem 1 — orphan InProgress (no `testPointId`)
## TMS context

With `adapterMode=0`, TMS already creates InProgress results **bound to a test point**
(`testPointId` set).
With `adapterMode=0`, the test run is created from a plan. TMS pre-creates one **InProgress** result per test point (`testPointId` set).

Previously the Python adapter (after sync-storage accepted the cut) forced
`outcome=InProgress` and called create (`setAutoTestResultsForTestRun`) **without**
`testPointId`. That produced a second row (orphan). The TP-bound result stayed
InProgress forever; the final status landed on the orphan.

**Fix:** keep the final outcome after sync; before create, search InProgress by
`autotest_external_id`, prefer a result with a valid `testPointId`, then **PUT**
that result instead of posting a new one.
The adapter must **finalize** those rows with the real outcome and full payload (steps, parameters, …), not leave them stuck InProgress and not create orphan duplicates.

---

## Problem 2 — parametrized pytest leaves many InProgress forever

### What happens in practice
## Correct behaviour (current)

1. A test plan / filter creates many InProgress rows (one per test point).
2. The same autotest is parametrized (`@pytest.mark.parametrize`). Iterations often
share one `external_id` (name template without `{param}`).
3. Work items / test points frequently have **empty parameters**, while the adapter
sends **callspec parameters** on create.
4. The first matching logic used only `external_id`. The first TP-bound InProgress
was always chosen (or create ran when params on TMS did not “fit” create semantics).
5. Result: one empty TP might get updated once; other iterations **POST new results**
with parameters; remaining empty InProgress rows stay InProgress forever.
**Finalization uses `sendTestResults` only** (`POST /adapters/testRuns/{id}/test-results`).

Root cause: mismatch between WI parameters and
autotest/result parameters, plus search that ignores parameters.
- Applies even when an InProgress result already exists for the autotest.
- TMS merges the create payload into the plan-bound row (external id + configuration + parameters).
- The adapter **does not** search InProgress and **does not** PUT final status (removed workaround).

### Why short search is not enough
After Sync Storage accepts the cut, the adapter keeps the **final** outcome and calls `_write_test_realtime_internal` → `sendTestResults`.

`TestResultShortResponse` (search) has **no `parameters`** / `testPointId`.
Full detail comes from `GET /adapters/testResults/{id}` (`TestResultResponse`).
See [test-result-export-contract.md](./test-result-export-contract.md) and [mode0-duplicate-results-bulk-after-realtime.md](./mode0-duplicate-results-bulk-after-realtime.md).

---

## Solution (parameter-aware pick + claim)
## Historical issues (for context)

### Orphan without `testPointId`

Older behaviour: POST create without matching the plan row → second result (orphan), plan row stayed InProgress.

| File | Role |
|------|------|
| `client/helpers/test_result_matching.py` | Pure ranking: exact params > empty TMS params; prefer valid TP |
| `client/api_client.py` | `find_in_progress_test_result_id(external_id, parameters)`; claim chosen ids; `__load_test_result` passes `get_parameters()` |
| `services/adapter_manager.py` | After sync: keep **final** outcome; realtime write without forcing InProgress |
**Fix:** finalize via `sendTestResults`; TMS updates the existing slot instead of requiring a separate PUT-by-id path.

### Matching rules
### Duplicate at bulk (importRealtime=false)

For candidates with the same `external_id` (and not yet claimed in this process):
Test finalized at test finish via `sendTestResults`, then bulk at session end sent **again** → orphan duplicate.

1. **Exact parameter match** (normalized string key/value) — best.
2. Else **empty / missing parameters on TMS** — fallback for WI without params
(typical test-plan case).
3. Else **skip** if TMS has non-empty parameters that differ from the incoming ones
(do not overwrite the wrong callspec’s TP).
4. Among equal match quality, prefer a valid `testPointId`.
5. **Claim** the chosen result id so the next parametrize iteration does not reuse
the same empty TP.
**Fix:** skip bulk `sendTestResults` when `externalId` is already in `AdapterManager.__test_result_map`.

If nothing matches → create (previous behaviour for genuinely new results).
### Parametrized tests and many InProgress rows

Non-parametrized tests (`parameters` empty/`None`) still pick the empty-params
TP-bound InProgress first — same as the original orphan fix.
When several TPs share the same `external_id` with different (or empty) parameters, TMS matching on create uses the payload `parameters`. Adapter sends callspec parameters on `sendTestResults`; each iteration should get its own finalized row without leaving unrelated TPs stuck (TMS-side matching).

Helper `client/helpers/test_result_matching.py` remains for unit tests / future client-side ranking if needed; **`__load_test_result` no longer uses it**.

---

### What we do **not** change
## What PUT is still used for

- Adapters PUT body still cannot set `parameters` (OpenAPI PUT has no field) —
we complete the existing TP-bound row (outcome / duration / message / trace).
- Cloud-side “better” filling of WI params remains a TMS concern; the adapter only
stops creating endless orphans and stuck InProgress.
**Only** after session with `importRealtime=true`: attach pytest fixture **setup/teardown** steps to an already finalized result id. No status, no `stepResults`. See [test-result-export-contract.md](./test-result-export-contract.md).

---

## Note

Sync-storage Work X PUT fix still requires a published binary newer than
`v0.3.7-tms-5.7` for nested-step finalize issues unrelated to this matching.
Sync Storage Work X finalize may still affect nested steps on the **first** held test independently of this export contract; see [realtime-import-spec.md](../testit-python-commons/realtime-import-spec.md) (Cause B).
117 changes: 117 additions & 0 deletions docs/test-result-export-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Test result export contract (Python adapter)

Canonical rules for how the Python adapter sends test results to TMS.
Aligned with Java adapters [PR #278](https://github.com/testit-tms/adapters-java/pull/278) (TMS-41083).

**Related:** [realtime-import-spec.md](../testit-python-commons/realtime-import-spec.md), [mode0-duplicate-results-bulk-after-realtime.md](./mode0-duplicate-results-bulk-after-realtime.md)

---

## API methods

| TMS operation | Adapter API | When used |
|---------------|-------------|-----------|
| **Send test results** | `POST /adapters/testRuns/{id}/test-results` (`adapters_test_runs_id_test_results_post`, Java: `sendTestResults`) | **Final** test result: status, steps, parameters, message, attachments |
| **Update test result** | `PUT /adapters/testResults/{id}` (`adapters_test_results_id_put`) | **Only** fixture `setupResults` / `teardownResults` after session (see below) |

---

## Rules (correct behaviour)

### 1. Finalization = `sendTestResults` only

- Every finalized test result (Passed, Failed, Skipped, …) is sent via **`sendTestResults`**, including when TMS already has an **InProgress** row from a test plan (`adapterMode=0` / `1`).
- TMS merges/enriches the existing InProgress slot on create; the adapter **must not** finalize via PUT.
- Full nested `stepResults` use `AttachmentPutModelAutoTestStepResultsModel` on the POST path (`Converter.test_result_to_testrun_result_post_model`).

**Entry point:** `ApiClientWorker.__load_test_result` → always POST.

Debug log:

```text
Finalized test result via sendTestResults for <externalId> (resultId=<uuid>)
```

### 2. PUT = fixture setup/teardown only (`importRealtime=true`)

After the session, when tests were already sent per-test:

1. `AdapterManager.write_tests` → `ApiClientWorker.update_test_results`
2. PUT body contains **only** `setupResults` and `teardownResults` (`Converter.convert_test_result_with_all_setup_and_teardown_steps_to_test_results_id_put_request`)
3. **Must not** include: `stepResults`, `statusCode`, `duration`, `message`, `trace`, or fields copied from GET

Rationale: GET returns flat `StepResultApiModel` references without nested children. Re-sending them in PUT would **overwrite** the full step tree written during POST.

Unit test: `tests/client/test_converter_update_test_results.py`.

### 3. Never use PUT to change final status

- Do not PUT `statusCode` / outcome to finalize a test.
- Do not implement `findInProgress` → PUT finalize (removed; was a workaround before TMS/create merge was reliable).

### 4. Bulk at run end must not double-send

When `importRealtime=false` and a test was already finalized at test finish (e.g. Sync Storage master path stores `externalId → resultId` in `AdapterManager.__test_result_map`):

- `write_tests(..., finalized_external_ids=...)` **skips** `sendTestResults` for those external IDs
- Optionally refreshes autotest metadata only

Info log:

```text
Bulk import: skip sendTestResults for <externalId> (already finalized at test finish)
```

See [mode0-duplicate-results-bulk-after-realtime.md](./mode0-duplicate-results-bulk-after-realtime.md).

---

## Flow summary

### `importRealtime=true` (per-test upload)

```text
pytest_runtest_logfinish
→ write_test → write_test (autotest) + __load_test_result (sendTestResults)

pytest_sessionfinish
→ update_test_results → PUT setup/teardown only (no stepResults, no status)
```

### `importRealtime=false` (bulk at end)

```text
pytest_runtest_logfinish → buffer in AdapterManager.__test_results
(Sync Storage master: sendTestResults immediately + store in __test_result_map)

pytest_sessionfinish
→ write_tests → sendTestResults for buffered tests
→ skip sendTestResults for keys already in __test_result_map
```

---

## Affected code

| File | Role |
|------|------|
| `client/api_client.py` | `__load_test_result` (POST), `update_test_results` (PUT fixtures), `write_tests` (bulk + skip) |
| `client/converter.py` | `test_result_to_testrun_result_post_model`, `convert_test_result_with_all_setup_and_teardown_steps_to_test_results_id_put_request` |
| `services/adapter_manager.py` | `__test_result_map`, `write_tests` / `__write_tests_after_all` |

---

## Regression checklist

**Good:**

- Log `Finalized test result via sendTestResults` at test finish
- With `importRealtime=true`: nested steps still on test result after session (PUT did not touch `stepResults`)
- With bulk + Sync Storage: log `Bulk import: skip sendTestResults` for tests finalized at test finish
- One finalized row per autotest per run (no TP + orphan duplicate)

**Bad (bug is back):**

- `Updated existing test result` / PUT with final status at test finish
- PUT body includes `stepResults` from GET in `update_test_results`
- Second `sendTestResults` for the same test in one run without skip
2 changes: 1 addition & 1 deletion testit-adapter-behave/setup.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from setuptools import find_packages, setup

VERSION = "5.1.4"
VERSION = "5.1.5"

setup(
name='testit-adapter-behave',
Expand Down
2 changes: 1 addition & 1 deletion testit-adapter-nose/setup.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from setuptools import setup, find_packages

VERSION = "5.1.4"
VERSION = "5.1.5"

setup(
name='testit-adapter-nose',
Expand Down
Loading
Loading