inspec 1.51.0 → 1.51.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -15
- data/README.md +1 -1
- data/docs/glossary.md +99 -0
- data/docs/resources/aide_conf.md.erb +16 -9
- data/docs/resources/apache.md.erb +66 -0
- data/docs/resources/apache_conf.md.erb +11 -5
- data/docs/resources/apt.md.erb +1 -1
- data/docs/resources/audit_policy.md.erb +1 -1
- data/docs/resources/auditd_conf.md.erb +12 -9
- data/docs/resources/bash.md.erb +24 -12
- data/docs/resources/bond.md.erb +26 -24
- data/docs/resources/bridge.md.erb +18 -11
- data/docs/resources/bsd_service.md.erb +11 -2
- data/docs/resources/command.md.erb +30 -29
- data/docs/resources/cpan.md.erb +33 -17
- data/docs/resources/cran.md.erb +26 -17
- data/docs/resources/crontab.md.erb +18 -1
- data/docs/resources/csv.md.erb +13 -7
- data/docs/resources/{dh_params.md → dh_params.md.erb} +30 -6
- data/docs/resources/directory.md.erb +9 -4
- data/docs/resources/docker.md.erb +1 -1
- data/docs/resources/docker_container.md.erb +32 -26
- data/docs/resources/docker_image.md.erb +29 -26
- data/docs/resources/docker_service.md.erb +37 -31
- data/docs/resources/elasticsearch.md.erb +18 -32
- data/docs/resources/etc_fstab.md.erb +19 -15
- data/docs/resources/etc_group.md.erb +13 -39
- data/docs/resources/etc_hosts.md.erb +12 -5
- data/docs/resources/etc_hosts_allow.md.erb +9 -4
- data/docs/resources/etc_hosts_deny.md.erb +12 -7
- data/docs/resources/file.md.erb +139 -134
- data/docs/resources/filesystem.md.erb +5 -4
- data/docs/resources/firewalld.md.erb +1 -1
- data/docs/resources/gem.md.erb +2 -2
- data/docs/resources/group.md.erb +1 -1
- data/docs/resources/host.md.erb +1 -1
- data/docs/resources/iis_app.md.erb +1 -1
- data/docs/resources/iis_site.md.erb +1 -1
- data/docs/resources/interface.md.erb +1 -1
- data/docs/resources/iptables.md.erb +1 -1
- data/docs/resources/json.md.erb +1 -1
- data/docs/resources/kernel_module.md.erb +1 -1
- data/docs/resources/kernel_parameter.md.erb +1 -1
- data/docs/resources/launchd_service.md.erb +1 -1
- data/docs/resources/limits_conf.md.erb +1 -1
- data/docs/resources/login_def.md.erb +1 -1
- data/docs/resources/mount.md.erb +1 -1
- data/docs/resources/mysql_conf.md.erb +1 -1
- data/docs/resources/nginx_conf.md.erb +1 -1
- data/docs/resources/npm.md.erb +1 -1
- data/docs/resources/oneget.md.erb +1 -1
- data/docs/resources/os.md.erb +1 -1
- data/docs/resources/os_env.md.erb +2 -2
- data/docs/resources/package.md.erb +1 -1
- data/docs/resources/packages.md.erb +66 -0
- data/docs/resources/parse_config.md.erb +1 -1
- data/docs/resources/parse_config_file.md.erb +1 -1
- data/docs/resources/passwd.md.erb +1 -1
- data/docs/resources/pip.md.erb +1 -1
- data/docs/resources/port.md.erb +1 -1
- data/docs/resources/postgres_conf.md.erb +1 -1
- data/docs/resources/postgres_session.md.erb +1 -1
- data/docs/resources/powershell.md.erb +2 -2
- data/docs/resources/processes.md.erb +1 -1
- data/docs/resources/registry_key.md.erb +1 -1
- data/docs/resources/runit_service.md.erb +1 -1
- data/docs/resources/security_policy.md.erb +1 -1
- data/docs/resources/service.md.erb +1 -1
- data/docs/resources/shadow.md.erb +1 -1
- data/docs/resources/ssh_config.md.erb +1 -1
- data/docs/resources/sshd_config.md.erb +1 -1
- data/docs/resources/ssl.md.erb +1 -1
- data/docs/resources/sys_info.md.erb +1 -1
- data/docs/resources/systemd_service.md.erb +1 -1
- data/docs/resources/sysv_service.md.erb +1 -1
- data/docs/resources/upstart_service.md.erb +1 -1
- data/docs/resources/user.md.erb +1 -1
- data/docs/resources/users.md.erb +1 -1
- data/docs/resources/windows_feature.md.erb +1 -1
- data/docs/resources/windows_hotfix.md.erb +1 -1
- data/docs/resources/xinetd_conf.md.erb +1 -1
- data/docs/resources/xml.md.erb +1 -1
- data/docs/resources/yaml.md.erb +1 -1
- data/docs/resources/yum.md.erb +1 -1
- data/lib/inspec.rb +2 -1
- data/lib/inspec/base_cli.rb +98 -18
- data/lib/inspec/cli.rb +33 -21
- data/lib/inspec/formatters.rb +3 -0
- data/lib/inspec/formatters/base.rb +208 -0
- data/lib/inspec/formatters/json_rspec.rb +20 -0
- data/lib/inspec/formatters/show_progress.rb +12 -0
- data/lib/inspec/objects.rb +1 -0
- data/lib/inspec/objects/describe.rb +92 -0
- data/lib/inspec/reporters.rb +33 -0
- data/lib/inspec/reporters/base.rb +23 -0
- data/lib/inspec/reporters/cli.rb +395 -0
- data/lib/inspec/reporters/json.rb +132 -0
- data/lib/inspec/reporters/json_min.rb +44 -0
- data/lib/inspec/reporters/junit.rb +77 -0
- data/lib/inspec/runner.rb +14 -1
- data/lib/inspec/runner_rspec.rb +34 -14
- data/lib/inspec/schema.rb +1 -0
- data/lib/inspec/shell.rb +0 -1
- data/lib/inspec/version.rb +1 -1
- data/lib/resources/apache.rb +20 -0
- data/lib/resources/apache_conf.rb +33 -8
- data/lib/resources/audit_policy.rb +1 -1
- data/lib/resources/packages.rb +4 -3
- metadata +17 -4
- data/lib/inspec/rspec_json_formatter.rb +0 -940
@@ -26,7 +26,7 @@
|
|
26
26
|
module Inspec::Resources
|
27
27
|
class AuditPolicy < Inspec.resource(1)
|
28
28
|
name 'audit_policy'
|
29
|
-
desc 'Use the audit_policy InSpec audit resource to test auditing policies on the Microsoft Windows platform. An auditing policy is a category of security-related events to be audited. Auditing is disabled by default and may be enabled for categories like account management, logon events, policy changes, process tracking, privilege use, system events, or object access. For each auditing category property
|
29
|
+
desc 'Use the audit_policy InSpec audit resource to test auditing policies on the Microsoft Windows platform. An auditing policy is a category of security-related events to be audited. Auditing is disabled by default and may be enabled for categories like account management, logon events, policy changes, process tracking, privilege use, system events, or object access. For each enabled auditing category property, the auditing level may be set to No Auditing, Not Specified, Success, Success and Failure, or Failure.'
|
30
30
|
example "
|
31
31
|
describe audit_policy do
|
32
32
|
its('parameter') { should eq 'value' }
|
data/lib/resources/packages.rb
CHANGED
@@ -48,6 +48,7 @@ module Inspec::Resources
|
|
48
48
|
.add(:statuses, field: 'status', style: :simple)
|
49
49
|
.add(:names, field: 'name')
|
50
50
|
.add(:versions, field: 'version')
|
51
|
+
.add(:architectures, field: 'architecture')
|
51
52
|
.connect(self, :filtered_packages)
|
52
53
|
|
53
54
|
private
|
@@ -69,7 +70,7 @@ module Inspec::Resources
|
|
69
70
|
end
|
70
71
|
|
71
72
|
class PkgsManagement
|
72
|
-
PackageStruct = Struct.new(:status, :name, :version)
|
73
|
+
PackageStruct = Struct.new(:status, :name, :version, :architecture)
|
73
74
|
attr_reader :inspec
|
74
75
|
def initialize(inspec)
|
75
76
|
@inspec = inspec
|
@@ -80,7 +81,7 @@ module Inspec::Resources
|
|
80
81
|
class Debs < PkgsManagement
|
81
82
|
def build_package_list
|
82
83
|
# use two spaces as delimiter in case any of the fields has a space in it
|
83
|
-
command = "dpkg-query -W -f='${db:Status-Abbrev} ${Package} ${Version}\\n'"
|
84
|
+
command = "dpkg-query -W -f='${db:Status-Abbrev} ${Package} ${Version} ${Architecture}\\n'"
|
84
85
|
cmd = inspec.command(command)
|
85
86
|
all = cmd.stdout.split("\n")
|
86
87
|
return [] if all.nil?
|
@@ -97,7 +98,7 @@ module Inspec::Resources
|
|
97
98
|
class Rpms < PkgsManagement
|
98
99
|
def build_package_list
|
99
100
|
# use two spaces as delimiter in case any of the fields has a space in it
|
100
|
-
command = "rpm -qa --queryformat '%{NAME} %{VERSION}-%{RELEASE}\\n'" # rubocop:disable Style/FormatStringToken
|
101
|
+
command = "rpm -qa --queryformat '%{NAME} %{VERSION}-%{RELEASE} %{ARCH}\\n'" # rubocop:disable Style/FormatStringToken
|
101
102
|
cmd = inspec.command(command)
|
102
103
|
all = cmd.stdout.split("\n")
|
103
104
|
return [] if all.nil?
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: inspec
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.51.
|
4
|
+
version: 1.51.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dominik Richter
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-02-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: train
|
@@ -292,6 +292,7 @@ files:
|
|
292
292
|
- docs/README.md
|
293
293
|
- docs/dsl_inspec.md
|
294
294
|
- docs/dsl_resource.md
|
295
|
+
- docs/glossary.md
|
295
296
|
- docs/habitat.md
|
296
297
|
- docs/inspec_and_friends.md
|
297
298
|
- docs/matchers.md
|
@@ -299,6 +300,7 @@ files:
|
|
299
300
|
- docs/plugin_kitchen_inspec.md
|
300
301
|
- docs/profiles.md
|
301
302
|
- docs/resources/aide_conf.md.erb
|
303
|
+
- docs/resources/apache.md.erb
|
302
304
|
- docs/resources/apache_conf.md.erb
|
303
305
|
- docs/resources/apt.md.erb
|
304
306
|
- docs/resources/audit_policy.md.erb
|
@@ -314,7 +316,7 @@ files:
|
|
314
316
|
- docs/resources/cran.md.erb
|
315
317
|
- docs/resources/crontab.md.erb
|
316
318
|
- docs/resources/csv.md.erb
|
317
|
-
- docs/resources/dh_params.md
|
319
|
+
- docs/resources/dh_params.md.erb
|
318
320
|
- docs/resources/directory.md.erb
|
319
321
|
- docs/resources/docker.md.erb
|
320
322
|
- docs/resources/docker_container.md.erb
|
@@ -360,6 +362,7 @@ files:
|
|
360
362
|
- docs/resources/os.md.erb
|
361
363
|
- docs/resources/os_env.md.erb
|
362
364
|
- docs/resources/package.md.erb
|
365
|
+
- docs/resources/packages.md.erb
|
363
366
|
- docs/resources/parse_config.md.erb
|
364
367
|
- docs/resources/parse_config_file.md.erb
|
365
368
|
- docs/resources/passwd.md.erb
|
@@ -512,6 +515,10 @@ files:
|
|
512
515
|
- lib/inspec/expect.rb
|
513
516
|
- lib/inspec/fetcher.rb
|
514
517
|
- lib/inspec/file_provider.rb
|
518
|
+
- lib/inspec/formatters.rb
|
519
|
+
- lib/inspec/formatters/base.rb
|
520
|
+
- lib/inspec/formatters/json_rspec.rb
|
521
|
+
- lib/inspec/formatters/show_progress.rb
|
515
522
|
- lib/inspec/library_eval_context.rb
|
516
523
|
- lib/inspec/log.rb
|
517
524
|
- lib/inspec/metadata.rb
|
@@ -519,6 +526,7 @@ files:
|
|
519
526
|
- lib/inspec/objects.rb
|
520
527
|
- lib/inspec/objects/attribute.rb
|
521
528
|
- lib/inspec/objects/control.rb
|
529
|
+
- lib/inspec/objects/describe.rb
|
522
530
|
- lib/inspec/objects/each_loop.rb
|
523
531
|
- lib/inspec/objects/list.rb
|
524
532
|
- lib/inspec/objects/or_test.rb
|
@@ -536,9 +544,14 @@ files:
|
|
536
544
|
- lib/inspec/profile.rb
|
537
545
|
- lib/inspec/profile_context.rb
|
538
546
|
- lib/inspec/profile_vendor.rb
|
547
|
+
- lib/inspec/reporters.rb
|
548
|
+
- lib/inspec/reporters/base.rb
|
549
|
+
- lib/inspec/reporters/cli.rb
|
550
|
+
- lib/inspec/reporters/json.rb
|
551
|
+
- lib/inspec/reporters/json_min.rb
|
552
|
+
- lib/inspec/reporters/junit.rb
|
539
553
|
- lib/inspec/require_loader.rb
|
540
554
|
- lib/inspec/resource.rb
|
541
|
-
- lib/inspec/rspec_json_formatter.rb
|
542
555
|
- lib/inspec/rule.rb
|
543
556
|
- lib/inspec/runner.rb
|
544
557
|
- lib/inspec/runner_mock.rb
|
@@ -1,940 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
# author: Dominik Richter
|
3
|
-
# author: Christoph Hartmann
|
4
|
-
# author: John Kerry
|
5
|
-
|
6
|
-
require 'rspec/core'
|
7
|
-
require 'rspec/core/formatters/json_formatter'
|
8
|
-
|
9
|
-
# Vanilla RSpec JSON formatter with a slight extension to show example IDs.
|
10
|
-
# TODO: Remove these lines when RSpec includes the ID natively
|
11
|
-
class InspecRspecVanilla < RSpec::Core::Formatters::JsonFormatter
|
12
|
-
RSpec::Core::Formatters.register self
|
13
|
-
|
14
|
-
private
|
15
|
-
|
16
|
-
# We are cheating and overriding a private method in RSpec's core JsonFormatter.
|
17
|
-
# This is to avoid having to repeat this id functionality in both dump_summary
|
18
|
-
# and dump_profile (both of which call format_example).
|
19
|
-
# See https://github.com/rspec/rspec-core/blob/master/lib/rspec/core/formatters/json_formatter.rb
|
20
|
-
#
|
21
|
-
# rspec's example id here corresponds to an inspec test's control name -
|
22
|
-
# either explicitly specified or auto-generated by rspec itself.
|
23
|
-
def format_example(example)
|
24
|
-
res = super(example)
|
25
|
-
res[:id] = example.metadata[:id]
|
26
|
-
res
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
# Minimal JSON formatter for inspec. Only contains limited information about
|
31
|
-
# examples without any extras.
|
32
|
-
class InspecRspecMiniJson < RSpec::Core::Formatters::JsonFormatter
|
33
|
-
# Don't re-register all the call-backs over and over - we automatically
|
34
|
-
# inherit all callbacks registered by the parent class.
|
35
|
-
RSpec::Core::Formatters.register self, :dump_summary, :stop
|
36
|
-
|
37
|
-
# Called after stop has been called and the run is complete.
|
38
|
-
def dump_summary(summary)
|
39
|
-
@output_hash[:version] = Inspec::VERSION
|
40
|
-
@output_hash[:statistics] = {
|
41
|
-
duration: summary.duration,
|
42
|
-
}
|
43
|
-
end
|
44
|
-
|
45
|
-
# Called at the end of a complete RSpec run.
|
46
|
-
def stop(notification)
|
47
|
-
# This might be a bit confusing. The results are not actually organized
|
48
|
-
# by control. It is organized by test. So if a control has 3 tests, the
|
49
|
-
# output will have 3 control entries, each one with the same control id
|
50
|
-
# and different test results. An rspec example maps to an inspec test.
|
51
|
-
@output_hash[:controls] = notification.examples.map do |example|
|
52
|
-
format_example(example).tap do |hash|
|
53
|
-
e = example.exception
|
54
|
-
next unless e
|
55
|
-
|
56
|
-
if example.metadata[:sensitive]
|
57
|
-
hash[:message] = '*** sensitive output suppressed ***'
|
58
|
-
else
|
59
|
-
hash[:message] = exception_message(e)
|
60
|
-
end
|
61
|
-
|
62
|
-
next if e.is_a? RSpec::Expectations::ExpectationNotMetError
|
63
|
-
hash[:exception] = e.class.name
|
64
|
-
hash[:backtrace] = e.backtrace
|
65
|
-
|
66
|
-
# if the exception indicates the resource author wants to skip the test,
|
67
|
-
# we update the test status here.
|
68
|
-
hash[:status] = 'skipped' if e.is_a?(Inspec::Exceptions::ResourceSkipped)
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
private
|
74
|
-
|
75
|
-
def exception_message(exception)
|
76
|
-
if exception.is_a?(RSpec::Core::MultipleExceptionError)
|
77
|
-
exception.all_exceptions.map(&:message).uniq.join("\n\n")
|
78
|
-
else
|
79
|
-
exception.message
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
def format_example(example)
|
84
|
-
if !example.metadata[:description_args].empty? && example.metadata[:skip]
|
85
|
-
# For skipped profiles, rspec returns in full_description the skip_message as well. We don't want
|
86
|
-
# to mix the two, so we pick the full_description from the example.metadata[:example_group] hash.
|
87
|
-
code_description = example.metadata[:example_group][:description]
|
88
|
-
else
|
89
|
-
code_description = example.metadata[:full_description]
|
90
|
-
end
|
91
|
-
|
92
|
-
res = {
|
93
|
-
id: example.metadata[:id],
|
94
|
-
profile_id: example.metadata[:profile_id],
|
95
|
-
status: example.execution_result.status.to_s,
|
96
|
-
code_desc: code_description,
|
97
|
-
}
|
98
|
-
|
99
|
-
unless (pid = example.metadata[:profile_id]).nil?
|
100
|
-
res[:profile_id] = pid
|
101
|
-
end
|
102
|
-
|
103
|
-
if res[:status] == 'pending'
|
104
|
-
res[:status] = 'skipped'
|
105
|
-
res[:skip_message] = example.metadata[:description]
|
106
|
-
res[:resource] = example.metadata[:described_class].to_s
|
107
|
-
end
|
108
|
-
|
109
|
-
res
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
class InspecRspecJson < InspecRspecMiniJson
|
114
|
-
RSpec::Core::Formatters.register self, :stop, :dump_summary
|
115
|
-
attr_writer :backend
|
116
|
-
|
117
|
-
def initialize(*args)
|
118
|
-
super(*args)
|
119
|
-
@profiles = []
|
120
|
-
@profiles_info = nil
|
121
|
-
@backend = nil
|
122
|
-
end
|
123
|
-
|
124
|
-
# Called by the runner during example collection.
|
125
|
-
def add_profile(profile)
|
126
|
-
@profiles.push(profile)
|
127
|
-
end
|
128
|
-
|
129
|
-
def stop(notification)
|
130
|
-
super(notification)
|
131
|
-
|
132
|
-
@output_hash[:other_checks] = examples_without_controls
|
133
|
-
@output_hash[:profiles] = profiles_info
|
134
|
-
@output_hash[:platform] = {
|
135
|
-
name: os(:name),
|
136
|
-
release: os(:release),
|
137
|
-
}
|
138
|
-
|
139
|
-
examples_with_controls.each do |example|
|
140
|
-
control = example2control(example)
|
141
|
-
move_example_into_control(example, control)
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
private
|
146
|
-
|
147
|
-
def os(field)
|
148
|
-
return nil if @backend.nil?
|
149
|
-
@backend.os.params[field]
|
150
|
-
end
|
151
|
-
|
152
|
-
def all_unique_controls
|
153
|
-
Array(@all_controls).uniq
|
154
|
-
end
|
155
|
-
|
156
|
-
def profile_summary
|
157
|
-
failed = 0
|
158
|
-
skipped = 0
|
159
|
-
passed = 0
|
160
|
-
critical = 0
|
161
|
-
major = 0
|
162
|
-
minor = 0
|
163
|
-
|
164
|
-
all_unique_controls.each do |control|
|
165
|
-
next if control[:id].start_with? '(generated from '
|
166
|
-
next unless control[:results]
|
167
|
-
if control[:results].any? { |r| r[:status] == 'failed' }
|
168
|
-
failed += 1
|
169
|
-
if control[:impact] >= 0.7
|
170
|
-
critical += 1
|
171
|
-
elsif control[:impact] >= 0.4
|
172
|
-
major += 1
|
173
|
-
else
|
174
|
-
minor += 1
|
175
|
-
end
|
176
|
-
elsif control[:results].any? { |r| r[:status] == 'skipped' }
|
177
|
-
skipped += 1
|
178
|
-
else
|
179
|
-
passed += 1
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
|
-
total = failed + passed + skipped
|
184
|
-
|
185
|
-
{ 'total' => total,
|
186
|
-
'failed' => {
|
187
|
-
'total' => failed,
|
188
|
-
'critical' => critical,
|
189
|
-
'major' => major,
|
190
|
-
'minor' => minor,
|
191
|
-
},
|
192
|
-
'skipped' => skipped,
|
193
|
-
'passed' => passed }
|
194
|
-
end
|
195
|
-
|
196
|
-
def tests_summary
|
197
|
-
total = 0
|
198
|
-
failed = 0
|
199
|
-
skipped = 0
|
200
|
-
passed = 0
|
201
|
-
|
202
|
-
all_unique_controls.each do |control|
|
203
|
-
next unless control[:results]
|
204
|
-
control[:results].each do |result|
|
205
|
-
if result[:status] == 'failed'
|
206
|
-
failed += 1
|
207
|
-
elsif result[:status] == 'skipped'
|
208
|
-
skipped += 1
|
209
|
-
else
|
210
|
-
passed += 1
|
211
|
-
end
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
{ 'total' => total, 'failed' => failed, 'skipped' => skipped, 'passed' => passed }
|
216
|
-
end
|
217
|
-
|
218
|
-
def examples
|
219
|
-
@output_hash[:controls]
|
220
|
-
end
|
221
|
-
|
222
|
-
def examples_without_controls
|
223
|
-
examples.find_all { |example| example2control(example).nil? }
|
224
|
-
end
|
225
|
-
|
226
|
-
def examples_with_controls
|
227
|
-
(examples - examples_without_controls)
|
228
|
-
end
|
229
|
-
|
230
|
-
def profiles_info
|
231
|
-
@profiles_info ||= @profiles.map(&:info!).map(&:dup)
|
232
|
-
end
|
233
|
-
|
234
|
-
def example2control(example)
|
235
|
-
profile = profile_from_example(example)
|
236
|
-
return nil unless profile && profile[:controls]
|
237
|
-
profile[:controls].find { |x| x[:id] == example[:id] }
|
238
|
-
end
|
239
|
-
|
240
|
-
def profile_from_example(example)
|
241
|
-
profiles_info.find { |p| profile_contains_example?(p, example) }
|
242
|
-
end
|
243
|
-
|
244
|
-
def profile_contains_example?(profile, example)
|
245
|
-
profile_name = profile[:name]
|
246
|
-
example_profile_id = example[:profile_id]
|
247
|
-
|
248
|
-
# if either the profile name is nil or the profile in the given example
|
249
|
-
# is nil, assume the profile doesn't contain the example and default
|
250
|
-
# to creating a new profile. Otherwise, for profiles that have no
|
251
|
-
# metadata, this may incorrectly match a profile that does not contain
|
252
|
-
# this example, leading to Ruby exceptions.
|
253
|
-
return false if profile_name.nil? || example_profile_id.nil?
|
254
|
-
|
255
|
-
# The correct profile is one where the name of the profile, and the profile
|
256
|
-
# name in the example match. Additionally, the list of controls in the
|
257
|
-
# profile must contain the example in question (which we match by ID).
|
258
|
-
#
|
259
|
-
# While the profile name match is usually good enough, we must also match by
|
260
|
-
# the control ID in the case where an InSpec runner has multiple profiles of
|
261
|
-
# the same name (i.e. when Test Kitchen is running concurrently using a
|
262
|
-
# single test suite that uses the Flat source reader, in which case InSpec
|
263
|
-
# creates a fake profile with a name like "tests from /path/to/tests")
|
264
|
-
profile_name == example_profile_id && profile[:controls].any? { |control| control[:id] == example[:id] }
|
265
|
-
end
|
266
|
-
|
267
|
-
def move_example_into_control(example, control)
|
268
|
-
control[:results] ||= []
|
269
|
-
example.delete(:id)
|
270
|
-
example.delete(:profile_id)
|
271
|
-
control[:results].push(example)
|
272
|
-
end
|
273
|
-
|
274
|
-
def format_example(example)
|
275
|
-
super(example).tap do |res|
|
276
|
-
res[:run_time] = example.execution_result.run_time
|
277
|
-
res[:start_time] = example.execution_result.started_at.to_s
|
278
|
-
end
|
279
|
-
end
|
280
|
-
end
|
281
|
-
|
282
|
-
class InspecRspecCli < InspecRspecJson
|
283
|
-
RSpec::Core::Formatters.register self, :close
|
284
|
-
|
285
|
-
case RUBY_PLATFORM
|
286
|
-
when /windows|mswin|msys|mingw|cygwin/
|
287
|
-
|
288
|
-
# Most currently available Windows terminals have poor support
|
289
|
-
# for ANSI extended colors
|
290
|
-
COLORS = {
|
291
|
-
'critical' => "\033[0;1;31m",
|
292
|
-
'major' => "\033[0;1;31m",
|
293
|
-
'minor' => "\033[0;36m",
|
294
|
-
'failed' => "\033[0;1;31m",
|
295
|
-
'passed' => "\033[0;1;32m",
|
296
|
-
'skipped' => "\033[0;37m",
|
297
|
-
'reset' => "\033[0m",
|
298
|
-
}.freeze
|
299
|
-
|
300
|
-
# Most currently available Windows terminals have poor support
|
301
|
-
# for UTF-8 characters so use these boring indicators
|
302
|
-
INDICATORS = {
|
303
|
-
'critical' => ' [CRIT] ',
|
304
|
-
'major' => ' [MAJR] ',
|
305
|
-
'minor' => ' [MINR] ',
|
306
|
-
'failed' => ' [FAIL] ',
|
307
|
-
'skipped' => ' [SKIP] ',
|
308
|
-
'passed' => ' [PASS] ',
|
309
|
-
'unknown' => ' [UNKN] ',
|
310
|
-
'empty' => ' ',
|
311
|
-
'small' => ' ',
|
312
|
-
}.freeze
|
313
|
-
else
|
314
|
-
# Extended colors for everyone else
|
315
|
-
COLORS = {
|
316
|
-
'critical' => "\033[38;5;9m",
|
317
|
-
'major' => "\033[38;5;208m",
|
318
|
-
'minor' => "\033[0;36m",
|
319
|
-
'failed' => "\033[38;5;9m",
|
320
|
-
'passed' => "\033[38;5;41m",
|
321
|
-
'skipped' => "\033[38;5;247m",
|
322
|
-
'reset' => "\033[0m",
|
323
|
-
}.freeze
|
324
|
-
|
325
|
-
# Groovy UTF-8 characters for everyone else...
|
326
|
-
# ...even though they probably only work on Mac
|
327
|
-
INDICATORS = {
|
328
|
-
'critical' => ' × ',
|
329
|
-
'major' => ' ∅ ',
|
330
|
-
'minor' => ' ⊚ ',
|
331
|
-
'failed' => ' × ',
|
332
|
-
'skipped' => ' ↺ ',
|
333
|
-
'passed' => ' ✔ ',
|
334
|
-
'unknown' => ' ? ',
|
335
|
-
'empty' => ' ',
|
336
|
-
'small' => ' ',
|
337
|
-
}.freeze
|
338
|
-
end
|
339
|
-
|
340
|
-
MULTI_TEST_CONTROL_SUMMARY_MAX_LEN = 60
|
341
|
-
|
342
|
-
def initialize(*args)
|
343
|
-
@current_control = nil
|
344
|
-
@all_controls = []
|
345
|
-
@profile_printed = false
|
346
|
-
super(*args)
|
347
|
-
end
|
348
|
-
|
349
|
-
#
|
350
|
-
# This method is called through the RSpec Formatter interface for every
|
351
|
-
# example found in the test suite.
|
352
|
-
#
|
353
|
-
# Within #format_example we are getting an example and:
|
354
|
-
# * if this is an example, within a control, within a profile then we want
|
355
|
-
# to display the profile header, display the control, and then display
|
356
|
-
# the example.
|
357
|
-
# * if this is another example, within the same control, within the same
|
358
|
-
# profile we want to display the example.
|
359
|
-
# * if this is an example that does not map to a control (anonymous) then
|
360
|
-
# we want to store it for later to displayed at the end of a profile.
|
361
|
-
#
|
362
|
-
def format_example(example)
|
363
|
-
example_data = super(example)
|
364
|
-
control = create_or_find_control(example_data)
|
365
|
-
|
366
|
-
# If we are switching to a new control then we want to print the control
|
367
|
-
# we were previously collecting examples unless the last control is
|
368
|
-
# anonymous (no control). Anonymous controls and their examples are handled
|
369
|
-
# later on the profile change.
|
370
|
-
|
371
|
-
if switching_to_new_control?(control)
|
372
|
-
print_last_control_with_examples unless last_control_is_anonymous?
|
373
|
-
end
|
374
|
-
|
375
|
-
store_last_control(control)
|
376
|
-
|
377
|
-
# Each profile may have zero or more anonymous examples. These are examples
|
378
|
-
# that defined in a profile but outside of a control. They may be defined
|
379
|
-
# at the start, in-between, or end of list of examples. To display them
|
380
|
-
# at the very end of a profile, which means we have to wait for the profile
|
381
|
-
# to change to know we are done with a profile.
|
382
|
-
|
383
|
-
if switching_to_new_profile?(control.profile)
|
384
|
-
output.puts ''
|
385
|
-
print_anonymous_examples_associated_with_last_profile
|
386
|
-
clear_anonymous_examples_associated_with_last_profile
|
387
|
-
end
|
388
|
-
|
389
|
-
print_profile(control.profile)
|
390
|
-
store_last_profile(control.profile)
|
391
|
-
|
392
|
-
# The anonymous controls should be added to a hash that we will display
|
393
|
-
# when we are done examining all the examples within this profile.
|
394
|
-
|
395
|
-
if control.anonymous?
|
396
|
-
add_anonymous_example_within_this_profile(control.as_hash)
|
397
|
-
end
|
398
|
-
|
399
|
-
@all_controls.push(control.as_hash)
|
400
|
-
example_data
|
401
|
-
end
|
402
|
-
|
403
|
-
#
|
404
|
-
# This is the last method is invoked through the formatter interface.
|
405
|
-
# Because the profile
|
406
|
-
# we may have some remaining anonymous examples so we want to display them
|
407
|
-
# as well as a summary of the profile and test stats.
|
408
|
-
#
|
409
|
-
def close(_notification)
|
410
|
-
# when the profile has no controls or examples it will not have been printed.
|
411
|
-
# then we want to ensure we print all the profiles
|
412
|
-
print_last_control_with_examples unless last_control_is_anonymous?
|
413
|
-
output.puts ''
|
414
|
-
print_anonymous_examples_associated_with_last_profile
|
415
|
-
print_profiles_without_examples
|
416
|
-
print_profile_summary
|
417
|
-
print_tests_summary
|
418
|
-
end
|
419
|
-
|
420
|
-
private
|
421
|
-
|
422
|
-
#
|
423
|
-
# With the example we can find the profile associated with it and if there
|
424
|
-
# is already a control defined. If there is one then we will use that data
|
425
|
-
# to build our control object. If there isn't we simply create a new hash of
|
426
|
-
# controld data that will be populated from the examples that are found.
|
427
|
-
#
|
428
|
-
# @return [Control] A new control or one found associated with the example.
|
429
|
-
#
|
430
|
-
def create_or_find_control(example)
|
431
|
-
profile = profile_from_example(example)
|
432
|
-
|
433
|
-
control_data = {}
|
434
|
-
|
435
|
-
if profile && profile[:controls]
|
436
|
-
control_data = profile[:controls].find { |ctrl| ctrl[:id] == example[:id] }
|
437
|
-
end
|
438
|
-
|
439
|
-
control = Control.new(control_data, profile)
|
440
|
-
control.add_example(example)
|
441
|
-
|
442
|
-
control
|
443
|
-
end
|
444
|
-
|
445
|
-
#
|
446
|
-
# If there is already a control we have have seen before and it is different
|
447
|
-
# than the new control then we are indeed switching controls.
|
448
|
-
#
|
449
|
-
def switching_to_new_control?(control)
|
450
|
-
@last_control && @last_control != control
|
451
|
-
end
|
452
|
-
|
453
|
-
def store_last_control(control)
|
454
|
-
@last_control = control
|
455
|
-
end
|
456
|
-
|
457
|
-
def print_last_control_with_examples
|
458
|
-
return unless @last_control
|
459
|
-
|
460
|
-
print_control(@last_control)
|
461
|
-
@last_control.examples.each { |example| print_result(example) }
|
462
|
-
end
|
463
|
-
|
464
|
-
def last_control_is_anonymous?
|
465
|
-
@last_control && @last_control.anonymous?
|
466
|
-
end
|
467
|
-
|
468
|
-
#
|
469
|
-
# If there is a profile we have seen before and it is different than the
|
470
|
-
# new profile then we are indeed switching profiles.
|
471
|
-
#
|
472
|
-
def switching_to_new_profile?(new_profile)
|
473
|
-
@last_profile && @last_profile != new_profile
|
474
|
-
end
|
475
|
-
|
476
|
-
#
|
477
|
-
# Print all the anonymous examples that have been found for this profile
|
478
|
-
#
|
479
|
-
def print_anonymous_examples_associated_with_last_profile
|
480
|
-
Array(anonymous_examples_within_this_profile).uniq.each do |control|
|
481
|
-
print_anonymous_control(control)
|
482
|
-
end
|
483
|
-
output.puts '' unless Array(anonymous_examples_within_this_profile).empty?
|
484
|
-
end
|
485
|
-
|
486
|
-
#
|
487
|
-
# As we process examples we need an accumulator that will allow us to store
|
488
|
-
# all the examples that do not have a named control associated with them.
|
489
|
-
#
|
490
|
-
def anonymous_examples_within_this_profile
|
491
|
-
@anonymous_examples_within_this_profile ||= []
|
492
|
-
end
|
493
|
-
|
494
|
-
#
|
495
|
-
# Remove all controls from the anonymous examples that are tracked.
|
496
|
-
#
|
497
|
-
def clear_anonymous_examples_associated_with_last_profile
|
498
|
-
@anonymous_examples_within_this_profile = []
|
499
|
-
end
|
500
|
-
|
501
|
-
#
|
502
|
-
# Append a new control to the anonymous examples
|
503
|
-
#
|
504
|
-
def add_anonymous_example_within_this_profile(control)
|
505
|
-
anonymous_examples_within_this_profile.push(control)
|
506
|
-
end
|
507
|
-
|
508
|
-
def store_last_profile(new_profile)
|
509
|
-
@last_profile = new_profile
|
510
|
-
end
|
511
|
-
|
512
|
-
#
|
513
|
-
# Print the profile
|
514
|
-
#
|
515
|
-
# * For anonymous profiles, where are generated for examples and controls
|
516
|
-
# defined outside of a profile, simply display the target information
|
517
|
-
# * For profiles without a title use the name (or 'unknown'), version,
|
518
|
-
# and target information.
|
519
|
-
# * For all other profiles display the title with name (or 'unknown'),
|
520
|
-
# version, and target information.
|
521
|
-
#
|
522
|
-
def print_profile(profile)
|
523
|
-
return if profile.nil? || profile[:already_printed]
|
524
|
-
output.puts ''
|
525
|
-
|
526
|
-
if profile[:name].nil?
|
527
|
-
print_target
|
528
|
-
profile[:already_printed] = true
|
529
|
-
return
|
530
|
-
end
|
531
|
-
|
532
|
-
if profile[:title].nil?
|
533
|
-
output.puts "Profile: #{profile[:name] || 'unknown'}"
|
534
|
-
else
|
535
|
-
output.puts "Profile: #{profile[:title]} (#{profile[:name] || 'unknown'})"
|
536
|
-
end
|
537
|
-
|
538
|
-
output.puts 'Version: ' + (profile[:version] || '(not specified)')
|
539
|
-
print_target
|
540
|
-
profile[:already_printed] = true
|
541
|
-
end
|
542
|
-
|
543
|
-
def print_profiles_without_examples
|
544
|
-
profiles_info.reject { |p| p[:already_printed] }.each do |profile|
|
545
|
-
print_profile(profile)
|
546
|
-
print_line(
|
547
|
-
color: '', indicator: INDICATORS['empty'], id: '', profile: '',
|
548
|
-
summary: 'No tests executed.'
|
549
|
-
)
|
550
|
-
output.puts ''
|
551
|
-
end
|
552
|
-
end
|
553
|
-
|
554
|
-
#
|
555
|
-
# This target information displays which system that came under test
|
556
|
-
#
|
557
|
-
def print_target
|
558
|
-
return if @backend.nil?
|
559
|
-
connection = @backend.backend
|
560
|
-
return unless connection.respond_to?(:uri)
|
561
|
-
output.puts('Target: ' + connection.uri + "\n\n")
|
562
|
-
end
|
563
|
-
|
564
|
-
#
|
565
|
-
# We want to print the details about the control
|
566
|
-
#
|
567
|
-
def print_control(control)
|
568
|
-
print_line(
|
569
|
-
color: control.summary_indicator,
|
570
|
-
indicator: INDICATORS[control.summary_indicator] || INDICATORS['unknown'],
|
571
|
-
summary: format_lines(control.summary, INDICATORS['empty']),
|
572
|
-
id: "#{control.id}: ",
|
573
|
-
profile: control.profile_id,
|
574
|
-
)
|
575
|
-
end
|
576
|
-
|
577
|
-
def print_result(result)
|
578
|
-
test_skipped = result[:status] == 'skipped'
|
579
|
-
test_status = test_skipped ? 'skipped' : result[:status_type]
|
580
|
-
indicator = INDICATORS[result[:status]]
|
581
|
-
indicator = INDICATORS['empty'] if indicator.nil?
|
582
|
-
if result[:message]
|
583
|
-
msg = result[:code_desc] + "\n" + result[:message]
|
584
|
-
else
|
585
|
-
msg = result[:skip_message] || result[:code_desc]
|
586
|
-
end
|
587
|
-
print_line(
|
588
|
-
color: test_status,
|
589
|
-
indicator: INDICATORS['small'] + indicator,
|
590
|
-
summary: format_lines(msg, INDICATORS['empty']),
|
591
|
-
id: nil, profile: nil
|
592
|
-
)
|
593
|
-
end
|
594
|
-
|
595
|
-
def print_anonymous_control(control)
|
596
|
-
control_result = control[:results]
|
597
|
-
title = control_result[0][:code_desc].split[0..1].join(' ')
|
598
|
-
puts ' ' + title
|
599
|
-
# iterate over all describe blocks in anonoymous control block
|
600
|
-
control_result.each do |test|
|
601
|
-
control_id = ''
|
602
|
-
# display exceptions
|
603
|
-
unless test[:exception].nil?
|
604
|
-
test_result = test[:message]
|
605
|
-
else
|
606
|
-
# determine title
|
607
|
-
test_result = test[:skip_message] || test[:code_desc].split[2..-1].join(' ')
|
608
|
-
# show error message
|
609
|
-
test_result += "\n" + test[:message] unless test[:message].nil?
|
610
|
-
end
|
611
|
-
status_indicator = test[:status_type]
|
612
|
-
print_line(
|
613
|
-
color: status_indicator,
|
614
|
-
indicator: INDICATORS['small'] + INDICATORS[status_indicator] || INDICATORS['unknown'],
|
615
|
-
summary: format_lines(test_result, INDICATORS['empty']),
|
616
|
-
id: control_id,
|
617
|
-
profile: control[:profile_id],
|
618
|
-
)
|
619
|
-
end
|
620
|
-
end
|
621
|
-
|
622
|
-
def print_profile_summary
|
623
|
-
summary = profile_summary
|
624
|
-
return unless summary['total'] > 0
|
625
|
-
|
626
|
-
success_str = summary['passed'] == 1 ? '1 successful control' : "#{summary['passed']} successful controls"
|
627
|
-
failed_str = summary['failed']['total'] == 1 ? '1 control failure' : "#{summary['failed']['total']} control failures"
|
628
|
-
skipped_str = summary['skipped'] == 1 ? '1 control skipped' : "#{summary['skipped']} controls skipped"
|
629
|
-
|
630
|
-
success_color = summary['passed'] > 0 ? 'passed' : 'no_color'
|
631
|
-
failed_color = summary['failed']['total'] > 0 ? 'failed' : 'no_color'
|
632
|
-
skipped_color = summary['skipped'] > 0 ? 'skipped' : 'no_color'
|
633
|
-
|
634
|
-
s = format('Profile Summary: %s, %s, %s',
|
635
|
-
format_with_color(success_color, success_str),
|
636
|
-
format_with_color(failed_color, failed_str),
|
637
|
-
format_with_color(skipped_color, skipped_str))
|
638
|
-
output.puts(s) if summary['total'] > 0
|
639
|
-
end
|
640
|
-
|
641
|
-
def print_tests_summary
|
642
|
-
summary = tests_summary
|
643
|
-
|
644
|
-
failed_str = summary['failed'] == 1 ? '1 failure' : "#{summary['failed']} failures"
|
645
|
-
|
646
|
-
success_color = summary['passed'] > 0 ? 'passed' : 'no_color'
|
647
|
-
failed_color = summary['failed'] > 0 ? 'failed' : 'no_color'
|
648
|
-
skipped_color = summary['skipped'] > 0 ? 'skipped' : 'no_color'
|
649
|
-
|
650
|
-
s = format('Test Summary: %s, %s, %s',
|
651
|
-
format_with_color(success_color, "#{summary['passed']} successful"),
|
652
|
-
format_with_color(failed_color, failed_str),
|
653
|
-
format_with_color(skipped_color, "#{summary['skipped']} skipped"))
|
654
|
-
|
655
|
-
output.puts(s)
|
656
|
-
end
|
657
|
-
|
658
|
-
# Formats the line (called from print_line)
|
659
|
-
def format_line(fields)
|
660
|
-
format = '%indicator%id%summary'
|
661
|
-
format.gsub(/%\w+/) do |x|
|
662
|
-
term = x[1..-1]
|
663
|
-
fields.key?(term.to_sym) ? fields[term.to_sym].to_s : x
|
664
|
-
end
|
665
|
-
end
|
666
|
-
|
667
|
-
# Prints line; used to print results
|
668
|
-
def print_line(fields)
|
669
|
-
output.puts(format_with_color(fields[:color], format_line(fields)))
|
670
|
-
end
|
671
|
-
|
672
|
-
# Helps formatting summary lines (called from within print_line arguments)
|
673
|
-
def format_lines(lines, indentation)
|
674
|
-
lines.gsub(/\n/, "\n" + indentation)
|
675
|
-
end
|
676
|
-
|
677
|
-
def format_with_color(color_name, text)
|
678
|
-
return text unless RSpec.configuration.color
|
679
|
-
return text unless COLORS.key?(color_name)
|
680
|
-
|
681
|
-
"#{COLORS[color_name]}#{text}#{COLORS['reset']}"
|
682
|
-
end
|
683
|
-
|
684
|
-
#
|
685
|
-
# This class wraps a control hash object to provide a useful inteface for
|
686
|
-
# maintaining the associated profile, ids, results, title, etc.
|
687
|
-
#
|
688
|
-
class Control
|
689
|
-
include Comparable
|
690
|
-
|
691
|
-
STATUS_TYPES = {
|
692
|
-
'unknown' => -3,
|
693
|
-
'passed' => -2,
|
694
|
-
'skipped' => -1,
|
695
|
-
'minor' => 1,
|
696
|
-
'major' => 2,
|
697
|
-
'failed' => 2.5,
|
698
|
-
'critical' => 3,
|
699
|
-
}.freeze
|
700
|
-
|
701
|
-
def initialize(control, profile)
|
702
|
-
@control = control
|
703
|
-
@profile = profile
|
704
|
-
summary_calculation_is_needed
|
705
|
-
end
|
706
|
-
|
707
|
-
attr_reader :control, :profile
|
708
|
-
|
709
|
-
alias as_hash control
|
710
|
-
|
711
|
-
def id
|
712
|
-
control[:id]
|
713
|
-
end
|
714
|
-
|
715
|
-
def anonymous?
|
716
|
-
control[:id].to_s.start_with? '(generated from '
|
717
|
-
end
|
718
|
-
|
719
|
-
def profile_id
|
720
|
-
control[:profile_id]
|
721
|
-
end
|
722
|
-
|
723
|
-
def examples
|
724
|
-
control[:results]
|
725
|
-
end
|
726
|
-
|
727
|
-
def summary_indicator
|
728
|
-
calculate_summary! if summary_calculation_needed?
|
729
|
-
STATUS_TYPES.key(@summary_status)
|
730
|
-
end
|
731
|
-
|
732
|
-
def add_example(example)
|
733
|
-
control[:id] = example[:id]
|
734
|
-
control[:profile_id] = example[:profile_id]
|
735
|
-
|
736
|
-
example[:status_type] = status_type(example)
|
737
|
-
example.delete(:id)
|
738
|
-
example.delete(:profile_id)
|
739
|
-
|
740
|
-
control[:results] ||= []
|
741
|
-
control[:results].push(example)
|
742
|
-
summary_calculation_is_needed
|
743
|
-
end
|
744
|
-
|
745
|
-
# Determine title for control given current_control.
|
746
|
-
# Called from current_control_summary.
|
747
|
-
def title
|
748
|
-
title = control[:title]
|
749
|
-
if title
|
750
|
-
title
|
751
|
-
elsif examples.length == 1
|
752
|
-
# If it's an anonymous control, just go with the only description
|
753
|
-
# available for the underlying test.
|
754
|
-
examples[0][:code_desc].to_s
|
755
|
-
elsif examples.empty?
|
756
|
-
# Empty control block - if it's anonymous, there's nothing we can do.
|
757
|
-
# Is this case even possible?
|
758
|
-
'Empty anonymous control'
|
759
|
-
else
|
760
|
-
# Multiple tests - but no title. Do our best and generate some form of
|
761
|
-
# identifier or label or name.
|
762
|
-
title = (examples.map { |example| example[:code_desc] }).join('; ')
|
763
|
-
max_len = MULTI_TEST_CONTROL_SUMMARY_MAX_LEN
|
764
|
-
title = title[0..(max_len-1)] + '...' if title.length > max_len
|
765
|
-
title
|
766
|
-
end
|
767
|
-
end
|
768
|
-
|
769
|
-
# Return summary of the control which is usually a title with fails and skips
|
770
|
-
def summary
|
771
|
-
calculate_summary! if summary_calculation_needed?
|
772
|
-
suffix =
|
773
|
-
if examples.length == 1
|
774
|
-
# Single test - be nice and just print the exception message if the test
|
775
|
-
# failed. No need to say "1 failed".
|
776
|
-
examples[0][:message].to_s
|
777
|
-
else
|
778
|
-
[
|
779
|
-
!fails.empty? ? "#{fails.length} failed" : nil,
|
780
|
-
!skips.empty? ? "#{skips.length} skipped" : nil,
|
781
|
-
].compact.join(' ')
|
782
|
-
end
|
783
|
-
|
784
|
-
suffix == '' ? title : title + ' (' + suffix + ')'
|
785
|
-
end
|
786
|
-
|
787
|
-
# We are interested in comparing controls against other controls. It is
|
788
|
-
# important to compare their id values and the id values of their profiles.
|
789
|
-
# In the event that a control has the same id in a different profile we
|
790
|
-
# do not want them to be considered the same.
|
791
|
-
#
|
792
|
-
# Controls are never ordered so we don't care about the remaining
|
793
|
-
# implementation of the spaceship operator.
|
794
|
-
#
|
795
|
-
def <=>(other)
|
796
|
-
if id == other.id && profile_id == other.profile_id
|
797
|
-
0
|
798
|
-
else
|
799
|
-
-1
|
800
|
-
end
|
801
|
-
end
|
802
|
-
|
803
|
-
private
|
804
|
-
|
805
|
-
attr_reader :summary_calculation_needed, :skips, :fails, :passes
|
806
|
-
|
807
|
-
alias summary_calculation_needed? summary_calculation_needed
|
808
|
-
|
809
|
-
def summary_calculation_is_needed
|
810
|
-
@summary_calculation_needed = true
|
811
|
-
end
|
812
|
-
|
813
|
-
def summary_has_been_calculated
|
814
|
-
@summary_calculation_needed = false
|
815
|
-
end
|
816
|
-
|
817
|
-
def calculate_summary!
|
818
|
-
@summary_status = STATUS_TYPES['unknown']
|
819
|
-
@skips = []
|
820
|
-
@fails = []
|
821
|
-
@passes = []
|
822
|
-
examples.each { |example| update_summary(example) }
|
823
|
-
summary_has_been_calculated
|
824
|
-
end
|
825
|
-
|
826
|
-
def update_summary(example)
|
827
|
-
test_skipped = example[:status] == 'skipped'
|
828
|
-
status_type = test_skipped ? 'skipped' : example[:status_type]
|
829
|
-
example_status = STATUS_TYPES[status_type]
|
830
|
-
@summary_status = example_status if example_status > @summary_status
|
831
|
-
fails.push(example) if example_status > 0
|
832
|
-
passes.push(example) if example_status == STATUS_TYPES['passed']
|
833
|
-
skips.push(example) if example_status == STATUS_TYPES['skipped']
|
834
|
-
end
|
835
|
-
|
836
|
-
# Determines 'status_type' (critical, major, minor) of control given
|
837
|
-
# status (failed/passed/skipped) and impact value (0.0 - 1.0).
|
838
|
-
# Called from format_example, sets the 'status_type' for each 'example'
|
839
|
-
def status_type(example)
|
840
|
-
status = example[:status]
|
841
|
-
return status if status != 'failed' || control[:impact].nil?
|
842
|
-
if control[:impact] >= 0.7
|
843
|
-
'critical'
|
844
|
-
elsif control[:impact] >= 0.4
|
845
|
-
'major'
|
846
|
-
else
|
847
|
-
'minor'
|
848
|
-
end
|
849
|
-
end
|
850
|
-
end
|
851
|
-
end
|
852
|
-
|
853
|
-
class InspecRspecJUnit < InspecRspecJson
|
854
|
-
RSpec::Core::Formatters.register self, :close
|
855
|
-
|
856
|
-
#
|
857
|
-
# This is the last method is invoked through the formatter interface.
|
858
|
-
# Converts the junit formatter constructed output_hash into REXML generated
|
859
|
-
# XML and writes it to output.
|
860
|
-
#
|
861
|
-
def close(_notification)
|
862
|
-
require 'rexml/document'
|
863
|
-
xml_output = REXML::Document.new
|
864
|
-
xml_output.add(REXML::XMLDecl.new)
|
865
|
-
|
866
|
-
testsuites = REXML::Element.new('testsuites')
|
867
|
-
xml_output.add(testsuites)
|
868
|
-
|
869
|
-
@output_hash[:profiles].each do |profile|
|
870
|
-
testsuites.add(build_profile_xml(profile))
|
871
|
-
end
|
872
|
-
|
873
|
-
formatter = REXML::Formatters::Pretty.new
|
874
|
-
formatter.compact = true
|
875
|
-
output.puts formatter.write(xml_output.xml_decl, '')
|
876
|
-
output.puts formatter.write(xml_output.root, '')
|
877
|
-
end
|
878
|
-
|
879
|
-
private
|
880
|
-
|
881
|
-
def build_profile_xml(profile)
|
882
|
-
profile_name = profile[:name]
|
883
|
-
profile_xml = REXML::Element.new('testsuite')
|
884
|
-
profile_xml.add_attribute('name', profile_name)
|
885
|
-
profile_xml.add_attribute('tests', count_profile_tests(profile))
|
886
|
-
profile_xml.add_attribute('failed', count_profile_failed_tests(profile))
|
887
|
-
|
888
|
-
profile[:controls].each do |control|
|
889
|
-
next if control[:results].nil?
|
890
|
-
|
891
|
-
control[:results].each do |result|
|
892
|
-
profile_xml.add(build_result_xml(profile_name, control, result))
|
893
|
-
end
|
894
|
-
end
|
895
|
-
|
896
|
-
profile_xml
|
897
|
-
end
|
898
|
-
|
899
|
-
def build_result_xml(profile_name, control, result)
|
900
|
-
result_xml = REXML::Element.new('testcase')
|
901
|
-
result_xml.add_attribute('name', result[:code_desc])
|
902
|
-
# if there is no control title, we are likely receiving test results from a
|
903
|
-
# "naked" test (a test not located within a control block). Therefore, rather
|
904
|
-
# than outputting the auto-generated ID, i.e.
|
905
|
-
#
|
906
|
-
# "(generated from test_spec.rb:1 de0ce10e4bbbd4d0ff7a65f4234de8c1)")
|
907
|
-
#
|
908
|
-
# ... we'll output "Anonymous" instead.
|
909
|
-
result_xml.add_attribute('classname', control[:title].nil? ? "#{profile_name}.Anonymous" : "#{profile_name}.#{control[:id]}")
|
910
|
-
result_xml.add_attribute('time', result[:run_time])
|
911
|
-
|
912
|
-
if result[:status] == 'failed'
|
913
|
-
failure_element = REXML::Element.new('failure')
|
914
|
-
failure_element.add_attribute('message', result[:message])
|
915
|
-
result_xml.add(failure_element)
|
916
|
-
elsif result[:status] == 'skipped'
|
917
|
-
result_xml.add_element('skipped')
|
918
|
-
end
|
919
|
-
|
920
|
-
result_xml
|
921
|
-
end
|
922
|
-
|
923
|
-
def count_profile_tests(profile)
|
924
|
-
profile[:controls].reduce(0) { |acc, elem|
|
925
|
-
acc + (elem[:results].nil? ? 0 : elem[:results].count)
|
926
|
-
}
|
927
|
-
end
|
928
|
-
|
929
|
-
def count_profile_failed_tests(profile)
|
930
|
-
profile[:controls].reduce(0) { |acc, elem|
|
931
|
-
if elem[:results].nil?
|
932
|
-
acc
|
933
|
-
else
|
934
|
-
acc + elem[:results].reduce(0) { |fail_test_total, test_case|
|
935
|
-
test_case[:status] == 'failed' ? fail_test_total + 1 : fail_test_total
|
936
|
-
}
|
937
|
-
end
|
938
|
-
}
|
939
|
-
end
|
940
|
-
end
|