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 # Base options for testing tools.
19 private import parser_util
23 redef class ToolContext
25 var opt_full
= new OptionBool("Process also imported modules", "--full")
27 var opt_output
= new OptionString("Output name (default is 'nitunit.xml')", "-o", "--output")
29 var opt_dir
= new OptionString("Working directory (default is '.nitunit')", "--dir")
31 var opt_noact
= new OptionBool("Does not compile and run tests", "--no-act")
33 var opt_nitc
= new OptionString("nitc compiler to use", "--nitc")
35 var opt_no_time
= new OptionBool("Disable time information in XML", "--no-time")
37 # Working directory for testing.
38 fun test_dir
: String do
39 var dir
= opt_dir
.value
40 if dir
== null then return "nitunit.out"
44 # Search the `nitc` compiler to use
46 # If not `nitc` is suitable, then prints an error and quit.
49 var nitc
= opt_nitc
.value
51 if not nitc
.file_exists
then
52 fatal_error
(null, "error: cannot find `{nitc}` given by --nitc.")
60 if not nitc
.file_exists
then
61 fatal_error
(null, "error: cannot find `{nitc}` given by NITC.")
67 var nit_dir
= nit_dir
or else "."
68 nitc
= nit_dir
/ "bin/nitc"
69 if not nitc
.file_exists
then
70 fatal_error
(null, "Error: cannot find nitc. Set envvar NIT_DIR or NITC or use the --nitc option.")
76 # Execute a system command in a more safe context than `Sys::system`.
77 fun safe_exec
(command
: String): Int
80 var real_command
= """
82 ulimit -f {{{ulimit_file}}} 2> /dev/null
83 ulimit -t {{{ulimit_usertime}}} 2> /dev/null
87 return system
(real_command
)
90 # The maximum size (in KB) of files written by a command executed trough `safe_exec`
93 var ulimit_file
= 65536 is writable
95 # The maximum amount of cpu time (in seconds) for a command executed trough `safe_exec`
97 # Default: 10 CPU minute
98 var ulimit_usertime
= 600 is writable
100 # Show a single-line status to use as a progression.
102 # If `has_progress_bar` is true, then the output is a progress bar.
103 # The printed the line starts with `'\r'` and is not ended by a `'\n'`.
104 # So it is expected that:
105 # * no other output is printed between two calls
106 # * the last `show_unit_status` is followed by a new-line
108 # If `has_progress_bar` is false, then only the first and last state is shown
109 fun show_unit_status
(name
: String, tests
: SequenceRead[UnitTest])
111 var line
= "\r\x1B[K==== {name} ["
112 var done
= tests
.length
115 if not t
.is_done
then
118 else if t
.error
== null then
119 line
+= ".".green
.bold
126 if not has_progress_bar
then
128 print
"==== {name} | tests: {tests.length}"
133 if done
< tests
.length
then
134 line
+= "] {done}/{tests.length}"
136 line
+= "] tests: {tests.length} "
138 line
+= "OK".green
.bold
140 line
+= "KO: {fails}".red
.bold
146 # Is a progress bar printed?
148 # true if color (because and non-verbose mode
149 # (because verbose mode messes up with the progress bar).
150 fun has_progress_bar
: Bool
152 return not opt_no_color
.value
and opt_verbose
.value
<= 0
155 # Clear the line if `has_progress_bar` (no-op else)
156 fun clear_progress_bar
158 if has_progress_bar
then printn
"\r\x1B[K"
161 # Show the full description of the test-case.
163 # The output honors `--no-color`.
165 # `more message`, if any, is added after the error message.
166 fun show_unit
(test
: UnitTest, more_message
: nullable String) do
167 print test
.to_screen
(more_message
, not opt_no_color
.value
)
170 # Set the `NIT_TESTING_PATH` environment variable with `path`.
172 # If `path == null` then `NIT_TESTING_PATH` is set with the empty string.
173 fun set_testing_path
(path
: nullable String) do
174 "NIT_TESTING_PATH".setenv
(path
or else "")
178 # A unit test is an elementary test discovered, run and reported by nitunit.
180 # This class factorizes `DocUnit` and `TestCase`.
181 abstract class UnitTest
182 # The name of the unit to show in messages
183 fun full_name
: String is abstract
185 # The location of the unit test to show in messages.
186 fun location
: Location is abstract
188 # Flag that indicates if the unit test was compiled/run.
189 var is_done
: Bool = false is writable
191 # Error message occurred during test-case execution (or compilation).
193 # e.g.: `Runtime Error`
194 var error
: nullable String = null is writable
196 # Was the test case executed at least once?
198 # This will indicate the status of the test (failture or error)
199 var was_exec
= false is writable
201 # The raw output of the execution (or compilation)
203 # It merges the standard output and error output
204 var raw_output
: nullable String = null is writable
206 # The location where the error occurred, if it makes sense.
207 var error_location
: nullable Location = null is writable
209 # Additional noteworthy information when a test success.
210 var info
: nullable String = null
212 # Time for the execution, in seconds
213 var real_time
: Float = 0.0 is writable
215 # A colorful `[OK]` or `[KO]`.
216 fun status_tag
(color
: nullable Bool): String do
217 color
= color
or else true
220 else if error
!= null then
222 if color
then res
= res
.red
.bold
226 if color
then res
= res
.green
.bold
231 # The full (color) description of the test-case.
233 # `more message`, if any, is added after the error message.
234 fun to_screen
(more_message
: nullable String, color
: nullable Bool): String do
235 color
= color
or else true
237 var error
= self.error
238 if error
!= null then
239 if more_message
!= null then error
+= " " + more_message
240 var loc
= error_location
or else location
242 res
= "{status_tag(color)} {full_name}\n {loc.to_s.yellow}: {error}\n{loc.colored_line("1;31")}"
244 res
= "{status_tag(color)} {full_name}\n {loc}: {error}"
246 var output
= self.raw_output
247 if output
!= null then
248 res
+= "\n Output\n\t{output.chomp.replace("\n", "\n\t")}\n"
251 res
= "{status_tag(color)} {full_name}"
252 if more_message
!= null then res
+= more_message
261 # Return a `<testcase>` XML node in format compatible with Jenkins unit tests.
262 fun to_xml
: HTMLTag do
263 var tc
= new HTMLTag("testcase")
264 tc
.attr
("classname", xml_classname
)
265 tc
.attr
("name", xml_name
)
266 tc
.attr
("time", real_time
.to_s
)
268 var output
= self.raw_output
269 if output
!= null then output
= output
.trunc
(8192).filter_nonprintable
270 var error
= self.error
271 if error
!= null then
274 node
= tc
.open
("error").attr
("message", error
)
276 node
= tc
.open
("failure").attr
("message", error
)
278 if output
!= null then
281 else if output
!= null then
282 tc
.open
("system-err").append
(output
)
287 # The `classname` attribute of the XML format.
289 # NOTE: jenkins expects a '.' in the classname attr
292 fun xml_classname
: String is abstract
294 # The `name` attribute of the XML format.
297 fun xml_name
: String is abstract
301 # If needed, truncate `self` at `max_length` characters and append an informative `message`.
304 # assert "hello".trunc(10) == "hello"
305 # assert "hello".trunc(2) == "he[truncated. Full size is 5]"
306 # assert "hello".trunc(2, "...") == "he..."
308 fun trunc
(max_length
: Int, message
: nullable String): String
310 if length
<= max_length
then return self
311 if message
== null then message
= "[truncated. Full size is {length}]"
312 return substring
(0, max_length
) + message
315 # Use a special notation for whitespace characters that are not `'\n'` (LFD) or `' '` (space).
318 # assert "hello".filter_nonprintable == "hello"
319 # assert "\r\n\t".filter_nonprintable == "^13\n^9"
321 fun filter_nonprintable
: String
325 var cp
= c
.code_point
326 if cp
< 32 and c
!= '\n' then