Skip to content

单元测试代码覆盖率-脚本文件

查看单个类的覆盖情况

执行 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 "$@"