private import parser_util
import testing_base
-import markdown
+import markdown2
import html
+import realtime
# Extractor, Executor and Reporter for the tests in a module
class NitUnitExecutor
- super HTMLDecorator
# Toolcontext used to parse Nit code blocks.
var toolcontext: ToolContext
# The name of the suite
var name: String
- # Markdown processor used to parse markdown comments and extract code.
- var mdproc = new MarkdownProcessor
+ # Markdown parse used to parse markdown comments and extract code
+ private var md_parser = new MdParser
- init do
- mdproc.emitter.decorator = new NitunitDecorator(self)
- end
+ # Markdown visitor used to extract markdown code blocks
+ private var md_visitor = new NitunitMdVisitor(self) is lazy
# The associated documentation object
var mdoc: nullable MDoc = null
# Is used because a new code-block might just be added to it.
var last_docunit: nullable DocUnit = null
+ # Unit class name in XML output
var xml_classname: String is noautoinit
+ # Unit name in xml output
var xml_name: String is noautoinit
# The entry point for a new `ndoc` node
self.mdoc = mdoc
# Populate `blocks` from the markdown decorator
- mdproc.process(mdoc.content.join("\n"))
+ var md_node = md_parser.parse(mdoc.content.join("\n"))
+ md_visitor.enter_visit(md_node)
end
# All extracted docunits
var docunits = new Array[DocUnit]
- fun show_status(more_message: nullable String)
- do
- toolcontext.show_unit_status(name, docunits, more_message)
- end
+ # Display current testing status
+ fun show_status do toolcontext.show_unit_status(name, docunits)
+ # Update display when a test case is done
fun mark_done(du: DocUnit)
do
du.is_done = true
- show_status(du.full_name + " " + du.status_tag)
+ toolcontext.clear_progress_bar
+ toolcontext.show_unit(du)
+ show_status
end
# Execute all the docunits
return
end
- var simple_du = new Array[DocUnit]
+ # Try to group each nitunit into a single source file to fasten the compilation
+ var simple_du = new Array[DocUnit] # du that are simple statements
+ var single_du = new Array[DocUnit] # du that are modules or include classes
show_status
for du in docunits do
# Skip existing errors
if du.error != null then
- mark_done(du)
continue
end
if ast isa AExpr then
simple_du.add du
else
- test_single_docunit(du)
+ single_du.add du
end
end
- test_simple_docunits(simple_du)
-
- show_status
- print ""
+ # Try to mass compile all the simple du as a single nit module
+ compile_simple_docunits(simple_du)
+ # Try to mass compile all the single du in a single nitc invocation with many modules
+ compile_single_docunits(single_du)
+ # If the mass compilation fail, then each one will be compiled individually
+ # Now test them in order
for du in docunits do
- print du.to_screen
+ if du.error != null then
+ # Nothing to execute. Conclude
+ else if du.is_compiled then
+ # Already compiled. Execute it.
+ execute_simple_docunit(du)
+ else
+ # A mass compilation failed
+ # Need to try to recompile it, then execute it
+ test_single_docunit(du)
+ end
+ mark_done(du)
end
+ # 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.
+ # Compiles multiples doc-units in a shared program.
# Used for docunits simple block of code (without modules, classes, functions etc.)
#
- # In case of compilation error, the method fallbacks to `test_single_docunit` to
+ # In case of success, the docunits are compiled and the caller can call `execute_simple_docunit`.
+ #
+ # In case of compilation error, the docunits are let uncompiled.
+ # The caller should fallbacks to `test_single_docunit` to
# * locate exactly the compilation problem in the problematic docunit.
# * permit the execution of the other docunits that may be correct.
- fun test_simple_docunits(dus: Array[DocUnit])
+ fun compile_simple_docunits(dus: Array[DocUnit])
do
if dus.is_empty then return
var file = "{prefix}-0.nit"
+ toolcontext.info("Compile {dus.length} simple(s) doc-unit(s) in {file}", 1)
+
var dir = file.dirname
if dir != "" then dir.mkdir
var f
f = create_unitfile(file)
var i = 0
for du in dus do
-
i += 1
f.write("fun run_{i} do\n")
f.write("# {du.full_name}\n")
if res != 0 then
# Compilation error.
- # Fall-back to individual modes:
- for du in dus do
- test_single_docunit(du)
- end
+ # They should be generated and compiled independently
return
end
+ # Compilation was a success.
+ # Store what need to be executed for each one.
i = 0
for du in dus do
i += 1
- toolcontext.info("Execute doc-unit {du.full_name} in {file} {i}", 1)
- var res2 = toolcontext.safe_exec("{file.to_program_name}.bin {i} >'{file}.out1' 2>&1 </dev/null")
- du.was_exec = true
+ du.test_file = file
+ du.test_arg = i
+ du.is_compiled = true
+ end
+ end
- var content = "{file}.out1".to_path.read_all
- du.raw_output = content
+ # 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 or else 0
+ 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
- if res2 != 0 then
- du.error = "Runtime error in {file} with argument {i}"
- toolcontext.modelbuilder.failed_entities += 1
- end
- mark_done(du)
+ 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
- # Executes a single doc-unit in its own program.
- # Used for docunits larger than a single block of code (with modules, classes, functions etc.)
- fun test_single_docunit(du: DocUnit)
+ # Produce a single unit file for the docunit `du`.
+ fun generate_single_docunit(du: DocUnit): String
do
cpt += 1
var file = "{prefix}-{cpt}.nit"
- toolcontext.info("Execute doc-unit {du.full_name} in {file}", 1)
-
var f
f = create_unitfile(file)
f.write(du.block)
f.close
+ du.test_file = file
+ return file
+ end
+
+ # Executes a single doc-unit in its own program.
+ # Used for docunits larger than a single block of code (with modules, classes, functions etc.)
+ fun test_single_docunit(du: DocUnit)
+ do
+ var file = generate_single_docunit(du)
+
+ toolcontext.info("Compile doc-unit {du.full_name} in {file}", 1)
+
if toolcontext.opt_noact.value then return
var res = compile_unitfile(file)
- var res2 = 0
- if res == 0 then
- res2 = toolcontext.safe_exec("{file.to_program_name}.bin >'{file}.out1' 2>&1 </dev/null")
- du.was_exec = true
- end
-
var content = "{file}.out1".to_path.read_all
du.raw_output = content
+ du.test_file = file
+
if res != 0 then
du.error = "Compilation error in {file}"
toolcontext.modelbuilder.failed_entities += 1
- else if res2 != 0 then
- du.error = "Runtime error in {file}"
- toolcontext.modelbuilder.failed_entities += 1
+ return
end
- mark_done(du)
+
+ du.is_compiled = true
+ execute_simple_docunit(du)
end
# Create and fill the header of a unit file `file`.
# `file` should be a valid filepath for a Nit source file.
private fun create_unitfile(file: String): Writer
do
+ var mmodule = self.mmodule
var dir = file.dirname
if dir != "" then dir.mkdir
var f
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
end
- # Compile an unit file and return the compiler return code
+ # Compile a unit file and return the compiler return code
#
# Can terminate the program if the compiler is not found
private fun compile_unitfile(file: String): Int
do
+ var mmodule = self.mmodule
+ var nitc = toolcontext.find_nitc
+ var opts = new Array[String]
+ if mmodule != null then
+ # FIXME playing this way with the include dir is not safe nor robust
+ opts.add "-I {mmodule.filepath.as(not null).dirname}"
+ end
+ 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
+
+ # Compile a unit file and return the compiler return code
+ #
+ # Can terminate the program if the compiler is not found
+ private fun compile_single_docunits(dus: Array[DocUnit]): Int
+ do
+ # Generate all unitfiles
+ var files = new Array[String]
+ for du in dus do
+ files.add generate_single_docunit(du)
+ end
+
+ if files.is_empty then return 0
+
+ toolcontext.info("Compile {dus.length} single(s) doc-unit(s) at once", 1)
+
+ # Mass compile them
var nitc = toolcontext.find_nitc
var opts = new Array[String]
if mmodule != null then
+ # FIXME playing this way with the include dir is not safe nor robust
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 '{files.join("' '")}' {opts.join(" ")} > '{prefix}.out1' 2>&1 </dev/null --dir {prefix.dirname}"
var res = toolcontext.safe_exec(cmd)
+ if res != 0 then
+ # Mass compilation failure
+ return res
+ end
+
+ # Rename each file into it expected binary name
+ for du in dus do
+ var f = du.test_file.as(not null)
+ toolcontext.safe_exec("mv '{f.strip_extension(".nit")}' '{f}.bin'")
+ du.is_compiled = true
+ end
+
return res
end
end
-private class NitunitDecorator
- super HTMLDecorator
+private class NitunitMdVisitor
+ super MdVisitor
var executor: NitUnitExecutor
- redef fun add_code(v, block) do
- var code = block.raw_content
- var meta = block.meta or else "nit"
+ redef fun visit(node) do node.accept_nitunit(self)
+
+ fun parse_code(block: MdCodeBlock) do
+ var code = block.literal
+ if code == null then return
+
+ var meta = block.info or else "nit"
# Do not try to test non-nit code.
if meta != "nit" then return
+
# Try to parse code blocks
+ var executor = self.executor
var ast = executor.toolcontext.parse_something(code)
var mdoc = executor.mdoc
# The location is computed according to the starts of the mdoc and the block
# Note, the following assumes that all the comments of the mdoc are correctly aligned.
- var loc = block.block.location
+ var loc = block.location
var line_offset = loc.line_start + mdoc.location.line_start - 2
var column_offset = loc.column_start + mdoc.location.column_start
# Hack to handle precise location in blocks
# TODO remove when markdown is more reliable
- if block isa BlockFence then
+ if block isa MdFencedCodeBlock then
# Skip the starting fence
line_offset += 1
else
end
# Return and register a new empty docunit
- fun new_docunit: DocUnit
- do
+ fun new_docunit: DocUnit do
var mdoc = executor.mdoc
assert mdoc != null
- var next_number = 0
+ 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
+ name += "#" + next_number.to_s
end
var res = new DocUnit(mdoc, next_number, "", executor.xml_classname, name)
end
end
+redef class MdNode
+ private fun accept_nitunit(v: NitunitMdVisitor) do visit_all(v)
+end
+
+redef class MdCodeBlock
+ redef fun accept_nitunit(v) do v.parse_code(self)
+end
+
# A unit-test extracted from some documentation.
#
# A docunit is extracted from the code-blocks of mdocs.
# 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
+
+ # Was `test_file` successfully compiled?
+ var is_compiled = false
+
+ # 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
- return mentity.full_name
+ var res = mentity.full_name
+ if number > 1 then
+ res += "#{number}"
+ end
+ return res
else
return xml_classname + "." + xml_name
end
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],
+
+ 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
var ndoc = nclassdef.n_doc
if ndoc != null then
doc_entities += 1
- d2m.extract(ndoc.to_mdoc, "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name, "<class>")
+ 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
- d2m.extract(ndoc.to_mdoc, "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name, mpropdef.mproperty.full_name)
+ var a = mpropdef.full_name.split("$")
+ d2m.extract(ndoc.to_mdoc, "nitunit." + a[0] + "." + a[1], a[2])
end
end
end
doc_entities += 1
# NOTE: jenkins expects a '.' in the classname attr
- d2m.extract(mdoc, "nitunit." + mgroup.full_name, "<group>")
+ 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.as(not null).filename
toolcontext.info("nitunit: doc-unit file {file}", 2)