nitunit: enforce the testing of docunits (unless explicitely fence-tagged)
[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
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 # 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(ndoc.location, "invalid-block", "Error: There is a block of code that is not valid Nit, 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("{ndoc.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 node to localize warnings
76 var ndoc: nullable ADoc = null
77
78 # used to generate distinct names
79 var cpt = 0
80
81 # The entry point for a new `ndoc` node
82 # Fill the prepated `tc` (testcase) XTM node
83 fun extract(ndoc: ADoc, tc: HTMLTag)
84 do
85 blocks.clear
86 failures.clear
87
88 self.ndoc = ndoc
89
90 work(ndoc.to_mdoc)
91
92 toolcontext.check_errors
93
94 if not failures.is_empty then
95 for msg in failures do
96 var ne = new HTMLTag("failure")
97 ne.attr("message", msg)
98 tc.add ne
99 toolcontext.modelbuilder.failed_entities += 1
100 end
101 if blocks.is_empty then testsuite.add(tc)
102 end
103
104 if blocks.is_empty then return
105
106 for block in blocks do test_block(ndoc, tc, block)
107 end
108
109 # Execute a block
110 fun test_block(ndoc: ADoc, tc: HTMLTag, block: Array[String])
111 do
112 toolcontext.modelbuilder.unit_entities += 1
113
114 cpt += 1
115 var file = "{prefix}-{cpt}.nit"
116
117 toolcontext.info("Execute doc-unit {tc.attrs["name"]} in {file}", 1)
118
119 var dir = file.dirname
120 if dir != "" then dir.mkdir
121 var f
122 f = new OFStream.open(file)
123 f.write("# GENERATED FILE\n")
124 f.write("# Example extracted from a documentation\n")
125 f.write("import {mmodule.name}\n")
126 f.write("\n")
127 for text in block do
128 f.write(text)
129 end
130 f.close
131
132 if toolcontext.opt_noact.value then return
133
134 var nit_dir = toolcontext.nit_dir
135 var nitg = nit_dir/"bin/nitg"
136 if not nitg.file_exists then
137 toolcontext.error(null, "Cannot find nitg. Set envvar NIT_DIR.")
138 toolcontext.check_errors
139 end
140 var cmd = "{nitg} --ignore-visibility --no-color '{file}' -I {mmodule.location.file.filename.dirname} >'{file}.out1' 2>&1 </dev/null -o '{file}.bin'"
141 var res = sys.system(cmd)
142 var res2 = 0
143 if res == 0 then
144 res2 = sys.system("{file.to_program_name}.bin >>'{file}.out1' 2>&1 </dev/null")
145 end
146
147 var msg
148 f = new IFStream.open("{file}.out1")
149 var n2
150 n2 = new HTMLTag("system-err")
151 tc.add n2
152 msg = f.read_all
153 f.close
154
155 n2 = new HTMLTag("system-out")
156 tc.add n2
157 for text in block do n2.append(text)
158
159
160 if res != 0 then
161 var ne = new HTMLTag("failure")
162 ne.attr("message", msg)
163 tc.add ne
164 toolcontext.warning(ndoc.location, "failure", "FAILURE: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
165 toolcontext.modelbuilder.failed_entities += 1
166 else if res2 != 0 then
167 var ne = new HTMLTag("error")
168 ne.attr("message", msg)
169 tc.add ne
170 toolcontext.warning(ndoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
171 toolcontext.modelbuilder.failed_entities += 1
172 end
173 toolcontext.check_errors
174
175 testsuite.add(tc)
176 end
177 end
178
179 class SearchAssertVisitor
180 super Visitor
181 var foundit = false
182 redef fun visit(node)
183 do
184 if foundit then
185 return
186 else if node isa AAssertExpr then
187 foundit = true
188 return
189 else
190 node.visit_all(self)
191 end
192 end
193 end
194
195 redef class ModelBuilder
196 var total_entities = 0
197 var doc_entities = 0
198 var unit_entities = 0
199 var failed_entities = 0
200
201 fun test_markdown(mmodule: MModule): HTMLTag
202 do
203 var ts = new HTMLTag("testsuite")
204 toolcontext.info("nitunit: doc-unit {mmodule}", 2)
205 if not mmodule2nmodule.has_key(mmodule) then return ts
206
207 var nmodule = mmodule2nmodule[mmodule]
208
209 # usualy, only the original module must be imported in the unit test.
210 var o = mmodule
211 var g = o.mgroup
212 if g != null and g.mproject.name == "standard" then
213 # except for a unit test in a module of standard
214 # in this case, the whole standard must be imported
215 o = get_mmodule_by_name(nmodule, g, g.mproject.name).as(not null)
216 end
217
218 ts.attr("package", mmodule.full_name)
219
220 var prefix = toolcontext.test_dir
221 prefix = prefix.join_path(mmodule.to_s)
222 var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
223
224 var tc
225
226 do
227 total_entities += 1
228 var nmoduledecl = nmodule.n_moduledecl
229 if nmoduledecl == null then break label x
230 var ndoc = nmoduledecl.n_doc
231 if ndoc == null then break label x
232 doc_entities += 1
233 tc = new HTMLTag("testcase")
234 # NOTE: jenkins expects a '.' in the classname attr
235 tc.attr("classname", "nitunit." + mmodule.full_name + ".<module>")
236 tc.attr("name", "<module>")
237 d2m.extract(ndoc, tc)
238 end label x
239 for nclassdef in nmodule.n_classdefs do
240 var mclassdef = nclassdef.mclassdef
241 if mclassdef == null then continue
242 if nclassdef isa AStdClassdef then
243 total_entities += 1
244 var ndoc = nclassdef.n_doc
245 if ndoc != null then
246 doc_entities += 1
247 tc = new HTMLTag("testcase")
248 tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
249 tc.attr("name", "<class>")
250 d2m.extract(ndoc, tc)
251 end
252 end
253 for npropdef in nclassdef.n_propdefs do
254 var mpropdef = npropdef.mpropdef
255 if mpropdef == null then continue
256 total_entities += 1
257 var ndoc = npropdef.n_doc
258 if ndoc != null then
259 doc_entities += 1
260 tc = new HTMLTag("testcase")
261 tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
262 tc.attr("name", mpropdef.mproperty.full_name)
263 d2m.extract(ndoc, tc)
264 end
265 end
266 end
267
268 return ts
269 end
270 end