Rename REAMDE to README.md
[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, if any
29 var mmodule: nullable 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(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}")
61 failures.add("{mdoc.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 documentation object
76 var mdoc: nullable MDoc = 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(mdoc: MDoc, tc: HTMLTag)
86 do
87 blocks.clear
88 failures.clear
89
90 self.mdoc = mdoc
91
92 work(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(mdoc, 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.mdoc.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.mdoc.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.mdoc.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 if mmodule != null then
282 f.write("import {mmodule.name}\n")
283 end
284 f.write("\n")
285 return f
286 end
287
288 # Compile an unit file and return the compiler return code
289 #
290 # Can terminate the program if the compiler is not found
291 private fun compile_unitfile(file: String): Int
292 do
293 var nit_dir = toolcontext.nit_dir
294 var nitc = nit_dir/"bin/nitc"
295 if not nitc.file_exists then
296 toolcontext.error(null, "Error: cannot find nitc. Set envvar NIT_DIR.")
297 toolcontext.check_errors
298 end
299 var opts = new Array[String]
300 if mmodule != null then
301 opts.add "-I {mmodule.location.file.filename.dirname}"
302 end
303 var cmd = "{nitc} --ignore-visibility --no-color '{file}' {opts.join(" ")} >'{file}.out1' 2>&1 </dev/null -o '{file}.bin'"
304 var res = sys.system(cmd)
305 return res
306 end
307 end
308
309 # A unit-test to run
310 class DocUnit
311 # The doc that contains self
312 var mdoc: MDoc
313
314 # The XML node that contains the information about the execution
315 var testcase: HTMLTag
316
317 # The text of the code to execute
318 var block: String
319 end
320
321 redef class ModelBuilder
322 # Total number analyzed `MEntity`
323 var total_entities = 0
324
325 # The number of `MEntity` that have some documentation
326 var doc_entities = 0
327
328 # The total number of executed docunits
329 var unit_entities = 0
330
331 # The number failed docunits
332 var failed_entities = 0
333
334 # Extracts and executes all the docunits in the `mmodule`
335 # Returns a JUnit-compatible `<testsuite>` XML element that contains the results of the executions.
336 fun test_markdown(mmodule: MModule): HTMLTag
337 do
338 var ts = new HTMLTag("testsuite")
339 toolcontext.info("nitunit: doc-unit {mmodule}", 2)
340
341 var nmodule = mmodule2node(mmodule)
342 if nmodule == null then return ts
343
344 # usualy, only the original module must be imported in the unit test.
345 var o = mmodule
346 var g = o.mgroup
347 if g != null and g.mproject.name == "standard" then
348 # except for a unit test in a module of standard
349 # in this case, the whole standard must be imported
350 o = get_mmodule_by_name(nmodule, g, g.mproject.name).as(not null)
351 end
352
353 ts.attr("package", mmodule.full_name)
354
355 var prefix = toolcontext.test_dir
356 prefix = prefix.join_path(mmodule.to_s)
357 var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
358
359 var tc
360
361 do
362 total_entities += 1
363 var nmoduledecl = nmodule.n_moduledecl
364 if nmoduledecl == null then break label x
365 var ndoc = nmoduledecl.n_doc
366 if ndoc == null then break label x
367 doc_entities += 1
368 tc = new HTMLTag("testcase")
369 # NOTE: jenkins expects a '.' in the classname attr
370 tc.attr("classname", "nitunit." + mmodule.full_name + ".<module>")
371 tc.attr("name", "<module>")
372 d2m.extract(ndoc.to_mdoc, tc)
373 end label x
374 for nclassdef in nmodule.n_classdefs do
375 var mclassdef = nclassdef.mclassdef
376 if mclassdef == null then continue
377 if nclassdef isa AStdClassdef then
378 total_entities += 1
379 var ndoc = nclassdef.n_doc
380 if ndoc != null then
381 doc_entities += 1
382 tc = new HTMLTag("testcase")
383 tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
384 tc.attr("name", "<class>")
385 d2m.extract(ndoc.to_mdoc, tc)
386 end
387 end
388 for npropdef in nclassdef.n_propdefs do
389 var mpropdef = npropdef.mpropdef
390 if mpropdef == null then continue
391 total_entities += 1
392 var ndoc = npropdef.n_doc
393 if ndoc != null then
394 doc_entities += 1
395 tc = new HTMLTag("testcase")
396 tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
397 tc.attr("name", mpropdef.mproperty.full_name)
398 d2m.extract(ndoc.to_mdoc, tc)
399 end
400 end
401 end
402
403 d2m.run_tests
404
405 return ts
406 end
407
408 # Extracts and executes all the docunits in the readme of the `mgroup`
409 # Returns a JUnit-compatible `<testsuite>` XML element that contains the results of the executions.
410 fun test_group(mgroup: MGroup): HTMLTag
411 do
412 var ts = new HTMLTag("testsuite")
413 toolcontext.info("nitunit: doc-unit group {mgroup}", 2)
414
415 # usually, only the default module must be imported in the unit test.
416 var o = mgroup.default_mmodule
417
418 ts.attr("package", mgroup.full_name)
419
420 var prefix = toolcontext.test_dir
421 prefix = prefix.join_path(mgroup.to_s)
422 var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
423
424 var tc
425
426 total_entities += 1
427 var mdoc = mgroup.mdoc
428 if mdoc == null then return ts
429
430 doc_entities += 1
431 tc = new HTMLTag("testcase")
432 # NOTE: jenkins expects a '.' in the classname attr
433 tc.attr("classname", "nitunit." + mgroup.full_name)
434 tc.attr("name", "<group>")
435 d2m.extract(mdoc, tc)
436
437 d2m.run_tests
438
439 return ts
440 end
441
442 # Test a document object unrelated to a Nit entity
443 fun test_mdoc(mdoc: MDoc): HTMLTag
444 do
445 var ts = new HTMLTag("testsuite")
446 var file = mdoc.location.to_s
447
448 toolcontext.info("nitunit: doc-unit file {file}", 2)
449
450 ts.attr("package", file)
451
452 var prefix = toolcontext.test_dir / "file"
453 var d2m = new NitUnitExecutor(toolcontext, prefix, null, ts)
454
455 var tc
456
457 total_entities += 1
458 doc_entities += 1
459
460 tc = new HTMLTag("testcase")
461 # NOTE: jenkins expects a '.' in the classname attr
462 tc.attr("classname", "nitunit.<file>")
463 tc.attr("name", file)
464
465 d2m.extract(mdoc, tc)
466 d2m.run_tests
467
468 return ts
469 end
470 end