xml: Add classes to help testing a SAX parser.
authorJean-Christophe Beaupré <jcbrinfo@users.noreply.github.com>
Thu, 25 Sep 2014 19:25:03 +0000 (15:25 -0400)
committerJean-Christophe Beaupré <jcbrinfo@users.noreply.github.com>
Mon, 27 Oct 2014 17:50:01 +0000 (13:50 -0400)
Will be used to test SAXophoNit (A SAX parser in Nit).

Signed-off-by: Jean-Christophe Beaupré <jcbrinfo@users.noreply.github.com>

lib/saxophonit/saxophonit.nit [new file with mode: 0644]
lib/saxophonit/test_testing.nit [new file with mode: 0644]
lib/saxophonit/testing.nit [new file with mode: 0644]

diff --git a/lib/saxophonit/saxophonit.nit b/lib/saxophonit/saxophonit.nit
new file mode 100644 (file)
index 0000000..22d4314
--- /dev/null
@@ -0,0 +1,12 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# This file is free software, which comes along with NIT. This software is
+# distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. You can modify it is you want, provided this header
+# is kept unaltered, and a notification of the changes is added.
+# You are allowed to redistribute it and sell it, alone or is a part of
+# another product.
+
+# A SAX 2 parser in Nit.
+module saxophonit
diff --git a/lib/saxophonit/test_testing.nit b/lib/saxophonit/test_testing.nit
new file mode 100644 (file)
index 0000000..3bedb3b
--- /dev/null
@@ -0,0 +1,148 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# This file is free software, which comes along with NIT. This software is
+# distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. You can modify it is you want, provided this header
+# is kept unaltered, and a notification of the changes is added.
+# You are allowed to redistribute it and sell it, alone or is a part of
+# another product.
+
+# Test suite for `testing`.
+module test_testing is test_suite
+
+import saxophonit::testing
+import test_suite
+
+class TestSaxEventLogger
+       super TestSuite
+
+       # Constants for diff formatting.
+
+       # Treminal’s default formatting.
+       private var default: String is noinit
+
+       # Formatting for insertions.
+       private var ins: String is noinit
+
+       # Formatting for emphased insertions.
+       private var ins_em: String is noinit
+
+       # Formatting for deletions.
+       private var del: String is noinit
+
+       # Formatting for emphased deletions
+       private var del_em: String is noinit
+
+
+       private var a: SAXEventLogger = new SAXEventLogger
+       private var b: SAXEventLogger = new SAXEventLogger
+
+       private var init_done: Bool = false
+
+       redef fun before_test do
+               super
+               if not init_done then
+                       default = a.term_default
+                       ins = a.term_insertion
+                       ins_em = a.term_insertion_emphasis
+                       del = a.term_deletion
+                       del_em = a.term_deletion_emphasis
+                       init_done = true
+               end
+               a.clear_log
+               b.clear_log
+       end
+
+       private fun assert_equals(id: Int, expected: String, actual: String) do
+               assert equals: expected == actual else
+                       sys.stderr.write("\nAssert {id} failed. Expected:\n")
+                       sys.stderr.write(expected)
+                       sys.stderr.write("\nGot:\n")
+                       sys.stderr.write(actual)
+                       sys.stderr.write("\n")
+               end
+       end
+
+       fun test_diff_empty do
+               assert "" == a.diff(b).to_s
+               assert "" == b.diff(a).to_s
+       end
+
+       fun test_diff_equal1 do
+               b.start_document
+               a.start_document
+               assert "" == a.diff(b).to_s
+               assert "" == b.diff(a).to_s
+       end
+
+       fun test_diff_equal2 do
+               b.start_document
+               b.end_document
+               a.start_document
+               a.end_document
+               assert "" == a.diff(b).to_s
+               assert "" == b.diff(a).to_s
+       end
+
+       fun test_diff_insertion do
+               var exp: String
+               var test: String
+
+               b.start_document
+               b.start_dtd("html", "", "")
+               b.end_dtd
+               b.end_document
+
+               a.start_document
+               a.start_dtd("html", "", "")
+               a.end_document
+
+               exp = "= 0|start_document\n" +
+                               "= 1|start_dtd; html; ; \n" +
+                               "{del}< 2|{del_em}end_document{del}{default}\n" +
+                               "{ins}> 2|{ins_em}end_dtd{ins}{default}\n" +
+                               "{ins}> 3|{ins_em}end_document{ins}{default}\n"
+               test = a.diff(b).to_s
+               assert_equals(1, exp, test)
+
+               exp = "= 0|start_document\n" +
+                               "= 1|start_dtd; html; ; \n" +
+                               "{del}< 2|{del_em}end_dtd{del}{default}\n" +
+                               "{ins}> 2|{ins_em}end_document{ins}{default}\n" +
+                               "{del}< 3|{del_em}end_document{del}{default}\n"
+               test = b.diff(a).to_s
+               assert_equals(2, exp, test)
+       end
+
+       fun test_diff_edition do
+               var exp: String
+               var test: String
+
+               b.start_document
+               b.start_dtd("html", "", "")
+               b.end_dtd
+               b.end_document
+
+               a.start_document
+               a.start_dtd("html", "foo", "")
+               a.end_dtd
+               a.end_document
+
+               exp = "= 0|start_document\n" +
+                               "{del}< 1|start_dtd; html; {del_em}foo{del}; {default}\n" +
+                               "{ins}> 1|start_dtd; html; {ins_em}{ins}; {default}\n" +
+                               "= 2|end_dtd\n" +
+                               "= 3|end_document\n"
+               test = a.diff(b).to_s
+               assert_equals(1, exp, test)
+
+               exp = "= 0|start_document\n" +
+                               "{del}< 1|start_dtd; html; {del_em}{del}; {default}\n" +
+                               "{ins}> 1|start_dtd; html; {ins_em}foo{ins}; {default}\n" +
+                               "= 2|end_dtd\n" +
+                               "= 3|end_document\n"
+               test = b.diff(a).to_s
+               assert_equals(2, exp, test)
+       end
+end
diff --git a/lib/saxophonit/testing.nit b/lib/saxophonit/testing.nit
new file mode 100644 (file)
index 0000000..c940b15
--- /dev/null
@@ -0,0 +1,595 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# This file is free software, which comes along with NIT. This software is
+# distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. You can modify it is you want, provided this header
+# is kept unaltered, and a notification of the changes is added.
+# You are allowed to redistribute it and sell it, alone or is a part of
+# another product.
+
+# Various utilities to help testing SAXophoNit (and SAX parsers in general).
+module saxophonit::testing
+
+import sax::xml_reader
+import sax::input_source
+import sax::helpers::xml_filter_impl
+import sax::ext::decl_handler
+import sax::ext::lexical_handler
+import console
+import test_suite
+
+# A filter that internally log events it recieves.
+#
+# Usually, when testing, 2 `SAXEventLogger` are used: one on which methods are
+# manually called to simulate expected results, and another on which we attach
+# the tested `XMLReader`. Then, we can compare logs using `diff`.
+#
+# Note: In order to test the `XMLReader` behaviour with ill-formed documents,
+# fatal errors are not thrown by default.
+#
+# SEE: SAXTestSuite
+class SAXEventLogger
+       super XMLFilterImpl
+       super DeclHandler
+       super LexicalHandler
+
+       # The logged events.
+       #
+       # Each entry begins with the name of the event. Entries are sorted in the
+       # order they fired (the oldest first). Two event loggers have equivalent
+       # logs if and only if they received the same events in the same order and
+       # with equivalent arguments.
+       private var log: Array[Array[String]] = new Array[Array[String]]
+
+       # http://xml.org/sax/properties/declaration-handler
+       private var decl_handler: nullable DeclHandler = null
+       private var decl_handler_uri = "http://xml.org/sax/properties/declaration-handler"
+
+       # http://xml.org/sax/properties/lexical-handler
+       private var lexical_handler: nullable LexicalHandler = null
+       private var lexical_handler_uri = "http://xml.org/sax/properties/declaration-handler"
+
+
+       # Constants for diff formatting.
+
+       # Treminal’s default formatting.
+       var term_default: String = (new TermCharFormat).to_s
+
+       # Formatting for insertions.
+       var term_insertion: String =
+                       (new TermCharFormat).green_fg.normal_weight.to_s
+
+       # Formatting for emphased insertions.
+       var term_insertion_emphasis: String =
+                       (new TermCharFormat).green_fg.bold.to_s
+
+       # Formatting for deletions.
+       var term_deletion: String =
+                       (new TermCharFormat).red_fg.normal_weight.to_s
+
+       # Formatting for emphased deletions
+       var term_deletion_emphasis: String =
+                       (new TermCharFormat).red_fg.bold.to_s
+
+
+       # Clear the internal log.
+       fun clear_log do
+               log.clear
+       end
+
+       # Show the differences between the internal logs of `self` and `expected`.
+       #
+       # If there is no differences, return an empty string. Else, return a string
+       # designed to be printed in the terminal. In this case, `=` means “in both”,
+       # `<` means “in `self`” and `>` means “in `expected`”.
+       fun diff(expected: SAXEventLogger): Text do
+               var buf = new FlatBuffer
+               var sub_diff: Array[Int]
+               var equal: Bool
+               var i: Int = 0
+               var min: Int
+               var max: Int
+
+               if log.length < expected.log.length then
+                       equal = false
+                       min = log.length
+                       max = expected.log.length
+               else if expected.log.length < log.length then
+                       equal = false
+                       min = expected.log.length
+                       max = log.length
+               else
+                       equal = true
+                       min = log.length
+                       max = log.length
+               end
+
+               while i < min do
+                       sub_diff = diff_entry(log[i], expected.log[i])
+                       if sub_diff.length > 0 then
+                               if equal then
+                                       diff_append_matches(buf, log, [0..i[)
+                                       equal = false
+                               end
+                               diff_append_deletion(buf, log, i, sub_diff)
+                               diff_append_insertion(buf, expected.log, i, sub_diff)
+                       else if not equal then
+                               diff_append_matches(buf, log, [i..i])
+                       end
+                       i += 1
+               end
+               if log.length < expected.log.length then
+                       while i < max do
+                               diff_append_insertion(buf, expected.log, i,
+                                               [0..(expected.log[i].length)[)
+                               i += 1
+                       end
+               else
+                       while i < max do
+                               diff_append_deletion(buf, log, i, [0..(log[i].length)[)
+                               i += 1
+                       end
+               end
+               return buf
+       end
+
+       # Return the list of positions where `actual` and `expected` mismatch.
+       #
+       # Indexes are in ascending order.
+       private fun diff_entry(actual: Array[String], expected: Array[String]):
+                       Array[Int] do
+               var result = new Array[Int]
+               var i: Int = 0
+               var min: Int
+               var max: Int
+
+               if actual.length < expected.length then
+                       min = actual.length
+                       max = expected.length
+               else if expected.length < actual.length then
+                       min = expected.length
+                       max = actual.length
+               else
+                       min = actual.length
+                       max = actual.length
+               end
+
+               while i < min do
+                       if expected[i] != actual[i] then
+                               result.push(i)
+                       end
+                       i += 1
+               end
+               result.insert_all([i..max[, result.length)
+               return result
+       end
+
+       # Append matches to the diff.
+       #
+       # Parameters:
+       #
+       # * `buf`: buffer for the diff.
+       # * `log`: original log.
+       # * `range`: range to append to the diff.
+       private fun diff_append_matches(buf: Buffer, log: Array[Array[String]],
+                       range: Range[Int]) do
+               for i in range do
+                       buf.append("= {i}|{log[i].join("; ")}\n")
+               end
+       end
+
+       # Append a deletion to the diff.
+       #
+       # Parameters:
+       #
+       # * `buf`: buffer for the diff.
+       # * `log`: log that contains the deleted entry.
+       # * `entry_index`: index of the deleted entry in `log`.
+       # * `sorted_mismatches`: sorted list of indexes of the items to emphasize
+       # in the specified entry.
+       private fun diff_append_deletion(buf: Buffer, log: Array[Array[String]],
+                       entry_index: Int, sorted_mismatches: Collection[Int]) do
+               var sub_buf = new FlatBuffer
+
+               buf.append(term_deletion)
+               buf.append("< {entry_index}|")
+               diff_append_mismatch_entry(buf, log[entry_index], sorted_mismatches,
+                       term_deletion, term_deletion_emphasis)
+               buf.append(term_default)
+               buf.append("\n")
+       end
+
+       # Append a insertion to the diff.
+       #
+       # Parameters:
+       #
+       # * `buf`: buffer for the diff.
+       # * `log`: log that contains the inserted entry.
+       # * `entry_index`: index of the inserted entry in `log`.
+       # * `sorted_mismatches`: sorted list of indexes of the items to emphasize
+       # in the specified entry.
+       private fun diff_append_insertion(buf: Buffer, log: Array[Array[String]],
+                       entry_index: Int, sorted_mismatches: Collection[Int]) do
+               buf.append(term_insertion)
+               buf.append("> {entry_index}|")
+               diff_append_mismatch_entry(buf, log[entry_index], sorted_mismatches,
+                       term_insertion, term_insertion_emphasis)
+               buf.append(term_default)
+               buf.append("\n")
+       end
+
+       # Show an entry of a mismatch (without the margin).
+       #
+       # Append the string designed to be printed in the terminal to the
+       # specified buffer.
+       #
+       # Parameters:
+       #
+       # * `buf`: output buffer.
+       # * `entry`: entry to format.
+       # * `sorted_mismatches`: sorted list of indexes of the items to emphasize.
+       # * `term_normal`: terminal control code to re-apply the formatting that was
+       # in force prior calling this method.
+       # * `term_emphasis`: terminal control code to apply to items listed in
+       # `sorted_mismatches`.
+       private fun diff_append_mismatch_entry(buf: Buffer, entry: Array[String],
+                       sorted_mismatches: Collection[Int], term_normal: String,
+                       term_emphasis: String) do
+               var i: Int = 0
+               var j = sorted_mismatches.iterator
+               var length = entry.length
+
+               while i < length do
+                       while j.is_ok and j.item < i do
+                               j.next
+                       end
+                       if j.is_ok and j.item == i then
+                               buf.append(term_emphasis)
+                               buf.append(entry[i])
+                               buf.append(term_normal)
+                       else
+                               buf.append(entry[i])
+                       end
+                       i += 1
+                       if i < length then
+                               buf.append("; ")
+                       end
+               end
+       end
+
+       ############################################################################
+       # XMLReader
+
+       redef fun property(name: String): nullable Object do
+               assert sax_recognized: parent != null else
+                       sys.stderr.write("Property: {name}\n")
+               end
+               if decl_handler_uri == name then
+                       assert property_readable: property_readable(name) else
+                               sys.stderr.write("Property: {name}\n")
+                       end
+                       return decl_handler
+               else if lexical_handler_uri == name then
+                       assert property_readable: property_readable(name) else
+                               sys.stderr.write("Property: {name}\n")
+                       end
+                       return lexical_handler
+               else
+                       return parent.property(name)
+               end
+       end
+
+       redef fun property=(name: String, value: nullable Object) do
+               assert sax_recognized: parent != null else
+                       sys.stderr.write("Property: {name}\n")
+               end
+               if decl_handler_uri == name then
+                       assert property_readable: property_writable(name) else
+                               sys.stderr.write("Property: {name}\n")
+                       end
+                       decl_handler = value.as(nullable DeclHandler)
+               else if lexical_handler_uri == name then
+                       assert property_readable: property_writable(name) else
+                               sys.stderr.write("Property: {name}\n")
+                       end
+                       lexical_handler = value.as(nullable LexicalHandler)
+               else
+                       parent.property(name) = value
+               end
+       end
+
+       redef fun parse(input: InputSource) do
+               assert parent_is_not_null: parent != 0 else
+                       sys.stderr.write("No parent for filter.")
+               end
+               if parent.feature_writable(decl_handler_uri) then
+                       parent.property(decl_handler_uri) = self
+               end
+               if parent.feature_writable(lexical_handler_uri) then
+                       parent.property(lexical_handler_uri) = self
+               end
+               super
+       end
+
+
+       ############################################################################
+       # EntityResolver
+
+       redef fun resolve_entity(public_id: nullable String,
+                       system_id: nullable String):
+                       nullable InputSource do
+               log.push(["resolve_entity",
+                               public_id or else "^NULL",
+                               system_id or else "^NULL"])
+               return super
+       end
+
+
+       ############################################################################
+       # DTDHandler
+
+       redef fun notation_decl(name: String, public_id: String,
+                       system_id: String) do
+               log.push(["notation_decl", name, public_id, system_id])
+               super
+       end
+
+       redef fun unparsed_entity_decl(name: String, public_id: String,
+                       system_id: String) do
+               log.push(["unparsed_entity_decl", name, public_id, system_id])
+               super
+       end
+
+
+       ############################################################################
+       # ContentHandler
+
+       redef fun document_locator=(locator: SAXLocator) do
+               log.push(["document_locator=",
+                               locator.public_id or else "^NULL",
+                               locator.system_id or else "^NULL",
+                               locator.line_number.to_s,
+                               locator.column_number.to_s])
+               super
+       end
+
+       redef fun start_document do
+               log.push(["start_document"])
+               super
+       end
+
+       redef fun end_document do
+               log.push(["end_document"])
+               super
+       end
+
+       redef fun start_prefix_mapping(prefix: String, uri: String) do
+               log.push(["start_prefix_mapping", prefix, uri])
+               super
+       end
+
+       redef fun end_prefix_mapping(prefix: String) do
+               log.push(["end_prefix_mapping", prefix])
+               super
+       end
+
+       redef fun start_element(uri: String, local_name: String, qname: String,
+                       atts: Attributes) do
+               var entry = new Array[String]
+               var i = 0
+               var length = atts.length
+
+               entry.push("start_element")
+               entry.push(uri)
+               entry.push(local_name)
+               entry.push(qname)
+               while i < length do
+                       entry.push(atts.uri(i) or else "^NULL")
+                       entry.push(atts.local_name(i) or else "^NULL")
+                       entry.push(atts.qname(i) or else "^NULL")
+                       entry.push(atts.type_of(i) or else "^NULL")
+                       entry.push(atts.value_of(i) or else "^NULL")
+                       i += 1
+               end
+               log.push(entry)
+               super
+       end
+
+       redef fun end_element(uri: String, local_name: String, qname: String) do
+               log.push(["end_element", uri, local_name, qname])
+               super
+       end
+
+       redef fun characters(str: String) do
+               log.push(["characters", str])
+               super
+       end
+
+       redef fun ignorable_whitespace(str: String) do
+               log.push(["ignorable_witespace", str])
+               super
+       end
+
+       redef fun processing_instruction(target: String, data: nullable String) do
+               log.push(["processing_instruction", target, data or else "^NULL"])
+               super
+       end
+
+       redef fun skipped_entity(name: String) do
+               log.push(["skipped_entity", name])
+               super
+       end
+
+
+       ############################################################################
+       # ErrorHandler
+
+       redef fun warning(exception: SAXParseException) do
+               log.push(["warning", exception.full_message])
+               super
+       end
+
+       redef fun error(exception: SAXParseException) do
+               log.push(["error", exception.full_message])
+               super
+       end
+
+       redef fun fatal_error(exception: SAXParseException) do
+               log.push(["fatal_error", exception.full_message])
+               if error_handler != null then
+                       error_handler.fatal_error(exception)
+               end
+       end
+
+
+       ############################################################################
+       # DeclHandler
+
+       redef fun element_decl(name: String, model: String) do
+               log.push(["element_decl", name, model])
+               if decl_handler != null then
+                       decl_handler.element_decl(name, model)
+               end
+       end
+
+       redef fun attribute_decl(element_name: String,
+                       attribute_name: String,
+                       attribute_type: String,
+                       mode: nullable String,
+                       value: nullable String) do
+               log.push(["attribute_decl",
+                               element_name,
+                               attribute_name,
+                               attribute_type,
+                               mode or else "^NULL",
+                               value or else "^NULL"])
+               if decl_handler != null then
+                       decl_handler.attribute_decl(element_name, attribute_name,
+                                       attribute_type, mode, value)
+               end
+       end
+
+       redef fun internal_entity_decl(name: String, value: String) do
+               log.push(["internal_entity_decl", name, value])
+               if decl_handler != null then
+                       decl_handler.internal_entity_decl(name, value)
+               end
+       end
+
+       redef fun external_entity_decl(name: String, value: String) do
+               log.push(["external_entity_decl", name, value])
+               if decl_handler != null then
+                       decl_handler.external_entity_decl(name, value)
+               end
+       end
+
+
+       ############################################################################
+       # LexicalHandler
+
+       redef fun start_dtd(name: String, public_id: nullable String,
+                       system_id: nullable String) do
+               log.push(["start_dtd", name,
+                               public_id or else "^NULL",
+                               system_id or else "^NULL"])
+               if lexical_handler != null then
+                       lexical_handler.start_dtd(name, public_id, system_id)
+               end
+       end
+
+       redef fun end_dtd do
+               log.push(["end_dtd"])
+               if lexical_handler != null then
+                       lexical_handler.end_dtd
+               end
+       end
+
+       redef fun start_entity(name: String) do
+               log.push(["start_entity", name])
+               if lexical_handler != null then
+                       lexical_handler.start_entity(name)
+               end
+       end
+
+       redef fun end_entity(name: String) do
+               log.push(["end_entity", name])
+               if lexical_handler != null then
+                       lexical_handler.end_entity(name)
+               end
+       end
+
+       redef fun start_cdata do
+               log.push(["start_cdata"])
+               if lexical_handler != null then
+                       lexical_handler.start_cdata
+               end
+       end
+
+       redef fun end_cdata do
+               log.push(["end_cdata"])
+               if lexical_handler != null then
+                       lexical_handler.end_cdata
+               end
+       end
+
+       redef fun comment(str: String) do
+               log.push(["comment", str])
+               if lexical_handler != null then
+                       lexical_handler.comment(str)
+               end
+       end
+end
+
+
+# Base class for test suites on a SAX reader.
+abstract class SAXTestSuite
+       super TestSuite
+
+       # Logger of the expected event sequence.
+       var expected: SAXEventLogger = new SAXEventLogger
+
+       # Logger of the actual event sequence.
+       var actual: SAXEventLogger = new SAXEventLogger
+
+       # The tested SAX reader.
+       var reader: XMLReader is noinit
+
+       private var init_done: Bool = false
+
+       redef fun before_test do
+               super
+               if not init_done then
+                       reader = create_reader
+                       actual.parent = reader
+                       init_done = true
+               end
+               reader.feature("http://xml.org/sax/features/namespaces") = true
+               reader.feature("http://xml.org/sax/features/namespace-prefixes") = false
+               expected.clear_log
+               actual.clear_log
+       end
+
+       # Create a new SAX reader.
+       #
+       # This method is called at initialization to set `reader`.
+       fun create_reader: XMLReader is abstract
+
+       # Assert logs are equal.
+       fun assert_equals do
+               var diff = actual.diff(expected)
+
+               assert equals: diff.length <= 0 else
+                       sys.stderr.write("\n")
+                       sys.stderr.write("# {actual.term_deletion}< Actual{actual.term_default}\n")
+                       sys.stderr.write("# {actual.term_insertion}> Expected{actual.term_default}\n")
+                       sys.stderr.write(diff)
+                       sys.stderr.write("\n")
+               end
+       end
+
+       # Make the reader parse the specified string
+       fun parse_string(str: String) do
+               actual.parse(new InputSource.with_stream(new StringIStream(str)))
+       end
+end