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 # Program to extract and execute unit tests from nit source files
18 import modelize_property
19 intrude import markdown
22 # Extractor, Executor an Reporter for the tests in a module
26 # The name of the module to import
29 # The prefix of the generated Nit source-file
32 # The XML node associated to the module
33 var testsuite
: HTMLTag
36 init(toolcontext
: ToolContext, prefix
: String, modname
: String, testsuite
: HTMLTag)
40 self.modname
= modname
41 self.testsuite
= testsuite
44 # All blocks of code from a same `ADoc`
45 var blocks
= new Array[Array[String]]
47 redef fun process_code
(n
: HTMLTag, text
: String)
50 var ast
= toolcontext
.parse_something
(text
)
52 # We want executable code
53 if not (ast
isa AModule or ast
isa ABlockExpr or ast
isa AExpr) then
54 if ndoc
!= null and n
.tag
== "pre" and toolcontext
.opt_warn
.value
> 1 then
55 toolcontext
.warning
(ndoc
.location
, "Warning: There is a block of code that is not valid Nit, thus not considered a nitunit")
56 if ast
isa AError then toolcontext
.warning
(ast
.location
, ast
.message
)
57 ndoc
= null # To avoid multiple warning in the same node
62 # Search `assert` in the AST
63 var v
= new SearchAssertVisitor
66 if ndoc
!= null and n
.tag
== "pre" and toolcontext
.opt_warn
.value
> 1 then
67 toolcontext
.warning
(ndoc
.location
, "Warning: There is a block of Nit code without `assert`, thus not considered a nitunit")
68 ndoc
= null # To avoid multiple warning in the same node
73 # Create a first block
74 # Or create a new block for modules that are more than a main part
75 if blocks
.is_empty
or ast
isa AModule then
76 blocks
.add
(new Array[String])
83 # The associated node to localize warnings
84 var ndoc
: nullable ADoc
86 # used to generate distinct names
89 # The entry point for a new `ndoc` node
90 # Fill the prepated `tc` (testcase) XTM node
91 fun extract
(ndoc
: ADoc, tc
: HTMLTag)
98 toolcontext
.check_errors
100 if blocks
.is_empty
then return
102 for block
in blocks
do test_block
(ndoc
, tc
, block
)
106 fun test_block
(ndoc
: ADoc, tc
: HTMLTag, block
: Array[String])
108 toolcontext
.modelbuilder
.unit_entities
+= 1
111 var file
= "{prefix}{cpt}.nit"
113 toolcontext
.info
("Execute {tc.attrs["classname"]}.{tc.attrs["name"]} in {file}", 1)
115 var dir
= file
.dirname
116 if dir
!= "" then dir
.mkdir
118 f
= new OFStream.open
(file
)
119 f
.write
("# GENERATED FILE\n")
120 f
.write
("# Example extracted from a documentation\n")
121 var modname
= self.modname
122 f
.write
("import {modname}\n")
129 if toolcontext
.opt_noact
.value
then return
131 var nit_dir
= toolcontext
.nit_dir
132 var nitg
= "{nit_dir}/bin/nitg"
133 if nit_dir
== null or not nitg
.file_exists
then
134 toolcontext
.error
(null, "Cannot find nitg. Set envvar NIT_DIR.")
135 toolcontext
.check_errors
137 var cmd
= "{nitg} --ignore-visibility --no-color '{file}' -I . >'{file}.out1' 2>&1 </dev/null -o '{file}.bin'"
138 var res
= sys
.system
(cmd
)
141 res2
= sys
.system
("./{file}.bin >>'{file}.out1' 2>&1 </dev/null")
145 f
= new IFStream.open
("{file}.out1")
147 n2
= new HTMLTag("system-err")
152 n2
= new HTMLTag("system-out")
154 for text
in block
do n2
.append
(text
)
158 var ne
= new HTMLTag("failure")
159 ne
.attr
("message", msg
)
161 toolcontext
.warning
(ndoc
.location
, "FAILURE: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
162 toolcontext
.modelbuilder
.failed_entities
+= 1
163 else if res2
!= 0 then
164 var ne
= new HTMLTag("error")
165 ne
.attr
("message", msg
)
167 toolcontext
.warning
(ndoc
.location
, "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
168 toolcontext
.modelbuilder
.failed_entities
+= 1
170 toolcontext
.check_errors
176 class SearchAssertVisitor
179 redef fun visit
(node
)
183 else if node
isa AAssertExpr then
192 redef class ModelBuilder
193 var total_entities
= 0
195 var unit_entities
= 0
196 var failed_entities
= 0
198 fun test_markdown
(mmodule
: MModule): HTMLTag
200 var ts
= new HTMLTag("testsuite")
201 toolcontext
.info
("nitunit: {mmodule}", 2)
202 if not mmodule2nmodule
.has_key
(mmodule
) then return ts
204 var nmodule
= mmodule2nmodule
[mmodule
]
205 assert nmodule
!= null
207 # what module to import in the unit test.
208 # try to detect the main module of the project
209 # TODO do things correctly once the importation of arbitraty nested module is legal
213 o
= get_mmodule_by_name
(nmodule
, g
, g
.mproject
.name
).as(not null)
216 ts
.attr
("package", mmodule
.full_name
)
218 var prefix
= toolcontext
.opt_dir
.value
219 if prefix
== null then prefix
= ".nitunit"
220 prefix
= prefix
.join_path
(mmodule
.to_s
)
221 var d2m
= new NitUnitExecutor(toolcontext
, prefix
, o
.name
, ts
)
227 var nmoduledecl
= nmodule
.n_moduledecl
228 if nmoduledecl
== null then break label x
229 var ndoc
= nmoduledecl
.n_doc
230 if ndoc
== null then break label x
232 tc
= new HTMLTag("testcase")
233 # NOTE: jenkins expects a '.' in the classname attr
234 tc
.attr
("classname", mmodule
.full_name
+ ".<module>")
235 tc
.attr
("name", "<module>")
236 d2m
.extract
(ndoc
, tc
)
238 for nclassdef
in nmodule
.n_classdefs
do
239 var mclassdef
= nclassdef
.mclassdef
.as(not null)
240 if nclassdef
isa AStdClassdef then
242 var ndoc
= nclassdef
.n_doc
245 tc
= new HTMLTag("testcase")
246 tc
.attr
("classname", mmodule
.full_name
+ "." + mclassdef
.mclass
.full_name
)
247 tc
.attr
("name", "<class>")
248 d2m
.extract
(ndoc
, tc
)
251 for npropdef
in nclassdef
.n_propdefs
do
252 var mpropdef
= npropdef
.mpropdef
.as(not null)
254 var ndoc
= npropdef
.n_doc
257 tc
= new HTMLTag("testcase")
258 tc
.attr
("classname", mmodule
.full_name
+ "." + mclassdef
.mclass
.full_name
)
259 tc
.attr
("name", mpropdef
.mproperty
.full_name
)
260 d2m
.extract
(ndoc
, tc
)
269 redef class ToolContext
270 var opt_full
= new OptionBool("Process also imported modules", "--full")
271 var opt_output
= new OptionString("Output name (default is 'nitunit.xml')", "-o", "--output")
272 var opt_dir
= new OptionString("Working directory (default is '.nitunit')", "--dir")
273 var opt_noact
= new OptionBool("Does not compile and run tests", "--no-act")
276 var toolcontext
= new ToolContext
278 toolcontext
.option_context
.add_option
(toolcontext
.opt_full
, toolcontext
.opt_output
, toolcontext
.opt_dir
, toolcontext
.opt_noact
)
279 toolcontext
.tooldescription
= "Usage: nitunit [OPTION]... <file.nit>...\nExecutes the unit tests from Nit source files."
281 toolcontext
.process_options
(args
)
282 var args
= toolcontext
.option_context
.rest
284 var model
= new Model
285 var modelbuilder
= new ModelBuilder(model
, toolcontext
)
287 var mmodules
= modelbuilder
.parse
(args
)
288 modelbuilder
.run_phases
290 var page
= new HTMLTag("testsuites")
292 if toolcontext
.opt_full
.value
then mmodules
= model
.mmodules
295 page
.add modelbuilder
.test_markdown
(m
)
298 var file
= toolcontext
.opt_output
.value
299 if file
== null then file
= "nitunit.xml"
300 page
.write_to_file
(file
)
301 print
"Results saved in {file}"
303 if modelbuilder
.unit_entities
== 0 then
304 print
"No nitunits found"
305 else if modelbuilder
.failed_entities
== 0 and not toolcontext
.opt_noact
.value
then
308 print
"Entities: {modelbuilder.total_entities}; Documented ones: {modelbuilder.doc_entities}; With nitunits: {modelbuilder.unit_entities}; Failures: {modelbuilder.failed_entities}"