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
@@ -9,8 +9,9 @@ module PmdTester
9
9
  # Attention: we only consider java rulesets now.
10
10
  class RuleSetBuilder
11
11
  include PmdTester
12
- ALL_RULE_SETS = Set['bestpractices', 'codestyle', 'design', 'documentation',
13
- 'errorprone', 'multithreading', 'performance', 'security'].freeze
12
+ ALL_CATEGORIES = Set['bestpractices.xml', 'codestyle.xml', 'design.xml', 'documentation.xml',
13
+ 'errorprone.xml', 'multithreading.xml', 'performance.xml',
14
+ 'security.xml'].freeze
14
15
  PATH_TO_PMD_JAVA_BASED_RULES =
15
16
  'pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule'
16
17
  PATH_TO_PMD_XPATH_BASED_RULES = 'pmd-java/src/main/resources/category/java'
@@ -25,18 +26,35 @@ module PmdTester
25
26
 
26
27
  def build
27
28
  filenames = diff_filenames
28
- rule_sets = get_rule_sets(filenames)
29
- output_filter_set(rule_sets)
30
- build_config_file(rule_sets)
31
- logger.debug "Dynamic configuration: #{[rule_sets]}"
29
+ rule_refs = get_rule_refs(filenames)
30
+ output_filter_set(rule_refs)
31
+ build_config_file(rule_refs)
32
+ logger.debug "Dynamic configuration: #{[rule_refs]}"
33
+ rule_refs
32
34
  end
33
35
 
34
- def output_filter_set(rule_sets)
35
- if rule_sets == ALL_RULE_SETS
36
- # if `rule_sets` contains all rule sets, than no need to filter the baseline
37
- @options.filter_set = nil
36
+ def output_filter_set(rule_refs)
37
+ if rule_refs == ALL_CATEGORIES
38
+ if @options.mode == Options::ONLINE
39
+ @options.filter_set = Set[]
40
+ doc = File.open(@options.patch_config) { |f| Nokogiri::XML(f) }
41
+ rules = doc.css('ruleset rule')
42
+ rules.each do |r|
43
+ ref = r.attributes['ref'].content
44
+ ref.delete_prefix!('category/java/')
45
+ @options.filter_set.add(ref)
46
+ end
47
+
48
+ logger.debug "Using filter based on patch config #{@options.patch_config}: " \
49
+ "#{@options.filter_set}"
50
+ else
51
+ # if `rule_refs` contains all categories, then no need to filter the baseline
52
+ logger.debug 'No filter when comparing patch to baseline'
53
+ @options.filter_set = nil
54
+ end
38
55
  else
39
- @options.filter_set = rule_sets
56
+ logger.debug "Filter is now #{rule_refs}"
57
+ @options.filter_set = rule_refs
40
58
  end
41
59
  end
42
60
 
@@ -46,64 +64,92 @@ module PmdTester
46
64
  base = @options.base_branch
47
65
  patch = @options.patch_branch
48
66
  # We only need to support git here, since PMD's repo is using git.
49
- diff_cmd = "git diff --name-only #{base}..#{patch} -- pmd-core pmd-java"
67
+ diff_cmd = "git diff --name-only #{base}..#{patch} -- pmd-core/src/main pmd-java/src/main"
50
68
  filenames = Cmd.execute(diff_cmd)
51
69
  end
52
70
  filenames.split("\n")
53
71
  end
54
72
 
55
- def get_rule_sets(filenames)
56
- rule_sets = Set[]
73
+ def get_rule_refs(filenames)
74
+ categories, rules = determine_categories_rules(filenames)
75
+ logger.debug "Categories: #{categories}"
76
+ logger.debug "Rules: #{rules}"
77
+
78
+ # filter out all individual rules that are already covered by a complete category
79
+ categories.each do |cat|
80
+ rules.delete_if { |e| e.start_with?(cat) }
81
+ end
82
+
83
+ refs = Set[]
84
+ refs.merge(categories)
85
+ refs.merge(rules)
86
+ refs
87
+ end
88
+
89
+ def determine_categories_rules(filenames)
90
+ categories = Set[]
91
+ rules = Set[]
57
92
  filenames.each do |filename|
58
- match_data = %r{#{PATH_TO_PMD_JAVA_BASED_RULES}/([^/]+)/[^/]+Rule.java}.match(filename)
59
- if match_data.nil?
60
- match_data = %r{#{PATH_TO_PMD_XPATH_BASED_RULES}/([^/]+).xml}.match(filename)
93
+ match_data = check_single_filename(filename)
94
+
95
+ unless match_data.nil?
96
+ if match_data.size == 2
97
+ categories.add("#{match_data[1]}.xml")
98
+ else
99
+ rules.add("#{match_data[1]}.xml/#{match_data[2]}")
100
+ end
61
101
  end
62
102
 
63
- category = if match_data.nil?
64
- nil
65
- else
66
- match_data[1]
67
- end
103
+ next unless match_data.nil?
68
104
 
69
- if category.nil?
70
- rule_sets = ALL_RULE_SETS
71
- break
72
- else
73
- rule_sets.add(category)
74
- end
105
+ logger.debug "Change doesn't match specific rule/category - enable all rules"
106
+ categories = ALL_CATEGORIES
107
+ rules.clear
108
+ break
75
109
  end
76
- rule_sets
110
+ [categories, rules]
111
+ end
112
+
113
+ def check_single_filename(filename)
114
+ logger.debug "Checking #{filename}"
115
+ match_data = %r{#{PATH_TO_PMD_JAVA_BASED_RULES}/([^/]+)/([^/]+)Rule.java}.match(filename)
116
+ match_data = %r{#{PATH_TO_PMD_XPATH_BASED_RULES}/([^/]+).xml}.match(filename) if match_data.nil?
117
+ logger.debug "Matches: #{match_data.inspect}"
118
+ match_data
77
119
  end
78
120
 
79
- def build_config_file(rule_sets)
80
- if rule_sets.empty?
121
+ def build_config_file(rule_refs)
122
+ if rule_refs.empty?
81
123
  logger.info NO_JAVA_RULES_CHANGED_MESSAGE
82
- exit 0
124
+ return
83
125
  end
84
126
 
85
- doc = Nokogiri::XML(File.read(PATH_TO_ALL_JAVA_RULES))
86
- doc.search('rule').each do |rule|
87
- rule.remove unless match_ref?(rule, rule_sets)
127
+ if rule_refs == ALL_CATEGORIES
128
+ logger.debug 'All rules are used. Not generating a dynamic ruleset.'
129
+ logger.debug "Using the configured/default ruleset base_config=#{@options.base_config} "\
130
+ "patch_config=#{@options.patch_config}"
131
+ return
88
132
  end
89
133
 
90
- description = doc.at_css('description')
91
- description.content = 'The ruleset generated by PmdTester dynamically'
92
-
93
- write_dynamic_file(doc)
134
+ write_dynamic_file(rule_refs)
94
135
  end
95
136
 
96
- def match_ref?(rule_node, rule_sets)
97
- rule_sets.each do |rule_set|
98
- return true unless rule_node['ref'].index(rule_set).nil?
137
+ def write_dynamic_file(rule_refs)
138
+ logger.debug "Generating dynamic configuration for: #{[rule_refs]}"
139
+ builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
140
+ xml.ruleset('xmlns' => 'http://pmd.sourceforge.net/ruleset/2.0.0',
141
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
142
+ 'xsi:schemaLocation' => 'http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd',
143
+ 'name' => 'Dynamic PmdTester Ruleset') do
144
+ xml.description 'The ruleset generated by PmdTester dynamically'
145
+ rule_refs.each do |entry|
146
+ xml.rule('ref' => "category/java/#{entry}")
147
+ end
148
+ end
99
149
  end
100
-
101
- false
102
- end
103
-
104
- def write_dynamic_file(doc)
150
+ doc = builder.to_xml(indent: 4, encoding: 'UTF-8')
105
151
  File.open(PATH_TO_DYNAMIC_CONFIG, 'w') do |x|
106
- x << doc.to_s.gsub(/\n\s+\n/, "\n")
152
+ x << doc.gsub(/\n\s+\n/, "\n")
107
153
  end
108
154
  @options.base_config = PATH_TO_DYNAMIC_CONFIG
109
155
  @options.patch_config = PATH_TO_DYNAMIC_CONFIG
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rufus-scheduler'
4
+
5
+ module PmdTester
6
+ # Helper class that provides a simple progress logging
7
+ class SimpleProgressLogger
8
+ include PmdTester
9
+ def initialize(task_name)
10
+ @task_name = task_name
11
+ end
12
+
13
+ def start
14
+ logger.info "Starting #{@task_name}"
15
+ message_counter = 1
16
+ @scheduler = Rufus::Scheduler.new
17
+ @scheduler.every '2m' do
18
+ logger.info "Still #{@task_name} (#{message_counter})..."
19
+ message_counter += 1
20
+ end
21
+ end
22
+
23
+ def stop
24
+ @scheduler.shutdown
25
+ end
26
+ end
27
+ end
@@ -2,145 +2,90 @@
2
2
 
3
3
  module PmdTester
4
4
  # Building summary report to show the details about projects and pmd branchs
5
- class SummaryReportBuilder < HtmlReportBuilder
5
+ class SummaryReportBuilder
6
6
  include PmdTester
7
- REPORT_DIR = 'target/reports/diff'
8
- BASE_CONFIG_PATH = 'target/reports/diff/base_config.xml'
9
- PATCH_CONFIG_PATH = 'target/reports/diff/patch_config.xml'
10
- INDEX_PATH = 'target/reports/diff/index.html'
11
-
12
- def build(projects, base_name, patch_name)
13
- @projects = projects
14
- @base_details = get_branch_details(base_name)
15
- @patch_details = get_branch_details(patch_name)
16
-
17
- FileUtils.mkdir_p(REPORT_DIR) unless File.directory?(REPORT_DIR)
18
- index = File.new(INDEX_PATH, 'w')
19
-
20
- html_report = build_html_report('Summary report')
21
- copy_css(REPORT_DIR)
22
-
23
- index.puts html_report
24
- index.close
25
-
26
- logger.info 'Built summary report successfully!'
27
- end
28
-
29
- def get_branch_details(branch_name)
30
- details = PmdBranchDetail.new(branch_name)
31
- details.load
32
- details
33
- end
7
+ include LiquidRenderer
8
+ include ProjectHasher
34
9
 
35
- def build_body(doc)
36
- build_branch_details_section(doc)
37
- build_projects_section(doc)
38
- end
10
+ REPORT_DIR = 'target/reports/diff'
11
+ BASE_CONFIG_NAME = 'base_config.xml'
12
+ PATCH_CONFIG_NAME = 'patch_config.xml'
39
13
 
40
- def build_branch_details_section(doc)
41
- doc.div(class: 'section', id: 'branchdetails') do
42
- doc.h2 'Branch details:'
43
- build_branch_details_table(doc)
14
+ def write_all_projects(projects, base_details, patch_details)
15
+ projects.each do |project|
16
+ process_project(project, "#{REPORT_DIR}/#{project.name}")
44
17
  end
45
- end
18
+ logger.info 'Built all difference reports successfully!'
46
19
 
47
- def build_branch_details_table(doc)
48
- doc.table(class: 'bodyTable', border: '0') do
49
- build_branch_table_head(doc)
50
- build_branch_table_body(doc)
51
- end
20
+ FileUtils.mkdir_p(REPORT_DIR)
21
+ write_structure(REPORT_DIR)
22
+ copy_configs(REPORT_DIR, base_details, patch_details)
23
+ write_index(REPORT_DIR, base_details, patch_details, projects)
24
+ logger.info "Built summary report successfully in #{REPORT_DIR}!"
52
25
  end
53
26
 
54
- def build_branch_table_head(doc)
55
- doc.thead do
56
- doc.tr do
57
- doc.th 'Item'
58
- doc.th 'base'
59
- doc.th 'patch'
60
- end
61
- end
62
- end
27
+ private
63
28
 
64
- def build_branch_table_body(doc)
65
- doc.tbody do
66
- build_branch_table_row(doc, 'branch name', @base_details.branch_name,
67
- @patch_details.branch_name)
68
- build_branch_table_row(doc, 'branch last commit sha', @base_details.branch_last_sha,
69
- @patch_details.branch_last_sha)
70
- build_branch_table_row(doc, 'branch last commit message', @base_details.branch_last_message,
71
- @patch_details.branch_last_message)
72
- build_branch_table_row(doc, 'total execution time', @base_details.format_execution_time,
73
- @patch_details.format_execution_time)
74
- build_branch_config_table_row(doc)
75
- end
29
+ def process_project(project, dir)
30
+ logger.info "Rendering #{project.name}..."
31
+ LiquidProjectRenderer.new.write_project_index(project, dir)
76
32
  end
77
33
 
78
- def build_branch_config_table_row(doc)
79
- doc.tr do
80
- doc.td(class: 'c') { doc.text 'branch configuration' }
81
- base_config_src_path = @base_details.target_branch_config_path
82
- copy_branch_config_file(base_config_src_path, BASE_CONFIG_PATH)
83
- doc.td(class: 'a') do
84
- doc.a(href: './base_config.xml') { doc.text 'base config' }
85
- end
86
- patch_config_stc_path = @patch_details.target_branch_config_path
87
- FileUtils.cp(patch_config_stc_path, PATCH_CONFIG_PATH)
88
- doc.td(class: 'b') do
89
- doc.a(href: './patch_config.xml') { doc.text 'patch config' }
90
- end
91
- end
34
+ def write_structure(target_root)
35
+ logger.info 'Copying resources...'
36
+ copy_resource('css', target_root)
37
+ copy_resource('js', target_root)
92
38
  end
93
39
 
94
- def copy_branch_config_file(src, dest)
95
- FileUtils.cp(src, dest) if File.exist?(src)
40
+ def copy_configs(target_root, base_details, patch_details)
41
+ copy_file(base_details.target_branch_config_path, "#{target_root}/#{BASE_CONFIG_NAME}")
42
+ copy_file(patch_details.target_branch_config_path, "#{target_root}/#{PATCH_CONFIG_NAME}")
96
43
  end
97
44
 
98
- def build_branch_table_row(doc, item, base, patch)
99
- doc.tr do
100
- doc.td(class: 'c') { doc.text item }
101
- doc.td(class: 'a') { doc.text base }
102
- doc.td(class: 'b') { doc.text patch }
103
- end
45
+ def copy_file(src, dest)
46
+ FileUtils.cp(src, dest) if File.exist?(src)
104
47
  end
105
48
 
106
- def build_projects_section(doc)
107
- doc.div(class: 'section', id: 'projects') do
108
- doc.h2 'Projects:'
109
- build_projects_table(doc)
49
+ def write_index(target_root, base_details, patch_details, projects)
50
+ projects = projects.map do |p|
51
+ {
52
+ 'name' => p.name,
53
+ 'tag' => p.tag,
54
+ 'report_url' => "./#{p.name}/index.html",
55
+ **report_diff_to_h(p.report_diff)
56
+ }
110
57
  end
111
- end
112
58
 
113
- def build_projects_table(doc)
114
- doc.table(class: 'bodyTable', border: '0') do
115
- build_projects_table_head(doc)
116
- build_projects_table_body(doc)
117
- end
59
+ env = {
60
+ 'comparison_url' => create_comparison_url(base_details, patch_details),
61
+ 'base' => to_liquid(base_details, BASE_CONFIG_NAME),
62
+ 'patch' => to_liquid(patch_details, PATCH_CONFIG_NAME),
63
+ 'projects' => projects
64
+ }
65
+ logger.info 'Writing /index.html...'
66
+ render_and_write('project_index.html', "#{target_root}/index.html", env)
118
67
  end
119
68
 
120
- def build_projects_table_head(doc)
121
- doc.thead do
122
- doc.tr do
123
- doc.th 'project name'
124
- doc.th 'project branch/tag'
125
- doc.th 'diff exist?'
126
- doc.th 'introduce new errors?'
127
- end
128
- end
69
+ def create_comparison_url(base_details, patch_details)
70
+ base = CGI.escape(base_details.branch_name)
71
+ patch = CGI.escape(patch_details.branch_last_sha)
72
+ "https://github.com/pmd/pmd/compare/#{base}...#{patch}"
129
73
  end
130
74
 
131
- def build_projects_table_body(doc)
132
- doc.tbody do
133
- @projects.each do |project|
134
- doc.tr do
135
- doc.td do
136
- doc.a(href: project.diff_report_index_ref_path) { doc.text project.name }
137
- end
138
- doc.td project.tag
139
- doc.td project.diffs_exist? ? 'Yes' : 'No'
140
- doc.td project.introduce_new_errors? ? 'Yes' : 'No'
141
- end
142
- end
143
- end
75
+ def to_liquid(details, config_name)
76
+ {
77
+ 'tree_url' => "https://github.com/pmd/pmd/tree/#{CGI.escape(details.branch_last_sha)}",
78
+ 'name' => details.branch_name,
79
+ 'tip' => {
80
+ 'sha' => details.branch_last_sha,
81
+ 'message' => details.branch_last_message
82
+ },
83
+ 'execution_time' => PmdReportDetail.convert_seconds(details.execution_time),
84
+ 'jdk_info' => details.jdk_version,
85
+ 'locale' => details.language,
86
+ 'config_url' => config_name,
87
+ 'pr_number' => details.pull_request
88
+ }
144
89
  end
145
90
  end
146
91
  end
data/lib/pmdtester/cmd.rb CHANGED
@@ -7,20 +7,34 @@ module PmdTester
7
7
  class Cmd
8
8
  extend PmdTester
9
9
  def self.execute(cmd)
10
+ stdout, _stderr, _status = internal_execute(cmd)
11
+ stdout&.chomp!
12
+ stdout
13
+ end
14
+
15
+ def self.stderr_of(cmd)
16
+ _stdout, stderr, _status = internal_execute(cmd)
17
+ stderr
18
+ end
19
+
20
+ def self.internal_execute(cmd)
10
21
  logger.debug "execute command '#{cmd}'"
11
22
 
12
23
  stdout, stderr, status = Open3.capture3("#{cmd};")
13
24
 
14
25
  logger.debug stdout
15
26
  unless status.success?
27
+ logger.error stdout
16
28
  logger.error stderr
17
29
  raise CmdException.new(cmd, stderr)
18
30
  end
19
31
 
20
32
  stdout&.chomp!
21
33
 
22
- stdout
34
+ [stdout, stderr, status]
23
35
  end
36
+
37
+ private_class_method :internal_execute
24
38
  end
25
39
 
26
40
  # The exception should be raised when the shell command failed.