pmdtester 1.6.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +12 -0
  3. data/.github/workflows/build.yml +7 -7
  4. data/.github/workflows/manual-integration-tests.yml +7 -7
  5. data/.github/workflows/publish-release.yml +9 -9
  6. data/.hoerc +1 -1
  7. data/.rubocop_todo.yml +1 -14
  8. data/.vscode/launch.json +32 -0
  9. data/History.md +47 -0
  10. data/Manifest.txt +13 -0
  11. data/README.rdoc +76 -30
  12. data/Rakefile +11 -11
  13. data/config/custom.jfc +1126 -0
  14. data/config/project-list-with-cpd.xml +268 -0
  15. data/config/projectlist_1_2_0.xsd +1 -1
  16. data/config/projectlist_1_3_0.xsd +53 -0
  17. data/lib/pmdtester/builders/cpd_project_hasher.rb +70 -0
  18. data/lib/pmdtester/builders/liquid_renderer.rb +111 -16
  19. data/lib/pmdtester/builders/pmd_report_builder.rb +133 -37
  20. data/lib/pmdtester/builders/project_hasher.rb +24 -25
  21. data/lib/pmdtester/builders/rule_set_builder.rb +2 -0
  22. data/lib/pmdtester/builders/summary_report_builder.rb +6 -1
  23. data/lib/pmdtester/cmd.rb +16 -7
  24. data/lib/pmdtester/cpd_report_diff.rb +99 -0
  25. data/lib/pmdtester/jfr_summary.rb +119 -0
  26. data/lib/pmdtester/location.rb +38 -0
  27. data/lib/pmdtester/parsers/cpd_report_document.rb +241 -0
  28. data/lib/pmdtester/parsers/options.rb +19 -0
  29. data/lib/pmdtester/parsers/pmd_report_document.rb +14 -1
  30. data/lib/pmdtester/parsers/projects_parser.rb +1 -1
  31. data/lib/pmdtester/pmd_branch_detail.rb +29 -9
  32. data/lib/pmdtester/pmd_report_detail.rb +54 -13
  33. data/lib/pmdtester/pmd_tester_utils.rb +45 -17
  34. data/lib/pmdtester/pmd_violation.rb +15 -6
  35. data/lib/pmdtester/project.rb +63 -3
  36. data/lib/pmdtester/report_diff.rb +5 -13
  37. data/lib/pmdtester/runner.rb +185 -37
  38. data/lib/pmdtester/system_info.rb +58 -0
  39. data/lib/pmdtester/word_differ.rb +132 -0
  40. data/lib/pmdtester.rb +8 -1
  41. data/pmdtester.gemspec +17 -17
  42. data/resources/css/pmd-tester.css +15 -0
  43. data/resources/js/project-report.js +293 -112
  44. data/resources/project_cpd_report.html +144 -0
  45. data/resources/project_diff_report.html +151 -18
  46. data/resources/project_index.html +12 -3
  47. data/resources/project_pmd_report.html +17 -2
  48. metadata +63 -43
@@ -1,32 +1,18 @@
1
1
  /*
2
- This is what's included in project_diff_report.html
3
- to make the violation table work.
2
+ This is what's included in project_diff_report.html, project_pmd_report.html and project_cpd_report.html
3
+ to make the violation table and duplication table work.
4
4
 
5
- It depends on the `project` global var, which is generated
5
+ It depends on the `pmd_report` and `cpd_report` global vars, which is generated
6
6
  in another JS file by LiquidProjectRenderer
7
7
  */
8
8
 
9
9
  $(document).ready(function () {
10
10
 
11
- function makeCodeLink(violation) {
12
- let template = project.source_link_template
13
- template = template.replace('{file}', project.file_index[violation.f])
14
- template = template.replace('{line}', violation.l);
15
- return template
16
- }
17
-
18
11
  function extractFilename(path) {
19
12
  const pathArray = path.split("/");
20
13
  return pathArray[pathArray.length - 1];
21
14
  }
22
15
 
23
- function renderCodeSnippet(violation) {
24
- var node = document.createElement('p');
25
- var url = project.source_link_base + '/' + project.file_index[violation.f];
26
- window.pmd_code_snippets.fetch(document, node, url, violation.l, makeCodeLink(violation));
27
- return node;
28
- }
29
-
30
16
  const cssClass = {
31
17
  "+": "added",
32
18
  "-": "removed",
@@ -39,109 +25,304 @@ $(document).ready(function () {
39
25
  "~": "Changed",
40
26
  }
41
27
 
42
- var table = $('#violationsTable').DataTable({
43
- data: project.violations,
44
- columns: [
45
- // other attributes in data:
46
- // l: line
47
- // ol: old line
48
- {"data": "f"}, // file
49
- {"data": "r"}, // rule
50
- {"data": "m"}, // message
51
- {"data": "t"}, // type
52
- ],
53
- deferRender: true,
54
- // scrollY: "6000px",
55
- dom: 'Pfrtipl', // Search Panes, filtering input, processing display element, table, table information summary, pagination control, length changing input control
56
- searchPanes: {
57
- viewTotal: true,
58
- cascadePanes: true,
59
- columns: [0, 1, 3],
60
- order: ['Rule', 'Location (click row to expand)', 'Type'],
61
- threshold: 1 // always show filters in search pane (default: 0.6)
62
- },
63
- // scrollCollapse: true,
64
- // paging: false,
65
- columnDefs: [
66
- { //file column
67
- render(data, type, row) {
68
- data = project.file_index[data]
69
- // display only the file name (not full path), but use full
70
- // path for sorting and such
71
- if (type === "display") {
72
- let line = 'ol' in row ? row.ol + " -> " + row.l : row.l;
73
- //note : target='_blank' requires that the link open in a new tab
74
- return "<a href='" + makeCodeLink(row) + "' target='_blank' rel='noopener noreferrer'>" + extractFilename(data) + " @ line " + line + "</a>"
75
- } else if (type === "sort") {
76
- return data + "#" + row.line
77
- } else if (type === 'shortFile') {
78
- return extractFilename(data)
79
- } else {
80
- return data;
81
- }
82
- },
83
- searchPanes :{
84
- orthogonal: {
85
- 'display': 'shortFile',
86
- 'search': undefined
87
- }
88
- },
89
- targets: 0
28
+ function renderViolationsTable() {
29
+ const COLUMN_LINE = 0;
30
+ const COLUMN_LOCATION = 1;
31
+ const COLUMN_TYPE = 2;
32
+ const COLUMN_FILE = 3; // reference to file name in pmd_report.file_index
33
+ const COLUMN_RULE = 4; // reference to rule name in pmd_report.rule_index
34
+ const COLUMN_MESSAGE = 5;
35
+ const COLUMN_OLD_LOCATION = 6;
36
+
37
+ function makeCodeLink(violation) {
38
+ let template = pmd_report.source_link_template;
39
+ template = template.replace('{file}', pmd_report.file_index[violation[COLUMN_FILE]]);
40
+ template = template.replace('{line}', violation[COLUMN_LINE]);
41
+ return template;
42
+ }
43
+ function renderCodeSnippet(violation) {
44
+ var node = document.createElement('p');
45
+ var url = pmd_report.source_link_base + '/' + pmd_report.file_index[violation[COLUMN_FILE]];
46
+ window.pmd_code_snippets.fetch(document, node, url, violation[COLUMN_LINE], makeCodeLink(violation));
47
+ return node;
48
+ }
49
+
50
+ var table = $('#violationsTable').DataTable({
51
+ data: pmd_report.violations,
52
+ columns: [
53
+ {"data": COLUMN_FILE},
54
+ {"data": COLUMN_RULE},
55
+ {"data": COLUMN_MESSAGE},
56
+ {"data": COLUMN_TYPE},
57
+ ],
58
+ deferRender: true,
59
+ // scrollY: "6000px",
60
+ dom: 'Pfrtipl', // Search Panes, filtering input, processing display element, table, table information summary, pagination control, length changing input control
61
+ searchPanes: {
62
+ viewTotal: true,
63
+ cascadePanes: true,
64
+ columns: [0, 1, 3],
65
+ order: ['Rule', 'Location (click row to expand)', 'Type'],
66
+ threshold: 1 // always show filters in search pane (default: 0.6)
90
67
  },
91
- { // rule column
92
- render(data, type, row) {
93
- // display only the file name (not full path), but use full
94
- // path for sorting and such
95
- if (type === "display")
96
- return "<a href='#rule-summary-" + data + "'>" + data + "</a>"
97
- else
98
- return data;
68
+ // scrollCollapse: true,
69
+ // paging: false,
70
+ columnDefs: [
71
+ { //file column
72
+ render(data, type, row) {
73
+ let filepath = pmd_report.file_index[data];
74
+ // display only the file name (not full path), but use full
75
+ // path for sorting and such
76
+ if (type === "display") {
77
+ let line = row[COLUMN_LOCATION];
78
+ if (row[COLUMN_OLD_LOCATION] !== undefined) {
79
+ line = row[COLUMN_OLD_LOCATION] + "→" + line;
80
+ }
81
+ //note : target='_blank' requires that the link open in a new tab
82
+ return "<a href='" + makeCodeLink(row) + "' target='_blank' rel='noopener noreferrer'>" + extractFilename(filepath) + " @ line " + line + "</a>";
83
+ } else if (type === "sort") {
84
+ return filepath + "#" + row[COLUMN_LINE];
85
+ } else if (type === 'shortFile') {
86
+ return extractFilename(filepath);
87
+ } else {
88
+ return filepath;
89
+ }
90
+ },
91
+ searchPanes :{
92
+ orthogonal: {
93
+ 'display': 'shortFile',
94
+ 'search': undefined
95
+ }
96
+ },
97
+ targets: 0
99
98
  },
100
- searchPanes: {
101
- orthogonal: {
102
- 'display' : 'sort' // do not use the display, which is an <a>
103
- }
99
+ { // rule column
100
+ render(data, type, row) {
101
+ let rulename = pmd_report.rule_index[data];
102
+ if (type === "display")
103
+ return "<a href='#rule-summary-" + rulename + "'>" + rulename + "</a>"
104
+ else
105
+ return rulename;
106
+ },
107
+ searchPanes: {
108
+ orthogonal: {
109
+ 'display' : 'sort' // do not use the display, which is an <a>
110
+ }
111
+ },
112
+ targets: 1
104
113
  },
105
- targets: 1
106
- },
107
- { // type column
108
- visible: false,
109
- render(data, type, row) {
110
- return type ==='display' ? typeDisplay[data] : cssClass[data]
114
+ { // type column
115
+ visible: false,
116
+ render(data, type, row) {
117
+ return type ==='display' ? typeDisplay[data] : cssClass[data]
118
+ },
119
+ targets: 3
111
120
  },
112
- targets: 3
121
+ ],
122
+ displayLength: 25,
123
+ lengthMenu: [ [10, 20, 25, 50, 100, -1], [10, 20, 25, 50, 100, "All"] ],
124
+ rowCallback(row, data, index) {
125
+ $(row).addClass(cssClass[data[COLUMN_TYPE]]);
113
126
  },
114
- ],
115
- displayLength: 25,
116
- lengthMenu: [ [10, 20, 25, 50, 100, -1], [10, 20, 25, 50, 100, "All"] ],
117
- rowCallback(row, data, index) {
118
- $(row).addClass(cssClass[data.t]);
119
- },
120
- });
121
-
122
- $('#violationsTable tbody').on('click', 'tr', function() {
123
- // the event handler might be executed on "tr" elements in sub tables, such as
124
- // code snippets or it might be a child-row (a tr, whose previous sibling has class dt-hasChild)
125
- if (!this.parentElement.parentElement.id === 'violationsTable') {
126
- return;
127
- }
128
- if (this.previousElementSibling !== null && this.previousElementSibling.classList.contains('dt-hasChild')) {
129
- return;
130
- }
127
+ });
128
+
129
+ $('#violationsTable tbody').on('click', 'tr', function() {
130
+ // the event handler might be executed on "tr" elements in sub tables, such as
131
+ // code snippets or it might be a child-row (a tr, whose previous sibling has class dt-hasChild)
132
+ if (this.parentElement.parentElement.id !== 'violationsTable') {
133
+ return;
134
+ }
135
+ if (this.previousElementSibling !== null && this.previousElementSibling.classList.contains('dt-hasChild')) {
136
+ return;
137
+ }
138
+
139
+ var tr = $(this);
140
+ var row = table.row( tr );
141
+
142
+ if ( row.child.isShown() ) {
143
+ // This row is already open - close it
144
+ row.child.hide();
145
+ tr.removeClass('shown');
146
+ }
147
+ else {
148
+ // Open this row
149
+ row.child( renderCodeSnippet(row.data()) ).show();
150
+ tr.addClass('shown');
151
+ }
152
+ });
153
+ }
131
154
 
132
- var tr = $(this);
133
- var row = table.row( tr );
155
+ function renderDuplicationTable() {
156
+ const COLUMN_LOCATION = 0; // location array
157
+ const COLUMN_LOCATION_FILE = 0; // in the location array: reference to file name in cpd_report.file_index
158
+ const COLUMN_LOCATION_BEGIN_LINE = 1;
159
+ const COLUMN_LOCATION_END_LINE = 2;
160
+ const COLUMN_LOCATION_STRING = 3;
161
+ const COLUMN_LINES = 1;
162
+ const COLUMN_TOKENS = 2;
163
+ const COLUMN_CODEFRAGMENT = 3; // codefragment
164
+ const COLUMN_TYPE = 4; // type (+, -, ~)
165
+ const COLUMN_OLD_LINES = 5; // only for changed duplications
166
+ const COLUMN_OLD_TOKENS = 6; // only for changed duplications
167
+ const COLUMN_OLD_LOCATION = 7; // [array] only for changed duplications
134
168
 
135
- if ( row.child.isShown() ) {
136
- // This row is already open - close it
137
- row.child.hide();
138
- tr.removeClass('shown');
169
+ function makeCodeLinkDuplication(firstDuplication) {
170
+ let template = cpd_report.source_link_template;
171
+ template = template.replace('{file}', cpd_report.file_index[firstDuplication[COLUMN_LOCATION_FILE]]);
172
+ template = template.replace('{line}', `${firstDuplication[COLUMN_LOCATION_BEGIN_LINE]}-L${firstDuplication[COLUMN_LOCATION_END_LINE]}`);
173
+ return template;
139
174
  }
140
- else {
141
- // Open this row
142
- row.child( renderCodeSnippet(row.data()) ).show();
143
- tr.addClass('shown');
175
+ function renderCodeFragment(data) {
176
+ var node = document.createElement('p');
177
+ var innerHTML = `${data[COLUMN_LOCATION].length} locations:<br>`;
178
+ data[COLUMN_LOCATION].forEach(location => {
179
+ let url = makeCodeLinkDuplication(location);
180
+ innerHTML += `<a href="${url}" target="_blank" rel="noopener noreferrer">${url} @ line ${location[COLUMN_LOCATION_STRING]}</a><br>`;
181
+ });
182
+
183
+ if (data[COLUMN_OLD_LOCATION] !== undefined) {
184
+ innerHTML += `changed from ${data[COLUMN_OLD_LOCATION].length} previous locations:<br>`;
185
+ data[COLUMN_OLD_LOCATION].forEach(location => {
186
+ let url = makeCodeLinkDuplication(location);
187
+ innerHTML += `<a href="${url}" target="_blank" rel="noopener noreferrer">${url} @ line ${location[COLUMN_LOCATION_STRING]}</a><br>`;
188
+ });
189
+ }
190
+ if (data[COLUMN_OLD_LINES] !== undefined && data[COLUMN_OLD_TOKENS] !== undefined) {
191
+ innerHTML += `code fragment (lines: ${data[COLUMN_OLD_LINES]} → ${data[COLUMN_LINES]}, tokens: ${data[COLUMN_OLD_TOKENS]} → ${data[COLUMN_TOKENS]}):<br>`;
192
+ } else {
193
+ innerHTML += `code fragment (lines: ${data[COLUMN_LINES]}, tokens: ${data[COLUMN_TOKENS]}):<br>`;
194
+ }
195
+
196
+ let table = document.createElement('table');
197
+ table.classList.add('code-snippet');
198
+ let tableBody = document.createElement('tbody');
199
+ table.appendChild(tableBody);
200
+ // line number of first location
201
+ let lineNumber = data[COLUMN_LOCATION][0][COLUMN_LOCATION_BEGIN_LINE];
202
+ data[COLUMN_CODEFRAGMENT].split('\n').forEach(line => {
203
+ let tableRow = document.createElement('tr');
204
+ let lineNumberColumn = document.createElement('td');
205
+ lineNumberColumn.classList.add('line-number');
206
+ tableRow.appendChild(lineNumberColumn);
207
+ let lineNumberElement = document.createElement('code');
208
+ lineNumberColumn.appendChild(lineNumberElement);
209
+ lineNumberElement.setAttribute('data-line-number', lineNumber);
210
+
211
+ let codeColumn = document.createElement('td');
212
+ tableRow.appendChild(codeColumn);
213
+ let codeElement = document.createElement("code");
214
+ codeColumn.appendChild(codeElement);
215
+ // createTextNode escapes special chars
216
+ codeElement.appendChild(document.createTextNode(line));
217
+
218
+ tableBody.appendChild(tableRow); // append row to the table
219
+ lineNumber++; // increment line number for each line
220
+ });
221
+
222
+ node.innerHTML = innerHTML + table.outerHTML;
223
+ return node;
144
224
  }
145
- });
225
+
226
+ var cpdTable = $('#duplicationsTable').DataTable({
227
+ data: cpd_report.duplications,
228
+ columns: [
229
+ {"data": COLUMN_LOCATION},
230
+ {"data": COLUMN_LINES},
231
+ {"data": COLUMN_TOKENS},
232
+ {"data": COLUMN_CODEFRAGMENT},
233
+ {"data": COLUMN_TYPE},
234
+ ],
235
+ deferRender: true,
236
+ dom: 'Pfrtipl', // Search Panes, filtering input, processing display element, table, table information summary, pagination control, length changing input control
237
+ searchPanes: {
238
+ viewTotal: true,
239
+ cascadePanes: true,
240
+ columns: [0, 4],
241
+ order: ['Location (click row to expand)', 'Type'],
242
+ threshold: 1 // always show filters in search pane (default: 0.6)
243
+ },
244
+ columnDefs: [
245
+ { // locations column
246
+ render(data, type, row) {
247
+ let first = data[0];
248
+ let firstPath = cpd_report.file_index[first[COLUMN_LOCATION_FILE]];
249
+ let firstLocation = first[COLUMN_LOCATION_STRING];
250
+ let firstFilename = extractFilename(firstPath);
251
+ if (type === "display") {
252
+ return "<a href='" + makeCodeLinkDuplication(first) + "' target='_blank' rel='noopener noreferrer'>" + firstFilename + " @ line " + firstLocation + "</a>" + (data.length > 1 ? " (and " + (data.length - 1) + " more)" : "");
253
+ } else if (type === "sort") {
254
+ return firstPath + "#" + first[COLUMN_LOCATION_END_LINE];
255
+ } else if (type === 'filter') {
256
+ return firstFilename;
257
+ } else if (type === 'shortFile') {
258
+ return firstFilename;
259
+ } else {
260
+ return data;
261
+ }
262
+ },
263
+ searchPanes :{
264
+ orthogonal: {
265
+ 'display': 'shortFile',
266
+ 'search': undefined
267
+ }
268
+ },
269
+ targets: 0
270
+ },
271
+ { // codefragment column
272
+ render(data, type, row) {
273
+ if (type === "display") {
274
+ data = data.replace(/\r\n|\n/g, "\\n"); // replace newlines with \n for better display in the table
275
+ return data.length > 50 ? data.substring(0, 50) + "..." : data;
276
+ }
277
+ return data;
278
+ },
279
+ targets: 3
280
+ },
281
+ { // type column
282
+ visible: false,
283
+ render(data, type, row) {
284
+ return type ==='display' ? typeDisplay[data] : cssClass[data]
285
+ },
286
+ targets: 4
287
+ },
288
+ ],
289
+ displayLength: 25,
290
+ lengthMenu: [ [10, 20, 25, 50, 100, -1], [10, 20, 25, 50, 100, "All"] ],
291
+ rowCallback(row, data, index) {
292
+ $(row).addClass(cssClass[data[4]]);
293
+ },
294
+ });
295
+ $('#duplicationsTable tbody').on('click', 'tr', function() {
296
+ // the event handler might be executed on "tr" elements in sub tables, such as
297
+ // a child-row (a tr, whose previous sibling has class dt-hasChild)
298
+ if (this.parentElement.parentElement.id !== 'duplicationsTable') {
299
+ return;
300
+ }
301
+ if (this.previousElementSibling !== null && this.previousElementSibling.classList.contains('dt-hasChild')) {
302
+ return;
303
+ }
304
+
305
+ var tr = $(this);
306
+ var row = cpdTable.row( tr );
307
+
308
+ if ( row.child.isShown() ) {
309
+ // This row is already open - close it
310
+ row.child.hide();
311
+ tr.removeClass('shown');
312
+ }
313
+ else {
314
+ // Open this row
315
+ row.child( renderCodeFragment(row.data()) ).show();
316
+ tr.addClass('shown');
317
+ }
318
+ });
319
+ }
320
+
321
+ if ($('#violationsTable').length > 0) {
322
+ renderViolationsTable();
323
+ }
324
+ if ($('#duplicationsTable').length > 0) {
325
+ renderDuplicationTable();
326
+ }
146
327
 
147
328
  });
@@ -0,0 +1,144 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>CPD Report for {{project_name}}</title>
6
+
7
+ <link rel="stylesheet" type="text/css" href="../css/bootstrap-5.3.0-alpha3.min.css"/>
8
+ <link rel="stylesheet" type="text/css" href="../css/datatables.min.css"/>
9
+ <link rel="stylesheet" type="text/css" href="../css/pmd-tester.css">
10
+
11
+ <script src="../js/jquery-3.6.4.slim.min.js"></script>
12
+ <script src="../js/popper-2.11.7.min.js"></script>
13
+ <script src="../js/bootstrap-5.3.0-alpha3.min.js"></script>
14
+ <script src="../js/datatables.min.js"></script>
15
+ <script src="../js/code-snippets.js"></script>
16
+ <!-- This is generated -->
17
+ <script src="./{{branch}}_cpd_data.js"></script>
18
+
19
+ </head>
20
+ <body>
21
+ <div class="section">
22
+ <h1>CPD Report for {{project_name}}</h1>
23
+ </div>
24
+ <div class="section">
25
+ <h2>Summary</h2>
26
+ <div class="section-content">
27
+ <table id="table-summary" class="table">
28
+ <thead>
29
+ <tr>
30
+ <th></th>
31
+ <th>{{branch | capitalize}}</th>
32
+ </tr>
33
+ </thead>
34
+ <tbody>
35
+ <tr>
36
+ <td class="item"><a href="#section-duplications">Duplications</a></td>
37
+ <td class="{{branch}}">{{cpd_report.duplication_counts}}</td>
38
+ </tr>
39
+ <tr>
40
+ <td class="item"><a href="#section-cpd-errors">CPD Errors</a></td>
41
+ <td class="{{branch}}">{{cpd_report.error_counts}}</td>
42
+ </tr>
43
+ <tr>
44
+ <td class="item">Execution time</td>
45
+ <td class="{{branch}}">{{cpd_report.execution_time}}</td>
46
+ </tr>
47
+ <tr>
48
+ <td class="item">Timestamp</td>
49
+ <td class="{{branch}}">{{cpd_report.timestamp}}</td>
50
+ </tr>
51
+ <tr>
52
+ <td class="item">Exit Code</td>
53
+ <td class="{{branch}}">{{cpd_report.exit_code}} <a href="{{branch}}_cpd_stdout.txt">stdout</a> | <a href="{{branch}}_cpd_stderr.txt">stderr</a></td>
54
+ </tr>
55
+ <tr>
56
+ <td class="item">Full Report</td>
57
+ <td class="{{branch}}"><a href="{{branch}}_cpd_report.xml">{{branch}}_cpd_report.xml</a></td>
58
+ </tr>
59
+ <tr>
60
+ <td class="item">JFR Recording</td>
61
+ <td class="{{branch}}"><a href="{{branch}}_cpd_recording.jfr">{{branch}}_cpd_recording.jfr</a></td>
62
+ </tr>
63
+ <tr>
64
+ <td class="item">JFR Details</td>
65
+ <td class="{{branch}}">
66
+ <details><summary>Details</summary>
67
+ Execution Time: {{cpd_report.jfr_summary.execution_time}}<br>
68
+ Max Heap Memory: {{cpd_report.jfr_summary.max_heap_memory}}<br>
69
+ Max CPU Load: {{cpd_report.jfr_summary.max_cpu_load}}<br>
70
+ Avg CPU Load: {{cpd_report.jfr_summary.avg_cpu_load}}<br>
71
+ </details>
72
+ </td>
73
+ </tr>
74
+ </tbody>
75
+ </table>
76
+ </div>
77
+ </div>
78
+
79
+
80
+ <div class="section" id="section-duplications">
81
+
82
+ <h2>Duplications</h2>
83
+
84
+ <div class="section-content">
85
+
86
+ <table id="duplicationsTable" width="100%" class="table">
87
+ <thead>
88
+ <tr>
89
+ <th>Location (click row to expand)</th>
90
+ <th>Lines</th>
91
+ <th>Tokens</th>
92
+ <th>Code Fragment</th>
93
+ <th>Type</th>
94
+ </tr>
95
+ </thead>
96
+ </table>
97
+
98
+ </div>
99
+
100
+ </div>
101
+
102
+ <div class="section" id="section-cpd-errors">
103
+
104
+ <h2>CPD Errors</h2>
105
+
106
+ <div class="section-content">
107
+
108
+ <div class="table-responsive">
109
+ <table id="cpd-error-table" class="table">
110
+ <thead>
111
+ <tr>
112
+ <th>File</th>
113
+ <th>Description (click to expand)</th>
114
+ </tr>
115
+ </thead>
116
+ <tbody>
117
+ {% for error in cpd_report.errors %}
118
+ <tr id="cpd-error-{{forloop.index}}" data-bs-toggle="collapse" data-bs-target="#cpd-error-{{forloop.index}}-expanded">
119
+ <td><a href="{{error.file_url}}" target="_blank" rel="noopener noreferrer">{{error.short_filename}}</a></td>
120
+ <td>{{error.short_message | escape | replace: error.filename, "<span class='meta-var'>$FILE</span>" }}</td>
121
+ </tr>
122
+ <tr>
123
+ <td class="row-hidden" colspan="2">
124
+ <div class="accordion-body collapse" id="cpd-error-{{forloop.index}}-expanded">
125
+ <div class="collapsed-content-padder">
126
+ <pre>
127
+ {{ error.stack_trace_html }}
128
+ </pre>
129
+ </div>
130
+ </div>
131
+ </td>
132
+ </tr>
133
+ {% endfor %}
134
+ </tbody>
135
+ </table>
136
+ </div>
137
+
138
+ </div>
139
+ </div>
140
+
141
+ <script src="../js/project-report.js"></script>
142
+
143
+ </body>
144
+ </html>