pytest-platform-adapter 1.0.0__py3-none-any.whl → 1.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.
- pytest_platform_adapter/plugin.py +319 -13
- {pytest_platform_adapter-1.0.0.dist-info → pytest_platform_adapter-1.1.0.dist-info}/METADATA +73 -2
- pytest_platform_adapter-1.1.0.dist-info/RECORD +8 -0
- {pytest_platform_adapter-1.0.0.dist-info → pytest_platform_adapter-1.1.0.dist-info}/WHEEL +1 -1
- pytest_platform_adapter-1.0.0.dist-info/RECORD +0 -8
- {pytest_platform_adapter-1.0.0.dist-info → pytest_platform_adapter-1.1.0.dist-info}/entry_points.txt +0 -0
- {pytest_platform_adapter-1.0.0.dist-info → pytest_platform_adapter-1.1.0.dist-info/licenses}/LICENSE +0 -0
- {pytest_platform_adapter-1.0.0.dist-info → pytest_platform_adapter-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
|
-
from
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Dict, List, Optional, Set
|
|
5
6
|
|
|
6
7
|
import requests
|
|
7
|
-
from allure_pytest.utils import allure_title
|
|
8
|
+
from allure_pytest.utils import allure_label, allure_title
|
|
8
9
|
import logging
|
|
9
10
|
import pytest
|
|
11
|
+
from _pytest.stash import StashKey
|
|
10
12
|
|
|
11
13
|
logger = logging.getLogger('pytest-platform-adapter')
|
|
12
14
|
logger.setLevel(logging.INFO)
|
|
@@ -30,6 +32,43 @@ platform_path = None
|
|
|
30
32
|
pipeline_name = None
|
|
31
33
|
build_number = None
|
|
32
34
|
platform_use_https = False
|
|
35
|
+
ENV_SETTINGS_KEY: StashKey["EnvCheckSettings"] = StashKey()
|
|
36
|
+
ENV_RUNTIME_KEY: StashKey["EnvCheckRuntime"] = StashKey()
|
|
37
|
+
ITEM_KIND_KEY: StashKey[str] = StashKey()
|
|
38
|
+
BEHAVIOR_KEY: StashKey[Optional[str]] = StashKey()
|
|
39
|
+
ENV_XFAIL_REASON_KEY: StashKey[Optional[str]] = StashKey()
|
|
40
|
+
STASH_SENTINEL = object()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class EnvCheckSettings:
|
|
45
|
+
mode: str = 'all'
|
|
46
|
+
behavior_scope: str = 'feature'
|
|
47
|
+
fail_action: str = 'skip'
|
|
48
|
+
global_nodeids: List[str] = field(default_factory=list)
|
|
49
|
+
behavior_nodeids: List[str] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
def enable_global(self) -> bool:
|
|
52
|
+
return self.mode in {'global', 'all'} and bool(self.global_nodeids)
|
|
53
|
+
|
|
54
|
+
def enable_behavior(self) -> bool:
|
|
55
|
+
return self.mode in {'behavior', 'all'} and bool(self.behavior_nodeids)
|
|
56
|
+
|
|
57
|
+
def enabled(self) -> bool:
|
|
58
|
+
if self.mode == 'off':
|
|
59
|
+
return False
|
|
60
|
+
return self.enable_global() or self.enable_behavior()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class EnvCheckRuntime:
|
|
65
|
+
global_failures: Dict[str, str] = field(default_factory=dict)
|
|
66
|
+
behavior_failures: Dict[str, str] = field(default_factory=dict)
|
|
67
|
+
|
|
68
|
+
def first_global_failure(self) -> Optional[str]:
|
|
69
|
+
if not self.global_failures:
|
|
70
|
+
return None
|
|
71
|
+
return next(iter(self.global_failures.values()))
|
|
33
72
|
|
|
34
73
|
|
|
35
74
|
def pytest_addoption(parser):
|
|
@@ -52,6 +91,19 @@ def pytest_addoption(parser):
|
|
|
52
91
|
default=False,
|
|
53
92
|
help='扫描模式:快速生成 Allure 报告而不实际执行测试'
|
|
54
93
|
)
|
|
94
|
+
group.addoption(
|
|
95
|
+
'--env-check-mode',
|
|
96
|
+
action='store',
|
|
97
|
+
default='all',
|
|
98
|
+
choices=['off', 'global', 'behavior', 'all'],
|
|
99
|
+
help='环境检查模式:off(禁用)/global(仅全局)/behavior(仅行为)/all(全部)'
|
|
100
|
+
)
|
|
101
|
+
group.addoption(
|
|
102
|
+
'--env-check-scope',
|
|
103
|
+
action='store',
|
|
104
|
+
default=None,
|
|
105
|
+
help='特性级检查所使用的 Allure 标签层级:epic/feature/story'
|
|
106
|
+
)
|
|
55
107
|
parser.addini(
|
|
56
108
|
'platform_ip',
|
|
57
109
|
help='自动化平台API IP',
|
|
@@ -72,6 +124,28 @@ def pytest_addoption(parser):
|
|
|
72
124
|
help='上报自动化平台时启用HTTPS,默认不启用',
|
|
73
125
|
default=False
|
|
74
126
|
)
|
|
127
|
+
parser.addini(
|
|
128
|
+
'platform_env_global_checks',
|
|
129
|
+
type='linelist',
|
|
130
|
+
help='全局环境检查用例的绝对NodeID列表',
|
|
131
|
+
default=[]
|
|
132
|
+
)
|
|
133
|
+
parser.addini(
|
|
134
|
+
'platform_env_behavior_checks',
|
|
135
|
+
type='linelist',
|
|
136
|
+
help='特性级环境检查用例的绝对NodeID列表',
|
|
137
|
+
default=[]
|
|
138
|
+
)
|
|
139
|
+
parser.addini(
|
|
140
|
+
'platform_env_behavior_scope',
|
|
141
|
+
help='特性级检查的Allure层级(epic/feature/story)',
|
|
142
|
+
default='feature'
|
|
143
|
+
)
|
|
144
|
+
parser.addini(
|
|
145
|
+
'platform_env_fail_action',
|
|
146
|
+
help='环境检查失败后对业务用例的处理方式(skip/xfail/none)',
|
|
147
|
+
default='skip'
|
|
148
|
+
)
|
|
75
149
|
|
|
76
150
|
|
|
77
151
|
def pytest_collection_modifyitems(config, items):
|
|
@@ -79,21 +153,26 @@ def pytest_collection_modifyitems(config, items):
|
|
|
79
153
|
hook收集用例的过程,给--case_ids和--case_ids_file提供支持
|
|
80
154
|
修改测试用例集合,根据提供的测试用例ID过滤测试用例
|
|
81
155
|
"""
|
|
156
|
+
settings = get_env_settings(config)
|
|
157
|
+
forced_nodeids = _collect_forced_nodeids(settings)
|
|
158
|
+
initial_nodeids = {item.nodeid for item in items}
|
|
159
|
+
for missing in sorted(forced_nodeids - initial_nodeids):
|
|
160
|
+
logger.warning(f"配置的环境检查用例 {missing} 未被收集,检查 NodeID 是否正确")
|
|
161
|
+
|
|
82
162
|
target_ids = get_target_test_ids(config)
|
|
83
|
-
if not target_ids:
|
|
84
|
-
test_stats['total'] = len(items) # 更新总用例数
|
|
85
|
-
return
|
|
86
163
|
selected = []
|
|
87
164
|
deselected = []
|
|
88
165
|
for item in items:
|
|
166
|
+
nodeid = item.nodeid
|
|
167
|
+
should_force_run = nodeid in forced_nodeids
|
|
89
168
|
title = allure_title(item)
|
|
90
169
|
test_id = get_test_id_from_title(title)
|
|
91
|
-
if test_id in target_ids:
|
|
170
|
+
if not target_ids or should_force_run or test_id in target_ids:
|
|
92
171
|
selected.append(item)
|
|
93
172
|
else:
|
|
94
173
|
deselected.append(item)
|
|
95
|
-
# 检测 ID
|
|
96
|
-
if test_id in cases_ids:
|
|
174
|
+
# 检测 ID 是否有重复,只是单纯的检查一下,不影响执行。用例 id 为 1 的是环境检查脚本
|
|
175
|
+
if test_id in cases_ids and test_id != "0":
|
|
97
176
|
# 为 None 在这里就不用打印log了,因为在 get_test_id_from_title 里面就会报错一次
|
|
98
177
|
if test_id is None:
|
|
99
178
|
continue
|
|
@@ -102,11 +181,14 @@ def pytest_collection_modifyitems(config, items):
|
|
|
102
181
|
cases_ids.add(test_id)
|
|
103
182
|
if deselected:
|
|
104
183
|
config.hook.pytest_deselected(items=deselected)
|
|
105
|
-
|
|
106
|
-
selected_ids = [get_test_id_from_title(allure_title(item)) for item in
|
|
107
|
-
test_stats['total'] = len(
|
|
108
|
-
|
|
109
|
-
|
|
184
|
+
items[:] = selected
|
|
185
|
+
selected_ids = [get_test_id_from_title(allure_title(item)) for item in items]
|
|
186
|
+
test_stats['total'] = len(items) # 更新总用例数
|
|
187
|
+
if target_ids:
|
|
188
|
+
logger.info("目标测试用例ID (%d个): %s", len(target_ids), target_ids)
|
|
189
|
+
logger.info("实际执行用例ID (%d个): %s", len(selected_ids), selected_ids)
|
|
190
|
+
if env_checks_enabled(settings):
|
|
191
|
+
_apply_env_check_collection_logic(config, settings, items)
|
|
110
192
|
|
|
111
193
|
|
|
112
194
|
@pytest.hookimpl(hookwrapper=True)
|
|
@@ -122,6 +204,18 @@ def pytest_runtest_setup(item):
|
|
|
122
204
|
if item.config.getoption('--scan'):
|
|
123
205
|
# logger.info(f"扫描模式:跳过测试用例 {allure_title(item)}前置")
|
|
124
206
|
pytest.skip("扫描模式已启动,跳过测试用例前置")
|
|
207
|
+
settings = get_env_settings(item.config)
|
|
208
|
+
if env_checks_enabled(settings):
|
|
209
|
+
runtime = get_env_runtime(item.config)
|
|
210
|
+
kind = _get_item_kind(item)
|
|
211
|
+
global_reason = runtime.first_global_failure()
|
|
212
|
+
if global_reason and kind != 'global_check':
|
|
213
|
+
_apply_env_failure_action(settings, item, global_reason)
|
|
214
|
+
elif kind == 'business':
|
|
215
|
+
behavior = _ensure_behavior_stashed(item, settings.behavior_scope)
|
|
216
|
+
behavior_reason = runtime.behavior_failures.get(behavior) if behavior else None
|
|
217
|
+
if behavior_reason:
|
|
218
|
+
_apply_env_failure_action(settings, item, behavior_reason)
|
|
125
219
|
yield
|
|
126
220
|
|
|
127
221
|
|
|
@@ -133,6 +227,39 @@ def pytest_runtest_teardown(item):
|
|
|
133
227
|
yield
|
|
134
228
|
|
|
135
229
|
|
|
230
|
+
@pytest.hookimpl(hookwrapper=True)
|
|
231
|
+
def pytest_runtest_makereport(item, call):
|
|
232
|
+
outcome = yield
|
|
233
|
+
report = outcome.get_result()
|
|
234
|
+
settings = get_env_settings(item.config)
|
|
235
|
+
if not env_checks_enabled(settings):
|
|
236
|
+
return
|
|
237
|
+
if report.when != 'call':
|
|
238
|
+
return
|
|
239
|
+
kind = _get_item_kind(item)
|
|
240
|
+
if kind not in {'global_check', 'behavior_check'}:
|
|
241
|
+
return
|
|
242
|
+
runtime = get_env_runtime(item.config)
|
|
243
|
+
if report.failed:
|
|
244
|
+
reason = _format_env_failure_message(kind, item, report)
|
|
245
|
+
if kind == 'global_check':
|
|
246
|
+
runtime.global_failures[item.nodeid] = reason
|
|
247
|
+
else:
|
|
248
|
+
behavior = _ensure_behavior_stashed(item, settings.behavior_scope)
|
|
249
|
+
if behavior:
|
|
250
|
+
runtime.behavior_failures[behavior] = reason
|
|
251
|
+
else:
|
|
252
|
+
logger.warning("特性级环境检查 %s 缺少 %s 标签,无法绑定到具体行为", item.nodeid,
|
|
253
|
+
settings.behavior_scope)
|
|
254
|
+
else:
|
|
255
|
+
if kind == 'global_check':
|
|
256
|
+
runtime.global_failures.pop(item.nodeid, None)
|
|
257
|
+
else:
|
|
258
|
+
behavior = _ensure_behavior_stashed(item, settings.behavior_scope)
|
|
259
|
+
if behavior:
|
|
260
|
+
runtime.behavior_failures.pop(behavior, None)
|
|
261
|
+
|
|
262
|
+
|
|
136
263
|
@pytest.hookimpl
|
|
137
264
|
def pytest_runtest_logreport(report):
|
|
138
265
|
"""
|
|
@@ -209,6 +336,9 @@ def pytest_configure(config):
|
|
|
209
336
|
scan_enable, platform_ip, platform_port, platform_path, platform_use_https = config.getoption(
|
|
210
337
|
'--scan'), config.getini('platform_ip'), config.getini('platform_port'), config.getini(
|
|
211
338
|
'platform_path'), config.getini('platform_use_https')
|
|
339
|
+
settings = build_env_check_settings(config)
|
|
340
|
+
config.stash[ENV_SETTINGS_KEY] = settings
|
|
341
|
+
config.stash[ENV_RUNTIME_KEY] = EnvCheckRuntime()
|
|
212
342
|
pipeline_name = os.environ.get("JOB_NAME")
|
|
213
343
|
build_number = os.environ.get("BUILD_NUMBER")
|
|
214
344
|
handler = logging.StreamHandler()
|
|
@@ -220,6 +350,10 @@ def pytest_configure(config):
|
|
|
220
350
|
"markers",
|
|
221
351
|
"allure_title: 使用allure标题标记测试用例"
|
|
222
352
|
)
|
|
353
|
+
config.addinivalue_line(
|
|
354
|
+
"markers",
|
|
355
|
+
"environment_check: 标记环境检查用例"
|
|
356
|
+
)
|
|
223
357
|
|
|
224
358
|
|
|
225
359
|
def get_test_ids_from_file(file_path: str) -> List[str]:
|
|
@@ -263,3 +397,175 @@ def get_test_id_from_title(title: Optional[str]) -> Optional[str]:
|
|
|
263
397
|
else:
|
|
264
398
|
logger.error(f'存在无法解析用例ID的用例,用例标题为:{title}')
|
|
265
399
|
return None
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def build_env_check_settings(config) -> EnvCheckSettings:
|
|
403
|
+
mode = config.getoption('--env-check-mode') or 'all'
|
|
404
|
+
scope_opt = config.getoption('--env-check-scope')
|
|
405
|
+
scope_ini = config.getini('platform_env_behavior_scope') or 'feature'
|
|
406
|
+
scope = (scope_opt or scope_ini or 'feature').strip().lower()
|
|
407
|
+
if scope not in {'epic', 'feature', 'story'}:
|
|
408
|
+
logger.warning("未识别的行为层级 %s,回退到 feature", scope)
|
|
409
|
+
scope = 'feature'
|
|
410
|
+
fail_action_raw = config.getini('platform_env_fail_action')
|
|
411
|
+
fail_action = (fail_action_raw or 'skip').strip().lower()
|
|
412
|
+
if fail_action not in {'skip', 'xfail', 'none'}:
|
|
413
|
+
logger.warning("platform_env_fail_action=%s 不受支持,改用 skip", fail_action_raw)
|
|
414
|
+
fail_action = 'skip'
|
|
415
|
+
global_nodes = _normalize_nodeids(config.getini('platform_env_global_checks'))
|
|
416
|
+
behavior_nodes = _normalize_nodeids(config.getini('platform_env_behavior_checks'))
|
|
417
|
+
if mode not in {'off', 'global', 'behavior', 'all'}:
|
|
418
|
+
logger.warning("未识别的 env-check-mode %s,回退到 all", mode)
|
|
419
|
+
mode = 'all'
|
|
420
|
+
return EnvCheckSettings(
|
|
421
|
+
mode=mode,
|
|
422
|
+
behavior_scope=scope,
|
|
423
|
+
fail_action=fail_action,
|
|
424
|
+
global_nodeids=global_nodes,
|
|
425
|
+
behavior_nodeids=behavior_nodes,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def env_checks_enabled(settings: Optional[EnvCheckSettings]) -> bool:
|
|
430
|
+
return bool(settings and settings.enabled())
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _normalize_nodeids(values: Optional[List[str]]) -> List[str]:
|
|
434
|
+
if not values:
|
|
435
|
+
return []
|
|
436
|
+
normalized = []
|
|
437
|
+
for nodeid in values:
|
|
438
|
+
if not nodeid:
|
|
439
|
+
continue
|
|
440
|
+
value = nodeid.strip()
|
|
441
|
+
if value:
|
|
442
|
+
normalized.append(value)
|
|
443
|
+
return normalized
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def get_env_settings(config) -> EnvCheckSettings:
|
|
447
|
+
return config.stash.get(ENV_SETTINGS_KEY, EnvCheckSettings())
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def get_env_runtime(config) -> EnvCheckRuntime:
|
|
451
|
+
return config.stash.get(ENV_RUNTIME_KEY, EnvCheckRuntime())
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _collect_forced_nodeids(settings: Optional[EnvCheckSettings]) -> Set[str]:
|
|
455
|
+
if not env_checks_enabled(settings):
|
|
456
|
+
return set()
|
|
457
|
+
forced = set()
|
|
458
|
+
if settings.enable_global():
|
|
459
|
+
forced.update(settings.global_nodeids)
|
|
460
|
+
if settings.enable_behavior():
|
|
461
|
+
forced.update(settings.behavior_nodeids)
|
|
462
|
+
return forced
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _apply_env_check_collection_logic(config, settings: EnvCheckSettings, items: List[pytest.Item]) -> None:
|
|
466
|
+
node_map = {item.nodeid: item for item in items}
|
|
467
|
+
ordered: List[pytest.Item] = []
|
|
468
|
+
consumed: Set[str] = set()
|
|
469
|
+
missing_behaviors: List[str] = []
|
|
470
|
+
|
|
471
|
+
for nodeid in settings.global_nodeids:
|
|
472
|
+
item = node_map.get(nodeid)
|
|
473
|
+
if not item:
|
|
474
|
+
continue
|
|
475
|
+
ordered.append(item)
|
|
476
|
+
consumed.add(nodeid)
|
|
477
|
+
item.stash[ITEM_KIND_KEY] = 'global_check'
|
|
478
|
+
item.stash[BEHAVIOR_KEY] = None
|
|
479
|
+
|
|
480
|
+
behavior_checks: Dict[str, pytest.Item] = {}
|
|
481
|
+
for nodeid in settings.behavior_nodeids:
|
|
482
|
+
item = node_map.get(nodeid)
|
|
483
|
+
if not item:
|
|
484
|
+
continue
|
|
485
|
+
behavior = _ensure_behavior_stashed(item, settings.behavior_scope)
|
|
486
|
+
if not behavior:
|
|
487
|
+
missing_behaviors.append(nodeid)
|
|
488
|
+
continue
|
|
489
|
+
if behavior in behavior_checks:
|
|
490
|
+
logger.warning(
|
|
491
|
+
"特性级检查 %s 重复定义,沿用首次声明的用例 %s",
|
|
492
|
+
behavior,
|
|
493
|
+
behavior_checks[behavior].nodeid,
|
|
494
|
+
)
|
|
495
|
+
continue
|
|
496
|
+
behavior_checks[behavior] = item
|
|
497
|
+
consumed.add(nodeid)
|
|
498
|
+
item.stash[ITEM_KIND_KEY] = 'behavior_check'
|
|
499
|
+
|
|
500
|
+
if missing_behaviors:
|
|
501
|
+
logger.warning(
|
|
502
|
+
"下列特性级检查缺少 %s 标签:%s",
|
|
503
|
+
settings.behavior_scope,
|
|
504
|
+
missing_behaviors,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
behavior_inserted: Set[str] = set()
|
|
508
|
+
for item in items:
|
|
509
|
+
if item.nodeid in consumed:
|
|
510
|
+
continue
|
|
511
|
+
if item.stash.get(ITEM_KIND_KEY, STASH_SENTINEL) is STASH_SENTINEL:
|
|
512
|
+
item.stash[ITEM_KIND_KEY] = 'business'
|
|
513
|
+
behavior = _ensure_behavior_stashed(item, settings.behavior_scope)
|
|
514
|
+
if behavior and behavior in behavior_checks and behavior not in behavior_inserted:
|
|
515
|
+
ordered.append(behavior_checks[behavior])
|
|
516
|
+
behavior_inserted.add(behavior)
|
|
517
|
+
ordered.append(item)
|
|
518
|
+
|
|
519
|
+
for behavior, check_item in behavior_checks.items():
|
|
520
|
+
if behavior not in behavior_inserted:
|
|
521
|
+
ordered.append(check_item)
|
|
522
|
+
behavior_inserted.add(behavior)
|
|
523
|
+
|
|
524
|
+
if ordered:
|
|
525
|
+
items[:] = ordered
|
|
526
|
+
logger.debug(
|
|
527
|
+
"最终的环境检查执行顺序: %s",
|
|
528
|
+
[item.nodeid for item in items],
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _ensure_behavior_stashed(item: pytest.Item, scope: str) -> Optional[str]:
|
|
533
|
+
behavior = item.stash.get(BEHAVIOR_KEY, STASH_SENTINEL)
|
|
534
|
+
if behavior is not STASH_SENTINEL:
|
|
535
|
+
return behavior
|
|
536
|
+
labels = allure_label(item, scope) or []
|
|
537
|
+
value = labels[0] if labels else None
|
|
538
|
+
item.stash[BEHAVIOR_KEY] = value
|
|
539
|
+
return value
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _get_item_kind(item: pytest.Item) -> Optional[str]:
|
|
543
|
+
return item.stash.get(ITEM_KIND_KEY, None)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _apply_env_failure_action(settings: EnvCheckSettings, item: pytest.Item, reason: str) -> None:
|
|
547
|
+
if settings.fail_action == 'none':
|
|
548
|
+
return
|
|
549
|
+
if settings.fail_action == 'xfail':
|
|
550
|
+
_mark_item_expected_failure(item, reason)
|
|
551
|
+
return
|
|
552
|
+
pytest.skip(reason)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _mark_item_expected_failure(item: pytest.Item, reason: str) -> None:
|
|
556
|
+
existing = item.stash.get(ENV_XFAIL_REASON_KEY, None)
|
|
557
|
+
if existing:
|
|
558
|
+
return
|
|
559
|
+
item.add_marker(pytest.mark.xfail(reason=reason, run=True, strict=False))
|
|
560
|
+
item.stash[ENV_XFAIL_REASON_KEY] = reason
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _format_env_failure_message(kind: str, item: pytest.Item, report: pytest.TestReport) -> str:
|
|
564
|
+
prefix = "全局" if kind == 'global_check' else "特性级"
|
|
565
|
+
detail = ""
|
|
566
|
+
longrepr = getattr(report, "longreprtext", "")
|
|
567
|
+
if longrepr:
|
|
568
|
+
first_line = longrepr.strip().splitlines()[0]
|
|
569
|
+
if first_line:
|
|
570
|
+
detail = f": {first_line}"
|
|
571
|
+
return f"{prefix}环境检查失败({item.nodeid}){detail}"
|
{pytest_platform_adapter-1.0.0.dist-info → pytest_platform_adapter-1.1.0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest_platform_adapter
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Pytest集成自动化平台插件
|
|
5
5
|
Author-email: BlackYau <blackyau426@gmail.com>
|
|
6
6
|
Maintainer-email: BlackYau <blackyau426@gmail.com>
|
|
@@ -32,6 +32,7 @@ Description-Content-Type: text/markdown
|
|
|
32
32
|
License-File: LICENSE
|
|
33
33
|
Requires-Dist: pytest>=6.2.5
|
|
34
34
|
Requires-Dist: allure-pytest>=2.9.45
|
|
35
|
+
Dynamic: license-file
|
|
35
36
|
|
|
36
37
|
# pytest-platform-adapter
|
|
37
38
|
|
|
@@ -47,6 +48,7 @@ Pytest集成自动化平台插件
|
|
|
47
48
|
- 根据用例 `allure.title` 中的 ID 筛选执行的用例
|
|
48
49
|
- 跳过所有用例的执行,快速生成 Allure 报告
|
|
49
50
|
- 周期性的通过 RESTApi 上报执行用例的整体进度
|
|
51
|
+
- 在执行前插入全局 / 特性级环境检查用例,失败时可批量 skip/xfail 后续业务用例
|
|
50
52
|
|
|
51
53
|
## 环境依赖
|
|
52
54
|
|
|
@@ -56,12 +58,50 @@ Pytest集成自动化平台插件
|
|
|
56
58
|
|
|
57
59
|
## 安装
|
|
58
60
|
|
|
61
|
+
### 在线安装
|
|
62
|
+
|
|
63
|
+
#### 官网在线安装
|
|
64
|
+
|
|
59
65
|
执行以下命令即可完成安装
|
|
60
66
|
|
|
61
67
|
```shell
|
|
62
68
|
pip install pytest-platform-adapter
|
|
63
69
|
```
|
|
64
70
|
|
|
71
|
+
#### 国内镜像站在线安装
|
|
72
|
+
|
|
73
|
+
如果安装遇到网络相关问题,也可以使用临时使用清华大学 PyPI 源安装本插件,安装命令如下:
|
|
74
|
+
|
|
75
|
+
```shell
|
|
76
|
+
pip install -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple pytest-platform-adapter
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 离线安装
|
|
80
|
+
|
|
81
|
+
#### 官网下载离线安装
|
|
82
|
+
|
|
83
|
+
前往 PyPI 官网中的 pytest-platform-adapter 项目文件下载界面 https://pypi.org/project/pytest-platform-adapter/#files
|
|
84
|
+
|
|
85
|
+
下载最新的 Built Distribution(构建发布版本),例如:`pytest_platform_adapter-x.x.x-py3-none-any.whl` 传输到需要离线安装的环境上
|
|
86
|
+
|
|
87
|
+
然后在命令行中使用以下命令安装(你需要提前安装 `pytest` 和 `allure-pytest`)
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
pip install pytest_platform_adapter-1.1.0-py3-none-any.whl
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### 国内镜像站下载离线安装
|
|
94
|
+
|
|
95
|
+
前往清华大学 PyPI 源本插件的下载文件页 https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/pytest-platform-adapter/
|
|
96
|
+
|
|
97
|
+
下载最新(版本号最大)的扩展名为 `.whl` 的构建发布版本文件,例如:`pytest_platform_adapter-x.x.x-py3-none-any.whl` 传输到需要离线安装的环境上
|
|
98
|
+
|
|
99
|
+
然后在命令行中使用以下命令安装(你需要提前安装 `pytest` 和 `allure-pytest`)
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
pip install pytest_platform_adapter-1.1.0-py3-none-any.whl
|
|
103
|
+
```
|
|
104
|
+
|
|
65
105
|
## 使用方法
|
|
66
106
|
|
|
67
107
|
### 根据 Allure title 中的 ID 筛选用例
|
|
@@ -137,6 +177,37 @@ platform_use_https = False
|
|
|
137
177
|
|
|
138
178
|
其中 `JOB_NAME` 和 `BUILD_NUMBER` 是通过环境变量获取的。
|
|
139
179
|
|
|
180
|
+
### 环境检查
|
|
181
|
+
|
|
182
|
+
环境检查由额外的 pytest 用例构成,通过配置它们的 NodeID 可以在收集阶段将这些用例插入到业务用例之前执行。可同时启用“全局检查”和“特性级检查”。
|
|
183
|
+
|
|
184
|
+
#### 配置项-推荐
|
|
185
|
+
|
|
186
|
+
在 `pytest.ini` 中添加以下内容:
|
|
187
|
+
|
|
188
|
+
```ini
|
|
189
|
+
[pytest]
|
|
190
|
+
platform_env_global_checks =
|
|
191
|
+
tests/env_check/test_global_env_check.py::TestGlobalEnvCheck
|
|
192
|
+
platform_env_behavior_checks =
|
|
193
|
+
tests/env_check/test_function_env_check.py::TestFunctionEnvCheck
|
|
194
|
+
platform_env_behavior_scope = epic # epic / feature / story
|
|
195
|
+
platform_env_fail_action = skip # skip / xfail / none
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
- `platform_env_global_checks`:在每次用例执行前跑一遍的检查用例列表。
|
|
199
|
+
- `platform_env_behavior_checks`:特性级检查用例列表,插件会读取这些用例标签并在同一特性的首个用例前插入执行。
|
|
200
|
+
- `platform_env_behavior_scope`:定义特性的级别可选的有 epic/feature/story(分别对应 Allure Report 的 behaviors 中的一级目录、二级目录、三级目录)
|
|
201
|
+
- `platform_env_fail_action`:检查失败后对后续业务用例的处理方式,`skip` 为直接跳过、`xfail` 为继续执行并动态加上 `xfail`(失败记为 XFAIL、通过记为 XPASS)、`none` 仅记录日志不干预执行。
|
|
202
|
+
|
|
203
|
+
#### 命令行
|
|
204
|
+
|
|
205
|
+
- `--env-check-mode={off,global,behavior,all}`:快速控制启用的检查范围。
|
|
206
|
+
- `--env-check-scope=<epic|feature|story>`:临时覆盖行为层级设定。
|
|
207
|
+
- 当 `platform_env_fail_action=xfail` 时,会在受影响的业务用例上自动添加 `pytest.mark.xfail(run=True, strict=False)`,用例依旧执行,只是失败时记为 XFAIL。
|
|
208
|
+
|
|
209
|
+
当检查用例失败时,插件会在 `pytest_runtest_setup` 阶段统一对后续业务用例执行 `skip` 或 `xfail`,并在日志中说明对应的检查节点。检查用例自身依旧遵循 pytest 原生语义,可继续使用 `skip/xfail/flaky` 等标记。
|
|
210
|
+
|
|
140
211
|
|
|
141
212
|
## 调试方法
|
|
142
213
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pytest_platform_adapter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
pytest_platform_adapter/plugin.py,sha256=KAnSugCo9L8uceSO22LRi28myAclU3WSOo0Tpfiupp4,21604
|
|
3
|
+
pytest_platform_adapter-1.1.0.dist-info/licenses/LICENSE,sha256=QOkNlcvUHENY86LE8OEpmtwMM24v76pgF4FmkEk8aXI,1064
|
|
4
|
+
pytest_platform_adapter-1.1.0.dist-info/METADATA,sha256=wSKR3CVr40S3fNRTUDcCTNDXMWG_-yi2GP9vdiHBXLE,10437
|
|
5
|
+
pytest_platform_adapter-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
pytest_platform_adapter-1.1.0.dist-info/entry_points.txt,sha256=VorMHllVSkGNUUsVJOCrDMeW0-oLco5XcrmdfxND48c,61
|
|
7
|
+
pytest_platform_adapter-1.1.0.dist-info/top_level.txt,sha256=OnTaKf7GCc7yqBHQ4H2XLK8PAz7oHslUZKEsd1t5BpA,24
|
|
8
|
+
pytest_platform_adapter-1.1.0.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
pytest_platform_adapter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
pytest_platform_adapter/plugin.py,sha256=xNdbIscIhajhfe4EJOvdNw2psrjrTV-uofdCCSlOJZU,10336
|
|
3
|
-
pytest_platform_adapter-1.0.0.dist-info/LICENSE,sha256=QOkNlcvUHENY86LE8OEpmtwMM24v76pgF4FmkEk8aXI,1064
|
|
4
|
-
pytest_platform_adapter-1.0.0.dist-info/METADATA,sha256=afNaCFnL1KfaykAo8e3V7Y3rQY4AOAc1tC49iiiecRE,7010
|
|
5
|
-
pytest_platform_adapter-1.0.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
6
|
-
pytest_platform_adapter-1.0.0.dist-info/entry_points.txt,sha256=VorMHllVSkGNUUsVJOCrDMeW0-oLco5XcrmdfxND48c,61
|
|
7
|
-
pytest_platform_adapter-1.0.0.dist-info/top_level.txt,sha256=OnTaKf7GCc7yqBHQ4H2XLK8PAz7oHslUZKEsd1t5BpA,24
|
|
8
|
-
pytest_platform_adapter-1.0.0.dist-info/RECORD,,
|
{pytest_platform_adapter-1.0.0.dist-info → pytest_platform_adapter-1.1.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{pytest_platform_adapter-1.0.0.dist-info → pytest_platform_adapter-1.1.0.dist-info/licenses}/LICENSE
RENAMED
|
File without changes
|
{pytest_platform_adapter-1.0.0.dist-info → pytest_platform_adapter-1.1.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|