pmdtester 1.0.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.ci/build.sh +99 -0
  3. data/.ci/inc/fetch_ci_scripts.bash +19 -0
  4. data/.ci/manual-integration-tests.sh +37 -0
  5. data/.github/workflows/build.yml +55 -0
  6. data/.github/workflows/manual-integration-tests.yml +43 -0
  7. data/.gitignore +9 -0
  8. data/.hoerc +1 -1
  9. data/.rubocop.yml +9 -2
  10. data/.ruby-version +1 -0
  11. data/History.md +79 -0
  12. data/Manifest.txt +28 -9
  13. data/README.rdoc +59 -33
  14. data/Rakefile +7 -5
  15. data/config/all-java.xml +1 -1
  16. data/config/design.xml +1 -1
  17. data/config/project-list.xml +8 -7
  18. data/config/projectlist_1_0_0.xsd +2 -1
  19. data/config/projectlist_1_1_0.xsd +31 -0
  20. data/config/projectlist_1_2_0.xsd +39 -0
  21. data/lib/pmdtester.rb +8 -7
  22. data/lib/pmdtester/builders/liquid_renderer.rb +130 -0
  23. data/lib/pmdtester/builders/pmd_report_builder.rb +107 -79
  24. data/lib/pmdtester/builders/project_builder.rb +105 -0
  25. data/lib/pmdtester/builders/project_hasher.rb +128 -0
  26. data/lib/pmdtester/builders/rule_set_builder.rb +96 -47
  27. data/lib/pmdtester/builders/simple_progress_logger.rb +4 -4
  28. data/lib/pmdtester/builders/summary_report_builder.rb +63 -131
  29. data/lib/pmdtester/collection_by_file.rb +55 -0
  30. data/lib/pmdtester/parsers/options.rb +24 -0
  31. data/lib/pmdtester/parsers/pmd_report_document.rb +72 -28
  32. data/lib/pmdtester/parsers/projects_parser.rb +2 -4
  33. data/lib/pmdtester/pmd_branch_detail.rb +35 -19
  34. data/lib/pmdtester/pmd_configerror.rb +23 -24
  35. data/lib/pmdtester/pmd_error.rb +34 -34
  36. data/lib/pmdtester/pmd_report_detail.rb +10 -13
  37. data/lib/pmdtester/pmd_tester_utils.rb +58 -0
  38. data/lib/pmdtester/pmd_violation.rb +66 -28
  39. data/lib/pmdtester/project.rb +42 -56
  40. data/lib/pmdtester/report_diff.rb +203 -109
  41. data/lib/pmdtester/resource_locator.rb +4 -0
  42. data/lib/pmdtester/runner.rb +67 -64
  43. data/pmdtester.gemspec +28 -37
  44. data/resources/_includes/diff_pill_row.html +6 -0
  45. data/resources/css/bootstrap.min.css +7 -0
  46. data/resources/css/datatables.min.css +36 -0
  47. data/resources/css/pmd-tester.css +132 -0
  48. data/resources/js/bootstrap.min.js +7 -0
  49. data/resources/js/code-snippets.js +73 -0
  50. data/resources/js/datatables.min.js +726 -0
  51. data/resources/js/jquery-3.2.1.slim.min.js +4 -0
  52. data/resources/js/jquery.min.js +2 -0
  53. data/resources/js/popper.min.js +5 -0
  54. data/resources/js/project-report.js +137 -0
  55. data/resources/project_diff_report.html +214 -0
  56. data/resources/project_index.html +113 -0
  57. data/resources/project_pmd_report.html +186 -0
  58. metadata +73 -25
  59. data/.travis.yml +0 -40
  60. data/lib/pmdtester/builders/diff_builder.rb +0 -31
  61. data/lib/pmdtester/builders/diff_report/configerrors.rb +0 -50
  62. data/lib/pmdtester/builders/diff_report/errors.rb +0 -71
  63. data/lib/pmdtester/builders/diff_report/violations.rb +0 -77
  64. data/lib/pmdtester/builders/diff_report_builder.rb +0 -99
  65. data/lib/pmdtester/builders/html_report_builder.rb +0 -56
  66. data/resources/css/maven-base.css +0 -155
  67. data/resources/css/maven-theme.css +0 -171
@@ -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
@@ -28,8 +29,12 @@ module PmdTester
28
29
  attr_reader :threads
29
30
  attr_reader :html_flag
30
31
  attr_reader :auto_config_flag
32
+ attr_reader :filter_with_patch_config
31
33
  attr_reader :debug_flag
32
34
  attr_accessor :filter_set
35
+ attr_reader :keep_reports
36
+ attr_reader :error_recovery
37
+ attr_reader :baseline_download_url_prefix
33
38
 
34
39
  def initialize(argv)
35
40
  options = parse(argv)
@@ -44,8 +49,17 @@ module PmdTester
44
49
  @threads = options[:t]
45
50
  @html_flag = options[:f]
46
51
  @auto_config_flag = options[:a]
52
+ @filter_with_patch_config = options.filter_with_patch_config?
47
53
  @debug_flag = options[:d]
48
54
  @filter_set = nil
55
+ @keep_reports = options.keep_reports?
56
+ @error_recovery = options.error_recovery?
57
+ url = options[:baseline_download_url]
58
+ @baseline_download_url_prefix = if url[-1] == '/'
59
+ url
60
+ else
61
+ "#{url}/"
62
+ end
49
63
 
50
64
  # if the 'config' option is selected then `config` overrides `base_config` and `patch_config`
51
65
  @base_config = @config if !@config.nil? && @mode == 'local'
@@ -88,8 +102,18 @@ module PmdTester
88
102
  o.bool '-a', '--auto-gen-config',
89
103
  'whether to generate configurations automatically based on branch differences,' \
90
104
  'this option only works in online and local mode'
105
+ o.bool '--filter-with-patch-config',
106
+ 'whether to use patch config to filter baseline result as if --auto-gen-config ' \
107
+ 'has been used. This option only works in online mode.'
108
+ o.bool '--keep-reports',
109
+ 'whether to keep old reports and skip running PMD again if possible'
91
110
  o.bool '-d', '--debug',
92
111
  'whether change log level to DEBUG to see more information'
112
+ o.bool '--error-recovery',
113
+ 'enable error recovery mode when executing PMD. Might help to analyze errors.'
114
+ o.string '--baseline-download-url',
115
+ 'download url prefix from where to download the baseline in online mode',
116
+ default: DEFAULT_BASELINE_URL_PREFIX
93
117
  o.on '-v', '--version' do
94
118
  puts VERSION
95
119
  exit
@@ -8,79 +8,123 @@ module PmdTester
8
8
  attr_reader :violations
9
9
  attr_reader :errors
10
10
  attr_reader :configerrors
11
+
11
12
  def initialize(branch_name, working_dir, filter_set = nil)
12
- @violations = PmdViolations.new
13
- @errors = PmdErrors.new
14
- @configerrors = PmdConfigErrors.new
13
+ @violations = CollectionByFile.new
14
+ @errors = CollectionByFile.new
15
+ @configerrors = Hash.new { |hash, key| hash[key] = [] }
16
+
15
17
  @current_violations = []
16
18
  @current_violation = nil
17
19
  @current_error = nil
18
20
  @current_configerror = nil
19
- @current_element = ''
20
- @filename = ''
21
21
  @filter_set = filter_set
22
22
  @working_dir = working_dir
23
23
  @branch_name = branch_name
24
+
25
+ @cur_text = String.new(capacity: 200)
24
26
  end
25
27
 
26
28
  def start_element(name, attrs = [])
27
29
  attrs = attrs.to_h
28
- @current_element = name
29
30
 
30
31
  case name
31
32
  when 'file'
32
- @current_violations = []
33
- @current_filename = remove_work_dir!(attrs['name'])
33
+ handle_start_file attrs
34
34
  when 'violation'
35
- @current_violation = PmdViolation.new(attrs, @branch_name)
35
+ handle_start_violation attrs
36
36
  when 'error'
37
- @current_filename = remove_work_dir!(attrs['filename'])
38
- remove_work_dir!(attrs['msg'])
39
- @current_error = PmdError.new(attrs, @branch_name)
37
+ handle_start_error attrs
40
38
  when 'configerror'
41
- @current_configerror = PmdConfigError.new(attrs, @branch_name)
39
+ handle_start_configerror attrs
42
40
  end
43
41
  end
44
42
 
45
- def remove_work_dir!(str)
46
- str.sub!(/#{@working_dir}/, '')
43
+ def characters(string)
44
+ @cur_text << remove_work_dir!(string)
47
45
  end
48
46
 
49
- def characters(string)
50
- @current_violation&.text = string
47
+ def cdata_block(string)
48
+ @cur_text << remove_work_dir!(string)
51
49
  end
52
50
 
53
51
  def end_element(name)
54
52
  case name
55
53
  when 'file'
56
- @violations.add_violations_by_filename(@current_filename, @current_violations)
54
+ @violations.add_all(@current_filename, @current_violations)
57
55
  @current_filename = nil
58
56
  when 'violation'
59
- @current_violations.push(@current_violation) if match_filter_set?(@current_violation)
57
+ if match_filter_set?(@current_violation)
58
+ @current_violation.message = finish_text!
59
+ @current_violations.push(@current_violation)
60
+ end
60
61
  @current_violation = nil
61
62
  when 'error'
62
- @errors.add_error_by_filename(@current_filename, @current_error)
63
+ @current_error.stack_trace = finish_text!
64
+ @errors.add_all(@current_filename, [@current_error])
63
65
  @current_filename = nil
64
66
  @current_error = nil
65
67
  when 'configerror'
66
- @configerrors.add_error(@current_configerror)
68
+ @configerrors[@current_configerror.rulename].push(@current_configerror)
67
69
  @current_configerror = nil
68
70
  end
71
+ @cur_text.clear
69
72
  end
70
73
 
71
- def cdata_block(string)
72
- remove_work_dir!(string)
73
- @current_error&.text = string
74
+ private
75
+
76
+ # Modifies the string in place and returns it
77
+ # (this is what sub! does, except it returns nil if no replacement occurred)
78
+ def remove_work_dir!(str)
79
+ str.sub!(/#{@working_dir}/, '')
80
+ str
81
+ end
82
+
83
+ def finish_text!
84
+ res = @cur_text.strip!.dup.freeze
85
+ @cur_text.clear
86
+ res
74
87
  end
75
88
 
76
89
  def match_filter_set?(violation)
77
90
  return true if @filter_set.nil?
78
91
 
79
- @filter_set.each do |ruleset|
80
- return true if ruleset.eql?(violation.attrs['ruleset'].delete(' ').downcase)
81
- end
92
+ ruleset_attr = violation.ruleset_name.delete(' ').downcase! << '.xml'
93
+ return true if @filter_set.include?(ruleset_attr)
94
+
95
+ rule_ref = "#{ruleset_attr}/#{violation.rule_name}"
96
+
97
+ @filter_set.include?(rule_ref)
98
+ end
99
+
100
+ def handle_start_file(attrs)
101
+ @current_filename = remove_work_dir!(attrs['name'])
102
+ @current_violations = []
103
+ end
104
+
105
+ def handle_start_violation(attrs)
106
+ @current_violation = PmdViolation.new(
107
+ branch: @branch_name,
108
+ fname: @current_filename,
109
+ info_url: attrs['externalInfoUrl'],
110
+ bline: attrs['beginline'].to_i,
111
+ rule_name: attrs['rule'],
112
+ ruleset_name: attrs['ruleset'].freeze
113
+ )
114
+ end
115
+
116
+ def handle_start_error(attrs)
117
+ @current_filename = remove_work_dir!(attrs['filename'])
118
+
119
+ @current_error = PmdError.new(
120
+ branch: @branch_name,
121
+ filename: @current_filename,
122
+ short_message: remove_work_dir!(attrs['msg'])
123
+ )
124
+ end
82
125
 
83
- false
126
+ def handle_start_configerror(attrs)
127
+ @current_configerror = PmdConfigError.new(attrs, @branch_name)
84
128
  end
85
129
  end
86
130
  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_2_0.xsd')
27
25
  end
28
26
  end
29
27
 
@@ -10,10 +10,13 @@ module PmdTester
10
10
  attr_accessor :branch_last_sha
11
11
  attr_accessor :branch_last_message
12
12
  attr_accessor :branch_name
13
+ # Start of the regression report
14
+ attr_accessor :timestamp
13
15
  # The branch's execution time on all standard projects
14
16
  attr_accessor :execution_time
15
- attr_reader :jdk_version
16
- attr_reader :language
17
+ attr_accessor :jdk_version
18
+ attr_accessor :language
19
+ attr_accessor :pull_request
17
20
 
18
21
  def self.branch_filename(branch_name)
19
22
  branch_name&.tr('/', '_')
@@ -25,43 +28,56 @@ module PmdTester
25
28
  @branch_name = branch_name
26
29
  branch_filename = PmdBranchDetail.branch_filename(branch_name)
27
30
  @base_branch_dir = "target/reports/#{branch_filename}" unless @branch_name.nil?
31
+ @timestamp = Time.now
28
32
  @execution_time = 0
29
33
  # the result of command 'java -version' is going to stderr
30
34
  @jdk_version = Cmd.stderr_of('java -version')
31
- @language = ENV['LANG']
35
+ @language = ENV['LANG'] # the locale
36
+
37
+ prnum = ENV[PR_NUM_ENV_VAR]
38
+ @pull_request = prnum == 'false' ? nil : prnum
32
39
  end
33
40
 
34
- def load
35
- if File.exist?(branch_details_path)
36
- hash = JSON.parse(File.read(branch_details_path))
37
- @branch_last_sha = hash['branch_last_sha']
38
- @branch_last_message = hash['branch_last_message']
39
- @branch_name = hash['branch_name']
40
- @execution_time = hash['execution_time']
41
- @jdk_version = hash['jdk_version']
42
- @language = hash['language']
41
+ def self.load(branch_name, logger)
42
+ details = PmdBranchDetail.new(branch_name)
43
+ if File.exist?(details.path_to_save_file)
44
+ hash = JSON.parse(File.read(details.path_to_save_file))
45
+ details.branch_last_sha = hash['branch_last_sha']
46
+ details.branch_last_message = hash['branch_last_message']
47
+ details.branch_name = hash['branch_name']
48
+ details.timestamp = hash['timestamp']
49
+ details.execution_time = hash['execution_time']
50
+ details.jdk_version = hash['jdk_version']
51
+ details.language = hash['language']
52
+ details.pull_request = hash['pull_request']
43
53
  else
44
- @jdk_version = ''
45
- @language = ''
46
- logger.warn "#{branch_details_path} doesn't exist!"
54
+ details.timestamp = Time.now
55
+ details.jdk_version = ''
56
+ details.language = ''
57
+ logger&.warn "#{details.path_to_save_file} doesn't exist!"
47
58
  end
48
- self
59
+ details
49
60
  end
50
61
 
51
62
  def save
52
63
  hash = { branch_last_sha: @branch_last_sha,
53
64
  branch_last_message: @branch_last_message,
54
65
  branch_name: @branch_name,
66
+ timestamp: @timestamp,
55
67
  execution_time: @execution_time,
56
68
  jdk_version: @jdk_version,
57
- language: @language }
58
- file = File.new(branch_details_path, 'w')
69
+ language: @language,
70
+ pull_request: @pull_request }
71
+
72
+ FileUtils.mkdir_p(@base_branch_dir) unless File.directory?(@base_branch_dir)
73
+
74
+ file = File.new(path_to_save_file, 'w')
59
75
  file.puts JSON.generate(hash)
60
76
  file.close
61
77
  self
62
78
  end
63
79
 
64
- def branch_details_path
80
+ def path_to_save_file
65
81
  "#{@base_branch_dir}/branch_info.json"
66
82
  end
67
83
 
@@ -1,28 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PmdTester
4
- # This class is used to store pmd config errors and its size.
5
- class PmdConfigErrors
6
- attr_reader :errors
7
- attr_reader :size
8
-
9
- def initialize
10
- # key:rulename as String => value:PmdConfigError Array
11
- @errors = {}
12
- @size = 0
13
- end
14
-
15
- def add_error(error)
16
- rulename = error.rulename
17
- if @errors.key?(rulename)
18
- @errors[rulename].push(error)
19
- else
20
- @errors.store(rulename, [error])
21
- end
22
- @size += 1
23
- end
24
- end
25
-
26
4
  # This class represents a 'configerror' element of Pmd xml report
27
5
  # and which Pmd branch the 'configerror' is from
28
6
  class PmdConfigError
@@ -35,13 +13,13 @@ module PmdTester
35
13
  # <xs:attribute name="msg" type="xs:string" use="required" />
36
14
  # </xs:complexType>
37
15
  attr_reader :attrs
38
- attr_accessor :text
16
+ attr_accessor :old_error
39
17
 
40
18
  def initialize(attrs, branch)
41
19
  @attrs = attrs
42
20
 
21
+ @changed = false
43
22
  @branch = branch
44
- @text = ''
45
23
  end
46
24
 
47
25
  def rulename
@@ -52,10 +30,31 @@ module PmdTester
52
30
  @attrs['msg']
53
31
  end
54
32
 
33
+ def sort_key
34
+ rulename
35
+ end
36
+
37
+ def changed?
38
+ @changed
39
+ end
40
+
55
41
  def eql?(other)
56
42
  rulename.eql?(other.rulename) && msg.eql?(other.msg)
57
43
  end
58
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
+
59
58
  def hash
60
59
  [rulename, msg].hash
61
60
  end
@@ -1,30 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PmdTester
4
- # This class is used to store pmd errors and its size.
5
- class PmdErrors
6
- attr_reader :errors
7
- attr_reader :errors_size
8
-
9
- def initialize
10
- # key:filename as String => value:PmdError Array
11
- @errors = {}
12
- @errors_size = 0
13
- end
14
-
15
- def add_error_by_filename(filename, error)
16
- if @errors.key?(filename)
17
- @errors[filename].push(error)
18
- else
19
- @errors.store(filename, [error])
20
- end
21
- @errors_size += 1
22
- end
23
- end
24
-
25
4
  # This class represents a 'error' element of Pmd xml report
26
5
  # and which Pmd branch the 'error' is from
27
6
  class PmdError
7
+ include PmdTester
28
8
  # The pmd branch type, 'base' or 'patch'
29
9
  attr_reader :branch
30
10
 
@@ -37,31 +17,51 @@ module PmdTester
37
17
  # </xs:extension>
38
18
  # </xs:simpleContent>
39
19
  # </xs:complexType>
40
- attr_reader :attrs
41
- attr_accessor :text
42
-
43
- def initialize(attrs, branch)
44
- @attrs = attrs
20
+ attr_accessor :stack_trace
21
+ attr_accessor :old_error
22
+ attr_reader :filename, :short_message
45
23
 
24
+ def initialize(branch:, filename:, short_message:)
46
25
  @branch = branch
47
- @text = ''
26
+ @stack_trace = ''
27
+ @changed = false
28
+ @short_message = short_message
29
+ @filename = filename
48
30
  end
49
31
 
50
- def filename
51
- @attrs['filename']
32
+ def short_filename
33
+ filename.gsub(%r{([^/]*+/)+}, '')
52
34
  end
53
35
 
54
- def msg
55
- @attrs['msg']
36
+ def changed?
37
+ @changed
56
38
  end
57
39
 
58
40
  def eql?(other)
59
- filename.eql?(other.filename) && msg.eql?(other.msg) &&
60
- @text.eql?(other.text)
41
+ filename.eql?(other.filename) &&
42
+ short_message.eql?(other.short_message) &&
43
+ stack_trace.eql?(other.stack_trace)
61
44
  end
62
45
 
63
46
  def hash
64
- [filename, msg, @text].hash
47
+ [filename, stack_trace].hash
48
+ end
49
+
50
+ def sort_key
51
+ filename
52
+ end
53
+
54
+ def try_merge?(other)
55
+ if branch != BASE &&
56
+ branch != other.branch &&
57
+ filename == other.filename &&
58
+ !changed? # not already changed
59
+ @changed = true
60
+ @old_error = other
61
+ true
62
+ else
63
+ false
64
+ end
65
65
  end
66
66
  end
67
67
  end