Property definitions

nitc $ NitUnitExecutor :: defaultinit
# Extractor, Executor and Reporter for the tests in a module
class NitUnitExecutor

	# Toolcontext used to parse Nit code blocks.
	var toolcontext: ToolContext

	# The prefix of the generated Nit source-file
	var prefix: String

	# The module to import, if any
	var mmodule: nullable MModule

	# The XML node associated to the module
	var testsuite: HTMLTag

	# The name of the suite
	var name: String

	# 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

	# used to generate distinct names
	var cpt = 0

	# The last docunit extracted from a mdoc.
	#
	# 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
	# Fill `docunits` with new discovered unit of tests.
	fun extract(mdoc: MDoc, xml_classname, xml_name: String)
	do
		last_docunit = null
		self.xml_classname = xml_classname
		self.xml_name = xml_name

		self.mdoc = mdoc

		# Populate `blocks` from the markdown decorator
		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
		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
				continue
			end

			var ast = toolcontext.parse_something(du.block)
			if ast isa AExpr then
				simple_du.add du
			else
				single_du.add du
			end
		end

		# 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.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

	# Compiles multiples doc-units in a shared program.
	# Used for docunits simple block of code (without modules, classes, functions etc.)
	#
	# 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 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")
			f.write(du.block)
			f.write("end\n")
		end
		f.write("var a = args.first.to_i\n")
		for j in [1..i] do
			f.write("if a == {j} then run_{j}\n")
		end
		f.close

		if toolcontext.opt_noact.value then return

		var res = compile_unitfile(file)

		if res != 0 then
			# Compilation error.
			# 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
			du.test_file = file
			du.test_arg = i
			du.is_compiled = true
		end
	end

	# 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

		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

	# Produce a single unit file for the docunit `du`.
	fun generate_single_docunit(du: DocUnit): String
	do
		cpt += 1
		var file = "{prefix}-{cpt}.nit"

		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 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
			return
		end

		du.is_compiled = true
		execute_simple_docunit(du)
	end

	# Create and fill the header of a unit file `file`.
	#
	# A unit file is a Nit source file generated from one
	# or more docunits that will be compiled and executed.
	#
	# The handled on the file is returned and must be completed and closed.
	#
	# `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 = new FileWriter.open(file)
		f.write("# GENERATED FILE\n")
		f.write("# Docunits extracted from comments\n")
		if mmodule != null then
			f.write("intrude import {mmodule.name}\n")
		end
		f.write("\n")
		return f
	end

	# 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 -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
src/testing/testing_doc.nit:24,1--346,3