nitunit: factorize the search of a compiler
[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 private import parser_util
19 import testing_base
20 import markdown
21 import html
22
23 # Extractor, Executor and Reporter for the tests in a module
24 class NitUnitExecutor
25 super HTMLDecorator
26
27 # Toolcontext used to parse Nit code blocks.
28 var toolcontext: ToolContext
29
30 # The prefix of the generated Nit source-file
31 var prefix: String
32
33 # The module to import, if any
34 var mmodule: nullable MModule
35
36 # The XML node associated to the module
37 var testsuite: HTMLTag
38
39 # All blocks of code from a same `ADoc`
40 var blocks = new Array[Buffer]
41
42 # All failures from a same `ADoc`
43 var failures = new Array[String]
44
45 # Markdown processor used to parse markdown comments and extract code.
46 var mdproc = new MarkdownProcessor
47
48 init do
49 mdproc.emitter.decorator = new NitunitDecorator(self)
50 end
51
52 # The associated documentation object
53 var mdoc: nullable MDoc = null
54
55 # used to generate distinct names
56 var cpt = 0
57
58 # The entry point for a new `ndoc` node
59 # Fill `docunits` with new discovered unit of tests.
60 #
61 # `tc` (testcase) is the pre-filled XML node
62 fun extract(mdoc: MDoc, tc: HTMLTag)
63 do
64 blocks.clear
65 failures.clear
66
67 self.mdoc = mdoc
68
69 # Populate `blocks` from the markdown decorator
70 mdproc.process(mdoc.content.join("\n"))
71
72 toolcontext.check_errors
73
74 if not failures.is_empty then
75 for msg in failures do
76 var ne = new HTMLTag("failure")
77 ne.attr("message", msg)
78 tc.add ne
79 toolcontext.modelbuilder.unit_entities += 1
80 toolcontext.modelbuilder.failed_entities += 1
81 end
82 if blocks.is_empty then testsuite.add(tc)
83 end
84
85 if blocks.is_empty then return
86 for block in blocks do
87 docunits.add new DocUnit(mdoc, tc, block.write_to_string)
88 end
89 end
90
91 # All extracted docunits
92 var docunits = new Array[DocUnit]
93
94 # Execute all the docunits
95 fun run_tests
96 do
97 var simple_du = new Array[DocUnit]
98 for du in docunits do
99 var ast = toolcontext.parse_something(du.block)
100 if ast isa AExpr then
101 simple_du.add du
102 else
103 test_single_docunit(du)
104 end
105 end
106
107 test_simple_docunits(simple_du)
108 end
109
110 # Executes multiples doc-units in a shared program.
111 # Used for docunits simple block of code (without modules, classes, functions etc.)
112 #
113 # In case of compilation error, the method fallbacks to `test_single_docunit` to
114 # * locate exactly the compilation problem in the problematic docunit.
115 # * permit the execution of the other docunits that may be correct.
116 fun test_simple_docunits(dus: Array[DocUnit])
117 do
118 if dus.is_empty then return
119
120 var file = "{prefix}-0.nit"
121
122 var dir = file.dirname
123 if dir != "" then dir.mkdir
124 var f
125 f = create_unitfile(file)
126 var i = 0
127 for du in dus do
128
129 i += 1
130 f.write("fun run_{i} do\n")
131 f.write("# {du.testcase.attrs["name"]}\n")
132 f.write(du.block)
133 f.write("end\n")
134 end
135 f.write("var a = args.first.to_i\n")
136 for j in [1..i] do
137 f.write("if a == {j} then run_{j}\n")
138 end
139 f.close
140
141 if toolcontext.opt_noact.value then return
142
143 var res = compile_unitfile(file)
144
145 if res != 0 then
146 # Compilation error.
147 # Fall-back to individual modes:
148 for du in dus do
149 test_single_docunit(du)
150 end
151 return
152 end
153
154 i = 0
155 for du in dus do
156 var tc = du.testcase
157 toolcontext.modelbuilder.unit_entities += 1
158 i += 1
159 toolcontext.info("Execute doc-unit {du.testcase.attrs["name"]} in {file} {i}", 1)
160 var res2 = sys.system("{file.to_program_name}.bin {i} >>'{file}.out1' 2>&1 </dev/null")
161
162 var msg
163 f = new FileReader.open("{file}.out1")
164 var n2
165 n2 = new HTMLTag("system-err")
166 tc.add n2
167 msg = f.read_all
168 f.close
169
170 n2 = new HTMLTag("system-out")
171 tc.add n2
172 n2.append(du.block)
173
174 if res2 != 0 then
175 var ne = new HTMLTag("error")
176 ne.attr("message", msg)
177 tc.add ne
178 toolcontext.warning(du.mdoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
179 toolcontext.modelbuilder.failed_entities += 1
180 end
181 toolcontext.check_errors
182
183 testsuite.add(tc)
184 end
185 end
186
187 # Executes a single doc-unit in its own program.
188 # Used for docunits larger than a single block of code (with modules, classes, functions etc.)
189 fun test_single_docunit(du: DocUnit)
190 do
191 var tc = du.testcase
192 toolcontext.modelbuilder.unit_entities += 1
193
194 cpt += 1
195 var file = "{prefix}-{cpt}.nit"
196
197 toolcontext.info("Execute doc-unit {tc.attrs["name"]} in {file}", 1)
198
199 var f
200 f = create_unitfile(file)
201 f.write(du.block)
202 f.close
203
204 if toolcontext.opt_noact.value then return
205
206 var res = compile_unitfile(file)
207 var res2 = 0
208 if res == 0 then
209 res2 = sys.system("{file.to_program_name}.bin >>'{file}.out1' 2>&1 </dev/null")
210 end
211
212 var msg
213 f = new FileReader.open("{file}.out1")
214 var n2
215 n2 = new HTMLTag("system-err")
216 tc.add n2
217 msg = f.read_all
218 f.close
219
220 n2 = new HTMLTag("system-out")
221 tc.add n2
222 n2.append(du.block)
223
224
225 if res != 0 then
226 var ne = new HTMLTag("failure")
227 ne.attr("message", msg)
228 tc.add ne
229 toolcontext.warning(du.mdoc.location, "failure", "FAILURE: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
230 toolcontext.modelbuilder.failed_entities += 1
231 else if res2 != 0 then
232 var ne = new HTMLTag("error")
233 ne.attr("message", msg)
234 tc.add ne
235 toolcontext.warning(du.mdoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
236 toolcontext.modelbuilder.failed_entities += 1
237 end
238 toolcontext.check_errors
239
240 testsuite.add(tc)
241 end
242
243 # Create and fill the header of a unit file `file`.
244 #
245 # A unit file is a Nit source file generated from one
246 # or more docunits that will be compiled and executed.
247 #
248 # The handled on the file is returned and must be completed and closed.
249 #
250 # `file` should be a valid filepath for a Nit source file.
251 private fun create_unitfile(file: String): Writer
252 do
253 var dir = file.dirname
254 if dir != "" then dir.mkdir
255 var f
256 f = new FileWriter.open(file)
257 f.write("# GENERATED FILE\n")
258 f.write("# Docunits extracted from comments\n")
259 if mmodule != null then
260 f.write("import {mmodule.name}\n")
261 end
262 f.write("\n")
263 return f
264 end
265
266 # Compile an unit file and return the compiler return code
267 #
268 # Can terminate the program if the compiler is not found
269 private fun compile_unitfile(file: String): Int
270 do
271 var nitc = toolcontext.find_nitc
272 var opts = new Array[String]
273 if mmodule != null then
274 opts.add "-I {mmodule.filepath.dirname}"
275 end
276 var cmd = "{nitc} --ignore-visibility --no-color '{file}' {opts.join(" ")} >'{file}.out1' 2>&1 </dev/null -o '{file}.bin'"
277 var res = sys.system(cmd)
278 return res
279 end
280 end
281
282 private class NitunitDecorator
283 super HTMLDecorator
284
285 var executor: NitUnitExecutor
286
287 redef fun add_code(v, block) do
288 var code = block.raw_content
289 var meta = block.meta or else "nit"
290 # Do not try to test non-nit code.
291 if meta != "nit" then return
292 # Try to parse code blocks
293 var ast = executor.toolcontext.parse_something(code)
294
295 var mdoc = executor.mdoc
296 assert mdoc != null
297
298 # Skip pure comments
299 if ast isa TComment then return
300
301 # The location is computed according to the starts of the mdoc and the block
302 # Note, the following assumes that all the comments of the mdoc are correctly aligned.
303 var loc = block.block.location
304 var line_offset = loc.line_start + mdoc.location.line_start - 2
305 var column_offset = loc.column_start + mdoc.location.column_start
306 # Hack to handle precise location in blocks
307 # TODO remove when markdown is more reliable
308 if block isa BlockFence then
309 # Skip the starting fence
310 line_offset += 1
311 else
312 # Account a standard 4 space indentation
313 column_offset += 4
314 end
315
316 # We want executable code
317 if not (ast isa AModule or ast isa ABlockExpr or ast isa AExpr) then
318 var message
319 var l = ast.location
320 # Get real location of the node (or error)
321 var location = new Location(mdoc.location.file,
322 l.line_start + line_offset,
323 l.line_end + line_offset,
324 l.column_start + column_offset,
325 l.column_end + column_offset)
326 if ast isa AError then
327 message = ast.message
328 else
329 message = "Error: Invalid Nit code."
330 end
331
332 executor.toolcontext.warning(location, "invalid-block", "{message} To suppress this message, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`).")
333 executor.failures.add("{location}: {message}")
334 return
335 end
336
337 # Create a first block
338 # Or create a new block for modules that are more than a main part
339 if executor.blocks.is_empty or ast isa AModule then
340 executor.blocks.add(new Buffer)
341 end
342
343 # Add it to the file
344 executor.blocks.last.append code
345 end
346 end
347
348 # A unit-test to run
349 class DocUnit
350 # The doc that contains self
351 var mdoc: MDoc
352
353 # The XML node that contains the information about the execution
354 var testcase: HTMLTag
355
356 # The text of the code to execute
357 var block: String
358 end
359
360 redef class ModelBuilder
361 # Total number analyzed `MEntity`
362 var total_entities = 0
363
364 # The number of `MEntity` that have some documentation
365 var doc_entities = 0
366
367 # The total number of executed docunits
368 var unit_entities = 0
369
370 # The number failed docunits
371 var failed_entities = 0
372
373 # Extracts and executes all the docunits in the `mmodule`
374 # Returns a JUnit-compatible `<testsuite>` XML element that contains the results of the executions.
375 fun test_markdown(mmodule: MModule): HTMLTag
376 do
377 var ts = new HTMLTag("testsuite")
378 toolcontext.info("nitunit: doc-unit {mmodule}", 2)
379
380 var nmodule = mmodule2node(mmodule)
381 if nmodule == null then return ts
382
383 # usualy, only the original module must be imported in the unit test.
384 var o = mmodule
385 var g = o.mgroup
386 if g != null and g.mpackage.name == "core" then
387 # except for a unit test in a module of `core`
388 # in this case, the whole `core` must be imported
389 o = get_mmodule_by_name(nmodule, g, g.mpackage.name).as(not null)
390 end
391
392 ts.attr("package", mmodule.full_name)
393
394 var prefix = toolcontext.test_dir
395 prefix = prefix.join_path(mmodule.to_s)
396 var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
397
398 var tc
399
400 do
401 total_entities += 1
402 var nmoduledecl = nmodule.n_moduledecl
403 if nmoduledecl == null then break label x
404 var ndoc = nmoduledecl.n_doc
405 if ndoc == null then break label x
406 doc_entities += 1
407 tc = new HTMLTag("testcase")
408 # NOTE: jenkins expects a '.' in the classname attr
409 tc.attr("classname", "nitunit." + mmodule.full_name + ".<module>")
410 tc.attr("name", "<module>")
411 d2m.extract(ndoc.to_mdoc, tc)
412 end label x
413 for nclassdef in nmodule.n_classdefs do
414 var mclassdef = nclassdef.mclassdef
415 if mclassdef == null then continue
416 if nclassdef isa AStdClassdef then
417 total_entities += 1
418 var ndoc = nclassdef.n_doc
419 if ndoc != null then
420 doc_entities += 1
421 tc = new HTMLTag("testcase")
422 tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
423 tc.attr("name", "<class>")
424 d2m.extract(ndoc.to_mdoc, tc)
425 end
426 end
427 for npropdef in nclassdef.n_propdefs do
428 var mpropdef = npropdef.mpropdef
429 if mpropdef == null then continue
430 total_entities += 1
431 var ndoc = npropdef.n_doc
432 if ndoc != null then
433 doc_entities += 1
434 tc = new HTMLTag("testcase")
435 tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
436 tc.attr("name", mpropdef.mproperty.full_name)
437 d2m.extract(ndoc.to_mdoc, tc)
438 end
439 end
440 end
441
442 d2m.run_tests
443
444 return ts
445 end
446
447 # Extracts and executes all the docunits in the readme of the `mgroup`
448 # Returns a JUnit-compatible `<testsuite>` XML element that contains the results of the executions.
449 fun test_group(mgroup: MGroup): HTMLTag
450 do
451 var ts = new HTMLTag("testsuite")
452 toolcontext.info("nitunit: doc-unit group {mgroup}", 2)
453
454 # usually, only the default module must be imported in the unit test.
455 var o = mgroup.default_mmodule
456
457 ts.attr("package", mgroup.full_name)
458
459 var prefix = toolcontext.test_dir
460 prefix = prefix.join_path(mgroup.to_s)
461 var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
462
463 var tc
464
465 total_entities += 1
466 var mdoc = mgroup.mdoc
467 if mdoc == null then return ts
468
469 doc_entities += 1
470 tc = new HTMLTag("testcase")
471 # NOTE: jenkins expects a '.' in the classname attr
472 tc.attr("classname", "nitunit." + mgroup.full_name)
473 tc.attr("name", "<group>")
474 d2m.extract(mdoc, tc)
475
476 d2m.run_tests
477
478 return ts
479 end
480
481 # Test a document object unrelated to a Nit entity
482 fun test_mdoc(mdoc: MDoc): HTMLTag
483 do
484 var ts = new HTMLTag("testsuite")
485 var file = mdoc.location.to_s
486
487 toolcontext.info("nitunit: doc-unit file {file}", 2)
488
489 ts.attr("package", file)
490
491 var prefix = toolcontext.test_dir / "file"
492 var d2m = new NitUnitExecutor(toolcontext, prefix, null, ts)
493
494 var tc
495
496 total_entities += 1
497 doc_entities += 1
498
499 tc = new HTMLTag("testcase")
500 # NOTE: jenkins expects a '.' in the classname attr
501 tc.attr("classname", "nitunit.<file>")
502 tc.attr("name", file)
503
504 d2m.extract(mdoc, tc)
505 d2m.run_tests
506
507 return ts
508 end
509 end