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

future-architect / uroborosql / #771

12 Sep 2024 03:50PM UTC coverage: 90.374% (-0.8%) from 91.197%
#771

Pull #331

HidekiSugimoto189
Merge branch 'feature/add_new_dialect' of https://github.com/future-architect/uroborosql into feature/add_new_dialect
Pull Request #331: add oracle v12-23 and Mariadb 5,10 dialect

497 of 703 new or added lines in 42 files covered. (70.7%)

9 existing lines in 7 files now uncovered.

8928 of 9879 relevant lines covered (90.37%)

0.9 hits per line

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

91.75
/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.File;
10
import java.io.IOException;
11
import java.nio.charset.StandardCharsets;
12
import java.nio.file.Files;
13
import java.nio.file.Path;
14
import java.nio.file.Paths;
15
import java.util.ArrayList;
16
import java.util.Comparator;
17
import java.util.EnumSet;
18
import java.util.HashMap;
19
import java.util.List;
20
import java.util.Map;
21
import java.util.Set;
22
import java.util.concurrent.ConcurrentHashMap;
23
import java.util.stream.Collectors;
24

25
import javax.xml.parsers.DocumentBuilderFactory;
26
import javax.xml.parsers.ParserConfigurationException;
27
import javax.xml.transform.OutputKeys;
28
import javax.xml.transform.TransformerException;
29
import javax.xml.transform.TransformerFactory;
30
import javax.xml.transform.dom.DOMSource;
31
import javax.xml.transform.stream.StreamResult;
32

33
import org.w3c.dom.Document;
34
import org.w3c.dom.Element;
35

36
import jp.co.future.uroborosql.SqlAgent;
37
import jp.co.future.uroborosql.log.support.CoverageLoggingSupport;
38
import jp.co.future.uroborosql.utils.ObjectUtils;
39

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

61
                private void add(final CoverageSummaryTotal total) {
62
                        this.line.add(total.line);
1✔
63
                        this.branch.add(total.branch);
1✔
64
                }
1✔
65
        }
66

67
        /**
68
         * カバレッジ数値
69
         */
70
        private static class CoverageSummary {
71
                private int valid;
72
                private int covered;
73

74
                private double getRate() {
75
                        if (valid == 0) {
1✔
76
                                return 0;
1✔
77
                        }
78
                        return (double) covered / (double) valid;
1✔
79
                }
80

81
                private void add(final CoverageSummary o) {
82
                        this.valid += o.valid;
1✔
83
                        this.covered += o.covered;
1✔
84
                }
1✔
85
        }
86

87
        /**
88
         * ポイントブランチ情報
89
         */
90
        private static class PointBranch {
91
                private final Set<BranchCoverageState> status = EnumSet.noneOf(BranchCoverageState.class);
1✔
92

93
                private PointBranch(final Range range) {
1✔
94
                }
1✔
95

96
                private void add(final BranchCoverageState state) {
97
                        status.add(state);
1✔
98
                }
1✔
99

100
                private int coveredSize() {
101
                        return BranchCoverageState.getCoveredSize(status);
1✔
102
                }
103
        }
104

105
        /**
106
         * 行ブランチ情報
107
         */
108
        private static class LineBranch {
109
                private final Map<Range, PointBranch> branches = new HashMap<>();
1✔
110

111
                private LineBranch(final int rowIndex) {
1✔
112
                }
1✔
113

114
                private void add(final Range idx, final BranchCoverageState state) {
115
                        var branch = branches.computeIfAbsent(idx, k -> new PointBranch(idx));
1✔
116

117
                        branch.add(state);
1✔
118
                }
1✔
119

120
                private int branchSize() {
121
                        return branches.size() * 2;
1✔
122
                }
123

124
                private int coveredSize() {
125
                        return branches.values().stream().mapToInt(PointBranch::coveredSize).sum();
1✔
126
                }
127
        }
128

129
        /**
130
         * SQL別カバレッジ元情報
131
         */
132
        private static class SqlCoverage {
133
                private final String name;
134
                /** 各行範囲 */
135
                private final List<LineRange> lineRanges;
136

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

142
                private SqlCoverage(final String name, final String sql, final String md5, final Path sourcesDirPath,
143
                                final int hashIndex)
144
                                throws IOException {
1✔
145
                        this.name = hashIndex <= 0 ? name : name + "_hash_" + hashIndex;
1✔
146
                        this.lineRanges = CoverageHandler.parseLineRanges(sql);
1✔
147
                        this.hitLines = new int[lineRanges.stream()
1✔
148
                                        .mapToInt(LineRange::getLineIndex)
1✔
149
                                        .max().orElse(0) + 1];
1✔
150
                        writeSqlSource(sourcesDirPath, sql);
1✔
151
                }
1✔
152

153
                /**
154
                 * カバレッジ情報追加
155
                 *
156
                 * @param passRoute カバレッジ情報
157
                 */
158
                private void accept(final PassedRoute passRoute) {
159
                        //各行の通過情報を集計
160
                        for (var range : lineRanges) {
1✔
161
                                if (passRoute.isHit(range)) {
1✔
162
                                        hitLines[range.getLineIndex()]++;
1✔
163
                                }
164
                        }
1✔
165
                        //各行のブランチ情報を集計
166
                        passRoute.getRangeBranchStatus().forEach((range, state) -> {
1✔
167
                                var lineBranch = lineBranches.computeIfAbsent(toRow(range), LineBranch::new);
1✔
168
                                lineBranch.add(range, state);
1✔
169
                        });
1✔
170
                }
1✔
171

172
                private int toRow(final Range target) {
173
                        for (var range : lineRanges) {
1✔
174
                                if (range.hasIntersection(target)) {
1✔
175
                                        return range.getLineIndex();
1✔
176
                                }
177
                        }
1✔
178
                        return -1;
×
179
                }
180

181
                /**
182
                 * SQL ファイル書き込み
183
                 *
184
                 * @param sourcesDirPath ディレクトリ
185
                 * @param sql SQL
186
                 * @throws IOException IOエラー
187
                 */
188
                private void writeSqlSource(final Path sourcesDirPath, final String sql) throws IOException {
189
                        var path = sourcesDirPath.resolve(name);
1✔
190
                        Files.createDirectories(path.getParent());
1✔
191
                        Files.write(path, sql.getBytes(StandardCharsets.UTF_8));
1✔
192
                }
1✔
193
        }
194

195
        /**
196
         * パッケージで集約した情報
197
         */
198
        private static class PackageSummary {
199
                private final String packagePath;
200
                private final List<SqlCoverage> coverageInfos = new ArrayList<>();
1✔
201

202
                private PackageSummary(final String packagePath) {
1✔
203
                        this.packagePath = packagePath;
1✔
204
                }
1✔
205
        }
206

207
        private final Map<String, Map<String, SqlCoverage>> coverages = new ConcurrentHashMap<>();
1✔
208
        private final Path reportPath;
209
        private final Path sourcesDirPath;
210
        private boolean updated = true;
1✔
211

212
        /**
213
         * コンストラクタ<br>
214
         *
215
         * <pre>
216
         * system property "uroborosql.sql.coverage.file" が指定された場合、指定されたPATHに xmlレポートを出力します。
217
         * 指定の無い場合、デフォルトで "./target/coverage/sql-cover.xml" に xmlレポートを出力します。
218
         * </pre>
219
         */
220
        public CoberturaCoverageHandler() {
1✔
221
                var s = System.getProperty(SqlAgent.KEY_SQL_COVERAGE + ".file");
1✔
222
                if (ObjectUtils.isNotEmpty(s)) {
1✔
223
                        this.reportPath = Paths.get(s);
1✔
224
                } else {
225
                        this.reportPath = Paths.get("target", "coverage", "sql-cover.xml");
1✔
226
                }
227
                this.sourcesDirPath = this.reportPath.toAbsolutePath().getParent().resolve("sqls");
1✔
228
                init();
1✔
229
        }
1✔
230

231
        /**
232
         * コンストラクタ
233
         *
234
         * @param reportPath レポートファイルPATH
235
         */
236
        public CoberturaCoverageHandler(final Path reportPath) {
×
237
                this.reportPath = reportPath;
×
238
                this.sourcesDirPath = this.reportPath.toAbsolutePath().getParent().resolve("sqls");
×
239
                init();
×
240
        }
×
241

242
        @Override
243
        public synchronized void accept(final CoverageData coverageData) {
244
                if (ObjectUtils.isEmpty(coverageData.getSqlName())) {
1✔
245
                        //SQL名の設定されていないSQLは集約しない
246
                        return;
1✔
247
                }
248

249
                var map = coverages.computeIfAbsent(coverageData.getSqlName(),
1✔
250
                                k -> new ConcurrentHashMap<>());
1✔
251
                var sqlCoverage = map.get(coverageData.getMd5());
1✔
252
                if (sqlCoverage == null) {
1✔
253
                        try {
254
                                sqlCoverage = new SqlCoverage(coverageData.getSqlName(), coverageData.getSql(), coverageData.getMd5(),
1✔
255
                                                sourcesDirPath, map.size());
1✔
256
                        } catch (IOException ex) {
×
NEW
257
                                errorWith(COVERAGE_LOG)
×
NEW
258
                                                .setMessage(ex.getMessage())
×
NEW
259
                                                .setCause(ex)
×
NEW
260
                                                .log();
×
UNCOV
261
                                return;
×
262
                        }
1✔
263
                        map.put(coverageData.getMd5(), sqlCoverage);
1✔
264
                }
265

266
                sqlCoverage.accept(coverageData.getPassRoute());
1✔
267
                updated = true;
1✔
268
        }
1✔
269

270
        @Override
271
        public synchronized void onSqlAgentClose() {
272
                try {
273
                        write();
1✔
274
                } catch (Exception ex) {
1✔
275
                        errorWith(COVERAGE_LOG)
1✔
276
                                        .setMessage(ex.getMessage())
1✔
277
                                        .setCause(ex)
1✔
278
                                        .log();
1✔
279
                }
1✔
280
        }
1✔
281

282
        private void init() {
283
                //JVM終了時に書き込み
284
                Runtime.getRuntime().addShutdownHook(new Thread(() -> {
1✔
285
                        try {
286
                                write();
1✔
287
                        } catch (Exception ex) {
×
NEW
288
                                errorWith(COVERAGE_LOG)
×
NEW
289
                                                .setMessage(ex.getMessage())
×
NEW
290
                                                .setCause(ex)
×
NEW
291
                                                .log();
×
292
                        }
1✔
293
                }));
1✔
294
        }
1✔
295

296
        synchronized void write() throws IOException, ParserConfigurationException, TransformerException {
297
                if (!updated) {//更新が無い場合は書き込みしない
1✔
298
                        return;
1✔
299
                }
300

301
                var packageNodes = summaryPackages();
1✔
302

303
                var documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
1✔
304
                var document = documentBuilder.newDocument();
1✔
305

306
                var coverage = document.createElement("coverage");
1✔
307

308
                coverage.setAttribute("timestamp", String.valueOf(System.currentTimeMillis()));
1✔
309
                coverage.setAttribute("complexity", "0");
1✔
310
                coverage.setAttribute("version", "0.1");
1✔
311

312
                document.appendChild(coverage);
1✔
313
                var sources = document.createElement("sources");
1✔
314
                coverage.appendChild(sources);
1✔
315
                var source = document.createElement("source");
1✔
316
                source.setTextContent(sourcesDirPath.toString());
1✔
317
                sources.appendChild(source);
1✔
318

319
                var packages = document.createElement("packages");
1✔
320
                coverage.appendChild(packages);
1✔
321

322
                //packages内のrenderとカバレッジ集計
323
                var total = renderPackages(document, packages, packageNodes);
1✔
324

325
                var lines = total.line;
1✔
326
                coverage.setAttribute("lines-valid", String.valueOf(lines.valid));
1✔
327
                coverage.setAttribute("lines-covered", String.valueOf(lines.covered));
1✔
328
                coverage.setAttribute("lines-rate", String.valueOf(lines.getRate()));
1✔
329

330
                var branches = total.branch;
1✔
331
                coverage.setAttribute("branches-valid", String.valueOf(branches.valid));
1✔
332
                coverage.setAttribute("branches-covered", String.valueOf(branches.covered));
1✔
333
                coverage.setAttribute("branches-rate", String.valueOf(branches.getRate()));
1✔
334

335
                write(document);
1✔
336

337
                //書込が終了したので「更新なし」にする
338
                updated = false;
1✔
339
        }
1✔
340

341
        /**
342
         * パッケージ単位にまとめる
343
         *
344
         * @return パッケージ単位情報
345
         */
346
        private List<PackageSummary> summaryPackages() {
347
                var summaries = new HashMap<String, PackageSummary>();
1✔
348

349
                coverages.forEach((name, c) -> {
1✔
350
                        var p = Paths.get(name).getParent();
1✔
351
                        var pkg = p != null ? p.toString().replace(File.separatorChar, '.') : "_root_";
1✔
352
                        var summary = summaries.computeIfAbsent(pkg, k -> new PackageSummary(pkg));
1✔
353
                        summary.coverageInfos.addAll(c.values());
1✔
354
                });
1✔
355
                return summaries.values().stream()
1✔
356
                                .sorted(Comparator.comparing(p -> p.packagePath))
1✔
357
                                .collect(Collectors.toList());
1✔
358

359
        }
360

361
        private CoverageSummaryTotal renderPackages(final Document document, final Element packages,
362
                        final List<PackageSummary> packageNodes) {
363
                var allTotal = new CoverageSummaryTotal();
1✔
364
                for (var packageNode : packageNodes) {
1✔
365
                        var total = new CoverageSummaryTotal();
1✔
366
                        var packageElm = document.createElement("package");
1✔
367
                        packageElm.setAttribute("name", packageNode.packagePath);
1✔
368
                        packages.appendChild(packageElm);
1✔
369

370
                        var classes = document.createElement("classes");
1✔
371
                        packageElm.appendChild(classes);
1✔
372

373
                        for (var coverageInfo : packageNode.coverageInfos) {
1✔
374
                                //class内のrenderとカバレッジ集計
375
                                total.add(renderClass(document, classes, coverageInfo));
1✔
376
                        }
1✔
377
                        packageElm.setAttribute("line-rate", String.valueOf(total.line.getRate()));
1✔
378
                        packageElm.setAttribute("branch-rate", String.valueOf(total.branch.getRate()));
1✔
379

380
                        allTotal.add(total);
1✔
381
                }
1✔
382

383
                return allTotal;
1✔
384
        }
385

386
        private CoverageSummaryTotal renderClass(final Document document, final Element classes,
387
                        final SqlCoverage coverageInfo) {
388

389
                var total = new CoverageSummaryTotal();
1✔
390

391
                var classElm = document.createElement("class");
1✔
392
                classElm.setAttribute("name", coverageInfo.name);
1✔
393
                classElm.setAttribute("filename", coverageInfo.name);
1✔
394
                classes.appendChild(classElm);
1✔
395

396
                var methods = document.createElement("methods");
1✔
397
                classElm.appendChild(methods);
1✔
398

399
                var lines = document.createElement("lines");
1✔
400
                classElm.appendChild(lines);
1✔
401

402
                total.line.valid = coverageInfo.lineRanges.size();
1✔
403
                for (var range : coverageInfo.lineRanges) {
1✔
404
                        var no = range.getLineIndex() + 1;
1✔
405
                        var hit = coverageInfo.hitLines[range.getLineIndex()];
1✔
406
                        if (hit > 0) {
1✔
407
                                total.line.covered++;
1✔
408
                        }
409

410
                        var line = document.createElement("line");
1✔
411
                        lines.appendChild(line);
1✔
412
                        line.setAttribute("number", String.valueOf(no));
1✔
413
                        line.setAttribute("hits", String.valueOf(hit));
1✔
414
                        var lineBranch = coverageInfo.lineBranches.get(range.getLineIndex());
1✔
415
                        if (lineBranch != null) {
1✔
416
                                var size = lineBranch.branchSize();
1✔
417
                                var covered = lineBranch.coveredSize();
1✔
418
                                line.setAttribute("branch", "true");
1✔
419
                                line.setAttribute("condition-coverage",
1✔
420
                                                CoverageHandler.percentStr(covered, size) + "% (" + covered + "/" + size + ")");
1✔
421
                                total.branch.valid += size;
1✔
422
                                total.branch.covered += covered;
1✔
423

424
                        } else {
1✔
425
                                line.setAttribute("branch", "false");
1✔
426
                        }
427
                }
1✔
428

429
                classElm.setAttribute("line-rate", String.valueOf(total.line.getRate()));
1✔
430
                classElm.setAttribute("branch-rate", String.valueOf(total.branch.getRate()));
1✔
431

432
                return total;
1✔
433
        }
434

435
        /**
436
         * XML書き込み
437
         *
438
         * @param document xml document
439
         * @throws IOException IOエラー
440
         * @throws TransformerException XML書き込みエラー
441
         */
442
        private void write(final Document document) throws IOException, TransformerException {
443
                var transformerFactory = TransformerFactory.newInstance();
1✔
444
                var transformer = transformerFactory.newTransformer();
1✔
445
                transformer.setOutputProperty(OutputKeys.INDENT, "yes");
1✔
446
                transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
1✔
447
                transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,
1✔
448
                                "http://cobertura.sourceforge.net/xml/coverage-04.dtd");
449
                try (var bufferedWriter = Files.newBufferedWriter(this.reportPath)) {
1✔
450
                        transformer.transform(new DOMSource(document), new StreamResult(bufferedWriter));
1✔
451
                }
452
        }
1✔
453
}
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