|
1 | 1 | import json |
2 | 2 | import re |
3 | | -from typing import Dict, List, Optional |
| 3 | +from typing import Dict, Iterable, List, Optional, Set |
| 4 | + |
| 5 | +from _pytest.mark.structures import Mark |
4 | 6 | from pytest import Item |
5 | 7 |
|
6 | 8 |
|
|
17 | 19 | # - tag |
18 | 20 | # - owner |
19 | 21 | # - extra_attributes |
| 22 | + |
| 23 | + |
| 24 | +def _get_desc(item: Item) -> str: |
| 25 | + try: |
| 26 | + doc = item.function.__doc__ # type: ignore |
| 27 | + except AttributeError: |
| 28 | + return "" |
| 29 | + return str(doc).strip() if doc else "" |
| 30 | + |
| 31 | + |
| 32 | +def _iter_markers(item: Item) -> List[Mark]: |
| 33 | + try: |
| 34 | + return list(item.iter_markers()) |
| 35 | + except AttributeError: |
| 36 | + return list(item.own_markers or []) |
| 37 | + |
| 38 | + |
| 39 | +def _is_tag_mark(mark: Mark) -> bool: |
| 40 | + if mark.name in {"attributes"}: |
| 41 | + return False |
| 42 | + if mark.args or mark.kwargs: |
| 43 | + return False |
| 44 | + return True |
| 45 | + |
| 46 | + |
| 47 | +def _collect_tags(markers: Iterable[Mark]) -> List[str]: |
| 48 | + """从 markers 中提取 tags(去重),只收集“无 args 且无 kwargs”的 mark.name。 |
| 49 | +
|
| 50 | + 示例: |
| 51 | + pytestmark = [pytest.mark.mod_tag] |
| 52 | +
|
| 53 | + @pytest.mark.class_tag |
| 54 | + class TestX: |
| 55 | + @pytest.mark.func_tag |
| 56 | + def test_ok(self): |
| 57 | + pass |
| 58 | +
|
| 59 | + => tags 包含:mod_tag / class_tag / func_tag |
| 60 | +
|
| 61 | + 反例(不会进 tags): |
| 62 | + @pytest.mark.slow(reason="too slow") # 有 kwargs |
| 63 | + @pytest.mark.owner("alice") # 有 args |
| 64 | + @pytest.mark.attributes({"k": "v"}) # 显式排除 |
| 65 | + """ |
| 66 | + tags: List[str] = [] |
| 67 | + seen: Set[str] = set() |
| 68 | + for mark in markers: |
| 69 | + name_str = str(mark.name) |
| 70 | + if not _is_tag_mark(mark) or name_str in seen: |
| 71 | + continue |
| 72 | + seen.add(name_str) |
| 73 | + tags.append(name_str) |
| 74 | + return tags |
| 75 | + |
| 76 | + |
| 77 | +def _try_set_owner(attributes: Dict[str, str], mark: Mark) -> None: |
| 78 | + """解析 owner:仅支持 `@pytest.mark.owner("alice")`。 |
| 79 | +
|
| 80 | + 示例: |
| 81 | + @pytest.mark.owner("alice") |
| 82 | + def test_ok(): |
| 83 | + pass |
| 84 | +
|
| 85 | + => attributes["owner"] == "alice" |
| 86 | + """ |
| 87 | + if "owner" in attributes: |
| 88 | + return |
| 89 | + if mark.name != "owner" or not mark.args: |
| 90 | + return |
| 91 | + attributes["owner"] = str(mark.args[0]) |
| 92 | + |
| 93 | + |
| 94 | +def _try_set_extra_attributes(attributes: Dict[str, str], mark: Mark) -> None: |
| 95 | + """解析 extra_attributes:仅支持 `@pytest.mark.extra_attributes({"k": "v"})`。 |
| 96 | +
|
| 97 | + 示例: |
| 98 | + @pytest.mark.extra_attributes({"env": "AA", "skip": None}) |
| 99 | + def test_ok(): |
| 100 | + pass |
| 101 | +
|
| 102 | + => attributes["extra_attributes"] == '[{"env": "AA"}]' # None 会被过滤 |
| 103 | + """ |
| 104 | + if mark.name != "extra_attributes" or not mark.args: |
| 105 | + return |
| 106 | + data = mark.args[0] |
| 107 | + if not isinstance(data, dict): |
| 108 | + return |
| 109 | + attr_list = [{k: v} for k, v in data.items() if v is not None] |
| 110 | + attributes["extra_attributes"] = json.dumps(attr_list) |
| 111 | + |
| 112 | + |
| 113 | +def _try_set_coding_testcase_id(attributes: Dict[str, str], item: Item, mark: Mark) -> None: |
| 114 | + """解析 coding_testcase_id:面向参数化用例,通过参数 id 映射到外部用例 ID。 |
| 115 | +
|
| 116 | + 示例: |
| 117 | + @pytest.mark.coding_testcase_id({"case1": "CID-001"}) |
| 118 | + @pytest.mark.parametrize("x", [pytest.param(1, id="case1")]) |
| 119 | + def test_p(x): |
| 120 | + pass |
| 121 | +
|
| 122 | + => item.name == "test_p[case1]" |
| 123 | + => attributes["coding_testcase_id"] == "CID-001" |
| 124 | + """ |
| 125 | + if mark.name != "coding_testcase_id" or not mark.args: |
| 126 | + return |
| 127 | + if "[" not in item.name: |
| 128 | + return |
| 129 | + data = mark.args[0] |
| 130 | + if not isinstance(data, dict): |
| 131 | + return |
| 132 | + case_data_name = item.name.split("[", 1)[1][:-1] |
| 133 | + if case_data_name in data: |
| 134 | + attributes["coding_testcase_id"] = str(data[case_data_name]) |
| 135 | + |
| 136 | + |
20 | 137 | def parse_case_attributes(item: Item, comment_fields: Optional[List[str]] = None) -> Dict[str, str]: |
21 | 138 | """parse testcase attributes""" |
22 | | - desc: str = (str(item.function.__doc__) if item.function.__doc__ else "").strip() # type: ignore |
| 139 | + desc = _get_desc(item) |
23 | 140 | attributes: Dict[str, str] = {"description": desc} |
24 | 141 | if comment_fields: |
25 | 142 | attributes.update(scan_comment_fields(desc, comment_fields)) |
26 | 143 |
|
27 | | - if not item.own_markers: |
28 | | - return attributes |
29 | | - tags = [] |
30 | | - for mark in item.own_markers: |
31 | | - if not mark.args and mark.name != "attributes": |
32 | | - tags.append(mark.name) |
33 | | - elif mark.args and mark.name == "owner": |
34 | | - attributes["owner"] = str(mark.args[0]) |
35 | | - elif mark.name == "extra_attributes": |
36 | | - extra_attr = {} |
37 | | - attr_list = [] |
38 | | - for key in mark.args[0]: |
39 | | - if mark.args[0][key] is None: |
40 | | - continue |
41 | | - extra_attr[key] = mark.args[0][key] |
42 | | - attr_list.append(extra_attr) |
43 | | - attributes["extra_attributes"] = json.dumps(attr_list) |
44 | | - elif mark.name == "coding_testcase_id": |
45 | | - case_data_name = item.name.split("[")[1][:-1] |
46 | | - for data_name in mark.args[0]: |
47 | | - if data_name == case_data_name: |
48 | | - attributes["coding_testcase_id"] = mark.args[0][data_name] |
49 | | - |
50 | | - attributes["tags"] = json.dumps(tags) |
| 144 | + markers = _iter_markers(item) |
| 145 | + for mark in markers: |
| 146 | + _try_set_owner(attributes, mark) |
| 147 | + _try_set_extra_attributes(attributes, mark) |
| 148 | + _try_set_coding_testcase_id(attributes, item, mark) |
| 149 | + |
| 150 | + attributes["tags"] = json.dumps(_collect_tags(markers)) |
51 | 151 | return attributes |
52 | 152 |
|
53 | 153 |
|
|
0 commit comments