pmdtester 1.0.0.pre.beta3 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
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