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,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+ require_relative './html_report_builder'
5
+
6
+ module PmdTester
7
+ # Building diff report for a single project
8
+ class DiffReportBuilder < HtmlReportBuilder
9
+ include PmdTester
10
+ NO_DIFFERENCES_MESSAGE = 'No differences found!'
11
+
12
+ def build(project)
13
+ @project = project
14
+ @report_diff = project.report_diff
15
+
16
+ index = File.new(project.diff_report_index_path, 'w')
17
+
18
+ html_report = build_html_report('pmd xml difference report')
19
+ copy_css(project.target_diff_report_path)
20
+
21
+ index.puts html_report
22
+ index.close
23
+
24
+ logger.info "Built difference report of #{project.name} successfully!"
25
+ end
26
+
27
+ def build_body(doc)
28
+ violation_diffs = @report_diff.violation_diffs
29
+ error_diffs = @report_diff.error_diffs
30
+ doc.body(class: 'composite') do
31
+ doc.div(id: 'contentBox') do
32
+ build_summary_section(doc)
33
+ build_violations_section(doc, violation_diffs)
34
+ build_errors_section(doc, error_diffs)
35
+ end
36
+ end
37
+ end
38
+
39
+ def build_summary_section(doc)
40
+ doc.div(class: 'section', id: 'Summary') do
41
+ doc.h2 'Summary:'
42
+ build_summary_table(doc)
43
+ end
44
+ end
45
+
46
+ def build_summary_table(doc)
47
+ doc.table(class: 'bodyTable', border: '0') do
48
+ doc.thead do
49
+ doc.tr do
50
+ doc.th 'Item'
51
+ doc.th 'Base'
52
+ doc.th 'Patch'
53
+ doc.th 'Difference'
54
+ end
55
+ end
56
+
57
+ build_summary_table_body(doc)
58
+ end
59
+ end
60
+
61
+ def build_summary_table_body(doc)
62
+ doc.tbody do
63
+ build_summary_row(doc, 'number of errors', @report_diff.base_errors_size,
64
+ @report_diff.patch_errors_size, @report_diff.error_diffs_size)
65
+ build_summary_row(doc, 'number of violations', @report_diff.base_violations_size,
66
+ @report_diff.patch_violations_size, @report_diff.violation_diffs_size)
67
+ build_summary_row(doc, 'execution time', @report_diff.base_execution_time,
68
+ @report_diff.patch_execution_time, @report_diff.diff_execution_time)
69
+ build_summary_row(doc, 'timestamp', @report_diff.base_timestamp,
70
+ @report_diff.patch_timestamp, '')
71
+ end
72
+ end
73
+
74
+ def build_summary_row(doc, item, base, patch, diff)
75
+ doc.tr do
76
+ doc.td(class: 'c') { doc.text item }
77
+ doc.td(class: 'a') { doc.text base }
78
+ doc.td(class: 'b') { doc.text patch }
79
+ doc.td(class: 'c') { doc.text diff }
80
+ end
81
+ end
82
+
83
+ def build_filename_h3(doc, filename)
84
+ doc.h3 do
85
+ doc.a(href: @project.get_webview_url(filename)) do
86
+ doc.text @project.get_path_inside_project(filename)
87
+ end
88
+ end
89
+ end
90
+
91
+ def build_violations_section(doc, violation_diffs)
92
+ doc.div(class: 'section', id: 'Violations') do
93
+ doc.h2 'Violations:'
94
+
95
+ doc.h3 NO_DIFFERENCES_MESSAGE if violation_diffs.empty?
96
+ violation_diffs.each do |key, value|
97
+ doc.div(class: 'section') do
98
+ build_filename_h3(doc, key)
99
+ build_violation_table(doc, key, value)
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ def build_violation_table(doc, key, value)
106
+ doc.table(class: 'bodyTable', border: '0') do
107
+ build_violation_table_head(doc)
108
+ build_violation_table_body(doc, key, value)
109
+ end
110
+ end
111
+
112
+ def build_violation_table_head(doc)
113
+ doc.thead do
114
+ doc.tr do
115
+ doc.th
116
+ doc.th 'priority'
117
+ doc.th 'Rule'
118
+ doc.th 'Message'
119
+ doc.th 'Line'
120
+ end
121
+ end
122
+ end
123
+
124
+ def build_violation_table_body(doc, key, value)
125
+ doc.tbody do
126
+ a_index = 1
127
+ value.each do |pmd_violation|
128
+ build_violation_table_row(doc, key, pmd_violation, a_index)
129
+ a_index += 1
130
+ end
131
+ end
132
+ end
133
+
134
+ def build_violation_table_row(doc, key, pmd_violation, a_index)
135
+ doc.tr(class: pmd_violation.branch == 'base' ? 'a' : 'b') do
136
+ # The anchor
137
+ doc.td do
138
+ doc.a(id: "A#{a_index}", href: "#A#{a_index}") { doc.text '#' }
139
+ end
140
+
141
+ violation = pmd_violation.attrs
142
+
143
+ # The priority of the rule
144
+ doc.td violation['priority']
145
+
146
+ # The rule that trigger the violation
147
+ doc.td do
148
+ doc.a(href: (violation['externalInfoUrl']).to_s) { doc.text violation['rule'] }
149
+ end
150
+
151
+ # The violation message
152
+ doc.td pmd_violation.text
153
+
154
+ # The begin line of the violation
155
+ line = violation['beginline']
156
+
157
+ # The link to the source file
158
+ doc.td do
159
+ link = get_link_to_source(violation, key)
160
+ doc.a(href: link.to_s) { doc.text line }
161
+ end
162
+ end
163
+ end
164
+
165
+ def get_link_to_source(violation, key)
166
+ l_str = @project.type == 'git' ? 'L' : 'l'
167
+ line_str = "##{l_str}#{violation['beginline']}"
168
+ @project.get_webview_url(key) + line_str
169
+ end
170
+
171
+ def build_errors_section(doc, error_diffs)
172
+ doc.div(class: 'section', id: 'Errors') do
173
+ doc.h2 do
174
+ doc.text 'Errors:'
175
+ end
176
+
177
+ doc.h3 NO_DIFFERENCES_MESSAGE if error_diffs.empty?
178
+ error_diffs.each do |key, value|
179
+ doc.div(class: 'section') do
180
+ build_filename_h3(doc, key)
181
+ build_errors_table(doc, value)
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ def build_errors_table(doc, errors)
188
+ doc.table(class: 'bodyTable', border: '0') do
189
+ build_errors_table_head(doc)
190
+ build_errors_table_body(doc, errors)
191
+ end
192
+ end
193
+
194
+ def build_errors_table_head(doc)
195
+ doc.thead do
196
+ doc.tr do
197
+ doc.th
198
+ doc.th 'Message'
199
+ doc.th 'Details'
200
+ end
201
+ end
202
+ end
203
+
204
+ def build_errors_table_body(doc, errors)
205
+ doc.tbody do
206
+ b_index = 1
207
+ errors.each do |pmd_error|
208
+ doc.tr(class: pmd_error.branch == 'base' ? 'a' : 'b') do
209
+ # The anchor
210
+ doc.td do
211
+ doc.a(id: "B#{b_index}", href: "#B#{b_index}") { doc.text '#' }
212
+ end
213
+
214
+ # The error message
215
+ doc.td pmd_error.msg
216
+
217
+ # Details of error
218
+ doc.td pmd_error.text
219
+
220
+ b_index += 1
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../resource_locator'
4
+ module PmdTester
5
+ # This class is the parent of all classes which is used to build html report
6
+ class HtmlReportBuilder
7
+ CSS_SRC_DIR = ResourceLocator.locate('resources/css')
8
+
9
+ def build_html_report(title_name)
10
+ html_builder = Nokogiri::HTML::Builder.new do |doc|
11
+ doc.html do
12
+ build_head(doc, title_name)
13
+ build_body(doc)
14
+ end
15
+ end
16
+ html_builder.to_html
17
+ end
18
+
19
+ def build_head(doc, title_name)
20
+ doc.head do
21
+ doc.title title_name
22
+
23
+ doc.style(type: 'text/css', media: 'all') do
24
+ doc.text '@import url("./css/maven-base.css");@import url("./css/maven-theme.css");'
25
+ end
26
+ end
27
+ end
28
+
29
+ def copy_css(report_dir)
30
+ css_dest_dir = "#{report_dir}/css"
31
+ FileUtils.copy_entry(CSS_SRC_DIR, css_dest_dir)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require_relative '../cmd'
5
+ require_relative '../project'
6
+ require_relative '../pmd_branch_detail'
7
+ require_relative '../pmd_report_detail'
8
+
9
+ module PmdTester
10
+ # Building pmd xml reports according to a list of standard
11
+ # projects and branch of pmd source code
12
+ class PmdReportBuilder
13
+ include PmdTester
14
+ def initialize(branch_config, projects, local_git_repo, pmd_branch_name)
15
+ @branch_config = branch_config
16
+ @projects = projects
17
+ @local_git_repo = local_git_repo
18
+ @pmd_branch_name = pmd_branch_name
19
+ @pwd = Dir.getwd
20
+
21
+ @pmd_branch_details = PmdBranchDetail.new(pmd_branch_name)
22
+ end
23
+
24
+ def execute_reset_cmd(type, tag)
25
+ case type
26
+ when 'git'
27
+ reset_cmd = "git reset --hard #{tag}"
28
+ when 'hg'
29
+ reset_cmd = "hg up #{tag}"
30
+ end
31
+
32
+ Cmd.execute(reset_cmd)
33
+ end
34
+
35
+ def get_projects
36
+ logger.info 'Cloning projects started'
37
+
38
+ @projects.each do |project|
39
+ logger.info "Start cloning #{project.name} repository"
40
+ path = project.local_source_path
41
+ clone_cmd = "#{project.type} clone #{project.connection} #{path}"
42
+ if File.exist?(path)
43
+ logger.warn "Skipping clone, project path #{path} already exists"
44
+ else
45
+ Cmd.execute(clone_cmd)
46
+ end
47
+
48
+ Dir.chdir(path) do
49
+ execute_reset_cmd(project.type, project.tag)
50
+ end
51
+ logger.info "Cloning #{project.name} completed"
52
+ end
53
+ end
54
+
55
+ def get_pmd_binary_file
56
+ logger.info 'Start packaging PMD'
57
+ Dir.chdir(@local_git_repo) do
58
+ checkout_cmd = "git checkout #{@pmd_branch_name}"
59
+ Cmd.execute(checkout_cmd)
60
+
61
+ @pmd_branch_details.branch_last_sha = get_last_commit_sha
62
+ @pmd_branch_details.branch_last_message = get_last_commit_message
63
+
64
+ package_cmd = './mvnw clean package -Dpmd.skip=true -Dmaven.test.skip=true' \
65
+ ' -Dmaven.checkstyle.skip=true -Dmaven.javadoc.skip=true'
66
+ Cmd.execute(package_cmd)
67
+
68
+ version_cmd = "./mvnw -q -Dexec.executable=\"echo\" -Dexec.args='${project.version}' " \
69
+ '--non-recursive org.codehaus.mojo:exec-maven-plugin:1.5.0:exec'
70
+ @pmd_version = Cmd.execute(version_cmd)
71
+
72
+ unzip_cmd = "unzip -qo pmd-dist/target/pmd-bin-#{@pmd_version}.zip -d #{@pwd}/target"
73
+ Cmd.execute(unzip_cmd)
74
+ end
75
+ logger.info 'Packaging PMD completed'
76
+ end
77
+
78
+ def get_last_commit_sha
79
+ get_last_commit_sha_cmd = 'git rev-parse HEAD'
80
+ Cmd.execute(get_last_commit_sha_cmd)
81
+ end
82
+
83
+ def get_last_commit_message
84
+ get_last_commit_message_cmd = 'git log -1 --pretty=%B'
85
+ Cmd.execute(get_last_commit_message_cmd)
86
+ end
87
+
88
+ def generate_pmd_report(src_root_dir, report_file)
89
+ run_path = "target/pmd-bin-#{@pmd_version}/bin/run.sh"
90
+ pmd_cmd = "#{run_path} pmd -d #{src_root_dir} -f xml -R #{@branch_config} " \
91
+ "-r #{report_file} -failOnViolation false"
92
+ start_time = Time.now
93
+ Cmd.execute(pmd_cmd)
94
+ end_time = Time.now
95
+ [end_time - start_time, end_time]
96
+ end
97
+
98
+ def generate_pmd_reports
99
+ logger.info "Generating PMD report started -- branch #{@pmd_branch_name}"
100
+ get_pmd_binary_file
101
+
102
+ sum_time = 0
103
+ @projects.each do |project|
104
+ logger.info "Generating #{project.name}'s PMD report'"
105
+ execution_time, end_time =
106
+ generate_pmd_report(project.local_source_path,
107
+ project.get_pmd_report_path(@pmd_branch_name))
108
+ sum_time += execution_time
109
+
110
+ report_details = PmdReportDetail.new
111
+ report_details.execution_time = execution_time
112
+ report_details.timestamp = end_time
113
+ report_details.save(project.get_report_info_path(@pmd_branch_name))
114
+ logger.info "#{project.name}'s PMD report was generated successfully"
115
+ end
116
+
117
+ @pmd_branch_details.execution_time = sum_time
118
+ @pmd_branch_details.save
119
+ FileUtils.cp(@branch_config, @pmd_branch_details.target_branch_config_path)
120
+ @pmd_branch_details
121
+ end
122
+
123
+ def build
124
+ get_projects
125
+ generate_pmd_reports
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+ require 'set'
5
+ require_relative '../cmd'
6
+ require_relative '../resource_locator'
7
+
8
+ module PmdTester
9
+ # This class is responsible for generation dynamic configuration
10
+ # according to the difference between base and patch branch of Pmd.
11
+ # Attention: we only consider java rulesets now.
12
+ class RuleSetBuilder
13
+ include PmdTester
14
+ ALL_RULE_SETS = Set['bestpractices', 'codestyle', 'design', 'documentation',
15
+ 'errorprone', 'multithreading', 'performance', 'security'].freeze
16
+ PATH_TO_PMD_JAVA_BASED_RULES =
17
+ 'pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule'
18
+ PATH_TO_PMD_XPATH_BASED_RULES = 'pmd-java/src/main/resources/category/java'
19
+ PATH_TO_ALL_JAVA_RULES =
20
+ ResourceLocator.locate('config/all-java.xml')
21
+ PATH_TO_DYNAMIC_CONFIG = 'target/dynamic-config.xml'
22
+ NO_JAVA_RULES_CHANGED_MESSAGE = 'No java rules have been changed!'
23
+
24
+ def initialize(options)
25
+ @options = options
26
+ end
27
+
28
+ def build
29
+ filenames = diff_filenames
30
+ rule_sets = get_rule_sets(filenames)
31
+ output_filter_set(rule_sets)
32
+ build_config_file(rule_sets)
33
+ logger.debug "Dynamic configuration: #{[rule_sets]}"
34
+ end
35
+
36
+ def output_filter_set(rule_sets)
37
+ if rule_sets == ALL_RULE_SETS
38
+ # if `rule_sets` contains all rule sets, than no need to filter the baseline
39
+ @options.filter_set = nil
40
+ else
41
+ @options.filter_set = rule_sets
42
+ end
43
+ end
44
+
45
+ def diff_filenames
46
+ filenames = nil
47
+ Dir.chdir(@options.local_git_repo) do
48
+ base = @options.base_branch
49
+ patch = @options.patch_branch
50
+ # We only need to support git here, since PMD's repo is using git.
51
+ diff_cmd = "git diff --name-only #{base}..#{patch} -- pmd/core pmd/java"
52
+ filenames = Cmd.execute(diff_cmd)
53
+ end
54
+ filenames.split("\n")
55
+ end
56
+
57
+ def get_rule_sets(filenames)
58
+ rule_sets = Set[]
59
+ filenames.each do |filename|
60
+ match_data = %r{#{PATH_TO_PMD_JAVA_BASED_RULES}/([^/]+)/[^/]+Rule.java}.match(filename)
61
+ if match_data.nil?
62
+ match_data = %r{#{PATH_TO_PMD_XPATH_BASED_RULES}/([^/]+).xml}.match(filename)
63
+ end
64
+
65
+ category = if match_data.nil?
66
+ nil
67
+ else
68
+ match_data[1]
69
+ end
70
+
71
+ if category.nil?
72
+ rule_sets = ALL_RULE_SETS
73
+ break
74
+ else
75
+ rule_sets.add(category)
76
+ end
77
+ end
78
+ rule_sets
79
+ end
80
+
81
+ def build_config_file(rule_sets)
82
+ if rule_sets.empty?
83
+ puts NO_JAVA_RULES_CHANGED_MESSAGE
84
+ exit 0
85
+ end
86
+
87
+ doc = Nokogiri::XML(File.read(PATH_TO_ALL_JAVA_RULES))
88
+ doc.search('rule').each do |rule|
89
+ rule.remove unless match_ref?(rule, rule_sets)
90
+ end
91
+
92
+ description = doc.at_css('description')
93
+ description.content = 'The ruleset generated by PmdTester dynamically'
94
+
95
+ write_dynamic_file(doc)
96
+ end
97
+
98
+ def match_ref?(rule_node, rule_sets)
99
+ rule_sets.each do |rule_set|
100
+ return true unless rule_node['ref'].index(rule_set).nil?
101
+ end
102
+
103
+ false
104
+ end
105
+
106
+ def write_dynamic_file(doc)
107
+ File.open(PATH_TO_DYNAMIC_CONFIG, 'w') do |x|
108
+ x << doc.to_s.gsub(/\n\s+\n/, "\n")
109
+ end
110
+ @options.base_config = PATH_TO_DYNAMIC_CONFIG
111
+ @options.patch_config = PATH_TO_DYNAMIC_CONFIG
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './html_report_builder'
4
+ require_relative '../pmd_branch_detail'
5
+
6
+ module PmdTester
7
+ # Building summary report to show the details about projects and pmd branchs
8
+ class SummaryReportBuilder < HtmlReportBuilder
9
+ include PmdTester
10
+ REPORT_DIR = 'target/reports/diff'
11
+ BASE_CONFIG_PATH = 'target/reports/diff/base_config.xml'
12
+ PATCH_CONFIG_PATH = 'target/reports/diff/patch_config.xml'
13
+ INDEX_PATH = 'target/reports/diff/index.html'
14
+
15
+ def build(projects, base_name, patch_name)
16
+ @projects = projects
17
+ @base_details = get_branch_details(base_name)
18
+ @patch_details = get_branch_details(patch_name)
19
+
20
+ FileUtils.mkdir_p(REPORT_DIR) unless File.directory?(REPORT_DIR)
21
+ index = File.new(INDEX_PATH, 'w')
22
+
23
+ html_report = build_html_report('Summary report')
24
+ copy_css(REPORT_DIR)
25
+
26
+ index.puts html_report
27
+ index.close
28
+
29
+ logger.info 'Built summary report successfully!'
30
+ end
31
+
32
+ def get_branch_details(branch_name)
33
+ details = PmdBranchDetail.new(branch_name)
34
+ details.load
35
+ details
36
+ end
37
+
38
+ def build_body(doc)
39
+ build_branch_details_section(doc)
40
+ build_projects_section(doc)
41
+ end
42
+
43
+ def build_branch_details_section(doc)
44
+ doc.div(class: 'section', id: 'branchdetails') do
45
+ doc.h2 'Branch details:'
46
+ build_branch_details_table(doc)
47
+ end
48
+ end
49
+
50
+ def build_branch_details_table(doc)
51
+ doc.table(class: 'bodyTable', border: '0') do
52
+ build_branch_table_head(doc)
53
+ build_branch_table_body(doc)
54
+ end
55
+ end
56
+
57
+ def build_branch_table_head(doc)
58
+ doc.thead do
59
+ doc.tr do
60
+ doc.th 'Item'
61
+ doc.th 'base'
62
+ doc.th 'patch'
63
+ end
64
+ end
65
+ end
66
+
67
+ def build_branch_table_body(doc)
68
+ doc.tbody do
69
+ build_branch_table_row(doc, 'branch name', @base_details.branch_name,
70
+ @patch_details.branch_name)
71
+ build_branch_table_row(doc, 'branch last commit sha', @base_details.branch_last_sha,
72
+ @patch_details.branch_last_sha)
73
+ build_branch_table_row(doc, 'branch last commit message', @base_details.branch_last_message,
74
+ @patch_details.branch_last_message)
75
+ build_branch_table_row(doc, 'total execution time', @base_details.format_execution_time,
76
+ @patch_details.format_execution_time)
77
+ build_branch_config_table_row(doc)
78
+ end
79
+ end
80
+
81
+ def build_branch_config_table_row(doc)
82
+ doc.tr do
83
+ doc.td(class: 'c') { doc.text 'branch configuration' }
84
+ base_config_src_path = @base_details.target_branch_config_path
85
+ copy_branch_config_file(base_config_src_path, BASE_CONFIG_PATH)
86
+ doc.td(class: 'a') do
87
+ doc.a(href: './base_config.xml') { doc.text 'base config' }
88
+ end
89
+ patch_config_stc_path = @patch_details.target_branch_config_path
90
+ FileUtils.cp(patch_config_stc_path, PATCH_CONFIG_PATH)
91
+ doc.td(class: 'b') do
92
+ doc.a(href: './patch_config.xml') { doc.text 'patch config' }
93
+ end
94
+ end
95
+ end
96
+
97
+ def copy_branch_config_file(src, dest)
98
+ FileUtils.cp(src, dest) if File.exist?(src)
99
+ end
100
+
101
+ def build_branch_table_row(doc, item, base, patch)
102
+ doc.tr do
103
+ doc.td(class: 'c') { doc.text item }
104
+ doc.td(class: 'a') { doc.text base }
105
+ doc.td(class: 'b') { doc.text patch }
106
+ end
107
+ end
108
+
109
+ def build_projects_section(doc)
110
+ doc.div(class: 'section', id: 'projects') do
111
+ doc.h2 'Projects:'
112
+ build_projects_table(doc)
113
+ end
114
+ end
115
+
116
+ def build_projects_table(doc)
117
+ doc.table(class: 'bodyTable', border: '0') do
118
+ build_projects_table_head(doc)
119
+ build_projects_table_body(doc)
120
+ end
121
+ end
122
+
123
+ def build_projects_table_head(doc)
124
+ doc.thead do
125
+ doc.tr do
126
+ doc.th 'project name'
127
+ doc.th 'project branch/tag'
128
+ doc.th 'diff exist?'
129
+ doc.th 'introduce new errors?'
130
+ end
131
+ end
132
+ end
133
+
134
+ def build_projects_table_body(doc)
135
+ doc.tbody do
136
+ @projects.each do |project|
137
+ doc.tr do
138
+ doc.td do
139
+ doc.a(href: project.diff_report_index_ref_path) { doc.text project.name }
140
+ end
141
+ doc.td project.tag
142
+ doc.td project.diffs_exist? ? 'Yes' : 'No'
143
+ doc.td project.introduce_new_errors? ? 'Yes' : 'No'
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module PmdTester
6
+ # Containing the common method for executing shell command
7
+ class Cmd
8
+ extend PmdTester
9
+ def self.execute(cmd)
10
+ logger.debug "execute command '#{cmd}'"
11
+
12
+ stdout, stderr, status = Open3.capture3("#{cmd};")
13
+
14
+ logger.debug stdout
15
+ unless status.success?
16
+ logger.error stderr
17
+ raise CmdException.new(cmd, stderr)
18
+ end
19
+
20
+ stdout&.chomp!
21
+
22
+ stdout
23
+ end
24
+ end
25
+
26
+ # The exception should be raised when the shell command failed.
27
+ class CmdException < StandardError
28
+ attr_reader :cmd
29
+ attr_reader :error
30
+ attr_reader :message
31
+
32
+ COMMON_MSG = 'An error occurred while executing the shell command'
33
+
34
+ def initialize(cmd, error)
35
+ @cmd = cmd
36
+ @error = error
37
+ @message = "#{COMMON_MSG} '#{cmd}'"
38
+ end
39
+ end
40
+ end