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