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

future-architect / uroborosql / #747

22 Jul 2024 01:02PM UTC coverage: 91.342% (+0.4%) from 90.988%
#747

push

web-flow
refactoring api (#311)

* v0.x to master merge

* fix nanoseconds diff (java8 to java11)

* eclipse cleanup and  optimize import

* eclipse format

* optimize Imports

* remove unused annotation

* library version up

* migrate v0.x to master
- update java version 8 to 11
- update junit4 to junit5
- library version update

* fix test failed

* remove unused annotation

* fixed bug

* use var

* fix java8 coding style

* Refactoring TransactionContextManager

* Refactoring SqlAgent

* 途中コミット

* fix testcase

* fix review

* fix javadoc comments

* merge v0.x PR

* cleanup code

* cleanup test code

* change build status badge

* - agent.query, update, proc, batch にそれぞれSupplierを引数にとるメソッドを追加
- SqlQuery.paramとSqlEntityUpdate.setにFunctionを受け取るメソッドを追加

* testcaseの整形

* - SqlFluent と ExtractionCondition の分離
- Arrays.asList -> List.of への変更
- テストの整形

* - v0.x系の不具合対応の追いつき
- ログ出力の整理

* - SqlKindの整理(ENTITY_XXXの追加)
- REPL_LOGの追加
- Deprecatedメソッドの削除(SqlAgent)
- SqlAgent, ExecutionContextでsetterをfluent APIに変更

* DB接続URLの修正

* add and fix testcases.

* add event testcases.

* fix typo

* fix event handler logic.

* fix javadoc verify error.

* rename event name and add SystemColumnTest

634 of 646 new or added lines in 29 files covered. (98.14%)

2 existing lines in 1 file now uncovered.

8746 of 9575 relevant lines covered (91.34%)

0.91 hits per line

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

93.43
/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.slf4j.Logger;
34
import org.slf4j.LoggerFactory;
35
import org.w3c.dom.Document;
36
import org.w3c.dom.Element;
37

38
import jp.co.future.uroborosql.SqlAgent;
39
import jp.co.future.uroborosql.utils.ObjectUtils;
40

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

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

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

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

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

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

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

97
                private PointBranch(final Range range) {
1✔
98
                }
1✔
99

100
                private void add(final BranchCoverageState state) {
101
                        status.add(state);
1✔
102
                }
1✔
103

104
                private int coveredSize() {
105
                        return BranchCoverageState.getCoveredSize(status);
1✔
106
                }
107
        }
108

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

115
                private LineBranch(final int rowIndex) {
1✔
116
                }
1✔
117

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

121
                        branch.add(state);
1✔
122
                }
1✔
123

124
                private int branchSize() {
125
                        return branches.size() * 2;
1✔
126
                }
127

128
                private int coveredSize() {
129
                        return branches.values().stream().mapToInt(PointBranch::coveredSize).sum();
1✔
130
                }
131
        }
132

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

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

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

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

176
                private int toRow(final Range target) {
177
                        for (var range : lineRanges) {
1✔
178
                                if (range.hasIntersection(target)) {
1✔
179
                                        return range.getLineIndex();
1✔
180
                                }
181
                        }
1✔
182
                        return -1;
×
183
                }
184

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

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

206
                private PackageSummary(final String packagePath) {
1✔
207
                        this.packagePath = packagePath;
1✔
208
                }
1✔
209
        }
210

211
        private final Map<String, Map<String, SqlCoverage>> coverages = new ConcurrentHashMap<>();
1✔
212
        private final Path reportPath;
213
        private final Path sourcesDirPath;
214
        private boolean updated = true;
1✔
215

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

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

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

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

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

271
        @Override
272
        public synchronized void onSqlAgentClose() {
273
                try {
274
                        write();
1✔
UNCOV
275
                } catch (Exception ex) {
×
UNCOV
276
                        COVERAGE_LOG.error(ex.getMessage(), ex);
×
277
                }
1✔
278
        }
1✔
279

280
        private void init() {
281
                //JVM終了時に書き込み
282
                Runtime.getRuntime().addShutdownHook(new Thread(() -> {
1✔
283
                        try {
284
                                write();
1✔
285
                        } catch (Exception ex) {
×
286
                                COVERAGE_LOG.error(ex.getMessage(), ex);
×
287
                        }
1✔
288
                }));
1✔
289
        }
1✔
290

291
        synchronized void write() throws IOException, ParserConfigurationException, TransformerException {
292
                if (!updated) {//更新が無い場合は書き込みしない
1✔
293
                        return;
1✔
294
                }
295

296
                var packageNodes = summaryPackages();
1✔
297

298
                var documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
1✔
299
                var document = documentBuilder.newDocument();
1✔
300

301
                var coverage = document.createElement("coverage");
1✔
302

303
                coverage.setAttribute("timestamp", String.valueOf(System.currentTimeMillis()));
1✔
304
                coverage.setAttribute("complexity", "0");
1✔
305
                coverage.setAttribute("version", "0.1");
1✔
306

307
                document.appendChild(coverage);
1✔
308
                var sources = document.createElement("sources");
1✔
309
                coverage.appendChild(sources);
1✔
310
                var source = document.createElement("source");
1✔
311
                source.setTextContent(sourcesDirPath.toString());
1✔
312
                sources.appendChild(source);
1✔
313

314
                var packages = document.createElement("packages");
1✔
315
                coverage.appendChild(packages);
1✔
316

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

320
                var lines = total.line;
1✔
321
                coverage.setAttribute("lines-valid", String.valueOf(lines.valid));
1✔
322
                coverage.setAttribute("lines-covered", String.valueOf(lines.covered));
1✔
323
                coverage.setAttribute("lines-rate", String.valueOf(lines.getRate()));
1✔
324

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

330
                write(document);
1✔
331

332
                //書込が終了したので「更新なし」にする
333
                updated = false;
1✔
334
        }
1✔
335

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

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

354
        }
355

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

365
                        var classes = document.createElement("classes");
1✔
366
                        packageElm.appendChild(classes);
1✔
367

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

375
                        allTotal.add(total);
1✔
376
                }
1✔
377

378
                return allTotal;
1✔
379
        }
380

381
        private CoverageSummaryTotal renderClass(final Document document, final Element classes,
382
                        final SqlCoverage coverageInfo) {
383

384
                var total = new CoverageSummaryTotal();
1✔
385

386
                var classElm = document.createElement("class");
1✔
387
                classElm.setAttribute("name", coverageInfo.name);
1✔
388
                classElm.setAttribute("filename", coverageInfo.name);
1✔
389
                classes.appendChild(classElm);
1✔
390

391
                var methods = document.createElement("methods");
1✔
392
                classElm.appendChild(methods);
1✔
393

394
                var lines = document.createElement("lines");
1✔
395
                classElm.appendChild(lines);
1✔
396

397
                total.line.valid = coverageInfo.lineRanges.size();
1✔
398
                for (var range : coverageInfo.lineRanges) {
1✔
399
                        var no = range.getLineIndex() + 1;
1✔
400
                        var hit = coverageInfo.hitLines[range.getLineIndex()];
1✔
401
                        if (hit > 0) {
1✔
402
                                total.line.covered++;
1✔
403
                        }
404

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

419
                        } else {
1✔
420
                                line.setAttribute("branch", "false");
1✔
421
                        }
422
                }
1✔
423

424
                classElm.setAttribute("line-rate", String.valueOf(total.line.getRate()));
1✔
425
                classElm.setAttribute("branch-rate", String.valueOf(total.branch.getRate()));
1✔
426

427
                return total;
1✔
428
        }
429

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