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