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