nitunit: keeps track of the source directory of the tested module.
[nit.git] / src / nitunit.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 # Program to extract and execute unit tests from nit source files
16 module nitunit
17
18 import modelize_property
19 intrude import markdown
20 import parser_util
21
22 # Extractor, Executor an Reporter for the tests in a module
23 class NitUnitExecutor
24 super Doc2Mdwn
25
26 # The module to import
27 var mmodule: MModule
28
29 # The prefix of the generated Nit source-file
30 var prefix: String
31
32 # The XML node associated to the module
33 var testsuite: HTMLTag
34
35 # Initialize a new e
36 init(toolcontext: ToolContext, prefix: String, mmodule: MModule, testsuite: HTMLTag)
37 do
38 super(toolcontext)
39 self.prefix = prefix
40 self.mmodule = mmodule
41 self.testsuite = testsuite
42 end
43
44 # All blocks of code from a same `ADoc`
45 var blocks = new Array[Array[String]]
46
47 redef fun process_code(n: HTMLTag, text: String)
48 do
49 # Try to parse it
50 var ast = toolcontext.parse_something(text)
51
52 # We want executable code
53 if not (ast isa AModule or ast isa ABlockExpr or ast isa AExpr) then
54 if ndoc != null and n.tag == "pre" and toolcontext.opt_warn.value > 1 then
55 toolcontext.warning(ndoc.location, "Warning: There is a block of code that is not valid Nit, thus not considered a nitunit")
56 if ast isa AError then toolcontext.warning(ast.location, ast.message)
57 ndoc = null # To avoid multiple warning in the same node
58 end
59 return
60 end
61
62 # Search `assert` in the AST
63 var v = new SearchAssertVisitor
64 v.enter_visit(ast)
65 if not v.foundit then
66 if ndoc != null and n.tag == "pre" and toolcontext.opt_warn.value > 1 then
67 toolcontext.warning(ndoc.location, "Warning: There is a block of Nit code without `assert`, thus not considered a nitunit")
68 ndoc = null # To avoid multiple warning in the same node
69 end
70 return
71 end
72
73 # Create a first block
74 # Or create a new block for modules that are more than a main part
75 if blocks.is_empty or ast isa AModule then
76 blocks.add(new Array[String])
77 end
78
79 # Add it to the file
80 blocks.last.add(text)
81 end
82
83 # The associated node to localize warnings
84 var ndoc: nullable ADoc
85
86 # used to generate distinct names
87 var cpt = 0
88
89 # The entry point for a new `ndoc` node
90 # Fill the prepated `tc` (testcase) XTM node
91 fun extract(ndoc: ADoc, tc: HTMLTag)
92 do
93 blocks.clear
94
95 self.ndoc = ndoc
96
97 work(ndoc.to_mdoc)
98 toolcontext.check_errors
99
100 if blocks.is_empty then return
101
102 for block in blocks do test_block(ndoc, tc, block)
103 end
104
105 # Execute a block
106 fun test_block(ndoc: ADoc, tc: HTMLTag, block: Array[String])
107 do
108 toolcontext.modelbuilder.unit_entities += 1
109
110 cpt += 1
111 var file = "{prefix}{cpt}.nit"
112
113 toolcontext.info("Execute {tc.attrs["classname"]}.{tc.attrs["name"]} in {file}", 1)
114
115 var dir = file.dirname
116 if dir != "" then dir.mkdir
117 var f
118 f = new OFStream.open(file)
119 f.write("# GENERATED FILE\n")
120 f.write("# Example extracted from a documentation\n")
121 f.write("import {mmodule.name}\n")
122 f.write("\n")
123 for text in block do
124 f.write(text)
125 end
126 f.close
127
128 if toolcontext.opt_noact.value then return
129
130 var nit_dir = toolcontext.nit_dir
131 var nitg = "{nit_dir}/bin/nitg"
132 if nit_dir == null or not nitg.file_exists then
133 toolcontext.error(null, "Cannot find nitg. Set envvar NIT_DIR.")
134 toolcontext.check_errors
135 end
136 var cmd = "{nitg} --ignore-visibility --no-color '{file}' -I {mmodule.location.file.filename.dirname} >'{file}.out1' 2>&1 </dev/null -o '{file}.bin'"
137 var res = sys.system(cmd)
138 var res2 = 0
139 if res == 0 then
140 res2 = sys.system("./{file}.bin >>'{file}.out1' 2>&1 </dev/null")
141 end
142
143 var msg
144 f = new IFStream.open("{file}.out1")
145 var n2
146 n2 = new HTMLTag("system-err")
147 tc.add n2
148 msg = f.read_all
149 f.close
150
151 n2 = new HTMLTag("system-out")
152 tc.add n2
153 for text in block do n2.append(text)
154
155
156 if res != 0 then
157 var ne = new HTMLTag("failure")
158 ne.attr("message", msg)
159 tc.add ne
160 toolcontext.warning(ndoc.location, "FAILURE: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
161 toolcontext.modelbuilder.failed_entities += 1
162 else if res2 != 0 then
163 var ne = new HTMLTag("error")
164 ne.attr("message", msg)
165 tc.add ne
166 toolcontext.warning(ndoc.location, "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
167 toolcontext.modelbuilder.failed_entities += 1
168 end
169 toolcontext.check_errors
170
171 testsuite.add(tc)
172 end
173 end
174
175 class SearchAssertVisitor
176 super Visitor
177 var foundit = false
178 redef fun visit(node)
179 do
180 if foundit then
181 return
182 else if node isa AAssertExpr then
183 foundit = true
184 return
185 else
186 node.visit_all(self)
187 end
188 end
189 end
190
191 redef class ModelBuilder
192 var total_entities = 0
193 var doc_entities = 0
194 var unit_entities = 0
195 var failed_entities = 0
196
197 fun test_markdown(mmodule: MModule): HTMLTag
198 do
199 var ts = new HTMLTag("testsuite")
200 toolcontext.info("nitunit: {mmodule}", 2)
201 if not mmodule2nmodule.has_key(mmodule) then return ts
202
203 var nmodule = mmodule2nmodule[mmodule]
204 assert nmodule != null
205
206 # what module to import in the unit test.
207 # try to detect the main module of the project
208 # TODO do things correctly once the importation of arbitraty nested module is legal
209 var o = mmodule
210 var g = o.mgroup
211 if g != null and g.mproject.name == "standard" then
212 o = get_mmodule_by_name(nmodule, g, g.mproject.name).as(not null)
213 end
214
215 ts.attr("package", mmodule.full_name)
216
217 var prefix = toolcontext.opt_dir.value
218 if prefix == null then prefix = ".nitunit"
219 prefix = prefix.join_path(mmodule.to_s)
220 var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
221
222 var tc
223
224 do
225 total_entities += 1
226 var nmoduledecl = nmodule.n_moduledecl
227 if nmoduledecl == null then break label x
228 var ndoc = nmoduledecl.n_doc
229 if ndoc == null then break label x
230 doc_entities += 1
231 tc = new HTMLTag("testcase")
232 # NOTE: jenkins expects a '.' in the classname attr
233 tc.attr("classname", mmodule.full_name + ".<module>")
234 tc.attr("name", "<module>")
235 d2m.extract(ndoc, tc)
236 end label x
237 for nclassdef in nmodule.n_classdefs do
238 var mclassdef = nclassdef.mclassdef.as(not null)
239 if nclassdef isa AStdClassdef then
240 total_entities += 1
241 var ndoc = nclassdef.n_doc
242 if ndoc != null then
243 doc_entities += 1
244 tc = new HTMLTag("testcase")
245 tc.attr("classname", mmodule.full_name + "." + mclassdef.mclass.full_name)
246 tc.attr("name", "<class>")
247 d2m.extract(ndoc, tc)
248 end
249 end
250 for npropdef in nclassdef.n_propdefs do
251 var mpropdef = npropdef.mpropdef.as(not null)
252 total_entities += 1
253 var ndoc = npropdef.n_doc
254 if ndoc != null then
255 doc_entities += 1
256 tc = new HTMLTag("testcase")
257 tc.attr("classname", mmodule.full_name + "." + mclassdef.mclass.full_name)
258 tc.attr("name", mpropdef.mproperty.full_name)
259 d2m.extract(ndoc, tc)
260 end
261 end
262 end
263
264 return ts
265 end
266 end
267
268 redef class ToolContext
269 var opt_full = new OptionBool("Process also imported modules", "--full")
270 var opt_output = new OptionString("Output name (default is 'nitunit.xml')", "-o", "--output")
271 var opt_dir = new OptionString("Working directory (default is '.nitunit')", "--dir")
272 var opt_noact = new OptionBool("Does not compile and run tests", "--no-act")
273 end
274
275 var toolcontext = new ToolContext
276
277 toolcontext.option_context.add_option(toolcontext.opt_full, toolcontext.opt_output, toolcontext.opt_dir, toolcontext.opt_noact)
278 toolcontext.tooldescription = "Usage: nitunit [OPTION]... <file.nit>...\nExecutes the unit tests from Nit source files."
279
280 toolcontext.process_options(args)
281 var args = toolcontext.option_context.rest
282
283 var model = new Model
284 var modelbuilder = new ModelBuilder(model, toolcontext)
285
286 var mmodules = modelbuilder.parse(args)
287 modelbuilder.run_phases
288
289 var page = new HTMLTag("testsuites")
290
291 if toolcontext.opt_full.value then mmodules = model.mmodules
292
293 for m in mmodules do
294 page.add modelbuilder.test_markdown(m)
295 end
296
297 var file = toolcontext.opt_output.value
298 if file == null then file = "nitunit.xml"
299 page.write_to_file(file)
300 print "Results saved in {file}"
301
302 if modelbuilder.unit_entities == 0 then
303 print "No nitunits found"
304 else if modelbuilder.failed_entities == 0 and not toolcontext.opt_noact.value then
305 print "Success"
306 end
307 print "Entities: {modelbuilder.total_entities}; Documented ones: {modelbuilder.doc_entities}; With nitunits: {modelbuilder.unit_entities}; Failures: {modelbuilder.failed_entities}"