Module: Beaker::LoggerJunit

Defined in:
lib/beaker/logger_junit.rb

Overview

The Beaker JUnit Logger class This module handles message reporting from Beaker to the JUnit format

There is a specific pattern for using this class. Here's a list of example usages:

Constant Summary collapse

INVALID_XML_CHARS =

Every character is_valid_xml rejects, as one pattern. Note that this tracks that method rather than the specification it cites: #xD is absent from both, and both take the last range as 0x100000 rather than the 0x10000 the specification gives. Changing either would change the contents of every junit report, so they are left alone here and the specs walk the whole range to keep the two in step.

/[^\u{9}\u{A}\u{20}-\u{D7FF}\u{E000}-\u{FFFD}\u{100000}-\u{10FFFF}]/

Class Method Summary collapse

Class Method Details

.copy_stylesheet_into_xml_dir(stylesheet, xml_file) ⇒ Object

copies given stylesheet into the directory of the xml file given

Parameters:

  • stylesheet (String)

    Path to the stylesheet file

  • xml_file (String)

    Path to the xml file

Returns:

  • nil



69
70
71
72
73
# File 'lib/beaker/logger_junit.rb', line 69

def self.copy_stylesheet_into_xml_dir(stylesheet, xml_file)
  return if File.file?(File.join(File.dirname(xml_file), File.basename(stylesheet)))

  FileUtils.copy(stylesheet, File.join(File.dirname(xml_file), File.basename(stylesheet)))
end

.escape_invalid_xml_chars(string) ⇒ String

Escape invalid XML UTF-8 codes from provided string, see http://www.w3.org/TR/xml/#charsets for valid character specification

Parameters:

  • string (String)

    The string to remove invalid codes from

Returns:

  • (String)

    Properly escaped string



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/beaker/logger_junit.rb', line 135

def self.escape_invalid_xml_chars string
  # Bytes that are not valid in their own encoding cannot be represented in
  # xml at all, and would make the gsub below raise. Drop them, the same
  # way Logger#convert does further upstream. Raising matters more here
  # than it might look: TestSuiteResult calls #format_cdata from inside a
  # blanket rescue that abandons the whole junit document, so one stray
  # byte in one sublog loses every test result for the run.
  string = string.dup.force_encoding(Encoding::UTF_8) unless string.encoding == Encoding::UTF_8
  string = string.scrub('') unless string.valid_encoding?

  # This walked the string a character at a time, unpacking each one into
  # an array and joining it back into a string just to read its codepoint.
  # That is around four objects per character, and it runs over the whole
  # sublog of every test case at the end of a run: 28 million objects, and
  # 98MB of permanently grown ruby heap, for a 6MB suite.
  string.gsub(INVALID_XML_CHARS) { |char| "\\#{char.ord}" }
end

.finish(doc, xml_file) ⇒ Object

writes out xml content for a doc

Parameters:

  • doc (REXML::Document)

    doc containing content to write

  • xml_file (String)

    Path to the xml file to write

Returns:

  • nil



41
42
43
44
45
# File 'lib/beaker/logger_junit.rb', line 41

def self.finish(doc, xml_file)
  # junit/name.xml will be created in a directory relative to the CWD

  File.open(xml_file, 'w') { |f| doc.write(f, 2) }
end

.format_cdata(string) ⇒ String

Remove color codes and invalid XML characters from provided string

Parameters:

  • string (String)

    The string to format

Returns:

  • (String)

    the correctly formatted cdata



119
120
121
# File 'lib/beaker/logger_junit.rb', line 119

def self.format_cdata string
  self.escape_invalid_xml_chars(Logger.strip_color_codes(string))
end

.get_doc_for_filename(filename, stylesheet, already_exists) ⇒ REXML::Document

gives the document object for a particular file

Parameters:

  • filename (String)

    Path to the file that you're opening

  • stylesheet (String)

    Path to the stylesheet for this doc

  • already_exists (Boolean)

    Whether or not the file already exists

Returns:

  • (REXML::Document)

    Doc that you want to write in



103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/beaker/logger_junit.rb', line 103

def self.get_doc_for_filename(filename, stylesheet, already_exists)
  if already_exists
    doc = REXML::Document.new File.open(filename)
  else
    # no existing file, create a new one
    doc = REXML::Document.new
    doc << REXML::XMLDecl.new("1.0", "UTF-8")
    instruction_content = "type='text/xsl' href='#{File.basename(stylesheet)}'"
    doc << REXML::Instruction.new("xml-stylesheet", instruction_content)
  end
  return doc
end

.get_testsuites_from_doc(doc, name, already_existed) ⇒ Rexml::Element

sets up doc & gives us the suites for the testsuite named

Parameters:

  • doc (REXML::Document)

    Doc that you're getting suites from

  • name (String)

    Testsuite node name

  • already_existed (Boolean)

    Whether or not the doc already existed

Returns:

  • (Rexml::Element)

    testsuites



82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/beaker/logger_junit.rb', line 82

def self.get_testsuites_from_doc(doc, name, already_existed)
  # check to see if an output file already exists, if it does add or replace test suite data
  if already_existed
    suites = REXML::XPath.first(doc, "testsuites")
    # remove old data
    suites.elements.each("testsuite") do |e|
      suites.delete_element e if /#{name}/.match?(e.name)
    end
  else
    suites = doc.add_element(REXML::Element.new('testsuites'))
  end
  return suites
end

.get_xml_contents(xml_file, name, stylesheet) ⇒ REXML::Document, REXML::Element

gets the xml doc & suites in order to build your xml output on top of

Parameters:

  • xml_file (String)

    Path to the xml file

  • name (String)

    Name of the testsuite you're writing

  • stylesheet (String)

    Path to the stylesheet file

Returns:

  • (REXML::Document)

    doc to use for your xml content

  • (REXML::Element)

    suites to add your content to



55
56
57
58
59
60
61
# File 'lib/beaker/logger_junit.rb', line 55

def self.get_xml_contents(xml_file, name, stylesheet)
  self.copy_stylesheet_into_xml_dir(stylesheet, xml_file)
  xml_file_already_exists = File.file?(xml_file)
  doc = self.get_doc_for_filename(xml_file, stylesheet, xml_file_already_exists)
  suites = self.get_testsuites_from_doc(doc, name, xml_file_already_exists)
  return doc, suites
end

.is_valid_xml(int) ⇒ Boolean

Determine if the provided number falls in the range of accepted xml unicode values See http://www.w3.org/TR/xml/#charsets for valid for valid character specifications.

Parameters:

  • int (Integer)

    The number to check against

Returns:

  • (Boolean)

    True, if the number corresponds to a valid xml unicode character, otherwise false



157
158
159
160
161
162
163
164
# File 'lib/beaker/logger_junit.rb', line 157

def self.is_valid_xml(int)
  return (int == 0x9 or
    int == 0xA or
    (int >= 0x0020 and int <= 0xD7FF) or
    (int >= 0xE000 and int <= 0xFFFD) or
    (int >= 0x100000 and int <= 0x10FFFF)
         )
end

.write_xml(xml_file, stylesheet, &block) ⇒ Object

writes the xml created in the block to the xml file given

Note: Error Recovery should take place in the caller of this method in order to recover gracefully

Parameters:

  • xml_file (String)

    Path to the xml file

  • stylesheet (String)

    Path to the stylesheet file

  • block (Proc)

    XML message construction block

Returns:

  • nil



20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/beaker/logger_junit.rb', line 20

def self.write_xml(xml_file, stylesheet, &block)
  doc, suites = self.get_xml_contents(xml_file, name, stylesheet)

  if block
    case block.arity
    when 2
      yield doc, suites
    else
      raise ArgumentError.new "write_xml block takes 2 arguments, not #{block.arity}"
    end
  end

  self.finish(doc, xml_file)
end