Rename REAMDE to README.md
[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. Examples: 'TestFoo', 'TestFoo*', 'TestFoo::test_foo', 'TestFoo::test_foo*', 'test_foo', 'test_foo*'", "-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 var before_test = mclassdef.before_test
62 var after_test = mclassdef.after_test
63 for mpropdef in mclassdef.mpropdefs do
64 if not mpropdef isa MMethodDef or not mpropdef.is_test then continue
65 if not case_match_pattern(mpropdef) then continue
66 toolcontext.modelbuilder.total_tests += 1
67 var test = new TestCase(suite, mpropdef, toolcontext)
68 test.before_test = before_test
69 test.after_test = after_test
70 suite.add_test test
71 end
72 end
73 # method to execute after all tests in the module
74 var after_module = mmodule.after_test
75 if after_module != null then
76 toolcontext.modelbuilder.total_tests += 1
77 suite.after_module = new TestCase(suite, after_module, toolcontext)
78 end
79 suite.run
80 return suite
81 end
82
83 # Is the test suite name match the pattern option?
84 private fun suite_match_pattern(suite: MClassDef): Bool do
85 var pattern = mbuilder.toolcontext.opt_pattern.value
86 if pattern == null then return true
87 var ps = pattern.split_with("::")
88 var p = ps.first
89 if ps.length == 1 and p.first.is_lower then return true
90 if ps.length == 2 and p.first.is_lower then return false
91 if p.has_suffix("*") then
92 p = p.substring(0, p.length - 1)
93 if suite.name.has_prefix(p) then return true
94 else
95 if suite.name == p then return true
96 end
97 return false
98 end
99
100 # Is the test case name match the pattern option?
101 private fun case_match_pattern(case: MPropDef): Bool do
102 var pattern = mbuilder.toolcontext.opt_pattern.value
103 if pattern == null then return true
104 var ps = pattern.split_with("::")
105 var p = ps.last
106 if ps.length == 1 and p.first.is_upper then return true
107 if ps.length == 2 and p.first.is_upper then return false
108 if p.has_suffix("*") then
109 p = p.substring(0, p.length - 1)
110 if case.name.has_prefix(p) then return true
111 else
112 if case.name == p then return true
113 end
114 return false
115 end
116 end
117
118 # A test suite contains all the test cases for a `MModule`.
119 class TestSuite
120
121 # `MModule` under test
122 var mmodule: MModule
123
124 # `ToolContext` to use to display messages.
125 var toolcontext: ToolContext
126
127 # List of `TestCase` to be executed in this suite.
128 var test_cases = new Array[TestCase]
129
130 # Add a `TestCase` to the suite.
131 fun add_test(case: TestCase) do test_cases.add case
132
133 # Test to be executed before the whole test suite.
134 var before_module: nullable TestCase = null
135
136 # Test to be executed after the whole test suite.
137 var after_module: nullable TestCase = null
138
139 # Execute the test suite
140 fun run do
141 if not toolcontext.test_dir.file_exists then
142 toolcontext.test_dir.mkdir
143 end
144 write_to_nit
145 compile
146 toolcontext.info("Execute test-suite {mmodule.name}", 1)
147 var before_module = self.before_module
148 if not before_module == null then before_module.run
149 for case in test_cases do case.run
150 var after_module = self.after_module
151 if not after_module == null then after_module.run
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 if failure != null then
172 var f = new HTMLTag("failure")
173 f.attr("message", failure.to_s)
174 n.add f
175 else
176 for test in test_cases do n.add test.to_xml
177 end
178 return n
179 end
180
181 # Generated test file name.
182 fun test_file: String do
183 return toolcontext.test_dir / "gen_{mmodule.name.escape_to_c}"
184 end
185
186 # Compile all `test_cases` cases in one file.
187 fun compile do
188 # find nitc
189 var nit_dir = toolcontext.nit_dir
190 var nitc = nit_dir/"bin/nitc"
191 if not nitc.file_exists then
192 toolcontext.error(null, "Error: cannot find nitc. Set envvar NIT_DIR.")
193 toolcontext.check_errors
194 end
195 # compile test suite
196 var file = test_file
197 var include_dir = mmodule.location.file.filename.dirname
198 var cmd = "{nitc} --no-color '{file}.nit' -I {include_dir} -o '{file}.bin' > '{file}.out' 2>&1 </dev/null"
199 var res = sys.system(cmd)
200 var f = new FileReader.open("{file}.out")
201 var msg = f.read_all
202 f.close
203 # set test case result
204 var loc = mmodule.location
205 if res != 0 then
206 failure = msg
207 toolcontext.warning(loc, "failure", "FAILURE: {mmodule.name} (in file {file}.nit): {msg}")
208 toolcontext.modelbuilder.failed_tests += 1
209 end
210 toolcontext.check_errors
211 end
212
213 # Error occured during test-suite compilation.
214 var failure: nullable String = null
215 end
216
217 # A test case is a unit test considering only a `MMethodDef`.
218 class TestCase
219
220 # Test suite wich `self` belongs to.
221 var test_suite: TestSuite
222
223 # Test method to be compiled and tested.
224 var test_method: MMethodDef
225
226 # `ToolContext` to use to display messages and find `nitc` bin.
227 var toolcontext: ToolContext
228
229 # `MMethodDef` to call before the test case.
230 var before_test: nullable MMethodDef = null
231
232 # `MMethodDef` to call after the test case.
233 var after_test: nullable MMethodDef = null
234
235 # Generate the test unit for `self` in `file`.
236 fun write_to_nit(file: Template) do
237 var name = test_method.name
238 file.addn "if name == \"{name}\" then"
239 if test_method.mproperty.is_toplevel then
240 file.addn "\t{name}"
241 else
242 file.addn "\tvar subject = new {test_method.mclassdef.name}.nitunit"
243 if before_test != null then file.addn "\tsubject.{before_test.name}"
244 file.addn "\tsubject.{name}"
245 if after_test != null then file.addn "\tsubject.{after_test.name}"
246 end
247 file.addn "end"
248 end
249
250 # Execute the test case.
251 fun run do
252 toolcontext.info("Execute test-case {test_method.name}", 1)
253 was_exec = true
254 if toolcontext.opt_noact.value then return
255 # execute
256 var method_name = test_method.name
257 var test_file = test_suite.test_file
258 var res_name = "{test_file}_{method_name.escape_to_c}"
259 var res = sys.system("{test_file}.bin {method_name} > '{res_name}.out1' 2>&1 </dev/null")
260 var f = new FileReader.open("{res_name}.out1")
261 var msg = f.read_all
262 f.close
263 # set test case result
264 var loc = test_method.location
265 if res != 0 then
266 error = msg
267 toolcontext.warning(loc, "failure",
268 "ERROR: {method_name} (in file {test_file}.nit): {msg}")
269 toolcontext.modelbuilder.failed_tests += 1
270 end
271 toolcontext.check_errors
272 end
273
274 # Error occured during test-case execution.
275 var error: nullable String = null
276
277 # Was the test case executed at least one?
278 var was_exec = false
279
280 # Return the `TestCase` in XML format compatible with Jenkins.
281 fun to_xml: HTMLTag do
282 var mclassdef = test_method.mclassdef
283 var tc = new HTMLTag("testcase")
284 # NOTE: jenkins expects a '.' in the classname attr
285 tc.attr("classname", "nitunit." + mclassdef.mmodule.full_name + "." + mclassdef.mclass.full_name)
286 tc.attr("name", test_method.mproperty.full_name)
287 if was_exec then
288 tc.add new HTMLTag("system-err")
289 var n = new HTMLTag("system-out")
290 n.append "out"
291 tc.add n
292 if error != null then
293 n = new HTMLTag("error")
294 n.attr("message", error.to_s)
295 tc.add n
296 end
297 end
298 return tc
299 end
300 end
301
302 redef class MMethodDef
303 # TODO use annotations?
304
305 # Is the method a test_method?
306 # i.e. begins with "test_"
307 private fun is_test: Bool do return name.has_prefix("test_")
308
309 # Is the method a "before_test"?
310 private fun is_before: Bool do return name == "before_test"
311
312 # Is the method a "after_test"?
313 private fun is_after: Bool do return name == "after_test"
314
315 # Is the method a "before_module"?
316 private fun is_before_module: Bool do return mproperty.is_toplevel and name == "before_module"
317
318 # Is the method a "after_module"?
319 private fun is_after_module: Bool do return mproperty.is_toplevel and name == "after_module"
320 end
321
322 redef class MClassDef
323 # Is the class a TestClass?
324 # i.e. begins with "Test"
325 private fun is_test: Bool do
326 for sup in in_hierarchy.greaters do
327 if sup.name == "TestSuite" then return true
328 end
329 return false
330 end
331
332 # "before_test" method for this classdef.
333 private fun before_test: nullable MMethodDef do
334 for mpropdef in mpropdefs do
335 if mpropdef isa MMethodDef and mpropdef.is_before then return mpropdef
336 end
337 return null
338 end
339
340 # "after_test" method for this classdef.
341 private fun after_test: nullable MMethodDef do
342 for mpropdef in mpropdefs do
343 if mpropdef isa MMethodDef and mpropdef.is_after then return mpropdef
344 end
345 return null
346 end
347 end
348
349 redef class MModule
350 # "before_module" method for this module.
351 private fun before_test: nullable MMethodDef do
352 for mclassdef in mclassdefs do
353 if not mclassdef.name == "Object" then continue
354 for mpropdef in mclassdef.mpropdefs do
355 if mpropdef isa MMethodDef and mpropdef.is_before_module then return mpropdef
356 end
357 end
358 return null
359 end
360
361 # "after_module" method for this module.
362 private fun after_test: nullable MMethodDef do
363 for mclassdef in mclassdefs do
364 if not mclassdef.name == "Object" then continue
365 for mpropdef in mclassdef.mpropdefs do
366 if mpropdef isa MMethodDef and mpropdef.is_after_module then return mpropdef
367 end
368 end
369 return null
370 end
371 end
372
373 redef class ModelBuilder
374 # Number of test classes generated.
375 var total_classes = 0
376
377 # Number of tests generated.
378 var total_tests = 0
379
380 # Number of failed tests.
381 var failed_tests = 0
382
383 # Run NitUnit test file for mmodule (if exists).
384 fun test_unit(mmodule: MModule): HTMLTag do
385 var ts = new HTMLTag("testsuite")
386 toolcontext.info("nitunit: test-suite test_{mmodule}", 2)
387 var f = toolcontext.opt_file.value
388 var test_file = "test_{mmodule.name}.nit"
389 if f != null then
390 test_file = f
391 else if not test_file.file_exists then
392 var include_dir = mmodule.location.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