Skip to content
This repository was archived by the owner on Aug 7, 2024. It is now read-only.

Commit f493a69

Browse files
authored
QA-260 add check for tests without spec (#29)
1 parent 8aff21d commit f493a69

File tree

5 files changed

+82
-23
lines changed

5 files changed

+82
-23
lines changed

src/pytest_allure_spec_coverage/matcher.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"""Matcher of tests cases and scenarios"""
1414
import itertools
1515
import os
16-
from contextlib import suppress
16+
import warnings
1717
from dataclasses import dataclass, field
1818
from typing import (
1919
Callable,
@@ -28,7 +28,6 @@
2828
Tuple,
2929
Type,
3030
)
31-
3231
import pytest
3332
from _pytest.config import ExitCode
3433
from _pytest.main import Session
@@ -46,6 +45,10 @@
4645
from .xdist_shared import XdistSharedStorage
4746

4847

48+
class SpecCollectorWarning(UserWarning):
49+
"""Warn for spec collector issues"""
50+
51+
4952
def is_xdist_first_worker():
5053
"""True if running on first xdist worker"""
5154
return os.getenv("PYTEST_XDIST_WORKER") == "gw0"
@@ -183,6 +186,7 @@ class ScenariosMatcher:
183186
storage: Optional[XdistSharedStorage]
184187

185188
scenarios: Collection[Scenario] = field(default_factory=list)
189+
nonexistent: [PytestItems] = field(default_factory=list)
186190
matches: Mapping[Scenario, PytestItems] = field(default_factory=dict)
187191

188192
@property
@@ -210,15 +214,17 @@ def spec_coverage_percent(self):
210214
"""Coverage percent"""
211215
return int((len(self.scenarios) - len(tuple(self.missed))) / len(self.scenarios) * 100)
212216

213-
def match(self, items: List[pytest.Item], deselected=False) -> None:
217+
def match(self, items: List[pytest.Item], deselected: bool = False) -> None:
214218
"""Match collected tests items with its scenarios"""
215219

216220
if not self.matches:
217221
self.matches = {sc: PytestItems() for sc in self.scenarios}
218222
sc_lookup = {sc.id: sc for sc in self.scenarios}
219223
for item in items:
220224
for key in scenario_ids(item):
221-
with suppress(KeyError):
225+
if key not in sc_lookup:
226+
self.nonexistent.append(item)
227+
else:
222228
if deselected:
223229
self.matches[sc_lookup[key]].deselected.append(item)
224230
else:
@@ -343,12 +349,25 @@ def pytest_collection_finish(self):
343349
Important that fail will be after terminal reporting hooks
344350
"""
345351
yield
346-
if self.config.fail_under and self.spec_coverage_percent < self.config.fail_under:
347-
pytest.exit(
348-
f"Spec coverage percent is {self.spec_coverage_percent}%, "
349-
f"and it is less than target {self.config.fail_under}%",
350-
returncode=ExitCode.NO_TESTS_COLLECTED,
351-
)
352+
warn_message = ""
353+
if self.nonexistent:
354+
tests_without_spec = "\n ".join(item.name for item in self.nonexistent)
355+
warn_message = f"The following tests linked with nonexistent spec:\n {tests_without_spec}"
356+
if not self.config.fail_under:
357+
if warn_message:
358+
warnings.warn(SpecCollectorWarning(warn_message))
359+
else:
360+
exit_message = ""
361+
if self.spec_coverage_percent < self.config.fail_under:
362+
exit_message += (
363+
f"Spec coverage percent is {self.spec_coverage_percent}%, "
364+
f"and it is less than target {self.config.fail_under}%\n"
365+
)
366+
if exit_message or warn_message:
367+
pytest.exit(
368+
exit_message + warn_message,
369+
returncode=ExitCode.NO_TESTS_COLLECTED,
370+
)
352371

353372
def pytest_terminal_summary(self, terminalreporter: TerminalReporter):
354373
"""

src/pytest_allure_spec_coverage/xdist_shared.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66

77
class XdistSharedStorage:
8-
"""Xdist shared storage implementation """
8+
"""Xdist shared storage implementation"""
99

1010
@staticmethod
1111
def is_xdist_master(config):

tests/examples/matcher_pytester_test.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,6 @@ def test_multiple_scenarios_case():
3131
"""Case with multiple implemented scenarios"""
3232

3333

34-
@pytest.mark.scenario("non_existent")
35-
def test_non_existent_scenario_case():
36-
"""Case implemented non-existent scenario"""
37-
38-
3934
@pytest.mark.scenario("simple_scenario")
4035
@pytest.mark.scenario("simple_scenario")
4136
def test_duplicated_scenarios_case():

tests/examples/non_existent_test.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""Non-existent scenario"""
2+
import pytest
3+
4+
5+
@pytest.mark.scenario("non_existent")
6+
def test_non_existent_scenario_case():
7+
"""Case implemented non-existent scenario"""
8+
9+
10+
@pytest.mark.scenario("simple_scenario")
11+
def test_single_scenario_case():
12+
"""Case with the single one scenario"""

tests/plugin/test_matcher.py

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
from pytest_allure_spec_coverage.common import allure_listener
2929
from pytest_allure_spec_coverage.matcher import ScenariosMatcher, make_allure_labels
3030
from pytest_allure_spec_coverage.models.scenario import Scenario
31-
from ..examples.collector import scenarios
3231
from .common import run_tests, run_with_allure
32+
from ..examples.collector import scenarios
3333

3434

3535
@dataclass
@@ -49,7 +49,6 @@ def test_cases() -> Tuple[Collection[TestCase], Collection[Scenario], Collection
4949
return (
5050
(
5151
TestCase("test_abandoned_case"),
52-
TestCase("test_non_existent_scenario_case"),
5352
TestCase("test_single_scenario_case", matches=[simple_scenario]),
5453
TestCase("test_multiple_scenarios_case", matches=[simple_scenario, nested_scenario]),
5554
TestCase("test_duplicated_scenarios_case", matches=[simple_scenario, simple_scenario]),
@@ -194,7 +193,9 @@ def test_matcher(
194193

195194
with allure.step("Check summary for coverage percent"):
196195
percent = (len(scenarios) - len(not_implemented)) * 100 // len(scenarios)
197-
assert f"{percent}%" in pytester_result.outlines[-1]
196+
assert any(
197+
f"{percent}%" in outline for outline in pytester_result.outlines
198+
), f'Should be "{percent}%" in outlines'
198199

199200

200201
@pytest.mark.usefixtures("_conftest")
@@ -216,7 +217,9 @@ def test_matcher_without_allure(
216217

217218
with allure.step("Check summary for coverage percent"):
218219
# Last one line will be greetings while previous one with stats
219-
assert f"{percent}%" in pytester_result.outlines[-2]
220+
assert any(
221+
f"{percent}%" in outline for outline in pytester_result.outlines
222+
), f'Should be "{percent}%" in outlines'
220223

221224

222225
@pytest.mark.usefixtures("_conftest")
@@ -231,13 +234,43 @@ def test_sc_only(pytester: Pytester):
231234
outcomes={"passed": 0},
232235
)
233236
assert pytester_result.ret == ExitCode.NO_TESTS_COLLECTED
234-
assert "_pytest.outcomes.Exit" in pytester_result.outlines[-1]
235-
assert "50% specification coverage" in pytester_result.outlines[-2]
237+
assert any(
238+
"_pytest.outcomes.Exit" in outline for outline in pytester_result.outlines
239+
), 'Should be "_pytest.outcomes.Exit" in outlines'
240+
assert any(
241+
"50% specification coverage" in outline for outline in pytester_result.outlines
242+
), 'Should be "50% specification coverage" in outlines'
236243
with allure.step("Assert that --sc-target less than coverage"):
237244
pytester_result, _ = run_with_allure(
238245
pytester=pytester,
239246
testfile_path="sc_only_test.py",
240247
additional_opts=["--sc-type", "test", "--sc-only", "--sc-target", "25"],
241248
outcomes={"passed": 0},
242249
)
243-
assert "🎉🎉🎉" in pytester_result.outlines[-1]
250+
assert any("🎉🎉🎉" in outline for outline in pytester_result.outlines), 'Should be "🎉🎉🎉" in outlines'
251+
252+
253+
@pytest.mark.usefixtures("_conftest")
254+
def test_non_existent(pytester: Pytester):
255+
"""Test that nonexistent spec warns or raise error"""
256+
with allure.step("Assert that non_existent test has warning"):
257+
pytester_result, _ = run_with_allure(
258+
pytester=pytester,
259+
testfile_path="non_existent_test.py",
260+
additional_opts=["--sc-type", "test"],
261+
outcomes={"passed": 2},
262+
)
263+
assert pytester_result.ret == ExitCode.OK
264+
assert " test_non_existent_scenario_case" in pytester_result.outlines
265+
assert any("2 passed, 1 warning, 25% specification coverage" in outline for outline in pytester_result.outlines)
266+
with allure.step("Assert that non_existent test raise error in sc_only mode"):
267+
pytester_result, _ = run_with_allure(
268+
pytester=pytester,
269+
testfile_path="non_existent_test.py",
270+
additional_opts=["--sc-type", "test", "--sc-only", "--sc-target", "25"],
271+
outcomes={"passed": 0},
272+
)
273+
assert pytester_result.ret == ExitCode.NO_TESTS_COLLECTED
274+
assert any(
275+
"The following tests linked with nonexistent spec" in outline for outline in pytester_result.outlines
276+
)

0 commit comments

Comments
 (0)