pmdtester 1.6.1 → 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 +9 -7
- data/.github/workflows/manual-integration-tests.yml +9 -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 +54 -0
- data/Manifest.txt +13 -0
- data/README.rdoc +77 -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 +139 -41
- data/lib/pmdtester/builders/project_hasher.rb +24 -25
- data/lib/pmdtester/builders/rule_set_builder.rb +43 -11
- data/lib/pmdtester/builders/summary_report_builder.rb +6 -1
- data/lib/pmdtester/cmd.rb +24 -9
- 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,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module PmdTester
|
|
6
|
+
# This class reads a jfr recording and extract a summary
|
|
7
|
+
class JfrSummary
|
|
8
|
+
attr_accessor :execution_time, :max_heap_memory, :max_cpu_load, :avg_cpu_load, :recording_path
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@execution_time = 0
|
|
12
|
+
@max_heap_memory = 0
|
|
13
|
+
@max_cpu_load = 0
|
|
14
|
+
@avg_cpu_load = 0
|
|
15
|
+
@recording_path = ''
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def load(jfr_recording)
|
|
19
|
+
@recording_path = jfr_recording
|
|
20
|
+
start_time = get_start_time(jfr_recording)
|
|
21
|
+
end_time = get_end_time(jfr_recording)
|
|
22
|
+
@execution_time = end_time - start_time
|
|
23
|
+
|
|
24
|
+
gc_heap_summary = get_gc_heap_summary(jfr_recording)
|
|
25
|
+
unless gc_heap_summary.empty?
|
|
26
|
+
@max_heap_memory = gc_heap_summary.map do |e|
|
|
27
|
+
e.dig(:values, :heapUsed)
|
|
28
|
+
end.max
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
cpu_load = get_cpu_load(jfr_recording)
|
|
32
|
+
unless cpu_load.empty?
|
|
33
|
+
@max_cpu_load = cpu_load.map { |e| e.dig(:values, :jvmUser) }.max
|
|
34
|
+
@avg_cpu_load = cpu_load.map { |e| e.dig(:values, :jvmUser) }.sum / cpu_load.size
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_h
|
|
41
|
+
{
|
|
42
|
+
execution_time: @execution_time,
|
|
43
|
+
max_heap_memory: @max_heap_memory,
|
|
44
|
+
max_cpu_load: @max_cpu_load,
|
|
45
|
+
avg_cpu_load: @avg_cpu_load,
|
|
46
|
+
recording_path: @recording_path
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_h_for_liquid
|
|
51
|
+
{
|
|
52
|
+
'execution_time' => PmdReportDetail.convert_seconds(@execution_time),
|
|
53
|
+
'max_heap_memory' => format_memory(@max_heap_memory),
|
|
54
|
+
'max_cpu_load' => format_percentage(@max_cpu_load),
|
|
55
|
+
'avg_cpu_load' => format_percentage(@avg_cpu_load)
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.from_h(hash)
|
|
60
|
+
jfr_summary = JfrSummary.new
|
|
61
|
+
jfr_summary.execution_time = hash[:execution_time] || 0
|
|
62
|
+
jfr_summary.max_heap_memory = hash[:max_heap_memory] || 0
|
|
63
|
+
jfr_summary.max_cpu_load = hash[:max_cpu_load] || 0
|
|
64
|
+
jfr_summary.avg_cpu_load = hash[:avg_cpu_load] || 0
|
|
65
|
+
jfr_summary.recording_path = hash[:recording_path] || ''
|
|
66
|
+
jfr_summary
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def get_start_time(jfr_recording)
|
|
72
|
+
stdout = Cmd.execute_successfully("jfr print --json --events jdk.JVMInformation #{jfr_recording}",
|
|
73
|
+
nil, debug_log_stdout: false)
|
|
74
|
+
jvm_info = JSON.parse(stdout, symbolize_names: true).dig(:recording, :events, 0, :values, :jvmStartTime)
|
|
75
|
+
return Time.at(0) if jvm_info.nil?
|
|
76
|
+
|
|
77
|
+
Time.parse(jvm_info)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def get_end_time(jfr_recording)
|
|
81
|
+
stdout = Cmd.execute_successfully("jfr print --json --events jdk.Shutdown #{jfr_recording}",
|
|
82
|
+
nil, debug_log_stdout: false)
|
|
83
|
+
shutdown_info = JSON.parse(stdout, symbolize_names: true).dig(:recording, :events, 0, :values, :startTime)
|
|
84
|
+
return Time.at(0) if shutdown_info.nil?
|
|
85
|
+
|
|
86
|
+
Time.parse(shutdown_info)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def get_gc_heap_summary(jfr_recording)
|
|
90
|
+
stdout = Cmd.execute_successfully("jfr print --json --events jdk.GCHeapSummary #{jfr_recording}",
|
|
91
|
+
nil, debug_log_stdout: false)
|
|
92
|
+
events = JSON.parse(stdout, symbolize_names: true).dig(:recording, :events)
|
|
93
|
+
return [] if events.nil?
|
|
94
|
+
|
|
95
|
+
events
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def get_cpu_load(jfr_recording)
|
|
99
|
+
stdout = Cmd.execute_successfully("jfr print --json --events jdk.CPULoad #{jfr_recording}",
|
|
100
|
+
nil, debug_log_stdout: false)
|
|
101
|
+
events = JSON.parse(stdout, symbolize_names: true).dig(:recording, :events)
|
|
102
|
+
return [] if events.nil?
|
|
103
|
+
|
|
104
|
+
events
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def format_memory(bytes)
|
|
108
|
+
return '0 MB' if bytes.nil? || bytes.zero?
|
|
109
|
+
|
|
110
|
+
"#{(bytes / (1024 * 1024)).round} MB"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def format_percentage(value)
|
|
114
|
+
return '0%' if value.nil? || value.zero?
|
|
115
|
+
|
|
116
|
+
"#{(value * 100).round}%"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -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
|