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