• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

future-architect / uroborosql / #839

05 May 2025 05:28AM UTC coverage: 90.172% (-0.02%) from 90.196%
#839

push

web-flow
backport #350 add updateChained method. (#351)

* backport #350 add updateChained method.

* update github-action settings

* fix review

17 of 19 new or added lines in 3 files covered. (89.47%)

2 existing lines in 1 file now uncovered.

7936 of 8801 relevant lines covered (90.17%)

0.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.53
/src/main/java/jp/co/future/uroborosql/coverage/CoberturaCoverageHandler.java
1
/**
2
 * Copyright (c) 2017-present, Future Corporation
3
 *
4
 * This source code is licensed under the MIT license found in the
5
 * LICENSE file in the root directory of this source tree.
6
 */
7
package jp.co.future.uroborosql.coverage;
8

9
import java.io.BufferedWriter;
10
import java.io.File;
11
import java.io.IOException;
12
import java.nio.charset.StandardCharsets;
13
import java.nio.file.Files;
14
import java.nio.file.Path;
15
import java.nio.file.Paths;
16
import java.util.ArrayList;
17
import java.util.Comparator;
18
import java.util.EnumSet;
19
import java.util.HashMap;
20
import java.util.List;
21
import java.util.Map;
22
import java.util.Set;
23
import java.util.concurrent.ConcurrentHashMap;
24
import java.util.stream.Collectors;
25

26
import javax.xml.parsers.DocumentBuilder;
27
import javax.xml.parsers.DocumentBuilderFactory;
28
import javax.xml.parsers.ParserConfigurationException;
29
import javax.xml.transform.OutputKeys;
30
import javax.xml.transform.Transformer;
31
import javax.xml.transform.TransformerException;
32
import javax.xml.transform.TransformerFactory;
33
import javax.xml.transform.dom.DOMSource;
34
import javax.xml.transform.stream.StreamResult;
35

36
import org.slf4j.Logger;
37
import org.slf4j.LoggerFactory;
38
import org.w3c.dom.Document;
39
import org.w3c.dom.Element;
40

41
import jp.co.future.uroborosql.SqlAgent;
42
import jp.co.future.uroborosql.utils.StringUtils;
43

44
/**
45
 * Coberturaカバレッジレポート出力ハンドラ<br>
46
 * Jenkins Cobertura プラグイン で集計することができるレポートファイルを出力します。
47
 *
48
 * <pre>
49
 * デフォルトコンストラクタで生成される場合、レポートファイルの出力先は以下のように決定されます。
50
 *
51
 * system property "uroborosql.sql.coverage.file" が指定された場合、指定されたPATHに xmlレポートを出力します。
52
 * 指定の無い場合、デフォルトで "./target/coverage/sql-cover.xml" に xmlレポートを出力します。
53
 * </pre>
54
 *
55
 * @author ota
56
 */
57
public class CoberturaCoverageHandler implements CoverageHandler {
58
        protected static final Logger LOG = LoggerFactory.getLogger(CoberturaCoverageHandler.class);
1✔
59

60
        /**
61
         * カバレッジ数値 line branch セット
62
         */
63
        private static class CoverageSummaryTotal {
1✔
64
                private final CoverageSummary line = new CoverageSummary();
1✔
65
                private final CoverageSummary branch = new CoverageSummary();
1✔
66

67
                private void add(final CoverageSummaryTotal total) {
68
                        this.line.add(total.line);
1✔
69
                        this.branch.add(total.branch);
1✔
70
                }
1✔
71
        }
72

73
        /**
74
         * カバレッジ数値
75
         */
76
        private static class CoverageSummary {
77
                private int valid;
78
                private int covered;
79

80
                private double getRate() {
81
                        if (valid == 0) {
1✔
82
                                return 0;
1✔
83
                        }
84
                        return (double) covered / (double) valid;
1✔
85
                }
86

87
                private void add(final CoverageSummary o) {
88
                        this.valid += o.valid;
1✔
89
                        this.covered += o.covered;
1✔
90
                }
1✔
91
        }
92

93
        /**
94
         * ポイントブランチ情報
95
         */
96
        private static class PointBranch {
97
                @SuppressWarnings("unused")
98
                private final Range range;
99
                private final Set<BranchCoverageState> status = EnumSet.noneOf(BranchCoverageState.class);
1✔
100

101
                private PointBranch(final Range range) {
1✔
102
                        this.range = range;
1✔
103
                }
1✔
104

105
                private void add(final BranchCoverageState state) {
106
                        status.add(state);
1✔
107
                }
1✔
108

109
                private int coveredSize() {
110
                        return BranchCoverageState.getCoveredSize(status);
1✔
111
                }
112
        }
113

114
        /**
115
         * 行ブランチ情報
116
         */
117
        private static class LineBranch {
118
                @SuppressWarnings("unused")
119
                private final int rowIndex;
120
                private final Map<Range, PointBranch> branches = new HashMap<>();
1✔
121

122
                private LineBranch(final int rowIndex) {
1✔
123
                        this.rowIndex = rowIndex;
1✔
124
                }
1✔
125

126
                private void add(final Range idx, final BranchCoverageState state) {
127
                        PointBranch branch = branches.computeIfAbsent(idx, k -> new PointBranch(idx));
1✔
128

129
                        branch.add(state);
1✔
130
                }
1✔
131

132
                private int branchSize() {
133
                        return branches.size() * 2;
1✔
134
                }
135

136
                private int coveredSize() {
137
                        return branches.values().stream().mapToInt(p -> p.coveredSize()).sum();
1✔
138
                }
139
        }
140

141
        /**
142
         * SQL別カバレッジ元情報
143
         */
144
        private static class SqlCoverage {
145
                private final String name;
146
                @SuppressWarnings("unused")
147
                private final String md5;
148
                /** 各行範囲 */
149
                private final List<LineRange> lineRanges;
150

151
                /** 各行ブランチカバレッジ情報 */
152
                private final Map<Integer, LineBranch> lineBranches = new HashMap<>();
1✔
153
                /** 各行通過回数 */
154
                private final int[] hitLines;
155

156
                private SqlCoverage(final String name, final String sql, final String md5, final Path sourcesDirPath,
157
                                final int hashIndex)
158
                                throws IOException {
1✔
159
                        this.name = hashIndex <= 0 ? name : name + "_hash_" + hashIndex;
1✔
160
                        this.md5 = md5;
1✔
161
                        this.lineRanges = CoverageHandler.parseLineRanges(sql);
1✔
162
                        this.hitLines = new int[lineRanges.stream()
1✔
163
                                        .mapToInt(LineRange::getLineIndex)
1✔
164
                                        .max().orElse(0) + 1];
1✔
165
                        writeSqlSource(sourcesDirPath, sql);
1✔
166
                }
1✔
167

168
                /**
169
                 * カバレッジ情報追加
170
                 *
171
                 * @param passRoute カバレッジ情報
172
                 */
173
                private void accept(final PassedRoute passRoute) {
174
                        //各行の通過情報を集計
175
                        for (LineRange range : lineRanges) {
1✔
176
                                if (passRoute.isHit(range)) {
1✔
177
                                        hitLines[range.getLineIndex()]++;
1✔
178
                                }
179
                        }
1✔
180
                        //各行のブランチ情報を集計
181
                        passRoute.getRangeBranchStatus().forEach((range, state) -> {
1✔
182
                                LineBranch lineBranch = lineBranches.computeIfAbsent(toRow(range), k -> new LineBranch(k));
1✔
183
                                lineBranch.add(range, state);
1✔
184
                        });
1✔
185
                }
1✔
186

187
                private int toRow(final Range target) {
188
                        for (LineRange range : lineRanges) {
1✔
189
                                if (range.hasIntersection(target)) {
1✔
190
                                        return range.getLineIndex();
1✔
191
                                }
192
                        }
1✔
193
                        return -1;
×
194
                }
195

196
                /**
197
                 * SQL ファイル書き込み
198
                 *
199
                 * @param sourcesDirPath ディレクトリ
200
                 * @param sql SQL
201
                 * @throws IOException IOエラー
202
                 */
203
                private void writeSqlSource(final Path sourcesDirPath, final String sql) throws IOException {
204
                        Path path = sourcesDirPath.resolve(name);
1✔
205
                        Files.createDirectories(path.getParent());
1✔
206
                        Files.write(path, sql.getBytes(StandardCharsets.UTF_8));
1✔
207
                }
1✔
208
        }
209

210
        /**
211
         * パッケージで集約した情報
212
         */
213
        private static class PackageSummary {
214
                private final String packagePath;
215
                private final List<SqlCoverage> coverageInfos = new ArrayList<>();
1✔
216

217
                private PackageSummary(final String packagePath) {
1✔
218
                        this.packagePath = packagePath;
1✔
219
                }
1✔
220
        }
221

222
        private final Map<String, Map<String, SqlCoverage>> coverages = new ConcurrentHashMap<>();
1✔
223
        private final Path reportPath;
224
        private final Path sourcesDirPath;
225
        private boolean updated = true;
1✔
226

227
        /**
228
         * コンストラクタ<br>
229
         *
230
         * <pre>
231
         * system property "uroborosql.sql.coverage.file" が指定された場合、指定されたPATHに xmlレポートを出力します。
232
         * 指定の無い場合、デフォルトで "./target/coverage/sql-cover.xml" に xmlレポートを出力します。
233
         * </pre>
234
         */
235
        public CoberturaCoverageHandler() {
1✔
236
                String s = System.getProperty(SqlAgent.KEY_SQL_COVERAGE + ".file");
1✔
237
                if (StringUtils.isNotEmpty(s)) {
1✔
238
                        this.reportPath = Paths.get(s);
1✔
239
                } else {
240
                        this.reportPath = Paths.get("target", "coverage", "sql-cover.xml");
1✔
241
                }
242
                this.sourcesDirPath = this.reportPath.toAbsolutePath().getParent().resolve("sqls");
1✔
243
                init();
1✔
244
        }
1✔
245

246
        /**
247
         * コンストラクタ
248
         *
249
         * @param reportPath レポートファイルPATH
250
         */
251
        public CoberturaCoverageHandler(final Path reportPath) {
×
252
                this.reportPath = reportPath;
×
253
                this.sourcesDirPath = this.reportPath.toAbsolutePath().getParent().resolve("sqls");
×
254
                init();
×
255
        }
×
256

257
        @Override
258
        public synchronized void accept(final CoverageData coverageData) {
259
                if (StringUtils.isEmpty(coverageData.getSqlName())) {
1✔
260
                        //SQL名の設定されていないSQLは集約しない
261
                        return;
1✔
262
                }
263

264
                Map<String, SqlCoverage> map = coverages.computeIfAbsent(coverageData.getSqlName(),
1✔
265
                                k -> new ConcurrentHashMap<>());
1✔
266
                SqlCoverage sqlCoverage = map.get(coverageData.getMd5());
1✔
267
                if (sqlCoverage == null) {
1✔
268
                        try {
269
                                sqlCoverage = new SqlCoverage(coverageData.getSqlName(), coverageData.getSql(), coverageData.getMd5(),
1✔
270
                                                sourcesDirPath, map.size());
1✔
271
                        } catch (IOException e) {
×
272
                                LOG.error(e.getMessage(), e);
×
273
                                return;
×
274
                        }
1✔
275
                        map.put(coverageData.getMd5(), sqlCoverage);
1✔
276
                }
277

278
                sqlCoverage.accept(coverageData.getPassRoute());
1✔
279
                updated = true;
1✔
280
        }
1✔
281

282
        @Override
283
        public synchronized void onSqlAgentClose() {
284
                try {
285
                        write();
1✔
UNCOV
286
                } catch (Exception e) {
×
UNCOV
287
                        LOG.error(e.getMessage(), e);
×
288
                }
1✔
289
        }
1✔
290

291
        private void init() {
292
                //JVM終了時に書き込み
293
                Runtime.getRuntime().addShutdownHook(new Thread(() -> {
1✔
294
                        try {
295
                                write();
1✔
296
                        } catch (Exception e) {
×
297
                                LOG.error(e.getMessage(), e);
×
298
                        }
1✔
299
                }));
1✔
300
        }
1✔
301

302
        synchronized void write() throws IOException, ParserConfigurationException, TransformerException {
303
                if (!updated) {//更新が無い場合は書き込みしない
1✔
304
                        return;
1✔
305
                }
306

307
                List<PackageSummary> packageNodes = summaryPackages();
1✔
308

309
                DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
1✔
310
                Document document = documentBuilder.newDocument();
1✔
311

312
                Element coverage = document.createElement("coverage");
1✔
313

314
                coverage.setAttribute("timestamp", String.valueOf(System.currentTimeMillis()));
1✔
315
                coverage.setAttribute("complexity", "0");
1✔
316
                coverage.setAttribute("version", "0.1");
1✔
317

318
                document.appendChild(coverage);
1✔
319
                Element sources = document.createElement("sources");
1✔
320
                coverage.appendChild(sources);
1✔
321
                Element source = document.createElement("source");
1✔
322
                source.setTextContent(sourcesDirPath.toString());
1✔
323
                sources.appendChild(source);
1✔
324

325
                Element packages = document.createElement("packages");
1✔
326
                coverage.appendChild(packages);
1✔
327

328
                //packages内のrenderとカバレッジ集計
329
                CoverageSummaryTotal total = renderPackages(document, packages, packageNodes);
1✔
330

331
                CoverageSummary lines = total.line;
1✔
332
                coverage.setAttribute("lines-valid", String.valueOf(lines.valid));
1✔
333
                coverage.setAttribute("lines-covered", String.valueOf(lines.covered));
1✔
334
                coverage.setAttribute("lines-rate", String.valueOf(lines.getRate()));
1✔
335

336
                CoverageSummary branches = total.branch;
1✔
337
                coverage.setAttribute("branches-valid", String.valueOf(branches.valid));
1✔
338
                coverage.setAttribute("branches-covered", String.valueOf(branches.covered));
1✔
339
                coverage.setAttribute("branches-rate", String.valueOf(branches.getRate()));
1✔
340

341
                write(document);
1✔
342

343
                //書込が終了したので「更新なし」にする
344
                updated = false;
1✔
345
        }
1✔
346

347
        /**
348
         * パッケージ単位にまとめる
349
         *
350
         * @return パッケージ単位情報
351
         */
352
        private List<PackageSummary> summaryPackages() {
353
                Map<String, PackageSummary> summaries = new HashMap<>();
1✔
354

355
                coverages.forEach((name, c) -> {
1✔
356
                        Path p = Paths.get(name).getParent();
1✔
357
                        String pkg = p != null ? p.toString().replace(File.separatorChar, '.') : "_root_";
1✔
358
                        PackageSummary summary = summaries.computeIfAbsent(pkg, k -> new PackageSummary(pkg));
1✔
359
                        summary.coverageInfos.addAll(c.values());
1✔
360
                });
1✔
361
                return summaries.values().stream().sorted(Comparator.comparing(p -> p.packagePath))
1✔
362
                                .collect(Collectors.toList());
1✔
363

364
        }
365

366
        private CoverageSummaryTotal renderPackages(final Document document, final Element packages,
367
                        final List<PackageSummary> packageNodes) {
368
                CoverageSummaryTotal allTotal = new CoverageSummaryTotal();
1✔
369
                for (PackageSummary packageNode : packageNodes) {
1✔
370

371
                        CoverageSummaryTotal total = new CoverageSummaryTotal();
1✔
372
                        Element packageElm = document.createElement("package");
1✔
373
                        packageElm.setAttribute("name", packageNode.packagePath);
1✔
374
                        packages.appendChild(packageElm);
1✔
375

376
                        Element classes = document.createElement("classes");
1✔
377
                        packageElm.appendChild(classes);
1✔
378

379
                        for (SqlCoverage coverageInfo : packageNode.coverageInfos) {
1✔
380
                                //class内のrenderとカバレッジ集計
381
                                total.add(renderClass(document, classes, coverageInfo));
1✔
382
                        }
1✔
383
                        packageElm.setAttribute("line-rate", String.valueOf(total.line.getRate()));
1✔
384
                        packageElm.setAttribute("branch-rate", String.valueOf(total.branch.getRate()));
1✔
385

386
                        allTotal.add(total);
1✔
387
                }
1✔
388

389
                return allTotal;
1✔
390
        }
391

392
        private CoverageSummaryTotal renderClass(final Document document, final Element classes,
393
                        final SqlCoverage coverageInfo) {
394

395
                CoverageSummaryTotal total = new CoverageSummaryTotal();
1✔
396

397
                Element classElm = document.createElement("class");
1✔
398
                classElm.setAttribute("name", coverageInfo.name);
1✔
399
                classElm.setAttribute("filename", coverageInfo.name);
1✔
400
                classes.appendChild(classElm);
1✔
401

402
                Element methods = document.createElement("methods");
1✔
403
                classElm.appendChild(methods);
1✔
404

405
                Element lines = document.createElement("lines");
1✔
406
                classElm.appendChild(lines);
1✔
407

408
                total.line.valid = coverageInfo.lineRanges.size();
1✔
409
                for (LineRange range : coverageInfo.lineRanges) {
1✔
410
                        int no = range.getLineIndex() + 1;
1✔
411
                        int hit = coverageInfo.hitLines[range.getLineIndex()];
1✔
412
                        if (hit > 0) {
1✔
413
                                total.line.covered++;
1✔
414
                        }
415

416
                        Element line = document.createElement("line");
1✔
417
                        lines.appendChild(line);
1✔
418
                        line.setAttribute("number", String.valueOf(no));
1✔
419
                        line.setAttribute("hits", String.valueOf(hit));
1✔
420
                        LineBranch lineBranch = coverageInfo.lineBranches.get(range.getLineIndex());
1✔
421
                        if (lineBranch != null) {
1✔
422
                                int size = lineBranch.branchSize();
1✔
423
                                int covered = lineBranch.coveredSize();
1✔
424
                                line.setAttribute("branch", "true");
1✔
425
                                line.setAttribute("condition-coverage",
1✔
426
                                                CoverageHandler.percentStr(covered, size) + "% (" + covered + "/" + size + ")");
1✔
427
                                total.branch.valid += size;
1✔
428
                                total.branch.covered += covered;
1✔
429

430
                        } else {
1✔
431
                                line.setAttribute("branch", "false");
1✔
432
                        }
433
                }
1✔
434

435
                classElm.setAttribute("line-rate", String.valueOf(total.line.getRate()));
1✔
436
                classElm.setAttribute("branch-rate", String.valueOf(total.branch.getRate()));
1✔
437

438
                return total;
1✔
439
        }
440

441
        /**
442
         * XML書き込み
443
         *
444
         * @param document xml document
445
         * @throws IOException IOエラー
446
         * @throws TransformerException XML書き込みエラー
447
         */
448
        private void write(final Document document) throws IOException, TransformerException {
449
                TransformerFactory transformerFactory = TransformerFactory.newInstance();
1✔
450
                Transformer transformer = transformerFactory.newTransformer();
1✔
451
                transformer.setOutputProperty(OutputKeys.INDENT, "yes");
1✔
452
                transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
1✔
453
                transformer
1✔
454
                                .setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "http://cobertura.sourceforge.net/xml/coverage-04.dtd");
1✔
455
                try (BufferedWriter bufferedWriter = Files.newBufferedWriter(this.reportPath)) {
1✔
456
                        transformer.transform(new DOMSource(document), new StreamResult(bufferedWriter));
1✔
457
                }
458
        }
1✔
459
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc