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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +12 -0
- data/.github/workflows/build.yml +7 -7
- data/.github/workflows/manual-integration-tests.yml +7 -7
- data/.github/workflows/publish-release.yml +9 -9
- data/.hoerc +1 -1
- data/.rubocop_todo.yml +1 -14
- data/.vscode/launch.json +32 -0
- data/History.md +47 -0
- data/Manifest.txt +13 -0
- data/README.rdoc +76 -30
- data/Rakefile +11 -11
- data/config/custom.jfc +1126 -0
- data/config/project-list-with-cpd.xml +268 -0
- data/config/projectlist_1_2_0.xsd +1 -1
- data/config/projectlist_1_3_0.xsd +53 -0
- data/lib/pmdtester/builders/cpd_project_hasher.rb +70 -0
- data/lib/pmdtester/builders/liquid_renderer.rb +111 -16
- data/lib/pmdtester/builders/pmd_report_builder.rb +133 -37
- data/lib/pmdtester/builders/project_hasher.rb +24 -25
- data/lib/pmdtester/builders/rule_set_builder.rb +2 -0
- data/lib/pmdtester/builders/summary_report_builder.rb +6 -1
- data/lib/pmdtester/cmd.rb +16 -7
- data/lib/pmdtester/cpd_report_diff.rb +99 -0
- data/lib/pmdtester/jfr_summary.rb +119 -0
- data/lib/pmdtester/location.rb +38 -0
- data/lib/pmdtester/parsers/cpd_report_document.rb +241 -0
- data/lib/pmdtester/parsers/options.rb +19 -0
- data/lib/pmdtester/parsers/pmd_report_document.rb +14 -1
- data/lib/pmdtester/parsers/projects_parser.rb +1 -1
- data/lib/pmdtester/pmd_branch_detail.rb +29 -9
- data/lib/pmdtester/pmd_report_detail.rb +54 -13
- data/lib/pmdtester/pmd_tester_utils.rb +45 -17
- data/lib/pmdtester/pmd_violation.rb +15 -6
- data/lib/pmdtester/project.rb +63 -3
- data/lib/pmdtester/report_diff.rb +5 -13
- data/lib/pmdtester/runner.rb +185 -37
- data/lib/pmdtester/system_info.rb +58 -0
- data/lib/pmdtester/word_differ.rb +132 -0
- data/lib/pmdtester.rb +8 -1
- data/pmdtester.gemspec +17 -17
- data/resources/css/pmd-tester.css +15 -0
- data/resources/js/project-report.js +293 -112
- data/resources/project_cpd_report.html +144 -0
- data/resources/project_diff_report.html +151 -18
- data/resources/project_index.html +12 -3
- data/resources/project_pmd_report.html +17 -2
- metadata +63 -43
|
@@ -20,9 +20,17 @@ module PmdTester
|
|
|
20
20
|
|
|
21
21
|
@pmd_branch_details = PmdBranchDetail.new(@pmd_branch_name)
|
|
22
22
|
@project_builder = ProjectBuilder.new(@projects)
|
|
23
|
+
@run_pmd = options.run_pmd
|
|
24
|
+
@run_cpd = options.run_cpd
|
|
23
25
|
end
|
|
24
26
|
|
|
25
|
-
def
|
|
27
|
+
def with_changes(rules_changed, impl_changed)
|
|
28
|
+
@run_pmd &&= rules_changed
|
|
29
|
+
@run_cpd &&= impl_changed
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def create_pmd_package
|
|
26
34
|
logger.info "#{@pmd_branch_name}: Start packaging PMD"
|
|
27
35
|
Dir.chdir(@local_git_repo) do
|
|
28
36
|
checkout_build_branch # needs a clean working tree, otherwise fails
|
|
@@ -32,7 +40,7 @@ module PmdTester
|
|
|
32
40
|
# for local branches.
|
|
33
41
|
build_branch_sha = Cmd.execute_successfully("git rev-parse #{@pmd_branch_name}^{commit}")
|
|
34
42
|
|
|
35
|
-
raise "Wrong branch #{
|
|
43
|
+
raise "Wrong branch #{determine_last_commit_sha}" unless build_branch_sha == determine_last_commit_sha
|
|
36
44
|
|
|
37
45
|
distro_path = saved_distro_path(build_branch_sha)
|
|
38
46
|
logger.debug "#{@pmd_branch_name}: PMD Version is #{@pmd_version} " \
|
|
@@ -47,7 +55,7 @@ module PmdTester
|
|
|
47
55
|
|
|
48
56
|
# we're still on the build branch
|
|
49
57
|
@pmd_branch_details.branch_last_sha = build_branch_sha
|
|
50
|
-
@pmd_branch_details.branch_last_message =
|
|
58
|
+
@pmd_branch_details.branch_last_message = determine_last_commit_message
|
|
51
59
|
end
|
|
52
60
|
logger.info "#{@pmd_branch_name}: Packaging PMD completed"
|
|
53
61
|
end
|
|
@@ -82,38 +90,31 @@ module PmdTester
|
|
|
82
90
|
Cmd.execute_successfully(version_cmd)
|
|
83
91
|
end
|
|
84
92
|
|
|
85
|
-
def
|
|
86
|
-
|
|
87
|
-
Cmd.execute_successfully(
|
|
93
|
+
def determine_last_commit_sha
|
|
94
|
+
last_commit_sha_cmd = 'git rev-parse HEAD^{commit}'
|
|
95
|
+
Cmd.execute_successfully(last_commit_sha_cmd)
|
|
88
96
|
end
|
|
89
97
|
|
|
90
|
-
def
|
|
91
|
-
|
|
92
|
-
Cmd.execute_successfully(
|
|
98
|
+
def determine_last_commit_message
|
|
99
|
+
last_commit_message_cmd = 'git log -1 --pretty=%B'
|
|
100
|
+
Cmd.execute_successfully(last_commit_message_cmd)
|
|
93
101
|
end
|
|
94
102
|
|
|
95
103
|
def generate_pmd_report(project)
|
|
96
|
-
error_recovery_options = @error_recovery ? 'PMD_JAVA_OPTS="-Dpmd.error_recovery -ea" ' : ''
|
|
97
|
-
fail_on_violation = create_failonviolation_option
|
|
98
|
-
auxclasspath_option = create_auxclasspath_option(project)
|
|
99
|
-
pmd_cmd = "#{error_recovery_options}" \
|
|
100
|
-
"#{determine_run_path} -d #{project.local_source_path} -f xml " \
|
|
101
|
-
"-R #{project.get_config_path(@pmd_branch_name)} " \
|
|
102
|
-
"-r #{project.get_pmd_report_path(@pmd_branch_name)} " \
|
|
103
|
-
"#{fail_on_violation} -t #{@threads} " \
|
|
104
|
-
"#{auxclasspath_option}" \
|
|
105
|
-
"#{' --no-progress' if pmd7?}"
|
|
106
104
|
start_time = Time.now
|
|
107
105
|
exit_code = nil
|
|
106
|
+
stdout = ''
|
|
107
|
+
stderr = ''
|
|
108
108
|
if File.exist?(project.get_pmd_report_path(@pmd_branch_name))
|
|
109
109
|
logger.warn "#{@pmd_branch_name}: Skipping PMD run - report " \
|
|
110
110
|
"#{project.get_pmd_report_path(@pmd_branch_name)} already exists"
|
|
111
111
|
else
|
|
112
|
-
|
|
112
|
+
pmd_cmd = create_pmd_command(project)
|
|
113
|
+
status, stdout, stderr = Cmd.execute(pmd_cmd)
|
|
113
114
|
exit_code = status.exitstatus
|
|
114
115
|
end
|
|
115
116
|
end_time = Time.now
|
|
116
|
-
[end_time - start_time, end_time, exit_code]
|
|
117
|
+
[pmd_cmd, end_time - start_time, end_time, exit_code, stdout, stderr]
|
|
117
118
|
end
|
|
118
119
|
|
|
119
120
|
def generate_config_for(project)
|
|
@@ -140,34 +141,130 @@ module PmdTester
|
|
|
140
141
|
progress_logger = SimpleProgressLogger.new("generating #{project.name}'s PMD report")
|
|
141
142
|
progress_logger.start
|
|
142
143
|
generate_config_for(project)
|
|
143
|
-
execution_time, end_time, exit_code = generate_pmd_report(project)
|
|
144
|
+
cmd_line, execution_time, end_time, exit_code, stdout, stderr = generate_pmd_report(project)
|
|
144
145
|
progress_logger.stop
|
|
145
146
|
sum_time += execution_time
|
|
146
147
|
|
|
148
|
+
jfr_summary = JfrSummary.new.load("#{project.get_project_target_dir(@pmd_branch_name)}/pmd_recording.jfr")
|
|
147
149
|
PmdReportDetail.create(execution_time: execution_time, timestamp: end_time,
|
|
148
|
-
exit_code: exit_code,
|
|
150
|
+
cmdline: cmd_line, exit_code: exit_code, stdout: stdout, stderr: stderr,
|
|
151
|
+
oom: stderr.include?('java.lang.OutOfMemoryError'),
|
|
152
|
+
report_info_path: project.get_report_info_path(@pmd_branch_name),
|
|
153
|
+
jfr_summary: jfr_summary)
|
|
149
154
|
logger.info "#{project.name}'s PMD report was generated successfully (exit code: #{exit_code})"
|
|
150
155
|
end
|
|
151
156
|
|
|
152
|
-
@pmd_branch_details.execution_time
|
|
153
|
-
@pmd_branch_details.save
|
|
157
|
+
@pmd_branch_details.execution_time += sum_time
|
|
154
158
|
FileUtils.cp(@branch_config, @pmd_branch_details.target_branch_config_path)
|
|
155
|
-
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def generate_cpd_reports
|
|
162
|
+
logger.info "Generating CPD report started -- branch #{@pmd_branch_name}"
|
|
163
|
+
|
|
164
|
+
sum_time = 0
|
|
165
|
+
@projects.each do |project|
|
|
166
|
+
progress_logger = SimpleProgressLogger.new("generating #{project.name}'s CPD report")
|
|
167
|
+
progress_logger.start
|
|
168
|
+
cpd_cmd, execution_time, end_time, exit_code, stdout, stderr = generate_cpd_report(project)
|
|
169
|
+
progress_logger.stop
|
|
170
|
+
sum_time += execution_time
|
|
171
|
+
|
|
172
|
+
jfr_summary = JfrSummary.new.load("#{project.get_project_target_dir(@pmd_branch_name)}/cpd_recording.jfr")
|
|
173
|
+
PmdReportDetail.create(execution_time: execution_time, timestamp: end_time,
|
|
174
|
+
cmdline: cpd_cmd, exit_code: exit_code, stdout: stdout, stderr: stderr,
|
|
175
|
+
oom: stderr.include?('java.lang.OutOfMemoryError'),
|
|
176
|
+
report_info_path: project.get_cpd_report_info_path(@pmd_branch_name),
|
|
177
|
+
jfr_summary: jfr_summary)
|
|
178
|
+
logger.info "#{project.name}'s CPD report was generated successfully (exit code: #{exit_code})"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
@pmd_branch_details.execution_time += sum_time
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def generate_cpd_report(project)
|
|
185
|
+
start_time = Time.now
|
|
186
|
+
exit_code = nil
|
|
187
|
+
if File.exist?(project.get_cpd_report_path(@pmd_branch_name))
|
|
188
|
+
logger.warn "#{@pmd_branch_name}: Skipping CPD run - report " \
|
|
189
|
+
"#{project.get_cpd_report_path(@pmd_branch_name)} already exists"
|
|
190
|
+
else
|
|
191
|
+
cpd_cmd = create_cpd_command(project)
|
|
192
|
+
status, stdout, stderr = Cmd.execute(cpd_cmd, debug_log_stdout: false)
|
|
193
|
+
exit_code = status.exitstatus
|
|
194
|
+
end
|
|
195
|
+
stdout = filter_cpd_report_from_stdout(project, exit_code, stdout)
|
|
196
|
+
end_time = Time.now
|
|
197
|
+
[cpd_cmd, end_time - start_time, end_time, exit_code, stdout, stderr]
|
|
156
198
|
end
|
|
157
199
|
|
|
158
200
|
# returns the branch details
|
|
159
201
|
def build
|
|
160
202
|
@project_builder.clone_projects
|
|
161
203
|
@project_builder.build_projects
|
|
162
|
-
|
|
163
|
-
|
|
204
|
+
create_pmd_package
|
|
205
|
+
@pmd_branch_details.execution_time = 0
|
|
206
|
+
generate_pmd_reports if @run_pmd
|
|
207
|
+
generate_cpd_reports if @run_cpd
|
|
208
|
+
@pmd_branch_details.save
|
|
209
|
+
@pmd_branch_details
|
|
164
210
|
end
|
|
165
211
|
|
|
166
212
|
private
|
|
167
213
|
|
|
214
|
+
def create_pmd_command(project)
|
|
215
|
+
error_recovery_options = @error_recovery ? ' -Dpmd.error_recovery -ea' : ''
|
|
216
|
+
java_opts = 'PMD_JAVA_OPTS="-XX:StartFlightRecording:' \
|
|
217
|
+
"filename=#{project.get_project_target_dir(@pmd_branch_name)}/pmd_recording.jfr," \
|
|
218
|
+
"settings=#{ResourceLocator.locate('config/custom.jfc')},dumponexit=true" \
|
|
219
|
+
"#{error_recovery_options}\" "
|
|
220
|
+
fail_on_violation = create_failonviolation_option
|
|
221
|
+
auxclasspath_option = create_auxclasspath_option(project)
|
|
222
|
+
"#{java_opts}" \
|
|
223
|
+
"#{determine_run_path} -d #{project.local_source_path} -f xml " \
|
|
224
|
+
"-R #{project.get_config_path(@pmd_branch_name)} " \
|
|
225
|
+
"-r #{project.get_pmd_report_path(@pmd_branch_name)} " \
|
|
226
|
+
"#{fail_on_violation} -t #{@threads} " \
|
|
227
|
+
"#{auxclasspath_option}" \
|
|
228
|
+
"#{' --no-progress' if pmd7?}"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def create_cpd_command(project)
|
|
232
|
+
error_recovery_options = @error_recovery ? ' -Dpmd.error_recovery -ea' : ''
|
|
233
|
+
java_opts = "PMD_JAVA_OPTS=\"-Xmx#{project.cpd_options.max_memory} -XX:StartFlightRecording:" \
|
|
234
|
+
"filename=#{project.get_project_target_dir(@pmd_branch_name)}/cpd_recording.jfr," \
|
|
235
|
+
"settings=#{ResourceLocator.locate('config/custom.jfc')},dumponexit=true" \
|
|
236
|
+
"#{error_recovery_options}\" "
|
|
237
|
+
"#{java_opts}" \
|
|
238
|
+
"#{determine_run_path(command: 'cpd')} #{get_directories_option(project)} -f xml " \
|
|
239
|
+
"--language #{project.cpd_options.language} --minimum-tokens #{project.cpd_options.minimum_tokens} " \
|
|
240
|
+
'--skip-lexical-errors'
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# NOTE: --report-file is only supported in PMD 7.14.0+. To support 7.0.0, we use stdout.
|
|
244
|
+
def filter_cpd_report_from_stdout(project, exit_code, stdout)
|
|
245
|
+
if [0, 4, 5].include?(exit_code)
|
|
246
|
+
# when running with JFR, there are logs from JFR at the beginning
|
|
247
|
+
# we want to remove those logs from the stdout, because they would break the XML parsing
|
|
248
|
+
xml_start = stdout.index('<?xml')
|
|
249
|
+
return stdout if xml_start.nil?
|
|
250
|
+
|
|
251
|
+
stdout_filtered = stdout[0, xml_start]
|
|
252
|
+
cpd_report = stdout[xml_start, stdout.length - xml_start]
|
|
253
|
+
File.write(project.get_cpd_report_path(@pmd_branch_name), cpd_report)
|
|
254
|
+
return stdout_filtered
|
|
255
|
+
end
|
|
256
|
+
stdout
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def get_directories_option(project)
|
|
260
|
+
project.cpd_options.directories.map do |dir|
|
|
261
|
+
"-d #{Pathname.new("#{project.clone_root_path}/#{dir}").cleanpath}"
|
|
262
|
+
end.join(' ')
|
|
263
|
+
end
|
|
264
|
+
|
|
168
265
|
def checkout_build_branch
|
|
169
266
|
logger.info "#{@pmd_branch_name}: Checking out the branch"
|
|
170
|
-
#
|
|
267
|
+
# the checkout will fail if the tree is dirty
|
|
171
268
|
Cmd.execute_successfully("git checkout #{@pmd_branch_name}")
|
|
172
269
|
|
|
173
270
|
# determine the version
|
|
@@ -225,15 +322,14 @@ module PmdTester
|
|
|
225
322
|
Semver.compare(@pmd_version, '7.0.0-SNAPSHOT') >= 0
|
|
226
323
|
end
|
|
227
324
|
|
|
228
|
-
def determine_run_path
|
|
325
|
+
def determine_run_path(command: 'check')
|
|
229
326
|
run_path = "#{saved_distro_path(@pmd_branch_details.branch_last_sha)}/bin"
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
run_path
|
|
327
|
+
if File.exist?("#{run_path}/pmd")
|
|
328
|
+
# New PMD 7 CLI script (pmd/pmd#4059)
|
|
329
|
+
"#{run_path}/pmd #{command}"
|
|
330
|
+
else
|
|
331
|
+
"#{run_path}/run.sh pmd"
|
|
332
|
+
end
|
|
237
333
|
end
|
|
238
334
|
|
|
239
335
|
def find_pmd_dist_target
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'differ'
|
|
4
|
-
|
|
5
3
|
module PmdTester
|
|
6
4
|
# Turn a project report into a hash that can be rendered somewhere else
|
|
7
5
|
module ProjectHasher
|
|
@@ -13,34 +11,37 @@ module PmdTester
|
|
|
13
11
|
'error_counts' => rdiff.error_counts.to_h.transform_keys(&:to_s),
|
|
14
12
|
'configerror_counts' => rdiff.configerror_counts.to_h.transform_keys(&:to_s),
|
|
15
13
|
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'diff_execution_time' => PmdReportDetail.convert_seconds(rdiff.patch_report.exec_time -
|
|
19
|
-
rdiff.base_report.exec_time),
|
|
20
|
-
|
|
21
|
-
'base_timestamp' => rdiff.base_report.timestamp,
|
|
22
|
-
'patch_timestamp' => rdiff.patch_report.timestamp,
|
|
14
|
+
'base_details' => rdiff.base_report.report_details.to_h,
|
|
15
|
+
'patch_details' => rdiff.patch_report.report_details.to_h,
|
|
23
16
|
|
|
24
|
-
'
|
|
25
|
-
|
|
17
|
+
'diff_execution_time' => PmdReportDetail.convert_seconds(rdiff.patch_report.report_details.execution_time -
|
|
18
|
+
rdiff.base_report.report_details.execution_time),
|
|
26
19
|
|
|
27
20
|
'rule_diffs' => rdiff.rule_summaries
|
|
28
21
|
}
|
|
29
22
|
end
|
|
30
23
|
|
|
31
24
|
def violations_to_hash(project, violations_by_file, is_diff)
|
|
25
|
+
rulename_index = {}
|
|
26
|
+
violations_by_file.each_value do |vs|
|
|
27
|
+
vs.each do |v|
|
|
28
|
+
rulename_index[v.rule_name] = rulename_index.size unless rulename_index.include?(v.rule_name)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
32
31
|
filename_index = []
|
|
33
32
|
all_vs = []
|
|
34
33
|
violations_by_file.each do |file, vs|
|
|
35
34
|
file_ref = filename_index.size
|
|
36
35
|
filename_index.push(project.get_local_path(file))
|
|
37
36
|
vs.each do |v|
|
|
38
|
-
|
|
37
|
+
rule_ref = rulename_index[v.rule_name]
|
|
38
|
+
all_vs.push(make_violation_datable(file_ref, rule_ref, v, is_diff: is_diff))
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
{
|
|
43
43
|
'file_index' => filename_index,
|
|
44
|
+
'rule_index' => rulename_index.keys,
|
|
44
45
|
'violations' => all_vs
|
|
45
46
|
}
|
|
46
47
|
end
|
|
@@ -111,24 +112,22 @@ module PmdTester
|
|
|
111
112
|
end
|
|
112
113
|
end
|
|
113
114
|
|
|
114
|
-
def
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
h
|
|
115
|
+
def make_violation_datable(file_ref, rule_ref, violation, is_diff: true)
|
|
116
|
+
type = is_diff ? violation_type(violation) : '+'
|
|
117
|
+
old_location = []
|
|
118
|
+
if is_diff && violation.changed? && !violation.location.eql?(violation.old_location)
|
|
119
|
+
old_location = [violation.old_location.to_s]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
[violation.line, violation.location.to_s, type, file_ref, rule_ref,
|
|
123
|
+
create_violation_message(violation, is_diff && violation.changed?)] + old_location
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
def create_violation_message(violation, is_diff)
|
|
127
127
|
return escape_html(violation.message) unless is_diff
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
diff.format_as(:html)
|
|
129
|
+
WordDiffer.diff_words(escape_html(violation.old_message),
|
|
130
|
+
escape_html(violation.message))
|
|
132
131
|
end
|
|
133
132
|
|
|
134
133
|
def escape_html(string)
|
|
@@ -23,6 +23,7 @@ module PmdTester
|
|
|
23
23
|
# Returns false, when no rules are affected and regression tester can be skipped.
|
|
24
24
|
#
|
|
25
25
|
def build?
|
|
26
|
+
logger.info 'Autogenerating a dynamic ruleset based on source changes'
|
|
26
27
|
languages = determine_languages
|
|
27
28
|
filenames = diff_filenames(languages)
|
|
28
29
|
all_rules_hash = determine_all_rules
|
|
@@ -34,6 +35,7 @@ module PmdTester
|
|
|
34
35
|
else
|
|
35
36
|
logger.info NO_RULES_CHANGED_MESSAGE
|
|
36
37
|
end
|
|
38
|
+
logger.debug "Rules have changed: #{run_required}"
|
|
37
39
|
run_required
|
|
38
40
|
end
|
|
39
41
|
|
|
@@ -6,6 +6,7 @@ module PmdTester
|
|
|
6
6
|
include PmdTester
|
|
7
7
|
include LiquidRenderer
|
|
8
8
|
include ProjectHasher
|
|
9
|
+
include CpdProjectHasher
|
|
9
10
|
|
|
10
11
|
REPORT_DIR = 'target/reports/diff'
|
|
11
12
|
BASE_CONFIG_NAME = 'base_config.xml'
|
|
@@ -52,7 +53,8 @@ module PmdTester
|
|
|
52
53
|
'name' => p.name,
|
|
53
54
|
'tag' => p.tag,
|
|
54
55
|
'report_url' => "./#{p.name}/index.html",
|
|
55
|
-
|
|
56
|
+
'pmd_report' => report_diff_to_h(p.report_diff),
|
|
57
|
+
'cpd_report' => cpd_report_diff_to_h(p.cpd_report_diff)
|
|
56
58
|
}
|
|
57
59
|
end
|
|
58
60
|
|
|
@@ -83,6 +85,9 @@ module PmdTester
|
|
|
83
85
|
'timestamp' => details.timestamp,
|
|
84
86
|
'execution_time' => PmdReportDetail.convert_seconds(details.execution_time),
|
|
85
87
|
'jdk_info' => details.jdk_version,
|
|
88
|
+
'cpu_info' => details.cpu_info,
|
|
89
|
+
'physical_memory' => details.physical_memory,
|
|
90
|
+
'os_info' => details.os_info,
|
|
86
91
|
'locale' => details.language,
|
|
87
92
|
'config_url' => config_name,
|
|
88
93
|
'pr_number' => details.pull_request
|
data/lib/pmdtester/cmd.rb
CHANGED
|
@@ -7,13 +7,22 @@ module PmdTester
|
|
|
7
7
|
class Cmd
|
|
8
8
|
extend PmdTester
|
|
9
9
|
|
|
10
|
+
#
|
|
11
|
+
# Executes the given command and returns the process status,
|
|
12
|
+
# stdout and stderr.
|
|
13
|
+
#
|
|
14
|
+
def self.execute(cmd, debug_log_stdout: true)
|
|
15
|
+
stdout, stderr, status = internal_execute(cmd, nil, debug_log_stdout)
|
|
16
|
+
[status, stdout, stderr]
|
|
17
|
+
end
|
|
18
|
+
|
|
10
19
|
#
|
|
11
20
|
# Executes the given command and returns the process status.
|
|
12
21
|
# stdout and stderr are written to the files "stdout.txt" and "stderr.txt"
|
|
13
22
|
# in path.
|
|
14
23
|
#
|
|
15
|
-
def self.
|
|
16
|
-
stdout, stderr, status = internal_execute(cmd, nil)
|
|
24
|
+
def self.execute_save_output(cmd, path)
|
|
25
|
+
stdout, stderr, status = internal_execute(cmd, nil, true)
|
|
17
26
|
|
|
18
27
|
file = File.new("#{path}/stdout.txt", 'w')
|
|
19
28
|
file.puts stdout
|
|
@@ -26,8 +35,8 @@ module PmdTester
|
|
|
26
35
|
status
|
|
27
36
|
end
|
|
28
37
|
|
|
29
|
-
def self.execute_successfully(cmd, extra_java_home = nil)
|
|
30
|
-
stdout, stderr, status = internal_execute(cmd, extra_java_home)
|
|
38
|
+
def self.execute_successfully(cmd, extra_java_home = nil, debug_log_stdout: true)
|
|
39
|
+
stdout, stderr, status = internal_execute(cmd, extra_java_home, debug_log_stdout)
|
|
31
40
|
|
|
32
41
|
unless status.success?
|
|
33
42
|
logger.error "Command failed: #{cmd}"
|
|
@@ -40,11 +49,11 @@ module PmdTester
|
|
|
40
49
|
end
|
|
41
50
|
|
|
42
51
|
def self.stderr_of(cmd)
|
|
43
|
-
_stdout, stderr, _status = internal_execute(cmd, nil)
|
|
52
|
+
_stdout, stderr, _status = internal_execute(cmd, nil, true)
|
|
44
53
|
stderr
|
|
45
54
|
end
|
|
46
55
|
|
|
47
|
-
def self.internal_execute(cmd, extra_java_home)
|
|
56
|
+
def self.internal_execute(cmd, extra_java_home, debug_log_stdout)
|
|
48
57
|
logger.debug "execute command '#{cmd}' (extra_java_home: #{extra_java_home})"
|
|
49
58
|
|
|
50
59
|
new_env = ENV.to_h
|
|
@@ -56,7 +65,7 @@ module PmdTester
|
|
|
56
65
|
stdout, stderr, status = Open3.capture3(new_env, "#{cmd};")
|
|
57
66
|
|
|
58
67
|
logger.debug "status: #{status}"
|
|
59
|
-
logger.debug "stdout: #{stdout}"
|
|
68
|
+
logger.debug "stdout: #{stdout}" if debug_log_stdout
|
|
60
69
|
logger.debug "stderr: #{stderr}"
|
|
61
70
|
|
|
62
71
|
stdout&.chomp!
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PmdTester
|
|
4
|
+
# A full CPD report, created by the report XML parser,
|
|
5
|
+
# can be diffed with another report into a CpdReportDiff
|
|
6
|
+
class CpdReport
|
|
7
|
+
attr_reader :duplications, :errors,
|
|
8
|
+
:file,
|
|
9
|
+
:report_details
|
|
10
|
+
|
|
11
|
+
def initialize(report_details:, report_document:, file:)
|
|
12
|
+
initialize_empty
|
|
13
|
+
initialize_with_report_document report_document unless report_document.nil?
|
|
14
|
+
@report_details = report_details
|
|
15
|
+
@file = file
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.empty
|
|
19
|
+
new(report_details: PmdTester::PmdReportDetail.empty, report_document: nil, file: '')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def initialize_with_report_document(report_document)
|
|
25
|
+
@duplications = report_document.duplications
|
|
26
|
+
@errors = report_document.errors
|
|
27
|
+
|
|
28
|
+
PmdTester.logger.debug("Loaded #{@duplications.size} duplications")
|
|
29
|
+
PmdTester.logger.debug("Loaded #{@errors.size} errors")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize_empty
|
|
33
|
+
@duplications = []
|
|
34
|
+
@errors = []
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# This class represents all the diff report information,
|
|
39
|
+
# including the summary information of the original cpd reports,
|
|
40
|
+
# as well as the specific information of the diff report.
|
|
41
|
+
class CpdReportDiff
|
|
42
|
+
include PmdTester
|
|
43
|
+
|
|
44
|
+
attr_reader :duplication_counts, :error_counts, :duplication_diffs, :error_diffs
|
|
45
|
+
attr_accessor :base_report, :patch_report
|
|
46
|
+
|
|
47
|
+
def initialize(base_report:, patch_report:)
|
|
48
|
+
@duplication_counts = RunningDiffCounters.new(base_report.duplications.size)
|
|
49
|
+
@error_counts = RunningDiffCounters.new(base_report.errors.size)
|
|
50
|
+
|
|
51
|
+
@base_report = base_report
|
|
52
|
+
@patch_report = patch_report
|
|
53
|
+
|
|
54
|
+
@duplication_diffs = []
|
|
55
|
+
@error_diffs = []
|
|
56
|
+
|
|
57
|
+
diff_base_with_patch
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def diff_base_with_patch
|
|
63
|
+
@duplication_counts.patch_total = @patch_report.duplications.size
|
|
64
|
+
@error_counts.patch_total = @patch_report.errors.size
|
|
65
|
+
|
|
66
|
+
@duplication_diffs = (@base_report.duplications + @patch_report.duplications) \
|
|
67
|
+
- (@base_report.duplications & @patch_report.duplications)
|
|
68
|
+
merge_changed_items(@duplication_diffs)
|
|
69
|
+
count(@duplication_diffs, @duplication_counts)
|
|
70
|
+
|
|
71
|
+
@error_diffs = (@base_report.errors + @patch_report.errors) \
|
|
72
|
+
- (@base_report.errors & @patch_report.errors)
|
|
73
|
+
merge_changed_items(@error_diffs)
|
|
74
|
+
count(@error_diffs, @error_counts)
|
|
75
|
+
|
|
76
|
+
self
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def count(diffs, counter)
|
|
80
|
+
diffs.each do |item|
|
|
81
|
+
if item.changed?
|
|
82
|
+
counter.changed += 1
|
|
83
|
+
elsif item.branch.eql?(BASE)
|
|
84
|
+
counter.removed += 1
|
|
85
|
+
else
|
|
86
|
+
counter.new += 1
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def merge_changed_items(diffs)
|
|
92
|
+
diffs.delete_if do |item|
|
|
93
|
+
item.branch == BASE &&
|
|
94
|
+
# try_merge will set item2.changed = true if it succeeds
|
|
95
|
+
diffs.any? { |item2| item2.branch == PATCH && item2.try_merge?(item) }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module PmdTester
|
|
6
|
+
# This class reads a jfr recording and extract a summary
|
|
7
|
+
class JfrSummary
|
|
8
|
+
attr_accessor :execution_time, :max_heap_memory, :max_cpu_load, :avg_cpu_load, :recording_path
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@execution_time = 0
|
|
12
|
+
@max_heap_memory = 0
|
|
13
|
+
@max_cpu_load = 0
|
|
14
|
+
@avg_cpu_load = 0
|
|
15
|
+
@recording_path = ''
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def load(jfr_recording)
|
|
19
|
+
@recording_path = jfr_recording
|
|
20
|
+
start_time = get_start_time(jfr_recording)
|
|
21
|
+
end_time = get_end_time(jfr_recording)
|
|
22
|
+
@execution_time = end_time - start_time
|
|
23
|
+
|
|
24
|
+
gc_heap_summary = get_gc_heap_summary(jfr_recording)
|
|
25
|
+
unless gc_heap_summary.empty?
|
|
26
|
+
@max_heap_memory = gc_heap_summary.map do |e|
|
|
27
|
+
e.dig(:values, :heapUsed)
|
|
28
|
+
end.max
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
cpu_load = get_cpu_load(jfr_recording)
|
|
32
|
+
unless cpu_load.empty?
|
|
33
|
+
@max_cpu_load = cpu_load.map { |e| e.dig(:values, :jvmUser) }.max
|
|
34
|
+
@avg_cpu_load = cpu_load.map { |e| e.dig(:values, :jvmUser) }.sum / cpu_load.size
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_h
|
|
41
|
+
{
|
|
42
|
+
execution_time: @execution_time,
|
|
43
|
+
max_heap_memory: @max_heap_memory,
|
|
44
|
+
max_cpu_load: @max_cpu_load,
|
|
45
|
+
avg_cpu_load: @avg_cpu_load,
|
|
46
|
+
recording_path: @recording_path
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_h_for_liquid
|
|
51
|
+
{
|
|
52
|
+
'execution_time' => PmdReportDetail.convert_seconds(@execution_time),
|
|
53
|
+
'max_heap_memory' => format_memory(@max_heap_memory),
|
|
54
|
+
'max_cpu_load' => format_percentage(@max_cpu_load),
|
|
55
|
+
'avg_cpu_load' => format_percentage(@avg_cpu_load)
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.from_h(hash)
|
|
60
|
+
jfr_summary = JfrSummary.new
|
|
61
|
+
jfr_summary.execution_time = hash[:execution_time] || 0
|
|
62
|
+
jfr_summary.max_heap_memory = hash[:max_heap_memory] || 0
|
|
63
|
+
jfr_summary.max_cpu_load = hash[:max_cpu_load] || 0
|
|
64
|
+
jfr_summary.avg_cpu_load = hash[:avg_cpu_load] || 0
|
|
65
|
+
jfr_summary.recording_path = hash[:recording_path] || ''
|
|
66
|
+
jfr_summary
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def get_start_time(jfr_recording)
|
|
72
|
+
stdout = Cmd.execute_successfully("jfr print --json --events jdk.JVMInformation #{jfr_recording}",
|
|
73
|
+
nil, debug_log_stdout: false)
|
|
74
|
+
jvm_info = JSON.parse(stdout, symbolize_names: true).dig(:recording, :events, 0, :values, :jvmStartTime)
|
|
75
|
+
return Time.at(0) if jvm_info.nil?
|
|
76
|
+
|
|
77
|
+
Time.parse(jvm_info)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def get_end_time(jfr_recording)
|
|
81
|
+
stdout = Cmd.execute_successfully("jfr print --json --events jdk.Shutdown #{jfr_recording}",
|
|
82
|
+
nil, debug_log_stdout: false)
|
|
83
|
+
shutdown_info = JSON.parse(stdout, symbolize_names: true).dig(:recording, :events, 0, :values, :startTime)
|
|
84
|
+
return Time.at(0) if shutdown_info.nil?
|
|
85
|
+
|
|
86
|
+
Time.parse(shutdown_info)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def get_gc_heap_summary(jfr_recording)
|
|
90
|
+
stdout = Cmd.execute_successfully("jfr print --json --events jdk.GCHeapSummary #{jfr_recording}",
|
|
91
|
+
nil, debug_log_stdout: false)
|
|
92
|
+
events = JSON.parse(stdout, symbolize_names: true).dig(:recording, :events)
|
|
93
|
+
return [] if events.nil?
|
|
94
|
+
|
|
95
|
+
events
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def get_cpu_load(jfr_recording)
|
|
99
|
+
stdout = Cmd.execute_successfully("jfr print --json --events jdk.CPULoad #{jfr_recording}",
|
|
100
|
+
nil, debug_log_stdout: false)
|
|
101
|
+
events = JSON.parse(stdout, symbolize_names: true).dig(:recording, :events)
|
|
102
|
+
return [] if events.nil?
|
|
103
|
+
|
|
104
|
+
events
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def format_memory(bytes)
|
|
108
|
+
return '0 MB' if bytes.nil? || bytes.zero?
|
|
109
|
+
|
|
110
|
+
"#{(bytes / (1024 * 1024)).round} MB"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def format_percentage(value)
|
|
114
|
+
return '0%' if value.nil? || value.zero?
|
|
115
|
+
|
|
116
|
+
"#{(value * 100).round}%"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|