robotframework-testselection 0.1.0__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.
- TestSelection/__init__.py +3 -0
- TestSelection/cli.py +256 -0
- TestSelection/embedding/__init__.py +1 -0
- TestSelection/embedding/embedder.py +43 -0
- TestSelection/embedding/models.py +198 -0
- TestSelection/embedding/ports.py +24 -0
- TestSelection/execution/__init__.py +1 -0
- TestSelection/execution/listener.py +44 -0
- TestSelection/execution/prerun_modifier.py +43 -0
- TestSelection/execution/runner.py +75 -0
- TestSelection/parsing/__init__.py +1 -0
- TestSelection/parsing/datadriver_reader.py +54 -0
- TestSelection/parsing/keyword_resolver.py +51 -0
- TestSelection/parsing/suite_collector.py +85 -0
- TestSelection/parsing/text_builder.py +79 -0
- TestSelection/pipeline/__init__.py +1 -0
- TestSelection/pipeline/artifacts.py +110 -0
- TestSelection/pipeline/cache.py +74 -0
- TestSelection/pipeline/errors.py +18 -0
- TestSelection/pipeline/execute.py +52 -0
- TestSelection/pipeline/select.py +183 -0
- TestSelection/pipeline/vectorize.py +190 -0
- TestSelection/py.typed +0 -0
- TestSelection/selection/__init__.py +25 -0
- TestSelection/selection/dpp.py +31 -0
- TestSelection/selection/facility.py +25 -0
- TestSelection/selection/filtering.py +21 -0
- TestSelection/selection/fps.py +67 -0
- TestSelection/selection/kmedoids.py +32 -0
- TestSelection/selection/registry.py +70 -0
- TestSelection/selection/strategy.py +142 -0
- TestSelection/shared/__init__.py +1 -0
- TestSelection/shared/config.py +31 -0
- TestSelection/shared/types.py +117 -0
- robotframework_testselection-0.1.0.dist-info/METADATA +408 -0
- robotframework_testselection-0.1.0.dist-info/RECORD +39 -0
- robotframework_testselection-0.1.0.dist-info/WHEEL +4 -0
- robotframework_testselection-0.1.0.dist-info/entry_points.txt +2 -0
- robotframework_testselection-0.1.0.dist-info/licenses/LICENSE +191 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Selection strategy protocol and domain value objects."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Protocol, runtime_checkable
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from numpy.typing import NDArray
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@runtime_checkable
|
|
14
|
+
class SelectionStrategy(Protocol):
|
|
15
|
+
"""Protocol for diversity selection algorithms.
|
|
16
|
+
|
|
17
|
+
All implementations take a (N, d) embedding matrix and return
|
|
18
|
+
k indices into that matrix.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def name(self) -> str: ...
|
|
23
|
+
|
|
24
|
+
def select(
|
|
25
|
+
self, vectors: NDArray[np.float32], k: int, seed: int = 42
|
|
26
|
+
) -> list[int]: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class SelectedTest:
|
|
31
|
+
"""A test case that was selected by the diversity algorithm."""
|
|
32
|
+
|
|
33
|
+
name: str
|
|
34
|
+
id: str
|
|
35
|
+
suite: str
|
|
36
|
+
is_datadriver: bool
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class DiversityMetrics:
|
|
41
|
+
"""Statistics describing the diversity of a selected test subset."""
|
|
42
|
+
|
|
43
|
+
avg_pairwise_distance: float
|
|
44
|
+
min_pairwise_distance: float
|
|
45
|
+
suite_coverage: int
|
|
46
|
+
suite_total: int
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def suite_coverage_ratio(self) -> float:
|
|
50
|
+
if self.suite_total == 0:
|
|
51
|
+
return 0.0
|
|
52
|
+
return self.suite_coverage / self.suite_total
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class TagFilter:
|
|
57
|
+
"""Pre-filter criteria applied before diversity selection."""
|
|
58
|
+
|
|
59
|
+
include_tags: frozenset[str] = frozenset()
|
|
60
|
+
exclude_tags: frozenset[str] = frozenset()
|
|
61
|
+
include_datadriver: bool = True
|
|
62
|
+
|
|
63
|
+
def matches(self, tags: frozenset[str], is_datadriver: bool) -> bool:
|
|
64
|
+
if not self.include_datadriver and is_datadriver:
|
|
65
|
+
return False
|
|
66
|
+
normalized_tags = frozenset(t.lower() for t in tags)
|
|
67
|
+
if self.include_tags and not (normalized_tags & self.include_tags):
|
|
68
|
+
return False
|
|
69
|
+
return not (self.exclude_tags and (normalized_tags & self.exclude_tags))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class SelectionResult:
|
|
74
|
+
"""Aggregate root: the output of the selection pipeline stage."""
|
|
75
|
+
|
|
76
|
+
strategy: str
|
|
77
|
+
k: int
|
|
78
|
+
seed: int
|
|
79
|
+
total_tests: int
|
|
80
|
+
filtered_tests: int
|
|
81
|
+
selected: tuple[SelectedTest, ...]
|
|
82
|
+
diversity_metrics: DiversityMetrics
|
|
83
|
+
|
|
84
|
+
def to_json(self, path: Path) -> None:
|
|
85
|
+
data = {
|
|
86
|
+
"strategy": self.strategy,
|
|
87
|
+
"k": self.k,
|
|
88
|
+
"seed": self.seed,
|
|
89
|
+
"total_tests": self.total_tests,
|
|
90
|
+
"filtered_tests": self.filtered_tests,
|
|
91
|
+
"selected": [
|
|
92
|
+
{
|
|
93
|
+
"name": t.name,
|
|
94
|
+
"id": t.id,
|
|
95
|
+
"suite": t.suite,
|
|
96
|
+
"is_datadriver": t.is_datadriver,
|
|
97
|
+
}
|
|
98
|
+
for t in self.selected
|
|
99
|
+
],
|
|
100
|
+
"diversity_metrics": {
|
|
101
|
+
"avg_pairwise_distance": self.diversity_metrics.avg_pairwise_distance,
|
|
102
|
+
"min_pairwise_distance": self.diversity_metrics.min_pairwise_distance,
|
|
103
|
+
"suite_coverage": self.diversity_metrics.suite_coverage,
|
|
104
|
+
"suite_total": self.diversity_metrics.suite_total,
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
path.write_text(json.dumps(data, indent=2))
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def from_json(cls, path: Path) -> SelectionResult:
|
|
111
|
+
data = json.loads(path.read_text())
|
|
112
|
+
selected = tuple(
|
|
113
|
+
SelectedTest(
|
|
114
|
+
name=t["name"],
|
|
115
|
+
id=t["id"],
|
|
116
|
+
suite=t.get("suite", ""),
|
|
117
|
+
is_datadriver=t.get("is_datadriver", False),
|
|
118
|
+
)
|
|
119
|
+
for t in data["selected"]
|
|
120
|
+
)
|
|
121
|
+
return cls(
|
|
122
|
+
strategy=data["strategy"],
|
|
123
|
+
k=data["k"],
|
|
124
|
+
seed=data.get("seed", 42),
|
|
125
|
+
total_tests=data["total_tests"],
|
|
126
|
+
filtered_tests=data.get("filtered_tests", data["total_tests"]),
|
|
127
|
+
selected=selected,
|
|
128
|
+
diversity_metrics=DiversityMetrics(
|
|
129
|
+
avg_pairwise_distance=data.get("diversity_metrics", {}).get(
|
|
130
|
+
"avg_pairwise_distance", 0.0
|
|
131
|
+
),
|
|
132
|
+
min_pairwise_distance=data.get("diversity_metrics", {}).get(
|
|
133
|
+
"min_pairwise_distance", 0.0
|
|
134
|
+
),
|
|
135
|
+
suite_coverage=data.get("diversity_metrics", {}).get(
|
|
136
|
+
"suite_coverage", 0
|
|
137
|
+
),
|
|
138
|
+
suite_total=data.get("diversity_metrics", {}).get(
|
|
139
|
+
"suite_total", 0
|
|
140
|
+
),
|
|
141
|
+
),
|
|
142
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Shared kernel: value objects, types, and configuration."""
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from TestSelection.shared.types import NOISE_PREFIXES
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class PipelineConfig:
|
|
11
|
+
"""Configuration for a complete pipeline run."""
|
|
12
|
+
|
|
13
|
+
model_name: str = "all-MiniLM-L6-v2"
|
|
14
|
+
resolve_depth: int = 0
|
|
15
|
+
k: int = 50
|
|
16
|
+
strategy: str = "fps"
|
|
17
|
+
seed: int = 42
|
|
18
|
+
output_dir: Path = Path("./results")
|
|
19
|
+
force_reindex: bool = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class TextBuilderConfig:
|
|
24
|
+
"""Configuration for TextRepresentationBuilder."""
|
|
25
|
+
|
|
26
|
+
resolve_depth: int = 0
|
|
27
|
+
include_tags: bool = True
|
|
28
|
+
include_suite_name: bool = False
|
|
29
|
+
noise_prefixes: frozenset[str] = field(
|
|
30
|
+
default_factory=lambda: frozenset(NOISE_PREFIXES)
|
|
31
|
+
)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class TestCaseId:
|
|
10
|
+
"""Stable identity for a test case, derived from source file + name."""
|
|
11
|
+
|
|
12
|
+
value: str
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def from_source_and_name(cls, source: str, name: str) -> TestCaseId:
|
|
16
|
+
raw = f"{source}::{name}"
|
|
17
|
+
return cls(value=hashlib.md5(raw.encode()).hexdigest())
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class Tag:
|
|
22
|
+
"""A Robot Framework test tag, normalized for comparison."""
|
|
23
|
+
|
|
24
|
+
value: str
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def normalized(self) -> str:
|
|
28
|
+
return self.value.lower().strip()
|
|
29
|
+
|
|
30
|
+
def __eq__(self, other: object) -> bool:
|
|
31
|
+
if isinstance(other, Tag):
|
|
32
|
+
return self.normalized == other.normalized
|
|
33
|
+
return NotImplemented
|
|
34
|
+
|
|
35
|
+
def __hash__(self) -> int:
|
|
36
|
+
return hash(self.normalized)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class SuitePath:
|
|
41
|
+
"""Path to a .robot file or test directory."""
|
|
42
|
+
|
|
43
|
+
value: Path
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def is_directory(self) -> bool:
|
|
47
|
+
return self.value.is_dir()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class FileHash:
|
|
52
|
+
"""Content hash of a .robot file for change detection."""
|
|
53
|
+
|
|
54
|
+
path: str
|
|
55
|
+
md5: str
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class TextRepresentation:
|
|
60
|
+
"""Embeddable text representation of a test case.
|
|
61
|
+
|
|
62
|
+
Built from test name, tags, keyword names, and semantic arguments.
|
|
63
|
+
Excludes DOM locators, variable placeholders, and XPaths.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
text: str
|
|
67
|
+
resolve_depth: int = 0
|
|
68
|
+
includes_tags: bool = True
|
|
69
|
+
includes_keyword_args: bool = True
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
NOISE_PREFIXES = ("id:", "css:", "xpath:", "//", "${", "@{", "%{", "&{")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class KeywordTree:
|
|
77
|
+
"""Resolved keyword call tree for a single keyword invocation."""
|
|
78
|
+
|
|
79
|
+
keyword_name: str
|
|
80
|
+
args: tuple[str, ...]
|
|
81
|
+
children: tuple[KeywordTree, ...] = ()
|
|
82
|
+
|
|
83
|
+
def flatten(self) -> str:
|
|
84
|
+
"""Convert to natural language, filtering noise arguments."""
|
|
85
|
+
kw = self.keyword_name.replace("_", " ")
|
|
86
|
+
semantic_args = [
|
|
87
|
+
a
|
|
88
|
+
for a in self.args
|
|
89
|
+
if not any(a.startswith(p) for p in NOISE_PREFIXES)
|
|
90
|
+
]
|
|
91
|
+
text = kw
|
|
92
|
+
if semantic_args:
|
|
93
|
+
text += f" with {', '.join(semantic_args)}"
|
|
94
|
+
children_text = " ".join(c.flatten() for c in self.children)
|
|
95
|
+
return f"{text} {children_text}".strip()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class UserKeywordRef:
|
|
100
|
+
"""Domain representation of a user keyword from robot.api."""
|
|
101
|
+
|
|
102
|
+
name: str
|
|
103
|
+
normalized_name: str
|
|
104
|
+
body_items: tuple
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class TestCaseRecord:
|
|
109
|
+
"""Domain entity representing a parsed test case ready for embedding."""
|
|
110
|
+
|
|
111
|
+
test_id: TestCaseId
|
|
112
|
+
name: str
|
|
113
|
+
tags: frozenset[Tag]
|
|
114
|
+
suite_source: SuitePath
|
|
115
|
+
suite_name: str
|
|
116
|
+
text_representation: TextRepresentation
|
|
117
|
+
is_datadriver: bool = False
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: robotframework-testselection
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Vector-based diverse test case selection for Robot Framework
|
|
5
|
+
Project-URL: Homepage, https://github.com/manykarim/robotframework-testselection
|
|
6
|
+
Project-URL: Documentation, https://github.com/manykarim/robotframework-testselection#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/manykarim/robotframework-testselection
|
|
8
|
+
Project-URL: Issues, https://github.com/manykarim/robotframework-testselection/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/manykarim/robotframework-testselection/blob/main/CHANGELOG.md
|
|
10
|
+
Author: Many Kasiriha
|
|
11
|
+
License: Apache-2.0
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: diversity,embeddings,machine-learning,nlp,robotframework,test-selection,testing
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Framework :: Robot Framework
|
|
16
|
+
Classifier: Framework :: Robot Framework :: Library
|
|
17
|
+
Classifier: Framework :: Robot Framework :: Tool
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
27
|
+
Classifier: Topic :: Software Development :: Testing
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Requires-Dist: numpy>=1.24
|
|
30
|
+
Requires-Dist: robotframework>=7.0
|
|
31
|
+
Requires-Dist: scikit-learn>=1.3
|
|
32
|
+
Provides-Extra: all
|
|
33
|
+
Requires-Dist: apricot-select>=0.6; extra == 'all'
|
|
34
|
+
Requires-Dist: chromadb>=0.4; extra == 'all'
|
|
35
|
+
Requires-Dist: dppy>=0.3; extra == 'all'
|
|
36
|
+
Requires-Dist: mypy; extra == 'all'
|
|
37
|
+
Requires-Dist: pytest-benchmark; extra == 'all'
|
|
38
|
+
Requires-Dist: pytest-cov; extra == 'all'
|
|
39
|
+
Requires-Dist: pytest>=8.0; extra == 'all'
|
|
40
|
+
Requires-Dist: ruff; extra == 'all'
|
|
41
|
+
Requires-Dist: scikit-learn-extra>=0.3; extra == 'all'
|
|
42
|
+
Requires-Dist: sentence-transformers>=2.2; extra == 'all'
|
|
43
|
+
Provides-Extra: chromadb
|
|
44
|
+
Requires-Dist: chromadb>=0.4; extra == 'chromadb'
|
|
45
|
+
Provides-Extra: dev
|
|
46
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
47
|
+
Requires-Dist: pytest-benchmark; extra == 'dev'
|
|
48
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
49
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
50
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
51
|
+
Provides-Extra: selection-extras
|
|
52
|
+
Requires-Dist: apricot-select>=0.6; extra == 'selection-extras'
|
|
53
|
+
Requires-Dist: dppy>=0.3; extra == 'selection-extras'
|
|
54
|
+
Requires-Dist: scikit-learn-extra>=0.3; extra == 'selection-extras'
|
|
55
|
+
Provides-Extra: vectorize
|
|
56
|
+
Requires-Dist: sentence-transformers>=2.2; extra == 'vectorize'
|
|
57
|
+
Description-Content-Type: text/markdown
|
|
58
|
+
|
|
59
|
+
# robotframework-testselection
|
|
60
|
+
|
|
61
|
+
Vector-based diverse test case selection for Robot Framework. Embeds test cases as semantic vectors and selects maximally diverse subsets to reduce test suite execution time while preserving coverage breadth.
|
|
62
|
+
|
|
63
|
+
## How It Works
|
|
64
|
+
|
|
65
|
+
The system operates as a **3-stage pipeline**:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Vectorize Select Execute
|
|
69
|
+
(.robot files) ──► (embeddings.npz ──► (selected_tests.json
|
|
70
|
+
test_manifest.json) + robot --prerunmodifier)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
1. **Vectorize** — Parses `.robot` files via the Robot Framework API, converts each test case to a natural-language text representation (name + tags + resolved keyword tree), then encodes with `all-MiniLM-L6-v2` (384-dim sentence embeddings).
|
|
74
|
+
2. **Select** — Loads embedding vectors and applies a diversity-maximizing selection algorithm (default: Farthest Point Sampling) to choose *k* tests that are as semantically different from each other as possible.
|
|
75
|
+
3. **Execute** — Runs the selected tests via Robot Framework using a `PreRunModifier` (for standard tests) and a `Listener v3` (for DataDriver-generated tests).
|
|
76
|
+
|
|
77
|
+
If any stage fails, the pipeline **gracefully degrades** by running all tests (exit code 2).
|
|
78
|
+
|
|
79
|
+
## Installation
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# From PyPI
|
|
83
|
+
pip install robotframework-testselection
|
|
84
|
+
|
|
85
|
+
# With sentence-transformers for vectorization
|
|
86
|
+
pip install robotframework-testselection[vectorize]
|
|
87
|
+
|
|
88
|
+
# With all optional selection algorithms
|
|
89
|
+
pip install robotframework-testselection[selection-extras]
|
|
90
|
+
|
|
91
|
+
# Everything
|
|
92
|
+
pip install robotframework-testselection[all]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Development Install (from source)
|
|
96
|
+
|
|
97
|
+
Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
uv sync --extra vectorize
|
|
101
|
+
uv sync --extra all
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Dependency Groups
|
|
105
|
+
|
|
106
|
+
| Group | Packages | Purpose |
|
|
107
|
+
|-------|----------|---------|
|
|
108
|
+
| *(base)* | `robotframework`, `numpy`, `scikit-learn` | Core pipeline |
|
|
109
|
+
| `vectorize` | `sentence-transformers` | Embedding model (Stage 1) |
|
|
110
|
+
| `selection-extras` | `scikit-learn-extra`, `dppy`, `apricot-select` | k-Medoids, DPP, Facility Location strategies |
|
|
111
|
+
| `chromadb` | `chromadb` | Alternative vector storage |
|
|
112
|
+
| `dev` | `pytest`, `pytest-cov`, `pytest-benchmark`, `ruff`, `mypy` | Development |
|
|
113
|
+
|
|
114
|
+
## Quick Start
|
|
115
|
+
|
|
116
|
+
### Full Pipeline (one command)
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
testcase-select run \
|
|
120
|
+
--suite tests/robot/ \
|
|
121
|
+
--k 20 \
|
|
122
|
+
--strategy fps \
|
|
123
|
+
--seed 42 \
|
|
124
|
+
--output-dir ./results/
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Stage-by-Stage
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# Stage 1: Vectorize
|
|
131
|
+
testcase-select vectorize \
|
|
132
|
+
--suite tests/robot/ \
|
|
133
|
+
--output ./artifacts/ \
|
|
134
|
+
--model all-MiniLM-L6-v2 \
|
|
135
|
+
--resolve-depth 2
|
|
136
|
+
|
|
137
|
+
# Stage 2: Select
|
|
138
|
+
testcase-select select \
|
|
139
|
+
--artifacts ./artifacts/ \
|
|
140
|
+
--k 20 \
|
|
141
|
+
--strategy fps \
|
|
142
|
+
--seed 42 \
|
|
143
|
+
--output selected_tests.json
|
|
144
|
+
|
|
145
|
+
# Stage 3: Execute
|
|
146
|
+
testcase-select execute \
|
|
147
|
+
--suite tests/robot/ \
|
|
148
|
+
--selection selected_tests.json \
|
|
149
|
+
--output-dir ./results/
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### With DataDriver CSV Files
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
testcase-select vectorize \
|
|
156
|
+
--suite tests/robot/ \
|
|
157
|
+
--output ./artifacts/ \
|
|
158
|
+
--datadriver-csv tests/data/login.csv tests/data/search.csv
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Direct Robot Framework Integration
|
|
162
|
+
|
|
163
|
+
You can also use the components directly with `robot`:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# PreRunModifier for standard tests
|
|
167
|
+
robot --prerunmodifier TestSelection.execution.prerun_modifier.DiversePreRunModifier:selected_tests.json tests/
|
|
168
|
+
|
|
169
|
+
# Listener v3 for DataDriver tests
|
|
170
|
+
robot --listener TestSelection.execution.listener.DiverseDataDriverListener:selected_tests.json tests/
|
|
171
|
+
|
|
172
|
+
# Both together
|
|
173
|
+
robot \
|
|
174
|
+
--prerunmodifier TestSelection.execution.prerun_modifier.DiversePreRunModifier:selected_tests.json \
|
|
175
|
+
--listener TestSelection.execution.listener.DiverseDataDriverListener:selected_tests.json \
|
|
176
|
+
tests/
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Environment Variables
|
|
180
|
+
|
|
181
|
+
| Variable | Default | Description |
|
|
182
|
+
|----------|---------|-------------|
|
|
183
|
+
| `DIVERSE_K` | `50` | Number of tests to select |
|
|
184
|
+
| `DIVERSE_STRATEGY` | `fps` | Selection algorithm |
|
|
185
|
+
| `DIVERSE_SEED` | `42` | Random seed for reproducibility |
|
|
186
|
+
| `DIVERSE_OUTPUT` | *(none)* | Output file path for selection JSON |
|
|
187
|
+
|
|
188
|
+
## Selection Strategies
|
|
189
|
+
|
|
190
|
+
| Strategy | Name | Dependencies | Description |
|
|
191
|
+
|----------|------|-------------|-------------|
|
|
192
|
+
| **FPS** | `fps` | *(base)* | Farthest Point Sampling. Greedy farthest-first traversal. O(N*k*d). 2-approximation guarantee for max-min dispersion. **Default.** |
|
|
193
|
+
| **FPS Multi-Start** | `fps_multi` | *(base)* | Runs FPS from multiple random starting points, keeps the result with the highest minimum pairwise distance. Mitigates initial-point sensitivity. |
|
|
194
|
+
| **k-Medoids** | `kmedoids` | `selection-extras` | PAM algorithm for medoid-based clustering. Better centroid representativeness. |
|
|
195
|
+
| **DPP** | `dpp` | `selection-extras` | Determinantal Point Process. Probabilistic repulsion-based sampling. |
|
|
196
|
+
| **Facility Location** | `facility` | `selection-extras` | Submodular facility location maximization. Optimizes for both diversity and representativeness. |
|
|
197
|
+
|
|
198
|
+
## Tag Filtering
|
|
199
|
+
|
|
200
|
+
Filter tests by tags before selection:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
# Include only smoke and regression tests
|
|
204
|
+
testcase-select select \
|
|
205
|
+
--artifacts ./artifacts/ \
|
|
206
|
+
--k 20 \
|
|
207
|
+
--include-tags smoke regression
|
|
208
|
+
|
|
209
|
+
# Exclude slow tests
|
|
210
|
+
testcase-select select \
|
|
211
|
+
--artifacts ./artifacts/ \
|
|
212
|
+
--k 20 \
|
|
213
|
+
--exclude-tags slow manual
|
|
214
|
+
|
|
215
|
+
# Exclude DataDriver tests
|
|
216
|
+
testcase-select select \
|
|
217
|
+
--artifacts ./artifacts/ \
|
|
218
|
+
--k 20 \
|
|
219
|
+
--no-datadriver
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Caching
|
|
223
|
+
|
|
224
|
+
Stage 1 uses **content-hash caching**. It computes MD5 hashes of all `.robot` and `.csv` files and stores them alongside artifacts. On subsequent runs, vectorization is skipped when no source files have changed. Use `--force` to bypass the cache:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
testcase-select vectorize --suite tests/ --output ./artifacts/ --force
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Artifacts
|
|
231
|
+
|
|
232
|
+
The pipeline produces these artifacts:
|
|
233
|
+
|
|
234
|
+
| File | Stage | Format | Contents |
|
|
235
|
+
|------|-------|--------|----------|
|
|
236
|
+
| `embeddings.npz` | Vectorize | NumPy compressed | N x 384 float32 matrix |
|
|
237
|
+
| `test_manifest.json` | Vectorize | JSON | Test metadata (names, tags, suites, IDs) |
|
|
238
|
+
| `file_hashes.json` | Vectorize | JSON | Source file MD5 hashes for cache invalidation |
|
|
239
|
+
| `selected_tests.json` | Select | JSON | Selected test list + diversity metrics |
|
|
240
|
+
|
|
241
|
+
### Selection Output Format
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"strategy": "fps",
|
|
246
|
+
"k": 20,
|
|
247
|
+
"seed": 42,
|
|
248
|
+
"total_tests": 150,
|
|
249
|
+
"filtered_tests": 120,
|
|
250
|
+
"selected": [
|
|
251
|
+
{
|
|
252
|
+
"name": "Login With Valid Credentials",
|
|
253
|
+
"id": "a1b2c3d4...",
|
|
254
|
+
"suite": "tests/login.robot",
|
|
255
|
+
"is_datadriver": false
|
|
256
|
+
}
|
|
257
|
+
],
|
|
258
|
+
"diversity_metrics": {
|
|
259
|
+
"avg_pairwise_distance": 0.8234,
|
|
260
|
+
"min_pairwise_distance": 0.4512,
|
|
261
|
+
"suite_coverage": 8,
|
|
262
|
+
"suite_total": 10
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## CI/CD Integration
|
|
268
|
+
|
|
269
|
+
Pre-built configurations are provided in `config/`:
|
|
270
|
+
|
|
271
|
+
### GitHub Actions
|
|
272
|
+
|
|
273
|
+
```yaml
|
|
274
|
+
# .github/workflows/diverse-tests.yml
|
|
275
|
+
# Copy from config/github-actions.yml
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Features:
|
|
279
|
+
- 3-job pipeline with artifact transfer between stages
|
|
280
|
+
- Content-hash caching (`actions/cache@v4`) to skip vectorization
|
|
281
|
+
- Automatic PR annotations with selection summary
|
|
282
|
+
- `workflow_dispatch` for manual runs with custom k/strategy
|
|
283
|
+
|
|
284
|
+
### GitLab CI
|
|
285
|
+
|
|
286
|
+
```yaml
|
|
287
|
+
# .gitlab-ci.yml
|
|
288
|
+
# Copy from config/gitlab-ci.yml
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Jenkins
|
|
292
|
+
|
|
293
|
+
```groovy
|
|
294
|
+
// Jenkinsfile
|
|
295
|
+
// Copy from config/Jenkinsfile
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Project Structure
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
src/TestSelection/
|
|
302
|
+
shared/ # Shared kernel (types, config)
|
|
303
|
+
types.py # TestCaseId, Tag, TestCaseRecord, KeywordTree, ...
|
|
304
|
+
config.py # PipelineConfig, TextBuilderConfig
|
|
305
|
+
parsing/ # Bounded context: Robot Framework parsing
|
|
306
|
+
suite_collector.py # RobotApiAdapter (TestSuite.from_file_system)
|
|
307
|
+
keyword_resolver.py# Recursive keyword tree resolution
|
|
308
|
+
text_builder.py # Natural language text representation
|
|
309
|
+
datadriver_reader.py # DataDriver CSV reader
|
|
310
|
+
embedding/ # Bounded context: vector embedding
|
|
311
|
+
ports.py # EmbeddingModel protocol
|
|
312
|
+
embedder.py # SentenceTransformerAdapter (ACL)
|
|
313
|
+
models.py # EmbeddingMatrix aggregate, ManifestEntry
|
|
314
|
+
selection/ # Bounded context: diversity selection
|
|
315
|
+
strategy.py # SelectionStrategy protocol, SelectionResult
|
|
316
|
+
fps.py # FarthestPointSampling, FPSMultiStart
|
|
317
|
+
kmedoids.py # KMedoidsSelection (optional)
|
|
318
|
+
dpp.py # DPPSelection (optional)
|
|
319
|
+
facility.py # FacilityLocationSelection (optional)
|
|
320
|
+
registry.py # StrategyRegistry with auto-discovery
|
|
321
|
+
filtering.py # Tag-based pre-selection filtering
|
|
322
|
+
execution/ # Bounded context: Robot Framework execution
|
|
323
|
+
prerun_modifier.py # DiversePreRunModifier (SuiteVisitor)
|
|
324
|
+
listener.py # DiverseDataDriverListener (Listener v3)
|
|
325
|
+
runner.py # ExecutionRunner
|
|
326
|
+
pipeline/ # Orchestration layer
|
|
327
|
+
vectorize.py # Stage 1 orchestrator
|
|
328
|
+
select.py # Stage 2 orchestrator
|
|
329
|
+
execute.py # Stage 3 orchestrator
|
|
330
|
+
cache.py # Content-hash cache invalidator
|
|
331
|
+
artifacts.py # Artifact storage and validation
|
|
332
|
+
errors.py # Domain error hierarchy
|
|
333
|
+
cli.py # CLI entry point (argparse)
|
|
334
|
+
|
|
335
|
+
tests/
|
|
336
|
+
fixtures/ # Sample .robot and .csv files
|
|
337
|
+
unit/ # Unit tests
|
|
338
|
+
integration/ # Integration tests
|
|
339
|
+
benchmarks/ # Performance benchmarks
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Development
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
# Install with dev dependencies
|
|
346
|
+
uv sync --extra dev
|
|
347
|
+
|
|
348
|
+
# Run all tests
|
|
349
|
+
uv run pytest tests/ -v --benchmark-disable
|
|
350
|
+
|
|
351
|
+
# Run unit tests only
|
|
352
|
+
uv run pytest tests/unit/ -v
|
|
353
|
+
|
|
354
|
+
# Run integration tests only
|
|
355
|
+
uv run pytest tests/integration/ -v
|
|
356
|
+
|
|
357
|
+
# Run benchmarks
|
|
358
|
+
uv run pytest tests/benchmarks/ -v
|
|
359
|
+
|
|
360
|
+
# Lint
|
|
361
|
+
uv run ruff check src/
|
|
362
|
+
|
|
363
|
+
# Type check
|
|
364
|
+
uv run mypy src/
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Test Markers
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
# Skip slow tests (ML model loading)
|
|
371
|
+
uv run pytest -m "not slow"
|
|
372
|
+
|
|
373
|
+
# Only integration tests
|
|
374
|
+
uv run pytest -m integration
|
|
375
|
+
|
|
376
|
+
# Only benchmarks
|
|
377
|
+
uv run pytest -m benchmark
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Performance
|
|
381
|
+
|
|
382
|
+
Benchmarked on 384-dimensional normalized vectors:
|
|
383
|
+
|
|
384
|
+
| N (tests) | FPS Time | FPS Multi (5 starts) |
|
|
385
|
+
|-----------|----------|---------------------|
|
|
386
|
+
| 100 | 12 ms | — |
|
|
387
|
+
| 500 | 99 ms | — |
|
|
388
|
+
| 1,000 | 304 ms | 376 ms (3 starts) |
|
|
389
|
+
| 5,000 | 8.0 s | — |
|
|
390
|
+
|
|
391
|
+
Keyword resolution: ~18 us/depth-1 resolve, ~61 us/depth-3 resolve. Text building for 100 tests: ~458 us.
|
|
392
|
+
|
|
393
|
+
## Architecture Decision Records
|
|
394
|
+
|
|
395
|
+
| ADR | Title |
|
|
396
|
+
|-----|-------|
|
|
397
|
+
| [ADR-001](docs/adr/ADR-001-pipeline-architecture.md) | 3-Stage Pipeline Architecture |
|
|
398
|
+
| [ADR-002](docs/adr/ADR-002-embedding-model-and-storage.md) | Embedding Model and Storage |
|
|
399
|
+
| [ADR-003](docs/adr/ADR-003-selection-algorithm-strategy.md) | Selection Algorithm Strategy Pattern |
|
|
400
|
+
| [ADR-004](docs/adr/ADR-004-robot-framework-integration.md) | Robot Framework Integration |
|
|
401
|
+
| [ADR-005](docs/adr/ADR-005-project-structure-and-uv.md) | Project Structure and uv |
|
|
402
|
+
| [ADR-006](docs/adr/ADR-006-text-representation-strategy.md) | Text Representation Strategy |
|
|
403
|
+
| [ADR-007](docs/adr/ADR-007-cicd-integration-and-caching.md) | CI/CD Integration and Caching |
|
|
404
|
+
| [ADR-008](docs/adr/ADR-008-reliability-and-observability.md) | Reliability and Observability |
|
|
405
|
+
|
|
406
|
+
## License
|
|
407
|
+
|
|
408
|
+
Apache 2.0
|