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

future-architect / uroborosql / #770

12 Sep 2024 03:28PM UTC coverage: 90.35% (-0.9%) from 91.21%
#770

push

web-flow
 Enable per-SQL-ID log suppression (#322) (#332)

* 各Logger用のインタフェースを作成し必要なクラスがimplementsするように修正。そのうえでSLF4J 2.xのAPIを利用するように変更(性能改善)

* Changed to use slf4j v2.0 API
Also added suppressLogging API

* Refactoring log-related class and method names.

384 of 587 new or added lines in 32 files covered. (65.42%)

9 existing lines in 7 files now uncovered.

8885 of 9834 relevant lines covered (90.35%)

0.9 hits per line

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

96.5
/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 jp.co.future.uroborosql.coverage.CoverageHandler;
27
import jp.co.future.uroborosql.coverage.LineRange;
28
import jp.co.future.uroborosql.coverage.PassedRoute;
29
import jp.co.future.uroborosql.coverage.Range;
30
import jp.co.future.uroborosql.coverage.Ranges;
31
import jp.co.future.uroborosql.log.support.CoverageLoggingSupport;
32
import jp.co.future.uroborosql.utils.ObjectUtils;
33

34
class SqlCoverageReport implements CoverageLoggingSupport {
35
        private final String name;
36
        private final String sql;
37
        private final Path path;
38
        private final Path reportDirPath;
39
        private final String md5;
40
        private boolean updated = true;
1✔
41
        /**
42
         * 各行範囲
43
         */
44
        private final List<LineRange> lineRanges;
45
        private final int sqlLineCount;
46

47
        /**
48
         * ブランチカバレッジ情報
49
         */
50
        private final Map<Range, RangeBranch> branches = new HashMap<>();
1✔
51

52
        /**
53
         * 各行通過回数
54
         */
55
        private final int[] hitLines;
56

57
        /**
58
         * 各通過範囲
59
         */
60
        private final Ranges passRanges = new Ranges();
1✔
61

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

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

92
                //各通過情報を集計v
93
                passRanges.addAll(passRoute.getHitRanges());
1✔
94

95
                updated = true;
1✔
96

97
        }
1✔
98

99
        public String getMd5() {
100
                return md5;
×
101
        }
102

103
        public String getName() {
104
                return name;
1✔
105
        }
106

107
        void writeHtml() {
108
                if (!updated) {
1✔
109
                        return;
1✔
110
                }
111
                try {
112
                        Files.createDirectories(this.path.getParent());
1✔
113
                        try (var writer = Files.newBufferedWriter(this.path, StandardCharsets.UTF_8)) {
1✔
114
                                writePrefix(writer);
1✔
115

116
                                writeHeaderSection(writer);
1✔
117

118
                                writeTablePrefix(writer);
1✔
119
                                writeLineNoSection(writer);
1✔
120
                                writeHitsSection(writer);
1✔
121
                                writeSourceSection(writer);
1✔
122
                                writeTableSuffix(writer);
1✔
123

124
                                writeSuffix(writer);
1✔
125
                        }
126
                } catch (IOException ex) {
×
NEW
127
                        errorWith(COVERAGE_LOG)
×
NEW
128
                                        .setMessage(ex.getMessage())
×
NEW
129
                                        .setCause(ex)
×
NEW
130
                                        .log();
×
131
                }
1✔
132
                updated = false;
1✔
133
        }
1✔
134

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

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

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

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

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

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

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

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

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

227
                var size = branch.branchSize();
1✔
228
                var covered = branch.coveredSize();
1✔
229

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

238
        private int appendPassed(final BufferedWriter writer, final int start, final Range pass) throws IOException {
239

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

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

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

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

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

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

291
                return ret;
1✔
292
        }
293

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

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

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

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

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

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

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

399
        int getLineValidSize() {
400
                return lineRanges.size();
1✔
401
        }
402

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

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

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

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

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