1987d8ca19a8a4e4f091b3e177ce224f72fc5083
[nit.git] / lib / saxophonit / testing.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # This file is free software, which comes along with NIT. This software is
4 # distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
5 # without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
6 # PARTICULAR PURPOSE. You can modify it is you want, provided this header
7 # is kept unaltered, and a notification of the changes is added.
8 # You are allowed to redistribute it and sell it, alone or is a part of
9 # another product.
10
11 # Various utilities to help testing SAXophoNit (and SAX parsers in general).
12 module saxophonit::testing
13
14 import sax::xml_reader
15 import sax::input_source
16 import sax::helpers::xml_filter_impl
17 import sax::ext::decl_handler
18 import sax::ext::lexical_handler
19 import console
20 import test_suite
21
22 # A filter that internally log events it recieves.
23 #
24 # Usually, when testing, 2 `SAXEventLogger` are used: one on which methods are
25 # manually called to simulate expected results, and another on which we attach
26 # the tested `XMLReader`. Then, we can compare logs using `diff`.
27 #
28 # Note: In order to test the `XMLReader` behaviour with ill-formed documents,
29 # fatal errors are not thrown by default.
30 #
31 # SEE: SAXTestSuite
32 class SAXEventLogger
33 super XMLFilterImpl
34 super DeclHandler
35 super LexicalHandler
36
37 # The logged events.
38 #
39 # Each entry begins with the name of the event. Entries are sorted in the
40 # order they fired (the oldest first). Two event loggers have equivalent
41 # logs if and only if they received the same events in the same order and
42 # with equivalent arguments.
43 private var log = new Array[Array[String]]
44
45 # http://xml.org/sax/properties/declaration-handler
46 private var decl_handler: nullable DeclHandler = null
47 private var decl_handler_uri = "http://xml.org/sax/properties/declaration-handler"
48
49 # http://xml.org/sax/properties/lexical-handler
50 private var lexical_handler: nullable LexicalHandler = null
51 private var lexical_handler_uri = "http://xml.org/sax/properties/declaration-handler"
52
53
54 # Constants for diff formatting.
55
56 # Treminal’s default formatting.
57 var term_default: String = (new TermCharFormat).to_s
58
59 # Formatting for insertions.
60 var term_insertion: String =
61 (new TermCharFormat).green_fg.normal_weight.to_s
62
63 # Formatting for emphased insertions.
64 var term_insertion_emphasis: String =
65 (new TermCharFormat).green_fg.bold.to_s
66
67 # Formatting for deletions.
68 var term_deletion: String =
69 (new TermCharFormat).red_fg.normal_weight.to_s
70
71 # Formatting for emphased deletions
72 var term_deletion_emphasis: String =
73 (new TermCharFormat).red_fg.bold.to_s
74
75
76 # Clear the internal log.
77 fun clear_log do
78 log.clear
79 end
80
81 # Show the differences between the internal logs of `self` and `expected`.
82 #
83 # If there is no differences, return an empty string. Else, return a string
84 # designed to be printed in the terminal. In this case, `=` means “in both”,
85 # `<` means “in `self`” and `>` means “in `expected`”.
86 fun diff(expected: SAXEventLogger): Text do
87 var buf = new FlatBuffer
88 var sub_diff: Array[Int]
89 var equal: Bool
90 var i = 0
91 var min: Int
92 var max: Int
93
94 if log.length < expected.log.length then
95 equal = false
96 min = log.length
97 max = expected.log.length
98 else if expected.log.length < log.length then
99 equal = false
100 min = expected.log.length
101 max = log.length
102 else
103 equal = true
104 min = log.length
105 max = log.length
106 end
107
108 while i < min do
109 sub_diff = diff_entry(log[i], expected.log[i])
110 if sub_diff.length > 0 then
111 if equal then
112 diff_append_matches(buf, log, [0..i[)
113 equal = false
114 end
115 diff_append_deletion(buf, log, i, sub_diff)
116 diff_append_insertion(buf, expected.log, i, sub_diff)
117 else if not equal then
118 diff_append_matches(buf, log, [i..i])
119 end
120 i += 1
121 end
122 if log.length < expected.log.length then
123 while i < max do
124 diff_append_insertion(buf, expected.log, i,
125 [0..(expected.log[i].length)[)
126 i += 1
127 end
128 else
129 while i < max do
130 diff_append_deletion(buf, log, i, [0..(log[i].length)[)
131 i += 1
132 end
133 end
134 return buf
135 end
136
137 # Return the list of positions where `actual` and `expected` mismatch.
138 #
139 # Indexes are in ascending order.
140 private fun diff_entry(actual: Array[String], expected: Array[String]):
141 Array[Int] do
142 var result = new Array[Int]
143 var i = 0
144 var min: Int
145 var max: Int
146
147 if actual.length < expected.length then
148 min = actual.length
149 max = expected.length
150 else if expected.length < actual.length then
151 min = expected.length
152 max = actual.length
153 else
154 min = actual.length
155 max = actual.length
156 end
157
158 while i < min do
159 if expected[i] != actual[i] then
160 result.push(i)
161 end
162 i += 1
163 end
164 result.insert_all([i..max[, result.length)
165 return result
166 end
167
168 # Append matches to the diff.
169 #
170 # Parameters:
171 #
172 # * `buf`: buffer for the diff.
173 # * `log`: original log.
174 # * `range`: range to append to the diff.
175 private fun diff_append_matches(buf: Buffer, log: Array[Array[String]],
176 range: Range[Int]) do
177 for i in range do
178 buf.append("= {i}|{log[i].join("; ")}\n")
179 end
180 end
181
182 # Append a deletion to the diff.
183 #
184 # Parameters:
185 #
186 # * `buf`: buffer for the diff.
187 # * `log`: log that contains the deleted entry.
188 # * `entry_index`: index of the deleted entry in `log`.
189 # * `sorted_mismatches`: sorted list of indexes of the items to emphasize
190 # in the specified entry.
191 private fun diff_append_deletion(buf: Buffer, log: Array[Array[String]],
192 entry_index: Int, sorted_mismatches: Collection[Int]) do
193 buf.append(term_deletion)
194 buf.append("< {entry_index}|")
195 diff_append_mismatch_entry(buf, log[entry_index], sorted_mismatches,
196 term_deletion, term_deletion_emphasis)
197 buf.append(term_default)
198 buf.append("\n")
199 end
200
201 # Append a insertion to the diff.
202 #
203 # Parameters:
204 #
205 # * `buf`: buffer for the diff.
206 # * `log`: log that contains the inserted entry.
207 # * `entry_index`: index of the inserted entry in `log`.
208 # * `sorted_mismatches`: sorted list of indexes of the items to emphasize
209 # in the specified entry.
210 private fun diff_append_insertion(buf: Buffer, log: Array[Array[String]],
211 entry_index: Int, sorted_mismatches: Collection[Int]) do
212 buf.append(term_insertion)
213 buf.append("> {entry_index}|")
214 diff_append_mismatch_entry(buf, log[entry_index], sorted_mismatches,
215 term_insertion, term_insertion_emphasis)
216 buf.append(term_default)
217 buf.append("\n")
218 end
219
220 # Show an entry of a mismatch (without the margin).
221 #
222 # Append the string designed to be printed in the terminal to the
223 # specified buffer.
224 #
225 # Parameters:
226 #
227 # * `buf`: output buffer.
228 # * `entry`: entry to format.
229 # * `sorted_mismatches`: sorted list of indexes of the items to emphasize.
230 # * `term_normal`: terminal control code to re-apply the formatting that was
231 # in force prior calling this method.
232 # * `term_emphasis`: terminal control code to apply to items listed in
233 # `sorted_mismatches`.
234 private fun diff_append_mismatch_entry(buf: Buffer, entry: Array[String],
235 sorted_mismatches: Collection[Int], term_normal: String,
236 term_emphasis: String) do
237 var i = 0
238 var j = sorted_mismatches.iterator
239 var length = entry.length
240
241 while i < length do
242 while j.is_ok and j.item < i do
243 j.next
244 end
245 if j.is_ok and j.item == i then
246 buf.append(term_emphasis)
247 buf.append(entry[i])
248 buf.append(term_normal)
249 else
250 buf.append(entry[i])
251 end
252 i += 1
253 if i < length then
254 buf.append("; ")
255 end
256 end
257 end
258
259 ############################################################################
260 # XMLReader
261
262 redef fun property(name: String): nullable Object do
263 assert sax_recognized: parent != null else
264 sys.stderr.write("Property: {name}\n")
265 end
266 if decl_handler_uri == name then
267 assert property_readable: property_readable(name) else
268 sys.stderr.write("Property: {name}\n")
269 end
270 return decl_handler
271 else if lexical_handler_uri == name then
272 assert property_readable: property_readable(name) else
273 sys.stderr.write("Property: {name}\n")
274 end
275 return lexical_handler
276 else
277 return parent.property(name)
278 end
279 end
280
281 redef fun property=(name: String, value: nullable Object) do
282 assert sax_recognized: parent != null else
283 sys.stderr.write("Property: {name}\n")
284 end
285 if decl_handler_uri == name then
286 assert property_readable: property_writable(name) else
287 sys.stderr.write("Property: {name}\n")
288 end
289 decl_handler = value.as(nullable DeclHandler)
290 else if lexical_handler_uri == name then
291 assert property_readable: property_writable(name) else
292 sys.stderr.write("Property: {name}\n")
293 end
294 lexical_handler = value.as(nullable LexicalHandler)
295 else
296 parent.property(name) = value
297 end
298 end
299
300 redef fun parse(input: InputSource) do
301 assert parent_is_not_null: parent != 0 else
302 sys.stderr.write("No parent for filter.")
303 end
304 if parent.feature_writable(decl_handler_uri) then
305 parent.property(decl_handler_uri) = self
306 end
307 if parent.feature_writable(lexical_handler_uri) then
308 parent.property(lexical_handler_uri) = self
309 end
310 super
311 end
312
313
314 ############################################################################
315 # EntityResolver
316
317 redef fun resolve_entity(public_id: nullable String,
318 system_id: nullable String):
319 nullable InputSource do
320 log.push(["resolve_entity",
321 public_id or else "^NULL",
322 system_id or else "^NULL"])
323 return super
324 end
325
326
327 ############################################################################
328 # DTDHandler
329
330 redef fun notation_decl(name: String, public_id: String,
331 system_id: String) do
332 log.push(["notation_decl", name, public_id, system_id])
333 super
334 end
335
336 redef fun unparsed_entity_decl(name: String, public_id: String,
337 system_id: String) do
338 log.push(["unparsed_entity_decl", name, public_id, system_id])
339 super
340 end
341
342
343 ############################################################################
344 # ContentHandler
345
346 redef fun document_locator=(locator: SAXLocator) do
347 log.push(["document_locator=",
348 locator.public_id or else "^NULL",
349 locator.system_id or else "^NULL",
350 locator.line_number.to_s,
351 locator.column_number.to_s])
352 super
353 end
354
355 redef fun start_document do
356 log.push(["start_document"])
357 super
358 end
359
360 redef fun end_document do
361 log.push(["end_document"])
362 super
363 end
364
365 redef fun start_prefix_mapping(prefix: String, uri: String) do
366 log.push(["start_prefix_mapping", prefix, uri])
367 super
368 end
369
370 redef fun end_prefix_mapping(prefix: String) do
371 log.push(["end_prefix_mapping", prefix])
372 super
373 end
374
375 redef fun start_element(uri: String, local_name: String, qname: String,
376 atts: Attributes) do
377 var entry = new Array[String]
378 var i = 0
379 var length = atts.length
380
381 entry.push("start_element")
382 entry.push(uri)
383 entry.push(local_name)
384 entry.push(qname)
385 while i < length do
386 entry.push(atts.uri(i) or else "^NULL")
387 entry.push(atts.local_name(i) or else "^NULL")
388 entry.push(atts.qname(i) or else "^NULL")
389 entry.push(atts.type_of(i) or else "^NULL")
390 entry.push(atts.value_of(i) or else "^NULL")
391 i += 1
392 end
393 log.push(entry)
394 super
395 end
396
397 redef fun end_element(uri: String, local_name: String, qname: String) do
398 log.push(["end_element", uri, local_name, qname])
399 super
400 end
401
402 redef fun characters(str: String) do
403 log.push(["characters", str])
404 super
405 end
406
407 redef fun ignorable_whitespace(str: String) do
408 log.push(["ignorable_witespace", str])
409 super
410 end
411
412 redef fun processing_instruction(target: String, data: nullable String) do
413 log.push(["processing_instruction", target, data or else "^NULL"])
414 super
415 end
416
417 redef fun skipped_entity(name: String) do
418 log.push(["skipped_entity", name])
419 super
420 end
421
422
423 ############################################################################
424 # ErrorHandler
425
426 redef fun warning(exception: SAXParseException) do
427 log.push(["warning", exception.full_message])
428 super
429 end
430
431 redef fun error(exception: SAXParseException) do
432 log.push(["error", exception.full_message])
433 super
434 end
435
436 redef fun fatal_error(exception: SAXParseException) do
437 log.push(["fatal_error", exception.full_message])
438 if error_handler != null then
439 error_handler.fatal_error(exception)
440 end
441 end
442
443
444 ############################################################################
445 # DeclHandler
446
447 redef fun element_decl(name: String, model: String) do
448 log.push(["element_decl", name, model])
449 if decl_handler != null then
450 decl_handler.element_decl(name, model)
451 end
452 end
453
454 redef fun attribute_decl(element_name: String,
455 attribute_name: String,
456 attribute_type: String,
457 mode: nullable String,
458 value: nullable String) do
459 log.push(["attribute_decl",
460 element_name,
461 attribute_name,
462 attribute_type,
463 mode or else "^NULL",
464 value or else "^NULL"])
465 if decl_handler != null then
466 decl_handler.attribute_decl(element_name, attribute_name,
467 attribute_type, mode, value)
468 end
469 end
470
471 redef fun internal_entity_decl(name: String, value: String) do
472 log.push(["internal_entity_decl", name, value])
473 if decl_handler != null then
474 decl_handler.internal_entity_decl(name, value)
475 end
476 end
477
478 redef fun external_entity_decl(name: String, value: String) do
479 log.push(["external_entity_decl", name, value])
480 if decl_handler != null then
481 decl_handler.external_entity_decl(name, value)
482 end
483 end
484
485
486 ############################################################################
487 # LexicalHandler
488
489 redef fun start_dtd(name: String, public_id: nullable String,
490 system_id: nullable String) do
491 log.push(["start_dtd", name,
492 public_id or else "^NULL",
493 system_id or else "^NULL"])
494 if lexical_handler != null then
495 lexical_handler.start_dtd(name, public_id, system_id)
496 end
497 end
498
499 redef fun end_dtd do
500 log.push(["end_dtd"])
501 if lexical_handler != null then
502 lexical_handler.end_dtd
503 end
504 end
505
506 redef fun start_entity(name: String) do
507 log.push(["start_entity", name])
508 if lexical_handler != null then
509 lexical_handler.start_entity(name)
510 end
511 end
512
513 redef fun end_entity(name: String) do
514 log.push(["end_entity", name])
515 if lexical_handler != null then
516 lexical_handler.end_entity(name)
517 end
518 end
519
520 redef fun start_cdata do
521 log.push(["start_cdata"])
522 if lexical_handler != null then
523 lexical_handler.start_cdata
524 end
525 end
526
527 redef fun end_cdata do
528 log.push(["end_cdata"])
529 if lexical_handler != null then
530 lexical_handler.end_cdata
531 end
532 end
533
534 redef fun comment(str: String) do
535 log.push(["comment", str])
536 if lexical_handler != null then
537 lexical_handler.comment(str)
538 end
539 end
540 end
541
542
543 # Base class for test suites on a SAX reader.
544 abstract class SAXTestSuite
545 super TestSuite
546
547 # Logger of the expected event sequence.
548 var expected = new SAXEventLogger
549
550 # Logger of the actual event sequence.
551 var actual = new SAXEventLogger
552
553 # The tested SAX reader.
554 var reader: XMLReader is noinit
555
556 private var init_done: Bool = false
557
558 redef fun before_test do
559 super
560 if not init_done then
561 reader = create_reader
562 actual.parent = reader
563 init_done = true
564 end
565 reader.feature("http://xml.org/sax/features/namespaces") = true
566 reader.feature("http://xml.org/sax/features/namespace-prefixes") = false
567 expected.clear_log
568 actual.clear_log
569 end
570
571 # Create a new SAX reader.
572 #
573 # This method is called at initialization to set `reader`.
574 fun create_reader: XMLReader is abstract
575
576 # Assert logs are equal.
577 fun assert_equals do
578 var diff = actual.diff(expected)
579
580 assert equals: diff.length <= 0 else
581 sys.stderr.write("\n")
582 sys.stderr.write("# {actual.term_deletion}< Actual{actual.term_default}\n")
583 sys.stderr.write("# {actual.term_insertion}> Expected{actual.term_default}\n")
584 sys.stderr.write(diff)
585 sys.stderr.write("\n")
586 end
587 end
588
589 # Make the reader parse the specified string
590 fun parse_string(str: String) do
591 actual.parse(new InputSource.with_stream(new StringIStream(str)))
592 end
593 end