nitunit: factorize file creation and compilation for docunits
[nit.git] / src / testing / testing_doc.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 code comments.
16 module testing_doc
17
18 import testing_base
19 intrude import docdown
20
21 # Extractor, Executor and Reporter for the tests in a module
22 class NitUnitExecutor
23 super Doc2Mdwn
24
25 # The prefix of the generated Nit source-file
26 var prefix: String
27
28 # The module to import
29 var mmodule: MModule
30
31 # The XML node associated to the module
32 var testsuite: HTMLTag
33
34 # All blocks of code from a same `ADoc`
35 var blocks = new Array[Array[String]]
36
37 # All failures from a same `ADoc`
38 var failures = new Array[String]
39
40 redef fun process_code(n: HTMLTag, text: String, tag: nullable String)
41 do
42 # Skip non-blocks
43 if n.tag != "pre" then return
44
45 # Skip strict non-nit
46 if tag != null and tag != "nit" and tag != "" then
47 return
48 end
49
50 # Try to parse it
51 var ast = toolcontext.parse_something(text)
52
53 # Skip pure comments
54 if ast isa TComment then return
55
56 # We want executable code
57 if not (ast isa AModule or ast isa ABlockExpr or ast isa AExpr) then
58 var message = ""
59 if ast isa AError then message = " At {ast.location}: {ast.message}."
60 toolcontext.warning(ndoc.location, "invalid-block", "Error: There is a block of code that is not valid Nit, thus not considered a nitunit. To suppress this warning, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`).{message}")
61 failures.add("{ndoc.location}: Invalid block of code.{message}")
62 return
63 end
64
65 # Create a first block
66 # Or create a new block for modules that are more than a main part
67 if blocks.is_empty or ast isa AModule then
68 blocks.add(new Array[String])
69 end
70
71 # Add it to the file
72 blocks.last.add(text)
73 end
74
75 # The associated node to localize warnings
76 var ndoc: nullable ADoc = null
77
78 # used to generate distinct names
79 var cpt = 0
80
81 # The entry point for a new `ndoc` node
82 # Fill `docunits` with new discovered unit of tests.
83 #
84 # `tc` (testcase) is the pre-filled XML node
85 fun extract(ndoc: ADoc, tc: HTMLTag)
86 do
87 blocks.clear
88 failures.clear
89
90 self.ndoc = ndoc
91
92 work(ndoc.to_mdoc)
93
94 toolcontext.check_errors
95
96 if not failures.is_empty then
97 for msg in failures do
98 var ne = new HTMLTag("failure")
99 ne.attr("message", msg)
100 tc.add ne
101 toolcontext.modelbuilder.failed_entities += 1
102 end
103 if blocks.is_empty then testsuite.add(tc)
104 end
105
106 if blocks.is_empty then return
107
108 for block in blocks do
109 docunits.add new DocUnit(ndoc, tc, block.join(""))
110 end
111 end
112
113 # All extracted docunits
114 var docunits = new Array[DocUnit]
115
116 # Execute all the docunits
117 fun run_tests
118 do
119 var simple_du = new Array[DocUnit]
120 for du in docunits do
121 var ast = toolcontext.parse_something(du.block)
122 if ast isa AExpr then
123 simple_du.add du
124 else
125 test_single_docunit(du)
126 end
127 end
128
129 test_simple_docunits(simple_du)
130 end
131
132 # Executes multiples doc-units in a shared program.
133 # Used for docunits simple block of code (without modules, classes, functions etc.)
134 #
135 # In case of compilation error, the method fallbacks to `test_single_docunit` to
136 # * locate exactly the compilation problem in the problematic docunit.
137 # * permit the execution of the other docunits that may be correct.
138 fun test_simple_docunits(dus: Array[DocUnit])
139 do
140 if dus.is_empty then return
141
142 var file = "{prefix}-0.nit"
143
144 var dir = file.dirname
145 if dir != "" then dir.mkdir
146 var f
147 f = create_unitfile(file)
148 var i = 0
149 for du in dus do
150
151 i += 1
152 f.write("fun run_{i} do\n")
153 f.write("# {du.testcase.attrs["name"]}\n")
154 f.write(du.block)
155 f.write("end\n")
156 end
157 f.write("var a = args.first.to_i\n")
158 for j in [1..i] do
159 f.write("if a == {j} then run_{j}\n")
160 end
161 f.close
162
163 if toolcontext.opt_noact.value then return
164
165 var res = compile_unitfile(file)
166
167 if res != 0 then
168 # Compilation error.
169 # Fall-back to individual modes:
170 for du in dus do
171 test_single_docunit(du)
172 end
173 return
174 end
175
176 i = 0
177 for du in dus do
178 var tc = du.testcase
179 toolcontext.modelbuilder.unit_entities += 1
180 i += 1
181 toolcontext.info("Execute doc-unit {du.testcase.attrs["name"]} in {file} {i}", 1)
182 var res2 = sys.system("{file.to_program_name}.bin {i} >>'{file}.out1' 2>&1 </dev/null")
183
184 var msg
185 f = new FileReader.open("{file}.out1")
186 var n2
187 n2 = new HTMLTag("system-err")
188 tc.add n2
189 msg = f.read_all
190 f.close
191
192 n2 = new HTMLTag("system-out")
193 tc.add n2
194 n2.append(du.block)
195
196 if res2 != 0 then
197 var ne = new HTMLTag("error")
198 ne.attr("message", msg)
199 tc.add ne
200 toolcontext.warning(du.ndoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
201 toolcontext.modelbuilder.failed_entities += 1
202 end
203 toolcontext.check_errors
204
205 testsuite.add(tc)
206 end
207 end
208
209 # Executes a single doc-unit in its own program.
210 # Used for docunits larger than a single block of code (with modules, classes, functions etc.)
211 fun test_single_docunit(du: DocUnit)
212 do
213 var tc = du.testcase
214 toolcontext.modelbuilder.unit_entities += 1
215
216 cpt += 1
217 var file = "{prefix}-{cpt}.nit"
218
219 toolcontext.info("Execute doc-unit {tc.attrs["name"]} in {file}", 1)
220
221 var f
222 f = create_unitfile(file)
223 f.write(du.block)
224 f.close
225
226 if toolcontext.opt_noact.value then return
227
228 var res = compile_unitfile(file)
229 var res2 = 0
230 if res == 0 then
231 res2 = sys.system("{file.to_program_name}.bin >>'{file}.out1' 2>&1 </dev/null")
232 end
233
234 var msg
235 f = new FileReader.open("{file}.out1")
236 var n2
237 n2 = new HTMLTag("system-err")
238 tc.add n2
239 msg = f.read_all
240 f.close
241
242 n2 = new HTMLTag("system-out")
243 tc.add n2
244 n2.append(du.block)
245
246
247 if res != 0 then
248 var ne = new HTMLTag("failure")
249 ne.attr("message", msg)
250 tc.add ne
251 toolcontext.warning(du.ndoc.location, "failure", "FAILURE: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
252 toolcontext.modelbuilder.failed_entities += 1
253 else if res2 != 0 then
254 var ne = new HTMLTag("error")
255 ne.attr("message", msg)
256 tc.add ne
257 toolcontext.warning(du.ndoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
258 toolcontext.modelbuilder.failed_entities += 1
259 end
260 toolcontext.check_errors
261
262 testsuite.add(tc)
263 end
264
265 # Create and fill the header of a unit file `file`.
266 #
267 # A unit file is a Nit source file generated from one
268 # or more docunits that will be compiled and executed.
269 #
270 # The handled on the file is returned and must be completed and closed.
271 #
272 # `file` should be a valid filepath for a Nit source file.
273 private fun create_unitfile(file: String): Writer
274 do
275 var dir = file.dirname
276 if dir != "" then dir.mkdir
277 var f
278 f = new FileWriter.open(file)
279 f.write("# GENERATED FILE\n")
280 f.write("# Docunits extracted from comments\n")
281 f.write("import {mmodule.name}\n")
282 f.write("\n")
283 return f
284 end
285
286 # Compile an unit file and return the compiler return code
287 #
288 # Can terminate the program if the compiler is not found
289 private fun compile_unitfile(file: String): Int
290 do
291 var nit_dir = toolcontext.nit_dir
292 var nitg = nit_dir/"bin/nitg"
293 if not nitg.file_exists then
294 toolcontext.error(null, "Cannot find nitg. Set envvar NIT_DIR.")
295 toolcontext.check_errors
296 end
297 var cmd = "{nitg} --ignore-visibility --no-color '{file}' -I {mmodule.location.file.filename.dirname} >'{file}.out1' 2>&1 </dev/null -o '{file}.bin'"
298 var res = sys.system(cmd)
299 return res
300 end
301 end
302
303 # A unit-test to run
304 class DocUnit
305 # The original comment node
306 var ndoc: ADoc
307
308 # The XML node that contains the information about the execution
309 var testcase: HTMLTag
310
311 # The text of the code to execute
312 var block: String
313 end
314
315 redef class ModelBuilder
316 # Total number analyzed `MEntity`
317 var total_entities = 0
318
319 # The number of `MEntity` that have some documentation
320 var doc_entities = 0
321
322 # The total number of executed docunits
323 var unit_entities = 0
324
325 # The number failed docunits
326 var failed_entities = 0
327
328 # Extracts and executes all the docunits in the `mmodule`
329 # Returns a JUnit-compatible `<testsuite>` XML element that contains the results of the executions.
330 fun test_markdown(mmodule: MModule): HTMLTag
331 do
332 var ts = new HTMLTag("testsuite")
333 toolcontext.info("nitunit: doc-unit {mmodule}", 2)
334
335 var nmodule = mmodule2node(mmodule)
336 if nmodule == null then return ts
337
338 # usualy, only the original module must be imported in the unit test.
339 var o = mmodule
340 var g = o.mgroup
341 if g != null and g.mproject.name == "standard" then
342 # except for a unit test in a module of standard
343 # in this case, the whole standard must be imported
344 o = get_mmodule_by_name(nmodule, g, g.mproject.name).as(not null)
345 end
346
347 ts.attr("package", mmodule.full_name)
348
349 var prefix = toolcontext.test_dir
350 prefix = prefix.join_path(mmodule.to_s)
351 var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
352
353 var tc
354
355 do
356 total_entities += 1
357 var nmoduledecl = nmodule.n_moduledecl
358 if nmoduledecl == null then break label x
359 var ndoc = nmoduledecl.n_doc
360 if ndoc == null then break label x
361 doc_entities += 1
362 tc = new HTMLTag("testcase")
363 # NOTE: jenkins expects a '.' in the classname attr
364 tc.attr("classname", "nitunit." + mmodule.full_name + ".<module>")
365 tc.attr("name", "<module>")
366 d2m.extract(ndoc, tc)
367 end label x
368 for nclassdef in nmodule.n_classdefs do
369 var mclassdef = nclassdef.mclassdef
370 if mclassdef == null then continue
371 if nclassdef isa AStdClassdef then
372 total_entities += 1
373 var ndoc = nclassdef.n_doc
374 if ndoc != null then
375 doc_entities += 1
376 tc = new HTMLTag("testcase")
377 tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
378 tc.attr("name", "<class>")
379 d2m.extract(ndoc, tc)
380 end
381 end
382 for npropdef in nclassdef.n_propdefs do
383 var mpropdef = npropdef.mpropdef
384 if mpropdef == null then continue
385 total_entities += 1
386 var ndoc = npropdef.n_doc
387 if ndoc != null then
388 doc_entities += 1
389 tc = new HTMLTag("testcase")
390 tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
391 tc.attr("name", mpropdef.mproperty.full_name)
392 d2m.extract(ndoc, tc)
393 end
394 end
395 end
396
397 d2m.run_tests
398
399 return ts
400 end
401 end