tool: new program nitunit.nit
[nit.git] / src / nitunit.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 # Program to extract and execute unit tests from nit source files
16 module nitunit
17
18 import modelize_property
19 intrude import markdown
20 import parser_util
21
22 # Extractor, Executor an Reporter for the tests in a module
23 class NitUnitExecutor
24 super Doc2Mdwn
25
26 var toolcontext: ToolContext
27
28 # The name of the module to import
29 var modname: String
30
31 # The prefix of the generated Nit source-file
32 var prefix: String
33
34 # The XML node associated to the module
35 var testsuite: HTMLTag
36
37 # Initialize a new e
38 init(toolcontext: ToolContext, prefix: String, modname: String, testsuite: HTMLTag)
39 do
40 self.toolcontext = toolcontext
41 self.prefix = prefix
42 self.modname = modname
43 self.testsuite = testsuite
44 end
45
46 # All blocks of code from a same `ADoc`
47 var block = new Array[String]
48
49 redef fun process_code(n: HTMLTag, text: String)
50 do
51 # Try to parse it
52 var ast = toolcontext.parse_something(text)
53
54 # We want executable code
55 if not (ast isa AModule or ast isa ABlockExpr or ast isa AExpr) then return
56
57 # Search `assert` in the AST
58 var v = new SearchAssertVisitor
59 v.enter_visit(ast)
60 if not v.foundit then return
61
62 # Add it to the file
63 block.add(text)
64 end
65
66 # used to generate distinct names
67 var cpt = 0
68
69 # The entry point for a new `ndoc` node
70 # Fill the prepated `tc` (testcase) XTM node
71 fun extract(ndoc: ADoc, tc: HTMLTag)
72 do
73 block.clear
74
75 work(ndoc)
76
77 if block.is_empty then return
78
79 cpt += 1
80 var file = "{prefix}{cpt}.nit"
81
82 toolcontext.info("Execute {tc.attrs["classname"]}.{tc.attrs["name"]} in {file}", 2)
83
84 var dir = file.dirname
85 if dir != "" then dir.mkdir
86 var f
87 f = new OFStream.open(file)
88 f.write("# GENERATED FILE\n")
89 f.write("# Example extracted from a documentation\n")
90 var modname = self.modname
91 f.write("import {modname}\n")
92 f.write("\n")
93 for text in block do
94 f.write(text)
95 end
96 f.close
97
98 var cmd = "../bin/nitg --no-color '{file}' -I . >'{file}.out1' 2>&1 </dev/null -o '{file}.bin'"
99 var res = sys.system(cmd)
100 var res2 = 0
101 if res == 0 then
102 res2 = sys.system("./{file}.bin >>'{file}.out1' 2>&1 </dev/null")
103 end
104
105 var msg
106 f = new IFStream.open("{file}.out1")
107 var n2
108 n2 = new HTMLTag("system-err")
109 tc.add n2
110 msg = f.read_all
111 f.close
112
113 n2 = new HTMLTag("system-out")
114 tc.add n2
115 for text in block do n2.append(text)
116
117
118 if res != 0 then
119 var ne = new HTMLTag("failure")
120 ne.attr("message", msg)
121 tc.add ne
122 toolcontext.warning(ndoc.location, "FAILURE: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
123 else if res2 != 0 then
124 var ne = new HTMLTag("error")
125 ne.attr("message", msg)
126 tc.add ne
127 toolcontext.warning(ndoc.location, "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
128 end
129 toolcontext.check_errors
130
131 testsuite.add(tc)
132 end
133 end
134
135 class SearchAssertVisitor
136 super Visitor
137 var foundit = false
138 redef fun visit(node)
139 do
140 if foundit then
141 return
142 else if node isa AAssertExpr then
143 foundit = true
144 return
145 else
146 node.visit_all(self)
147 end
148 end
149 end
150
151 redef class ModelBuilder
152 fun test_markdown(mmodule: MModule): HTMLTag
153 do
154 toolcontext.info("nitunit: {mmodule}", 2)
155 var o = mmodule
156 var d = o.public_owner
157 while d != null do
158 o = d
159 d = o.public_owner
160 end
161
162 var ts = new HTMLTag("testsuite")
163 ts.attr("package", mmodule.full_name)
164
165 var prefix = toolcontext.opt_dir.value
166 if prefix == null then prefix = ".nitunit"
167 prefix = prefix.join_path(mmodule.to_s)
168 var d2m = new NitUnitExecutor(toolcontext, prefix, o.name, ts)
169
170 var tc
171
172 if mmodule2nmodule.has_key(mmodule) then
173 var nmodule = mmodule2nmodule[mmodule]
174 do
175 var nmoduledecl = nmodule.n_moduledecl
176 if nmoduledecl == null then break label x
177 var ndoc = nmoduledecl.n_doc
178 if ndoc == null then break label x
179 tc = new HTMLTag("testcase")
180 # NOTE: jenkins expects a '.' in the classname attr
181 tc.attr("classname", mmodule.full_name + ".<module>")
182 tc.attr("name", "<module>")
183 d2m.extract(ndoc, tc)
184 end label x
185 for nclassdef in nmodule.n_classdefs do
186 var mclassdef = nclassdef.mclassdef.as(not null)
187 if nclassdef isa AStdClassdef then
188 var ndoc = nclassdef.n_doc
189 if ndoc != null then
190 tc = new HTMLTag("testcase")
191 tc.attr("classname", mmodule.full_name + "." + mclassdef.mclass.full_name)
192 tc.attr("name", "<class>")
193 d2m.extract(ndoc, tc)
194 end
195 end
196 for npropdef in nclassdef.n_propdefs do
197 var mpropdef = npropdef.mpropdef.as(not null)
198 var ndoc = npropdef.n_doc
199 if ndoc != null then
200 tc = new HTMLTag("testcase")
201 tc.attr("classname", mmodule.full_name + "." + mclassdef.mclass.full_name)
202 tc.attr("name", mpropdef.mproperty.full_name)
203 d2m.extract(ndoc, tc)
204 end
205 end
206 end
207 end
208
209 return ts
210 end
211 end
212
213 redef class ToolContext
214 var opt_full = new OptionBool("Process also imported modules", "--full")
215 var opt_output = new OptionString("Output name (default is 'nitunit.xml')", "-o", "--output")
216 var opt_dir = new OptionString("Working directory (default is '.nitunit')", "--dir")
217 end
218
219 var toolcontext = new ToolContext
220
221 toolcontext.option_context.add_option(toolcontext.opt_full, toolcontext.opt_output, toolcontext.opt_dir)
222
223
224 toolcontext.process_options
225 var args = toolcontext.option_context.rest
226 if args.is_empty or toolcontext.opt_help.value then
227 print "usage: nitunit [options] file.nit..."
228 toolcontext.option_context.usage
229 return
230 end
231
232 var model = new Model
233 var modelbuilder = new ModelBuilder(model, toolcontext)
234
235 var mmodules = modelbuilder.parse(args)
236 modelbuilder.run_phases
237
238 var page = new HTMLTag("testsuites")
239
240 if toolcontext.opt_full.value then mmodules = model.mmodules
241
242 for m in mmodules do
243 page.add modelbuilder.test_markdown(m)
244 end
245
246 var file = toolcontext.opt_output.value
247 if file == null then file = "nitunit.xml"
248 page.save(file)