pmdtester 1.6.2 → 1.7.0
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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +12 -0
- data/.github/workflows/build.yml +7 -7
- data/.github/workflows/manual-integration-tests.yml +7 -7
- data/.github/workflows/publish-release.yml +9 -9
- data/.hoerc +1 -1
- data/.rubocop_todo.yml +1 -14
- data/.vscode/launch.json +32 -0
- data/History.md +47 -0
- data/Manifest.txt +13 -0
- data/README.rdoc +76 -30
- data/Rakefile +11 -11
- data/config/custom.jfc +1126 -0
- data/config/project-list-with-cpd.xml +268 -0
- data/config/projectlist_1_2_0.xsd +1 -1
- data/config/projectlist_1_3_0.xsd +53 -0
- data/lib/pmdtester/builders/cpd_project_hasher.rb +70 -0
- data/lib/pmdtester/builders/liquid_renderer.rb +111 -16
- data/lib/pmdtester/builders/pmd_report_builder.rb +133 -37
- data/lib/pmdtester/builders/project_hasher.rb +24 -25
- data/lib/pmdtester/builders/rule_set_builder.rb +2 -0
- data/lib/pmdtester/builders/summary_report_builder.rb +6 -1
- data/lib/pmdtester/cmd.rb +16 -7
- data/lib/pmdtester/cpd_report_diff.rb +99 -0
- data/lib/pmdtester/jfr_summary.rb +119 -0
- data/lib/pmdtester/location.rb +38 -0
- data/lib/pmdtester/parsers/cpd_report_document.rb +241 -0
- data/lib/pmdtester/parsers/options.rb +19 -0
- data/lib/pmdtester/parsers/pmd_report_document.rb +14 -1
- data/lib/pmdtester/parsers/projects_parser.rb +1 -1
- data/lib/pmdtester/pmd_branch_detail.rb +29 -9
- data/lib/pmdtester/pmd_report_detail.rb +54 -13
- data/lib/pmdtester/pmd_tester_utils.rb +45 -17
- data/lib/pmdtester/pmd_violation.rb +15 -6
- data/lib/pmdtester/project.rb +63 -3
- data/lib/pmdtester/report_diff.rb +5 -13
- data/lib/pmdtester/runner.rb +185 -37
- data/lib/pmdtester/system_info.rb +58 -0
- data/lib/pmdtester/word_differ.rb +132 -0
- data/lib/pmdtester.rb +8 -1
- data/pmdtester.gemspec +17 -17
- data/resources/css/pmd-tester.css +15 -0
- data/resources/js/project-report.js +293 -112
- data/resources/project_cpd_report.html +144 -0
- data/resources/project_diff_report.html +151 -18
- data/resources/project_index.html +12 -3
- data/resources/project_pmd_report.html +17 -2
- metadata +63 -43
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PmdTester
|
|
4
|
+
# This class represents a location in a source file
|
|
5
|
+
class Location
|
|
6
|
+
attr_reader :beginline, :endline, :begincolumn, :endcolumn
|
|
7
|
+
|
|
8
|
+
def initialize(beginline:, endline:, begincolumn:, endcolumn:)
|
|
9
|
+
@beginline = beginline
|
|
10
|
+
@endline = endline
|
|
11
|
+
@begincolumn = begincolumn
|
|
12
|
+
@endcolumn = endcolumn
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def eql?(other)
|
|
16
|
+
beginline == other.beginline &&
|
|
17
|
+
endline == other.endline &&
|
|
18
|
+
begincolumn == other.begincolumn &&
|
|
19
|
+
endcolumn == other.endcolumn
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def hash
|
|
23
|
+
[beginline, endline, begincolumn, endcolumn].hash
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_s
|
|
27
|
+
if beginline == endline
|
|
28
|
+
if begincolumn == endcolumn
|
|
29
|
+
"#{beginline}:#{begincolumn}"
|
|
30
|
+
else
|
|
31
|
+
"#{beginline}:#{begincolumn}-#{endcolumn}"
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
"#{beginline}:#{begincolumn}-#{endline}:#{endcolumn}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'nokogiri'
|
|
4
|
+
module PmdTester
|
|
5
|
+
# Parses a CPD report XML file into a set of duplications and errors
|
|
6
|
+
class CpdReportDocument < Nokogiri::XML::SAX::Document
|
|
7
|
+
attr_reader :duplications, :errors
|
|
8
|
+
|
|
9
|
+
def initialize(branch_name, working_dir)
|
|
10
|
+
super()
|
|
11
|
+
@duplications = []
|
|
12
|
+
@errors = []
|
|
13
|
+
|
|
14
|
+
@working_dir = working_dir
|
|
15
|
+
@branch_name = branch_name
|
|
16
|
+
|
|
17
|
+
@current_duplication = nil
|
|
18
|
+
@current_error = nil
|
|
19
|
+
@cur_text = String.new(capacity: 200)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def parse(file_path)
|
|
23
|
+
parser = Nokogiri::XML::SAX::Parser.new(self)
|
|
24
|
+
parser.parse(File.open(file_path)) if File.exist?(file_path)
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def start_element(name, attrs = [])
|
|
29
|
+
attrs = attrs.to_h
|
|
30
|
+
|
|
31
|
+
case name
|
|
32
|
+
when 'duplication'
|
|
33
|
+
handle_start_duplication attrs
|
|
34
|
+
when 'file'
|
|
35
|
+
handle_start_file attrs
|
|
36
|
+
when 'codefragment'
|
|
37
|
+
handle_start_codefragment
|
|
38
|
+
when 'error'
|
|
39
|
+
handle_start_error attrs
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def characters(string)
|
|
44
|
+
@cur_text << string
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def cdata_block(string)
|
|
48
|
+
@cur_text << string
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def end_element(name)
|
|
52
|
+
case name
|
|
53
|
+
when 'duplication'
|
|
54
|
+
handle_end_duplication
|
|
55
|
+
when 'codefragment'
|
|
56
|
+
handle_end_codefragment
|
|
57
|
+
when 'error'
|
|
58
|
+
handle_end_error
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# Modifies the string in place and returns it
|
|
65
|
+
# (this is what sub! does, except it returns nil if no replacement occurred)
|
|
66
|
+
def remove_work_dir!(str)
|
|
67
|
+
str.sub!(%r{#{@working_dir}/}, '')
|
|
68
|
+
str
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def finish_text!
|
|
72
|
+
remove_work_dir!(@cur_text)
|
|
73
|
+
res = @cur_text.dup.freeze
|
|
74
|
+
@cur_text.clear
|
|
75
|
+
res
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def handle_start_duplication(attrs)
|
|
79
|
+
@current_duplication = { lines: attrs['lines'].to_i,
|
|
80
|
+
tokens: attrs['tokens'].to_i,
|
|
81
|
+
files: [],
|
|
82
|
+
codefragment: '',
|
|
83
|
+
branch: @branch_name }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def handle_end_duplication
|
|
87
|
+
@duplications << Duplication.new(
|
|
88
|
+
lines: @current_duplication[:lines],
|
|
89
|
+
tokens: @current_duplication[:tokens],
|
|
90
|
+
files: @current_duplication[:files],
|
|
91
|
+
codefragment: @current_duplication[:codefragment],
|
|
92
|
+
branch: @current_duplication[:branch]
|
|
93
|
+
)
|
|
94
|
+
@current_duplication = nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def handle_start_file(attrs)
|
|
98
|
+
return if @current_duplication.nil?
|
|
99
|
+
|
|
100
|
+
file_info = DuplicationFileInfo.new(
|
|
101
|
+
path: remove_work_dir!(attrs['path']),
|
|
102
|
+
location: Location.new(
|
|
103
|
+
beginline: attrs['line'].to_i,
|
|
104
|
+
endline: attrs['endline'].to_i,
|
|
105
|
+
begincolumn: attrs['column'].to_i,
|
|
106
|
+
endcolumn: attrs['endcolumn'].to_i
|
|
107
|
+
),
|
|
108
|
+
begintoken: attrs['begintoken'].to_i,
|
|
109
|
+
endtoken: attrs['endtoken'].to_i
|
|
110
|
+
)
|
|
111
|
+
@current_duplication[:files] << file_info
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def handle_start_codefragment
|
|
115
|
+
@cur_text.clear
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def handle_end_codefragment
|
|
119
|
+
@current_duplication[:codefragment] = finish_text!
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def handle_start_error(attrs)
|
|
123
|
+
@current_error = PmdError.new(
|
|
124
|
+
filename: remove_work_dir!(attrs['filename']),
|
|
125
|
+
short_message: remove_work_dir!(attrs['msg']),
|
|
126
|
+
branch: @branch_name
|
|
127
|
+
)
|
|
128
|
+
@cur_text.clear
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def handle_end_error
|
|
132
|
+
@current_error.stack_trace = finish_text!
|
|
133
|
+
@errors << @current_error
|
|
134
|
+
@current_error = nil
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Represents a duplication entry in a CPD report
|
|
139
|
+
class Duplication
|
|
140
|
+
attr_reader :lines, :tokens, :files, :codefragment, :branch, :old_lines, :old_tokens, :old_files, :old_codefragment
|
|
141
|
+
|
|
142
|
+
def initialize(lines:, tokens:, files:, codefragment:, branch:)
|
|
143
|
+
@lines = lines
|
|
144
|
+
@tokens = tokens
|
|
145
|
+
@files = files
|
|
146
|
+
@codefragment = codefragment
|
|
147
|
+
@branch = branch
|
|
148
|
+
|
|
149
|
+
@changed = false
|
|
150
|
+
@old_lines = nil
|
|
151
|
+
@old_tokens = nil
|
|
152
|
+
@old_files = nil
|
|
153
|
+
@old_codefragment = nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def eql?(other)
|
|
157
|
+
return false unless other.is_a?(Duplication)
|
|
158
|
+
|
|
159
|
+
lines == other.lines &&
|
|
160
|
+
tokens == other.tokens &&
|
|
161
|
+
files.eql?(other.files) &&
|
|
162
|
+
codefragment == other.codefragment
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def hash
|
|
166
|
+
[lines, tokens, files, codefragment].hash
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def try_merge?(other)
|
|
170
|
+
if branch != BASE && branch != other.branch &&
|
|
171
|
+
!changed? && # not already changed
|
|
172
|
+
same_or_similar_locations?(other.files)
|
|
173
|
+
@changed = true
|
|
174
|
+
@old_lines = other.lines
|
|
175
|
+
@old_tokens = other.tokens
|
|
176
|
+
@old_files = other.files
|
|
177
|
+
@old_codefragment = other.codefragment
|
|
178
|
+
true
|
|
179
|
+
else
|
|
180
|
+
false
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# only makes sense if this is a diff
|
|
185
|
+
def added?
|
|
186
|
+
branch != BASE && !changed?
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# only makes sense if this is a diff
|
|
190
|
+
def changed?
|
|
191
|
+
@changed
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# only makes sense if this is a diff
|
|
195
|
+
def removed?
|
|
196
|
+
branch == BASE
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
private
|
|
200
|
+
|
|
201
|
+
def same_or_similar_locations?(other_files)
|
|
202
|
+
files.each do |file_info|
|
|
203
|
+
other_files.each do |other_file_info|
|
|
204
|
+
return true if file_info.path == other_file_info.path &&
|
|
205
|
+
(file_info.location.eql?(other_file_info.location) ||
|
|
206
|
+
location_move?(file_info.location, other_file_info.location))
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
false
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def location_move?(this_location, other_location)
|
|
213
|
+
(this_location.beginline - other_location.beginline).abs <= 5
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Represents a single file location of a duplication in a CPD report
|
|
218
|
+
class DuplicationFileInfo
|
|
219
|
+
attr_reader :path, :location, :begintoken, :endtoken
|
|
220
|
+
|
|
221
|
+
def initialize(path:, location:, begintoken:, endtoken:)
|
|
222
|
+
@path = path
|
|
223
|
+
@location = location
|
|
224
|
+
@begintoken = begintoken
|
|
225
|
+
@endtoken = endtoken
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def eql?(other)
|
|
229
|
+
return false unless other.is_a?(DuplicationFileInfo)
|
|
230
|
+
|
|
231
|
+
path == other.path &&
|
|
232
|
+
location.eql?(other.location) &&
|
|
233
|
+
begintoken == other.begintoken &&
|
|
234
|
+
endtoken == other.endtoken
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def hash
|
|
238
|
+
[path, location, begintoken, endtoken].hash
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
@@ -5,6 +5,7 @@ require 'slop'
|
|
|
5
5
|
module PmdTester
|
|
6
6
|
class MissRequiredOptionError < StandardError; end
|
|
7
7
|
class InvalidModeError < StandardError; end
|
|
8
|
+
class InvalidOptionError < StandardError; end
|
|
8
9
|
|
|
9
10
|
# The Options is a class responsible of parsing all the
|
|
10
11
|
# command line options
|
|
@@ -36,6 +37,8 @@ module PmdTester
|
|
|
36
37
|
attr_reader :keep_reports
|
|
37
38
|
attr_reader :error_recovery
|
|
38
39
|
attr_reader :baseline_download_url_prefix
|
|
40
|
+
attr_reader :run_cpd
|
|
41
|
+
attr_reader :run_pmd
|
|
39
42
|
|
|
40
43
|
def initialize(argv)
|
|
41
44
|
options = parse(argv)
|
|
@@ -56,6 +59,8 @@ module PmdTester
|
|
|
56
59
|
@keep_reports = options.keep_reports?
|
|
57
60
|
@error_recovery = options.error_recovery?
|
|
58
61
|
url = options[:baseline_download_url]
|
|
62
|
+
@run_cpd = !options.no_cpd?
|
|
63
|
+
@run_pmd = !options.no_pmd?
|
|
59
64
|
@baseline_download_url_prefix = if url[-1] == '/'
|
|
60
65
|
url
|
|
61
66
|
else
|
|
@@ -116,6 +121,10 @@ module PmdTester
|
|
|
116
121
|
o.string '--baseline-download-url',
|
|
117
122
|
'download url prefix from where to download the baseline in online mode',
|
|
118
123
|
default: DEFAULT_BASELINE_URL_PREFIX
|
|
124
|
+
o.bool '--no-cpd', 'do not execute CPD (Copy Paste Detector) and compare duplications; only execute PMD',
|
|
125
|
+
default: false
|
|
126
|
+
o.bool '--no-pmd', 'do not execute PMD and compare rule violations; only execute CPD (Copy Paste Detector)',
|
|
127
|
+
default: false
|
|
119
128
|
o.on '-v', '--version' do
|
|
120
129
|
puts VERSION
|
|
121
130
|
exit
|
|
@@ -129,6 +138,7 @@ module PmdTester
|
|
|
129
138
|
|
|
130
139
|
def check_options
|
|
131
140
|
check_common_options
|
|
141
|
+
check_pmd_cpd_options
|
|
132
142
|
case @mode
|
|
133
143
|
when LOCAL
|
|
134
144
|
check_local_options
|
|
@@ -164,6 +174,15 @@ module PmdTester
|
|
|
164
174
|
check_option(ANY, 'patch branch name', @patch_branch)
|
|
165
175
|
end
|
|
166
176
|
|
|
177
|
+
def check_pmd_cpd_options
|
|
178
|
+
return unless !@run_pmd && !@run_cpd
|
|
179
|
+
|
|
180
|
+
msg = 'Both "--no-cpd" and "--no-pmd" are given. At least one of PMD and CPD must be executed. ' \
|
|
181
|
+
'Please check your options.'
|
|
182
|
+
logger.error msg
|
|
183
|
+
raise InvalidOptionError, msg
|
|
184
|
+
end
|
|
185
|
+
|
|
167
186
|
def check_option(mode, option_name, option)
|
|
168
187
|
if option.nil?
|
|
169
188
|
msg = "#{option_name} is required in #{mode} mode."
|
|
@@ -10,6 +10,7 @@ module PmdTester
|
|
|
10
10
|
attr_reader :configerrors
|
|
11
11
|
|
|
12
12
|
def initialize(branch_name, working_dir, filter_set = nil)
|
|
13
|
+
super()
|
|
13
14
|
@violations = CollectionByFile.new
|
|
14
15
|
@errors = CollectionByFile.new
|
|
15
16
|
@configerrors = Hash.new { |hash, key| hash[key] = [] }
|
|
@@ -25,6 +26,12 @@ module PmdTester
|
|
|
25
26
|
@cur_text = String.new(capacity: 200)
|
|
26
27
|
end
|
|
27
28
|
|
|
29
|
+
def parse(file_path)
|
|
30
|
+
parser = Nokogiri::XML::SAX::Parser.new(self)
|
|
31
|
+
parser.parse(File.open(file_path)) if File.exist?(file_path)
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
28
35
|
def start_element(name, attrs = [])
|
|
29
36
|
attrs = attrs.to_h
|
|
30
37
|
|
|
@@ -109,7 +116,13 @@ module PmdTester
|
|
|
109
116
|
info_url: attrs['externalInfoUrl'],
|
|
110
117
|
bline: attrs['beginline'].to_i,
|
|
111
118
|
rule_name: attrs['rule'],
|
|
112
|
-
ruleset_name: attrs['ruleset'].freeze
|
|
119
|
+
ruleset_name: attrs['ruleset'].freeze,
|
|
120
|
+
location: Location.new(
|
|
121
|
+
beginline: attrs['beginline'].to_i,
|
|
122
|
+
endline: attrs['endline'].to_i,
|
|
123
|
+
begincolumn: attrs['begincolumn'].to_i,
|
|
124
|
+
endcolumn: attrs['endcolumn'].to_i
|
|
125
|
+
)
|
|
113
126
|
)
|
|
114
127
|
end
|
|
115
128
|
|
|
@@ -16,6 +16,9 @@ module PmdTester
|
|
|
16
16
|
attr_accessor :execution_time
|
|
17
17
|
attr_accessor :jdk_version
|
|
18
18
|
attr_accessor :language
|
|
19
|
+
attr_accessor :cpu_info
|
|
20
|
+
attr_accessor :physical_memory
|
|
21
|
+
attr_accessor :os_info
|
|
19
22
|
attr_accessor :pull_request
|
|
20
23
|
|
|
21
24
|
def self.branch_filename(branch_name)
|
|
@@ -33,6 +36,10 @@ module PmdTester
|
|
|
33
36
|
# the result of command 'java -version' is going to stderr
|
|
34
37
|
@jdk_version = Cmd.stderr_of('java -version')
|
|
35
38
|
@language = ENV.fetch('LANG') # the locale
|
|
39
|
+
system_info = PmdTester::SystemInfo.new
|
|
40
|
+
@cpu_info = system_info.cpu_info
|
|
41
|
+
@physical_memory = system_info.physical_memory
|
|
42
|
+
@os_info = system_info.uname
|
|
36
43
|
|
|
37
44
|
prnum = ENV.fetch(PR_NUM_ENV_VAR, 'false')
|
|
38
45
|
@pull_request = prnum == 'false' ? nil : prnum
|
|
@@ -41,19 +48,14 @@ module PmdTester
|
|
|
41
48
|
def self.load(branch_name, logger)
|
|
42
49
|
details = PmdBranchDetail.new(branch_name)
|
|
43
50
|
if File.exist?(details.path_to_save_file)
|
|
44
|
-
|
|
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']
|
|
51
|
+
details.parse_from_json
|
|
53
52
|
else
|
|
54
53
|
details.timestamp = Time.now
|
|
55
54
|
details.jdk_version = ''
|
|
56
55
|
details.language = ''
|
|
56
|
+
details.cpu_info = ''
|
|
57
|
+
details.physical_memory = ''
|
|
58
|
+
details.os_info = ''
|
|
57
59
|
logger&.warn "#{details.path_to_save_file} doesn't exist!"
|
|
58
60
|
end
|
|
59
61
|
details
|
|
@@ -66,6 +68,9 @@ module PmdTester
|
|
|
66
68
|
timestamp: @timestamp,
|
|
67
69
|
execution_time: @execution_time,
|
|
68
70
|
jdk_version: @jdk_version,
|
|
71
|
+
cpu_info: @cpu_info,
|
|
72
|
+
physical_memory: @physical_memory,
|
|
73
|
+
os_info: @os_info,
|
|
69
74
|
language: @language,
|
|
70
75
|
pull_request: @pull_request }
|
|
71
76
|
|
|
@@ -92,5 +97,20 @@ module PmdTester
|
|
|
92
97
|
def format_execution_time
|
|
93
98
|
PmdReportDetail.convert_seconds(@execution_time)
|
|
94
99
|
end
|
|
100
|
+
|
|
101
|
+
def parse_from_json
|
|
102
|
+
hash = JSON.parse(File.read(path_to_save_file))
|
|
103
|
+
@branch_last_sha = hash['branch_last_sha']
|
|
104
|
+
@branch_last_message = hash['branch_last_message']
|
|
105
|
+
@branch_name = hash['branch_name']
|
|
106
|
+
@timestamp = hash['timestamp']
|
|
107
|
+
@execution_time = hash['execution_time']
|
|
108
|
+
@jdk_version = hash['jdk_version']
|
|
109
|
+
@language = hash['language']
|
|
110
|
+
@cpu_info = hash['cpu_info']
|
|
111
|
+
@physical_memory = hash['physical_memory']
|
|
112
|
+
@os_info = hash['os_info']
|
|
113
|
+
@pull_request = hash['pull_request']
|
|
114
|
+
end
|
|
95
115
|
end
|
|
96
116
|
end
|
|
@@ -3,29 +3,32 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
|
|
5
5
|
module PmdTester
|
|
6
|
-
# This class represents all details about
|
|
6
|
+
# This class represents all details about an execution of PMD
|
|
7
7
|
class PmdReportDetail
|
|
8
8
|
attr_accessor :execution_time
|
|
9
9
|
attr_accessor :timestamp
|
|
10
10
|
attr_accessor :working_dir
|
|
11
|
+
attr_accessor :cmdline
|
|
11
12
|
attr_accessor :exit_code
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@working_dir = working_dir
|
|
17
|
-
@exit_code = exit_code.nil? ? '?' : exit_code.to_s
|
|
18
|
-
end
|
|
13
|
+
attr_accessor :stdout
|
|
14
|
+
attr_accessor :stderr
|
|
15
|
+
attr_accessor :oom
|
|
16
|
+
attr_accessor :jfr_summary
|
|
19
17
|
|
|
20
18
|
def save(report_info_path)
|
|
21
19
|
hash = {
|
|
22
20
|
execution_time: @execution_time,
|
|
23
21
|
timestamp: @timestamp,
|
|
24
22
|
working_dir: @working_dir,
|
|
25
|
-
|
|
23
|
+
cmdline: @cmdline,
|
|
24
|
+
exit_code: @exit_code,
|
|
25
|
+
stdout: @stdout,
|
|
26
|
+
stderr: @stderr,
|
|
27
|
+
oom: @oom,
|
|
28
|
+
jfr_summary: @jfr_summary.to_h
|
|
26
29
|
}
|
|
27
30
|
file = File.new(report_info_path, 'w')
|
|
28
|
-
file.puts JSON.
|
|
31
|
+
file.puts JSON.pretty_generate(hash)
|
|
29
32
|
file.close
|
|
30
33
|
end
|
|
31
34
|
|
|
@@ -39,20 +42,58 @@ module PmdTester
|
|
|
39
42
|
end
|
|
40
43
|
end
|
|
41
44
|
|
|
42
|
-
def
|
|
45
|
+
def execution_time_formatted
|
|
43
46
|
self.class.convert_seconds(@execution_time)
|
|
44
47
|
end
|
|
45
48
|
|
|
46
|
-
def
|
|
49
|
+
def to_h
|
|
50
|
+
{
|
|
51
|
+
'timestamp' => @timestamp,
|
|
52
|
+
'exit_code' => @exit_code,
|
|
53
|
+
'cmdline' => @cmdline,
|
|
54
|
+
'execution_time' => execution_time_formatted,
|
|
55
|
+
'oom' => @oom,
|
|
56
|
+
'jfr_summary' => @jfr_summary.to_h_for_liquid
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.create(execution_time: 0, timestamp: '', working_dir: Dir.getwd, cmdline: '',
|
|
61
|
+
exit_code: nil, stdout: '', stderr: '', oom: false,
|
|
62
|
+
report_info_path:, jfr_summary: nil)
|
|
47
63
|
detail = PmdReportDetail.new(execution_time: execution_time, timestamp: timestamp,
|
|
48
|
-
working_dir: working_dir,
|
|
64
|
+
working_dir: working_dir, cmdline: cmdline, oom: oom,
|
|
65
|
+
exit_code: exit_code, stdout: stdout, stderr: stderr, jfr_summary: jfr_summary)
|
|
49
66
|
detail.save(report_info_path)
|
|
50
67
|
detail
|
|
51
68
|
end
|
|
52
69
|
|
|
70
|
+
def self.empty
|
|
71
|
+
new(execution_time: 0, timestamp: '', working_dir: Dir.getwd, cmdline: '',
|
|
72
|
+
exit_code: nil, stdout: '', stderr: '', oom: false)
|
|
73
|
+
end
|
|
74
|
+
|
|
53
75
|
# convert seconds into HH::MM::SS
|
|
54
76
|
def self.convert_seconds(seconds)
|
|
55
77
|
Time.at(seconds.abs).utc.strftime('%H:%M:%S')
|
|
56
78
|
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def initialize(execution_time: 0, timestamp: '', working_dir: Dir.getwd, cmdline: '',
|
|
83
|
+
exit_code: nil, stdout: '', stderr: '', oom: false, jfr_summary: nil)
|
|
84
|
+
@execution_time = execution_time
|
|
85
|
+
@timestamp = timestamp
|
|
86
|
+
@working_dir = working_dir
|
|
87
|
+
@cmdline = cmdline
|
|
88
|
+
@exit_code = exit_code.nil? ? '?' : exit_code.to_s
|
|
89
|
+
@stdout = stdout
|
|
90
|
+
@stderr = stderr
|
|
91
|
+
@oom = oom
|
|
92
|
+
@jfr_summary = if jfr_summary.instance_of? JfrSummary
|
|
93
|
+
jfr_summary
|
|
94
|
+
else
|
|
95
|
+
JfrSummary.from_h(jfr_summary || {})
|
|
96
|
+
end
|
|
97
|
+
end
|
|
57
98
|
end
|
|
58
99
|
end
|
|
@@ -7,12 +7,17 @@ module PmdTester
|
|
|
7
7
|
|
|
8
8
|
# Parse the base and the patch report, compute their diff
|
|
9
9
|
# Returns a +ReportDiff+
|
|
10
|
-
def build_report_diff(base_report_file, patch_report_file, base_info, patch_info, filter_set = nil
|
|
10
|
+
def build_report_diff(base_report_file, patch_report_file, base_info, patch_info, filter_set = nil,
|
|
11
|
+
rules_changed: true)
|
|
11
12
|
base_details = PmdReportDetail.load(base_info)
|
|
12
|
-
patch_details = PmdReportDetail.load(patch_info)
|
|
13
|
-
|
|
14
13
|
base_report = parse_pmd_report(base_report_file, BASE, base_details, filter_set)
|
|
15
|
-
|
|
14
|
+
|
|
15
|
+
if rules_changed
|
|
16
|
+
patch_details = PmdReportDetail.load(patch_info)
|
|
17
|
+
patch_report = parse_pmd_report(patch_report_file, PATCH, patch_details)
|
|
18
|
+
else
|
|
19
|
+
patch_report = base_report
|
|
20
|
+
end
|
|
16
21
|
|
|
17
22
|
logger.info 'Calculating diffs'
|
|
18
23
|
ReportDiff.new(base_report: base_report, patch_report: patch_report)
|
|
@@ -21,35 +26,58 @@ module PmdTester
|
|
|
21
26
|
# Parse the +report_file+ to produce a +Report+.
|
|
22
27
|
# For the schema of xml reports, refer to https://pmd.github.io/schema/report_2_0_0.xsd
|
|
23
28
|
def parse_pmd_report(report_file, branch, report_details, filter_set = nil)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
logger.info "Parsing #{report_file}"
|
|
29
|
+
logger.info "Parsing PMD Report #{report_file}"
|
|
27
30
|
doc = PmdReportDocument.new(branch, report_details.working_dir, filter_set)
|
|
28
|
-
|
|
29
|
-
parser.parse_file(report_file) if File.exist?(report_file)
|
|
31
|
+
.parse(report_file)
|
|
30
32
|
Report.new(
|
|
33
|
+
report_details: report_details,
|
|
31
34
|
report_document: doc,
|
|
32
|
-
file: report_file
|
|
35
|
+
file: report_file
|
|
36
|
+
)
|
|
37
|
+
end
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
# Parse the base and the patch CPD report, compute their diff
|
|
40
|
+
# Returns a +CpdReportDiff+
|
|
41
|
+
def build_cpd_report_diff(base_report_file, patch_report_file, base_info, patch_info, impl_changed:)
|
|
42
|
+
base_details = PmdReportDetail.load(base_info)
|
|
43
|
+
base_report = parse_cpd_report(base_report_file, BASE, base_details)
|
|
44
|
+
|
|
45
|
+
if impl_changed
|
|
46
|
+
patch_details = PmdReportDetail.load(patch_info)
|
|
47
|
+
patch_report = parse_cpd_report(patch_report_file, PATCH, patch_details)
|
|
48
|
+
else
|
|
49
|
+
patch_report = base_report
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
logger.info 'Calculating CPD diffs'
|
|
53
|
+
CpdReportDiff.new(base_report: base_report, patch_report: patch_report)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def parse_cpd_report(report_file, branch, report_details)
|
|
57
|
+
logger.info "Parsing CPD Report #{report_file}"
|
|
58
|
+
doc = CpdReportDocument.new(branch, report_details.working_dir).parse(report_file)
|
|
59
|
+
CpdReport.new(
|
|
60
|
+
report_details: report_details,
|
|
61
|
+
report_document: doc,
|
|
62
|
+
file: report_file
|
|
37
63
|
)
|
|
38
64
|
end
|
|
39
65
|
|
|
40
66
|
# Fill the report_diff field of every project
|
|
41
|
-
def compute_project_diffs(projects, base_branch, patch_branch, filter_set
|
|
67
|
+
def compute_project_diffs(projects, base_branch, patch_branch, filter_set, rules_changed:, impl_changed:)
|
|
42
68
|
projects.each do |project|
|
|
43
69
|
logger.info "Preparing report for #{project.name}"
|
|
44
70
|
logger.info " with filter #{filter_set}" unless filter_set.nil?
|
|
45
|
-
project.compute_report_diff(base_branch, patch_branch, filter_set
|
|
71
|
+
project.compute_report_diff(base_branch, patch_branch, filter_set,
|
|
72
|
+
rules_changed: rules_changed, impl_changed: impl_changed)
|
|
46
73
|
end
|
|
47
74
|
end
|
|
48
75
|
|
|
49
76
|
# Build the diff reports and write them all
|
|
50
|
-
def build_html_reports(projects, base_branch_details, patch_branch_details, filter_set
|
|
77
|
+
def build_html_reports(projects, base_branch_details, patch_branch_details, filter_set,
|
|
78
|
+
rules_changed:, impl_changed:)
|
|
51
79
|
compute_project_diffs(projects, base_branch_details.branch_name, patch_branch_details.branch_name,
|
|
52
|
-
filter_set)
|
|
80
|
+
filter_set, rules_changed: rules_changed, impl_changed: impl_changed)
|
|
53
81
|
|
|
54
82
|
SummaryReportBuilder.new.write_all_projects(projects,
|
|
55
83
|
base_branch_details,
|