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
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PmdTester
4
+ # A collection of things, grouped by file.
5
+ #
6
+ # (Note: this replaces PmdErrors and PmdViolations)
7
+ class CollectionByFile
8
+ def initialize
9
+ # a hash of filename -> [list of items]
10
+ @hash = Hash.new([])
11
+ @total = 0
12
+ end
13
+
14
+ def add_all(filename, values)
15
+ return if values.empty?
16
+
17
+ if @hash.key?(filename)
18
+ @hash[filename].concat(values)
19
+ else
20
+ @hash[filename] = values
21
+ end
22
+ @total += values.size
23
+ end
24
+
25
+ def total_size
26
+ @total
27
+ end
28
+
29
+ def all_files
30
+ @hash.keys
31
+ end
32
+
33
+ def num_files
34
+ @hash.size
35
+ end
36
+
37
+ def all_values
38
+ @hash.values.flatten
39
+ end
40
+
41
+ def each_value(&block)
42
+ @hash.each_value do |vs|
43
+ vs.each(&block)
44
+ end
45
+ end
46
+
47
+ def [](fname)
48
+ @hash[fname]
49
+ end
50
+
51
+ def to_h
52
+ @hash
53
+ end
54
+ end
55
+ end
@@ -16,6 +16,7 @@ module PmdTester
16
16
  SINGLE = 'single'
17
17
  DEFAULT_CONFIG_PATH = ResourceLocator.locate('config/all-java.xml')
18
18
  DEFAULT_LIST_PATH = ResourceLocator.locate('config/project-list.xml')
19
+ DEFAULT_BASELINE_URL_PREFIX = 'https://sourceforge.net/projects/pmd/files/pmd-regression-tester/'
19
20
 
20
21
  attr_reader :local_git_repo
21
22
  attr_reader :base_branch
@@ -25,10 +26,14 @@ module PmdTester
25
26
  attr_reader :config
26
27
  attr_reader :project_list
27
28
  attr_reader :mode
29
+ attr_reader :threads
28
30
  attr_reader :html_flag
29
31
  attr_reader :auto_config_flag
30
32
  attr_reader :debug_flag
31
33
  attr_accessor :filter_set
34
+ attr_reader :keep_reports
35
+ attr_reader :error_recovery
36
+ attr_reader :baseline_download_url_prefix
32
37
 
33
38
  def initialize(argv)
34
39
  options = parse(argv)
@@ -40,10 +45,19 @@ module PmdTester
40
45
  @config = options[:c]
41
46
  @project_list = options[:l]
42
47
  @mode = options[:m]
48
+ @threads = options[:t]
43
49
  @html_flag = options[:f]
44
50
  @auto_config_flag = options[:a]
45
51
  @debug_flag = options[:d]
46
52
  @filter_set = nil
53
+ @keep_reports = options.keep_reports?
54
+ @error_recovery = options.error_recovery?
55
+ url = options[:baseline_download_url]
56
+ @baseline_download_url_prefix = if url[-1] == '/'
57
+ url
58
+ else
59
+ "#{url}/"
60
+ end
47
61
 
48
62
  # if the 'config' option is selected then `config` overrides `base_config` and `patch_config`
49
63
  @base_config = @config if !@config.nil? && @mode == 'local'
@@ -61,8 +75,8 @@ module PmdTester
61
75
  single: Set this option to 'single' if your patch branch contains changes
62
76
  for any option that can't work on master/base branch
63
77
  online: Set this option to 'online' if you want to download
64
- 'the PMD report of master/base branch rather than generating it locally
65
- local: Default option is 'local'
78
+ the PMD report of master/base branch rather than generating it locally
79
+ local: Default option is 'local', PMD reports for the base and patch branches are generated locally.
66
80
  DOC
67
81
 
68
82
  Slop.parse argv do |o|
@@ -79,13 +93,22 @@ module PmdTester
79
93
  'path to the file which contains the list of standard projects',
80
94
  default: DEFAULT_LIST_PATH
81
95
  o.string '-m', '--mode', mode_message, default: 'local'
96
+ o.integer '-t', '--threads', 'Sets the number of threads used by PMD.' \
97
+ ' Set threads to 0 to disable multi-threading processing.', default: 1
82
98
  o.bool '-f', '--html-flag',
83
99
  'whether to not generate the html diff report in single mode'
84
100
  o.bool '-a', '--auto-gen-config',
85
101
  'whether to generate configurations automatically based on branch differences,' \
86
102
  'this option only works in online and local mode'
103
+ o.bool '--keep-reports',
104
+ 'whether to keep old reports and skip running PMD again if possible'
87
105
  o.bool '-d', '--debug',
88
106
  'whether change log level to DEBUG to see more information'
107
+ o.bool '--error-recovery',
108
+ 'enable error recovery mode when executing PMD. Might help to analyze errors.'
109
+ o.string '--baseline-download-url',
110
+ 'download url prefix from where to download the baseline in online mode',
111
+ default: DEFAULT_BASELINE_URL_PREFIX
89
112
  o.on '-v', '--version' do
90
113
  puts VERSION
91
114
  exit
@@ -7,74 +7,126 @@ module PmdTester
7
7
  class PmdReportDocument < Nokogiri::XML::SAX::Document
8
8
  attr_reader :violations
9
9
  attr_reader :errors
10
+ attr_reader :configerrors
11
+ attr_reader :infos_by_rules
12
+
10
13
  def initialize(branch_name, working_dir, filter_set = nil)
11
- @violations = PmdViolations.new
12
- @errors = PmdErrors.new
14
+ @violations = CollectionByFile.new
15
+ @errors = CollectionByFile.new
16
+ @configerrors = Hash.new { |hash, key| hash[key] = [] }
17
+
18
+ @infos_by_rules = {}
13
19
  @current_violations = []
14
20
  @current_violation = nil
15
21
  @current_error = nil
16
- @current_element = ''
17
- @filename = ''
22
+ @current_configerror = nil
18
23
  @filter_set = filter_set
19
24
  @working_dir = working_dir
20
25
  @branch_name = branch_name
26
+
27
+ @cur_text = String.new(capacity: 200)
21
28
  end
22
29
 
23
30
  def start_element(name, attrs = [])
24
31
  attrs = attrs.to_h
25
- @current_element = name
26
32
 
27
33
  case name
28
34
  when 'file'
29
- @current_violations = []
30
- @current_filename = remove_work_dir!(attrs['name'])
35
+ handle_start_file attrs
31
36
  when 'violation'
32
- @current_violation = PmdViolation.new(attrs, @branch_name)
37
+ handle_start_violation attrs
33
38
  when 'error'
34
- @current_filename = remove_work_dir!(attrs['filename'])
35
- remove_work_dir!(attrs['msg'])
36
- @current_error = PmdError.new(attrs, @branch_name)
39
+ handle_start_error attrs
40
+ when 'configerror'
41
+ handle_start_configerror attrs
37
42
  end
38
43
  end
39
44
 
40
- def remove_work_dir!(str)
41
- str.sub!(/#{@working_dir}/, '')
45
+ def characters(string)
46
+ @cur_text << remove_work_dir!(string)
42
47
  end
43
48
 
44
- def characters(string)
45
- @current_violation.text = string unless @current_violation.nil?
49
+ def cdata_block(string)
50
+ @cur_text << remove_work_dir!(string)
46
51
  end
47
52
 
48
53
  def end_element(name)
49
54
  case name
50
55
  when 'file'
51
- unless @current_violations.empty?
52
- @violations.add_violations_by_filename(@current_filename, @current_violations)
53
- end
56
+ @violations.add_all(@current_filename, @current_violations)
54
57
  @current_filename = nil
55
58
  when 'violation'
56
- @current_violations.push(@current_violation) if match_filter_set?(@current_violation)
59
+ if match_filter_set?(@current_violation)
60
+ @current_violation.message = finish_text!
61
+ @current_violations.push(@current_violation)
62
+ end
57
63
  @current_violation = nil
58
64
  when 'error'
59
- @errors.add_error_by_filename(@current_filename, @current_error)
65
+ @current_error.stack_trace = finish_text!
66
+ @errors.add_all(@current_filename, [@current_error])
60
67
  @current_filename = nil
61
68
  @current_error = nil
69
+ when 'configerror'
70
+ @configerrors[@current_configerror.rulename].push(@current_configerror)
71
+ @current_configerror = nil
62
72
  end
73
+ @cur_text.clear
63
74
  end
64
75
 
65
- def cdata_block(string)
66
- remove_work_dir!(string)
67
- @current_error.text = string unless @current_error.nil?
76
+ private
77
+
78
+ # Modifies the string in place and returns it
79
+ # (this is what sub! does, except it returns nil if no replacement occurred)
80
+ def remove_work_dir!(str)
81
+ str.sub!(/#{@working_dir}/, '')
82
+ str
83
+ end
84
+
85
+ def finish_text!
86
+ res = @cur_text.strip!.dup.freeze
87
+ @cur_text.clear
88
+ res
68
89
  end
69
90
 
70
91
  def match_filter_set?(violation)
71
92
  return true if @filter_set.nil?
72
93
 
73
- @filter_set.each do |ruleset|
74
- return true if ruleset.eql?(violation.attrs['ruleset'].delete(' ').downcase)
75
- end
94
+ ruleset_attr = violation.ruleset_name.delete(' ').downcase! << '.xml'
95
+ return true if @filter_set.include?(ruleset_attr)
96
+
97
+ rule_ref = "#{ruleset_attr}/#{violation.rule_name}"
98
+
99
+ @filter_set.include?(rule_ref)
100
+ end
101
+
102
+ def handle_start_file(attrs)
103
+ @current_filename = remove_work_dir!(attrs['name'])
104
+ @current_violations = []
105
+ end
106
+
107
+ def handle_start_violation(attrs)
108
+ @current_violation = PmdViolation.new(
109
+ branch: @branch_name,
110
+ fname: @current_filename,
111
+ info_url: attrs['externalInfoUrl'],
112
+ bline: attrs['beginline'].to_i,
113
+ rule_name: attrs['rule'],
114
+ ruleset_name: attrs['ruleset'].freeze
115
+ )
116
+ end
117
+
118
+ def handle_start_error(attrs)
119
+ @current_filename = remove_work_dir!(attrs['filename'])
120
+
121
+ @current_error = PmdError.new(
122
+ branch: @branch_name,
123
+ filename: @current_filename,
124
+ short_message: remove_work_dir!(attrs['msg'])
125
+ )
126
+ end
76
127
 
77
- false
128
+ def handle_start_configerror(attrs)
129
+ @current_configerror = PmdConfigError.new(attrs, @branch_name)
78
130
  end
79
131
  end
80
132
  end
@@ -11,9 +11,7 @@ module PmdTester
11
11
  document = Nokogiri::XML(File.read(list_file))
12
12
 
13
13
  errors = schema.validate(document)
14
- unless errors.empty?
15
- raise ProjectsParserException.new(errors), "Schema validate failed: In #{list_file}"
16
- end
14
+ raise ProjectsParserException.new(errors), "Schema validate failed: In #{list_file}" unless errors.empty?
17
15
 
18
16
  projects = []
19
17
  document.xpath('//project').each do |project|
@@ -23,7 +21,7 @@ module PmdTester
23
21
  end
24
22
 
25
23
  def schema_file_path
26
- ResourceLocator.locate('config/projectlist_1_0_0.xsd')
24
+ ResourceLocator.locate('config/projectlist_1_1_0.xsd')
27
25
  end
28
26
  end
29
27
 
@@ -5,11 +5,16 @@ require 'json'
5
5
  module PmdTester
6
6
  # This class represents all details about branch of pmd
7
7
  class PmdBranchDetail
8
+ include PmdTester
9
+
8
10
  attr_accessor :branch_last_sha
9
11
  attr_accessor :branch_last_message
10
12
  attr_accessor :branch_name
11
13
  # The branch's execution time on all standard projects
12
14
  attr_accessor :execution_time
15
+ attr_accessor :jdk_version
16
+ attr_accessor :language
17
+ attr_accessor :pull_request
13
18
 
14
19
  def self.branch_filename(branch_name)
15
20
  branch_name&.tr('/', '_')
@@ -22,32 +27,51 @@ module PmdTester
22
27
  branch_filename = PmdBranchDetail.branch_filename(branch_name)
23
28
  @base_branch_dir = "target/reports/#{branch_filename}" unless @branch_name.nil?
24
29
  @execution_time = 0
30
+ # the result of command 'java -version' is going to stderr
31
+ @jdk_version = Cmd.stderr_of('java -version')
32
+ @language = ENV['LANG'] # the locale
33
+
34
+ prnum = ENV[PR_NUM_ENV_VAR]
35
+ @pull_request = prnum == 'false' ? nil : prnum
25
36
  end
26
37
 
27
- def load
28
- if File.exist?(branch_details_path)
29
- hash = JSON.parse(File.read(branch_details_path))
30
- @branch_last_sha = hash['branch_last_sha']
31
- @branch_last_message = hash['branch_last_message']
32
- @branch_name = hash['branch_name']
33
- @execution_time = hash['execution_time']
34
- hash
38
+ def self.load(branch_name, logger)
39
+ details = PmdBranchDetail.new(branch_name)
40
+ if File.exist?(details.path_to_save_file)
41
+ hash = JSON.parse(File.read(details.path_to_save_file))
42
+ details.branch_last_sha = hash['branch_last_sha']
43
+ details.branch_last_message = hash['branch_last_message']
44
+ details.branch_name = hash['branch_name']
45
+ details.execution_time = hash['execution_time']
46
+ details.jdk_version = hash['jdk_version']
47
+ details.language = hash['language']
48
+ details.pull_request = hash['pull_request']
35
49
  else
36
- {}
50
+ details.jdk_version = ''
51
+ details.language = ''
52
+ logger&.warn "#{details.path_to_save_file} doesn't exist!"
37
53
  end
54
+ details
38
55
  end
39
56
 
40
57
  def save
41
58
  hash = { branch_last_sha: @branch_last_sha,
42
59
  branch_last_message: @branch_last_message,
43
60
  branch_name: @branch_name,
44
- execution_time: @execution_time }
45
- file = File.new(branch_details_path, 'w')
61
+ execution_time: @execution_time,
62
+ jdk_version: @jdk_version,
63
+ language: @language,
64
+ pull_request: @pull_request }
65
+
66
+ FileUtils.mkdir_p(@base_branch_dir) unless File.directory?(@base_branch_dir)
67
+
68
+ file = File.new(path_to_save_file, 'w')
46
69
  file.puts JSON.generate(hash)
47
70
  file.close
71
+ self
48
72
  end
49
73
 
50
- def branch_details_path
74
+ def path_to_save_file
51
75
  "#{@base_branch_dir}/branch_info.json"
52
76
  end
53
77
 
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PmdTester
4
+ # This class represents a 'configerror' element of Pmd xml report
5
+ # and which Pmd branch the 'configerror' is from
6
+ class PmdConfigError
7
+ # The pmd branch type, 'base' or 'patch'
8
+ attr_reader :branch
9
+
10
+ # The schema of 'configerror' node:
11
+ # <xs:complexType name="configerror">
12
+ # <xs:attribute name="rule" type="xs:string" use="required" />
13
+ # <xs:attribute name="msg" type="xs:string" use="required" />
14
+ # </xs:complexType>
15
+ attr_reader :attrs
16
+ attr_accessor :old_error
17
+
18
+ def initialize(attrs, branch)
19
+ @attrs = attrs
20
+
21
+ @changed = false
22
+ @branch = branch
23
+ end
24
+
25
+ def rulename
26
+ @attrs['rule']
27
+ end
28
+
29
+ def msg
30
+ @attrs['msg']
31
+ end
32
+
33
+ def sort_key
34
+ rulename
35
+ end
36
+
37
+ def changed?
38
+ @changed
39
+ end
40
+
41
+ def eql?(other)
42
+ rulename.eql?(other.rulename) && msg.eql?(other.msg)
43
+ end
44
+
45
+ def try_merge?(other)
46
+ if branch != BASE &&
47
+ branch != other.branch &&
48
+ rulename == other.rulename &&
49
+ !changed? # not already changed
50
+ @changed = true
51
+ @old_error = other
52
+ true
53
+ end
54
+
55
+ false
56
+ end
57
+
58
+ def hash
59
+ [rulename, msg].hash
60
+ end
61
+ end
62
+ end