Appearance
查看单个类的覆盖情况
执行 mvn test 命令,然后打开浏览器查看动作
quick-coverage.sh
#!/bin/bash
# JaCoCo覆盖率检查脚本
# 用法: ./check-coverage.sh [类名]
# 例如: ./check-coverage.sh PickGoodsSelfProductAppService
# 例如: ./check-coverage.sh PickGoodsPlatformActivityDomainService
# 默认类名
DEFAULT_CLASS="SelfActivityApplyProductService"
CLASS_NAME=${1:-$DEFAULT_CLASS}
set -e
# 帮助信息
if [| "$1" == "--help"](); then
echo "JaCoCo覆盖率检查脚本"
echo ""
echo "用法:"
echo " $0 [类名]"
echo ""
echo "参数:"
echo " 类名 要检查覆盖率的Java类名(不包含.java扩展名)"
echo " 如果不提供,则使用默认类: PickGoodsSelfProductAppService"
echo ""
echo "示例:"
echo " $0 # 使用默认类,生成 coverage-analysis-PickGoodsSelfProductAppService.md" echo " $0 PickGoodsSelfProductAppService # 检查应用服务类,生成对应的分析文件"
echo " $0 PickGoodsPlatformActivityDomainService # 检查领域服务类,生成对应的分析文件"
echo ""
echo "选项:"
echo " -h, --help 显示此帮助信息"
exit 0
fi
# 覆盖率阈值设置
COVERAGE_THRESHOLD=80
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} JaCoCo 覆盖率检查脚本${NC}"
echo -e "${BLUE}========================================${NC}"
echo -e "目标类: ${YELLOW}${CLASS_NAME}${NC}"
echo ""
# 切换到项目根目录
cd "$(dirname "$0")/.."
# 1. 运行测试并生成JaCoCo报告
echo -e "${BLUE}步骤1: 运行测试并生成JaCoCo报告...${NC}"
./mvnw test -Dmaven.test.failure.ignore=true jacoco:report -pl soms-service-start -q
if [ $? -eq 0 ]; then
echo -e "${GREEN}✓ 测试和报告生成完成${NC}"
else
echo -e "${RED}✗ 测试或报告生成失败${NC}"
exit 1
fi
# 2. 动态查找类的包路径和检查报告文件
JACOCO_REPORT_DIR="soms-service-start/target/jacoco"
echo ""
echo -e "${BLUE}步骤2: 查找类的包路径...${NC}"
# 查找类文件以确定正确的包路径
CLASS_FILE=$(find . -name "${CLASS_NAME}.java" -path "*/src/main/java/*" | head -1)
if [ -z "$CLASS_FILE" ]; then
echo -e "${RED}✗ 未找到类文件: ${CLASS_NAME}.java${NC}"
echo -e "${YELLOW}请检查类名是否正确${NC}"
exit 1
fi
# 从文件路径提取包路径
PACKAGE_PATH=$(echo "$CLASS_FILE" | sed 's|.*/src/main/java/||' | sed 's|/[^/]*\.java$||')
echo -e "${GREEN}✓ 找到类文件: ${PACKAGE_PATH}/${CLASS_NAME}.java${NC}"
# 构建HTML报告文件路径 - JaCoCo使用点号分隔包名
PACKAGE_PATH_FOR_HTML=$(echo "$PACKAGE_PATH" | sed 's|/|.|g')
HTML_REPORT_FILE="${JACOCO_REPORT_DIR}/${PACKAGE_PATH_FOR_HTML}/${CLASS_NAME}.java.html"
echo ""
echo -e "${BLUE}步骤3: 检查报告文件...${NC}"
if [ -f "$HTML_REPORT_FILE" ]; then
echo -e "${GREEN}✓ 找到覆盖率报告文件${NC}"
else
echo -e "${RED}✗ 未找到覆盖率报告文件: $HTML_REPORT_FILE${NC}"
echo -e "${YELLOW}可能的原因:${NC}"
echo -e " - 类名不正确"
echo -e " - 测试未执行成功"
echo -e " - JaCoCo报告未生成"
exit 1
fi
# 4. 解析XML报告获取覆盖率数据
XML_REPORT_FILE="${JACOCO_REPORT_DIR}/jacoco.xml"
echo ""
echo -e "${BLUE}步骤4: 解析覆盖率数据...${NC}"
if [ -f "$XML_REPORT_FILE" ]; then
# 提取特定类的覆盖率信息
python3 << EOF
import xml.etree.ElementTree as ET
import sys
try:
tree = ET.parse('$XML_REPORT_FILE') root = tree.getroot() # 转换包路径格式 (将/替换为.)
package_name = '$PACKAGE_PATH'.replace('/', '.') # 查找目标类
target_class = None for package in root.findall('.//package[@name="$PACKAGE_PATH"]'): for clazz in package.findall('.//class'): class_name = clazz.get('name', '') if '$CLASS_NAME' in class_name: target_class = clazz break if target_class is not None: break if target_class is None:
print("未找到类 $CLASS_NAME 的覆盖率数据")
sys.exit(1) print("\\n" + "="*60)
print(f"📊 {target_class.get('name')} 覆盖率报告")
print("="*60) # 获取各种覆盖率指标
counters = target_class.findall('counter') coverage_data = {} for counter in counters:
counter_type = counter.get('type') missed = int(counter.get('missed', 0)) covered = int(counter.get('covered', 0)) total = missed + covered if total > 0:
percentage = (covered / total) * 100 coverage_data[counter_type] = { 'covered': covered, 'missed': missed, 'total': total, 'percentage': percentage } # 显示覆盖率统计
metrics = [ ('INSTRUCTION', '指令覆盖率'),
('BRANCH', '分支覆盖率'),
('LINE', '行覆盖率'),
('METHOD', '方法覆盖率'),
('CLASS', '类覆盖率')
] for metric_key, metric_name in metrics:
if metric_key in coverage_data: data = coverage_data[metric_key] percentage = data['percentage'] status = "🟢" if percentage >= 80 else "🟡" if percentage >= 60 else "🔴"
print(f"{status} {metric_name:12} {percentage:6.1f}% ({data['covered']}/{data['total']})") else: print(f"⚪ {metric_name:12} 无数据")
print("="*60)
# 生成方法级别的覆盖率进度报告
methods_data = [] for method in target_class.findall('method'): method_name = method.get('name', 'unknown') method_line = method.get('line', 'unknown') method_coverage = {}
for counter in method.findall('counter'): counter_type = counter.get('type') missed = int(counter.get('missed', 0)) covered = int(counter.get('covered', 0)) total = missed + covered if total > 0:
percentage = (covered / total) * 100 method_coverage[counter_type] = { 'covered': covered, 'missed': missed, 'total': total, 'percentage': percentage } # 计算方法的综合状态
line_coverage = method_coverage.get('LINE', {}) instruction_coverage = method_coverage.get('INSTRUCTION', {}) branch_coverage = method_coverage.get('BRANCH', {}) if line_coverage:
line_pct = line_coverage['percentage'] status = "🟢 已达标" if line_pct >= 80 else "🟡 部分覆盖" if line_pct >= 50 else "🔴 未达标"
else: line_pct = 0 status = "🔴 未覆盖"
methods_data.append({
'name': method_name, 'line': method_line, 'line_coverage': line_coverage, 'instruction_coverage': instruction_coverage, 'branch_coverage': branch_coverage, 'line_percentage': line_pct, 'status': status }) print("生成未覆盖方法代码文件...")
except Exception as e:
print(f"解析XML报告时出错: {e}")
sys.exit(1)EOF
# 5. 生成覆盖率分析报告
echo ""
echo -e "${BLUE}步骤5: 生成覆盖率分析报告...${NC}"
python3 << EOF
import xml.etree.ElementTree as ET
import sys
try:
tree = ET.parse('$XML_REPORT_FILE') root = tree.getroot() # 查找目标类
target_class = None for package in root.findall('.//package[@name="$PACKAGE_PATH"]'): for clazz in package.findall('.//class'): class_name = clazz.get('name', '') if '$CLASS_NAME' in class_name: target_class = clazz break if target_class is not None: break if target_class is None:
print("未找到类 $CLASS_NAME 的覆盖率数据")
sys.exit(1) # 获取各种覆盖率指标
counters = target_class.findall('counter') coverage_data = {} for counter in counters:
counter_type = counter.get('type') missed = int(counter.get('missed', 0)) covered = int(counter.get('covered', 0)) total = missed + covered if total > 0:
percentage = (covered / total) * 100 coverage_data[counter_type] = { 'covered': covered, 'missed': missed, 'total': total, 'percentage': percentage } # 生成方法级别的覆盖率进度报告
methods_data = [] for method in target_class.findall('method'): method_name = method.get('name', 'unknown') method_line = method.get('line', 'unknown') method_coverage = {}
for counter in method.findall('counter'): counter_type = counter.get('type') missed = int(counter.get('missed', 0)) covered = int(counter.get('covered', 0)) total = missed + covered if total > 0:
percentage = (covered / total) * 100 method_coverage[counter_type] = { 'covered': covered, 'missed': missed, 'total': total, 'percentage': percentage } # 计算方法的综合状态
line_coverage = method_coverage.get('LINE', {}) if line_coverage:
line_pct = line_coverage['percentage'] else: line_pct = 0 methods_data.append({
'name': method_name, 'line': method_line, 'line_coverage': line_coverage, 'line_percentage': line_pct }) # 生成简洁的覆盖率分析文件
with open(f'plan/coverage-analysis-$CLASS_NAME.md', 'w', encoding='utf-8') as f: f.write(f"# {target_class.get('name').split('/')[-1]} 覆盖率分析\\n\\n")
# 整体覆盖率 - 只显示关键指标
f.write("## 整体覆盖率\\n\\n")
class_coverage = coverage_data.get('CLASS', {}).get('percentage', 0) method_coverage = coverage_data.get('METHOD', {}).get('percentage', 0) line_coverage = coverage_data.get('LINE', {}).get('percentage', 0) branch_coverage = coverage_data.get('BRANCH', {}).get('percentage', 0) f.write(f"**类覆盖率**: {class_coverage:.1f}%\\n")
f.write(f"**方法覆盖率**: {method_coverage:.1f}%\\n")
f.write(f"**行覆盖率**: {line_coverage:.1f}%\\n")
f.write(f"**分支覆盖率**: {branch_coverage:.1f}%\\n\\n")
# 未达标方法
threshold = $COVERAGE_THRESHOLD underperforming_methods = [m for m in methods_data if m['line_percentage'] < threshold and not m['name'].startswith('lambda')] if underperforming_methods: f.write(f"## 未达到{threshold}%覆盖率的方法\\n\\n")
for method in underperforming_methods: f.write(f"- **{method['name']}** (行 {method['line']}) - {method['line_percentage']:.1f}%\\n") else: f.write(f"## ✅ 所有方法都达到{threshold}%覆盖率\\n")
print("\\n📄 已生成覆盖率分析报告:")
print(f" 🔍 plan/coverage-analysis-$CLASS_NAME.md")
except Exception as e:
print(f"解析XML报告时出错: {e}")
sys.exit(1)EOF
else
echo -e "${RED}✗ 未找到XML报告文件: $XML_REPORT_FILE${NC}"
exit 1
fi
# 6. 打开HTML报告
echo ""
echo -e "${BLUE}步骤6: 打开覆盖率报告...${NC}"
# 获取绝对路径
ABS_HTML_PATH="$(pwd)/$HTML_REPORT_FILE"
echo -e "${GREEN}✓ 覆盖率检查完成!${NC}"
echo ""
echo -e "${YELLOW}📊 报告文件位置:${NC}"
echo -e " HTML报告: $ABS_HTML_PATH"
echo -e " 分析报告: $(pwd)/plan/coverage-analysis-${CLASS_NAME}.md"
echo ""
echo -e "${YELLOW}🌐 浏览器访问:${NC}"
echo -e " file://$ABS_HTML_PATH"
echo ""
echo -e "${YELLOW}💡 IDE本地服务器访问:${NC}"
echo -e " http://localhost:63342/soms-service/soms-service-start/target/jacoco/${PACKAGE_PATH_FOR_HTML}/${CLASS_NAME}.java.html"
echo ""
# 尝试自动打开浏览器 (可选)
#if command -v open >/dev/null 2>&1; then
# echo -e "${BLUE}正在尝试打开浏览器...${NC}"
# open "file://$ABS_HTML_PATH" 2>/dev/null || echo -e "${YELLOW}无法自动打开浏览器,请手动打开上述链接${NC}"
#elif command -v xdg-open >/dev/null 2>&1; then
# echo -e "${BLUE}正在尝试打开浏览器...${NC}"
# xdg-open "file://$ABS_HTML_PATH" 2>/dev/null || echo -e "${YELLOW}无法自动打开浏览器,请手动打开上述链接${NC}"
#else
# echo -e "${YELLOW}请手动在浏览器中打开上述链接查看详细报告${NC}"
#fi
echo ""
echo -e "${GREEN}🎉 脚本执行完成!${NC}"方法场景计划文件
# PickGoodsSelfProductAppService.plmApply 方法测试场景计划
## 方法签名
```java
public Result<PickGoodsProductApplyResponse> plmApply(TrendFollowPlmProductApplyRequest request)
```
## 方法逻辑分析
该方法是一个简单的代理方法,主要职责:
1. 接收 `TrendFollowPlmProductApplyRequest` 请求参数
2. 通过 `PickGoodsSelfProductConvertor.INSTANCE.toModel(request)` 转换请求模型
3. 调用 `pickGoodsSelfProductDomainService.plmApply()` 执行业务逻辑
4. 将返回的 List 包装成 Result 对象返回
## 测试场景规划
### 场景1:正常流程 - 成功处理请求
- [x] **测试目标**: 验证正常请求的处理流程
- **测试数据**: 创建有效的 TrendFollowPlmProductApplyRequest 对象
- **Mock设置**:
- pickGoodsSelfProductDomainService.plmApply() 返回包含数据的 List
- **预期结果**:
- 返回 Result 对象,其中 data 为 List,total 为 List.size()
- 验证转换器和领域服务的调用
### 场景2:空列表返回
- [x] **测试目标**: 验证领域服务返回空列表时的处理
- **测试数据**: 创建有效的 TrendFollowPlmProductApplyRequest 对象
- **Mock设置**:
- pickGoodsSelfProductDomainService.plmApply() 返回空 List
- **预期结果**:
- 返回 Result 对象,其中 data 为空 List,total 为 0
### 场景3:空参数输入
- [x] **测试目标**: 验证 null 参数的处理
- **测试数据**: request = null
- **Mock设置**:
- 根据转换器和领域服务的行为设置相应的 Mock
- **预期结果**:
- 如果转换器或领域服务抛出异常,则应该抛出相应异常
- 如果能正常处理,则返回相应结果
### 场景4:多条数据返回
- [x] **测试目标**: 验证返回多条数据时的处理
- **测试数据**: 创建有效的 TrendFollowPlmProductApplyRequest 对象
- **Mock设置**:
- pickGoodsSelfProductDomainService.plmApply() 返回包含多个元素的 List (例如3个)
- **预期结果**:
- 返回 Result 对象,其中 data 包含3个元素,total 为 3
### 场景5:领域服务异常处理
- [x] **测试目标**: 验证领域服务抛出异常时的处理
- **测试数据**: 创建有效的 TrendFollowPlmProductApplyRequest 对象
- **Mock设置**:
- pickGoodsSelfProductDomainService.plmApply() 抛出 RuntimeException
- **预期结果**:
- 异常应该向上传播,不被捕获
## 依赖分析
- **PickGoodsSelfProductConvertor.INSTANCE**: 静态转换器实例
- **pickGoodsSelfProductDomainService**: 注入的领域服务
## 测试重要性
- 该方法覆盖率为 0%,是优先处理的目标
- 作为应用服务层的入口方法,需要确保其正确性
- 验证请求转换和结果包装的逻辑
## 实现注意事项
- 需要 Mock PickGoodsSelfProductDomainService
- 可能需要 Mock 静态方法 PickGoodsSelfProductConvertor.INSTANCE.toModel()
- 验证 Result 对象的构造是否正确当前分支的增量覆盖率
diff_cover_2.sh
#!/usr/bin/env bash
set -euo pipefail
# ===== 配置参数 =====MODULES=${MODULES:-"soms-service-start"} # 多个模块用逗号分隔,为空时自动检测
THRESHOLD=${THRESHOLD:-100} # 覆盖率阈值
OUTPUT_DIR=${OUTPUT_DIR:-"./reports"} # 报告输出目录
SKIP_TESTS=${SKIP_TESTS:-0} # 跳过测试执行: 1=跳过
FORCE_ANALYSIS=${FORCE_ANALYSIS:-0} # 强制分析: 1=强制
BASE_BRANCH=${BASE_BRANCH:-""} # 基准分支
# ===== 颜色定义 =====RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# ===== 日志函数 =====log_info() {
echo -e "${BLUE}ℹ️ $1${NC}" >&2
}
log_success() {
echo -e "${GREEN}✅ $1${NC}" >&2
}
log_warning() {
echo -e "${YELLOW}⚠️ $1${NC}" >&2
}
log_error() {
echo -e "${RED}❌ $1${NC}" >&2
}
# ===== 自动检测远程分支 =====detect_base_branch() {
log_info "自动检测远程基准分支..."
# 获取远程默认分支
local default_branch
default_branch=$(git remote show origin | grep -E "HEAD 分支|HEAD branch" | sed 's/.*[::] *//' | xargs 2>/dev/null || echo "main")
# 尝试获取远程分支信息
if git fetch origin >/dev/null 2>&1; then
BASE_BRANCH="origin/${default_branch}"
log_success "检测到远程基准分支: $BASE_BRANCH"
else
log_error "无法获取远程分支信息,请检查网络连接"
exit 1
fi
# 检查分支是否存在
if ! git show-ref --verify --quiet "refs/remotes/$BASE_BRANCH" 2>/dev/null && ! git show-ref --verify --quiet "refs/heads/$BASE_BRANCH" 2>/dev/null; then
log_error "基准分支 $BASE_BRANCH 不存在"
exit 1
fi
}
# ===== 检查分支完整性 =====check_branch_integrity() {
log_info "检查当前分支是否包含基准分支的所有提交..."
# 获取当前分支名
local current_branch
current_branch=$(git rev-parse --abbrev-ref HEAD)
# 检查当前分支是否包含基准分支的所有提交
if git merge-base --is-ancestor "$BASE_BRANCH" "$current_branch" 2>/dev/null; then
log_success "当前分支 $current_branch 包含基准分支 $BASE_BRANCH 的所有提交"
else
log_warning "当前分支 $current_branch 不包含基准分支 $BASE_BRANCH 的所有提交"
log_warning "这可能导致增量覆盖率分析不准确"
# 显示缺失的提交数量
local missing_commits
missing_commits=$(git rev-list --count "$BASE_BRANCH" ^"$current_branch" 2>/dev/null || echo "未知")
log_warning "基准分支领先当前分支 $missing_commits 个提交"
# 询问是否继续
if ["${FORCE_ANALYSIS:-0}" != "1"](); then
log_warning "建议先合并基准分支或使用 --force 参数强制分析"
log_warning "设置环境变量 FORCE_ANALYSIS=1 可以跳过此检查"
exit 1
else
log_warning "已设置 FORCE_ANALYSIS=1,继续分析..."
fi
fi}
# ===== 自动检测模块 =====detect_modules() {
if [-n "$MODULES"](); then
log_info "使用指定的模块: $MODULES"
return 0
fi
log_info "自动检测项目模块..."
# 检测 Gradle 项目模块
if [-f "settings.gradle"](); then
local gradle_modules
gradle_modules=$(grep "include " settings.gradle | sed "s/.*include ['\"]//g" | sed "s/['\"].*//g" | tr '\n' ',' | sed 's/,$//')
if [-n "$gradle_modules"](); then
MODULES="$gradle_modules"
log_success "检测到 Gradle 模块: $MODULES"
return 0
fi
fi # 检测 Maven 项目模块
if [-f "pom.xml"](); then
local maven_modules
maven_modules=$(grep -E "<module>" pom.xml | sed "s/.*<module>//g" | sed "s/<\/module>.*//g" | tr '\n' ',' | sed 's/,$//')
if [-n "$maven_modules"](); then
MODULES="$maven_modules"
log_success "检测到 Maven 模块: $MODULES"
return 0
fi
fi # 默认模块检测(基于目录结构)
local detected_modules=""
for dir in */; do
if [-d "${dir}src/main/java"]() || [-d "${dir}src/main/kotlin"](); then
local module_name="${dir%/}"
if [-z "$detected_modules"](); then
detected_modules="$module_name"
else
detected_modules="$detected_modules,$module_name"
fi
fi done if [-n "$detected_modules"](); then
MODULES="$detected_modules"
log_success "检测到模块: $MODULES"
else
log_error "未检测到模块,请手动指定 MODULES 环境变量"
exit 1
fi
}
# ===== 检查环境 =====check_environment() {
log_info "检查环境依赖..."
# 检查必要工具
for tool in git python3 pip3; do
if ! command -v "$tool" &> /dev/null; then
log_error "$tool 未安装或不在PATH中"
exit 1
fi
done # 检查并安装 diff-cover if ! python3 -c "import diff_cover" 2>/dev/null; then
log_info "安装 diff-cover..." pip3 install --break-system-packages diff_cover >/dev/null 2>&1
log_success "diff-cover 安装完成"
fi
# 设置 diff-cover 命令路径
DIFF_COVER_CMD=""
if command -v diff-cover &> /dev/null; then
DIFF_COVER_CMD="diff-cover"
elif [ -f "/Users/80892291/Library/Python/3.9/bin/diff-cover" ]; then
DIFF_COVER_CMD="/Users/80892291/Library/Python/3.9/bin/diff-cover"
else
# 尝试查找用户本地安装的 diff-cover local user_bin="$HOME/Library/Python/3.9/bin/diff-cover"
if [ -f "$user_bin" ]; then
DIFF_COVER_CMD="$user_bin"
else
log_error "未找到 diff-cover 命令"
exit 1
fi
fi log_info "使用 diff-cover 命令: $DIFF_COVER_CMD"
log_success "环境检查通过"
}
# ===== 检查覆盖率报告 =====check_coverage_report() {
local module="$1"
if [$SKIP_TESTS -eq 1](); then
log_info "[$module] 跳过测试执行,使用现有报告..."
else
log_info "[$module] 检查覆盖率报告..."
log_warning "请确保已运行测试并生成了 Jacoco 报告"
log_warning "例如: mvn test jacoco:report -pl $module"
fi
local coverage_xml="$module/target/jacoco/jacoco.xml"
if [ ! -f "$coverage_xml" ]; then
log_error "未找到 Jacoco 报告: $coverage_xml"
log_error "请先运行测试生成覆盖率报告"
return 1
fi
log_success "找到 Jacoco 报告: $coverage_xml"
echo "$coverage_xml"
}
# ===== 转换Jacoco XML为Cobertura XML =====
convert_jacoco_to_cobertura() {
local jacoco_xml="$1"
local cobertura_xml="$2"
log_info "转换 Jacoco XML 为 Cobertura XML 格式..."
# 创建临时转换脚本
local script_file="/tmp/jacoco_to_cobertura_$$.py"
cat > "$script_file" << 'EOF'
#!/usr/bin/env python3
import xml.etree.ElementTree as ET
import sys
import os
import time
def convert_jacoco_to_cobertura(jacoco_file, cobertura_file):
try: tree = ET.parse(jacoco_file) root = tree.getroot() coverage = ET.Element('coverage')
coverage.set('line-rate', '0.0') coverage.set('branch-rate', '0.0') coverage.set('lines-covered', '0') coverage.set('lines-valid', '0') coverage.set('branches-covered', '0') coverage.set('branches-valid', '0') coverage.set('complexity', '0.0') coverage.set('version', '1.9') coverage.set('timestamp', str(int(time.time()))) sources = ET.SubElement(coverage, 'sources')
source = ET.SubElement(sources, 'source') # 动态获取模块名和源码路径
jacoco_dir = os.path.dirname(jacoco_file) # 从 jacoco.xml 路径推导模块路径: dps-application/target/jacoco/jacoco.xml -> dps-application
module_name = os.path.basename(os.path.dirname(os.path.dirname(jacoco_dir))) source.text = os.path.join(os.getcwd(), module_name, 'src', 'main', 'java') packages = ET.SubElement(coverage, 'packages')
total_lines_covered = 0 total_lines_valid = 0 for package in root.findall('.//package'):
pkg_name = package.get('name', '') pkg_lines_covered = 0 pkg_lines_valid = 0 pkg_elem = ET.SubElement(packages, 'package')
pkg_elem.set('name', pkg_name) pkg_elem.set('line-rate', '0.0') pkg_elem.set('branch-rate', '0.0') pkg_elem.set('complexity', '0.0') classes = ET.SubElement(pkg_elem, 'classes')
for sourcefile in package.findall('.//sourcefile'):
class_name = sourcefile.get('name', '').replace('.java', '') class_lines_covered = 0 class_lines_valid = 0 class_elem = ET.SubElement(classes, 'class')
class_elem.set('name', class_name) class_elem.set('filename', f"{pkg_name.replace('.', '/')}/{sourcefile.get('name', '')}") class_elem.set('line-rate', '0.0') class_elem.set('branch-rate', '0.0') class_elem.set('complexity', '0.0') lines = ET.SubElement(class_elem, 'lines')
for line in sourcefile.findall('.//line'):
line_num = line.get('nr', '0') line_hits = int(line.get('ci', '0')) line_elem = ET.SubElement(lines, 'line')
line_elem.set('number', line_num) line_elem.set('hits', str(line_hits)) line_elem.set('branch', 'false') class_lines_valid += 1
if line_hits > 0: class_lines_covered += 1 if class_lines_valid > 0:
class_rate = class_lines_covered / class_lines_valid class_elem.set('line-rate', f"{class_rate:.4f}") pkg_lines_covered += class_lines_covered
pkg_lines_valid += class_lines_valid if pkg_lines_valid > 0:
pkg_rate = pkg_lines_covered / pkg_lines_valid pkg_elem.set('line-rate', f"{pkg_rate:.4f}") total_lines_covered += pkg_lines_covered
total_lines_valid += pkg_lines_valid if total_lines_valid > 0:
total_rate = total_lines_covered / total_lines_valid coverage.set('line-rate', f"{total_rate:.4f}") coverage.set('lines-covered', str(total_lines_covered)) coverage.set('lines-valid', str(total_lines_valid)) tree = ET.ElementTree(coverage)
ET.indent(tree, space=" ", level=0) tree.write(cobertura_file, encoding='utf-8', xml_declaration=True) return True except Exception as e:
print(f"转换失败: {e}", file=sys.stderr)
return False
if __name__ == "__main__":
if len(sys.argv) != 3: print("用法: python3 jacoco_to_cobertura.py <jacoco.xml> <cobertura.xml>", file=sys.stderr)
sys.exit(1) if convert_jacoco_to_cobertura(sys.argv[1], sys.argv[2]):
print(f"转换成功: {sys.argv[2]}")
sys.exit(0) else: sys.exit(1)EOF
# 执行转换
if python3 "$script_file" "$jacoco_xml" "$cobertura_xml"; then
log_success "转换完成: $cobertura_xml"
rm -f "$script_file"
return 0
else
log_error "转换失败"
rm -f "$script_file"
return 1
fi
}
# ===== 使用diff-cover分析增量覆盖率 =====analyze_diff_coverage() {
local module="$1"
local cobertura_xml="$2"
log_info "[$module] 使用 diff-cover 分析增量覆盖率..."
# 创建报告目录
mkdir -p "$OUTPUT_DIR"
local html_report="$OUTPUT_DIR/${module}-diff-cover-report.html"
local json_report="$OUTPUT_DIR/${module}-diff-cover-report.json"
# 执行diff-cover,指定源码根目录以正确匹配路径
# 添加更多报告选项以增强覆盖情况展示
local diff_output
if diff_output=$("$DIFF_COVER_CMD" "$cobertura_xml" \
--compare-branch "$BASE_BRANCH" \
--src-roots "$module/src/main/java" \
--format "html:$html_report" \
--format "json:$json_report" \
--format "markdown:$OUTPUT_DIR/${module}-diff-cover-report.md" \
--show-uncovered \
--fail-under 0 2>&1); then
log_success "[$module] diff-cover 分析完成"
log_info "报告文件:"
log_info " - HTML: $html_report"
log_info " - JSON: $json_report"
log_info " - Markdown: $OUTPUT_DIR/${module}-diff-cover-report.md"
# 显示详细的覆盖率信息
echo ""
echo "========================================"
echo "📊 [$module] 增量覆盖率详情"
echo "========================================"
# 提取并显示覆盖率摘要信息
local coverage_summary=$(echo "$diff_output" | sed -n '/^Total:/,/^Coverage:/p')
if [-n "$coverage_summary"](); then
echo "$coverage_summary"
fi
# 显示文件级别的覆盖率信息
local file_coverage=$(echo "$diff_output" | grep -E "\.java \([0-9.]+%\):" | head -10)
if [-n "$file_coverage"](); then
echo ""
echo "📁 文件覆盖率详情:"
echo "$file_coverage" | while read -r line; do
echo " $line"
done
# 如果文件超过10个,显示省略信息
local total_files=$(echo "$diff_output" | grep -c "\.java \([0-9.]+%\):")
if [$total_files -gt 10](); then
echo " ... 还有 $((total_files - 10)) 个文件"
fi
fi echo "========================================"
echo ""
# 从控制台输出中解析覆盖率
local coverage_percent=$(echo "$diff_output" | grep "Coverage:" | sed 's/.*Coverage: \([0-9.]*\)%.*/\1/')
if [-z "$coverage_percent"](); then
# 如果没有找到覆盖率信息,检查是否有"No lines with coverage information"
if echo "$diff_output" | grep -q "No lines with coverage information"; then
log_info "[$module] 没有检测到需要覆盖的代码行"
return 0
else
log_warning "[$module] 无法解析覆盖率信息"
return 0
fi
fi if (( $(echo "$coverage_percent < $THRESHOLD" | bc -l) )); then
log_warning "[$module] 覆盖率 $coverage_percent% 低于阈值 $THRESHOLD%"
return 0 # 阈值不达标只是警告,不返回失败
else
log_success "[$module] 覆盖率 $coverage_percent% 达到阈值 $THRESHOLD%"
return 0
fi
else log_error "[$module] diff-cover 分析失败"
return 1
fi
}
# ===== 主函数 =====main() {
log_info "开始基于 diff-cover 的增量覆盖率分析..."
check_environment
detect_modules
detect_base_branch
check_branch_integrity
local success_count=0
local total_count=0
IFS=',' read -ra MODULE_LIST <<< "$MODULES"
for module in "${MODULE_LIST[@]}"; do
module=$(echo "$module" | xargs)
total_count=$((total_count + 1))
log_info "处理模块: $module"
# 检查覆盖率报告
local coverage_xml
if ! coverage_xml=$(check_coverage_report "$module"); then
log_error "模块 $module 处理失败"
continue
fi
# 转换Jacoco XML为Cobertura XML
local cobertura_xml="$module/target/jacoco/cobertura.xml"
# 确保目标目录存在
mkdir -p "$(dirname "$cobertura_xml")"
if ! convert_jacoco_to_cobertura "$coverage_xml" "$cobertura_xml"; then
log_error "模块 $module XML转换失败"
continue
fi
# 使用diff-cover分析
if analyze_diff_coverage "$module" "$cobertura_xml"; then
success_count=$((success_count + 1))
else
log_error "模块 $module diff-cover分析失败"
fi
done # 总结
echo ""
echo "========================================"
echo "📊 分析完成"
echo "========================================"
echo "成功处理模块: $success_count/$total_count"
echo "报告目录: $OUTPUT_DIR"
if [ $success_count -eq $total_count ]; then
log_success "所有模块分析完成"
exit 0
else
log_error "部分模块分析失败"
exit 1
fi
}
# ===== 脚本入口 =====main "$@"当前分支的增量覆盖率读取第二个版本
diff_cover_venv.sh
#!/usr/bin/env bash
set -euo pipefail
# ===== 合并版本:虚拟环境 + diff-cover 分析脚本 =====# 特点:单文件,用完即抛,不修改工程代码
# ===== 配置参数 =====MODULES=${MODULES:-"soms-service-start"} # 多个模块用逗号分隔,为空时自动检测
THRESHOLD=${THRESHOLD:-100} # 覆盖率阈值
OUTPUT_DIR=${OUTPUT_DIR:-"./reports"} # 报告输出目录
SKIP_TESTS=${SKIP_TESTS:-0} # 跳过测试执行检查,使用现有报告: 1=跳过
FORCE_ANALYSIS=${FORCE_ANALYSIS:-0} # 强制分析: 1=强制
BASE_BRANCH=${BASE_BRANCH:-""} # 基准分支
SKIP_BRANCH_CHECK=${SKIP_BRANCH_CHECK:-0} # 跳过远程分支比对: 1=跳过
# 虚拟环境相关配置
VENV_DIR=${VENV_DIR:-"./temp_venv"} # 临时虚拟环境目录
CLEANUP_VENV=${CLEANUP_VENV:-1} # 执行后是否清理虚拟环境: 1=清理
# ===== 颜色定义 =====RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# ===== 日志函数 =====log_info() {
echo -e "${BLUE}ℹ️ $1${NC}" >&2
}
log_success() {
echo -e "${GREEN}✅ $1${NC}" >&2
}
log_warning() {
echo -e "${YELLOW}⚠️ $1${NC}" >&2
}
log_error() {
echo -e "${RED}❌ $1${NC}" >&2
}
# ===== 检查环境依赖 =====check_environment() {
log_info "检查环境依赖..."
# 检查必要工具
for tool in git python3 bc; do
if ! command -v "$tool" &> /dev/null; then
log_error "$tool 未安装或不在PATH中"
log_info "请安装 $tool 后重试"
exit 1
fi
done log_success "环境检查通过"
}
# ===== 虚拟环境管理 =====create_temp_venv() {
if [-d "$VENV_DIR"](); then
if ["$CLEANUP_VENV" == "1"](); then
log_info "发现已存在的虚拟环境,清理后重建: $VENV_DIR"
rm -rf "$VENV_DIR"
else
log_info "发现已存在的虚拟环境,复用现有环境: $VENV_DIR"
# 检查现有环境是否可用
if [-f "$VENV_DIR/bin/activate"](); then
log_success "复用现有虚拟环境"
return 0
else
log_warning "现有虚拟环境不完整,重建..."
rm -rf "$VENV_DIR"
fi
fi fi log_info "创建临时虚拟环境: $VENV_DIR"
# 创建虚拟环境
if python3 -m venv "$VENV_DIR" >/dev/null 2>&1; then
log_success "虚拟环境创建成功"
else
log_error "虚拟环境创建失败"
exit 1
fi
}
setup_venv() {
log_info "激活虚拟环境并安装依赖..."
# 激活虚拟环境
source "$VENV_DIR/bin/activate"
log_success "虚拟环境已激活"
# 检查diff-cover是否已安装
if python -c "import diff_cover" 2>/dev/null; then
log_success "diff-cover已安装,跳过安装步骤"
return 0
fi
# 升级pip
log_info "升级pip..."
pip install --upgrade pip >/dev/null 2>&1
# 安装diff-cover
log_info "安装diff-cover..."
if pip install diff-cover >/dev/null 2>&1; then
log_success "diff-cover安装完成"
else
log_error "diff-cover安装失败"
exit 1
fi
# 验证安装
if python -c "import diff_cover" 2>/dev/null; then
log_success "依赖验证通过"
else
log_error "依赖验证失败"
exit 1
fi
}
cleanup_venv() {
if ["$CLEANUP_VENV" == "1" && -d "$VENV_DIR"](); then
log_info "清理临时虚拟环境: $VENV_DIR"
rm -rf "$VENV_DIR"
log_success "临时虚拟环境已清理"
fi
}
# ===== 验证分支是否存在 =====validate_branch() {
local branch="$1"
local branch_type="$2"
if git show-ref --verify --quiet "refs/remotes/$branch" 2>/dev/null; then
log_success "$branch_type 验证通过: $branch"
return 0
else
log_error "$branch_type $branch 不存在"
log_info "可用的分支:"
git branch -a 2>/dev/null | head -10 | while read -r line; do
log_info " $line"
done
log_info ""
log_info "解决方案:"
log_info " 1. 手动指定远程分支: BASE_BRANCH=origin/your-branch ./diff_cover_venv.sh"
log_info " 2. 拉取远程分支: git fetch origin"
log_info " 3. 检查远程分支是否存在: git branch -r"
exit 1
fi
}
# ===== 自动检测远程分支 =====detect_base_branch() {
# 如果用户已经指定了基准分支,直接使用
if [-n "$BASE_BRANCH"](); then
log_info "使用用户指定的基准分支: $BASE_BRANCH"
validate_branch "$BASE_BRANCH" "基准分支"
return 0
fi
# 如果设置了跳过远程分支比对,使用当前分支
if ["$SKIP_BRANCH_CHECK" == "1"](); then
log_info "跳过远程分支比对,使用当前分支作为基准"
BASE_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")
log_success "使用当前分支作为基准: $BASE_BRANCH"
return 0
fi
log_info "自动检测远程基准分支..."
# 检查是否有远程仓库配置
if ! git remote get-url origin >/dev/null 2>&1; then
log_error "未配置远程仓库 origin,请检查 Git 配置"
log_info "解决方案:"
log_info " 1. 检查是否在 Git 仓库中: git status"
log_info " 2. 添加远程仓库: git remote add origin <仓库URL>"
exit 1
fi
# 只从远程获取默认分支,不允许降级到本地分支
local default_branch=""
if ! git fetch origin >/dev/null 2>&1; then
log_error "无法连接到远程仓库 origin" log_info "解决方案:"
log_info " 1. 检查网络连接"
log_info " 2. 检查远程仓库配置: git remote -v"
log_info " 3. 手动指定远程分支: BASE_BRANCH=origin/main ./diff_cover_venv.sh"
exit 1
fi
# 尝试多种方式获取远程默认分支
default_branch=$(git remote show origin | sed -n '/HEAD branch/s/.*: //p' 2>/dev/null)
# 如果上面失败,尝试从远程分支列表获取
if [-z "$default_branch"](); then
default_branch=$(git ls-remote --symref origin HEAD | sed -n 's/.*refs\/heads\/\([^[:space:]]*\).*/\1/p' 2>/dev/null)
fi
# 如果还是失败,尝试常见的默认分支名
if [-z "$default_branch"](); then
for branch in main master develop; do
if git ls-remote --heads origin "$branch" | grep -q "$branch"; then
default_branch="$branch"
log_info "通过远程分支检测到默认分支: $branch"
break
fi
done fi # 如果仍然找不到远程分支,报错
if [-z "$default_branch"](); then
log_error "无法检测到远程默认分支"
log_info "可用的远程分支:"
git ls-remote --heads origin 2>/dev/null | sed 's/.*refs\/heads\///' | head -10 | while read -r branch; do
log_info " origin/$branch"
done
log_info ""
log_info "解决方案:"
log_info " 1. 手动指定远程分支: BASE_BRANCH=origin/main ./diff_cover_venv.sh"
log_info " 2. 检查远程仓库是否有默认分支"
exit 1
fi
# 设置基准分支 - 只使用远程分支
if ["$default_branch" == *"origin/"*](); then
BASE_BRANCH="$default_branch"
else
# 只使用远程分支,不允许降级到本地分支
BASE_BRANCH="origin/$default_branch"
fi
log_success "检测到基准分支: $BASE_BRANCH"
# 最终验证远程分支是否存在
validate_branch "$BASE_BRANCH" "基准分支"
}
# ===== 检查分支完整性 =====check_branch_integrity() {
# 如果设置了跳过远程分支比对,也跳过分支完整性检查
if ["$SKIP_BRANCH_CHECK" == "1"](); then
log_info "跳过分支完整性检查"
return 0
fi
log_info "检查当前分支是否包含基准分支的所有提交..."
# 获取当前分支名
local current_branch
current_branch=$(git rev-parse --abbrev-ref HEAD)
# 检查当前分支是否包含基准分支的所有提交
if git merge-base --is-ancestor "$BASE_BRANCH" "$current_branch" 2>/dev/null; then
log_success "当前分支 $current_branch 包含基准分支 $BASE_BRANCH 的所有提交"
else
log_warning "当前分支 $current_branch 不包含基准分支 $BASE_BRANCH 的所有提交"
log_warning "这可能导致增量覆盖率分析不准确"
# 显示缺失的提交数量
local missing_commits
missing_commits=$(git rev-list --count "$BASE_BRANCH" ^"$current_branch" 2>/dev/null || echo "未知")
log_warning "基准分支领先当前分支 $missing_commits 个提交"
# 询问是否继续
if ["${FORCE_ANALYSIS:-0}" != "1"](); then
log_warning "建议先合并基准分支或使用 --force 参数强制分析"
log_warning "设置环境变量 FORCE_ANALYSIS=1 可以跳过此检查"
exit 1
else
log_warning "已设置 FORCE_ANALYSIS=1,继续分析..."
fi
fi}
# ===== 自动检测模块 =====detect_modules() {
if [-n "$MODULES"](); then
log_info "使用指定的模块: $MODULES"
return 0
fi
log_info "自动检测项目模块..."
# 检测 Gradle 项目模块
if [-f "settings.gradle"](); then
local gradle_modules
gradle_modules=$(grep "include " settings.gradle | sed "s/.*include ['\"]//g" | sed "s/['\"].*//g" | tr '\n' ',' | sed 's/,$//')
if [-n "$gradle_modules"](); then
MODULES="$gradle_modules"
log_success "检测到 Gradle 模块: $MODULES"
return 0
fi
fi # 检测 Maven 项目模块
if [-f "pom.xml"](); then
local maven_modules
maven_modules=$(grep -E "<module>" pom.xml | sed "s/.*<module>//g" | sed "s/<\/module>.*//g" | tr '\n' ',' | sed 's/,$//')
if [-n "$maven_modules"](); then
MODULES="$maven_modules"
log_success "检测到 Maven 模块: $MODULES"
return 0
fi
fi # 默认模块检测(基于目录结构)
local detected_modules=""
for dir in */; do
if [-d "${dir}src/main/java"]() || [-d "${dir}src/main/kotlin"](); then
local module_name="${dir%/}"
if [-z "$detected_modules"](); then
detected_modules="$module_name"
else
detected_modules="$detected_modules,$module_name"
fi
fi done if [-n "$detected_modules"](); then
MODULES="$detected_modules"
log_success "检测到模块: $MODULES"
else
log_error "未检测到模块,请手动指定 MODULES 环境变量"
exit 1
fi
}
# ===== 检查覆盖率报告 =====check_coverage_report() {
local module="$1"
if [$SKIP_TESTS -eq 1](); then
log_info "[$module] 跳过测试执行检查,直接使用现有报告..."
else
log_info "[$module] 检查覆盖率报告..."
log_warning "请确保已运行测试并生成了 Jacoco 报告"
log_warning "例如: mvn test jacoco:report -pl $module"
fi
local coverage_xml="$module/target/jacoco/jacoco.xml"
if [ ! -f "$coverage_xml" ]; then
log_error "未找到 Jacoco 报告: $coverage_xml"
log_error "请先运行测试生成覆盖率报告"
return 1
fi
log_success "找到 Jacoco 报告: $coverage_xml"
echo "$coverage_xml"
}
# ===== 转换Jacoco XML为Cobertura XML =====
convert_jacoco_to_cobertura() {
local jacoco_xml="$1"
local cobertura_xml="$2"
log_info "转换 Jacoco XML 为 Cobertura XML 格式..."
# 创建临时转换脚本
local script_file="/tmp/jacoco_to_cobertura_$$.py"
cat > "$script_file" << 'EOF'
#!/usr/bin/env python3
import xml.etree.ElementTree as ET
import sys
import os
import time
def convert_jacoco_to_cobertura(jacoco_file, cobertura_file):
try: tree = ET.parse(jacoco_file) root = tree.getroot() coverage = ET.Element('coverage')
coverage.set('line-rate', '0.0') coverage.set('branch-rate', '0.0') coverage.set('lines-covered', '0') coverage.set('lines-valid', '0') coverage.set('branches-covered', '0') coverage.set('branches-valid', '0') coverage.set('complexity', '0.0') coverage.set('version', '1.9') coverage.set('timestamp', str(int(time.time()))) sources = ET.SubElement(coverage, 'sources')
source = ET.SubElement(sources, 'source') # 动态获取模块名和源码路径
jacoco_dir = os.path.dirname(jacoco_file) # 从 jacoco.xml 路径推导模块路径: hinton-application/target/jacoco/jacoco.xml -> hinton-application
module_name = os.path.basename(os.path.dirname(os.path.dirname(jacoco_dir))) source.text = os.path.join(os.getcwd(), module_name, 'src', 'main', 'java') packages = ET.SubElement(coverage, 'packages')
total_lines_covered = 0 total_lines_valid = 0 for package in root.findall('.//package'):
pkg_name = package.get('name', '') pkg_lines_covered = 0 pkg_lines_valid = 0 pkg_elem = ET.SubElement(packages, 'package')
pkg_elem.set('name', pkg_name) pkg_elem.set('line-rate', '0.0') pkg_elem.set('branch-rate', '0.0') pkg_elem.set('complexity', '0.0') classes = ET.SubElement(pkg_elem, 'classes')
for sourcefile in package.findall('.//sourcefile'):
class_name = sourcefile.get('name', '').replace('.java', '') class_lines_covered = 0 class_lines_valid = 0 class_elem = ET.SubElement(classes, 'class')
class_elem.set('name', class_name) class_elem.set('filename', f"{pkg_name.replace('.', '/')}/{sourcefile.get('name', '')}") class_elem.set('line-rate', '0.0') class_elem.set('branch-rate', '0.0') class_elem.set('complexity', '0.0') lines = ET.SubElement(class_elem, 'lines')
for line in sourcefile.findall('.//line'):
line_num = line.get('nr', '0') line_hits = int(line.get('ci', '0')) line_elem = ET.SubElement(lines, 'line')
line_elem.set('number', line_num) line_elem.set('hits', str(line_hits)) line_elem.set('branch', 'false') class_lines_valid += 1
if line_hits > 0: class_lines_covered += 1 if class_lines_valid > 0:
class_rate = class_lines_covered / class_lines_valid class_elem.set('line-rate', f"{class_rate:.4f}") pkg_lines_covered += class_lines_covered
pkg_lines_valid += class_lines_valid if pkg_lines_valid > 0:
pkg_rate = pkg_lines_covered / pkg_lines_valid pkg_elem.set('line-rate', f"{pkg_rate:.4f}") total_lines_covered += pkg_lines_covered
total_lines_valid += pkg_lines_valid if total_lines_valid > 0:
total_rate = total_lines_covered / total_lines_valid coverage.set('line-rate', f"{total_rate:.4f}") coverage.set('lines-covered', str(total_lines_covered)) coverage.set('lines-valid', str(total_lines_valid)) tree = ET.ElementTree(coverage)
ET.indent(tree, space=" ", level=0) tree.write(cobertura_file, encoding='utf-8', xml_declaration=True) return True except Exception as e:
print(f"转换失败: {e}", file=sys.stderr)
return False
if __name__ == "__main__":
if len(sys.argv) != 3: print("用法: python3 jacoco_to_cobertura.py <jacoco.xml> <cobertura.xml>", file=sys.stderr)
sys.exit(1) if convert_jacoco_to_cobertura(sys.argv[1], sys.argv[2]):
print(f"转换成功: {sys.argv[2]}")
sys.exit(0) else: sys.exit(1)EOF
# 执行转换
if python "$script_file" "$jacoco_xml" "$cobertura_xml"; then
log_success "转换完成: $cobertura_xml"
rm -f "$script_file"
return 0
else
log_error "转换失败"
rm -f "$script_file"
return 1
fi
}
# ===== 使用diff-cover分析增量覆盖率 =====analyze_diff_coverage() {
local module="$1"
local cobertura_xml="$2"
log_info "[$module] 使用 diff-cover 分析增量覆盖率..."
# 创建报告目录(如果不存在)
[! -d "$OUTPUT_DIR"]() && mkdir -p "$OUTPUT_DIR"
local html_report="$OUTPUT_DIR/${module}-diff-cover-report.html"
local json_report="$OUTPUT_DIR/${module}-diff-cover-report.json"
# 执行diff-cover,指定源码根目录以正确匹配路径
# 添加更多报告选项以增强覆盖情况展示
local diff_output
local diff_cmd="diff-cover \"$cobertura_xml\""
# 如果设置了跳过远程分支比对,不进行分支比对
if ["$SKIP_BRANCH_CHECK" == "1"](); then
log_info "[$module] 跳过分支比对,进行整体覆盖率分析"
diff_cmd="$diff_cmd --src-roots \"$module/src/main/java\""
else
diff_cmd="$diff_cmd --compare-branch \"$BASE_BRANCH\" --src-roots \"$module/src/main/java\""
fi
# 添加调试信息
log_info "[$module] 执行命令: $diff_cmd"
log_info "[$module] HTML报告路径: $html_report"
log_info "[$module] JSON报告路径: $json_report"
if diff_output=$(eval "$diff_cmd \ --html-report \"$html_report\" \
--json-report \"$json_report\" \
--markdown-report \"$OUTPUT_DIR/${module}-diff-cover-report.md\" \
--show-uncovered \ --fail-under 0" 2>&1); then
log_success "[$module] diff-cover 分析完成"
log_info "报告文件:"
# 检查报告文件是否真的被创建
if [-f "$html_report"](); then
log_info " - HTML: $html_report ✅"
else
log_warning " - HTML: $html_report ❌ (文件未创建)"
fi
if [-f "$json_report"](); then
log_info " - JSON: $json_report ✅"
else
log_warning " - JSON: $json_report ❌ (文件未创建)"
fi
if [-f "$OUTPUT_DIR/${module}-diff-cover-report.md"](); then
log_info " - Markdown: $OUTPUT_DIR/${module}-diff-cover-report.md ✅"
else
log_warning " - Markdown: $OUTPUT_DIR/${module}-diff-cover-report.md ❌ (文件未创建)"
fi
# 显示详细的覆盖率信息
echo ""
echo "========================================"
echo "📊 [$module] 增量覆盖率详情"
echo "========================================"
# 提取并显示覆盖率摘要信息
local coverage_summary=$(echo "$diff_output" | sed -n '/^Total:/,/^Coverage:/p')
if [-n "$coverage_summary"](); then
echo "$coverage_summary"
fi
# 显示文件级别的覆盖率信息
local file_coverage=$(echo "$diff_output" | grep -E "\.java \([0-9.]+%\):" | head -10)
if [-n "$file_coverage"](); then
echo ""
echo "📁 文件覆盖率详情:"
echo "$file_coverage" | while read -r line; do
echo " $line"
done
# 如果文件超过10个,显示省略信息
local total_files=$(echo "$diff_output" | grep -c "\.java \([0-9.]+%\):")
if [$total_files -gt 10](); then
echo " ... 还有 $((total_files - 10)) 个文件"
fi
fi echo "========================================"
echo ""
# 从控制台输出中解析覆盖率
local coverage_percent=$(echo "$diff_output" | grep "Coverage:" | sed 's/.*Coverage: \([0-9.]*\)%.*/\1/')
local total_lines=$(echo "$diff_output" | grep "Total:" | sed 's/.*Total: *\([0-9]*\) lines.*/\1/')
local missing_lines=$(echo "$diff_output" | grep "Missing:" | sed 's/.*Missing: *\([0-9]*\) lines.*/\1/')
# 调试信息(可选,用于排查问题)
# log_info "[$module] 调试 - 原始输出片段:"
# echo "$diff_output" | grep -E "(Total:|Missing:|Coverage:)" | head -3 | while read -r line; do # log_info "[$module] 调试 - $line" # done if [-z "$coverage_percent"](); then
# 如果没有找到覆盖率信息,检查是否有"No lines with coverage information"
if echo "$diff_output" | grep -q "No lines with coverage information"; then
log_info "[$module] 没有检测到需要覆盖的代码行"
COVERAGE_INFO="0,0,0" # 设置 coverage_percent,total_lines,missing_lines return 0
else
log_warning "[$module] 无法解析覆盖率信息"
COVERAGE_INFO="0,0,0" # 设置 coverage_percent,total_lines,missing_lines return 0
fi
fi # 将覆盖率信息写入全局变量供主函数使用
COVERAGE_INFO="${coverage_percent},${total_lines:-0},${missing_lines:-0}"
if (( $(echo "$coverage_percent < $THRESHOLD" | bc -l) )); then
log_warning "[$module] 覆盖率 $coverage_percent% 低于阈值 $THRESHOLD%"
return 0 # 阈值不达标只是警告,不返回失败
else
log_success "[$module] 覆盖率 $coverage_percent% 达到阈值 $THRESHOLD%"
return 0
fi
else log_error "[$module] diff-cover 分析失败"
log_error "命令输出: $diff_output"
return 1
fi
}
# ===== 显示使用帮助 =====show_help() {
cat << EOF
合并版本:虚拟环境 + diff-cover 分析脚本
用法:
$0 [选项] [环境变量...]
选项:
--venv-dir DIR 指定虚拟环境目录 (默认: ./temp_venv)
--no-cleanup 执行后不清理虚拟环境,下次运行时复用
--help, -h 显示此帮助信息
环境变量:
MODULES 要分析的模块列表 (默认: hinton-application,hinton-domain)
THRESHOLD 覆盖率阈值 (默认: 100)
OUTPUT_DIR 报告输出目录 (默认: ./reports)
SKIP_TESTS 跳过测试执行检查,使用现有报告 (默认: 0)
FORCE_ANALYSIS 强制分析 (默认: 0)
BASE_BRANCH 基准分支 (默认: 自动检测)
SKIP_BRANCH_CHECK 跳过远程分支比对 (默认: 0)
VENV_DIR 虚拟环境目录路径
CLEANUP_VENV 是否清理虚拟环境 (1=清理, 0=保留)
示例:
$0 # 使用默认设置
$0 --venv-dir ./my_venv # 指定虚拟环境目录
$0 --no-cleanup # 执行后保留虚拟环境
$0 MODULES=hinton-application,hinton-domain THRESHOLD=80 # 设置环境变量
$0 BASE_BRANCH=origin/main # 手动指定基准分支
$0 SKIP_BRANCH_CHECK=1 # 跳过远程分支比对
常见问题解决:
1. 找不到远程分支 origin/main: - 检查网络连接: git fetch origin
- 手动指定远程分支: BASE_BRANCH=origin/main ./diff_cover_venv.sh
- 检查远程仓库配置: git remote -v
2. 未配置远程仓库:
- 添加远程仓库: git remote add origin <仓库URL>
- 手动指定远程分支: BASE_BRANCH=origin/main ./diff_cover_venv.sh
3. 网络连接问题:
- 检查网络连接和代理设置
- 手动指定远程分支: BASE_BRANCH=origin/main ./diff_cover_venv.sh
- 跳过远程分支比对: SKIP_BRANCH_CHECK=1 ./diff_cover_venv.sh
4. 跳过远程分支比对:
- 使用环境变量: SKIP_BRANCH_CHECK=1 ./diff_cover_venv.sh
- 将进行整体覆盖率分析,不进行分支比对
注意:
- 脚本会自动创建临时虚拟环境
- 默认情况下执行完成后会自动清理虚拟环境
- 使用 --no-cleanup 可以保留虚拟环境,下次运行时复用(节省时间)
- 如果自动检测失败,可以手动指定 BASE_BRANCH 环境变量
- 使用 SKIP_BRANCH_CHECK=1 可以跳过远程分支比对,进行整体覆盖率分析
- 虚拟环境复用逻辑:保留时复用,清理时重建
EOF
}
# ===== 解析命令行参数 =====parse_args() {
while [$# -gt 0](); do
case $1 in
--venv-dir)
VENV_DIR="$2"
shift 2
;;
--no-cleanup)
CLEANUP_VENV=0
shift
;;
--help|-h)
show_help
exit 0
;;
*)
# 其他参数作为环境变量处理
break
;;
esac done}
# ===== 主函数 =====main() {
log_info "开始基于虚拟环境的增量覆盖率分析..."
# 解析参数
parse_args "$@"
# 设置退出时清理
trap cleanup_venv EXIT
# 检查环境依赖
check_environment
# 创建临时虚拟环境
create_temp_venv
# 设置虚拟环境
setup_venv
# 检测模块和分支
detect_modules
detect_base_branch
check_branch_integrity
local success_count=0
local total_count=0
local total_covered_lines=0
local total_missing_lines=0
local total_all_lines=0
IFS=',' read -ra MODULE_LIST <<< "$MODULES"
for module in "${MODULE_LIST[@]}"; do
module=$(echo "$module" | xargs)
total_count=$((total_count + 1))
log_info "处理模块: $module"
# 检查覆盖率报告
local coverage_xml
if ! coverage_xml=$(check_coverage_report "$module"); then
log_error "模块 $module 处理失败"
continue
fi
# 转换Jacoco XML为Cobertura XML
local cobertura_xml="$module/target/jacoco/cobertura.xml"
# 确保目标目录存在
[! -d "$(dirname "$cobertura_xml")"]() && mkdir -p "$(dirname "$cobertura_xml")"
if ! convert_jacoco_to_cobertura "$coverage_xml" "$cobertura_xml"; then
log_error "模块 $module XML转换失败"
continue
fi
# 使用diff-cover分析并获取覆盖率信息
if analyze_diff_coverage "$module" "$cobertura_xml"; then
success_count=$((success_count + 1))
# 解析覆盖率信息: coverage_percent,total_lines,missing_lines
IFS=',' read -r coverage_percent total_lines missing_lines <<< "$COVERAGE_INFO"
# 累计统计信息
total_all_lines=$((total_all_lines + total_lines))
total_missing_lines=$((total_missing_lines + missing_lines))
total_covered_lines=$((total_covered_lines + (total_lines - missing_lines)))
log_info "[$module] 覆盖率: ${coverage_percent}% (${total_lines}行总计, ${missing_lines}行缺失)"
else
log_error "模块 $module diff-cover分析失败"
fi
done # 总结
echo ""
echo "========================================"
echo "📊 分析完成"
echo "========================================"
echo "成功处理模块: $success_count/$total_count"
echo "报告目录: $OUTPUT_DIR"
# 计算合并的覆盖率
if [ $total_all_lines -gt 0 ]; then
local overall_coverage
overall_coverage=$(echo "scale=2; $total_covered_lines * 100 / $total_all_lines" | bc -l)
echo ""
echo "========================================"
echo "🎯 合并覆盖率统计"
echo "========================================"
echo "总代码行数: $total_all_lines"
echo "已覆盖行数: $total_covered_lines"
echo "缺失行数: $total_missing_lines"
echo "合并覆盖率: ${overall_coverage}%"
# 检查是否达到阈值
if (( $(echo "$overall_coverage < $THRESHOLD" | bc -l) )); then
log_warning "合并覆盖率 ${overall_coverage}% 低于阈值 ${THRESHOLD}%"
else
log_success "合并覆盖率 ${overall_coverage}% 达到阈值 ${THRESHOLD}%"
fi
else log_warning "没有检测到需要覆盖的代码行"
fi
if [ $success_count -eq $total_count ]; then
log_success "所有模块分析完成"
exit 0
else
log_error "部分模块分析失败"
exit 1
fi
}
# ===== 脚本入口 =====main "$@"