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