1 # This file is part of NIT ( http://www.nitlanguage.org ).
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
15 # Testing from code comments.
19 intrude import docdown
21 # Extractor, Executor and Reporter for the tests in a module
25 # The prefix of the generated Nit source-file
28 # The module to import, if any
29 var mmodule
: nullable MModule
31 # The XML node associated to the module
32 var testsuite
: HTMLTag
34 # All blocks of code from a same `ADoc`
35 var blocks
= new Array[Array[String]]
37 # All failures from a same `ADoc`
38 var failures
= new Array[String]
40 redef fun process_code
(n
: HTMLTag, text
: String, tag
: nullable String)
43 if n
.tag
!= "pre" then return
46 if tag
!= null and tag
!= "nit" and tag
!= "" then
51 var ast
= toolcontext
.parse_something
(text
)
54 if ast
isa TComment then return
56 # We want executable code
57 if not (ast
isa AModule or ast
isa ABlockExpr or ast
isa AExpr) then
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}")
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])
75 # The associated documentation object
76 var mdoc
: nullable MDoc = null
78 # used to generate distinct names
81 # The entry point for a new `ndoc` node
82 # Fill `docunits` with new discovered unit of tests.
84 # `tc` (testcase) is the pre-filled XML node
85 fun extract
(mdoc
: MDoc, tc
: HTMLTag)
94 toolcontext
.check_errors
96 if not failures
.is_empty
then
97 for msg
in failures
do
98 var ne
= new HTMLTag("failure")
99 ne
.attr
("message", msg
)
101 toolcontext
.modelbuilder
.failed_entities
+= 1
103 if blocks
.is_empty
then testsuite
.add
(tc
)
106 if blocks
.is_empty
then return
108 for block
in blocks
do
109 docunits
.add
new DocUnit(mdoc
, tc
, block
.join
(""))
113 # All extracted docunits
114 var docunits
= new Array[DocUnit]
116 # Execute all the docunits
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
125 test_single_docunit
(du
)
129 test_simple_docunits
(simple_du
)
132 # Executes multiples doc-units in a shared program.
133 # Used for docunits simple block of code (without modules, classes, functions etc.)
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])
140 if dus
.is_empty
then return
142 var file
= "{prefix}-0.nit"
144 var dir
= file
.dirname
145 if dir
!= "" then dir
.mkdir
147 f
= create_unitfile
(file
)
152 f
.write
("fun run_{i} do\n")
153 f
.write
("# {du.testcase.attrs["name"]}\n")
157 f
.write
("var a = args.first.to_i\n")
159 f
.write
("if a == {j} then run_{j}\n")
163 if toolcontext
.opt_noact
.value
then return
165 var res
= compile_unitfile
(file
)
169 # Fall-back to individual modes:
171 test_single_docunit
(du
)
179 toolcontext
.modelbuilder
.unit_entities
+= 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")
185 f
= new FileReader.open
("{file}.out1")
187 n2
= new HTMLTag("system-err")
192 n2
= new HTMLTag("system-out")
197 var ne
= new HTMLTag("error")
198 ne
.attr
("message", msg
)
200 toolcontext
.warning
(du
.mdoc
.location
, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
201 toolcontext
.modelbuilder
.failed_entities
+= 1
203 toolcontext
.check_errors
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)
214 toolcontext
.modelbuilder
.unit_entities
+= 1
217 var file
= "{prefix}-{cpt}.nit"
219 toolcontext
.info
("Execute doc-unit {tc.attrs["name"]} in {file}", 1)
222 f
= create_unitfile
(file
)
226 if toolcontext
.opt_noact
.value
then return
228 var res
= compile_unitfile
(file
)
231 res2
= sys
.system
("{file.to_program_name}.bin >>'{file}.out1' 2>&1 </dev/null")
235 f
= new FileReader.open
("{file}.out1")
237 n2
= new HTMLTag("system-err")
242 n2
= new HTMLTag("system-out")
248 var ne
= new HTMLTag("failure")
249 ne
.attr
("message", msg
)
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
)
257 toolcontext
.warning
(du
.mdoc
.location
, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
258 toolcontext
.modelbuilder
.failed_entities
+= 1
260 toolcontext
.check_errors
265 # Create and fill the header of a unit file `file`.
267 # A unit file is a Nit source file generated from one
268 # or more docunits that will be compiled and executed.
270 # The handled on the file is returned and must be completed and closed.
272 # `file` should be a valid filepath for a Nit source file.
273 private fun create_unitfile
(file
: String): Writer
275 var dir
= file
.dirname
276 if dir
!= "" then dir
.mkdir
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")
288 # Compile an unit file and return the compiler return code
290 # Can terminate the program if the compiler is not found
291 private fun compile_unitfile
(file
: String): Int
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
299 var opts
= new Array[String]
300 if mmodule
!= null then
301 opts
.add
"-I {mmodule.location.file.filename.dirname}"
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
)
311 # The doc that contains self
314 # The XML node that contains the information about the execution
315 var testcase
: HTMLTag
317 # The text of the code to execute
321 redef class ModelBuilder
322 # Total number analyzed `MEntity`
323 var total_entities
= 0
325 # The number of `MEntity` that have some documentation
328 # The total number of executed docunits
329 var unit_entities
= 0
331 # The number failed docunits
332 var failed_entities
= 0
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
338 var ts
= new HTMLTag("testsuite")
339 toolcontext
.info
("nitunit: doc-unit {mmodule}", 2)
341 var nmodule
= mmodule2node
(mmodule
)
342 if nmodule
== null then return ts
344 # usualy, only the original module must be imported in the unit test.
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)
353 ts
.attr
("package", mmodule
.full_name
)
355 var prefix
= toolcontext
.test_dir
356 prefix
= prefix
.join_path
(mmodule
.to_s
)
357 var d2m
= new NitUnitExecutor(toolcontext
, prefix
, o
, ts
)
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
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
)
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
379 var ndoc
= nclassdef
.n_doc
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
)
388 for npropdef
in nclassdef
.n_propdefs
do
389 var mpropdef
= npropdef
.mpropdef
390 if mpropdef
== null then continue
392 var ndoc
= npropdef
.n_doc
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
)
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
412 var ts
= new HTMLTag("testsuite")
413 toolcontext
.info
("nitunit: doc-unit group {mgroup}", 2)
415 # usually, only the default module must be imported in the unit test.
416 var o
= mgroup
.default_mmodule
418 ts
.attr
("package", mgroup
.full_name
)
420 var prefix
= toolcontext
.test_dir
421 prefix
= prefix
.join_path
(mgroup
.to_s
)
422 var d2m
= new NitUnitExecutor(toolcontext
, prefix
, o
, ts
)
427 var mdoc
= mgroup
.mdoc
428 if mdoc
== null then return ts
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
)
442 # Test a document object unrelated to a Nit entity
443 fun test_mdoc
(mdoc
: MDoc): HTMLTag
445 var ts
= new HTMLTag("testsuite")
446 var file
= mdoc
.location
.to_s
448 toolcontext
.info
("nitunit: doc-unit file {file}", 2)
450 ts
.attr
("package", file
)
452 var prefix
= toolcontext
.test_dir
/ "file"
453 var d2m
= new NitUnitExecutor(toolcontext
, prefix
, null, ts
)
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
)
465 d2m
.extract
(mdoc
, tc
)