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.
Files changed (286) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +933 -101
  3. data/Gemfile +8 -17
  4. data/MAINTAINERS.md +2 -1
  5. data/MAINTAINERS.toml +9 -4
  6. data/README.md +49 -28
  7. data/Rakefile +54 -39
  8. data/bin/inspec +1 -1
  9. data/docs/dsl_inspec.md +17 -4
  10. data/docs/dsl_resource.md +1 -1
  11. data/docs/habitat.md +192 -0
  12. data/docs/inspec_and_friends.md +4 -2
  13. data/docs/matchers.md +11 -0
  14. data/docs/migration.md +293 -0
  15. data/docs/profiles.md +124 -76
  16. data/docs/resources/aide_conf.md.erb +81 -0
  17. data/docs/resources/apache_conf.md.erb +10 -10
  18. data/docs/resources/apt.md.erb +13 -13
  19. data/docs/resources/audit_policy.md.erb +10 -10
  20. data/docs/resources/auditd_conf.md.erb +10 -10
  21. data/docs/resources/auditd_rules.md.erb +11 -11
  22. data/docs/resources/bash.md.erb +12 -12
  23. data/docs/resources/bond.md.erb +15 -15
  24. data/docs/resources/bridge.md.erb +11 -11
  25. data/docs/resources/bsd_service.md.erb +11 -11
  26. data/docs/resources/command.md.erb +23 -23
  27. data/docs/resources/crontab.md.erb +84 -0
  28. data/docs/resources/csv.md.erb +10 -10
  29. data/docs/resources/dh_params.md +191 -0
  30. data/docs/resources/directory.md.erb +8 -8
  31. data/docs/resources/docker.md.erb +160 -0
  32. data/docs/resources/docker_container.md.erb +89 -0
  33. data/docs/resources/docker_image.md.erb +86 -0
  34. data/docs/resources/etc_group.md.erb +16 -16
  35. data/docs/resources/etc_hosts.md.erb +62 -0
  36. data/docs/resources/file.md.erb +80 -58
  37. data/docs/resources/gem.md.erb +32 -13
  38. data/docs/resources/group.md.erb +12 -12
  39. data/docs/resources/grub_conf.md.erb +11 -11
  40. data/docs/resources/host.md.erb +27 -18
  41. data/docs/resources/http.md.erb +104 -0
  42. data/docs/resources/iis_app.md.erb +126 -0
  43. data/docs/resources/iis_site.md.erb +17 -17
  44. data/docs/resources/inetd_conf.md.erb +10 -10
  45. data/docs/resources/ini.md.erb +9 -9
  46. data/docs/resources/interface.md.erb +13 -13
  47. data/docs/resources/iptables.md.erb +21 -14
  48. data/docs/resources/json.md.erb +11 -11
  49. data/docs/resources/kernel_module.md.erb +73 -12
  50. data/docs/resources/kernel_parameter.md.erb +12 -12
  51. data/docs/resources/key_rsa.md.erb +70 -0
  52. data/docs/resources/launchd_service.md.erb +11 -11
  53. data/docs/resources/limits_conf.md.erb +10 -10
  54. data/docs/resources/login_def.md.erb +12 -12
  55. data/docs/resources/mount.md.erb +14 -14
  56. data/docs/resources/mssql_session.md.erb +62 -0
  57. data/docs/resources/mysql_conf.md.erb +14 -14
  58. data/docs/resources/mysql_session.md.erb +22 -10
  59. data/docs/resources/npm.md.erb +13 -13
  60. data/docs/resources/ntp_conf.md.erb +9 -9
  61. data/docs/resources/oneget.md.erb +11 -11
  62. data/docs/resources/oracledb_session.md.erb +53 -0
  63. data/docs/resources/os.md.erb +17 -17
  64. data/docs/resources/os_env.md.erb +12 -12
  65. data/docs/resources/package.md.erb +29 -16
  66. data/docs/resources/parse_config.md.erb +18 -18
  67. data/docs/resources/parse_config_file.md.erb +29 -23
  68. data/docs/resources/{etc_passwd.md.erb → passwd.md.erb} +17 -17
  69. data/docs/resources/pip.md.erb +15 -15
  70. data/docs/resources/port.md.erb +23 -23
  71. data/docs/resources/postgres_conf.md.erb +15 -13
  72. data/docs/resources/postgres_hba_conf.md.erb +104 -0
  73. data/docs/resources/postgres_ident_conf.md.erb +87 -0
  74. data/docs/resources/postgres_session.md.erb +28 -21
  75. data/docs/resources/powershell.md.erb +14 -14
  76. data/docs/resources/processes.md.erb +123 -0
  77. data/docs/resources/rabbitmq_config.md.erb +55 -0
  78. data/docs/resources/registry_key.md.erb +44 -21
  79. data/docs/resources/runit_service.md.erb +11 -11
  80. data/docs/resources/security_policy.md.erb +10 -10
  81. data/docs/resources/service.md.erb +18 -18
  82. data/docs/resources/{etc_shadow.md.erb → shadow.md.erb} +19 -19
  83. data/docs/resources/ssh_config.md.erb +13 -13
  84. data/docs/resources/sshd_config.md.erb +15 -15
  85. data/docs/resources/ssl.md.erb +13 -13
  86. data/docs/resources/sys_info.md.erb +10 -10
  87. data/docs/resources/systemd_service.md.erb +11 -11
  88. data/docs/resources/sysv_service.md.erb +11 -11
  89. data/docs/resources/upstart_service.md.erb +11 -11
  90. data/docs/resources/user.md.erb +20 -20
  91. data/docs/resources/users.md.erb +19 -19
  92. data/docs/resources/vbscript.md.erb +13 -13
  93. data/docs/resources/virtualization.md.erb +71 -0
  94. data/docs/resources/windows_feature.md.erb +10 -10
  95. data/docs/resources/windows_task.md.erb +103 -0
  96. data/docs/resources/wmi.md.erb +10 -10
  97. data/docs/resources/x509_certificate.md.erb +146 -0
  98. data/docs/resources/xinetd_conf.md.erb +17 -17
  99. data/docs/resources/xml.md.erb +75 -0
  100. data/docs/resources/yaml.md.erb +25 -11
  101. data/docs/resources/yum.md.erb +24 -16
  102. data/docs/resources/zfs_dataset.md.erb +63 -0
  103. data/docs/resources/zfs_pool.md.erb +57 -0
  104. data/docs/ruby_usage.md +50 -0
  105. data/docs/shared/matcher_cmp.md.erb +4 -6
  106. data/examples/inheritance/README.md +49 -3
  107. data/examples/inheritance/controls/example.rb +4 -1
  108. data/examples/inheritance/inspec.yml +3 -1
  109. data/examples/meta-profile/README.md +29 -3
  110. data/examples/meta-profile/controls/example.rb +10 -5
  111. data/examples/meta-profile/inspec.yml +4 -8
  112. data/examples/profile/README.md +1 -1
  113. data/examples/profile/controls/example.rb +0 -1
  114. data/examples/profile/controls/gordon.rb +0 -1
  115. data/examples/profile/inspec.yml +1 -1
  116. data/examples/profile-attribute/inspec.yml +1 -1
  117. data/examples/profile-sensitive/README.md +29 -0
  118. data/examples/profile-sensitive/controls/sensitive-failures.rb +9 -0
  119. data/examples/profile-sensitive/controls/sensitive.rb +9 -0
  120. data/examples/profile-sensitive/inspec.yml +8 -0
  121. data/inspec.gemspec +10 -2
  122. data/lib/bundles/inspec-artifact/README.md +1 -0
  123. data/lib/bundles/inspec-artifact/cli.rb +284 -0
  124. data/lib/bundles/inspec-artifact.rb +7 -0
  125. data/lib/bundles/inspec-compliance/README.md +44 -15
  126. data/lib/bundles/inspec-compliance/api.rb +135 -29
  127. data/lib/bundles/inspec-compliance/cli.rb +128 -21
  128. data/lib/bundles/inspec-compliance/configuration.rb +8 -0
  129. data/lib/bundles/inspec-compliance/http.rb +26 -19
  130. data/lib/bundles/inspec-compliance/target.rb +50 -25
  131. data/lib/bundles/inspec-habitat/cli.rb +31 -0
  132. data/lib/bundles/inspec-habitat/log.rb +10 -0
  133. data/lib/bundles/inspec-habitat/profile.rb +372 -0
  134. data/lib/bundles/inspec-habitat.rb +12 -0
  135. data/lib/bundles/inspec-init/templates/profile/controls/example.rb +0 -1
  136. data/lib/bundles/inspec-init/templates/profile/inspec.yml +1 -1
  137. data/lib/bundles/inspec-supermarket/api.rb +7 -5
  138. data/lib/bundles/inspec-supermarket/cli.rb +2 -2
  139. data/lib/fetchers/git.rb +4 -2
  140. data/lib/fetchers/local.rb +4 -1
  141. data/lib/fetchers/url.rb +64 -10
  142. data/lib/inspec/archive/tar.rb +1 -1
  143. data/lib/inspec/backend.rb +26 -3
  144. data/lib/inspec/base_cli.rb +44 -7
  145. data/lib/inspec/cached_fetcher.rb +2 -2
  146. data/lib/inspec/cli.rb +34 -12
  147. data/lib/inspec/completions/fish.sh.erb +34 -0
  148. data/lib/inspec/control_eval_context.rb +0 -2
  149. data/lib/inspec/dependencies/cache.rb +0 -1
  150. data/lib/inspec/dependencies/dependency_set.rb +2 -2
  151. data/lib/inspec/dependencies/lockfile.rb +13 -6
  152. data/lib/inspec/dependencies/requirement.rb +6 -6
  153. data/lib/inspec/dependencies/resolver.rb +4 -4
  154. data/lib/inspec/dsl.rb +3 -5
  155. data/lib/inspec/env_printer.rb +9 -1
  156. data/lib/inspec/exceptions.rb +8 -0
  157. data/lib/inspec/fetcher.rb +1 -1
  158. data/lib/inspec/file_provider.rb +47 -11
  159. data/lib/inspec/library_eval_context.rb +1 -1
  160. data/lib/inspec/metadata.rb +62 -18
  161. data/lib/inspec/method_source.rb +24 -0
  162. data/lib/inspec/objects/attribute.rb +21 -0
  163. data/lib/inspec/objects/control.rb +21 -3
  164. data/lib/inspec/objects/each_loop.rb +2 -1
  165. data/lib/inspec/objects/list.rb +1 -1
  166. data/lib/inspec/objects/or_test.rb +17 -2
  167. data/lib/inspec/objects/tag.rb +27 -0
  168. data/lib/inspec/objects/test.rb +9 -8
  169. data/lib/inspec/objects.rb +1 -0
  170. data/lib/inspec/plugins/fetcher.rb +4 -5
  171. data/lib/inspec/plugins/resource.rb +1 -2
  172. data/lib/inspec/plugins/source_reader.rb +3 -3
  173. data/lib/inspec/plugins.rb +1 -1
  174. data/lib/inspec/polyfill.rb +0 -1
  175. data/lib/inspec/profile.rb +92 -23
  176. data/lib/inspec/profile_context.rb +1 -1
  177. data/lib/inspec/profile_vendor.rb +66 -0
  178. data/lib/inspec/resource.rb +29 -3
  179. data/lib/inspec/rspec_json_formatter.rb +672 -285
  180. data/lib/inspec/rule.rb +12 -16
  181. data/lib/inspec/runner.rb +26 -15
  182. data/lib/inspec/runner_rspec.rb +1 -0
  183. data/lib/inspec/runtime_profile.rb +26 -0
  184. data/lib/inspec/schema.rb +185 -0
  185. data/lib/inspec/secrets/yaml.rb +8 -1
  186. data/lib/inspec/secrets.rb +1 -1
  187. data/lib/inspec/shell.rb +92 -22
  188. data/lib/inspec/source_reader.rb +1 -1
  189. data/lib/inspec/version.rb +1 -1
  190. data/lib/inspec.rb +1 -1
  191. data/lib/matchers/matchers.rb +67 -17
  192. data/lib/resources/aide_conf.rb +162 -0
  193. data/lib/resources/apache.rb +0 -1
  194. data/lib/resources/apache_conf.rb +3 -3
  195. data/lib/resources/apt.rb +2 -1
  196. data/lib/resources/audit_policy.rb +0 -1
  197. data/lib/resources/auditd_conf.rb +1 -2
  198. data/lib/resources/auditd_rules.rb +5 -5
  199. data/lib/resources/bond.rb +1 -1
  200. data/lib/resources/bridge.rb +1 -1
  201. data/lib/resources/command.rb +5 -3
  202. data/lib/resources/crontab.rb +102 -0
  203. data/lib/resources/csv.rb +18 -1
  204. data/lib/resources/dh_params.rb +84 -0
  205. data/lib/resources/directory.rb +7 -3
  206. data/lib/resources/docker.rb +198 -0
  207. data/lib/resources/docker_container.rb +114 -0
  208. data/lib/resources/docker_image.rb +96 -0
  209. data/lib/resources/etc_group.rb +2 -3
  210. data/lib/resources/etc_hosts.rb +81 -0
  211. data/lib/resources/file.rb +88 -30
  212. data/lib/resources/gem.rb +26 -6
  213. data/lib/resources/groups.rb +4 -5
  214. data/lib/resources/grub_conf.rb +3 -4
  215. data/lib/resources/host.rb +175 -35
  216. data/lib/resources/http.rb +73 -0
  217. data/lib/resources/iis_app.rb +103 -0
  218. data/lib/resources/iis_site.rb +23 -0
  219. data/lib/resources/inetd_conf.rb +9 -4
  220. data/lib/resources/interface.rb +4 -3
  221. data/lib/resources/json.rb +37 -14
  222. data/lib/resources/kernel_module.rb +77 -10
  223. data/lib/resources/kernel_parameter.rb +0 -1
  224. data/lib/resources/key_rsa.rb +67 -0
  225. data/lib/resources/limits_conf.rb +3 -4
  226. data/lib/resources/login_def.rb +2 -3
  227. data/lib/resources/mount.rb +34 -4
  228. data/lib/resources/mssql_session.rb +81 -12
  229. data/lib/resources/mysql.rb +0 -1
  230. data/lib/resources/mysql_conf.rb +1 -2
  231. data/lib/resources/mysql_session.rb +18 -7
  232. data/lib/resources/nginx_conf.rb +95 -0
  233. data/lib/resources/ntp_conf.rb +2 -3
  234. data/lib/resources/oracledb_session.rb +138 -0
  235. data/lib/resources/os_env.rb +7 -5
  236. data/lib/resources/package.rb +70 -13
  237. data/lib/resources/packages.rb +111 -0
  238. data/lib/resources/parse_config.rb +3 -4
  239. data/lib/resources/passwd.rb +4 -5
  240. data/lib/resources/pip.rb +29 -21
  241. data/lib/resources/port.rb +133 -14
  242. data/lib/resources/postgres.rb +53 -43
  243. data/lib/resources/postgres_conf.rb +40 -13
  244. data/lib/resources/postgres_hba_conf.rb +101 -0
  245. data/lib/resources/postgres_ident_conf.rb +79 -0
  246. data/lib/resources/postgres_session.rb +28 -20
  247. data/lib/resources/powershell.rb +2 -2
  248. data/lib/resources/processes.rb +61 -26
  249. data/lib/resources/rabbitmq_conf.rb +53 -0
  250. data/lib/resources/registry_key.rb +34 -11
  251. data/lib/resources/security_policy.rb +2 -2
  252. data/lib/resources/service.rb +17 -7
  253. data/lib/resources/ssh_conf.rb +17 -6
  254. data/lib/resources/ssl.rb +3 -4
  255. data/lib/resources/sys_info.rb +1 -1
  256. data/lib/resources/toml.rb +26 -0
  257. data/lib/resources/users.rb +41 -41
  258. data/lib/resources/virtualization.rb +252 -0
  259. data/lib/resources/windows_task.rb +106 -0
  260. data/lib/resources/x509_certificate.rb +143 -0
  261. data/lib/resources/xinetd.rb +18 -2
  262. data/lib/resources/xml.rb +27 -0
  263. data/lib/resources/yaml.rb +10 -2
  264. data/lib/resources/yum.rb +25 -7
  265. data/lib/resources/zfs_dataset.rb +60 -0
  266. data/lib/resources/zfs_pool.rb +49 -0
  267. data/lib/source_readers/flat.rb +2 -1
  268. data/lib/source_readers/inspec.rb +30 -11
  269. data/lib/utils/command_wrapper.rb +3 -3
  270. data/lib/utils/database_helpers.rb +77 -0
  271. data/lib/utils/erlang_parser.rb +192 -0
  272. data/lib/utils/filter.rb +2 -1
  273. data/lib/utils/filter_array.rb +0 -1
  274. data/lib/utils/find_files.rb +1 -2
  275. data/lib/utils/latest_version.rb +21 -0
  276. data/lib/utils/nginx_parser.rb +76 -0
  277. data/lib/utils/object_traversal.rb +5 -4
  278. data/lib/utils/parser.rb +23 -4
  279. data/lib/utils/plugin_registry.rb +3 -3
  280. data/lib/utils/simpleconfig.rb +25 -8
  281. data/lib/utils/spdx.rb +13 -0
  282. data/lib/utils/spdx.txt +344 -0
  283. metadata +168 -22
  284. data/docs/cli.md +0 -342
  285. data/docs/resources/process.md.erb +0 -73
  286. /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
- hash[:message] = e.message
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 format_example(example)
66
- if example.metadata[:description_args].length == 0
67
- code_description = example.metadata[:full_description]
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, :start, :stop, :dump_summary
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
- examples.each do |example|
132
- control = example2control(example, @profiles_info)
133
- next missing.push(example) if control.nil?
134
- dump_one_example(example, control)
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
- @output_hash[:profiles] = @profiles_info
138
- @output_hash[:other_checks] = missing
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 controls_summary
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
- @control_tests.each do |control|
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
- all_tests = @anonymous_tests + @control_tests
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
- private
214
+ def examples
215
+ @output_hash[:controls]
216
+ end
205
217
 
206
- #
207
- # TODO(ssd+vj): We should probably solve this by either ensuring the example has
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 profile_contains_example?(profile, example)
217
- # Heuristic for finding the profile an example came from:
218
- # Case 1: The profile_id on the example matches the name of the profile
219
- # Case 2: The profile contains a control that matches the id of the example
220
- if profile[:name] == example[:profile_id]
221
- true
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, profiles)
230
- profile = example2profile(example, profiles)
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
- STATUS_TYPES = {
247
- 'unknown' => -3,
248
- 'passed' => -2,
249
- 'skipped' => -1,
250
- 'minor' => 1,
251
- 'major' => 2,
252
- 'failed' => 2.5,
253
- 'critical' => 3,
254
- }.freeze
255
-
256
- COLORS = {
257
- 'critical' => "\033[31;1m",
258
- 'major' => "\033[31m",
259
- 'minor' => "\033[33m",
260
- 'failed' => "\033[31m",
261
- 'passed' => "\033[32m",
262
- 'skipped' => "\033[37m",
263
- 'reset' => "\033[0m",
264
- }.freeze
265
-
266
- INDICATORS = {
267
- 'critical' => ' ✖ ',
268
- 'major' => ' ✖ ',
269
- 'minor' => ' ✖ ',
270
- 'failed' => ' ✖ ',
271
- 'skipped' => ' ○ ',
272
- 'passed' => ' ✔ ',
273
- 'unknown' => ' ? ',
274
- 'empty' => ' ',
275
- 'small' => ' ',
276
- }.freeze
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
- @current_profile = nil
287
- @missing_controls = []
288
- @anonymous_tests = []
289
- @control_tests = []
340
+ @all_controls = []
341
+ @profile_printed = false
290
342
  super(*args)
291
343
  end
292
344
 
293
- def close(_notification) # rubocop:disable Metrics/AbcSize
294
- flush_current_control
295
- output.puts('') unless @current_control.nil?
296
- print_tests
297
- output.puts('')
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
- @profiles_info.each do |profile|
300
- next if profile[:already_printed]
301
- @current_profile = profile
302
- next unless print_current_profile
303
- print_line(
304
- color: '', indicator: @indicators['empty'], id: '', profile: '',
305
- summary: 'No tests executed.'
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
- controls_res = controls_summary
311
- tests_res = tests_summary
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
- s = format('Profile Summary: %s%d successful%s, %s%d failures%s, %s%d skipped%s',
314
- COLORS['passed'], controls_res['passed'], COLORS['reset'],
315
- COLORS['failed'], controls_res['failed']['total'], COLORS['reset'],
316
- COLORS['skipped'], controls_res['skipped'], COLORS['reset'])
317
- output.puts(s) if controls_res['total'] > 0
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
- s = format('Test Summary: %s%d successful%s, %s%d failures%s, %s%d skipped%s',
320
- COLORS['passed'], tests_res['passed'], COLORS['reset'],
321
- COLORS['failed'], tests_res['failed'], COLORS['reset'],
322
- COLORS['skipped'], tests_res['skipped'], COLORS['reset'])
323
- output.puts(s) if !@anonymous_tests.empty? || @current_control.nil?
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
- def status_type(data, control)
329
- status = data[:status]
330
- return status if status != 'failed' || control[:impact].nil?
331
- if control[:impact] >= 0.7
332
- 'critical'
333
- elsif control[:impact] >= 0.4
334
- 'major'
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
- 'minor'
337
- end
338
- end
339
-
340
- def current_control_infos
341
- summary_status = STATUS_TYPES['unknown']
342
- skips = []
343
- fails = []
344
- passes = []
345
- @current_control[:results].each do |r|
346
- i = STATUS_TYPES[r[:status_type]]
347
- summary_status = i if i > summary_status
348
- fails.push(r) if i > 0
349
- passes.push(r) if i == STATUS_TYPES['passed']
350
- skips.push(r) if i == STATUS_TYPES['skipped']
351
- end
352
- [fails, skips, passes, STATUS_TYPES.key(summary_status)]
353
- end
354
-
355
- def current_control_title
356
- title = @current_control[:title]
357
- res = @current_control[:results]
358
- if title
359
- title
360
- elsif res.length == 1
361
- # If it's an anonymous control, just go with the only description
362
- # available for the underlying test.
363
- res[0][:code_desc].to_s
364
- elsif res.length == 0
365
- # Empty control block - if it's anonymous, there's nothing we can do.
366
- # Is this case even possible?
367
- 'Empty anonymous control'
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
- # Multiple tests - but no title. Do our best and generate some form of
370
- # identifier or label or name.
371
- title = (res.map { |r| r[:code_desc] }).join('; ')
372
- max_len = MULTI_TEST_CONTROL_SUMMARY_MAX_LEN
373
- title = title[0..(max_len-1)] + '...' if title.length > max_len
374
- title
375
- end
376
- end
377
-
378
- def current_control_summary(fails, skips)
379
- title = current_control_title
380
- res = @current_control[:results]
381
- suffix =
382
- if res.length == 1
383
- # Single test - be nice and just print the exception message if the test
384
- # failed. No need to say "1 failed".
385
- res[0][:message].to_s
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
- (fails.length > 0) ? "#{fails.length} failed" : nil,
389
- (skips.length > 0) ? "#{skips.length} skipped" : nil,
390
- ].compact.join(' ')
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
- if suffix == ''
393
- title
394
- else
395
- title + ' (' + suffix + ')'
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
- @format.gsub(/%\w+/) do |x|
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 + @colors['reset']
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 print_results(all)
415
- all.each do |x|
416
- test_status = x[:status_type]
417
- test_color = @colors[test_status]
418
- indicator = @indicators[x[:status]]
419
- indicator = @indicators['empty'] if indicator.nil?
420
- if x[:message]
421
- msg = x[:code_desc] + "\n" + x[:message]
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
- msg = x[:skip_message] || x[:code_desc]
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
- def print_tests # rubocop:disable Metrics/AbcSize
435
- @anonymous_tests.each do |control|
436
- control_result = control[:results]
437
- title = control_result[0][:code_desc].split[0..1].join(' ')
438
- puts ' ' + title
439
- # iterate over all describe blocks in anonoymous control block
440
- control_result.each do |test|
441
- control_id = ''
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
- # determine title
447
- test_result = test[:skip_message] || test[:code_desc].split[2..-1].join(' ')
448
- # show error message
449
- test_result += "\n" + test[:message] unless test[:message].nil?
775
+ [
776
+ !fails.empty? ? "#{fails.uniq.length} failed" : nil,
777
+ !skips.empty? ? "#{skips.uniq.length} skipped" : nil,
778
+ ].compact.join(' ')
450
779
  end
451
- status_indicator = test[:status_type]
452
- print_line(
453
- color: @colors[status_indicator] || '',
454
- indicator: @indicators['small'] + @indicators[status_indicator] || @indicators['unknown'],
455
- summary: format_lines(test_result, @indicators['empty']),
456
- id: control_id,
457
- profile: control[:profile_id],
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
- def flush_current_control
464
- return if @current_control.nil?
800
+ private
465
801
 
466
- prev_profile = @current_profile
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
- fails, skips, passes, summary_indicator = current_control_infos
471
- summary = current_control_summary(fails, skips)
804
+ alias summary_calculation_needed? summary_calculation_needed
472
805
 
473
- control_id = @current_control[:id].to_s
474
- control_id += ': '
475
- if control_id.start_with? '(generated from '
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
- print_results(fails + skips + passes)
810
+ def summary_has_been_calculated
811
+ @summary_calculation_needed = false
488
812
  end
489
- end
490
813
 
491
- def print_target(before, after)
492
- return if @backend.nil?
493
- connection = @backend.backend
494
- return unless connection.respond_to?(:uri)
495
- output.puts(before + connection.uri + after)
496
- end
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
- def print_current_profile
499
- profile = @current_profile
500
- return false if profile.nil?
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
- output.puts ''
503
- profile[:already_printed] = true
504
- if profile[:name].nil?
505
- print_target('Target: ', "\n\n")
506
- return true
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
- if profile[:title].nil?
510
- output.puts "Profile: #{profile[:name] || 'unknown'}"
511
- else
512
- output.puts "Profile: #{profile[:title]} (#{profile[:name] || 'unknown'})"
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
- output.puts 'Version: ' + (profile[:version] || 'unknown')
516
- print_target('Target: ', "\n")
517
- output.puts
518
- true
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
- def format_example(example)
522
- data = super(example)
523
- control = example2control(data, @profiles_info) || {}
524
- control[:id] = data[:id]
525
- control[:profile_id] = data[:profile_id]
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
- data[:status_type] = status_type(data, control)
528
- dump_one_example(data, control)
882
+ profile[:controls].each do |control|
883
+ next if control[:results].nil?
529
884
 
530
- @current_control ||= control
531
- if control[:id].nil?
532
- @missing_controls.push(data)
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
- data
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