Merge: doc: fixed some typos and other misc. corrections
[nit.git] / src / testing / testing_suite.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Testing from external files.
16 module testing_suite
17
18 import testing_base
19 import html
20 private import parse_annotations
21 private import realtime
22
23 redef class ToolContext
24 # --pattern
25 var opt_pattern = new OptionString("Only run test case with name that match pattern", "-p", "--pattern")
26 # --autosav
27 var opt_autosav = new OptionBool("Automatically create/update .res files for black box testing", "--autosav")
28 end
29
30 # Used to test nitunit test files.
31 class NitUnitTester
32
33 # `ModelBuilder` used to parse test files.
34 var mbuilder: ModelBuilder
35
36 # Compile and execute `mmodule` as a test suite.
37 fun test_module_unit(mmodule: MModule): TestSuite do
38 var toolcontext = mbuilder.toolcontext
39 var suite = new TestSuite(mmodule, toolcontext)
40 # method to execute before all tests in the module
41 for mmethod in mmodule.before_all do
42 toolcontext.modelbuilder.total_tests += 1
43 suite.before_all.add new TestCase(suite, mmethod, toolcontext)
44 end
45 # generate all test cases
46 for mclassdef in mmodule.mclassdefs do
47 if not mclassdef.is_test then continue
48 if not suite_match_pattern(mclassdef) then continue
49 toolcontext.modelbuilder.total_classes += 1
50
51 var test_class = new TestClass
52
53 # method to execute before all tests in the class
54 for mmethod in mclassdef.before_all do
55 toolcontext.modelbuilder.total_tests += 1
56 test_class.before_all.add new TestCase(suite, mmethod, toolcontext)
57 end
58
59 var before = mclassdef.before
60 var after = mclassdef.after
61
62 for mpropdef in mclassdef.mpropdefs do
63 if not mpropdef isa MMethodDef or not mpropdef.is_test then continue
64 if not case_match_pattern(mpropdef) then continue
65 toolcontext.modelbuilder.total_tests += 1
66 var test = new TestCase(suite, mpropdef, toolcontext)
67 test.before = before
68 test.after = after
69 test_class.test_cases.add test
70 end
71
72 # method to execute after all tests in the class
73 for mmethod in mclassdef.after_all do
74 toolcontext.modelbuilder.total_tests += 1
75 test_class.after_all.add new TestCase(suite, mmethod, toolcontext)
76 end
77
78 suite.test_classes.add test_class
79 end
80 # method to execute after all tests in the module
81 for mmethod in mmodule.after_all do
82 toolcontext.modelbuilder.total_tests += 1
83 suite.after_all.add new TestCase(suite, mmethod, toolcontext)
84 end
85 suite.run
86 return suite
87 end
88
89 # Is the test suite name match the pattern option?
90 private fun suite_match_pattern(suite: MClassDef): Bool do
91 var pattern = mbuilder.toolcontext.opt_pattern.value
92 if pattern == null then return true
93 var ps = pattern.split_with("::")
94 var p = ps.first
95 if ps.length == 1 and p.first.is_lower then return true
96 if ps.length == 2 and p.first.is_lower then return false
97 if p.has_suffix("*") then
98 p = p.substring(0, p.length - 1)
99 if suite.name.has_prefix(p) then return true
100 else
101 if suite.name == p then return true
102 end
103 return false
104 end
105
106 # Is the test case name match the pattern option?
107 private fun case_match_pattern(case: MPropDef): Bool do
108 var pattern = mbuilder.toolcontext.opt_pattern.value
109 if pattern == null then return true
110 var ps = pattern.split_with("::")
111 var p = ps.last
112 if ps.length == 1 and p.first.is_upper then return true
113 if ps.length == 2 and p.first.is_upper then return false
114 if p.has_suffix("*") then
115 p = p.substring(0, p.length - 1)
116 if case.name.has_prefix(p) then return true
117 else
118 if case.name == p then return true
119 end
120 return false
121 end
122 end
123
124 # A test suite contains all the test cases for a `MModule`.
125 class TestSuite
126
127 # `MModule` under test
128 var mmodule: MModule
129
130 # `ToolContext` to use to display messages.
131 var toolcontext: ToolContext
132
133 # List of `TestCase` to be executed in this suite.
134 var test_classes = new Array[TestClass]
135
136 # Tests to be executed before the whole test suite.
137 var before_all = new Array[TestCase]
138
139 # Tests to be executed after the whole test suite.
140 var after_all = new Array[TestCase]
141
142 # Display test suite status in std-out.
143 fun show_status do
144 var test_cases = new Array[TestCase]
145 for test_class in test_classes do
146 test_cases.add_all test_class.before_all
147 test_cases.add_all test_class.test_cases
148 test_cases.add_all test_class.after_all
149 end
150 test_cases.add_all before_all
151 test_cases.add_all after_all
152 toolcontext.show_unit_status("Test-suite of module " + mmodule.full_name, test_cases)
153 end
154
155 # Execute the test suite
156 fun run do
157 set_env
158 show_status
159 if not toolcontext.test_dir.file_exists then
160 toolcontext.test_dir.mkdir
161 end
162 write_to_nit
163 compile
164 if failure != null then
165 for test_class in test_classes do
166 for case in test_class.test_cases do
167 case.fail "Compilation Error"
168 case.raw_output = failure
169 toolcontext.clear_progress_bar
170 toolcontext.show_unit(case)
171 end
172 end
173 show_status
174 print ""
175 return
176 end
177 toolcontext.info("Execute test-suite {mmodule.name}", 1)
178
179 for before_module in before_all do
180 before_module.run
181 toolcontext.clear_progress_bar
182 toolcontext.show_unit(before_module)
183 if before_module.error != null then
184 for test_class in test_classes do
185 for case in test_class.before_all do
186 case.fail "Nitunit Error: before module test failed"
187 toolcontext.clear_progress_bar
188 toolcontext.show_unit(case)
189 end
190 for case in test_class.test_cases do
191 case.fail "Nitunit Error: before module test failed"
192 toolcontext.clear_progress_bar
193 toolcontext.show_unit(case)
194 end
195 for case in test_class.after_all do
196 case.fail "Nitunit Error: before module test failed"
197 toolcontext.clear_progress_bar
198 toolcontext.show_unit(case)
199 end
200 end
201 for after_module in after_all do
202 after_module.fail "Nitunit Error: before module test failed"
203 toolcontext.clear_progress_bar
204 toolcontext.show_unit(after_module)
205 end
206 show_status
207 print ""
208 return
209 end
210 end
211
212 for test_class in test_classes do
213 for case in test_class.before_all do
214 case.run
215 toolcontext.clear_progress_bar
216 toolcontext.show_unit(case)
217 if case.error != null then
218 for scase in test_class.test_cases do
219 scase.fail "Nitunit Error: before class test failed"
220 toolcontext.clear_progress_bar
221 toolcontext.show_unit(scase)
222 end
223 for scase in test_class.after_all do
224 scase.fail "Nitunit Error: before class test failed"
225 toolcontext.clear_progress_bar
226 toolcontext.show_unit(scase)
227 end
228 show_status
229 print ""
230 return
231 end
232 end
233 for case in test_class.test_cases do
234 case.run
235 toolcontext.clear_progress_bar
236 toolcontext.show_unit(case)
237 show_status
238 end
239 for after_class in test_class.after_all do
240 after_class.run
241 toolcontext.clear_progress_bar
242 toolcontext.show_unit(after_class)
243 show_status
244 end
245 end
246
247 for after_module in after_all do
248 after_module.run
249 toolcontext.clear_progress_bar
250 toolcontext.show_unit(after_module)
251 show_status
252 end
253
254 show_status
255 print ""
256 end
257
258 # Write the test unit for `self` in a nit compilable file.
259 fun write_to_nit do
260 var file = new Template
261 file.addn "intrude import core"
262 file.addn "import {mmodule.name}\n"
263 file.addn "var name = args.first"
264 for before_module in before_all do
265 before_module.write_to_nit(file)
266 end
267 for test_class in test_classes do
268 for case in test_class.before_all do
269 case.write_to_nit(file)
270 end
271 for case in test_class.test_cases do
272 case.write_to_nit(file)
273 end
274 for case in test_class.after_all do
275 case.write_to_nit(file)
276 end
277 end
278 for after_module in after_all do
279 after_module.write_to_nit(file)
280 end
281 file.write_to_file("{test_file}.nit")
282 end
283
284 # Return the test suite in XML format compatible with Jenkins.
285 # Contents depends on the `run` execution.
286 fun to_xml: HTMLTag do
287 var n = new HTMLTag("testsuite")
288 n.attr("package", mmodule.name)
289 for test_class in test_classes do
290 for test in test_class.test_cases do n.add test.to_xml
291 end
292 return n
293 end
294
295 # Generated test file name.
296 fun test_file: String do
297 return toolcontext.test_dir / "gen_{mmodule.name.escape_to_c}"
298 end
299
300 # Compile all `test_cases` cases in one file.
301 fun compile do
302 # find nitc
303 var nitc = toolcontext.find_nitc
304 # compile test suite
305 var file = test_file
306 var module_file = mmodule.location.file
307 if module_file == null then
308 toolcontext.error(null, "Error: cannot find module file for {mmodule.name}.")
309 toolcontext.check_errors
310 return
311 end
312 var include_dir = module_file.filename.dirname
313 var cmd = "{nitc} --no-color -q '{file}.nit' -I {include_dir} -o '{file}.bin' > '{file}.out' 2>&1 </dev/null"
314 var res = toolcontext.safe_exec(cmd)
315 var f = new FileReader.open("{file}.out")
316 var msg = f.read_all
317 f.close
318 if res != 0 then
319 failure = msg
320 end
321 end
322
323 # Set environment variables for test suite execution
324 fun set_env do
325 var loc = mmodule.location.file
326 if loc == null then return
327 toolcontext.set_testing_path(loc.filename)
328 end
329
330 # Error occured during test-suite compilation.
331 var failure: nullable String = null
332 end
333
334 # A test class contains multiple test cases
335 #
336 # For each test class, methods can be executed before and after all cases.
337 class TestClass
338 # List of `TestCase` to be executed in this suite.
339 var test_cases = new Array[TestCase]
340
341 # Tests to be executed before the whole test suite.
342 var before_all = new Array[TestCase]
343
344 # Tests to be executed after the whole test suite.
345 var after_all = new Array[TestCase]
346 end
347
348 # A test case is a unit test considering only a `MMethodDef`.
349 class TestCase
350 super UnitTest
351
352 # Test suite wich `self` belongs to.
353 var test_suite: TestSuite
354
355 # Test method to be compiled and tested.
356 var test_method: MMethodDef
357
358 # Cases to execute before this one
359 var before = new Array[MMethodDef]
360
361 # Cases to execute after this one
362 var after = new Array[MMethodDef]
363
364 redef fun full_name do return test_method.full_name
365
366 redef fun location do return test_method.location
367
368 # `ToolContext` to use to display messages and find `nitc` bin.
369 var toolcontext: ToolContext
370
371 # Generate the test unit for `self` in `file`.
372 fun write_to_nit(file: Template) do
373 var name = test_method.name
374 file.addn "if name == \"{name}\" then"
375 if test_method.mproperty.is_toplevel then
376 file.addn "\t{name}"
377 else
378 file.addn "\tvar subject = new {test_method.mclassdef.name}.intern"
379 for mmethod in before do
380 file.addn "\tsubject.{mmethod.name}"
381 end
382 file.addn "\tsubject.{name}"
383 for mmethod in after do
384 file.addn "\tsubject.{mmethod.name}"
385 end
386 end
387 file.addn "end"
388 end
389
390 # Execute the test case.
391 fun run do
392 toolcontext.info("Execute test-case {test_method.name}", 1)
393 was_exec = true
394 if toolcontext.opt_noact.value then return
395 # execute
396 var method_name = test_method.name
397 var test_file = test_suite.test_file
398 var res_name = "{test_file}_{method_name.escape_to_c}"
399 var clock = new Clock
400 var res = toolcontext.safe_exec("{test_file}.bin {method_name} > '{res_name}.out1' 2>&1 </dev/null")
401 if not toolcontext.opt_no_time.value then real_time = clock.total
402
403 var raw_output = "{res_name}.out1".to_path.read_all
404 self.raw_output = raw_output
405 # set test case result
406 if res != 0 then
407 error = "Runtime Error in file {test_file}.nit"
408 toolcontext.modelbuilder.failed_tests += 1
409 else
410 # no error, check with res file, if any.
411 var mmodule = test_method.mclassdef.mmodule
412 var file = mmodule.filepath
413 if file != null then
414 var tries = [ file.dirname / mmodule.name + ".sav" / test_method.name + ".res",
415 file.dirname / "sav" / test_method.name + ".res" ,
416 file.dirname / test_method.name + ".res" ]
417 var savs = [ for t in tries do if t.file_exists then t ]
418 if savs.length == 1 then
419 var sav = savs.first
420 toolcontext.info("Diff output with {sav}", 1)
421 res = toolcontext.safe_exec("diff -u --label 'expected:{sav}' --label 'got:{res_name}.out1' '{sav}' '{res_name}.out1' > '{res_name}.diff' 2>&1 </dev/null")
422 if res == 0 then
423 # OK
424 else if toolcontext.opt_autosav.value then
425 raw_output.write_to_file(sav)
426 info = "Expected output updated: {sav} (--autoupdate)"
427 else
428 self.raw_output = "Diff\n" + "{res_name}.diff".to_path.read_all
429 error = "Difference with expected output: diff -u {sav} {res_name}.out1"
430 toolcontext.modelbuilder.failed_tests += 1
431 end
432 else if savs.length > 1 then
433 toolcontext.info("Conflicting diffs: {savs.join(", ")}", 1)
434 error = "Conflicting expected output: {savs.join(", ", " and ")} all exist"
435 toolcontext.modelbuilder.failed_tests += 1
436 else if not raw_output.is_empty then
437 toolcontext.info("No diff: {tries.join(", ", " or ")} not found", 1)
438 if toolcontext.opt_autosav.value then
439 var sav = tries.first
440 sav.dirname.mkdir
441 raw_output.write_to_file(sav)
442 info = "Expected output saved: {sav} (--autoupdate)"
443 end
444 end
445 end
446 end
447 is_done = true
448 end
449
450 # Make the test case fail without testing it
451 #
452 # Useful when the compilation or the before_test failed.
453 fun fail(message: String) do
454 is_done = true
455 error = message
456 toolcontext.modelbuilder.failed_tests += 1
457 end
458
459 redef fun xml_classname do
460 var a = test_method.full_name.split("$")
461 return "nitunit.{a[0]}.{a[1]}"
462 end
463
464 redef fun xml_name do
465 var a = test_method.full_name.split("$")
466 return a[2]
467 end
468 end
469
470 redef class MClassDef
471 # Methods tagged with `before` in this class definition
472 private fun before: Array[MMethodDef] do
473 var res = new ArraySet[MMethodDef]
474 for mpropdef in mpropdefs do
475 if mpropdef isa MMethodDef and mpropdef.is_before then
476 res.add mpropdef
477 end
478 end
479 var in_hierarchy = self.in_hierarchy
480 if in_hierarchy == null then return res.to_a
481 for mclassdef in in_hierarchy.direct_greaters do
482 res.add_all mclassdef.before
483 end
484 var lin = res.to_a
485 mmodule.linearize_mpropdefs(lin)
486 return lin
487 end
488
489 # Methods tagged with `before_all` in this class definition
490 private fun before_all: Array[MMethodDef] do
491 var res = new ArraySet[MMethodDef]
492 for mpropdef in mpropdefs do
493 if mpropdef isa MMethodDef and mpropdef.is_before_all then
494 res.add mpropdef
495 end
496 end
497 var in_hierarchy = self.in_hierarchy
498 if in_hierarchy == null then return res.to_a
499 for mclassdef in in_hierarchy.direct_greaters do
500 res.add_all mclassdef.before_all
501 end
502 var lin = res.to_a
503 mmodule.linearize_mpropdefs(lin)
504 return lin
505 end
506
507 # Methods tagged with `after` in this class definition
508 private fun after: Array[MMethodDef] do
509 var res = new ArraySet[MMethodDef]
510 for mpropdef in mpropdefs do
511 if mpropdef isa MMethodDef and mpropdef.is_after then
512 res.add mpropdef
513 end
514 end
515 var in_hierarchy = self.in_hierarchy
516 if in_hierarchy == null then return res.to_a
517 for mclassdef in in_hierarchy.direct_greaters do
518 res.add_all mclassdef.after
519 end
520 var lin = res.to_a
521 mmodule.linearize_mpropdefs(lin)
522 return lin.reversed
523 end
524
525 # Methods tagged with `after_all` in this class definition
526 private fun after_all: Array[MMethodDef] do
527 var res = new ArraySet[MMethodDef]
528 for mpropdef in mpropdefs do
529 if mpropdef isa MMethodDef and mpropdef.is_after_all then
530 res.add mpropdef
531 end
532 end
533 var in_hierarchy = self.in_hierarchy
534 if in_hierarchy == null then return res.to_a
535 for mclassdef in in_hierarchy.direct_greaters do
536 res.add_all mclassdef.after_all
537 end
538 var lin = res.to_a
539 mmodule.linearize_mpropdefs(lin)
540 return lin.reversed
541 end
542 end
543
544 redef class MModule
545 # Methods tagged with `before_all` at the module level (in `Sys`)
546 private fun before_all: Array[MMethodDef] do
547 var res = new Array[MMethodDef]
548 for mmodule in in_importation.greaters do
549 for mclassdef in mmodule.mclassdefs do
550 if mclassdef.name != "Sys" then continue
551 for mpropdef in mclassdef.mpropdefs do
552 if not mpropdef isa MMethodDef or not mpropdef.is_before_all then continue
553 res.add mpropdef
554 end
555 end
556 end
557 var lin = res.to_a
558 linearize_mpropdefs(lin)
559 return lin
560 end
561
562 # Methods tagged with `after_all` at the module level (in `Sys`)
563 private fun after_all: Array[MMethodDef] do
564 var res = new Array[MMethodDef]
565 for mmodule in in_importation.greaters do
566 for mclassdef in mmodule.mclassdefs do
567 if mclassdef.name != "Sys" then continue
568 for mpropdef in mclassdef.mpropdefs do
569 if not mpropdef isa MMethodDef or not mpropdef.is_after_all then continue
570 res.add mpropdef
571 end
572 end
573 end
574 var lin = res.to_a
575 linearize_mpropdefs(lin)
576 return lin.reversed
577 end
578 end
579
580 redef class ModelBuilder
581 # Number of test classes generated.
582 var total_classes = 0
583
584 # Number of tests generated.
585 var total_tests = 0
586
587 # Number of failed tests.
588 var failed_tests = 0
589
590 # Run NitUnit test suite for `mmodule` (if it is one).
591 fun test_unit(mmodule: MModule): nullable HTMLTag do
592 # is the module a test_suite?
593 if not mmodule.is_test then return null
594 toolcontext.info("nitunit: test-suite {mmodule}", 2)
595
596 var tester = new NitUnitTester(self)
597 var res = tester.test_module_unit(mmodule)
598 return res.to_xml
599 end
600 end