pmdtester 1.0.0.pre.beta2

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