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,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