├── .gitignore ├── Pipfile ├── Pipfile.lock ├── README.md ├── diff.gif ├── diff_processor.py ├── main.py ├── pics ├── help.png └── report.png └── report.css /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | gitpython = "*" 10 | 11 | [requires] 12 | python_version = "3.7" 13 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "a63e82561d652c1a5e85796eb26eccdb755fdac690462a6f5206f1818f28d3f7" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "gitdb": { 20 | "hashes": [ 21 | "sha256:6f0ecd46f99bb4874e5678d628c3a198e2b4ef38daea2756a2bfd8df7dd5c1a5", 22 | "sha256:ba1132c0912e8c917aa8aa990bee26315064c7b7f171ceaaac0afeb1dc656c6a" 23 | ], 24 | "version": "==4.0.4" 25 | }, 26 | "gitpython": { 27 | "hashes": [ 28 | "sha256:6d4f10e2aaad1864bb0f17ec06a2c2831534140e5883c350d58b4e85189dab74", 29 | "sha256:71b8dad7409efbdae4930f2b0b646aaeccce292484ffa0bc74f1195582578b3d" 30 | ], 31 | "index": "pypi", 32 | "version": "==3.1.1" 33 | }, 34 | "smmap": { 35 | "hashes": [ 36 | "sha256:52ea78b3e708d2c2b0cfe93b6fc3fbeec53db913345c26be6ed84c11ed8bebc1", 37 | "sha256:b46d3fc69ba5f367df96d91f8271e8ad667a198d5a28e215a6c3d9acd133a911" 38 | ], 39 | "version": "==3.0.2" 40 | } 41 | }, 42 | "develop": {} 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jacoco-diff 2 | 在 jacoco 覆盖率报告的基础上,计算出增量覆盖率 3 | 4 | 5 | # 结果展示 6 | ### 命令行提示 7 | ![pic](https://github.com/raoweijian/jacoco-diff/blob/master/pics/help.png) 8 | 9 | ### 覆盖率报告 10 | 11 | 新增的行首增加蓝色钻石标志,与其它钻石不冲突 12 | 13 | ![pic](https://github.com/raoweijian/jacoco-diff/blob/master/pics/report.png) 14 | 15 | # 用法 16 | ```shell 17 | # 假设工程路径为 ~/project/test_project 18 | cd ~/project/test_project 19 | 20 | # 执行单测,生成 jacoco 覆盖率报告 21 | mvn clean test 22 | 23 | # 使用本工具,计算增量覆盖率,并修改覆盖率报告 24 | python main.py -d ~/project/test_project -o HEAD~1 25 | ``` 26 | 27 | ## 参数说明 28 | \-h, \-\-help 打印帮助信息 29 | 30 | \-d, \-dir 工程根目录 31 | 32 | \-o, \-old_version 指定对比的版本号, 如果该参数没有给出,默认与前一个版本进行对比(HEAD\~1)。该参数支持 git commit hash 或者 HEAD~n 的格式。 33 | -------------------------------------------------------------------------------- /diff.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoweijian/jacoco-diff/984df382c91f9b48c6a94719f4ac612777d34f58/diff.gif -------------------------------------------------------------------------------- /diff_processor.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | 4 | from git import Repo 5 | 6 | 7 | class DiffProcessor(): 8 | def __init__(self, project_dir, old_version): 9 | self.project_dir = project_dir 10 | self.old_version = old_version 11 | self.repo = Repo(self.project_dir) 12 | 13 | def resolve_file_info(self, file_name): 14 | full_path = os.path.join(self.project_dir, file_name) 15 | package = self.get_package(full_path) 16 | 17 | class_ = re.search('(\w+)\.java$', file_name).group(1) 18 | 19 | is_interface = self.is_interface(full_path) 20 | 21 | return (package, class_, is_interface) 22 | 23 | def get_package(self, file_name): 24 | """获取package名""" 25 | ret = '' 26 | with open(file_name) as fp: 27 | for line in fp: 28 | line = line.strip() 29 | match = re.match('package\s+(\S+);', line) 30 | if match: 31 | ret = match.group(1) 32 | break 33 | return ret 34 | 35 | def is_interface(self, file_name): 36 | """判断某个文件是否是接口""" 37 | ret = False 38 | name = re.search('(\w+)\.java$', file_name).group(1) 39 | reg_interface = re.compile('public\s+interface\s+{}'.format(name)) 40 | with open(file_name) as fp: 41 | for line in fp: 42 | line = line.strip() 43 | match = re.match(reg_interface, line) 44 | if match: 45 | ret = True 46 | break 47 | return ret 48 | 49 | def get_diff(self): 50 | """获取diff详情""" 51 | diff = self.repo.git.diff(self.old_version, self.repo.head).split("\n") 52 | ret = {} 53 | 54 | file_name = "" 55 | diff_lines = [] 56 | current_line = 0 57 | for line in diff: 58 | if line.startswith('diff --git'): 59 | # 进入新的block 60 | if file_name != "": 61 | ret[file_name] = diff_lines 62 | file_name = re.findall('b/(\S+)$', line)[0] 63 | diff_lines = [] 64 | current_line = 0 65 | 66 | elif re.match('@@ -\d+,\d+ \+(\d+),\d+ @@', line): 67 | match = re.match('@@ -\d+,\d+ \+(\d+),\d+ @@', line) 68 | current_line = int(match.group(1)) - 1 69 | 70 | elif line.startswith("-"): 71 | continue 72 | elif line.startswith("+") and not line.startswith('+++'): 73 | current_line += 1 74 | diff_lines.append(current_line) 75 | else: 76 | current_line += 1 77 | ret[file_name] = diff_lines 78 | 79 | return ret 80 | 81 | def modify_html(self, html_file_name, diff_lines): 82 | new_line_count = 0 83 | cover_line_count = 0 84 | 85 | content = [] 86 | with open(html_file_name, 'r') as fp: 87 | content = fp.readlines() 88 | 89 | for i in range(1, len(content)): 90 | if i + 1 in diff_lines: 91 | match = re.search('class="([^"]+)"', content[i]) 92 | if match: 93 | content[i] = re.sub('class="([^"]+)"', lambda m: 'class="{}-diff"'.format(m.group(1)), content[i]) 94 | css_class = match.group(1) 95 | new_line_count += 1 96 | if css_class.startswith("fc") or css_class.startswith("pc"): 97 | cover_line_count += 1 98 | 99 | with open(html_file_name, 'w') as fp: 100 | fp.write("".join(content)) 101 | 102 | return new_line_count, cover_line_count 103 | 104 | def process_diff(self): 105 | ret = {} 106 | diff_result = self.get_diff() 107 | 108 | for file_name in diff_result: 109 | # 过滤掉只有删除,没有新增的代码 110 | if diff_result[file_name] == []: 111 | continue 112 | 113 | # 过滤掉非 java 文件和测试代码 114 | if not file_name.endswith(".java") or 'src/test/java/' in file_name: 115 | continue 116 | 117 | package, class_, is_interface = self.resolve_file_info(file_name) 118 | # 过滤掉接口和非指定的module 119 | if is_interface: 120 | continue 121 | 122 | html_file_name = os.path.join(self.project_dir, 'target/site/jacoco/', package, "{}.java.html".format(class_)) 123 | new_line_count, cover_line_count = self.modify_html(html_file_name, diff_result[file_name]) 124 | print("package {}, class {}, 新增 {} 行, 覆盖 {} 行".format(package, class_, new_line_count, cover_line_count)) 125 | 126 | # 信息存进返回值 127 | if package not in ret: 128 | ret[package] = {} 129 | ret[package][class_] = {"new": new_line_count, "cover": cover_line_count} 130 | 131 | return ret 132 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf8 3 | 4 | import os 5 | import sys 6 | import shutil 7 | import argparse 8 | 9 | from diff_processor import DiffProcessor 10 | 11 | 12 | def main(argv): 13 | # 解析命令行参数 14 | parser = argparse.ArgumentParser(description="计算增量覆盖率的工具") 15 | parser.add_argument('-dir', type=str, help="工程根目录") 16 | parser.add_argument('-old_version', type=str, default="HEAD~1", help='指定对比的版本号') 17 | opts = parser.parse_args(argv[1:]) 18 | if opts.dir is None: 19 | parser.print_help() 20 | sys.exit() 21 | 22 | # 获取增量覆盖率信息 23 | processor = DiffProcessor(opts.dir, opts.old_version) 24 | diff_cov_info = processor.process_diff() 25 | 26 | # 拷贝 css 和图片资源 27 | shutil.copy('diff.gif', os.path.join(opts.dir, "target/site/jacoco/jacoco-resources")) 28 | shutil.copy('report.css', os.path.join(opts.dir, "target/site/jacoco/jacoco-resources")) 29 | 30 | return 0 31 | 32 | 33 | if __name__ == "__main__": 34 | sys.exit(main(sys.argv)) 35 | -------------------------------------------------------------------------------- /pics/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoweijian/jacoco-diff/984df382c91f9b48c6a94719f4ac612777d34f58/pics/help.png -------------------------------------------------------------------------------- /pics/report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raoweijian/jacoco-diff/984df382c91f9b48c6a94719f4ac612777d34f58/pics/report.png -------------------------------------------------------------------------------- /report.css: -------------------------------------------------------------------------------- 1 | body, td { 2 | font-family:sans-serif; 3 | font-size:10pt; 4 | } 5 | 6 | h1 { 7 | font-weight:bold; 8 | font-size:18pt; 9 | } 10 | 11 | .breadcrumb { 12 | border:#d6d3ce 1px solid; 13 | padding:2px 4px 2px 4px; 14 | } 15 | 16 | .breadcrumb .info { 17 | float:right; 18 | } 19 | 20 | .breadcrumb .info a { 21 | margin-left:8px; 22 | } 23 | 24 | .el_report { 25 | padding-left:18px; 26 | background-image:url(report.gif); 27 | background-position:left center; 28 | background-repeat:no-repeat; 29 | } 30 | 31 | .el_group { 32 | padding-left:18px; 33 | background-image:url(group.gif); 34 | background-position:left center; 35 | background-repeat:no-repeat; 36 | } 37 | 38 | .el_bundle { 39 | padding-left:18px; 40 | background-image:url(bundle.gif); 41 | background-position:left center; 42 | background-repeat:no-repeat; 43 | } 44 | 45 | .el_package { 46 | padding-left:18px; 47 | background-image:url(package.gif); 48 | background-position:left center; 49 | background-repeat:no-repeat; 50 | } 51 | 52 | .el_class { 53 | padding-left:18px; 54 | background-image:url(class.gif); 55 | background-position:left center; 56 | background-repeat:no-repeat; 57 | } 58 | 59 | .el_source { 60 | padding-left:18px; 61 | background-image:url(source.gif); 62 | background-position:left center; 63 | background-repeat:no-repeat; 64 | } 65 | 66 | .el_method { 67 | padding-left:18px; 68 | background-image:url(method.gif); 69 | background-position:left center; 70 | background-repeat:no-repeat; 71 | } 72 | 73 | .el_session { 74 | padding-left:18px; 75 | background-image:url(session.gif); 76 | background-position:left center; 77 | background-repeat:no-repeat; 78 | } 79 | 80 | pre.source { 81 | border:#d6d3ce 1px solid; 82 | font-family:monospace; 83 | } 84 | 85 | pre.source ol { 86 | margin-bottom: 0px; 87 | margin-top: 0px; 88 | } 89 | 90 | pre.source li { 91 | border-left: 1px solid #D6D3CE; 92 | color: #A0A0A0; 93 | padding-left: 0px; 94 | } 95 | 96 | pre.source span.fc { 97 | background-color:#ccffcc; 98 | } 99 | 100 | pre.source span.nc { 101 | background-color:#ffaaaa; 102 | } 103 | 104 | pre.source span.pc { 105 | background-color:#ffffcc; 106 | } 107 | 108 | pre.source span.fc-diff { 109 | background-color:#ccffcc; 110 | background-image: url(diff.gif); 111 | background-repeat: no-repeat; 112 | background-position: 2px center; 113 | } 114 | 115 | pre.source span.nc-diff { 116 | background-color:#ffaaaa; 117 | background-image: url(diff.gif); 118 | background-repeat: no-repeat; 119 | background-position: 2px center; 120 | } 121 | 122 | pre.source span.pc-diff { 123 | background-color:#ffffcc; 124 | background-image: url(diff.gif); 125 | background-repeat: no-repeat; 126 | background-position: 2px center; 127 | } 128 | 129 | pre.source span.bfc { 130 | background-image: url(branchfc.gif); 131 | background-repeat: no-repeat; 132 | background-position: 2px center; 133 | } 134 | 135 | pre.source span.bfc:hover { 136 | background-color:#80ff80; 137 | } 138 | 139 | pre.source span.bnc { 140 | background-image: url(branchnc.gif); 141 | background-repeat: no-repeat; 142 | background-position: 2px center; 143 | } 144 | 145 | pre.source span.bnc:hover { 146 | background-color:#ff8080; 147 | } 148 | 149 | pre.source span.bpc { 150 | background-image: url(branchpc.gif); 151 | background-repeat: no-repeat; 152 | background-position: 2px center; 153 | } 154 | 155 | pre.source span.bpc:hover { 156 | background-color:#ffff80; 157 | } 158 | 159 | /*add by weijianrao*/ 160 | pre.source span.bnc-diff { 161 | background-image: url(branchnc.gif), url(diff.gif); 162 | background-repeat: no-repeat, no-repeat; 163 | background-position: 2px center, 15px center; 164 | } 165 | 166 | pre.source span.bfc-diff { 167 | background-image: url(branchfc.gif), url(diff.gif); 168 | background-repeat: no-repeat, no-repeat; 169 | background-position: 2px center, 15px center; 170 | } 171 | 172 | pre.source span.bpc-diff { 173 | background-image: url(branchpc.gif), url(diff.gif); 174 | background-repeat: no-repeat, no-repeat; 175 | background-position: 2px center, 15px center; 176 | } 177 | 178 | /* end of weijianrao*/ 179 | 180 | table.coverage { 181 | empty-cells:show; 182 | border-collapse:collapse; 183 | } 184 | 185 | table.coverage thead { 186 | background-color:#e0e0e0; 187 | } 188 | 189 | table.coverage thead td { 190 | white-space:nowrap; 191 | padding:2px 14px 0px 6px; 192 | border-bottom:#b0b0b0 1px solid; 193 | } 194 | 195 | table.coverage thead td.bar { 196 | border-left:#cccccc 1px solid; 197 | } 198 | 199 | table.coverage thead td.ctr1 { 200 | text-align:right; 201 | border-left:#cccccc 1px solid; 202 | } 203 | 204 | table.coverage thead td.ctr2 { 205 | text-align:right; 206 | padding-left:2px; 207 | } 208 | 209 | table.coverage thead td.sortable { 210 | cursor:pointer; 211 | background-image:url(sort.gif); 212 | background-position:right center; 213 | background-repeat:no-repeat; 214 | } 215 | 216 | table.coverage thead td.up { 217 | background-image:url(up.gif); 218 | } 219 | 220 | table.coverage thead td.down { 221 | background-image:url(down.gif); 222 | } 223 | 224 | table.coverage tbody td { 225 | white-space:nowrap; 226 | padding:2px 6px 2px 6px; 227 | border-bottom:#d6d3ce 1px solid; 228 | } 229 | 230 | table.coverage tbody tr:hover { 231 | background: #f0f0d0 !important; 232 | } 233 | 234 | table.coverage tbody td.bar { 235 | border-left:#e8e8e8 1px solid; 236 | } 237 | 238 | table.coverage tbody td.ctr1 { 239 | text-align:right; 240 | padding-right:14px; 241 | border-left:#e8e8e8 1px solid; 242 | } 243 | 244 | table.coverage tbody td.ctr2 { 245 | text-align:right; 246 | padding-right:14px; 247 | padding-left:2px; 248 | } 249 | 250 | table.coverage tfoot td { 251 | white-space:nowrap; 252 | padding:2px 6px 2px 6px; 253 | } 254 | 255 | table.coverage tfoot td.bar { 256 | border-left:#e8e8e8 1px solid; 257 | } 258 | 259 | table.coverage tfoot td.ctr1 { 260 | text-align:right; 261 | padding-right:14px; 262 | border-left:#e8e8e8 1px solid; 263 | } 264 | 265 | table.coverage tfoot td.ctr2 { 266 | text-align:right; 267 | padding-right:14px; 268 | padding-left:2px; 269 | } 270 | 271 | .footer { 272 | margin-top:20px; 273 | border-top:#d6d3ce 1px solid; 274 | padding-top:2px; 275 | font-size:8pt; 276 | color:#a0a0a0; 277 | } 278 | 279 | .footer a { 280 | color:#a0a0a0; 281 | } 282 | 283 | .right { 284 | float:right; 285 | } 286 | --------------------------------------------------------------------------------