Merge: doc: fixed some typos and other misc. corrections
[nit.git] / src / testing / testing_doc.nit
index 936f6dc..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
@@ -39,12 +39,11 @@ class NitUnitExecutor
        # 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
@@ -57,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
@@ -72,21 +73,21 @@ 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]
 
-       fun show_status
-       do
-               toolcontext.show_unit_status(name, docunits)
-       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
-               toolcontext.clear_status
+               toolcontext.clear_progress_bar
                toolcontext.show_unit(du)
                show_status
        end
@@ -99,7 +100,8 @@ class NitUnitExecutor
                end
 
                # Try to group each nitunit into a single source file to fasten the compilation
-               var simple_du = new Array[DocUnit]
+               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
@@ -110,19 +112,27 @@ class NitUnitExecutor
                        var ast = toolcontext.parse_something(du.block)
                        if ast isa AExpr then
                                simple_du.add du
+                       else
+                               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
                        if du.error != null then
                                # Nothing to execute. Conclude
-                       else if du.test_file != null then
+                       else if du.is_compiled then
                                # Already compiled. Execute it.
                                execute_simple_docunit(du)
                        else
-                               # Need to try to compile it, then execute it
+                               # A mass compilation failed
+                               # Need to try to recompile it, then execute it
                                test_single_docunit(du)
                        end
                        mark_done(du)
@@ -137,25 +147,29 @@ class NitUnitExecutor
                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")
@@ -174,7 +188,7 @@ class NitUnitExecutor
 
                if res != 0 then
                        # Compilation error.
-                       # They will be executed independently
+                       # They should be generated and compiled independently
                        return
                end
 
@@ -185,6 +199,7 @@ class NitUnitExecutor
                        i += 1
                        du.test_file = file
                        du.test_arg = i
+                       du.is_compiled = true
                end
        end
 
@@ -192,9 +207,11 @@ class NitUnitExecutor
        fun execute_simple_docunit(du: DocUnit)
        do
                var file = du.test_file.as(not null)
-               var i = du.test_arg.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
 
                var content = "{file}.out1".to_path.read_all
@@ -206,39 +223,45 @@ class NitUnitExecutor
                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
+
+               du.is_compiled = true
+               execute_simple_docunit(du)
        end
 
        # Create and fill the header of a unit file `file`.
@@ -251,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
@@ -258,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
@@ -301,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
@@ -360,8 +431,7 @@ 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
 
@@ -379,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.
@@ -398,6 +476,9 @@ class DocUnit
        # 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
 
@@ -445,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
 
@@ -519,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
@@ -529,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
@@ -561,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
 
@@ -572,7 +657,7 @@ redef class ModelBuilder
        fun test_mdoc(mdoc: MDoc): HTMLTag
        do
                var ts = new HTMLTag("testsuite")
-               var file = mdoc.location.file.filename
+               var file = mdoc.location.file.as(not null).filename
 
                toolcontext.info("nitunit: doc-unit file {file}", 2)