Replace Nokogiri with REXML in the JUnit formatter

In #1454, we welcomed a newly-revamped JUnit formatter which has
a dependency on Nokogiri. Unfortunately, this had led us to problems
getting InSpec included in Chef omnibus builds (see chef/chef#5937)
because Chef is using Ruby 2.4.1 and the Nokogiri maintainers have
not yet released a windows binary gem that supports Ruby 2.4.x.
This has led to breaking builds in Chef's CI platform and would
block the acceptance of chef/chef#5937.

This change replaces Nokogiri use with REXML instead. While REXML
can be slower than Nokogiri, it does not require native extensions
and is supported on all Chef platforms.

Signed-off-by: Adam Leff <adam@leff.co>
This commit is contained in:
Adam Leff 2017-04-03 14:15:40 -04:00
parent 68a930f141
commit 73d46f9c49
No known key found for this signature in database
GPG key ID: 7A5136DE1C1112F8
6 changed files with 60 additions and 65 deletions

View file

@ -51,18 +51,11 @@ do_build() {
local _libxslt_dir
_bundler_dir="$(pkg_path_for bundler)"
_libxml2_dir="$(pkg_path_for libxml2)"
_libxslt_dir="$(pkg_path_for libxslt)"
export GEM_HOME=${pkg_path}/vendor/bundle
export GEM_PATH=${_bundler_dir}:${GEM_HOME}
export BUNDLE_SILENCE_ROOT_WARNING=1
# don't let bundler split up the nokogiri config string (it breaks
# the build), so specify it as an env var instead
export NOKOGIRI_CONFIG="--use-system-libraries --with-zlib-dir=${_zlib_dir} --with-xslt-dir=${_libxslt_dir} --with-xml2-include=${_libxml2_dir}/include/libxml2 --with-xml2-lib=${_libxml2_dir}/lib"
bundle config build.nokogiri "${NOKOGIRI_CONFIG}"
bundle install --jobs "$(nproc)" --retry 5 --standalone \
--path "$pkg_prefix/bundle" \
--binstubs "$pkg_prefix/bin"

View file

@ -39,7 +39,6 @@ Gem::Specification.new do |spec|
spec.add_dependency 'mixlib-log'
spec.add_dependency 'sslshake', '~> 1'
spec.add_dependency 'parallel', '~> 1.9'
spec.add_dependency 'nokogiri', '~> 1.6'
spec.add_dependency 'faraday', '>=0.9.0'
spec.add_dependency 'toml', '~> 0.1'
spec.add_dependency 'addressable', '~> 2.5'

View file

@ -795,51 +795,61 @@ class InspecRspecJUnit < InspecRspecJson
#
# This is the last method is invoked through the formatter interface.
# Converts the junit formatter constructed output_hash into nokogiri generated
# Converts the junit formatter constructed output_hash into REXML generated
# XML and writes it to output.
#
def close(_notification)
require 'nokogiri'
xml_output = Nokogiri::XML::Builder.new { |xml|
xml.testsuites do
@output_hash[:profiles].each do |profile|
build_profile_xml(xml, profile)
end
end
}.to_xml
output.puts xml_output
require 'rexml/document'
xml_output = REXML::Document.new
xml_output.add(REXML::XMLDecl.new)
testsuites = REXML::Element.new('testsuites')
xml_output.add(testsuites)
@output_hash[:profiles].each do |profile|
testsuites.add(build_profile_xml(profile))
end
formatter = REXML::Formatters::Pretty.new
formatter.compact = true
output.puts formatter.write(xml_output.xml_decl, '')
output.puts formatter.write(xml_output.root, '')
end
private
def build_profile_xml(xml, profile)
xml.testsuite(
name: profile[:name],
tests: count_profile_tests(profile),
failed: count_profile_failed_tests(profile),
) do
profile[:controls].each do |control|
build_control_xml(xml, control)
def build_profile_xml(profile)
profile_xml = REXML::Element.new('testsuite')
profile_xml.add_attribute('name', profile[:name])
profile_xml.add_attribute('tests', count_profile_tests(profile))
profile_xml.add_attribute('failed', count_profile_failed_tests(profile))
profile[:controls].each do |control|
next if control[:results].nil?
control[:results].each do |result|
profile_xml.add(build_result_xml(control, result))
end
end
profile_xml
end
def build_control_xml(xml, control)
return if control[:results].nil?
control[:results].each do |result|
build_result_xml(xml, control, result)
end
end
def build_result_xml(control, result)
result_xml = REXML::Element.new('testcase')
result_xml.add_attribute('name', result[:code_desc])
result_xml.add_attribute('class', control[:title].nil? ? 'Anonymous' : control[:id])
result_xml.add_attribute('time', result[:run_time])
def build_result_xml(xml, control, result)
test_class = control[:title].nil? ? 'Anonymous' : control[:id]
xml.testcase(name: result[:code_desc], class: test_class, time: result[:run_time]) do
if result[:status] == 'failed'
xml.failure(message: result[:message])
elsif result[:status] == 'skipped'
xml.skipped
end
if result[:status] == 'failed'
failure_element = REXML::Element.new('failure')
failure_element.add_attribute('message', result[:message])
result_xml.add(failure_element)
elsif result[:status] == 'skipped'
result_xml.add_element('skipped')
end
result_xml
end
def count_profile_tests(profile)

View file

@ -23,7 +23,6 @@ dependency 'ruby'
dependency 'rubygems'
dependency 'bundler'
dependency 'rb-readline'
dependency 'nokogiri'
dependency 'appbundler'
license :project_license

View file

@ -2,7 +2,7 @@
# author: John Kerry
require 'functional/helper'
require 'nokogiri'
require 'rexml/document'
describe 'inspec exec with junit formatter' do
include FunctionalHelper
@ -11,47 +11,47 @@ describe 'inspec exec with junit formatter' do
out = inspec('exec ' + example_control + ' --format junit --no-create-lockfile')
out.stderr.must_equal ''
out.exit_status.must_equal 0
doc = Nokogiri::XML(out.stdout)
doc.errors.length.must_equal 0
doc = REXML::Document.new(out.stdout)
doc.has_elements?.must_equal true
end
it 'can execute the profile with the junit formatter' do
out = inspec('exec ' + example_profile + ' --format junit --no-create-lockfile')
out.stderr.must_equal ''
out.exit_status.must_equal 0
doc = Nokogiri::XML(out.stdout)
doc.errors.length.must_equal 0
doc = REXML::Document.new(out.stdout)
doc.has_elements?.must_equal true
end
describe 'execute a profile with junit formatting' do
let(:doc) { Nokogiri::XML(inspec('exec ' + example_profile + ' --format junit --no-create-lockfile').stdout) }
let(:doc) { REXML::Document.new(inspec('exec ' + example_profile + ' --format junit --no-create-lockfile').stdout) }
describe 'the document' do
it 'has only one testsuite' do
doc.xpath("//testsuite").length.must_equal 1
doc.elements.to_a("//testsuite").length.must_equal 1
end
end
describe 'the test suite' do
let(:suite) { doc.xpath("//testsuites/testsuite").first}
let(:suite) { doc.elements.to_a("//testsuites/testsuite").first }
it 'must have 5 testcase children' do
suite.xpath("//testcase").length.must_equal 5
suite.elements.to_a("//testcase").length.must_equal 5
end
it 'has the tests attribute with 5 total tests' do
suite["tests"].must_equal "5"
suite.attribute('tests').value.must_equal "5"
end
it 'has the failures attribute with 0 total tests' do
suite["failed"].must_equal "0"
suite.attribute('failed').value.must_equal "0"
end
it 'has 2 elements named "File /tmp should be directory"' do
suite.xpath("//testcase[@name='File /tmp should be directory']").length.must_equal 2
REXML::XPath.match(suite, "//testcase[@name='File /tmp should be directory']").length.must_equal 2
end
describe 'the testcase named "gordon_config Can\'t find file ..."' do
let(:gordon_yml_tests) { suite.xpath("//testcase[@class='gordon-1.0' and @name='gordon_config']") }
let(:gordon_yml_tests) { REXML::XPath.match(suite, "//testcase[@class='gordon-1.0' and @name='gordon_config']") }
let(:first_gordon_test) {gordon_yml_tests.first}
it 'should be unique' do
@ -59,7 +59,7 @@ describe 'inspec exec with junit formatter' do
end
it 'should be skipped' do
first_gordon_test.xpath("//skipped").length.must_equal 1
first_gordon_test.elements.to_a('//skipped').length.must_equal 1
end
end
end

View file

@ -8,7 +8,6 @@ PATH
json (>= 1.8, < 3.0)
method_source (~> 0.8)
mixlib-log
nokogiri (~> 1.6)
parallel (~> 1.9)
pry (~> 0)
rainbow (~> 2)
@ -56,7 +55,7 @@ GEM
concurrent-ruby (1.0.5)
contracts (0.13.0)
diff-lcs (1.3)
docker-api (1.33.2)
docker-api (1.33.3)
excon (>= 0.38.0)
json
dotenv (2.2.0)
@ -67,13 +66,12 @@ GEM
eventmachine (1.2.3)
excon (0.55.0)
execjs (2.7.0)
faraday (0.11.0)
faraday (0.12.0.1)
multipart-post (>= 1.2, < 3)
fast_blank (1.0.0)
fastimage (2.1.0)
ffi (1.9.18)
github-markup (1.5.0)
rinku
github-markup (1.6.0)
gssapi (1.2.0)
ffi (>= 1.0.1)
gyoku (1.3.1)
@ -148,7 +146,6 @@ GEM
middleman-syntax (3.0.0)
middleman-core (>= 3.2)
rouge (~> 2.0)
mini_portile2 (2.1.0)
minitest (5.10.1)
mixlib-log (1.7.1)
mixlib-shellout (2.2.7)
@ -157,8 +154,6 @@ GEM
net-scp (1.2.1)
net-ssh (>= 2.6.5)
net-ssh (4.1.0)
nokogiri (1.7.1)
mini_portile2 (~> 2.1.0)
nori (2.6.0)
padrino-helpers (0.13.3.3)
i18n (~> 0.6, >= 0.6.7)
@ -183,7 +178,6 @@ GEM
rb-inotify (0.9.8)
ffi (>= 0.5.0)
redcarpet (3.4.0)
rinku (2.0.2)
rouge (2.0.7)
rspec (3.5.0)
rspec-core (~> 3.5.0)
@ -233,7 +227,7 @@ GEM
winrm-fs (~> 1.0)
tzinfo (1.2.3)
thread_safe (~> 0.1)
uglifier (3.1.11)
uglifier (3.1.13)
execjs (>= 0.3.0, < 3)
winrm (2.1.3)
builder (>= 2.1.2)