sax :: sax_locator
Interface for associating a SAX event with a document location.core :: union_find
union–find algorithm using an efficient disjoint-set data structure
# 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
# 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 = 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 = 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 = 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
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 = 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) 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, value) 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) 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, system_id) do
log.push(["resolve_entity",
public_id or else "^NULL",
system_id or else "^NULL"])
return super
end
############################################################################
# DTDHandler
redef fun notation_decl(name, public_id, system_id) do
log.push(["notation_decl", name, public_id, system_id])
super
end
redef fun unparsed_entity_decl(name, public_id, system_id) do
log.push(["unparsed_entity_decl", name, public_id, system_id])
super
end
############################################################################
# ContentHandler
redef fun document_locator=(locator) 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, uri) do
log.push(["start_prefix_mapping", prefix, uri])
super
end
redef fun end_prefix_mapping(prefix) do
log.push(["end_prefix_mapping", prefix])
super
end
redef fun start_element(uri, local_name, qname, atts) 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, local_name, qname) do
log.push(["end_element", uri, local_name, qname])
super
end
redef fun characters(str) do
log.push(["characters", str])
super
end
redef fun ignorable_whitespace(str) do
log.push(["ignorable_witespace", str])
super
end
redef fun processing_instruction(target, data) do
log.push(["processing_instruction", target, data or else "^NULL"])
super
end
redef fun skipped_entity(name) do
log.push(["skipped_entity", name])
super
end
############################################################################
# ErrorHandler
redef fun warning(exception) do
log.push(["warning", exception.full_message])
super
end
redef fun error(exception) do
log.push(["error", exception.full_message])
super
end
redef fun fatal_error(exception) 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, model) 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, attribute_name, attribute_type, mode, value) 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, value) 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, value) 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, public_id, system_id) 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) do
log.push(["start_entity", name])
if lexical_handler != null then
lexical_handler.start_entity(name)
end
end
redef fun end_entity(name) 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) 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
# Logger of the expected event sequence.
var expected = new SAXEventLogger
# Logger of the actual event sequence.
var actual = new SAXEventLogger
# The tested SAX reader.
var reader: XMLReader is noinit
private var init_done: Bool = false
fun before_test is before do
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 StringReader(str)))
end
end
lib/saxophonit/testing.nit:11,1--580,3