231 lines
7.9 KiB
Python
231 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Woodpecker CI 场景测试
|
|
模拟不同分支场景下的 CI/CD 流程执行
|
|
"""
|
|
|
|
import yaml
|
|
from pathlib import Path
|
|
from typing import Dict, List, Set
|
|
|
|
|
|
class ScenarioTester:
|
|
"""场景测试器"""
|
|
|
|
def __init__(self, config_path: str):
|
|
self.config_path = Path(config_path)
|
|
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|
self.config = yaml.safe_load(f)
|
|
|
|
self.scenarios = {
|
|
"场景1: feature分支开发": {
|
|
"branch": "feature/new-feature",
|
|
"event": "push",
|
|
"expected_steps": [
|
|
"lint", "type-check", "security-scan",
|
|
"unit-tests", "e2e-smoke"
|
|
],
|
|
"unexpected_steps": [
|
|
"e2e-standard", "e2e-deep", "build-image",
|
|
"deploy-production", "archive-to-main"
|
|
]
|
|
},
|
|
"场景2: feature分支PR": {
|
|
"branch": "feature/another-feature",
|
|
"event": "pull_request",
|
|
"expected_steps": [
|
|
"lint", "type-check", "security-scan",
|
|
"unit-tests", "e2e-smoke"
|
|
],
|
|
"unexpected_steps": [
|
|
"e2e-standard", "e2e-deep", "build-image",
|
|
"deploy-production", "archive-to-main"
|
|
]
|
|
},
|
|
"场景3: dev分支集成": {
|
|
"branch": "dev",
|
|
"event": "push",
|
|
"expected_steps": [
|
|
"lint", "type-check", "security-scan",
|
|
"unit-tests", "e2e-standard"
|
|
],
|
|
"unexpected_steps": [
|
|
"e2e-smoke", "e2e-deep", "build-image",
|
|
"deploy-production", "archive-to-main"
|
|
]
|
|
},
|
|
"场景4: release分支部署": {
|
|
"branch": "release/v1.0.0",
|
|
"event": "push",
|
|
"expected_steps": [
|
|
"lint", "type-check", "security-scan",
|
|
"unit-tests", "e2e-standard", "e2e-deep",
|
|
"e2e-performance", "e2e-accessibility", "e2e-visual",
|
|
"build-image", "deploy-production", "archive-to-main"
|
|
],
|
|
"unexpected_steps": ["e2e-smoke"]
|
|
},
|
|
"场景5: release主分支部署": {
|
|
"branch": "release",
|
|
"event": "push",
|
|
"expected_steps": [
|
|
"lint", "type-check", "security-scan",
|
|
"unit-tests", "e2e-standard", "e2e-deep",
|
|
"e2e-performance", "e2e-accessibility", "e2e-visual",
|
|
"build-image", "deploy-production", "archive-to-main"
|
|
],
|
|
"unexpected_steps": ["e2e-smoke"]
|
|
},
|
|
"场景6: main分支只读": {
|
|
"branch": "main",
|
|
"event": "push",
|
|
"expected_steps": [],
|
|
"unexpected_steps": [
|
|
"lint", "type-check", "security-scan",
|
|
"unit-tests", "build-image", "deploy-production"
|
|
]
|
|
}
|
|
}
|
|
|
|
def match_branch(self, pattern: str, branch: str) -> bool:
|
|
"""匹配分支模式"""
|
|
if pattern == branch:
|
|
return True
|
|
if pattern.endswith("/**"):
|
|
prefix = pattern[:-3]
|
|
return branch.startswith(prefix + "/")
|
|
return False
|
|
|
|
def get_triggered_steps(self, branch: str, event: str) -> Set[str]:
|
|
"""获取触发的步骤"""
|
|
triggered = set()
|
|
|
|
for step_name, step_config in self.config.get('steps', {}).items():
|
|
if not isinstance(step_config, dict):
|
|
continue
|
|
|
|
when_config = step_config.get('when', {})
|
|
if not when_config:
|
|
triggered.add(step_name)
|
|
continue
|
|
|
|
event_match = False
|
|
branch_match = False
|
|
|
|
if isinstance(when_config, list):
|
|
for condition in when_config:
|
|
if isinstance(condition, dict):
|
|
if 'event' in condition:
|
|
if event in condition['event']:
|
|
event_match = True
|
|
if 'branch' in condition:
|
|
for pattern in condition['branch']:
|
|
if self.match_branch(pattern, branch):
|
|
branch_match = True
|
|
break
|
|
elif isinstance(when_config, dict):
|
|
if 'event' in when_config:
|
|
if event in when_config['event']:
|
|
event_match = True
|
|
if 'branch' in when_config:
|
|
for pattern in when_config['branch']:
|
|
if self.match_branch(pattern, branch):
|
|
branch_match = True
|
|
break
|
|
|
|
if event_match and branch_match:
|
|
triggered.add(step_name)
|
|
|
|
return triggered
|
|
|
|
def run_scenario(self, scenario_name: str, scenario: Dict) -> Dict:
|
|
"""运行单个场景"""
|
|
branch = scenario['branch']
|
|
event = scenario['event']
|
|
expected = set(scenario['expected_steps'])
|
|
unexpected = set(scenario['unexpected_steps'])
|
|
|
|
triggered = self.get_triggered_steps(branch, event)
|
|
|
|
missing = expected - triggered
|
|
extra = triggered & unexpected
|
|
|
|
passed = len(missing) == 0 and len(extra) == 0
|
|
|
|
return {
|
|
"scenario": scenario_name,
|
|
"branch": branch,
|
|
"event": event,
|
|
"passed": passed,
|
|
"triggered": triggered,
|
|
"expected": expected,
|
|
"missing": missing,
|
|
"extra": extra
|
|
}
|
|
|
|
def run_all_scenarios(self):
|
|
"""运行所有场景"""
|
|
print("\n" + "="*70)
|
|
print("Woodpecker CI 场景测试")
|
|
print("="*70)
|
|
|
|
results = []
|
|
|
|
for scenario_name, scenario in self.scenarios.items():
|
|
result = self.run_scenario(scenario_name, scenario)
|
|
results.append(result)
|
|
|
|
print(f"\n📋 {scenario_name}")
|
|
print(f" 分支: {result['branch']}")
|
|
print(f" 事件: {result['event']}")
|
|
|
|
if result['passed']:
|
|
print(f" ✅ 测试通过")
|
|
else:
|
|
print(f" ❌ 测试失败")
|
|
|
|
print(f" 触发步骤 ({len(result['triggered'])}): {', '.join(sorted(result['triggered']))}")
|
|
|
|
if result['missing']:
|
|
print(f" ⚠️ 缺少步骤: {', '.join(sorted(result['missing']))}")
|
|
|
|
if result['extra']:
|
|
print(f" ⚠️ 多余步骤: {', '.join(sorted(result['extra']))}")
|
|
|
|
print("\n" + "="*70)
|
|
print("测试总结")
|
|
print("="*70)
|
|
|
|
passed_count = sum(1 for r in results if r['passed'])
|
|
total_count = len(results)
|
|
|
|
print(f"\n✅ 通过: {passed_count}/{total_count}")
|
|
print(f"❌ 失败: {total_count - passed_count}/{total_count}")
|
|
|
|
if passed_count == total_count:
|
|
print("\n✅ 所有场景测试通过!")
|
|
else:
|
|
print("\n❌ 部分场景测试失败,请检查配置")
|
|
|
|
print("\n" + "="*70)
|
|
|
|
return results
|
|
|
|
|
|
def main():
|
|
tester = ScenarioTester(".woodpecker.yml")
|
|
results = tester.run_all_scenarios()
|
|
|
|
failed = [r for r in results if not r['passed']]
|
|
if failed:
|
|
print("\n失败的场景:")
|
|
for result in failed:
|
|
print(f" - {result['scenario']}")
|
|
exit(1)
|
|
else:
|
|
exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|