import testing_base
import markdown
import html
+import realtime
# Extractor, Executor and Reporter for the tests in a module
class NitUnitExecutor
# The XML node associated to the module
var testsuite: HTMLTag
- # The current test-case xml element
- var tc: HTMLTag is noautoinit
-
- # All failures from a same `ADoc`
- var failures = new Array[String]
+ # The name of the suite
+ var name: String
# Markdown processor used to parse markdown comments and extract code.
var mdproc = new MarkdownProcessor
init do
- mdproc.emitter.decorator = new NitunitDecorator(self)
+ mdproc.decorator = new NitunitDecorator(self)
end
# The associated documentation object
# Is used because a new code-block might just be added to it.
var last_docunit: nullable DocUnit = null
+ var xml_classname: String is noautoinit
+
+ var xml_name: String is noautoinit
+
# The entry point for a new `ndoc` node
# Fill `docunits` with new discovered unit of tests.
- #
- # `tc` (testcase) is the pre-filled XML node
- fun extract(mdoc: MDoc, tc: HTMLTag)
+ fun extract(mdoc: MDoc, xml_classname, xml_name: String)
do
last_docunit = null
- failures.clear
- self.tc = tc
+ self.xml_classname = xml_classname
+ self.xml_name = xml_name
self.mdoc = mdoc
# Populate `blocks` from the markdown decorator
mdproc.process(mdoc.content.join("\n"))
-
- toolcontext.check_errors
-
- if not failures.is_empty then
- for msg in failures do
- var ne = new HTMLTag("failure")
- ne.attr("message", msg)
- tc.add ne
- toolcontext.modelbuilder.unit_entities += 1
- toolcontext.modelbuilder.failed_entities += 1
- end
- if last_docunit == null then testsuite.add(tc)
- end
end
# All extracted docunits
var docunits = new Array[DocUnit]
+ fun show_status
+ do
+ toolcontext.show_unit_status(name, docunits)
+ end
+
+ fun mark_done(du: DocUnit)
+ do
+ du.is_done = true
+ toolcontext.clear_progress_bar
+ toolcontext.show_unit(du)
+ show_status
+ end
+
# Execute all the docunits
fun run_tests
do
+ if docunits.is_empty then
+ return
+ end
+
+ # Try to group each nitunit into a single source file to fasten the compilation
var simple_du = new Array[DocUnit]
+ show_status
for du in docunits do
+ # Skip existing errors
+ if du.error != null then
+ continue
+ end
+
var ast = toolcontext.parse_something(du.block)
if ast isa AExpr then
simple_du.add du
+ end
+ end
+ test_simple_docunits(simple_du)
+
+ # Now test them in order
+ for du in docunits do
+ if du.error != null then
+ # Nothing to execute. Conclude
+ else if du.test_file != null then
+ # Already compiled. Execute it.
+ execute_simple_docunit(du)
else
+ # Need to try to compile it, then execute it
test_single_docunit(du)
end
+ mark_done(du)
end
- test_simple_docunits(simple_du)
+ # Final status
+ show_status
+ print ""
+
+ for du in docunits do
+ testsuite.add du.to_xml
+ end
end
# Executes multiples doc-units in a shared program.
i += 1
f.write("fun run_{i} do\n")
- f.write("# {du.testcase.attrs["name"]}\n")
+ f.write("# {du.full_name}\n")
f.write(du.block)
f.write("end\n")
end
if res != 0 then
# Compilation error.
- # Fall-back to individual modes:
- for du in dus do
- test_single_docunit(du)
- end
+ # They will be executed independently
return
end
+ # Compilation was a success.
+ # Store what need to be executed for each one.
i = 0
for du in dus do
- var tc = du.testcase
- toolcontext.modelbuilder.unit_entities += 1
i += 1
- toolcontext.info("Execute doc-unit {du.testcase.attrs["name"]} in {file} {i}", 1)
- var res2 = toolcontext.safe_exec("{file.to_program_name}.bin {i} >'{file}.out1' 2>&1 </dev/null")
-
- f = new FileReader.open("{file}.out1")
- var n2
- n2 = new HTMLTag("system-err")
- tc.add n2
- var content = f.read_all
- var msg = content.trunc(8192).filter_nonprintable
- n2.append(msg)
- f.close
-
- n2 = new HTMLTag("system-out")
- tc.add n2
- n2.append(du.block)
-
- if res2 != 0 then
- var ne = new HTMLTag("error")
- ne.attr("message", "Runtime error")
- tc.add ne
- toolcontext.warning(du.mdoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): Runtime error\n{msg}")
- toolcontext.modelbuilder.failed_entities += 1
- end
- toolcontext.check_errors
+ du.test_file = file
+ du.test_arg = i
+ end
+ end
- testsuite.add(tc)
+ # Execute a docunit compiled by `test_single_docunit`
+ fun execute_simple_docunit(du: DocUnit)
+ do
+ var file = du.test_file.as(not null)
+ var i = du.test_arg.as(not null)
+ toolcontext.info("Execute doc-unit {du.full_name} in {file} {i}", 1)
+ var clock = new Clock
+ var res2 = toolcontext.safe_exec("{file.to_program_name}.bin {i} >'{file}.out1' 2>&1 </dev/null")
+ if not toolcontext.opt_no_time.value then du.real_time = clock.total
+ du.was_exec = true
+
+ var content = "{file}.out1".to_path.read_all
+ du.raw_output = content
+
+ if res2 != 0 then
+ du.error = "Runtime error in {file} with argument {i}"
+ toolcontext.modelbuilder.failed_entities += 1
end
end
# Used for docunits larger than a single block of code (with modules, classes, functions etc.)
fun test_single_docunit(du: DocUnit)
do
- var tc = du.testcase
- toolcontext.modelbuilder.unit_entities += 1
-
cpt += 1
var file = "{prefix}-{cpt}.nit"
- toolcontext.info("Execute doc-unit {tc.attrs["name"]} in {file}", 1)
+ toolcontext.info("Execute doc-unit {du.full_name} in {file}", 1)
var f
f = create_unitfile(file)
var res = compile_unitfile(file)
var res2 = 0
if res == 0 then
+ var clock = new Clock
res2 = toolcontext.safe_exec("{file.to_program_name}.bin >'{file}.out1' 2>&1 </dev/null")
+ if not toolcontext.opt_no_time.value then du.real_time = clock.total
+ du.was_exec = true
end
- f = new FileReader.open("{file}.out1")
- var n2
- n2 = new HTMLTag("system-err")
- tc.add n2
- var content = f.read_all
- var msg = content.trunc(8192).filter_nonprintable
- n2.append(msg)
- f.close
-
- n2 = new HTMLTag("system-out")
- tc.add n2
- n2.append(du.block)
-
+ var content = "{file}.out1".to_path.read_all
+ du.raw_output = content
if res != 0 then
- var ne = new HTMLTag("failure")
- ne.attr("message", "Compilation Error")
- tc.add ne
- toolcontext.warning(du.mdoc.location, "failure", "FAILURE: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}):\n{msg}")
+ du.error = "Compilation error in {file}"
toolcontext.modelbuilder.failed_entities += 1
else if res2 != 0 then
- var ne = new HTMLTag("error")
- ne.attr("message", "Runtime Error")
- tc.add ne
- toolcontext.warning(du.mdoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}):\n{msg}")
+ du.error = "Runtime error in {file}"
toolcontext.modelbuilder.failed_entities += 1
end
- toolcontext.check_errors
-
- testsuite.add(tc)
end
# Create and fill the header of a unit file `file`.
f.write("# GENERATED FILE\n")
f.write("# Docunits extracted from comments\n")
if mmodule != null then
- f.write("import {mmodule.name}\n")
+ f.write("intrude import {mmodule.name}\n")
end
f.write("\n")
return f
if mmodule != null then
opts.add "-I {mmodule.filepath.dirname}"
end
- var cmd = "{nitc} --ignore-visibility --no-color '{file}' {opts.join(" ")} >'{file}.out1' 2>&1 </dev/null -o '{file}.bin'"
+ var cmd = "{nitc} --ignore-visibility --no-color -q '{file}' {opts.join(" ")} >'{file}.out1' 2>&1 </dev/null -o '{file}.bin'"
var res = toolcontext.safe_exec(cmd)
return res
end
message = "Error: Invalid Nit code."
end
- executor.toolcontext.warning(location, "invalid-block", "{message} To suppress this message, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`).")
- executor.failures.add("{location}: {message}")
+ var du = new_docunit
+ du.block += code
+ du.error_location = location
+ du.error = message
+ executor.toolcontext.modelbuilder.failed_entities += 1
return
end
# Or create a new block for modules that are more than a main part
var last_docunit = executor.last_docunit
if last_docunit == null or ast isa AModule then
- last_docunit = new DocUnit(executor.mdoc.as(not null), executor.tc, "")
- executor.docunits.add last_docunit
+ last_docunit = new_docunit
+ executor.last_docunit = last_docunit
end
# Add it to the file
last_docunit.block += code
+
+ # In order to retrieve precise positions,
+ # the real position of each line of the raw_content is stored.
+ # See `DocUnit::real_location`
+ line_offset -= loc.line_start - 1
+ for i in [loc.line_start..loc.line_end] do
+ last_docunit.lines.add i + line_offset
+ last_docunit.columns.add column_offset
+ end
+ end
+
+ # Return and register a new empty docunit
+ fun new_docunit: DocUnit
+ do
+ var mdoc = executor.mdoc
+ assert mdoc != null
+
+ var next_number = 1
+ var name = executor.xml_name
+ if executor.docunits.not_empty and executor.docunits.last.mdoc == mdoc then
+ next_number = executor.docunits.last.number + 1
+ name += "#" + next_number.to_s
+ end
+
+ var res = new DocUnit(mdoc, next_number, "", executor.xml_classname, name)
+ executor.docunits.add res
+ executor.toolcontext.modelbuilder.unit_entities += 1
+ return res
end
end
# A docunit is extracted from the code-blocks of mdocs.
# Each mdoc can contains more than one docunit, and a single docunit can be made of more that a single code-block.
class DocUnit
+ super UnitTest
+
# The doc that contains self
var mdoc: MDoc
- # The XML node that contains the information about the execution
- var testcase: HTMLTag
+ # The numbering of self in mdoc (starting with 0)
+ var number: Int
+
+ # The generated Nit source file that contains the unit-test
+ #
+ # Note that a same generated file can be used for multiple tests.
+ # See `test_arg` that is used to distinguish them
+ var test_file: nullable String = null
+
+ # The command-line argument to use when executing the test, if any.
+ var test_arg: nullable Int = null
+
+ redef fun full_name do
+ var mentity = mdoc.original_mentity
+ if mentity != null then
+ var res = mentity.full_name
+ if number > 1 then
+ res += "#{number}"
+ end
+ return res
+ else
+ return xml_classname + "." + xml_name
+ end
+ end
# The text of the code to execute.
#
# This is the verbatim content on one, or more, code-blocks from `mdoc`
var block: String
+
+ # For each line in `block`, the associated line in the mdoc
+ #
+ # Is used to give precise locations
+ var lines = new Array[Int]
+
+ # For each line in `block`, the associated column in the mdoc
+ #
+ # Is used to give precise locations
+ var columns = new Array[Int]
+
+ # The location of the whole docunit.
+ #
+ # If `self` is made of multiple code-blocks, then the location
+ # starts at the first code-books and finish at the last one, thus includes anything between.
+ redef var location is lazy do
+ return new Location(mdoc.location.file, lines.first, lines.last+1, columns.first+1, 0)
+ end
+
+ # Compute the real location of a node on the `ast` based on `mdoc.location`
+ #
+ # The result is basically: ast_location + markdown location of the piece + mdoc.location
+ #
+ # The fun is that a single docunit can be made of various pieces of code blocks.
+ fun real_location(ast_location: Location): Location
+ do
+ var mdoc = self.mdoc
+ var res = new Location(mdoc.location.file, lines[ast_location.line_start-1],
+ lines[ast_location.line_end-1],
+ columns[ast_location.line_start-1] + ast_location.column_start,
+ columns[ast_location.line_end-1] + ast_location.column_end)
+ return res
+ end
+
+ redef fun to_xml
+ do
+ var res = super
+ res.open("system-out").append(block)
+ return res
+ end
+
+ redef var xml_classname
+ redef var xml_name
end
redef class ModelBuilder
var prefix = toolcontext.test_dir
prefix = prefix.join_path(mmodule.to_s)
- var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
-
- var tc
+ var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts, "Docunits of module {mmodule.full_name}")
do
total_entities += 1
var ndoc = nmoduledecl.n_doc
if ndoc == null then break label x
doc_entities += 1
- tc = new HTMLTag("testcase")
# NOTE: jenkins expects a '.' in the classname attr
- tc.attr("classname", "nitunit." + mmodule.full_name + ".<module>")
- tc.attr("name", "<module>")
- d2m.extract(ndoc.to_mdoc, tc)
+ d2m.extract(ndoc.to_mdoc, "nitunit." + mmodule.full_name + ".<module>", "<module>")
end label x
for nclassdef in nmodule.n_classdefs do
var mclassdef = nclassdef.mclassdef
var ndoc = nclassdef.n_doc
if ndoc != null then
doc_entities += 1
- tc = new HTMLTag("testcase")
- tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
- tc.attr("name", "<class>")
- d2m.extract(ndoc.to_mdoc, tc)
+ d2m.extract(ndoc.to_mdoc, "nitunit." + mclassdef.full_name.replace("$", "."), "<class>")
end
end
for npropdef in nclassdef.n_propdefs do
var ndoc = npropdef.n_doc
if ndoc != null then
doc_entities += 1
- tc = new HTMLTag("testcase")
- tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
- tc.attr("name", mpropdef.mproperty.full_name)
- d2m.extract(ndoc.to_mdoc, tc)
+ var a = mpropdef.full_name.split("$")
+ d2m.extract(ndoc.to_mdoc, "nitunit." + a[0] + "." + a[1], a[2])
end
end
end
var prefix = toolcontext.test_dir
prefix = prefix.join_path(mgroup.to_s)
- var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
-
- var tc
+ var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts, "Docunits of group {mgroup.full_name}")
total_entities += 1
var mdoc = mgroup.mdoc
if mdoc == null then return ts
doc_entities += 1
- tc = new HTMLTag("testcase")
# NOTE: jenkins expects a '.' in the classname attr
- tc.attr("classname", "nitunit." + mgroup.full_name)
- tc.attr("name", "<group>")
- d2m.extract(mdoc, tc)
+ d2m.extract(mdoc, "nitunit." + mgroup.mpackage.name + "." + mgroup.name + ".<group>", "<group>")
d2m.run_tests
fun test_mdoc(mdoc: MDoc): HTMLTag
do
var ts = new HTMLTag("testsuite")
- var file = mdoc.location.to_s
+ var file = mdoc.location.file.filename
toolcontext.info("nitunit: doc-unit file {file}", 2)
ts.attr("package", file)
var prefix = toolcontext.test_dir / "file"
- var d2m = new NitUnitExecutor(toolcontext, prefix, null, ts)
-
- var tc
+ var d2m = new NitUnitExecutor(toolcontext, prefix, null, ts, "Docunits of file {file}")
total_entities += 1
doc_entities += 1
- tc = new HTMLTag("testcase")
# NOTE: jenkins expects a '.' in the classname attr
- tc.attr("classname", "nitunit.<file>")
- tc.attr("name", file)
-
- d2m.extract(mdoc, tc)
+ d2m.extract(mdoc, "nitunit.<file>", file)
d2m.run_tests
return ts