nitunit: use the new markdown parser
[nit.git] / src / testing / testing_doc.nit
index 6a27637..46f5a4a 100644 (file)
@@ -17,12 +17,12 @@ module testing_doc
 
 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
@@ -36,12 +36,14 @@ class NitUnitExecutor
        # The XML node associated to the module
        var testsuite: HTMLTag
 
-       # Markdown processor used to parse markdown comments and extract code.
-       var mdproc = new MarkdownProcessor
+       # The name of the suite
+       var name: String
 
-       init do
-               mdproc.emitter.decorator = new NitunitDecorator(self)
-       end
+       # Markdown parse used to parse markdown comments and extract code
+       private var md_parser = new MdParser
+
+       # 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
@@ -54,8 +56,10 @@ class NitUnitExecutor
        # 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
@@ -69,25 +73,39 @@ class NitUnitExecutor
                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]
 
+       # 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
+               toolcontext.clear_progress_bar
+               toolcontext.show_unit(du)
+               show_status
        end
 
        # Execute all the docunits
        fun run_tests
        do
-               var simple_du = new Array[DocUnit]
+               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] # 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
 
@@ -95,40 +113,63 @@ class NitUnitExecutor
                        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)
+               # 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")
@@ -147,65 +188,80 @@ class NitUnitExecutor
 
                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`.
@@ -218,6 +274,7 @@ class NitUnitExecutor
        # `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
@@ -225,39 +282,86 @@ class NitUnitExecutor
                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
@@ -268,12 +372,12 @@ private class NitunitDecorator
 
                # 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
@@ -327,16 +431,15 @@ private class NitunitDecorator
        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)
@@ -346,6 +449,14 @@ private class NitunitDecorator
        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.
@@ -359,10 +470,26 @@ class DocUnit
        # 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
@@ -399,10 +526,13 @@ class DocUnit
        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
 
@@ -453,7 +583,7 @@ redef class ModelBuilder
 
                var prefix = toolcontext.test_dir
                prefix = prefix.join_path(mmodule.to_s)
-               var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
+               var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts, "Docunits of module {mmodule.full_name}")
 
                do
                        total_entities += 1
@@ -473,7 +603,7 @@ redef class ModelBuilder
                                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
@@ -483,7 +613,8 @@ redef class ModelBuilder
                                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
@@ -507,7 +638,7 @@ redef class ModelBuilder
 
                var prefix = toolcontext.test_dir
                prefix = prefix.join_path(mgroup.to_s)
-               var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
+               var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts, "Docunits of group {mgroup.full_name}")
 
                total_entities += 1
                var mdoc = mgroup.mdoc
@@ -515,7 +646,7 @@ redef class ModelBuilder
 
                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
 
@@ -526,14 +657,14 @@ redef class ModelBuilder
        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)
 
                ts.attr("package", file)
 
                var prefix = toolcontext.test_dir / "file"
-               var d2m = new NitUnitExecutor(toolcontext, prefix, null, ts)
+               var d2m = new NitUnitExecutor(toolcontext, prefix, null, ts, "Docunits of file {file}")
 
                total_entities += 1
                doc_entities += 1