Merge: Added contributing guidelines and link from readme
[nit.git] / src / testing / testing_suite.nit
index 25b1fc8..f102c6c 100644 (file)
@@ -20,10 +20,10 @@ import html
 private import annotation
 
 redef class ToolContext
-       # -- target-file
-       var opt_file = new OptionString("Specify test suite location.", "-t", "--target-file")
        # --pattern
-       var opt_pattern = new OptionString("Only run test case with name that match pattern. Examples: 'TestFoo', 'TestFoo*', 'TestFoo::test_foo', 'TestFoo::test_foo*', 'test_foo', 'test_foo*'", "-p", "--pattern")
+       var opt_pattern = new OptionString("Only run test case with name that match pattern", "-p", "--pattern")
+       # --autosav
+       var opt_autosav = new OptionBool("Automatically create/update .res files for black box testing", "--autosav")
 end
 
 # Used to test nitunit test files.
@@ -32,20 +32,9 @@ class NitUnitTester
        # `ModelBuilder` used to parse test files.
        var mbuilder: ModelBuilder
 
-       # Parse a file and return the contained `MModule`.
-       private fun parse_module_unit(file: String): nullable MModule do
-               var mmodule = mbuilder.parse([file]).first
-               if mbuilder.get_mmodule_annotation("test_suite", mmodule) == null then return null
-               mbuilder.run_phases
-               return mmodule
-       end
-
-       # Compile and execute the test suite for a NitUnit `file`.
-       fun test_module_unit(file: String): nullable TestSuite do
+       # Compile and execute `mmodule` as a test suite.
+       fun test_module_unit(mmodule: MModule): TestSuite do
                var toolcontext = mbuilder.toolcontext
-               var mmodule = parse_module_unit(file)
-               # is the module a test_suite?
-               if mmodule == null then return null
                var suite = new TestSuite(mmodule, toolcontext)
                # method to execute before all tests in the module
                var before_module = mmodule.before_test
@@ -58,15 +47,11 @@ class NitUnitTester
                        if not mclassdef.is_test then continue
                        if not suite_match_pattern(mclassdef) then continue
                        toolcontext.modelbuilder.total_classes += 1
-                       var before_test = mclassdef.before_test
-                       var after_test = mclassdef.after_test
                        for mpropdef in mclassdef.mpropdefs do
                                if not mpropdef isa MMethodDef or not mpropdef.is_test then continue
                                if not case_match_pattern(mpropdef) then continue
                                toolcontext.modelbuilder.total_tests += 1
                                var test = new TestCase(suite, mpropdef, toolcontext)
-                               test.before_test = before_test
-                               test.after_test = after_test
                                suite.add_test test
                        end
                end
@@ -136,24 +121,46 @@ class TestSuite
        # Test to be executed after the whole test suite.
        var after_module: nullable TestCase = null
 
+       fun show_status
+       do
+               toolcontext.show_unit_status("Test-suite of module " + mmodule.full_name, test_cases)
+       end
+
        # Execute the test suite
        fun run do
+               show_status
                if not toolcontext.test_dir.file_exists then
                        toolcontext.test_dir.mkdir
                end
+               write_to_nit
+               compile
                toolcontext.info("Execute test-suite {mmodule.name}", 1)
                var before_module = self.before_module
-               if not before_module == null then run_case(before_module)
-               for case in test_cases do run_case(case)
+               if not before_module == null then before_module.run
+               for case in test_cases do
+                       case.run
+                       toolcontext.clear_progress_bar
+                       toolcontext.show_unit(case)
+                       show_status
+               end
+
                var after_module = self.after_module
-               if not after_module == null then run_case(after_module)
+               if not after_module == null then after_module.run
+
+               show_status
+               print ""
        end
 
-       # Execute a test case
-       fun run_case(test_case: TestCase) do
-               test_case.write_to_nit
-               test_case.compile
-               test_case.run
+       # Write the test unit for `self` in a nit compilable file.
+       fun write_to_nit do
+               var file = new Template
+               file.addn "intrude import test_suite"
+               file.addn "import {mmodule.name}\n"
+               file.addn "var name = args.first"
+               for case in test_cases do
+                       case.write_to_nit(file)
+               end
+               file.write_to_file("{test_file}.nit")
        end
 
        # Return the test suite in XML format compatible with Jenkins.
@@ -161,13 +168,57 @@ class TestSuite
        fun to_xml: HTMLTag do
                var n = new HTMLTag("testsuite")
                n.attr("package", mmodule.name)
-               for test in test_cases do n.add test.to_xml
+               var failure = self.failure
+               if failure != null then
+                       var f = new HTMLTag("failure")
+                       f.attr("message", failure.to_s)
+                       n.add f
+               else
+                       for test in test_cases do n.add test.to_xml
+               end
                return n
        end
+
+       # Generated test file name.
+       fun test_file: String do
+               return toolcontext.test_dir / "gen_{mmodule.name.escape_to_c}"
+       end
+
+       # Compile all `test_cases` cases in one file.
+       fun compile do
+               # find nitc
+               var nitc = toolcontext.find_nitc
+               # compile test suite
+               var file = test_file
+               var module_file = mmodule.location.file
+               if module_file == null then
+                       toolcontext.error(null, "Error: cannot find module file for {mmodule.name}.")
+                       toolcontext.check_errors
+                       return
+               end
+               var include_dir = module_file.filename.dirname
+               var cmd = "{nitc} --no-color -q '{file}.nit' -I {include_dir} -o '{file}.bin' > '{file}.out' 2>&1 </dev/null"
+               var res = toolcontext.safe_exec(cmd)
+               var f = new FileReader.open("{file}.out")
+               var msg = f.read_all
+               f.close
+               # set test case result
+               var loc = mmodule.location
+               if res != 0 then
+                       failure = msg
+                       toolcontext.warning(loc, "failure", "FAILURE: {mmodule.name} (in file {file}.nit): {msg}")
+                       toolcontext.modelbuilder.failed_tests += 1
+               end
+               toolcontext.check_errors
+       end
+
+       # Error occured during test-suite compilation.
+       var failure: nullable String = null
 end
 
 # A test case is a unit test considering only a `MMethodDef`.
 class TestCase
+       super UnitTest
 
        # Test suite wich `self` belongs to.
        var test_suite: TestSuite
@@ -175,66 +226,26 @@ class TestCase
        # Test method to be compiled and tested.
        var test_method: MMethodDef
 
-       # `ToolContext` to use to display messages and find `nitg` bin.
-       var toolcontext: ToolContext
-
-       # `MMethodDef` to call before the test case.
-       var before_test: nullable MMethodDef = null
+       redef fun full_name do return test_method.full_name
 
-       # `MMethodDef` to call after the test case.
-       var after_test: nullable MMethodDef = null
+       redef fun location do return test_method.location
 
-       # Generated test file name.
-       fun test_file: String do
-               var dir = toolcontext.test_dir
-               var mod = test_method.mclassdef.mmodule.name
-               var cls = test_method.mclassdef.name
-               var name = test_method.name
-               return "{dir}/{mod}_{cls}_{name}"
-       end
+       # `ToolContext` to use to display messages and find `nitc` bin.
+       var toolcontext: ToolContext
 
-       # Generate the test unit in a nit file.
-       fun write_to_nit do
+       # Generate the test unit for `self` in `file`.
+       fun write_to_nit(file: Template) do
                var name = test_method.name
-               var file = new Template
-               file.addn "intrude import test_suite"
-               file.addn "import {test_method.mclassdef.mmodule.name}\n"
+               file.addn "if name == \"{name}\" then"
                if test_method.mproperty.is_toplevel then
-                       file.addn name
+                       file.addn "\t{name}"
                else
-                       file.addn "var subject = new {test_method.mclassdef.name}.nitunit"
-                       if before_test != null then file.addn "subject.{before_test.name}"
-                       file.addn "subject.{name}"
-                       if after_test != null then file.addn "subject.{after_test.name}"
+                       file.addn "\tvar subject = new {test_method.mclassdef.name}.nitunit"
+                       file.addn "\tsubject.before_test"
+                       file.addn "\tsubject.{name}"
+                       file.addn "\tsubject.after_test"
                end
-               file.write_to_file("{test_file}.nit")
-       end
-
-       # Compile all test cases in once.
-       fun compile do
-               # find nitg
-               var nit_dir = toolcontext.nit_dir
-               var nitg = "{nit_dir or else ""}/bin/nitg"
-               if nit_dir == null or not nitg.file_exists then
-                       toolcontext.error(null, "Cannot find nitg. Set envvar NIT_DIR.")
-                       toolcontext.check_errors
-               end
-               # compile test suite
-               var file = test_file
-               var include_dir = test_method.mclassdef.mmodule.location.file.filename.dirname
-               var cmd = "{nitg} --no-color '{file}.nit' -I {include_dir} -o '{file}.bin' > '{file}.out' 2>&1 </dev/null"
-               var res = sys.system(cmd)
-               var f = new IFStream.open("{file}.out")
-               var msg = f.read_all
-               f.close
-               # set test case result
-               var loc = test_method.location
-               if res != 0 then
-                       failure = msg
-                       toolcontext.warning(loc, "failure", "FAILURE: {test_method.name} (in file {file}.nit): {msg}")
-                       toolcontext.modelbuilder.failed_tests += 1
-               end
-               toolcontext.check_errors
+               file.addn "end"
        end
 
        # Execute the test case.
@@ -243,54 +254,64 @@ class TestCase
                was_exec = true
                if toolcontext.opt_noact.value then return
                # execute
-               var file = test_file
-               var res = sys.system("./{file}.bin > '{file}.out1' 2>&1 </dev/null")
-               var f = new IFStream.open("{file}.out1")
-               var msg = f.read_all
-               f.close
+               var method_name = test_method.name
+               var test_file = test_suite.test_file
+               var res_name = "{test_file}_{method_name.escape_to_c}"
+               var res = toolcontext.safe_exec("{test_file}.bin {method_name} > '{res_name}.out1' 2>&1 </dev/null")
+               var raw_output = "{res_name}.out1".to_path.read_all
+               self.raw_output = raw_output
                # set test case result
-               var loc = test_method.location
                if res != 0 then
-                       error = msg
-                       toolcontext.warning(loc, "failure", "ERROR: {test_method.name} (in file {file}.nit): {msg}")
+                       error = "Runtime Error in file {test_file}.nit"
                        toolcontext.modelbuilder.failed_tests += 1
+               else
+                       # no error, check with res file, if any.
+                       var mmodule = test_method.mclassdef.mmodule
+                       var file = mmodule.filepath
+                       if file != null then
+                               var tries = [ file.dirname / mmodule.name + ".sav" / test_method.name + ".res",
+                                       file.dirname / "sav" / test_method.name + ".res" ,
+                                       file.dirname / test_method.name + ".res" ]
+                               var savs = [ for t in tries do if t.file_exists then t ]
+                               if savs.length == 1 then
+                                       var sav = savs.first
+                                       toolcontext.info("Diff output with {sav}", 1)
+                                       res = toolcontext.safe_exec("diff -u --label 'expected:{sav}' --label 'got:{res_name}.out1' '{sav}' '{res_name}.out1' > '{res_name}.diff' 2>&1 </dev/null")
+                                       if res == 0 then
+                                               # OK
+                                       else if toolcontext.opt_autosav.value then
+                                               raw_output.write_to_file(sav)
+                                               info = "Expected output updated: {sav} (--autoupdate)"
+                                       else
+                                               self.raw_output = "Diff\n" + "{res_name}.diff".to_path.read_all
+                                               error = "Difference with expected output: diff -u {sav} {res_name}.out1"
+                                               toolcontext.modelbuilder.failed_tests += 1
+                                       end
+                               else if savs.length > 1 then
+                                       toolcontext.info("Conflicting diffs: {savs.join(", ")}", 1)
+                                       error = "Conflicting expected output: {savs.join(", ", " and ")} all exist"
+                                       toolcontext.modelbuilder.failed_tests += 1
+                               else if not raw_output.is_empty then
+                                       toolcontext.info("No diff: {tries.join(", ", " or ")} not found", 1)
+                                       if toolcontext.opt_autosav.value then
+                                               var sav = tries.first
+                                               sav.dirname.mkdir
+                                               raw_output.write_to_file(sav)
+                                               info = "Expected output saved: {sav} (--autoupdate)"
+                                       end
+                               end
+                       end
                end
-               toolcontext.check_errors
+               is_done = true
        end
 
-       # Error occured during execution.
-       var error: nullable String = null
-
-       # Error occured during compilation.
-       var failure: nullable String = null
-
-       # Was the test case executed at least one?
-       var was_exec = false
-
-       # Return the `TestCase` in XML format compatible with Jenkins.
-       fun to_xml: HTMLTag do
+       redef fun xml_classname do
                var mclassdef = test_method.mclassdef
-               var tc = new HTMLTag("testcase")
-               # NOTE: jenkins expects a '.' in the classname attr
-               tc.attr("classname", "nitunit." + mclassdef.mmodule.full_name + "." + mclassdef.mclass.full_name)
-               tc.attr("name", test_method.mproperty.full_name)
-               if was_exec then
-                       tc.add  new HTMLTag("system-err")
-                       var n = new HTMLTag("system-out")
-                       n.append "out"
-                       tc.add n
-                       if error != null then
-                               n = new HTMLTag("error")
-                               n.attr("message", error.to_s)
-                               tc.add n
-                       end
-                       if failure != null then
-                               n = new HTMLTag("failure")
-                               n.attr("message", failure.to_s)
-                               tc.add n
-                       end
-               end
-               return tc
+               return "nitunit." + mclassdef.mmodule.full_name + "." + mclassdef.mclass.full_name
+       end
+
+       redef fun xml_name do
+               return test_method.mproperty.full_name
        end
 end
 
@@ -301,12 +322,6 @@ redef class MMethodDef
        # i.e. begins with "test_"
        private fun is_test: Bool do return name.has_prefix("test_")
 
-       # Is the method a "before_test"?
-       private fun is_before: Bool do return name == "before_test"
-
-       # Is the method a "after_test"?
-       private fun is_after: Bool do return name == "after_test"
-
        # Is the method a "before_module"?
        private fun is_before_module: Bool do return mproperty.is_toplevel and name == "before_module"
 
@@ -316,29 +331,15 @@ end
 
 redef class MClassDef
        # Is the class a TestClass?
-       # i.e. begins with "Test"
+       # i.e. is a subclass of `TestSuite`
        private fun is_test: Bool do
+               var in_hierarchy = self.in_hierarchy
+               if in_hierarchy == null then return false
                for sup in in_hierarchy.greaters do
                        if sup.name == "TestSuite" then return true
                end
                return false
        end
-
-       # "before_test" method for this classdef.
-       private fun before_test: nullable MMethodDef do
-               for mpropdef in mpropdefs do
-                       if mpropdef isa MMethodDef and mpropdef.is_before then return mpropdef
-               end
-               return null
-       end
-
-       # "after_test" method for this classdef.
-       private fun after_test: nullable MMethodDef do
-               for mpropdef in mpropdefs do
-                       if mpropdef isa MMethodDef and mpropdef.is_after then return mpropdef
-               end
-               return null
-       end
 end
 
 redef class MModule
@@ -366,32 +367,23 @@ redef class MModule
 end
 
 redef class ModelBuilder
+       # Number of test classes generated.
        var total_classes = 0
+
+       # Number of tests generated.
        var total_tests = 0
+
+       # Number of failed tests.
        var failed_tests = 0
 
-       # Run NitUnit test file for mmodule (if exists).
-       fun test_unit(mmodule: MModule): HTMLTag do
-               var ts = new HTMLTag("testsuite")
-               toolcontext.info("nitunit: test-suite test_{mmodule}", 2)
-               var f = toolcontext.opt_file.value
-               var test_file = "test_{mmodule.name}.nit"
-               if f != null then
-                       test_file = f
-               else if not test_file.file_exists then
-                       var include_dir = mmodule.location.file.filename.dirname
-                       test_file = "{include_dir}/{test_file}"
-               end
-               if not test_file.file_exists then
-                       toolcontext.info("Skip test for {mmodule}, no file {test_file} found", 1)
-                       return ts
-               end
+       # Run NitUnit test suite for `mmodule` (if it is one).
+       fun test_unit(mmodule: MModule): nullable HTMLTag do
+               # is the module a test_suite?
+               if get_mmodule_annotation("test_suite", mmodule) == null then return null
+               toolcontext.info("nitunit: test-suite {mmodule}", 2)
+
                var tester = new NitUnitTester(self)
-               var res = tester.test_module_unit(test_file)
-               if res == null then
-                       toolcontext.info("Skip test for {mmodule}, no test suite found", 1)
-                       return ts
-               end
+               var res = tester.test_module_unit(mmodule)
                return res.to_xml
        end
 end