nitunit: fix `before-all` and `after-all` detection
[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 before = mclassdef.before
52 var after = mclassdef.after
53
54 for mpropdef in mclassdef.mpropdefs do
55 if not mpropdef isa MMethodDef or not mpropdef.is_test then continue
56 if not case_match_pattern(mpropdef) then continue
57 toolcontext.modelbuilder.total_tests += 1
58 var test = new TestCase(suite, mpropdef, toolcontext)
59 test.before = before
60 test.after = after
61 suite.test_cases.add test
62 end
63 end
64 # method to execute after all tests in the module
65 for mmethod in mmodule.after_all do
66 toolcontext.modelbuilder.total_tests += 1
67 suite.after_all.add new TestCase(suite, mmethod, toolcontext)
68 end
69 suite.run
70 return suite
71 end
72
73 # Is the test suite name match the pattern option?
74 private fun suite_match_pattern(suite: MClassDef): Bool do
75 var pattern = mbuilder.toolcontext.opt_pattern.value
76 if pattern == null then return true
77 var ps = pattern.split_with("::")
78 var p = ps.first
79 if ps.length == 1 and p.first.is_lower then return true
80 if ps.length == 2 and p.first.is_lower then return false
81 if p.has_suffix("*") then
82 p = p.substring(0, p.length - 1)
83 if suite.name.has_prefix(p) then return true
84 else
85 if suite.name == p then return true
86 end
87 return false
88 end
89
90 # Is the test case name match the pattern option?
91 private fun case_match_pattern(case: MPropDef): Bool do
92 var pattern = mbuilder.toolcontext.opt_pattern.value
93 if pattern == null then return true
94 var ps = pattern.split_with("::")
95 var p = ps.last
96 if ps.length == 1 and p.first.is_upper then return true
97 if ps.length == 2 and p.first.is_upper then return false
98 if p.has_suffix("*") then
99 p = p.substring(0, p.length - 1)
100 if case.name.has_prefix(p) then return true
101 else
102 if case.name == p then return true
103 end
104 return false
105 end
106 end
107
108 # A test suite contains all the test cases for a `MModule`.
109 class TestSuite
110
111 # `MModule` under test
112 var mmodule: MModule
113
114 # `ToolContext` to use to display messages.
115 var toolcontext: ToolContext
116
117 # List of `TestCase` to be executed in this suite.
118 var test_cases = new Array[TestCase]
119
120 # Tests to be executed before the whole test suite.
121 var before_all = new Array[TestCase]
122
123 # Tests to be executed after the whole test suite.
124 var after_all = new Array[TestCase]
125
126 # Display test suite status in std-out.
127 fun show_status do
128 var test_cases = self.test_cases.to_a
129 test_cases.add_all before_all
130 test_cases.add_all after_all
131 toolcontext.show_unit_status("Test-suite of module " + mmodule.full_name, test_cases)
132 end
133
134 # Execute the test suite
135 fun run do
136 set_env
137 show_status
138 if not toolcontext.test_dir.file_exists then
139 toolcontext.test_dir.mkdir
140 end
141 write_to_nit
142 compile
143 if failure != null then
144 for case in test_cases do
145 case.fail "Compilation Error"
146 case.raw_output = failure
147 toolcontext.clear_progress_bar
148 toolcontext.show_unit(case)
149 end
150 show_status
151 print ""
152 return
153 end
154 toolcontext.info("Execute test-suite {mmodule.name}", 1)
155
156 for before_module in before_all do
157 before_module.run
158 toolcontext.clear_progress_bar
159 toolcontext.show_unit(before_module)
160 if before_module.error != null then
161 for case in test_cases do
162 case.fail "Nitunit Error: before_module test failed"
163 toolcontext.clear_progress_bar
164 toolcontext.show_unit(case)
165 end
166 for after_module in after_all do
167 after_module.fail "Nitunit Error: before_module test failed"
168 toolcontext.clear_progress_bar
169 toolcontext.show_unit(after_module)
170 end
171 show_status
172 print ""
173 return
174 end
175 end
176
177 for case in test_cases do
178 case.run
179 toolcontext.clear_progress_bar
180 toolcontext.show_unit(case)
181 show_status
182 end
183
184 for after_module in after_all do
185 after_module.run
186 toolcontext.clear_progress_bar
187 toolcontext.show_unit(after_module)
188 show_status
189 end
190
191 show_status
192 print ""
193 end
194
195 # Write the test unit for `self` in a nit compilable file.
196 fun write_to_nit do
197 var file = new Template
198 file.addn "intrude import core"
199 file.addn "import {mmodule.name}\n"
200 file.addn "var name = args.first"
201 for before_module in before_all do
202 before_module.write_to_nit(file)
203 end
204 for case in test_cases do
205 case.write_to_nit(file)
206 end
207 for after_module in after_all do
208 after_module.write_to_nit(file)
209 end
210 file.write_to_file("{test_file}.nit")
211 end
212
213 # Return the test suite in XML format compatible with Jenkins.
214 # Contents depends on the `run` execution.
215 fun to_xml: HTMLTag do
216 var n = new HTMLTag("testsuite")
217 n.attr("package", mmodule.name)
218 for test in test_cases do n.add test.to_xml
219 return n
220 end
221
222 # Generated test file name.
223 fun test_file: String do
224 return toolcontext.test_dir / "gen_{mmodule.name.escape_to_c}"
225 end
226
227 # Compile all `test_cases` cases in one file.
228 fun compile do
229 # find nitc
230 var nitc = toolcontext.find_nitc
231 # compile test suite
232 var file = test_file
233 var module_file = mmodule.location.file
234 if module_file == null then
235 toolcontext.error(null, "Error: cannot find module file for {mmodule.name}.")
236 toolcontext.check_errors
237 return
238 end
239 var include_dir = module_file.filename.dirname
240 var cmd = "{nitc} --no-color -q '{file}.nit' -I {include_dir} -o '{file}.bin' > '{file}.out' 2>&1 </dev/null"
241 var res = toolcontext.safe_exec(cmd)
242 var f = new FileReader.open("{file}.out")
243 var msg = f.read_all
244 f.close
245 if res != 0 then
246 failure = msg
247 end
248 end
249
250 # Set environment variables for test suite execution
251 fun set_env do
252 var loc = mmodule.location.file
253 if loc == null then return
254 toolcontext.set_testing_path(loc.filename)
255 end
256
257 # Error occured during test-suite compilation.
258 var failure: nullable String = null
259 end
260
261 # A test case is a unit test considering only a `MMethodDef`.
262 class TestCase
263 super UnitTest
264
265 # Test suite wich `self` belongs to.
266 var test_suite: TestSuite
267
268 # Test method to be compiled and tested.
269 var test_method: MMethodDef
270
271 # Cases to execute before this one
272 var before = new Array[MMethodDef]
273
274 # Cases to execute after this one
275 var after = new Array[MMethodDef]
276
277 redef fun full_name do return test_method.full_name
278
279 redef fun location do return test_method.location
280
281 # `ToolContext` to use to display messages and find `nitc` bin.
282 var toolcontext: ToolContext
283
284 # Generate the test unit for `self` in `file`.
285 fun write_to_nit(file: Template) do
286 var name = test_method.name
287 file.addn "if name == \"{name}\" then"
288 if test_method.mproperty.is_toplevel then
289 file.addn "\t{name}"
290 else
291 file.addn "\tvar subject = new {test_method.mclassdef.name}.intern"
292 for mmethod in before do
293 file.addn "\tsubject.{mmethod.name}"
294 end
295 file.addn "\tsubject.{name}"
296 for mmethod in after do
297 file.addn "\tsubject.{mmethod.name}"
298 end
299 end
300 file.addn "end"
301 end
302
303 # Execute the test case.
304 fun run do
305 toolcontext.info("Execute test-case {test_method.name}", 1)
306 was_exec = true
307 if toolcontext.opt_noact.value then return
308 # execute
309 var method_name = test_method.name
310 var test_file = test_suite.test_file
311 var res_name = "{test_file}_{method_name.escape_to_c}"
312 var clock = new Clock
313 var res = toolcontext.safe_exec("{test_file}.bin {method_name} > '{res_name}.out1' 2>&1 </dev/null")
314 if not toolcontext.opt_no_time.value then real_time = clock.total
315
316 var raw_output = "{res_name}.out1".to_path.read_all
317 self.raw_output = raw_output
318 # set test case result
319 if res != 0 then
320 error = "Runtime Error in file {test_file}.nit"
321 toolcontext.modelbuilder.failed_tests += 1
322 else
323 # no error, check with res file, if any.
324 var mmodule = test_method.mclassdef.mmodule
325 var file = mmodule.filepath
326 if file != null then
327 var tries = [ file.dirname / mmodule.name + ".sav" / test_method.name + ".res",
328 file.dirname / "sav" / test_method.name + ".res" ,
329 file.dirname / test_method.name + ".res" ]
330 var savs = [ for t in tries do if t.file_exists then t ]
331 if savs.length == 1 then
332 var sav = savs.first
333 toolcontext.info("Diff output with {sav}", 1)
334 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")
335 if res == 0 then
336 # OK
337 else if toolcontext.opt_autosav.value then
338 raw_output.write_to_file(sav)
339 info = "Expected output updated: {sav} (--autoupdate)"
340 else
341 self.raw_output = "Diff\n" + "{res_name}.diff".to_path.read_all
342 error = "Difference with expected output: diff -u {sav} {res_name}.out1"
343 toolcontext.modelbuilder.failed_tests += 1
344 end
345 else if savs.length > 1 then
346 toolcontext.info("Conflicting diffs: {savs.join(", ")}", 1)
347 error = "Conflicting expected output: {savs.join(", ", " and ")} all exist"
348 toolcontext.modelbuilder.failed_tests += 1
349 else if not raw_output.is_empty then
350 toolcontext.info("No diff: {tries.join(", ", " or ")} not found", 1)
351 if toolcontext.opt_autosav.value then
352 var sav = tries.first
353 sav.dirname.mkdir
354 raw_output.write_to_file(sav)
355 info = "Expected output saved: {sav} (--autoupdate)"
356 end
357 end
358 end
359 end
360 is_done = true
361 end
362
363 # Make the test case fail without testing it
364 #
365 # Useful when the compilation or the before_test failed.
366 fun fail(message: String) do
367 is_done = true
368 error = message
369 toolcontext.modelbuilder.failed_tests += 1
370 end
371
372 redef fun xml_classname do
373 var a = test_method.full_name.split("$")
374 return "nitunit.{a[0]}.{a[1]}"
375 end
376
377 redef fun xml_name do
378 var a = test_method.full_name.split("$")
379 return a[2]
380 end
381 end
382
383 redef class MClassDef
384 # Methods tagged with `before` in this class definition
385 private fun before: Array[MMethodDef] do
386 var res = new Array[MMethodDef]
387 for mpropdef in mpropdefs do
388 if mpropdef isa MMethodDef and mpropdef.is_before then
389 res.add mpropdef
390 end
391 end
392 var in_hierarchy = self.in_hierarchy
393 if in_hierarchy == null then return res
394 for mclassdef in in_hierarchy.direct_greaters do
395 res.add_all mclassdef.before
396 end
397 return res
398 end
399
400 # Methods tagged with `before_all` in this class definition
401 private fun before_all: Array[MMethodDef] do
402 var res = new Array[MMethodDef]
403 for mpropdef in mpropdefs do
404 if mpropdef isa MMethodDef and mpropdef.is_before_all then
405 res.add mpropdef
406 end
407 end
408 var in_hierarchy = self.in_hierarchy
409 if in_hierarchy == null then return res
410 for mclassdef in in_hierarchy.direct_greaters do
411 res.add_all mclassdef.before_all
412 end
413 return res
414 end
415
416 # Methods tagged with `after` in this class definition
417 private fun after: Array[MMethodDef] do
418 var res = new Array[MMethodDef]
419 for mpropdef in mpropdefs do
420 if mpropdef isa MMethodDef and mpropdef.is_after then
421 res.add mpropdef
422 end
423 end
424 var in_hierarchy = self.in_hierarchy
425 if in_hierarchy == null then return res
426 for mclassdef in in_hierarchy.direct_greaters do
427 res.add_all mclassdef.after
428 end
429 return res
430 end
431
432 # Methods tagged with `after_all` in this class definition
433 private fun after_all: Array[MMethodDef] do
434 var res = new Array[MMethodDef]
435 for mpropdef in mpropdefs do
436 if mpropdef isa MMethodDef and mpropdef.is_after_all then
437 res.add mpropdef
438 end
439 end
440 var in_hierarchy = self.in_hierarchy
441 if in_hierarchy == null then return res
442 for mclassdef in in_hierarchy.direct_greaters do
443 res.add_all mclassdef.after_all
444 end
445 return res
446 end
447 end
448
449 redef class MModule
450 # Methods tagged with `before_all` at the module level (in `Sys`)
451 private fun before_all: Array[MMethodDef] do
452 var res = new Array[MMethodDef]
453 for mmodule in in_importation.greaters do
454 for mclassdef in mmodule.mclassdefs do
455 if mclassdef.name != "Sys" then continue
456 for mpropdef in mclassdef.mpropdefs do
457 if not mpropdef isa MMethodDef or not mpropdef.is_before_all then continue
458 res.add mpropdef
459 end
460 end
461 end
462 return res
463 end
464
465 # Methods tagged with `after_all` at the module level (in `Sys`)
466 private fun after_all: Array[MMethodDef] do
467 var res = new Array[MMethodDef]
468 for mmodule in in_importation.greaters do
469 for mclassdef in mmodule.mclassdefs do
470 if mclassdef.name != "Sys" then continue
471 for mpropdef in mclassdef.mpropdefs do
472 if not mpropdef isa MMethodDef or not mpropdef.is_after_all then continue
473 res.add mpropdef
474 end
475 end
476 end
477 return res
478 end
479 end
480
481 redef class ModelBuilder
482 # Number of test classes generated.
483 var total_classes = 0
484
485 # Number of tests generated.
486 var total_tests = 0
487
488 # Number of failed tests.
489 var failed_tests = 0
490
491 # Run NitUnit test suite for `mmodule` (if it is one).
492 fun test_unit(mmodule: MModule): nullable HTMLTag do
493 # is the module a test_suite?
494 if not mmodule.is_test then return null
495 toolcontext.info("nitunit: test-suite {mmodule}", 2)
496
497 var tester = new NitUnitTester(self)
498 var res = tester.test_module_unit(mmodule)
499 return res.to_xml
500 end
501 end