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