lib/markdown: promote `BlockFence::meta` to `BlockCode` to simplify clients
[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 nit_dir = toolcontext.nit_dir
272 var nitc = nit_dir/"bin/nitc"
273 if not nitc.file_exists then
274 toolcontext.error(null, "Error: cannot find nitc. Set envvar NIT_DIR.")
275 toolcontext.check_errors
276 end
277 var opts = new Array[String]
278 if mmodule != null then
279 opts.add "-I {mmodule.location.file.filename.dirname}"
280 end
281 var cmd = "{nitc} --ignore-visibility --no-color '{file}' {opts.join(" ")} >'{file}.out1' 2>&1 </dev/null -o '{file}.bin'"
282 var res = sys.system(cmd)
283 return res
284 end
285 end
286
287 private class NitunitDecorator
288 super HTMLDecorator
289
290 var executor: NitUnitExecutor
291
292 redef fun add_code(v, block) do
293 var code = block.raw_content
294 var meta = block.meta or else "nit"
295 # Do not try to test non-nit code.
296 if meta != "nit" then return
297 # Try to parse code blocks
298 var ast = executor.toolcontext.parse_something(code)
299
300 # Skip pure comments
301 if ast isa TComment then return
302
303 # We want executable code
304 if not (ast isa AModule or ast isa ABlockExpr or ast isa AExpr) then
305 var message = ""
306 if ast isa AError then message = " At {ast.location}: {ast.message}."
307 executor.toolcontext.warning(executor.mdoc.location, "invalid-block", "Error: there is a block of invalid Nit code, thus not considered a nitunit. To suppress this warning, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`).{message}")
308 executor.failures.add("{executor.mdoc.location}: Invalid block of code.{message}")
309 return
310 end
311
312 # Create a first block
313 # Or create a new block for modules that are more than a main part
314 if executor.blocks.is_empty or ast isa AModule then
315 executor.blocks.add(new Buffer)
316 end
317
318 # Add it to the file
319 executor.blocks.last.append code
320 end
321 end
322
323 # A unit-test to run
324 class DocUnit
325 # The doc that contains self
326 var mdoc: MDoc
327
328 # The XML node that contains the information about the execution
329 var testcase: HTMLTag
330
331 # The text of the code to execute
332 var block: String
333 end
334
335 redef class ModelBuilder
336 # Total number analyzed `MEntity`
337 var total_entities = 0
338
339 # The number of `MEntity` that have some documentation
340 var doc_entities = 0
341
342 # The total number of executed docunits
343 var unit_entities = 0
344
345 # The number failed docunits
346 var failed_entities = 0
347
348 # Extracts and executes all the docunits in the `mmodule`
349 # Returns a JUnit-compatible `<testsuite>` XML element that contains the results of the executions.
350 fun test_markdown(mmodule: MModule): HTMLTag
351 do
352 var ts = new HTMLTag("testsuite")
353 toolcontext.info("nitunit: doc-unit {mmodule}", 2)
354
355 var nmodule = mmodule2node(mmodule)
356 if nmodule == null then return ts
357
358 # usualy, only the original module must be imported in the unit test.
359 var o = mmodule
360 var g = o.mgroup
361 if g != null and g.mproject.name == "core" then
362 # except for a unit test in a module of `core`
363 # in this case, the whole `core` must be imported
364 o = get_mmodule_by_name(nmodule, g, g.mproject.name).as(not null)
365 end
366
367 ts.attr("package", mmodule.full_name)
368
369 var prefix = toolcontext.test_dir
370 prefix = prefix.join_path(mmodule.to_s)
371 var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
372
373 var tc
374
375 do
376 total_entities += 1
377 var nmoduledecl = nmodule.n_moduledecl
378 if nmoduledecl == null then break label x
379 var ndoc = nmoduledecl.n_doc
380 if ndoc == null then break label x
381 doc_entities += 1
382 tc = new HTMLTag("testcase")
383 # NOTE: jenkins expects a '.' in the classname attr
384 tc.attr("classname", "nitunit." + mmodule.full_name + ".<module>")
385 tc.attr("name", "<module>")
386 d2m.extract(ndoc.to_mdoc, tc)
387 end label x
388 for nclassdef in nmodule.n_classdefs do
389 var mclassdef = nclassdef.mclassdef
390 if mclassdef == null then continue
391 if nclassdef isa AStdClassdef then
392 total_entities += 1
393 var ndoc = nclassdef.n_doc
394 if ndoc != null then
395 doc_entities += 1
396 tc = new HTMLTag("testcase")
397 tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
398 tc.attr("name", "<class>")
399 d2m.extract(ndoc.to_mdoc, tc)
400 end
401 end
402 for npropdef in nclassdef.n_propdefs do
403 var mpropdef = npropdef.mpropdef
404 if mpropdef == null then continue
405 total_entities += 1
406 var ndoc = npropdef.n_doc
407 if ndoc != null then
408 doc_entities += 1
409 tc = new HTMLTag("testcase")
410 tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
411 tc.attr("name", mpropdef.mproperty.full_name)
412 d2m.extract(ndoc.to_mdoc, tc)
413 end
414 end
415 end
416
417 d2m.run_tests
418
419 return ts
420 end
421
422 # Extracts and executes all the docunits in the readme of the `mgroup`
423 # Returns a JUnit-compatible `<testsuite>` XML element that contains the results of the executions.
424 fun test_group(mgroup: MGroup): HTMLTag
425 do
426 var ts = new HTMLTag("testsuite")
427 toolcontext.info("nitunit: doc-unit group {mgroup}", 2)
428
429 # usually, only the default module must be imported in the unit test.
430 var o = mgroup.default_mmodule
431
432 ts.attr("package", mgroup.full_name)
433
434 var prefix = toolcontext.test_dir
435 prefix = prefix.join_path(mgroup.to_s)
436 var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
437
438 var tc
439
440 total_entities += 1
441 var mdoc = mgroup.mdoc
442 if mdoc == null then return ts
443
444 doc_entities += 1
445 tc = new HTMLTag("testcase")
446 # NOTE: jenkins expects a '.' in the classname attr
447 tc.attr("classname", "nitunit." + mgroup.full_name)
448 tc.attr("name", "<group>")
449 d2m.extract(mdoc, tc)
450
451 d2m.run_tests
452
453 return ts
454 end
455
456 # Test a document object unrelated to a Nit entity
457 fun test_mdoc(mdoc: MDoc): HTMLTag
458 do
459 var ts = new HTMLTag("testsuite")
460 var file = mdoc.location.to_s
461
462 toolcontext.info("nitunit: doc-unit file {file}", 2)
463
464 ts.attr("package", file)
465
466 var prefix = toolcontext.test_dir / "file"
467 var d2m = new NitUnitExecutor(toolcontext, prefix, null, ts)
468
469 var tc
470
471 total_entities += 1
472 doc_entities += 1
473
474 tc = new HTMLTag("testcase")
475 # NOTE: jenkins expects a '.' in the classname attr
476 tc.attr("classname", "nitunit.<file>")
477 tc.attr("name", file)
478
479 d2m.extract(mdoc, tc)
480 d2m.run_tests
481
482 return ts
483 end
484 end