pmdtester 1.0.0.pre.beta3 → 1.1.2

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.ci/build.sh +67 -0
  3. data/.ci/files/env.gpg +1 -0
  4. data/.ci/inc/install-openjdk.inc +26 -0
  5. data/.ci/manual-integration-tests.sh +20 -0
  6. data/.github/workflows/build.yml +39 -0
  7. data/.github/workflows/manual-integration-tests.yml +32 -0
  8. data/.gitignore +9 -0
  9. data/.hoerc +1 -1
  10. data/.rubocop.yml +13 -2
  11. data/.rubocop_todo.yml +7 -8
  12. data/.ruby-version +1 -0
  13. data/Gemfile +1 -12
  14. data/History.md +104 -0
  15. data/Manifest.txt +30 -6
  16. data/README.rdoc +110 -60
  17. data/Rakefile +27 -15
  18. data/config/all-java.xml +1 -1
  19. data/config/design.xml +1 -1
  20. data/config/projectlist_1_0_0.xsd +2 -1
  21. data/config/projectlist_1_1_0.xsd +31 -0
  22. data/lib/pmdtester.rb +12 -4
  23. data/lib/pmdtester/builders/liquid_renderer.rb +73 -0
  24. data/lib/pmdtester/builders/pmd_report_builder.rb +134 -60
  25. data/lib/pmdtester/builders/project_builder.rb +100 -0
  26. data/lib/pmdtester/builders/project_hasher.rb +126 -0
  27. data/lib/pmdtester/builders/rule_set_builder.rb +94 -48
  28. data/lib/pmdtester/builders/simple_progress_logger.rb +27 -0
  29. data/lib/pmdtester/builders/summary_report_builder.rb +62 -117
  30. data/lib/pmdtester/cmd.rb +15 -1
  31. data/lib/pmdtester/collection_by_file.rb +55 -0
  32. data/lib/pmdtester/parsers/options.rb +25 -2
  33. data/lib/pmdtester/parsers/pmd_report_document.rb +79 -27
  34. data/lib/pmdtester/parsers/projects_parser.rb +2 -4
  35. data/lib/pmdtester/pmd_branch_detail.rb +36 -12
  36. data/lib/pmdtester/pmd_configerror.rb +62 -0
  37. data/lib/pmdtester/pmd_error.rb +34 -34
  38. data/lib/pmdtester/pmd_report_detail.rb +10 -13
  39. data/lib/pmdtester/pmd_tester_utils.rb +57 -0
  40. data/lib/pmdtester/pmd_violation.rb +66 -26
  41. data/lib/pmdtester/project.rb +28 -23
  42. data/lib/pmdtester/report_diff.rb +194 -70
  43. data/lib/pmdtester/resource_locator.rb +4 -0
  44. data/lib/pmdtester/runner.rb +81 -54
  45. data/pmdtester.gemspec +64 -0
  46. data/resources/_includes/diff_pill_row.html +6 -0
  47. data/resources/css/bootstrap.min.css +7 -0
  48. data/resources/css/datatables.min.css +36 -0
  49. data/resources/css/pmd-tester.css +132 -0
  50. data/resources/js/bootstrap.min.js +7 -0
  51. data/resources/js/code-snippets.js +73 -0
  52. data/resources/js/datatables.min.js +726 -0
  53. data/resources/js/jquery-3.2.1.slim.min.js +4 -0
  54. data/resources/js/jquery.min.js +2 -0
  55. data/resources/js/popper.min.js +5 -0
  56. data/resources/js/project-report.js +136 -0
  57. data/resources/project_diff_report.html +205 -0
  58. data/resources/project_index.html +102 -0
  59. metadata +122 -38
  60. data/.travis.yml +0 -22
  61. data/lib/pmdtester/builders/diff_builder.rb +0 -30
  62. data/lib/pmdtester/builders/diff_report_builder.rb +0 -225
  63. data/lib/pmdtester/builders/html_report_builder.rb +0 -33
  64. data/resources/css/maven-base.css +0 -155
  65. data/resources/css/maven-theme.css +0 -171
@@ -1,78 +1,91 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'fileutils'
4
+ require 'rufus-scheduler'
4
5
 
5
6
  module PmdTester
6
7
  # Building pmd xml reports according to a list of standard
7
8
  # projects and branch of pmd source code
8
9
  class PmdReportBuilder
9
10
  include PmdTester
10
- def initialize(branch_config, projects, local_git_repo, pmd_branch_name)
11
- @branch_config = branch_config
11
+
12
+ def initialize(projects, options, branch_config, branch_name)
12
13
  @projects = projects
13
- @local_git_repo = local_git_repo
14
- @pmd_branch_name = pmd_branch_name
14
+ @local_git_repo = options.local_git_repo
15
+ @threads = options.threads
16
+ @error_recovery = options.error_recovery
17
+ @branch_config = branch_config
18
+ @pmd_branch_name = branch_name
15
19
  @pwd = Dir.getwd
16
20
 
17
- @pmd_branch_details = PmdBranchDetail.new(pmd_branch_name)
21
+ @pmd_branch_details = PmdBranchDetail.new(@pmd_branch_name)
22
+ @project_builder = ProjectBuilder.new(@projects)
18
23
  end
19
24
 
20
- def execute_reset_cmd(type, tag)
21
- case type
22
- when 'git'
23
- reset_cmd = "git reset --hard #{tag}"
24
- when 'hg'
25
- reset_cmd = "hg up #{tag}"
26
- end
25
+ def get_pmd_binary_file
26
+ logger.info "#{@pmd_branch_name}: Start packaging PMD"
27
+ Dir.chdir(@local_git_repo) do
28
+ build_branch_sha = Cmd.execute("git rev-parse #{@pmd_branch_name}^{commit}")
27
29
 
28
- Cmd.execute(reset_cmd)
29
- end
30
+ checkout_build_branch # needs a clean working tree, otherwise fails
30
31
 
31
- def get_projects
32
- logger.info 'Cloning projects started'
32
+ raise "Wrong branch #{get_last_commit_sha}" unless build_branch_sha == get_last_commit_sha
33
33
 
34
- @projects.each do |project|
35
- logger.info "Start cloning #{project.name} repository"
36
- path = project.local_source_path
37
- clone_cmd = "#{project.type} clone #{project.connection} #{path}"
38
- if File.exist?(path)
39
- logger.warn "Skipping clone, project path #{path} already exists"
34
+ distro_path = saved_distro_path(build_branch_sha)
35
+ logger.debug "#{@pmd_branch_name}: PMD Version is #{@pmd_version} " \
36
+ "(sha=#{build_branch_sha})"
37
+ logger.debug "#{@pmd_branch_name}: distro_path=#{distro_path}"
38
+ if File.directory?(distro_path)
39
+ logger.info "#{@pmd_branch_name}: Skipping packaging - saved version exists " \
40
+ " in #{distro_path}"
40
41
  else
41
- Cmd.execute(clone_cmd)
42
+ build_pmd(into_dir: distro_path)
42
43
  end
43
44
 
44
- Dir.chdir(path) do
45
- execute_reset_cmd(project.type, project.tag)
46
- end
47
- logger.info "Cloning #{project.name} completed"
45
+ # we're still on the build branch
46
+ @pmd_branch_details.branch_last_sha = build_branch_sha
47
+ @pmd_branch_details.branch_last_message = get_last_commit_message
48
48
  end
49
+ logger.info "#{@pmd_branch_name}: Packaging PMD completed"
49
50
  end
50
51
 
51
- def get_pmd_binary_file
52
- logger.info 'Start packaging PMD'
53
- Dir.chdir(@local_git_repo) do
54
- checkout_cmd = "git checkout #{@pmd_branch_name}"
55
- Cmd.execute(checkout_cmd)
56
-
57
- @pmd_branch_details.branch_last_sha = get_last_commit_sha
58
- @pmd_branch_details.branch_last_message = get_last_commit_message
59
-
60
- package_cmd = './mvnw clean package -Dpmd.skip=true -Dmaven.test.skip=true' \
61
- ' -Dmaven.checkstyle.skip=true -Dmaven.javadoc.skip=true'
52
+ # builds pmd on currently checked out branch
53
+ def build_pmd(into_dir:)
54
+ # in CI there might have been a build performed already. In that case
55
+ # we reuse the build result, otherwise we build PMD freshly
56
+ pmd_dist_target = "pmd-dist/target/pmd-bin-#{@pmd_version}.zip"
57
+ binary_exists = File.exist?(pmd_dist_target)
58
+ logger.debug "#{@pmd_branch_name}: Does the file #{pmd_dist_target} exist? #{binary_exists} (cwd: #{Dir.getwd})"
59
+ if binary_exists
60
+ # that's a warning, because we don't know, whether this build really
61
+ # belongs to the current branch or whether it's from a previous branch.
62
+ # In CI, that's not a problem, because the workspace is always fresh.
63
+ logger.warn "#{@pmd_branch_name}: Reusing already existing #{pmd_dist_target}"
64
+ else
65
+ logger.info "#{@pmd_branch_name}: Building PMD #{@pmd_version}..."
66
+ package_cmd = './mvnw clean package' \
67
+ ' -Dmaven.test.skip=true' \
68
+ ' -Dmaven.javadoc.skip=true' \
69
+ ' -Dmaven.source.skip=true' \
70
+ ' -Dcheckstyle.skip=true' \
71
+ ' -Dpmd.skip=true' \
72
+ ' -T1C'
62
73
  Cmd.execute(package_cmd)
74
+ end
63
75
 
64
- version_cmd = "./mvnw -q -Dexec.executable=\"echo\" -Dexec.args='${project.version}' " \
65
- '--non-recursive org.codehaus.mojo:exec-maven-plugin:1.5.0:exec'
66
- @pmd_version = Cmd.execute(version_cmd)
76
+ logger.info "#{@pmd_branch_name}: Extracting the zip"
77
+ Cmd.execute("unzip -qo #{pmd_dist_target} -d pmd-dist/target/exploded")
78
+ Cmd.execute("mv pmd-dist/target/exploded/pmd-bin-#{@pmd_version} #{into_dir}")
79
+ end
67
80
 
68
- unzip_cmd = "unzip -qo pmd-dist/target/pmd-bin-#{@pmd_version}.zip -d #{@pwd}/target"
69
- Cmd.execute(unzip_cmd)
70
- end
71
- logger.info 'Packaging PMD completed'
81
+ def determine_pmd_version
82
+ version_cmd = "./mvnw -q -Dexec.executable=\"echo\" -Dexec.args='${project.version}' " \
83
+ '--non-recursive org.codehaus.mojo:exec-maven-plugin:1.5.0:exec'
84
+ Cmd.execute(version_cmd)
72
85
  end
73
86
 
74
87
  def get_last_commit_sha
75
- get_last_commit_sha_cmd = 'git rev-parse HEAD'
88
+ get_last_commit_sha_cmd = 'git rev-parse HEAD^{commit}'
76
89
  Cmd.execute(get_last_commit_sha_cmd)
77
90
  end
78
91
 
@@ -81,31 +94,55 @@ module PmdTester
81
94
  Cmd.execute(get_last_commit_message_cmd)
82
95
  end
83
96
 
84
- def generate_pmd_report(src_root_dir, report_file)
85
- run_path = "target/pmd-bin-#{@pmd_version}/bin/run.sh"
86
- pmd_cmd = "#{run_path} pmd -d #{src_root_dir} -f xml -R #{@branch_config} " \
87
- "-r #{report_file} -failOnViolation false"
97
+ def generate_pmd_report(project)
98
+ error_recovery_options = @error_recovery ? 'PMD_JAVA_OPTS="-Dpmd.error_recovery -ea" ' : ''
99
+ run_path = "#{saved_distro_path(@pmd_branch_details.branch_last_sha)}/bin/run.sh"
100
+ pmd_cmd = "#{error_recovery_options}" \
101
+ "#{run_path} pmd -d #{project.local_source_path} -f xml " \
102
+ "-R #{project.get_config_path(@pmd_branch_name)} " \
103
+ "-r #{project.get_pmd_report_path(@pmd_branch_name)} " \
104
+ "-failOnViolation false -t #{@threads} " \
105
+ "#{project.auxclasspath}"
88
106
  start_time = Time.now
89
- Cmd.execute(pmd_cmd)
107
+ if File.exist?(project.get_pmd_report_path(@pmd_branch_name))
108
+ logger.warn "#{@pmd_branch_name}: Skipping PMD run - report " \
109
+ "#{project.get_pmd_report_path(@pmd_branch_name)} already exists"
110
+ else
111
+ Cmd.execute(pmd_cmd)
112
+ end
90
113
  end_time = Time.now
91
114
  [end_time - start_time, end_time]
92
115
  end
93
116
 
117
+ def generate_config_for(project)
118
+ logger.debug "Generating ruleset with excludes from #{@branch_config}"
119
+ doc = Nokogiri::XML(File.read(@branch_config))
120
+ ruleset = doc.at_css('ruleset')
121
+ ruleset.add_child("\n")
122
+ project.exclude_pattern.each do |exclude_pattern|
123
+ ruleset.add_child(" <exclude-pattern>#{exclude_pattern}</exclude-pattern>\n")
124
+ end
125
+
126
+ File.open(project.get_config_path(@pmd_branch_name), 'w') do |x|
127
+ x << doc.to_s
128
+ end
129
+
130
+ logger.debug "Created file #{project.get_config_path(@pmd_branch_name)}"
131
+ end
132
+
94
133
  def generate_pmd_reports
95
134
  logger.info "Generating PMD report started -- branch #{@pmd_branch_name}"
96
- get_pmd_binary_file
97
135
 
98
136
  sum_time = 0
99
137
  @projects.each do |project|
100
- logger.info "Generating #{project.name}'s PMD report"
101
- execution_time, end_time =
102
- generate_pmd_report(project.local_source_path,
103
- project.get_pmd_report_path(@pmd_branch_name))
138
+ progress_logger = SimpleProgressLogger.new("generating #{project.name}'s PMD report")
139
+ progress_logger.start
140
+ generate_config_for(project)
141
+ execution_time, end_time = generate_pmd_report(project)
142
+ progress_logger.stop
104
143
  sum_time += execution_time
105
144
 
106
- report_details = PmdReportDetail.new
107
- report_details.execution_time = execution_time
108
- report_details.timestamp = end_time
145
+ report_details = PmdReportDetail.new(execution_time: execution_time, timestamp: end_time)
109
146
  report_details.save(project.get_report_info_path(@pmd_branch_name))
110
147
  logger.info "#{project.name}'s PMD report was generated successfully"
111
148
  end
@@ -116,9 +153,46 @@ module PmdTester
116
153
  @pmd_branch_details
117
154
  end
118
155
 
156
+ # returns the branch details
119
157
  def build
120
- get_projects
158
+ @project_builder.clone_projects
159
+ @project_builder.build_projects
160
+ get_pmd_binary_file
121
161
  generate_pmd_reports
122
162
  end
163
+
164
+ private
165
+
166
+ def checkout_build_branch
167
+ logger.info "#{@pmd_branch_name}: Checking out the branch"
168
+ # note that this would fail if the tree is dirty
169
+ Cmd.execute("git checkout #{@pmd_branch_name}")
170
+
171
+ # determine the version
172
+ @pmd_version = determine_pmd_version
173
+
174
+ return unless wd_has_dirty_git_changes
175
+
176
+ # working dir is dirty....
177
+ # we don't allow this because we need the SHA to address the zip file
178
+ logger.error "#{@pmd_branch_name}: Won\'t build without a clean working tree, " \
179
+ 'commit your changes'
180
+ end
181
+
182
+ def work_dir
183
+ "#{@pwd}/target"
184
+ end
185
+
186
+ # path to the unzipped distribution
187
+ # e.g. <cwd>/pmd-bin-<version>-<branch>-<sha>
188
+ def saved_distro_path(build_sha)
189
+ "#{work_dir}/pmd-bin-#{@pmd_version}" \
190
+ "-#{PmdBranchDetail.branch_filename(@pmd_branch_name)}" \
191
+ "-#{build_sha}"
192
+ end
193
+
194
+ def wd_has_dirty_git_changes
195
+ !Cmd.execute('git status --porcelain').empty?
196
+ end
123
197
  end
124
198
  end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tempfile'
5
+
6
+ module PmdTester
7
+ # Clones and builds the projects, that are configured in the project-list.xml
8
+ class ProjectBuilder
9
+ include PmdTester
10
+
11
+ def initialize(projects)
12
+ @projects = projects
13
+ end
14
+
15
+ def clone_projects
16
+ logger.info 'Cloning projects started'
17
+
18
+ @projects.each do |project|
19
+ logger.info "Start cloning #{project.name} repository"
20
+ path = project.local_source_path
21
+ clone_cmd = "#{project.type} clone #{project.connection} #{path}"
22
+ if File.exist?(path)
23
+ logger.warn "Skipping clone, project path #{path} already exists"
24
+ else
25
+ Cmd.execute(clone_cmd)
26
+ end
27
+
28
+ Dir.chdir(path) do
29
+ execute_reset_cmd(project.type, project.tag)
30
+ end
31
+ logger.info "Cloning #{project.name} completed"
32
+ end
33
+
34
+ logger.info 'Cloning projects completed'
35
+ end
36
+
37
+ def build_projects
38
+ logger.info 'Building projects started'
39
+
40
+ @projects.each do |project|
41
+ path = project.local_source_path
42
+ Dir.chdir(path) do
43
+ progress_logger = SimpleProgressLogger.new("building #{project.name} in #{path}")
44
+ progress_logger.start
45
+ prepare_project(project)
46
+ progress_logger.stop
47
+ end
48
+ logger.info "Building #{project.name} completed"
49
+ end
50
+
51
+ logger.info 'Building projects completed'
52
+ end
53
+
54
+ private
55
+
56
+ def prepare_project(project)
57
+ # Note: current working directory is the project directory,
58
+ # where the source code has been cloned to
59
+ if project.build_command
60
+ logger.debug "Executing build-command: #{project.build_command}"
61
+ run_as_script(Dir.getwd, project.build_command)
62
+ end
63
+ if project.auxclasspath_command
64
+ logger.debug "Executing auxclasspath-command: #{project.auxclasspath_command}"
65
+ auxclasspath = run_as_script(Dir.getwd, project.auxclasspath_command)
66
+ project.auxclasspath = "-auxclasspath #{auxclasspath}"
67
+ else
68
+ project.auxclasspath = ''
69
+ end
70
+ end
71
+
72
+ def run_as_script(path, command)
73
+ script = Tempfile.new(['pmd-regression-', '.sh'], path)
74
+ logger.debug "Creating script #{script.path}"
75
+ begin
76
+ script.write(command)
77
+ script.close
78
+ shell = 'sh -xe'
79
+ if command.start_with?('#!')
80
+ shell = command.lines[0].chomp[2..] # remove leading "#!"
81
+ end
82
+ stdout = Cmd.execute("#{shell} #{script.path}")
83
+ ensure
84
+ script.unlink
85
+ end
86
+ stdout
87
+ end
88
+
89
+ def execute_reset_cmd(type, tag)
90
+ case type
91
+ when 'git'
92
+ reset_cmd = "git checkout #{tag}; git reset --hard #{tag}"
93
+ when 'hg'
94
+ reset_cmd = "hg up #{tag}"
95
+ end
96
+
97
+ Cmd.execute(reset_cmd)
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'differ'
4
+
5
+ module PmdTester
6
+ # Turn a project report into a hash that can be rendered somewhere else
7
+ module ProjectHasher
8
+ include PmdTester
9
+
10
+ def report_diff_to_h(rdiff)
11
+ {
12
+ 'violation_counts' => rdiff.violation_counts.to_h.transform_keys(&:to_s),
13
+ 'error_counts' => rdiff.error_counts.to_h.transform_keys(&:to_s),
14
+ 'configerror_counts' => rdiff.configerror_counts.to_h.transform_keys(&:to_s),
15
+
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,
23
+
24
+ 'rule_diffs' => rdiff.rule_summaries
25
+ }
26
+ end
27
+
28
+ def errors_to_h(project)
29
+ errors = project.report_diff.error_diffs_by_file.values.flatten
30
+ errors.map { |e| error_to_hash(e, project) }
31
+ end
32
+
33
+ def configerrors_to_h(project)
34
+ configerrors = project.report_diff.configerror_diffs_by_rule.values.flatten
35
+ configerrors.map { |e| configerror_to_hash(e) }
36
+ end
37
+
38
+ def violations_to_hash(project)
39
+ filename_index = []
40
+ all_vs = []
41
+ project.report_diff.violation_diffs_by_file.each do |file, vs|
42
+ file_ref = filename_index.size
43
+ filename_index.push(project.get_local_path(file))
44
+ vs.each do |v|
45
+ all_vs.push(make_violation_hash(file_ref, v))
46
+ end
47
+ end
48
+
49
+ {
50
+ 'file_index' => filename_index,
51
+ 'violations' => all_vs
52
+ }
53
+ end
54
+
55
+ def link_template(project)
56
+ l_str = project.type == 'git' ? 'L' : 'l'
57
+ "#{project.webview_url}/{file}##{l_str}{line}"
58
+ end
59
+
60
+ def violation_type(violation)
61
+ if violation.changed?
62
+ '~'
63
+ elsif violation.branch == 'patch'
64
+ '+'
65
+ else
66
+ '-'
67
+ end
68
+ end
69
+
70
+ def make_violation_hash(file_ref, violation)
71
+ h = {
72
+ 't' => violation_type(violation),
73
+ 'l' => violation.line,
74
+ 'f' => file_ref,
75
+ 'r' => violation.rule_name,
76
+ 'm' => violation.changed? ? diff_fragments(violation) : violation.message
77
+ }
78
+ h['ol'] = violation.old_line if violation.changed? && violation.line != violation.old_line
79
+ h
80
+ end
81
+
82
+ def diff_fragments(violation)
83
+ diff = Differ.diff_by_word(violation.message, violation.old_message)
84
+ diff.format_as(:html)
85
+ end
86
+
87
+ def error_to_hash(error, project)
88
+ escaped_stacktrace = sanitize_stacktrace(error)
89
+ old_stacktrace = error.old_error.nil? ? nil : sanitize_stacktrace(error.old_error)
90
+
91
+ {
92
+ 'file_url' => project.get_webview_url(error.filename),
93
+ 'stack_trace_html' => escaped_stacktrace,
94
+ 'old_stack_trace_html' => old_stacktrace,
95
+ 'short_message' => error.short_message,
96
+ 'short_filename' => error.short_filename,
97
+ 'filename' => error.filename,
98
+ 'change_type' => change_type(error)
99
+ }
100
+ end
101
+
102
+ def sanitize_stacktrace(error)
103
+ CGI.escapeHTML(error.stack_trace)
104
+ .gsub(error.filename, '<span class="meta-var">$FILE</span>')
105
+ .gsub(/\w++(?=\(\w++\.java:\d++\))/, '<span class="stack-trace-method">\\0</span>')
106
+ end
107
+
108
+ def configerror_to_hash(configerror)
109
+ {
110
+ 'rule' => configerror.rulename,
111
+ 'message' => configerror.msg,
112
+ 'change_type' => change_type(configerror)
113
+ }
114
+ end
115
+
116
+ def change_type(item)
117
+ if item.branch == BASE
118
+ 'removed'
119
+ elsif item.changed?
120
+ 'changed'
121
+ else
122
+ 'added'
123
+ end
124
+ end
125
+ end
126
+ end