Merge: Added contributing guidelines and link from readme
[nit.git] / src / testing / testing_base.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 # Base options for testing tools.
16 module testing_base
17
18 import modelize
19 private import parser_util
20 import html
21 import console
22
23 redef class ToolContext
24 # opt --full
25 var opt_full = new OptionBool("Process also imported modules", "--full")
26 # opt --output
27 var opt_output = new OptionString("Output name (default is 'nitunit.xml')", "-o", "--output")
28 # opt --dirr
29 var opt_dir = new OptionString("Working directory (default is '.nitunit')", "--dir")
30 # opt --no-act
31 var opt_noact = new OptionBool("Does not compile and run tests", "--no-act")
32 # opt --nitc
33 var opt_nitc = new OptionString("nitc compiler to use", "--nitc")
34
35 # Working directory for testing.
36 fun test_dir: String do
37 var dir = opt_dir.value
38 if dir == null then return "nitunit.out"
39 return dir
40 end
41
42 # Search the `nitc` compiler to use
43 #
44 # If not `nitc` is suitable, then prints an error and quit.
45 fun find_nitc: String
46 do
47 var nitc = opt_nitc.value
48 if nitc != null then
49 if not nitc.file_exists then
50 fatal_error(null, "error: cannot find `{nitc}` given by --nitc.")
51 abort
52 end
53 return nitc
54 end
55
56 nitc = "NITC".environ
57 if nitc != "" then
58 if not nitc.file_exists then
59 fatal_error(null, "error: cannot find `{nitc}` given by NITC.")
60 abort
61 end
62 return nitc
63 end
64
65 var nit_dir = nit_dir
66 nitc = nit_dir/"bin/nitc"
67 if not nitc.file_exists then
68 fatal_error(null, "Error: cannot find nitc. Set envvar NIT_DIR or NITC or use the --nitc option.")
69 abort
70 end
71 return nitc
72 end
73
74 # Execute a system command in a more safe context than `Sys::system`.
75 fun safe_exec(command: String): Int
76 do
77 info(command, 2)
78 var real_command = """
79 bash -c "
80 ulimit -f {{{ulimit_file}}} 2> /dev/null
81 ulimit -t {{{ulimit_usertime}}} 2> /dev/null
82 {{{command}}}
83 "
84 """
85 return system(real_command)
86 end
87
88 # The maximum size (in KB) of files written by a command executed trough `safe_exec`
89 #
90 # Default: 64MB
91 var ulimit_file = 65536 is writable
92
93 # The maximum amount of cpu time (in seconds) for a command executed trough `safe_exec`
94 #
95 # Default: 10 CPU minute
96 var ulimit_usertime = 600 is writable
97
98 # Show a single-line status to use as a progression.
99 #
100 # If `has_progress_bar` is true, then the output is a progress bar.
101 # The printed the line starts with `'\r'` and is not ended by a `'\n'`.
102 # So it is expected that:
103 # * no other output is printed between two calls
104 # * the last `show_unit_status` is followed by a new-line
105 #
106 # If `has_progress_bar` is false, then only the first and last state is shown
107 fun show_unit_status(name: String, tests: SequenceRead[UnitTest])
108 do
109 var esc = 27.code_point.to_s
110 var line = "\r\x1B[K==== {name} ["
111 var done = tests.length
112 var fails = 0
113 for t in tests do
114 if not t.is_done then
115 line += " "
116 done -= 1
117 else if t.error == null then
118 line += ".".green.bold
119 else
120 line += "X".red.bold
121 fails += 1
122 end
123 end
124
125 if not has_progress_bar then
126 if done == 0 then
127 print "==== {name} | tests: {tests.length}"
128 end
129 return
130 end
131
132 if done < tests.length then
133 line += "] {done}/{tests.length}"
134 else
135 line += "] tests: {tests.length} "
136 if fails == 0 then
137 line += "OK".green.bold
138 else
139 line += "KO: {fails}".red.bold
140 end
141 end
142 printn "{line}"
143 end
144
145 # Is a progress bar printed?
146 #
147 # true if color (because and non-verbose mode
148 # (because verbose mode messes up with the progress bar).
149 fun has_progress_bar: Bool
150 do
151 return not opt_no_color.value and opt_verbose.value <= 0
152 end
153
154 # Clear the line if `has_progress_bar` (no-op else)
155 fun clear_progress_bar
156 do
157 if has_progress_bar then printn "\r\x1B[K"
158 end
159
160 # Show the full description of the test-case.
161 #
162 # The output honors `--no-color`.
163 #
164 # `more message`, if any, is added after the error message.
165 fun show_unit(test: UnitTest, more_message: nullable String) do
166 print test.to_screen(more_message, not opt_no_color.value)
167 end
168 end
169
170 # A unit test is an elementary test discovered, run and reported by nitunit.
171 #
172 # This class factorizes `DocUnit` and `TestCase`.
173 abstract class UnitTest
174 # The name of the unit to show in messages
175 fun full_name: String is abstract
176
177 # The location of the unit test to show in messages.
178 fun location: Location is abstract
179
180 # Flag that indicates if the unit test was compiled/run.
181 var is_done: Bool = false is writable
182
183 # Error message occurred during test-case execution (or compilation).
184 #
185 # e.g.: `Runtime Error`
186 var error: nullable String = null is writable
187
188 # Was the test case executed at least once?
189 #
190 # This will indicate the status of the test (failture or error)
191 var was_exec = false is writable
192
193 # The raw output of the execution (or compilation)
194 #
195 # It merges the standard output and error output
196 var raw_output: nullable String = null is writable
197
198 # The location where the error occurred, if it makes sense.
199 var error_location: nullable Location = null is writable
200
201 # Additional noteworthy information when a test success.
202 var info: nullable String = null
203
204 # A colorful `[OK]` or `[KO]`.
205 fun status_tag(color: nullable Bool): String do
206 color = color or else true
207 if not is_done then
208 return "[ ]"
209 else if error != null then
210 var res = "[KO]"
211 if color then res = res.red.bold
212 return res
213 else
214 var res = "[OK]"
215 if color then res = res.green.bold
216 return res
217 end
218 end
219
220 # The full (color) description of the test-case.
221 #
222 # `more message`, if any, is added after the error message.
223 fun to_screen(more_message: nullable String, color: nullable Bool): String do
224 color = color or else true
225 var res
226 var error = self.error
227 if error != null then
228 if more_message != null then error += " " + more_message
229 var loc = error_location or else location
230 if color then
231 res = "{status_tag(color)} {full_name}\n {loc.to_s.yellow}: {error}\n{loc.colored_line("1;31")}"
232 else
233 res = "{status_tag(color)} {full_name}\n {loc}: {error}"
234 end
235 var output = self.raw_output
236 if output != null then
237 res += "\n Output\n\t{output.chomp.replace("\n", "\n\t")}\n"
238 end
239 else
240 res = "{status_tag(color)} {full_name}"
241 if more_message != null then res += more_message
242 var info = self.info
243 if info != null then
244 res += "\n {info}"
245 end
246 end
247 return res
248 end
249
250 # Return a `<testcase>` XML node in format compatible with Jenkins unit tests.
251 fun to_xml: HTMLTag do
252 var tc = new HTMLTag("testcase")
253 tc.attr("classname", xml_classname)
254 tc.attr("name", xml_name)
255 var error = self.error
256 if error != null then
257 if was_exec then
258 tc.open("error").append(error)
259 else
260 tc.open("failure").append(error)
261 end
262 end
263 var output = self.raw_output
264 if output != null then
265 tc.open("system-err").append(output.trunc(8192).filter_nonprintable)
266 end
267 return tc
268 end
269
270 # The `classname` attribute of the XML format.
271 #
272 # NOTE: jenkins expects a '.' in the classname attr
273 #
274 # See to_xml
275 fun xml_classname: String is abstract
276
277 # The `name` attribute of the XML format.
278 #
279 # See to_xml
280 fun xml_name: String is abstract
281 end
282
283 redef class String
284 # If needed, truncate `self` at `max_length` characters and append an informative `message`.
285 #
286 # ~~~
287 # assert "hello".trunc(10) == "hello"
288 # assert "hello".trunc(2) == "he[truncated. Full size is 5]"
289 # assert "hello".trunc(2, "...") == "he..."
290 # ~~~
291 fun trunc(max_length: Int, message: nullable String): String
292 do
293 if length <= max_length then return self
294 if message == null then message = "[truncated. Full size is {length}]"
295 return substring(0, max_length) + message
296 end
297
298 # Use a special notation for whitespace characters that are not `'\n'` (LFD) or `' '` (space).
299 #
300 # ~~~
301 # assert "hello".filter_nonprintable == "hello"
302 # assert "\r\n\t".filter_nonprintable == "^13\n^9"
303 # ~~~
304 fun filter_nonprintable: String
305 do
306 var buf = new Buffer
307 for c in self do
308 var cp = c.code_point
309 if cp < 32 and c != '\n' then
310 buf.append "^{cp}"
311 else
312 buf.add c
313 end
314 end
315 return buf.to_s
316 end
317 end