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