nitunit: test only block of code
[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 markdown
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
29 var mmodule: 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 redef fun process_code(n: HTMLTag, text: String)
38 do
39 # Skip non-blocks
40 if n.tag != "pre" then return
41
42 # Try to parse it
43 var ast = toolcontext.parse_something(text)
44
45 # We want executable code
46 if not (ast isa AModule or ast isa ABlockExpr or ast isa AExpr) then
47 if ndoc != null and n.tag == "pre" and toolcontext.opt_warn.value > 1 then
48 toolcontext.warning(ndoc.location, "invalid-block", "Warning: There is a block of code that is not valid Nit, thus not considered a nitunit")
49 if ast isa AError then toolcontext.warning(ast.location, "syntax-error", ast.message)
50 ndoc = null # To avoid multiple warning in the same node
51 end
52 return
53 end
54
55 # Search `assert` in the AST
56 var v = new SearchAssertVisitor
57 v.enter_visit(ast)
58 if not v.foundit then
59 if ndoc != null and n.tag == "pre" and toolcontext.opt_warn.value > 1 then
60 toolcontext.warning(ndoc.location, "invalid-block", "Warning: There is a block of Nit code without `assert`, thus not considered a nitunit")
61 ndoc = null # To avoid multiple warning in the same node
62 end
63 return
64 end
65
66 # Create a first block
67 # Or create a new block for modules that are more than a main part
68 if blocks.is_empty or ast isa AModule then
69 blocks.add(new Array[String])
70 end
71
72 # Add it to the file
73 blocks.last.add(text)
74 end
75
76 # The associated node to localize warnings
77 var ndoc: nullable ADoc = null
78
79 # used to generate distinct names
80 var cpt = 0
81
82 # The entry point for a new `ndoc` node
83 # Fill the prepated `tc` (testcase) XTM node
84 fun extract(ndoc: ADoc, tc: HTMLTag)
85 do
86 blocks.clear
87
88 self.ndoc = ndoc
89
90 work(ndoc.to_mdoc)
91 toolcontext.check_errors
92
93 if blocks.is_empty then return
94
95 for block in blocks do test_block(ndoc, tc, block)
96 end
97
98 # Execute a block
99 fun test_block(ndoc: ADoc, tc: HTMLTag, block: Array[String])
100 do
101 toolcontext.modelbuilder.unit_entities += 1
102
103 cpt += 1
104 var file = "{prefix}{cpt}.nit"
105
106 toolcontext.info("Execute doc-unit {tc.attrs["name"]} in {file}", 1)
107
108 var dir = file.dirname
109 if dir != "" then dir.mkdir
110 var f
111 f = new OFStream.open(file)
112 f.write("# GENERATED FILE\n")
113 f.write("# Example extracted from a documentation\n")
114 f.write("import {mmodule.name}\n")
115 f.write("\n")
116 for text in block do
117 f.write(text)
118 end
119 f.close
120
121 if toolcontext.opt_noact.value then return
122
123 var nit_dir = toolcontext.nit_dir
124 var nitg = nit_dir/"bin/nitg"
125 if not nitg.file_exists then
126 toolcontext.error(null, "Cannot find nitg. Set envvar NIT_DIR.")
127 toolcontext.check_errors
128 end
129 var cmd = "{nitg} --ignore-visibility --no-color '{file}' -I {mmodule.location.file.filename.dirname} >'{file}.out1' 2>&1 </dev/null -o '{file}.bin'"
130 var res = sys.system(cmd)
131 var res2 = 0
132 if res == 0 then
133 res2 = sys.system("{file.to_program_name}.bin >>'{file}.out1' 2>&1 </dev/null")
134 end
135
136 var msg
137 f = new IFStream.open("{file}.out1")
138 var n2
139 n2 = new HTMLTag("system-err")
140 tc.add n2
141 msg = f.read_all
142 f.close
143
144 n2 = new HTMLTag("system-out")
145 tc.add n2
146 for text in block do n2.append(text)
147
148
149 if res != 0 then
150 var ne = new HTMLTag("failure")
151 ne.attr("message", msg)
152 tc.add ne
153 toolcontext.warning(ndoc.location, "failure", "FAILURE: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
154 toolcontext.modelbuilder.failed_entities += 1
155 else if res2 != 0 then
156 var ne = new HTMLTag("error")
157 ne.attr("message", msg)
158 tc.add ne
159 toolcontext.warning(ndoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
160 toolcontext.modelbuilder.failed_entities += 1
161 end
162 toolcontext.check_errors
163
164 testsuite.add(tc)
165 end
166 end
167
168 class SearchAssertVisitor
169 super Visitor
170 var foundit = false
171 redef fun visit(node)
172 do
173 if foundit then
174 return
175 else if node isa AAssertExpr then
176 foundit = true
177 return
178 else
179 node.visit_all(self)
180 end
181 end
182 end
183
184 redef class ModelBuilder
185 var total_entities = 0
186 var doc_entities = 0
187 var unit_entities = 0
188 var failed_entities = 0
189
190 fun test_markdown(mmodule: MModule): HTMLTag
191 do
192 var ts = new HTMLTag("testsuite")
193 toolcontext.info("nitunit: doc-unit {mmodule}", 2)
194 if not mmodule2nmodule.has_key(mmodule) then return ts
195
196 var nmodule = mmodule2nmodule[mmodule]
197
198 # usualy, only the original module must be imported in the unit test.
199 var o = mmodule
200 var g = o.mgroup
201 if g != null and g.mproject.name == "standard" then
202 # except for a unit test in a module of standard
203 # in this case, the whole standard must be imported
204 o = get_mmodule_by_name(nmodule, g, g.mproject.name).as(not null)
205 end
206
207 ts.attr("package", mmodule.full_name)
208
209 var prefix = toolcontext.test_dir
210 prefix = prefix.join_path(mmodule.to_s)
211 var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
212
213 var tc
214
215 do
216 total_entities += 1
217 var nmoduledecl = nmodule.n_moduledecl
218 if nmoduledecl == null then break label x
219 var ndoc = nmoduledecl.n_doc
220 if ndoc == null then break label x
221 doc_entities += 1
222 tc = new HTMLTag("testcase")
223 # NOTE: jenkins expects a '.' in the classname attr
224 tc.attr("classname", "nitunit." + mmodule.full_name + ".<module>")
225 tc.attr("name", "<module>")
226 d2m.extract(ndoc, tc)
227 end label x
228 for nclassdef in nmodule.n_classdefs do
229 var mclassdef = nclassdef.mclassdef
230 if mclassdef == null then continue
231 if nclassdef isa AStdClassdef then
232 total_entities += 1
233 var ndoc = nclassdef.n_doc
234 if ndoc != null then
235 doc_entities += 1
236 tc = new HTMLTag("testcase")
237 tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
238 tc.attr("name", "<class>")
239 d2m.extract(ndoc, tc)
240 end
241 end
242 for npropdef in nclassdef.n_propdefs do
243 var mpropdef = npropdef.mpropdef
244 if mpropdef == null then continue
245 total_entities += 1
246 var ndoc = npropdef.n_doc
247 if ndoc != null then
248 doc_entities += 1
249 tc = new HTMLTag("testcase")
250 tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
251 tc.attr("name", mpropdef.mproperty.full_name)
252 d2m.extract(ndoc, tc)
253 end
254 end
255 end
256
257 return ts
258 end
259 end