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
@@ -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 get_pmd_binary_file
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 #{get_last_commit_sha}" unless build_branch_sha == get_last_commit_sha
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 = get_last_commit_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 get_last_commit_sha
86
- get_last_commit_sha_cmd = 'git rev-parse HEAD^{commit}'
87
- Cmd.execute_successfully(get_last_commit_sha_cmd)
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 get_last_commit_message
91
- get_last_commit_message_cmd = 'git log -1 --pretty=%B'
92
- Cmd.execute_successfully(get_last_commit_message_cmd)
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
- status = Cmd.execute(pmd_cmd, project.get_project_target_dir(@pmd_branch_name))
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, report_info_path: project.get_report_info_path(@pmd_branch_name))
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 = sum_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
- @pmd_branch_details
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
- get_pmd_binary_file
163
- generate_pmd_reports
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
- # note that this would fail if the tree is dirty
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
- run_path = if File.exist?("#{run_path}/pmd")
231
- # New PMD 7 CLI script (pmd/pmd#4059)
232
- "#{run_path}/pmd check"
233
- else
234
- "#{run_path}/run.sh pmd"
235
- end
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
- 'base_execution_time' => PmdReportDetail.convert_seconds(rdiff.base_report.exec_time),
17
- 'patch_execution_time' => PmdReportDetail.convert_seconds(rdiff.patch_report.exec_time),
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
- 'base_exit_code' => rdiff.base_report.exit_code,
25
- 'patch_exit_code' => rdiff.patch_report.exit_code,
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
- all_vs.push(make_violation_hash(file_ref, v, is_diff: is_diff))
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 make_violation_hash(file_ref, violation, is_diff: true)
115
- h = {
116
- 't' => is_diff ? violation_type(violation) : '+',
117
- 'l' => violation.line,
118
- 'f' => file_ref,
119
- 'r' => violation.rule_name,
120
- 'm' => create_violation_message(violation, is_diff && violation.changed?)
121
- }
122
- h['ol'] = violation.old_line if is_diff && violation.changed? && violation.line != violation.old_line
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
- diff = Differ.diff_by_word(escape_html(violation.message),
130
- escape_html(violation.old_message))
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
- **report_diff_to_h(p.report_diff)
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.execute(cmd, path)
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