Upgrade Beaker 3 to 4
[integration/packaging/puppet-opendaylight.git] / spec / spec_helper_acceptance.rb
1 require 'beaker-rspec/spec_helper'
2 require 'beaker-rspec/helpers/serverspec'
3 require 'beaker-puppet'
4
5 include Beaker::DSL::InstallUtils::FOSSUtils
6 include Beaker::DSL::InstallUtils::ModuleUtils
7 include Beaker::DSL::Helpers::PuppetHelpers
8
9 # Install Puppet on all Beaker hosts
10 unless ENV['BEAKER_provision'] == 'no'
11   hosts.each do |host|
12     # Install Puppet
13     install_puppet_agent_on(host, {:puppet_collection => "pc1"})
14   end
15 end
16
17 RSpec.configure do |c|
18   # Project root
19   proj_root = File.expand_path(File.join(File.dirname(__FILE__), '..'))
20
21   # Readable test descriptions
22   c.formatter = :documentation
23
24   # Configure all nodes in nodeset
25   c.before :suite do
26     # Install opendaylight module on any/all Beaker hosts
27     # TODO: Should this be done in host.each loop?
28     puppet_module_install(:source => proj_root, :module_name => 'opendaylight')
29     hosts.each do |host|
30       # Install stdlib, a dependency of the odl mod
31       on host, puppet('module', 'install', 'puppetlabs-stdlib'), { :acceptable_exit_codes => [0] }
32       # Install apt, a dependency of the deb install method
33       on host, puppet('module', 'install', 'puppetlabs-apt'), { :acceptable_exit_codes => [0] }
34     end
35   end
36 end
37
38 #
39 # NB: These are a library of helper fns used by the Beaker tests
40 #
41
42 # NB: There are a large number of helper functions used in these tests.
43 # They make this code much more friendly, but may need to be referenced.
44 # The serverspec helpers (`should`, `be_running`...) are documented here:
45 #   http://serverspec.org/resource_types.html
46
47 def install_odl(options = {})
48   # Install params are passed via environment var, set in Rakefile
49   # Changing the installed version of ODL via `puppet apply` is not supported
50   # by puppet-odl, so it's not possible to vary these params in the same
51   # Beaker test run. Do a different run passing different env vars.
52   rpm_repo = ENV['RPM_REPO']
53   deb_repo = ENV['DEB_REPO']
54
55   # NB: These param defaults should match the ones used by the opendaylight
56   #   class, which are defined in opendaylight::params
57   # TODO: Remove this possible source of bugs^^
58   # Extract params if given, defaulting to odl class defaults if not
59   extra_features = options.fetch(:extra_features, ['odl-restconf'])
60   default_features = options.fetch(:default_features, ['standard', 'wrap', 'ssh'])
61   odl_rest_port = options.fetch(:odl_rest_port, 8181)
62   odl_bind_ip = options.fetch(:odl_bind_ip, '0.0.0.0')
63   log_levels = options.fetch(:log_levels, {})
64   enable_ha = options.fetch(:enable_ha, false)
65   ha_node_ips = options.fetch(:ha_node_ips, [])
66   ha_node_index = options.fetch(:ha_node_index, 0)
67   ha_db_modules = options.fetch(:ha_db_modules, { 'default' => false })
68   username = options.fetch(:username, 'admin')
69   password = options.fetch(:password, 'admin')
70   log_max_size = options.fetch(:log_max_size, '10GB')
71   log_max_rollover = options.fetch(:log_max_rollover, 2)
72   snat_mechanism = options.fetch(:snat_mechanism, 'controller')
73   enable_tls = options.fetch(:enable_tls, false)
74   tls_keystore_password = options.fetch(:tls_keystore_password, 'dummypass')
75   log_mechanism = options.fetch(:log_mechanism, 'file')
76   inherit_dscp_marking = options.fetch(:inherit_dscp_marking, false)
77   stats_polling_enabled = options.fetch(:stats_polling_enabled, false)
78
79   # Build script for consumption by Puppet apply
80   it 'should work idempotently with no errors' do
81     pp = <<-EOS
82     class { 'opendaylight':
83       rpm_repo => '#{rpm_repo}',
84       deb_repo => '#{deb_repo}',
85       default_features => #{default_features},
86       extra_features => #{extra_features},
87       odl_rest_port => #{odl_rest_port},
88       odl_bind_ip => '#{odl_bind_ip}',
89       enable_ha => #{enable_ha},
90       ha_node_ips => #{ha_node_ips},
91       ha_node_index => #{ha_node_index},
92       ha_db_modules => #{ha_db_modules},
93       log_levels => #{log_levels},
94       username => #{username},
95       password => #{password},
96       log_max_size => '#{log_max_size}',
97       log_max_rollover => #{log_max_rollover},
98       snat_mechanism => #{snat_mechanism},
99       enable_tls => #{enable_tls},
100       tls_keystore_password => #{tls_keystore_password},
101       log_mechanism => #{log_mechanism},
102       inherit_dscp_marking => #{inherit_dscp_marking},
103       stats_polling_enabled => #{stats_polling_enabled},
104     }
105     EOS
106
107     # Apply our Puppet manifest on the Beaker host
108     apply_manifest(pp, :catch_failures => true)
109
110     # Not checking for idempotence because of false failures
111     # related to package manager cache updates outputting to
112     # stdout and different IDs for the puppet manifest apply.
113     # I think this is a limitation in how Beaker can check
114     # for changes, not a problem with the Puppet module.
115     end
116 end
117
118 # Shared function that handles generic validations
119 # These should be common for all odl class param combos
120 def generic_validations(options = {})
121   java_opts = options.fetch(:java_opts, '-Djava.net.preferIPv4Stack=true')
122
123   # Verify ODL's directory
124   describe file('/opt/opendaylight/') do
125     it { should be_directory }
126     it { should be_owned_by 'odl' }
127     it { should be_grouped_into 'odl' }
128   end
129
130   # Verify ODL's systemd service
131   describe service('opendaylight') do
132     it { should be_enabled }
133     it { should be_enabled.with_level(3) }
134     it { should be_running.under('systemd') }
135   end
136
137   # Creation handled by RPM or Deb
138   describe user('odl') do
139     it { should exist }
140     it { should belong_to_group 'odl' }
141     # NB: This really shouldn't have a slash at the end!
142     #     The home dir set by the RPM is `/opt/opendaylight`.
143     #     Since we use the trailing slash elsewhere else, this
144     #     may look like a style issue. It isn't! It will make
145     #     Beaker tests fail if it ends with a `/`. A future
146     #     version of the ODL RPM may change this.
147     it { should have_home_directory '/opt/opendaylight' }
148   end
149
150   # Creation handled by RPM or Deb
151   describe group('odl') do
152     it { should exist }
153   end
154
155   # This should not be the odl user's home dir
156   describe file('/home/odl') do
157     # Home dir shouldn't be created for odl user
158     it { should_not be_directory }
159   end
160
161   # OpenDaylight will appear as a Java process
162   describe process('java') do
163     it { should be_running }
164   end
165
166   # Should contain Karaf features config file
167   describe file('/opt/opendaylight/etc/org.apache.karaf.features.cfg') do
168     it { should be_file }
169     it { should be_owned_by 'odl' }
170     it { should be_grouped_into 'odl' }
171   end
172
173   # Should contain karaf file with Java options set
174   describe file('/opt/opendaylight/bin/karaf') do
175     it { should be_file }
176     it { should be_owned_by 'odl' }
177     it { should be_grouped_into 'odl' }
178     its(:content) { should match /^EXTRA_JAVA_OPTS=#{java_opts}$/ }
179   end
180
181   # Should contain ODL NB port config file
182   describe file('/opt/opendaylight/etc/jetty.xml') do
183     it { should be_file }
184     it { should be_owned_by 'odl' }
185     it { should be_grouped_into 'odl' }
186   end
187
188   # Should contain log level config file
189   describe file('/opt/opendaylight/etc/org.ops4j.pax.logging.cfg') do
190     it { should be_file }
191     it { should be_owned_by 'odl' }
192     it { should be_grouped_into 'odl' }
193   end
194
195   if ['centos-7', 'centos-7-docker'].include? ENV['RS_SET']
196     # Validations for modern Red Hat family OSs
197
198     # Verify ODL systemd .service file
199     describe file('/usr/lib/systemd/system/opendaylight.service') do
200       it { should be_file }
201       it { should be_owned_by 'root' }
202       it { should be_grouped_into 'root' }
203       it { should be_mode '644' }
204     end
205
206     # Java 8 should be installed
207     describe package('java-1.8.0-openjdk') do
208       it { should be_installed }
209     end
210
211   # Ubuntu 16.04 specific validation
212   elsif ['ubuntu-16', 'ubuntu-16-docker'].include? ENV['RS_SET']
213
214     # Verify ODL systemd .service file
215     describe file('/lib/systemd/system/opendaylight.service') do
216       it { should be_file }
217       it { should be_owned_by 'root' }
218       it { should be_grouped_into 'root' }
219       it { should be_mode '644' }
220     end
221
222     # Java 8 should be installed
223     describe package('openjdk-8-jre-headless') do
224       it { should be_installed }
225     end
226
227   else
228     fail("Unexpected RS_SET (host OS): #{ENV['RS_SET']}")
229   end
230 end
231
232 # Shared function for validations related to log file settings
233 def log_settings_validations(options = {})
234   # Should contain log level config file with correct file size and rollover values
235   log_max_size = options.fetch(:log_max_size, '10GB')
236   log_max_rollover = options.fetch(:log_max_rollover, 2)
237   log_mechanism = options.fetch(:log_mechanism, 'file')
238
239   if log_mechanism == 'console'
240     describe file('/opt/opendaylight/etc/org.ops4j.pax.logging.cfg') do
241       it { should be_file }
242       it { should be_owned_by 'odl' }
243       it { should be_grouped_into 'odl' }
244       its(:content) { should match /^karaf.log.console=INFO/ }
245       its(:content) { should match /^log4j2.appender.console.direct = true/ }
246     end
247   else
248     describe file('/opt/opendaylight/etc/org.ops4j.pax.logging.cfg') do
249       it { should be_file }
250       it { should be_owned_by 'odl' }
251       it { should be_grouped_into 'odl' }
252       its(:content) { should match /^log4j2.appender.rolling.policies.size.size = #{log_max_size}/ }
253       its(:content) { should match /^log4j2.appender.rolling.strategy.type = DefaultRolloverStrategy/ }
254       its(:content) { should match /^log4j2.appender.rolling.strategy.max = #{log_max_rollover}/ }
255     end
256   end
257 end
258
259 # Shared function for validations related to the Karaf config file
260 def karaf_config_validations(options = {})
261   # NB: These param defaults should match the ones used by the opendaylight
262   #   class, which are defined in opendaylight::params
263   # TODO: Remove this possible source of bugs^^
264   extra_features = options.fetch(:extra_features, [])
265   default_features = options.fetch(:default_features, ['standard', 'wrap', 'ssh'])
266
267   # Create one list of all of the features
268   features = default_features + extra_features
269
270   describe file('/opt/opendaylight/etc/org.apache.karaf.features.cfg') do
271     it { should be_file }
272     it { should be_owned_by 'odl' }
273     it { should be_grouped_into 'odl' }
274     its(:content) { should match /^featuresBoot=#{features.join(",")}/ }
275   end
276 end
277
278 # Shared function for validations related to the ODL REST port config file
279 def port_config_validations(options = {})
280   # NB: This param default should match the one used by the opendaylight
281   #   class, which is defined in opendaylight::params
282   # TODO: Remove this possible source of bugs^^
283   odl_rest_port = options.fetch(:odl_rest_port, 8181)
284
285   describe file('/opt/opendaylight/etc/jetty.xml') do
286     it { should be_file }
287     it { should be_owned_by 'odl' }
288     it { should be_grouped_into 'odl' }
289     its(:content) { should match /Property name="jetty.port" default="#{odl_rest_port}"/ }
290   end
291
292   describe file('/opt/opendaylight/etc/org.ops4j.pax.web.cfg') do
293     it { should be_file }
294     it { should be_owned_by 'odl' }
295     it { should be_grouped_into 'odl' }
296     its(:content) { should match /org.osgi.service.http.port = #{odl_rest_port}/ }
297   end
298 end
299
300 # Shared function for validations related to the ODL bind IP
301 def odl_bind_ip_validation(options = {})
302   # NB: This param default should match the one used by the opendaylight
303   #   class, which is defined in opendaylight::params
304   # TODO: Remove this possible source of bugs^^
305   odl_bind_ip = options.fetch(:odl_bind_ip, '0.0.0.0')
306
307   if odl_bind_ip != '0.0.0.0'
308     describe file('/opt/opendaylight/etc/org.apache.karaf.shell.cfg') do
309       it { should be_file }
310       it { should be_owned_by 'odl' }
311       it { should be_grouped_into 'odl' }
312       its(:content) { should match /sshHost = #{odl_bind_ip}/ }
313     end
314
315     describe command("loop_count=0; until [[ \$loop_count -ge 30 ]]; do netstat -punta | grep 8101 | grep #{odl_bind_ip} && break; loop_count=\$[\$loop_count+1]; sleep 1; done; echo \"Waited \$loop_count seconds to detect ODL karaf bound to IP\"") do
316       its(:exit_status) { should eq 0 }
317     end
318   end
319 end
320
321 # Shared function for validations related to custom logging verbosity
322 def log_level_validations(options = {})
323   # NB: This param default should match the one used by the opendaylight
324   #   class, which is defined in opendaylight::params
325   # TODO: Remove this possible source of bugs^^
326   log_levels = options.fetch(:log_levels, {})
327
328   if log_levels.empty?
329     # Should contain log level config file
330     describe file('/opt/opendaylight/etc/org.ops4j.pax.logging.cfg') do
331       it { should be_file }
332       it { should be_owned_by 'odl' }
333       it { should be_grouped_into 'odl' }
334     end
335   else
336     # Should contain log level config file
337     describe file('/opt/opendaylight/etc/org.ops4j.pax.logging.cfg') do
338       it { should be_file }
339       it { should be_owned_by 'odl' }
340       it { should be_grouped_into 'odl' }
341     end
342     # Verify each custom log level config entry
343     log_levels.each_pair do |logger, level|
344       underscored_version = "#{logger}".gsub('.', '_')
345       describe file('/opt/opendaylight/etc/org.ops4j.pax.logging.cfg') do
346         it { should be_file }
347         it { should be_owned_by 'odl' }
348         it { should be_grouped_into 'odl' }
349         its(:content) { should match /^log4j2.logger.#{underscored_version}.level = #{level}/ }
350         its(:content) { should match /^log4j2.logger.#{underscored_version}.name = #{logger}/ }
351       end
352     end
353   end
354 end
355
356 # Shared function for validations related to ODL OVSDB HA config
357 def enable_ha_validations(options = {})
358   # NB: This param default should match the one used by the opendaylight
359   #   class, which is defined in opendaylight::params
360   # TODO: Remove this possible source of bugs^^
361   enable_ha = options.fetch(:enable_ha, false)
362   ha_node_ips = options.fetch(:ha_node_ips, [])
363   odl_bind_ip = options.fetch(:odl_bind_ip, '0.0.0.0')
364   ha_db_modules = options.fetch(:ha_db_modules, { 'default' => false })
365   # HA_NODE_IPS size
366   ha_node_count = ha_node_ips.size
367
368   if (enable_ha) && (ha_node_count < 2)
369     # Check for HA_NODE_COUNT < 2
370     fail("Number of HA nodes less than 2: #{ha_node_count} and HA Enabled")
371   end
372
373   if enable_ha
374     ha_node_index = ha_node_ips.index(odl_bind_ip)
375     describe file('/opt/opendaylight/configuration/initial/akka.conf') do
376       it { should be_file }
377       it { should be_owned_by 'odl' }
378       it { should be_grouped_into 'odl' }
379       its(:content) { should match /roles\s*=\s*\["member-#{ha_node_index}"\]/ }
380     end
381
382     ha_db_modules.each do |mod, urn|
383       describe file('/opt/opendaylight/configuration/initial/module-shards.conf') do
384         it { should be_file }
385         it { should be_owned_by 'odl' }
386         it { should be_grouped_into 'odl' }
387         its(:content) { should match /name = "#{mod}"/ }
388       end
389
390       if mod == 'default'
391         describe file('/opt/opendaylight/configuration/initial/modules.conf') do
392           it { should be_file }
393           it { should be_owned_by 'odl' }
394           it { should be_grouped_into 'odl' }
395         end
396       else
397         describe file('/opt/opendaylight/configuration/initial/modules.conf') do
398           it { should be_file }
399           it { should be_owned_by 'odl' }
400           it { should be_grouped_into 'odl' }
401           its(:content) { should match /name = "#{mod}"/ }
402           its(:content) { should match /namespace = "#{urn}"/ }
403         end
404       end
405     end
406   end
407 end
408
409 # Shared function that handles validations specific to RPM-type installs
410 def rpm_validations()
411   rpm_repo = ENV['RPM_REPO']
412
413   describe yumrepo('opendaylight') do
414     it { should exist }
415     it { should be_enabled }
416   end
417
418   describe package('opendaylight') do
419     it { should be_installed }
420   end
421 end
422
423 # Shared function that handles validations specific to Deb-type installs
424 def deb_validations()
425   deb_repo = ENV['DEB_REPO']
426   # Check ppa
427   # Docs: http://serverspec.org/resource_types.html#ppa
428   describe ppa(deb_repo) do
429     it { should exist }
430     it { should be_enabled }
431   end
432
433   describe package('opendaylight') do
434     it { should be_installed }
435   end
436 end
437
438 # Shared function for validations related to username/password
439 def username_password_validations(options = {})
440   # NB: This param default should match the one used by the opendaylight
441   #   class, which is defined in opendaylight::params
442   # TODO: Remove this possible source of bugs^^
443   odl_username = options.fetch(:username, 'admin')
444   odl_password = options.fetch(:password, 'admin')
445   odl_check_url = 'http://127.0.0.1:8181/restconf'
446
447   describe file('/opt/opendaylight/data/idmlight.db.mv.db') do
448     it { should be_file }
449   end
450
451   describe command("loop_count=0; until [[ \$loop_count -ge 300 ]]; do curl -o /dev/null --fail --silent --head -u #{odl_username}:#{odl_password} #{odl_check_url} && break; loop_count=\$[\$loop_count+1]; sleep 1; done; echo \"Waited \$loop_count seconds for ODL to become active\"") do
452     its(:exit_status) { should eq 0 }
453   end
454
455   describe command("curl -o /dev/null --fail --silent --head -u #{odl_username}:#{odl_password} #{odl_check_url}") do
456     its(:exit_status) { should eq 0 }
457   end
458 end
459
460 # Shared function for validations related to the SNAT config file
461 def snat_mechanism_validations(options = {})
462   # NB: This param default should match the one used by the opendaylight
463   #   class, which is defined in opendaylight::params
464   # TODO: Remove this possible source of bugs^^
465   snat_mechanism = options.fetch(:snat_mechanism, 'controller')
466
467   describe file('/opt/opendaylight/etc/opendaylight/datastore/initial/config/netvirt-natservice-config.xml') do
468     it { should be_file }
469     it { should be_owned_by 'odl' }
470     it { should be_grouped_into 'odl' }
471     its(:content) { should match /<nat-mode>#{snat_mechanism}<\/nat-mode>/ }
472   end
473 end
474
475 # Shared function for validations related to SFC
476 def sfc_validations(options = {})
477   # NB: This param default should match the one used by the opendaylight
478   #   class, which is defined in opendaylight::params
479   # TODO: Remove this possible source of bugs^^
480
481   extra_features = options.fetch(:extra_features, [])
482   if extra_features.include? 'odl-netvirt-sfc'
483     sfc_enabled = true
484   else
485     sfc_enabled = false
486   end
487
488   describe file('/opt/opendaylight/etc/opendaylight/datastore/initial/config/genius-itm-config.xml') do
489     it { should be_file }
490     it { should be_owned_by 'odl' }
491     it { should be_grouped_into 'odl' }
492     its(:content) { should match /<gpe-extension-enabled>#{sfc_enabled}<\/gpe-extension-enabled>/ }
493   end
494 end
495
496 # Shared function for validations related to tos value for DSCP marking
497 def dscp_validations(options = {})
498   # NB: This param default should match the one used by the opendaylight
499   #   class, which is defined in opendaylight::params
500   # TODO: Remove this possible source of bugs^^
501
502   inherit_dscp_marking = options.fetch(:inherit_dscp_marking, false)
503
504   if inherit_dscp_marking
505     describe file('/opt/opendaylight/etc/opendaylight/datastore/initial/config/genius-itm-config.xml') do
506       it { should be_file }
507       it { should be_owned_by 'odl' }
508       it { should be_grouped_into 'odl' }
509       its(:content) { should match /<default-tunnel-tos>inherit<\/default-tunnel-tos>/ }
510     end
511   end
512 end
513
514 def websocket_address_validations(options = {})
515   # NB: This param default should match the one used by the opendaylight
516   #   class, which is defined in opendaylight::params
517   # TODO: Remove this possible source of bugs^^
518   odl_bind_ip = options.fetch(:odl_bind_ip, '0.0.0.0')
519
520   if not odl_bind_ip.eql? '0.0.0.0'
521     describe file('/opt/opendaylight/etc/org.opendaylight.restconf.cfg') do
522       it { should be_file }
523       it { should be_owned_by 'odl' }
524       it { should be_grouped_into 'odl' }
525       its(:content) { should match /^websocket-address=#{odl_bind_ip}/ }
526     end
527   else
528     describe file('/opt/opendaylight/etc/org.opendaylight.restconf.cfg') do
529       it { should be_file }
530     end
531   end
532 end
533
534 def tls_validations(options = {})
535   # NB: This param default should match the one used by the opendaylight
536   #   class, which is defined in opendaylight::params
537   # TODO: Remove this possible source of bugs^^
538   tls_keystore_password = options.fetch(:tls_keystore_password)
539   odl_rest_port = options.fetch(:odl_rest_port, 8181)
540
541   describe file('/opt/opendaylight/etc/org.ops4j.pax.web.cfg') do
542     it { should be_file }
543     it { should be_owned_by 'odl' }
544     it { should be_grouped_into 'odl' }
545     its(:content) { should match /org.osgi.service.http.port.secure = #{odl_rest_port}/ }
546     its(:content) { should match /org.ops4j.pax.web.ssl.keystore = configuration\/ssl\/ctl.jks/ }
547     its(:content) { should match /org.ops4j.pax.web.ssl.password = #{tls_keystore_password}/ }
548     its(:content) { should match /org.ops4j.pax.web.ssl.keypassword = #{tls_keystore_password}/ }
549     its(:content) { should match /org.osgi.service.http.secure.enabled = true/ }
550     its(:content) { should match /org.osgi.service.http.enabled = false/ }
551   end
552
553   describe file('/opt/opendaylight/etc/org.opendaylight.ovsdb.library.cfg') do
554     it { should be_file }
555     it { should be_owned_by 'odl' }
556     it { should be_grouped_into 'odl' }
557     its(:content) { should match /use-ssl = true/ }
558   end
559
560   describe file('/opt/opendaylight/etc/opendaylight/datastore/initial/config/default-openflow-connection-config.xml') do
561     it { should be_file }
562     it { should be_owned_by 'odl' }
563     it { should be_grouped_into 'odl' }
564     its(:content) { should match /<keystore-password>#{tls_keystore_password}<\/keystore-password>/ }
565     its(:content) { should match /<truststore-password>#{tls_keystore_password}<\/truststore-password>/ }
566     its(:content) { should match /<transport-protocol>TLS<\/transport-protocol>/ }
567   end
568
569   describe file('/opt/opendaylight/etc/opendaylight/datastore/initial/config/aaa-cert-config.xml') do
570     it { should be_file }
571     it { should be_owned_by 'odl' }
572     it { should be_grouped_into 'odl' }
573     its(:content) { should match /<store-password>#{tls_keystore_password}<\/store-password>/ }
574     its(:content) { should match /<use-mdsal>false<\/use-mdsal>/ }
575   end
576
577   describe file('/opt/opendaylight/etc/jetty.xml') do
578     it { should be_file }
579     it { should be_owned_by 'odl' }
580     it { should be_grouped_into 'odl' }
581     its(:content) { should match /<Property name="jetty.secure.port" default="#{odl_rest_port}" \/>/ }
582   end
583 end
584
585 # Shared function for validations related to OVS statistics polling
586 def stats_polling_validations(options = {})
587   # NB: This param default should match the one used by the opendaylight
588   #   class, which is defined in opendaylight::params
589   # TODO: Remove this possible source of bugs^^
590
591   stats_polling_enabled = options.fetch(:stats_polling_enabled, false)
592   describe file('/opt/opendaylight/etc/org.opendaylight.openflowplugin.cfg') do
593     it { should be_file }
594     it { should be_owned_by 'odl' }
595     it { should be_grouped_into 'odl' }
596     its(:content) { should match /is-statistics-polling-on=#{stats_polling_enabled}/ }
597   end
598 end