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

future-architect / uroborosql / #743

28 Jun 2024 04:17PM UTC coverage: 90.988% (+0.5%) from 90.484%
#743

push

web-flow
add paramIfNotEmpty method (#318)

* 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

* add paramIfNotEmpty method and StringUtils rename ObjectUtils

* fix review comments.

* remove unused import

1695 of 1958 new or added lines in 97 files covered. (86.57%)

26 existing lines in 10 files now uncovered.

8249 of 9066 relevant lines covered (90.99%)

0.91 hits per line

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

97.65
/src/main/java/jp/co/future/uroborosql/coverage/reports/html/SqlCoverageReport.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.reports.html;
8

9
import java.io.BufferedWriter;
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.time.ZonedDateTime;
15
import java.time.format.DateTimeFormatter;
16
import java.util.ArrayList;
17
import java.util.Arrays;
18
import java.util.Comparator;
19
import java.util.HashMap;
20
import java.util.List;
21
import java.util.Map;
22
import java.util.Scanner;
23
import java.util.stream.Collectors;
24
import java.util.stream.IntStream;
25

26
import org.slf4j.Logger;
27
import org.slf4j.LoggerFactory;
28

29
import jp.co.future.uroborosql.coverage.CoverageHandler;
30
import jp.co.future.uroborosql.coverage.LineRange;
31
import jp.co.future.uroborosql.coverage.PassedRoute;
32
import jp.co.future.uroborosql.coverage.Range;
33
import jp.co.future.uroborosql.coverage.Ranges;
34
import jp.co.future.uroborosql.utils.ObjectUtils;
35

36
class SqlCoverageReport {
37
        /** カバレッジロガー. */
38
        private static final Logger COVERAGE_LOG = LoggerFactory.getLogger("jp.co.future.uroborosql.sql.coverage");
1✔
39

40
        private final String name;
41
        private final String sql;
42
        private final Path path;
43
        private final Path reportDirPath;
44
        private final String md5;
45
        private boolean updated = true;
1✔
46
        /**
47
         * 各行範囲
48
         */
49
        private final List<LineRange> lineRanges;
50
        private final int sqlLineCount;
51

52
        /**
53
         * ブランチカバレッジ情報
54
         */
55
        private final Map<Range, RangeBranch> branches = new HashMap<>();
1✔
56

57
        /**
58
         * 各行通過回数
59
         */
60
        private final int[] hitLines;
61

62
        /**
63
         * 各通過範囲
64
         */
65
        private final Ranges passRanges = new Ranges();
1✔
66

67
        SqlCoverageReport(final String name, final String sql, final String md5, final Path reportDirPath,
68
                        final int hashIndex) {
1✔
69
                this.name = hashIndex <= 0 ? name : name + "_hash_" + hashIndex;
1✔
70
                this.reportDirPath = reportDirPath;
1✔
71
                this.path = reportDirPath.resolve(this.name + ".html");
1✔
72
                this.sql = sql;
1✔
73
                this.md5 = md5;
1✔
74
                this.lineRanges = CoverageHandler.parseLineRanges(sql);
1✔
75
                this.sqlLineCount = CoverageHandler.getLineRanges(sql).size();
1✔
76
                this.hitLines = new int[this.sqlLineCount];
1✔
77
        }
1✔
78

79
        /**
80
         * カバレッジ情報追加
81
         *
82
         * @param passRoute カバレッジ情報
83
         */
84
        void accept(final PassedRoute passRoute) {
85
                //各行の通過情報を集計
86
                for (var range : lineRanges) {
1✔
87
                        if (passRoute.isHit(range)) {
1✔
88
                                hitLines[range.getLineIndex()]++;
1✔
89
                        }
90
                }
1✔
91
                //各行のブランチ情報を集計
92
                passRoute.getRangeBranchStatus().forEach((range, state) -> {
1✔
93
                        var branch = branches.computeIfAbsent(range, RangeBranch::new);
1✔
94
                        branch.add(state);
1✔
95
                });
1✔
96

97
                //各通過情報を集計v
98
                passRanges.addAll(passRoute.getHitRanges());
1✔
99

100
                updated = true;
1✔
101

102
        }
1✔
103

104
        public String getMd5() {
105
                return md5;
×
106
        }
107

108
        public String getName() {
109
                return name;
1✔
110
        }
111

112
        void writeHtml() {
113
                if (!updated) {
1✔
114
                        return;
1✔
115
                }
116
                try {
117
                        Files.createDirectories(this.path.getParent());
1✔
118
                        try (var writer = Files.newBufferedWriter(this.path, StandardCharsets.UTF_8)) {
1✔
119
                                writePrefix(writer);
1✔
120

121
                                writeHeaderSection(writer);
1✔
122

123
                                writeTablePrefix(writer);
1✔
124
                                writeLineNoSection(writer);
1✔
125
                                writeHitsSection(writer);
1✔
126
                                writeSourceSection(writer);
1✔
127
                                writeTableSuffix(writer);
1✔
128

129
                                writeSuffix(writer);
1✔
130
                        }
NEW
131
                } catch (IOException ex) {
×
NEW
132
                        COVERAGE_LOG.error(ex.getMessage(), ex);
×
133
                }
1✔
134
                updated = false;
1✔
135
        }
1✔
136

137
        private void writeLineNoSection(final BufferedWriter writer) throws IOException {
138
                writer.append("<td class=\"line-no\">");
1✔
139
                writer.newLine();
1✔
140
                writer.append("<pre>");
1✔
141
                writer.newLine();
1✔
142
                writer.append(IntStream.rangeClosed(1, this.sqlLineCount)
1✔
143
                                .mapToObj(String::valueOf)
1✔
144
                                .collect(Collectors.joining("\n")));
1✔
145
                writer.newLine();
1✔
146
                writer.append("</pre>");
1✔
147
                writer.newLine();
1✔
148
                writer.append("</td>");
1✔
149
                writer.newLine();
1✔
150
        }
1✔
151

152
        private void writeHitsSection(final BufferedWriter writer) throws IOException {
153
                writer.append("<td class=\"line-coverage\">");
1✔
154
                writer.append(IntStream.range(0, this.sqlLineCount)
1✔
155
                                .mapToObj(i -> {
1✔
156
                                        if (!isTargetLine(i)) {
1✔
157
                                                return "<span class=\"cline no-target\">&nbsp;</span>";
1✔
158
                                        } else {
159
                                                var className = hitLines[i] > 0 ? "cline-yes" : "cline-no";
1✔
160
                                                var text = hitLines[i] > 0 ? hitLines[i] + "<em>×</em>" : "<em>!</em>";
1✔
161
                                                return "<span class=\"cline " + className + "\">" + text + "</span>";
1✔
162
                                        }
163
                                })
164
                                .collect(Collectors.joining("\n")));
1✔
165
                writer.append("</td>");
1✔
166
        }
1✔
167

168
        private boolean isTargetLine(final int index) {
169
                return lineRanges.stream()
1✔
170
                                .mapToInt(LineRange::getLineIndex)
1✔
171
                                .anyMatch(i -> i == index);
1✔
172
        }
173

174
        private void writeSourceSection(final BufferedWriter writer) throws IOException {
175
                writer.append("<td class=\"source\">");
1✔
176
                writer.newLine();
1✔
177
                writer.append("<pre>");
1✔
178
                writer.newLine();
1✔
179
                writer.append("<code class=\"sql\">");
1✔
180

181
                var inactives = new Ranges(0, this.sql.length() - 1);
1✔
182
                inactives.minus(lineRanges);
1✔
183
                var passes = this.passRanges.copy();
1✔
184
                passes.addAll(inactives);//カバレッジ対象でない行を通過したとみなす。
1✔
185
                passes.minus(branches.values().stream()
1✔
186
                                .map(RangeBranch::getRange)
1✔
187
                                .collect(Collectors.toList()));
1✔
188

189
                var start = 0;
1✔
190
                var pass = nextPassRange(passes, start);
1✔
191
                var branch = nextRangeBranch(start);
1✔
192

193
                while (pass != null && branch != null) {
1✔
194
                        if (branch.getRange().getStart() <= pass.getStart()) {
1✔
195
                                //branch
196
                                start = appendBranch(writer, start, branch);
1✔
197
                        } else {
198
                                start = appendPassed(writer, start, pass);
1✔
199
                        }
200
                        pass = nextPassRange(passes, start);
1✔
201
                        branch = nextRangeBranch(start);
1✔
202
                }
203
                while (pass != null) {
1✔
204
                        start = appendPassed(writer, start, pass);
1✔
205
                        pass = nextPassRange(passes, start);
1✔
206
                }
207
                while (branch != null) {
1✔
208
                        start = appendBranch(writer, start, branch);
×
209
                        branch = nextRangeBranch(start);
×
210
                }
211

212
                if (start < this.sql.length()) {
1✔
213
                        appendNotCovered(writer, start, this.sql.length());
×
214
                }
215
                writer.append("</code>");
1✔
216
                writer.append("</pre>");
1✔
217
                writer.newLine();
1✔
218
                writer.append("</td>");
1✔
219
                writer.newLine();
1✔
220
        }
1✔
221

222
        private int appendBranch(final BufferedWriter writer, final int start, final RangeBranch branch)
223
                        throws IOException {
224
                var range = branch.getRange();
1✔
225
                if (start < range.getStart()) {
1✔
226
                        appendNotCovered(writer, start, range.getStart());
1✔
227
                }
228

229
                var size = branch.branchSize();
1✔
230
                var covered = branch.coveredSize();
1✔
231

232
                var html = size <= covered
1✔
233
                                ? buildLinesHtml(this.sql.substring(range.getStart(), range.getEnd() + 1), "", "")
1✔
234
                                : buildLinesHtml(this.sql.substring(range.getStart(), range.getEnd() + 1),
1✔
235
                                                "<span class=\"not-covered-branch\" title=\"branch not covered\" >", "</span>");
236
                writer.append(html);
1✔
237
                return range.getEnd() + 1;
1✔
238
        }
239

240
        private int appendPassed(final BufferedWriter writer, final int start, final Range pass) throws IOException {
241

242
                if (start < pass.getStart()) {
1✔
243
                        appendNotCovered(writer, start, pass.getStart());
1✔
244
                }
245
                var html = buildLinesHtml(this.sql.substring(Math.max(pass.getStart(), start), pass.getEnd() + 1),
1✔
246
                                "", "");
247
                writer.append(html);
1✔
248
                return pass.getEnd() + 1;
1✔
249
        }
250

251
        private void appendNotCovered(final BufferedWriter writer, final int start, final int end) throws IOException {
252
                var html = buildLinesHtml(this.sql.substring(start, end),
1✔
253
                                "<span class=\"not-covered\" title=\"statement not covered\" >", "</span>");
254
                writer.append(html);
1✔
255
        }
1✔
256

257
        private String buildLinesHtml(final String linesText, final String prefix, final String suffix) {
258
                return toLines(linesText).stream()
1✔
259
                                .map(this::escapeHtml4)
1✔
260
                                .map(s -> prefix + s + suffix)
1✔
261
                                .collect(Collectors.joining("\n"));
1✔
262
        }
263

264
        private Range nextPassRange(final Ranges passes, final int start) {
265
                return passes.stream()
1✔
266
                                .sorted()
1✔
267
                                .filter(r -> start <= r.getEnd())
1✔
268
                                .findFirst()
1✔
269
                                .orElse(null);
1✔
270
        }
271

272
        private RangeBranch nextRangeBranch(final int start) {
273
                return branches.values().stream()
1✔
274
                                .sorted(Comparator.comparing(RangeBranch::getRange))
1✔
275
                                .filter(r -> start <= r.getRange().getEnd())
1✔
276
                                .findFirst()
1✔
277
                                .orElse(null);
1✔
278
        }
279

280
        private static List<String> toLines(final String text) {
281
                var ret = new ArrayList<String>();
1✔
282
                var s = text + "+";//最後の改行を検知するためダミー文字を付与
1✔
283
                try (var scanner = new Scanner(s)) {
1✔
284
                        while (scanner.hasNextLine()) {
1✔
285
                                var line = scanner.nextLine();
1✔
286
                                ret.add(line);
1✔
287
                        }
1✔
288
                }
289
                //ダミー文字除去
290
                var last = ret.get(ret.size() - 1);
1✔
291
                ret.set(ret.size() - 1, last.substring(0, last.length() - 1));
1✔
292

293
                return ret;
1✔
294
        }
295

296
        private void writePrefix(final BufferedWriter writer) throws IOException {
297
                writer.append("<!DOCTYPE html>");
1✔
298
                writer.newLine();
1✔
299
                writer.append("<html lang=\"en\">");
1✔
300
                writer.newLine();
1✔
301
                writer.append("<head>");
1✔
302
                writer.newLine();
1✔
303
                writer.append("    <meta charset=\"utf-8\" />");
1✔
304
                writer.newLine();
1✔
305
                writer.append("    <title>uroboroSQL code coverage report for ").append(this.name).append("</title>");
1✔
306
                writer.newLine();
1✔
307
                writer.append("    <link rel=\"stylesheet\" href=\"").append(getAssetPath()).append("/style.css\">");
1✔
308
                writer.newLine();
1✔
309
                writer.append("    <script src=\"").append(getAssetPath()).append("/jquery-3.2.0.min.js\"></script>");
1✔
310
                writer.newLine();
1✔
311
                writer.append("    <script src=\"").append(getAssetPath()).append("/highlight.pack.js\"></script>");
1✔
312
                writer.newLine();
1✔
313
                writer.append("    <script src=\"").append(getAssetPath()).append("/sqlcov.js\"></script>");
1✔
314
                writer.newLine();
1✔
315
                writer.append("</head>");
1✔
316
                writer.newLine();
1✔
317
                writer.append("<body>");
1✔
318
                writer.newLine();
1✔
319
        }
1✔
320

321
        private void writeHeaderSection(final BufferedWriter writer) throws IOException {
322
                var lineCount = getLineValidSize();
1✔
323
                var lineCovered = getLineCoveredSize();
1✔
324
                var branchesCount = getBranchValidSize();
1✔
325
                var branchesCovered = getBranchCoveredSize();
1✔
326

327
                writer.append("<div class=\"global-header\">");
1✔
328
                writer.newLine();
1✔
329
                writer.append("    <img class=\"icon\" src=\"").append(getAssetPath()).append("/icon.png\" />");
1✔
330
                writer.newLine();
1✔
331
                writer.append("    <span class=\"title\">uroboroSQL coverage</span>");
1✔
332
                writer.newLine();
1✔
333
                writer.append("</div>");
1✔
334
                writer.newLine();
1✔
335
                writer.append("<h1>").append(escapeHtml4(this.name)).append("</h1>");
1✔
336
                writer.newLine();
1✔
337
                writer.append("<div class=\"nav\">")
1✔
338
                                .append("<a href=\"").append(getAssetPath()).append("/index.html\">All Files</a>")
1✔
339
                                .append("</div>");
1✔
340
                writer.newLine();
1✔
341
                writer.append("<div class=\"header\">");
1✔
342
                writer.newLine();
1✔
343
                writer.append("    <div class=\"summary\">");
1✔
344
                writer.newLine();
1✔
345
                writer.append("      <strong>").append(CoverageHandler.percentStr(lineCovered, lineCount))
1✔
346
                                .append("% </strong>");
1✔
347
                writer.newLine();
1✔
348
                writer.append("      <span>Lines</span>");
1✔
349
                writer.newLine();
1✔
350
                writer.append("      <span class=\"fraction\">").append(String.valueOf(lineCovered)).append("/")
1✔
351
                                .append(String.valueOf(lineCount)).append("</span>");
1✔
352
                writer.newLine();
1✔
353
                writer.append("    </div>");
1✔
354
                writer.newLine();
1✔
355
                writer.append("    <div class=\"summary\">");
1✔
356
                writer.newLine();
1✔
357
                writer.append("      <strong>").append(CoverageHandler.percentStr(branchesCovered, branchesCount))
1✔
358
                                .append("% </strong>");
1✔
359
                writer.newLine();
1✔
360
                writer.append("      <span>Branches</span>");
1✔
361
                writer.newLine();
1✔
362
                writer.append("      <span class=\"fraction\">").append(String.valueOf(branchesCovered)).append("/")
1✔
363
                                .append(String.valueOf(branchesCount)).append("</span>");
1✔
364
                writer.newLine();
1✔
365
                writer.append("    </div>");
1✔
366
                writer.newLine();
1✔
367
                writer.append("</div>");
1✔
368
                writer.newLine();
1✔
369
        }
1✔
370

371
        private String getAssetPath() {
372
                return this.path.getParent().relativize(this.reportDirPath).toString();
1✔
373
        }
374

375
        private void writeTablePrefix(final BufferedWriter writer) throws IOException {
376
                writer.append("<table class=\"coverage\">");
1✔
377
                writer.newLine();
1✔
378
                writer.append("<tr>");
1✔
379
                writer.newLine();
1✔
380
        }
1✔
381

382
        private void writeTableSuffix(final BufferedWriter writer) throws IOException {
383
                writer.append("</tr>");
1✔
384
                writer.newLine();
1✔
385
                writer.append("</table>");
1✔
386
                writer.newLine();
1✔
387
        }
1✔
388

389
        private void writeSuffix(final BufferedWriter writer) throws IOException {
390
                writer.append("<div class=\"footer\">code coverage report generated by ")
1✔
391
                                .append("<a href=\"https://github.com/future-architect/uroborosql\" target=\"_blank\">uroboroSQL</a> at ")
1✔
392
                                .append(ZonedDateTime.now().format(DateTimeFormatter.ISO_ZONED_DATE_TIME))
1✔
393
                                .append(".</div>");
1✔
394
                writer.newLine();
1✔
395
                writer.append("</body>");
1✔
396
                writer.newLine();
1✔
397
                writer.append("</html>");
1✔
398
                writer.newLine();
1✔
399
        }
1✔
400

401
        int getLineValidSize() {
402
                return lineRanges.size();
1✔
403
        }
404

405
        int getLineCoveredSize() {
406
                return (int) Arrays.stream(hitLines)
1✔
407
                                .filter(i -> i > 0)
1✔
408
                                .count();
1✔
409
        }
410

411
        int getBranchValidSize() {
412
                return branches.values().stream()
1✔
413
                                .mapToInt(RangeBranch::branchSize)
1✔
414
                                .sum();
1✔
415
        }
416

417
        int getBranchCoveredSize() {
418
                return branches.values().stream()
1✔
419
                                .mapToInt(RangeBranch::coveredSize)
1✔
420
                                .sum();
1✔
421
        }
422

423
        private String escapeHtml4(final String str) {
424
                if (ObjectUtils.isEmpty(str)) {
1✔
425
                        return "";
1✔
426
                } else {
427
                        return str.replace("\"", "&quot;")
1✔
428
                                        .replace("&", "&amp;")
1✔
429
                                        .replace("<", "&lt;")
1✔
430
                                        .replace(">", "&gt;");
1✔
431
                }
432
        }
433

434
}
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