diff --git a/.github/workflows/build-core-ut.yaml b/.github/workflows/build-core-ut.yaml index 16b9ed8ee1..54dc3203d2 100644 --- a/.github/workflows/build-core-ut.yaml +++ b/.github/workflows/build-core-ut.yaml @@ -82,7 +82,7 @@ jobs: run: make unittest_core - name: Unit Test Coverage - run: docker build -t unittest_coverage -f ./docker/Dockerfile_coverage . && docker run -v $(pwd):$(pwd) unittest_coverage bash -c "cd $(pwd)/core && gcovr --root . --lcov coverage.lcov --txt coverage.txt -e \".*sdk.*\" -e \".*observer.*\" -e \".*protobuf.*\" -e \".*unittest.*\" -e \".*config_server.*\" -e \".*fuse.*\" -e \".*go_pipeline.*\"" + run: docker build -t unittest_coverage -f ./docker/Dockerfile_coverage . && docker run -v $(pwd):$(pwd) unittest_coverage bash -c "cd $(pwd)/core && gcovr --root . --json coverage.json --json-summary-pretty --json-summary summary.json -e \".*sdk.*\" -e \".*observer.*\" -e \".*logger.*\" -e \".*unittest.*\" -e \".*config_server.*\" -e \".*go_pipeline.*\" -e \".*application.*\" -e \".*protobuf.*\" -e \".*runner.*\"" - name: Setup Python3.10 uses: actions/setup-python@v5 @@ -90,7 +90,7 @@ jobs: python-version: "3.10" - name: Report code coverage - run: python3 tools/coverage-diff/main.py core/coverage.txt + run: python3 tools/coverage-diff/main.py --path core/coverage.json --summary core/summary.json result: runs-on: arc-runner-set-ilogtail diff --git a/docker/Dockerfile_coverage b/docker/Dockerfile_coverage index e0fedd193d..a445ba1b98 100644 --- a/docker/Dockerfile_coverage +++ b/docker/Dockerfile_coverage @@ -30,4 +30,4 @@ RUN python3 -m pip install --upgrade pip RUN cp /usr/local/python3/bin/pip3 /usr/bin/pip3 && pip3 install gcovr==7.0 RUN cp /usr/local/python3/bin/gcovr /usr/bin/gcovr -CMD ["bash", "-c", "gcovr --root . --lcov coverage.lcov --txt coverage.txt -e \".*sdk.*\" -e \".*observer.*\" -e \".*lo.*\" -e \".*unittest.*\" -e \".*config_server.*\" -e \".*fuse.*\" -e \".*go_pipeline.*\""] +CMD ["bash", "-c", "gcovr --root . --json coverage.json --json-summary-pretty --json-summary summary.json -e \".*sdk.*\" -e \".*observer.*\" -e \".*logger.*\" -e \".*unittest.*\" -e \".*config_server.*\" -e \".*go_pipeline.*\" -e \".*application.*\" -e \".*protobuf.*\" -e \".*runner.*\""] diff --git a/tools/coverage-diff/main.py b/tools/coverage-diff/main.py index 086265837e..51f837d289 100644 --- a/tools/coverage-diff/main.py +++ b/tools/coverage-diff/main.py @@ -1,53 +1,104 @@ import argparse import subprocess import sys -import time +import json +import re + +ERROR_COLOR = '\033[31m' +RESET_COLOR = '\033[0m' def get_changed_files(): try: - # Run the git command to get the list of changed files - result = subprocess.Popen('git diff --name-only -r HEAD^1 HEAD', shell=True, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - # Split the result by new line to get each file name - out, err = result.communicate() - changed_files = out.splitlines() - result_files = [] - for file in changed_files: - fileStr = file.decode('utf-8') - if fileStr.startswith('core'): - result_files.append(fileStr[5:]) - return result_files + result = subprocess.check_output(['git', 'diff', '--unified=0', 'HEAD^1' ,'HEAD'], universal_newlines=True) + return result except subprocess.CalledProcessError as e: - print(f"An error occurred while running git command: {e}") + print(f'An error occurred while running git command: {e}') return [] +def parse_diff(diff_output): + changes = {} + + current_file = None + for line in diff_output.split('\n'): + # 识别文件名 + file_match = re.match(r'^diff --git a/(.*) b/(.*)$', line) + if file_match: + current_file = file_match.group(2) + changes[current_file] = [] + continue + + # 识别文件中的行变化 + hunk_match = re.match(r'^@@ -\d+(,\d+)? \+(\d+)(,(\d+))? @@', line) + if hunk_match and current_file: + start_line = int(hunk_match.group(2)) + line_count = int(hunk_match.group(4) if hunk_match.group(4) else 1) + for i in range(start_line, start_line + line_count): + changes[current_file].append(i) + + return changes + if __name__ == '__main__': - parser = argparse.ArgumentParser(description="A simple argparse example") - parser.add_argument("path", type=str, help="The path of coverage file") + parser = argparse.ArgumentParser(description='A simple argparse example') + parser.add_argument('--path', type=str, help='The path of coverage file') + parser.add_argument('--summary_path', type=str, help='The path of coverage file') args = parser.parse_args() changed_files = get_changed_files() - line_cache = "" - not_satified = [] + changed_lines = parse_diff(changed_files) + + with open(args.summary_path, 'r') as file: + summary = json.load(file) + print('='*20) + print('Total coverage rate: ', summary['line_percent'], '%') + print('='*20) + with open(args.path, 'r') as file: - for line in file: - if len(line_cache) > 0: - line = line_cache + line - line_cache = "" - if '/' in line or ('%' in line and 'TOTAL' not in line): - for changed_file in changed_files: - if line.startswith(changed_file): - units = line.split() - if len(units) < 4: - # some files with long filename will be split into 2 lines - line_cache = line - continue - coverage_rate = int(units[3][:-1]) - if coverage_rate < 50: - not_satified.append(changed_file) - print(line, flush=True) - break - else: - print(line, flush=True) - if len(not_satified) > 0: - print(f"Coverage rate is less than 50% for the following files: {not_satified}", flush=True) + coverage = json.load(file) + not_satified = {} + not_satified_count = 0 + satified_count = 0 + + for file in coverage['files']: + if 'core/' + file['file'] in changed_lines: + file_name = 'core/' + file['file'] + cur_satified = [] + cur_not_satified = [] + i = 0 + j = 0 + while i < len(file['lines']) and j < len(changed_lines[file_name]): + if file['lines'][i]['line_number'] == changed_lines[file_name][j]: + if file['lines'][i]['count'] == 0: + cur_not_satified.append(file['lines'][i]['line_number']) + else: + cur_satified.append(file['lines'][i]['line_number']) + i += 1 + j += 1 + elif file['lines'][i]['line_number'] < changed_lines[file_name][j]: + i += 1 + else: + j += 1 + if len(cur_satified) > 0 or len(cur_not_satified) > 0: + print('file: ', file_name) + if len(cur_satified) > 0: + print('covered lines: ', cur_satified) + satified_count += len(cur_satified) + if len(cur_not_satified) > 0: + print(f'{ERROR_COLOR}not covered lines:{RESET_COLOR} ', cur_not_satified) + not_satified_count += len(cur_not_satified) + print('') + if len(cur_not_satified) > 0: + not_satified[file_name] = cur_not_satified + + if not_satified_count + satified_count == 0: + print('No line to cover', flush=True) + sys.exit(0) + + coverage_rate = ((satified_count) / (not_satified_count + satified_count) ) * 100 + print('='*20) + if coverage_rate < 50: + print(f'{ERROR_COLOR}Diff coverage rate is less than 50%: {coverage_rate:.1f}%{RESET_COLOR}', flush=True) + print('='*20) sys.exit(1) + else: + print(f'Diff coverage rate is {coverage_rate:.1f}%', flush=True) + print('='*20) + sys.exit(0)