pmdtester 1.0.0.pre.beta2

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.
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'slop'
4
+ require_relative '../pmdtester'
5
+
6
+ module PmdTester
7
+ class MissRequiredOptionError < StandardError; end
8
+ class InvalidModeError < StandardError; end
9
+
10
+ # The Options is a class responsible of parsing all the
11
+ # command line options
12
+ class Options
13
+ include PmdTester
14
+ ANY = 'any'
15
+ LOCAL = 'local'
16
+ ONLINE = 'online'
17
+ SINGLE = 'single'
18
+ VERSION = '1.0.0-beta2'
19
+
20
+ attr_reader :local_git_repo
21
+ attr_reader :base_branch
22
+ attr_reader :patch_branch
23
+ attr_accessor :base_config
24
+ attr_accessor :patch_config
25
+ attr_reader :config
26
+ attr_reader :project_list
27
+ attr_reader :mode
28
+ attr_reader :html_flag
29
+ attr_reader :auto_config_flag
30
+ attr_reader :debug_flag
31
+ attr_accessor :filter_set
32
+
33
+ def initialize(argv)
34
+ options = parse(argv)
35
+ @local_git_repo = options[:r]
36
+ @base_branch = options[:b]
37
+ @patch_branch = options[:p]
38
+ @base_config = options[:bc]
39
+ @patch_config = options[:pc]
40
+ @config = options[:c]
41
+ @project_list = options[:l]
42
+ @mode = options[:m]
43
+ @html_flag = options[:f]
44
+ @auto_config_flag = options[:a]
45
+ @debug_flag = options[:d]
46
+ @filter_set = nil
47
+
48
+ # if the 'config' option is selected then `config` overrides `base_config` and `patch_config`
49
+ @base_config = @config if !@config.nil? && @mode == 'local'
50
+ @patch_config = @config if !@config.nil? && @mode == 'local'
51
+
52
+ logger.level = @debug_flag ? Logger::DEBUG : Logger::INFO
53
+ check_options
54
+ end
55
+
56
+ private
57
+
58
+ def parse(argv)
59
+ mode_message = <<-DOC
60
+ the mode of the tool: 'local', 'online' or 'single'
61
+ single: Set this option to 'single' if your patch branch contains changes
62
+ for any option that can't work on master/base branch
63
+ 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'
66
+ DOC
67
+
68
+ Slop.parse argv do |o|
69
+ o.string '-r', '--local-git-repo', 'path to the local PMD repository'
70
+ o.string '-b', '--base-branch', 'name of the base branch in local PMD repository'
71
+ o.string '-p', '--patch-branch',
72
+ 'name of the patch branch in local PMD repository'
73
+ o.string '-bc', '--base-config', 'path to the base PMD configuration file'
74
+ o.string '-pc', '--patch-config', 'path to the patch PMD configuration file'
75
+ o.string '-c', '--config', 'path to the base and patch PMD configuration file'
76
+ o.string '-l', '--list-of-project',
77
+ 'path to the file which contains the list of standard projects'
78
+ o.string '-m', '--mode', mode_message, default: 'local'
79
+ o.bool '-f', '--html-flag',
80
+ 'whether to not generate the html diff report in single mode'
81
+ o.bool '-a', '--auto-gen-config',
82
+ 'whether to generate configurations automatically based on branch differences,' \
83
+ 'this option only works in online and local mode'
84
+ o.bool '-d', '--debug',
85
+ 'whether change log level to DEBUG to see more information'
86
+ o.on '-v', '--version' do
87
+ puts VERSION
88
+ exit
89
+ end
90
+ o.on '-h', '--help' do
91
+ puts o
92
+ exit
93
+ end
94
+ end
95
+ end
96
+
97
+ def check_options
98
+ check_common_options
99
+ case @mode
100
+ when LOCAL
101
+ check_local_options
102
+ when SINGLE
103
+ check_single_options
104
+ when ONLINE
105
+ check_online_options
106
+ else
107
+ msg = "The mode '#{@mode}' is invalid!"
108
+ logger.error msg
109
+ raise InvalidModeError, msg
110
+ end
111
+ end
112
+
113
+ def check_local_options
114
+ check_option(LOCAL, 'base branch name', @base_branch)
115
+ check_option(LOCAL, 'base branch config path', @base_config) unless @auto_config_flag
116
+ check_option(LOCAL, 'patch branch name', @patch_branch)
117
+ check_option(LOCAL, 'patch branch config path', @patch_config) unless @auto_config_flag
118
+ check_option(LOCAL, 'list of projects file path', @project_list)
119
+ end
120
+
121
+ def check_single_options
122
+ check_option(SINGLE, 'patch branch name', @patch_branch)
123
+ check_option(SINGLE, 'patch branch config path', @patch_config)
124
+ check_option(SINGLE, 'list of projects file path', @project_list)
125
+ end
126
+
127
+ def check_online_options
128
+ check_option(ONLINE, 'base branch name', @base_branch)
129
+ check_option(ONLINE, 'patch branch name', @patch_branch)
130
+ end
131
+
132
+ def check_common_options
133
+ check_option(ANY, 'local git repository path', @local_git_repo)
134
+ check_option(ANY, 'patch branch name', @patch_branch)
135
+ end
136
+
137
+ def check_option(mode, option_name, option)
138
+ if option.nil?
139
+ msg = "#{option_name} is required in #{mode} mode."
140
+ logger.error msg
141
+ raise MissRequiredOptionError, msg
142
+ else
143
+ logger.info "#{option_name}: #{option}"
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+ require_relative '../pmd_violation'
5
+ require_relative '../pmd_error'
6
+ module PmdTester
7
+ # This class is used for registering types of events you are interested in handling.
8
+ # Also see: https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/SAX/Document
9
+ class PmdReportDocument < Nokogiri::XML::SAX::Document
10
+ attr_reader :violations
11
+ attr_reader :errors
12
+ def initialize(branch_name, working_dir, filter_set = nil)
13
+ @violations = PmdViolations.new
14
+ @errors = PmdErrors.new
15
+ @current_violations = []
16
+ @current_violation = nil
17
+ @current_error = nil
18
+ @current_element = ''
19
+ @filename = ''
20
+ @filter_set = filter_set
21
+ @working_dir = working_dir
22
+ @branch_name = branch_name
23
+ end
24
+
25
+ def start_element(name, attrs = [])
26
+ attrs = attrs.to_h
27
+ @current_element = name
28
+
29
+ case name
30
+ when 'file'
31
+ @current_violations = []
32
+ @current_filename = remove_work_dir!(attrs['name'])
33
+ when 'violation'
34
+ @current_violation = PmdViolation.new(attrs, @branch_name)
35
+ when 'error'
36
+ @current_filename = remove_work_dir!(attrs['filename'])
37
+ remove_work_dir!(attrs['msg'])
38
+ @current_error = PmdError.new(attrs, @branch_name)
39
+ end
40
+ end
41
+
42
+ def remove_work_dir!(str)
43
+ str.sub!(/#{@working_dir}/, '')
44
+ end
45
+
46
+ def characters(string)
47
+ @current_violation.text = string unless @current_violation.nil?
48
+ end
49
+
50
+ def end_element(name)
51
+ case name
52
+ when 'file'
53
+ unless @current_violations.empty?
54
+ @violations.add_violations_by_filename(@current_filename, @current_violations)
55
+ end
56
+ @current_filename = nil
57
+ when 'violation'
58
+ @current_violations.push(@current_violation) if match_filter_set?(@current_violation)
59
+ @current_violation = nil
60
+ when 'error'
61
+ @errors.add_error_by_filename(@current_filename, @current_error)
62
+ @current_filename = nil
63
+ @current_error = nil
64
+ end
65
+ end
66
+
67
+ def cdata_block(string)
68
+ remove_work_dir!(string)
69
+ @current_error.text = string unless @current_error.nil?
70
+ end
71
+
72
+ def match_filter_set?(violation)
73
+ return true if @filter_set.nil?
74
+
75
+ @filter_set.each do |ruleset|
76
+ return true if ruleset.eql?(violation.attrs['ruleset'].delete(' ').downcase)
77
+ end
78
+
79
+ false
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+ require_relative '../project'
5
+ require_relative '../resource_locator'
6
+
7
+ module PmdTester
8
+ # The ProjectsParser is a class responsible of parsing
9
+ # the projects XML file to get the Project object array
10
+ class ProjectsParser
11
+ def parse(list_file)
12
+ schema = Nokogiri::XML::Schema(File.read(schema_file_path))
13
+ document = Nokogiri::XML(File.read(list_file))
14
+
15
+ errors = schema.validate(document)
16
+ unless errors.empty?
17
+ raise ProjectsParserException.new(errors), "Schema validate failed: In #{list_file}"
18
+ end
19
+
20
+ projects = []
21
+ document.xpath('//project').each do |project|
22
+ projects.push(Project.new(project))
23
+ end
24
+ projects
25
+ end
26
+
27
+ def schema_file_path
28
+ ResourceLocator.locate('config/projectlist_1_0_0.xsd')
29
+ end
30
+ end
31
+
32
+ # When this exception is raised, it means that
33
+ # schema validate of 'project-list' xml file failed
34
+ class ProjectsParserException < RuntimeError
35
+ attr_reader :errors
36
+
37
+ def initialize(errors)
38
+ @errors = errors
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative './pmd_report_detail'
5
+
6
+ module PmdTester
7
+ # This class represents all details about branch of pmd
8
+ class PmdBranchDetail
9
+ attr_accessor :branch_last_sha
10
+ attr_accessor :branch_last_message
11
+ attr_accessor :branch_name
12
+ # The branch's execution time on all standard projects
13
+ attr_accessor :execution_time
14
+
15
+ def self.branch_filename(branch_name)
16
+ branch_name&.tr('/', '_')
17
+ end
18
+
19
+ def initialize(branch_name)
20
+ @branch_last_sha = ''
21
+ @branch_last_message = ''
22
+ @branch_name = branch_name
23
+ branch_filename = PmdBranchDetail.branch_filename(branch_name)
24
+ @base_branch_dir = "target/reports/#{branch_filename}" unless @branch_name.nil?
25
+ @execution_time = 0
26
+ end
27
+
28
+ def load
29
+ if File.exist?(branch_details_path)
30
+ hash = JSON.parse(File.read(branch_details_path))
31
+ @branch_last_sha = hash['branch_last_sha']
32
+ @branch_last_message = hash['branch_last_message']
33
+ @branch_name = hash['branch_name']
34
+ @execution_time = hash['execution_time']
35
+ hash
36
+ else
37
+ {}
38
+ end
39
+ end
40
+
41
+ def save
42
+ hash = { branch_last_sha: @branch_last_sha,
43
+ branch_last_message: @branch_last_message,
44
+ branch_name: @branch_name,
45
+ execution_time: @execution_time }
46
+ file = File.new(branch_details_path, 'w')
47
+ file.puts JSON.generate(hash)
48
+ file.close
49
+ end
50
+
51
+ def branch_details_path
52
+ "#{@base_branch_dir}/branch_info.json"
53
+ end
54
+
55
+ def target_branch_config_path
56
+ "#{@base_branch_dir}/config.xml"
57
+ end
58
+
59
+ def target_branch_project_list_path
60
+ "#{@base_branch_dir}/project-list.xml"
61
+ end
62
+
63
+ def format_execution_time
64
+ PmdReportDetail.convert_seconds(@execution_time)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
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
+ # This class represents a 'error' element of Pmd xml report
26
+ # and which Pmd branch the 'error' is from
27
+ class PmdError
28
+ # The pmd branch type, 'base' or 'patch'
29
+ attr_reader :branch
30
+
31
+ # The schema of 'error' node:
32
+ # <xs:complexType name="error">
33
+ # <xs:simpleContent>
34
+ # <xs:extension base="xs:string">
35
+ # <xs:attribute name="filename" type="xs:string" use="required"/>
36
+ # <xs:attribute name="msg" type="xs:string" use="required"/>
37
+ # </xs:extension>
38
+ # </xs:simpleContent>
39
+ # </xs:complexType>
40
+ attr_reader :attrs
41
+ attr_accessor :text
42
+
43
+ def initialize(attrs, branch)
44
+ @attrs = attrs
45
+
46
+ @branch = branch
47
+ @text = ''
48
+ end
49
+
50
+ def filename
51
+ @attrs['filename']
52
+ end
53
+
54
+ def msg
55
+ @attrs['msg']
56
+ end
57
+
58
+ def eql?(other)
59
+ filename.eql?(other.filename) && msg.eql?(other.msg) &&
60
+ @text.eql?(other.text)
61
+ end
62
+
63
+ def hash
64
+ [filename, msg, @text].hash
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module PmdTester
6
+ # This class represents all details about report of pmd
7
+ class PmdReportDetail
8
+ attr_accessor :execution_time
9
+ attr_accessor :timestamp
10
+ attr_reader :working_dir
11
+
12
+ def initialize
13
+ @execution_time = 0
14
+ @timestamp = ''
15
+ @working_dir = Dir.getwd
16
+ end
17
+
18
+ def save(report_info_path)
19
+ hash = { execution_time: @execution_time, timestamp: @timestamp, working_dir: @working_dir }
20
+ file = File.new(report_info_path, 'w')
21
+ file.puts JSON.generate(hash)
22
+ file.close
23
+ end
24
+
25
+ def load(report_info_path)
26
+ if File.exist?(report_info_path)
27
+ hash = JSON.parse(File.read(report_info_path))
28
+ @execution_time = hash['execution_time']
29
+ @timestamp = hash['timestamp']
30
+ @working_dir = hash['working_dir']
31
+ hash
32
+ else
33
+ puts "#{report_info_path} doesn't exist"
34
+ {}
35
+ end
36
+ end
37
+
38
+ def format_execution_time
39
+ self.class.convert_seconds(@execution_time)
40
+ end
41
+
42
+ # convert seconds into HH::MM::SS
43
+ def self.convert_seconds(seconds)
44
+ Time.at(seconds.abs).utc.strftime('%H:%M:%S')
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PmdTester
4
+ # This class is used to store pmd violations and its size.
5
+ class PmdViolations
6
+ attr_reader :violations
7
+ attr_reader :violations_size
8
+
9
+ def initialize
10
+ # key:filename as String => value:PmdViolation Array
11
+ @violations = {}
12
+ @violations_size = 0
13
+ end
14
+
15
+ def add_violations_by_filename(filename, violations)
16
+ @violations.store(filename, violations)
17
+ @violations_size += violations.size
18
+ end
19
+ end
20
+
21
+ # This class represents a 'violation' element of Pmd xml report
22
+ # and which pmd branch the 'violation' is from
23
+ class PmdViolation
24
+ # The pmd branch type, 'base' or 'patch'
25
+ attr_reader :branch
26
+
27
+ # The schema of 'violation' element:
28
+ # <xs:complexType name="violation">
29
+ # <xs:simpleContent>
30
+ # <xs:extension base="xs:string">
31
+ # <xs:attribute name="beginline" type="xs:integer" use="required" />
32
+ # <xs:attribute name="endline" type="xs:integer" use="required" />
33
+ # <xs:attribute name="begincolumn" type="xs:integer" use="required" />
34
+ # <xs:attribute name="endcolumn" type="xs:integer" use="required" />
35
+ # <xs:attribute name="rule" type="xs:string" use="required" />
36
+ # <xs:attribute name="ruleset" type="xs:string" use="required" />
37
+ # <xs:attribute name="package" type="xs:string" use="optional" />
38
+ # <xs:attribute name="class" type="xs:string" use="optional" />
39
+ # <xs:attribute name="method" type="xs:string" use="optional" />
40
+ # <xs:attribute name="variable" type="xs:string" use="optional" />
41
+ # <xs:attribute name="externalInfoUrl" type="xs:string" use="optional" />
42
+ # <xs:attribute name="priority" type="xs:string" use="required" />
43
+ # </xs:extension>
44
+ # </xs:simpleContent>
45
+ # </xs:complexType>
46
+
47
+ attr_reader :attrs
48
+ attr_accessor :text
49
+
50
+ def initialize(attrs, branch)
51
+ @attrs = attrs
52
+ @branch = branch
53
+ @text = ''
54
+ end
55
+
56
+ def eql?(other)
57
+ @attrs['beginline'].eql?(other.attrs['beginline']) &&
58
+ @attrs['rule'].eql?(other.attrs['rule']) &&
59
+ @text.eql?(other.text)
60
+ end
61
+
62
+ def hash
63
+ [@attrs['beginline'], @attrs['rule'], @text].hash
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ # PmdTester is a regression testing tool ensure that new problems
6
+ # and unexpected behaviors will not be introduced to PMD project
7
+ # after fixing an issue , and new rules can work as expected.
8
+ module PmdTester
9
+ def logger
10
+ PmdTester.logger
11
+ end
12
+
13
+ # Global, memoized, lazy initialized instance of a logger
14
+ def self.logger
15
+ @logger ||= Logger.new(STDOUT)
16
+ end
17
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './pmd_branch_detail'
4
+
5
+ module PmdTester
6
+ # This class represents all the information about the project
7
+ class Project
8
+ REPOSITORIES_PATH = 'target/repositories'
9
+
10
+ attr_reader :name
11
+ attr_reader :type
12
+ attr_reader :connection
13
+ attr_reader :webview_url
14
+ attr_reader :tag
15
+ attr_reader :exclude_pattern
16
+ attr_accessor :report_diff
17
+ # key: pmd branch name as String => value: local path of pmd report
18
+
19
+ def initialize(project)
20
+ @name = project.at_xpath('name').text
21
+ @type = project.at_xpath('type').text
22
+ @connection = project.at_xpath('connection').text
23
+
24
+ @tag = 'master'
25
+ tag_element = project.at_xpath('tag')
26
+ @tag = tag_element.text unless tag_element.nil?
27
+
28
+ webview_url_element = project.at_xpath('webview-url')
29
+ @webview_url = default_webview_url
30
+ @webview_url = webview_url_element.text unless webview_url_element.nil?
31
+
32
+ @exclude_pattern = []
33
+ project.xpath('exclude-pattern').each do |ep|
34
+ @exclude_pattern.push(ep.text)
35
+ end
36
+
37
+ @report_diff = nil
38
+ end
39
+
40
+ # Generate the default webview url for the projects
41
+ # stored on github.
42
+ # For other projects return value is `connection`.
43
+ def default_webview_url
44
+ if @type.eql?('git') && @connection.include?('github.com')
45
+ "#{@connection}/tree/#{@tag}"
46
+ else
47
+ @connection
48
+ end
49
+ end
50
+
51
+ # Change the file path from 'LOCAL_DIR/SOURCE_CODE_PATH' to
52
+ # 'WEB_VIEW_URL/SOURCE_CODE_PATH'
53
+ def get_webview_url(file_path)
54
+ file_path.gsub(%r{/#{local_source_path}}, @webview_url)
55
+ end
56
+
57
+ # Change the file path from 'LOCAL_DIR/SOURCE_CODE_PATH' to
58
+ # 'PROJECT_NAME/SOURCE_CODE_PATH'
59
+ def get_path_inside_project(file_path)
60
+ file_path.gsub(%r{/#{local_source_path}}, @name)
61
+ end
62
+
63
+ def get_pmd_report_path(branch_name)
64
+ if branch_name.nil?
65
+ nil
66
+ else
67
+ "#{get_project_target_dir(branch_name)}/pmd_report.xml"
68
+ end
69
+ end
70
+
71
+ def get_report_info_path(branch_name)
72
+ if branch_name.nil?
73
+ nil
74
+ else
75
+ "#{get_project_target_dir(branch_name)}/report_info.json"
76
+ end
77
+ end
78
+
79
+ def get_project_target_dir(branch_name)
80
+ branch_filename = PmdBranchDetail.branch_filename(branch_name)
81
+ dir = "target/reports/#{branch_filename}/#{@name}"
82
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
83
+ dir
84
+ end
85
+
86
+ def local_source_path
87
+ "#{REPOSITORIES_PATH}/#{@name}"
88
+ end
89
+
90
+ def target_diff_report_path
91
+ dir = "target/reports/diff/#{@name}"
92
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
93
+ dir
94
+ end
95
+
96
+ def diff_report_index_path
97
+ "#{target_diff_report_path}/index.html"
98
+ end
99
+
100
+ def diff_report_index_ref_path
101
+ "./#{name}/index.html"
102
+ end
103
+
104
+ def diffs_exist?
105
+ @report_diff.diffs_exist?
106
+ end
107
+
108
+ def introduce_new_errors?
109
+ @report_diff.introduce_new_errors?
110
+ end
111
+ end
112
+ end