saxophonit: `lexer.nit` is handmade.
[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: Array[Array[String]] = 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: Int = 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: Int = 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 var sub_buf = new FlatBuffer
194
195 buf.append(term_deletion)
196 buf.append("< {entry_index}|")
197 diff_append_mismatch_entry(buf, log[entry_index], sorted_mismatches,
198 term_deletion, term_deletion_emphasis)
199 buf.append(term_default)
200 buf.append("\n")
201 end
202
203 # Append a insertion to the diff.
204 #
205 # Parameters:
206 #
207 # * `buf`: buffer for the diff.
208 # * `log`: log that contains the inserted entry.
209 # * `entry_index`: index of the inserted entry in `log`.
210 # * `sorted_mismatches`: sorted list of indexes of the items to emphasize
211 # in the specified entry.
212 private fun diff_append_insertion(buf: Buffer, log: Array[Array[String]],
213 entry_index: Int, sorted_mismatches: Collection[Int]) do
214 buf.append(term_insertion)
215 buf.append("> {entry_index}|")
216 diff_append_mismatch_entry(buf, log[entry_index], sorted_mismatches,
217 term_insertion, term_insertion_emphasis)
218 buf.append(term_default)
219 buf.append("\n")
220 end
221
222 # Show an entry of a mismatch (without the margin).
223 #
224 # Append the string designed to be printed in the terminal to the
225 # specified buffer.
226 #
227 # Parameters:
228 #
229 # * `buf`: output buffer.
230 # * `entry`: entry to format.
231 # * `sorted_mismatches`: sorted list of indexes of the items to emphasize.
232 # * `term_normal`: terminal control code to re-apply the formatting that was
233 # in force prior calling this method.
234 # * `term_emphasis`: terminal control code to apply to items listed in
235 # `sorted_mismatches`.
236 private fun diff_append_mismatch_entry(buf: Buffer, entry: Array[String],
237 sorted_mismatches: Collection[Int], term_normal: String,
238 term_emphasis: String) do
239 var i: Int = 0
240 var j = sorted_mismatches.iterator
241 var length = entry.length
242
243 while i < length do
244 while j.is_ok and j.item < i do
245 j.next
246 end
247 if j.is_ok and j.item == i then
248 buf.append(term_emphasis)
249 buf.append(entry[i])
250 buf.append(term_normal)
251 else
252 buf.append(entry[i])
253 end
254 i += 1
255 if i < length then
256 buf.append("; ")
257 end
258 end
259 end
260
261 ############################################################################
262 # XMLReader
263
264 redef fun property(name: String): nullable Object do
265 assert sax_recognized: parent != null else
266 sys.stderr.write("Property: {name}\n")
267 end
268 if decl_handler_uri == name then
269 assert property_readable: property_readable(name) else
270 sys.stderr.write("Property: {name}\n")
271 end
272 return decl_handler
273 else if lexical_handler_uri == name then
274 assert property_readable: property_readable(name) else
275 sys.stderr.write("Property: {name}\n")
276 end
277 return lexical_handler
278 else
279 return parent.property(name)
280 end
281 end
282
283 redef fun property=(name: String, value: nullable Object) do
284 assert sax_recognized: parent != null else
285 sys.stderr.write("Property: {name}\n")
286 end
287 if decl_handler_uri == name then
288 assert property_readable: property_writable(name) else
289 sys.stderr.write("Property: {name}\n")
290 end
291 decl_handler = value.as(nullable DeclHandler)
292 else if lexical_handler_uri == name then
293 assert property_readable: property_writable(name) else
294 sys.stderr.write("Property: {name}\n")
295 end
296 lexical_handler = value.as(nullable LexicalHandler)
297 else
298 parent.property(name) = value
299 end
300 end
301
302 redef fun parse(input: InputSource) do
303 assert parent_is_not_null: parent != 0 else
304 sys.stderr.write("No parent for filter.")
305 end
306 if parent.feature_writable(decl_handler_uri) then
307 parent.property(decl_handler_uri) = self
308 end
309 if parent.feature_writable(lexical_handler_uri) then
310 parent.property(lexical_handler_uri) = self
311 end
312 super
313 end
314
315
316 ############################################################################
317 # EntityResolver
318
319 redef fun resolve_entity(public_id: nullable String,
320 system_id: nullable String):
321 nullable InputSource do
322 log.push(["resolve_entity",
323 public_id or else "^NULL",
324 system_id or else "^NULL"])
325 return super
326 end
327
328
329 ############################################################################
330 # DTDHandler
331
332 redef fun notation_decl(name: String, public_id: String,
333 system_id: String) do
334 log.push(["notation_decl", name, public_id, system_id])
335 super
336 end
337
338 redef fun unparsed_entity_decl(name: String, public_id: String,
339 system_id: String) do
340 log.push(["unparsed_entity_decl", name, public_id, system_id])
341 super
342 end
343
344
345 ############################################################################
346 # ContentHandler
347
348 redef fun document_locator=(locator: SAXLocator) do
349 log.push(["document_locator=",
350 locator.public_id or else "^NULL",
351 locator.system_id or else "^NULL",
352 locator.line_number.to_s,
353 locator.column_number.to_s])
354 super
355 end
356
357 redef fun start_document do
358 log.push(["start_document"])
359 super
360 end
361
362 redef fun end_document do
363 log.push(["end_document"])
364 super
365 end
366
367 redef fun start_prefix_mapping(prefix: String, uri: String) do
368 log.push(["start_prefix_mapping", prefix, uri])
369 super
370 end
371
372 redef fun end_prefix_mapping(prefix: String) do
373 log.push(["end_prefix_mapping", prefix])
374 super
375 end
376
377 redef fun start_element(uri: String, local_name: String, qname: String,
378 atts: Attributes) do
379 var entry = new Array[String]
380 var i = 0
381 var length = atts.length
382
383 entry.push("start_element")
384 entry.push(uri)
385 entry.push(local_name)
386 entry.push(qname)
387 while i < length do
388 entry.push(atts.uri(i) or else "^NULL")
389 entry.push(atts.local_name(i) or else "^NULL")
390 entry.push(atts.qname(i) or else "^NULL")
391 entry.push(atts.type_of(i) or else "^NULL")
392 entry.push(atts.value_of(i) or else "^NULL")
393 i += 1
394 end
395 log.push(entry)
396 super
397 end
398
399 redef fun end_element(uri: String, local_name: String, qname: String) do
400 log.push(["end_element", uri, local_name, qname])
401 super
402 end
403
404 redef fun characters(str: String) do
405 log.push(["characters", str])
406 super
407 end
408
409 redef fun ignorable_whitespace(str: String) do
410 log.push(["ignorable_witespace", str])
411 super
412 end
413
414 redef fun processing_instruction(target: String, data: nullable String) do
415 log.push(["processing_instruction", target, data or else "^NULL"])
416 super
417 end
418
419 redef fun skipped_entity(name: String) do
420 log.push(["skipped_entity", name])
421 super
422 end
423
424
425 ############################################################################
426 # ErrorHandler
427
428 redef fun warning(exception: SAXParseException) do
429 log.push(["warning", exception.full_message])
430 super
431 end
432
433 redef fun error(exception: SAXParseException) do
434 log.push(["error", exception.full_message])
435 super
436 end
437
438 redef fun fatal_error(exception: SAXParseException) do
439 log.push(["fatal_error", exception.full_message])
440 if error_handler != null then
441 error_handler.fatal_error(exception)
442 end
443 end
444
445
446 ############################################################################
447 # DeclHandler
448
449 redef fun element_decl(name: String, model: String) do
450 log.push(["element_decl", name, model])
451 if decl_handler != null then
452 decl_handler.element_decl(name, model)
453 end
454 end
455
456 redef fun attribute_decl(element_name: String,
457 attribute_name: String,
458 attribute_type: String,
459 mode: nullable String,
460 value: nullable String) do
461 log.push(["attribute_decl",
462 element_name,
463 attribute_name,
464 attribute_type,
465 mode or else "^NULL",
466 value or else "^NULL"])
467 if decl_handler != null then
468 decl_handler.attribute_decl(element_name, attribute_name,
469 attribute_type, mode, value)
470 end
471 end
472
473 redef fun internal_entity_decl(name: String, value: String) do
474 log.push(["internal_entity_decl", name, value])
475 if decl_handler != null then
476 decl_handler.internal_entity_decl(name, value)
477 end
478 end
479
480 redef fun external_entity_decl(name: String, value: String) do
481 log.push(["external_entity_decl", name, value])
482 if decl_handler != null then
483 decl_handler.external_entity_decl(name, value)
484 end
485 end
486
487
488 ############################################################################
489 # LexicalHandler
490
491 redef fun start_dtd(name: String, public_id: nullable String,
492 system_id: nullable String) do
493 log.push(["start_dtd", name,
494 public_id or else "^NULL",
495 system_id or else "^NULL"])
496 if lexical_handler != null then
497 lexical_handler.start_dtd(name, public_id, system_id)
498 end
499 end
500
501 redef fun end_dtd do
502 log.push(["end_dtd"])
503 if lexical_handler != null then
504 lexical_handler.end_dtd
505 end
506 end
507
508 redef fun start_entity(name: String) do
509 log.push(["start_entity", name])
510 if lexical_handler != null then
511 lexical_handler.start_entity(name)
512 end
513 end
514
515 redef fun end_entity(name: String) do
516 log.push(["end_entity", name])
517 if lexical_handler != null then
518 lexical_handler.end_entity(name)
519 end
520 end
521
522 redef fun start_cdata do
523 log.push(["start_cdata"])
524 if lexical_handler != null then
525 lexical_handler.start_cdata
526 end
527 end
528
529 redef fun end_cdata do
530 log.push(["end_cdata"])
531 if lexical_handler != null then
532 lexical_handler.end_cdata
533 end
534 end
535
536 redef fun comment(str: String) do
537 log.push(["comment", str])
538 if lexical_handler != null then
539 lexical_handler.comment(str)
540 end
541 end
542 end
543
544
545 # Base class for test suites on a SAX reader.
546 abstract class SAXTestSuite
547 super TestSuite
548
549 # Logger of the expected event sequence.
550 var expected: SAXEventLogger = new SAXEventLogger
551
552 # Logger of the actual event sequence.
553 var actual: SAXEventLogger = new SAXEventLogger
554
555 # The tested SAX reader.
556 var reader: XMLReader is noinit
557
558 private var init_done: Bool = false
559
560 redef fun before_test do
561 super
562 if not init_done then
563 reader = create_reader
564 actual.parent = reader
565 init_done = true
566 end
567 reader.feature("http://xml.org/sax/features/namespaces") = true
568 reader.feature("http://xml.org/sax/features/namespace-prefixes") = false
569 expected.clear_log
570 actual.clear_log
571 end
572
573 # Create a new SAX reader.
574 #
575 # This method is called at initialization to set `reader`.
576 fun create_reader: XMLReader is abstract
577
578 # Assert logs are equal.
579 fun assert_equals do
580 var diff = actual.diff(expected)
581
582 assert equals: diff.length <= 0 else
583 sys.stderr.write("\n")
584 sys.stderr.write("# {actual.term_deletion}< Actual{actual.term_default}\n")
585 sys.stderr.write("# {actual.term_insertion}> Expected{actual.term_default}\n")
586 sys.stderr.write(diff)
587 sys.stderr.write("\n")
588 end
589 end
590
591 # Make the reader parse the specified string
592 fun parse_string(str: String) do
593 actual.parse(new InputSource.with_stream(new StringIStream(str)))
594 end
595 end