pmdtester 1.6.1 → 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 +9 -7
- data/.github/workflows/manual-integration-tests.yml +9 -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 +54 -0
- data/Manifest.txt +13 -0
- data/README.rdoc +77 -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 +139 -41
- data/lib/pmdtester/builders/project_hasher.rb +24 -25
- data/lib/pmdtester/builders/rule_set_builder.rb +43 -11
- data/lib/pmdtester/builders/summary_report_builder.rb +6 -1
- data/lib/pmdtester/cmd.rb +24 -9
- 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
|
|
@@ -250,16 +346,18 @@ module PmdTester
|
|
|
250
346
|
|
|
251
347
|
def build_pmd_with_maven
|
|
252
348
|
logger.info "#{@pmd_branch_name}: Building PMD #{@pmd_version}..."
|
|
349
|
+
extra_java_home = nil
|
|
253
350
|
|
|
254
351
|
package_cmd = if Semver.compare(@pmd_version, '7.14.0') >= 0
|
|
255
352
|
# build command since PMD migrated to central portal
|
|
256
|
-
'./mvnw clean package ' \
|
|
353
|
+
'./mvnw clean package -V ' \
|
|
257
354
|
'-PfastSkip ' \
|
|
258
355
|
'-DskipTests ' \
|
|
259
356
|
'-T1C -B'
|
|
260
357
|
else
|
|
358
|
+
extra_java_home = "#{Dir.home}/openjdk11"
|
|
261
359
|
# build command for older PMD versions
|
|
262
|
-
'./mvnw clean package ' \
|
|
360
|
+
'./mvnw clean package -V ' \
|
|
263
361
|
"-s #{ResourceLocator.resource('maven-settings.xml')} " \
|
|
264
362
|
'-Pfor-dokka-maven-plugin ' \
|
|
265
363
|
'-Dmaven.test.skip=true ' \
|
|
@@ -270,8 +368,8 @@ module PmdTester
|
|
|
270
368
|
'-T1C -B'
|
|
271
369
|
end
|
|
272
370
|
|
|
273
|
-
logger.debug "#{@pmd_branch_name}: maven command: #{package_cmd}"
|
|
274
|
-
Cmd.execute_successfully(package_cmd)
|
|
371
|
+
logger.debug "#{@pmd_branch_name}: maven command: #{package_cmd} java_home: #{extra_java_home}"
|
|
372
|
+
Cmd.execute_successfully(package_cmd, extra_java_home)
|
|
275
373
|
end
|
|
276
374
|
end
|
|
277
375
|
end
|
|
@@ -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,9 +23,11 @@ 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
|
|
30
|
+
run_required, rule_refs = get_rule_refs(filenames, all_rules_hash)
|
|
29
31
|
if run_required
|
|
30
32
|
output_filter_set(rule_refs)
|
|
31
33
|
build_config_file(rule_refs)
|
|
@@ -33,6 +35,7 @@ module PmdTester
|
|
|
33
35
|
else
|
|
34
36
|
logger.info NO_RULES_CHANGED_MESSAGE
|
|
35
37
|
end
|
|
38
|
+
logger.debug "Rules have changed: #{run_required}"
|
|
36
39
|
run_required
|
|
37
40
|
end
|
|
38
41
|
|
|
@@ -71,8 +74,8 @@ module PmdTester
|
|
|
71
74
|
# filtering possible or if no rules are affected.
|
|
72
75
|
# Whether to run the regression test is returned as an additional boolean flag.
|
|
73
76
|
#
|
|
74
|
-
def get_rule_refs(filenames)
|
|
75
|
-
run_required, categories, rules = determine_categories_rules(filenames)
|
|
77
|
+
def get_rule_refs(filenames, all_rules_hash)
|
|
78
|
+
run_required, categories, rules = determine_categories_rules(filenames, all_rules_hash)
|
|
76
79
|
logger.debug "Regression test required: #{run_required}"
|
|
77
80
|
logger.debug "Categories: #{categories}"
|
|
78
81
|
logger.debug "Rules: #{rules}"
|
|
@@ -122,12 +125,12 @@ module PmdTester
|
|
|
122
125
|
|
|
123
126
|
private
|
|
124
127
|
|
|
125
|
-
def determine_categories_rules(filenames)
|
|
128
|
+
def determine_categories_rules(filenames, all_rules_hash)
|
|
126
129
|
regression_test_required = false
|
|
127
130
|
categories = Set[]
|
|
128
131
|
rules = Set[]
|
|
129
132
|
filenames.each do |filename|
|
|
130
|
-
matched = check_single_filename?(filename, categories, rules)
|
|
133
|
+
matched = check_single_filename?(filename, all_rules_hash, categories, rules)
|
|
131
134
|
regression_test_required = true if matched
|
|
132
135
|
|
|
133
136
|
next if matched
|
|
@@ -141,14 +144,13 @@ module PmdTester
|
|
|
141
144
|
[regression_test_required, categories, rules]
|
|
142
145
|
end
|
|
143
146
|
|
|
144
|
-
def check_single_filename?(filename, categories, rules)
|
|
147
|
+
def check_single_filename?(filename, all_rules_hash, categories, rules)
|
|
145
148
|
logger.debug "Checking #{filename}"
|
|
146
149
|
|
|
147
|
-
#
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
rules.add("#{match_data[1]}/#{match_data[2]}.xml/#{match_data[3]}")
|
|
150
|
+
# direct match of a Java-based rule implementation
|
|
151
|
+
if all_rules_hash.key?(filename)
|
|
152
|
+
logger.debug "Direct match: #{all_rules_hash[filename]}"
|
|
153
|
+
rules.add(all_rules_hash[filename])
|
|
152
154
|
return true
|
|
153
155
|
end
|
|
154
156
|
|
|
@@ -196,5 +198,35 @@ module PmdTester
|
|
|
196
198
|
end
|
|
197
199
|
languages
|
|
198
200
|
end
|
|
201
|
+
|
|
202
|
+
# Creates a mapping between java source files and the rule reference
|
|
203
|
+
# in the ruleset, e.g.:
|
|
204
|
+
# 'pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/design/NcssCountRule.java'
|
|
205
|
+
# => 'java/design.xml/NcssCount'
|
|
206
|
+
def determine_all_rules
|
|
207
|
+
all_rules_hash = {}
|
|
208
|
+
Dir.chdir(@options.local_git_repo) do
|
|
209
|
+
Dir.glob('**/src/main/resources/category/*/*.xml').each do |rulesetfile|
|
|
210
|
+
match_data = %r{.+/src/main/resources/category/([^/]+)/([^/.]+\.xml)}.match(rulesetfile)
|
|
211
|
+
ref_prefix = "#{match_data[1]}/#{match_data[2]}"
|
|
212
|
+
java_base_path = rulesetfile.gsub(%r{src/main/resources/category/.+/.+\.xml}, 'src/main/java')
|
|
213
|
+
|
|
214
|
+
doc = File.open(rulesetfile) { |f| Nokogiri::XML(f) }
|
|
215
|
+
rules = doc.css('ruleset rule')
|
|
216
|
+
rules.each do |r|
|
|
217
|
+
next if r.attributes['class'].nil? # skip rule references
|
|
218
|
+
|
|
219
|
+
ref = "#{ref_prefix}/#{r.attributes['name'].content}"
|
|
220
|
+
clazz = r.attributes['class'].content
|
|
221
|
+
|
|
222
|
+
# guess java file name at standard location src/main/java relative to the category ruleset
|
|
223
|
+
java_filename = "#{java_base_path}/#{clazz.gsub('.', '/')}.java"
|
|
224
|
+
|
|
225
|
+
all_rules_hash[java_filename] = ref
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
all_rules_hash
|
|
230
|
+
end
|
|
199
231
|
end
|
|
200
232
|
end
|
|
@@ -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)
|
|
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)
|
|
30
|
-
stdout, stderr, status = internal_execute(cmd)
|
|
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,17 +49,23 @@ module PmdTester
|
|
|
40
49
|
end
|
|
41
50
|
|
|
42
51
|
def self.stderr_of(cmd)
|
|
43
|
-
_stdout, stderr, _status = internal_execute(cmd)
|
|
52
|
+
_stdout, stderr, _status = internal_execute(cmd, nil, true)
|
|
44
53
|
stderr
|
|
45
54
|
end
|
|
46
55
|
|
|
47
|
-
def self.internal_execute(cmd)
|
|
48
|
-
logger.debug "execute command '#{cmd}'"
|
|
56
|
+
def self.internal_execute(cmd, extra_java_home, debug_log_stdout)
|
|
57
|
+
logger.debug "execute command '#{cmd}' (extra_java_home: #{extra_java_home})"
|
|
58
|
+
|
|
59
|
+
new_env = ENV.to_h
|
|
60
|
+
unless extra_java_home.nil?
|
|
61
|
+
new_env['JAVA_HOME'] = extra_java_home
|
|
62
|
+
new_env['PATH'] = "#{extra_java_home}/bin:#{new_env['PATH']}"
|
|
63
|
+
end
|
|
49
64
|
|
|
50
|
-
stdout, stderr, status = Open3.capture3("#{cmd};")
|
|
65
|
+
stdout, stderr, status = Open3.capture3(new_env, "#{cmd};")
|
|
51
66
|
|
|
52
67
|
logger.debug "status: #{status}"
|
|
53
|
-
logger.debug "stdout: #{stdout}"
|
|
68
|
+
logger.debug "stdout: #{stdout}" if debug_log_stdout
|
|
54
69
|
logger.debug "stderr: #{stderr}"
|
|
55
70
|
|
|
56
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
|