X-Git-Url: http://nitlanguage.org diff --git a/src/testing/testing_suite.nit b/src/testing/testing_suite.nit index 37f5c74..13cf20f 100644 --- a/src/testing/testing_suite.nit +++ b/src/testing/testing_suite.nit @@ -17,13 +17,14 @@ module testing_suite import testing_base import html -private import annotation +private import parse_annotations +private import realtime 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,49 +33,54 @@ 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 - if before_module != null then + for mmethod in mmodule.before_all do toolcontext.modelbuilder.total_tests += 1 - suite.before_module = new TestCase(suite, before_module, toolcontext) + suite.before_all.add new TestCase(suite, mmethod, toolcontext) end # generate all test cases for mclassdef in mmodule.mclassdefs do 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 + + var test_class = new TestClass + + # method to execute before all tests in the class + for mmethod in mclassdef.before_all do + toolcontext.modelbuilder.total_tests += 1 + test_class.before_all.add new TestCase(suite, mmethod, toolcontext) + end + + var before = mclassdef.before + var after = mclassdef.after + 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 + test.before = before + test.after = after + test_class.test_cases.add test + end + + # method to execute after all tests in the class + for mmethod in mclassdef.after_all do + toolcontext.modelbuilder.total_tests += 1 + test_class.after_all.add new TestCase(suite, mmethod, toolcontext) end + + suite.test_classes.add test_class end # method to execute after all tests in the module - var after_module = mmodule.after_test - if after_module != null then + for mmethod in mmodule.after_all do toolcontext.modelbuilder.total_tests += 1 - suite.after_module = new TestCase(suite, after_module, toolcontext) + suite.after_all.add new TestCase(suite, mmethod, toolcontext) end suite.run return suite @@ -125,40 +131,152 @@ class TestSuite var toolcontext: ToolContext # List of `TestCase` to be executed in this suite. - var test_cases = new Array[TestCase] + var test_classes = new Array[TestClass] - # Add a `TestCase` to the suite. - fun add_test(case: TestCase) do test_cases.add case + # Tests to be executed before the whole test suite. + var before_all = new Array[TestCase] - # Test to be executed before the whole test suite. - var before_module: nullable TestCase = null + # Tests to be executed after the whole test suite. + var after_all = new Array[TestCase] - # Test to be executed after the whole test suite. - var after_module: nullable TestCase = null + # Display test suite status in std-out. + fun show_status do + var test_cases = new Array[TestCase] + for test_class in test_classes do + test_cases.add_all test_class.before_all + test_cases.add_all test_class.test_cases + test_cases.add_all test_class.after_all + end + test_cases.add_all before_all + test_cases.add_all after_all + toolcontext.show_unit_status("Test-suite of module " + mmodule.full_name, test_cases) + end # Execute the test suite fun run do + set_env + show_status if not toolcontext.test_dir.file_exists then toolcontext.test_dir.mkdir end write_to_nit compile + if failure != null then + for test_class in test_classes do + for case in test_class.test_cases do + case.fail "Compilation Error" + case.raw_output = failure + toolcontext.clear_progress_bar + toolcontext.show_unit(case) + end + end + show_status + print "" + return + end toolcontext.info("Execute test-suite {mmodule.name}", 1) - var before_module = self.before_module - if not before_module == null then before_module.run - for case in test_cases do case.run - var after_module = self.after_module - if not after_module == null then after_module.run + + for before_module in before_all do + before_module.run + toolcontext.clear_progress_bar + toolcontext.show_unit(before_module) + if before_module.error != null then + for test_class in test_classes do + for case in test_class.before_all do + case.fail "Nitunit Error: before module test failed" + toolcontext.clear_progress_bar + toolcontext.show_unit(case) + end + for case in test_class.test_cases do + case.fail "Nitunit Error: before module test failed" + toolcontext.clear_progress_bar + toolcontext.show_unit(case) + end + for case in test_class.after_all do + case.fail "Nitunit Error: before module test failed" + toolcontext.clear_progress_bar + toolcontext.show_unit(case) + end + end + for after_module in after_all do + after_module.fail "Nitunit Error: before module test failed" + toolcontext.clear_progress_bar + toolcontext.show_unit(after_module) + end + show_status + print "" + return + end + end + + for test_class in test_classes do + for case in test_class.before_all do + case.run + toolcontext.clear_progress_bar + toolcontext.show_unit(case) + if case.error != null then + for scase in test_class.test_cases do + scase.fail "Nitunit Error: before class test failed" + toolcontext.clear_progress_bar + toolcontext.show_unit(scase) + end + for scase in test_class.after_all do + scase.fail "Nitunit Error: before class test failed" + toolcontext.clear_progress_bar + toolcontext.show_unit(scase) + end + show_status + print "" + return + end + end + for case in test_class.test_cases do + case.run + toolcontext.clear_progress_bar + toolcontext.show_unit(case) + show_status + end + for after_class in test_class.after_all do + after_class.run + toolcontext.clear_progress_bar + toolcontext.show_unit(after_class) + show_status + end + end + + for after_module in after_all do + after_module.run + toolcontext.clear_progress_bar + toolcontext.show_unit(after_module) + show_status + end + + show_status + print "" end # 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 "intrude import core" file.addn "import {mmodule.name}\n" file.addn "var name = args.first" - for case in test_cases do - case.write_to_nit(file) + for before_module in before_all do + before_module.write_to_nit(file) + end + for test_class in test_classes do + for case in test_class.before_all do + case.write_to_nit(file) + end + for case in test_class.test_cases do + case.write_to_nit(file) + end + for case in test_class.after_all do + case.write_to_nit(file) + end + end + for after_module in after_all do + after_module.write_to_nit(file) end file.write_to_file("{test_file}.nit") end @@ -168,12 +286,8 @@ class TestSuite fun to_xml: HTMLTag do var n = new HTMLTag("testsuite") n.attr("package", mmodule.name) - 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 + for test_class in test_classes do + for test in test_class.test_cases do n.add test.to_xml end return n end @@ -185,37 +299,55 @@ class TestSuite # Compile all `test_cases` cases in one file. fun compile do - # find nitg - var nit_dir = toolcontext.nit_dir - var nitg = nit_dir/"bin/nitg" - if not nitg.file_exists then - toolcontext.error(null, "Cannot find nitg. Set envvar NIT_DIR.") - toolcontext.check_errors - end + # find nitc + var nitc = toolcontext.find_nitc # compile test suite var file = test_file - var include_dir = mmodule.location.file.filename.dirname - var cmd = "{nitg} --no-color '{file}.nit' -I {include_dir} -o '{file}.bin' > '{file}.out' 2>&1 '{res_name}.out1' 2>&1 '{res_name}.out1' 2>&1 '{res_name}.diff' 2>&1 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 - return tc + is_done = true end -end - -redef class MMethodDef - # TODO use annotations? - - # Is the method a test_method? - # 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" + # Make the test case fail without testing it + # + # Useful when the compilation or the before_test failed. + fun fail(message: String) do + is_done = true + error = message + toolcontext.modelbuilder.failed_tests += 1 + end - # Is the method a "before_module"? - private fun is_before_module: Bool do return mproperty.is_toplevel and name == "before_module" + redef fun xml_classname do + var a = test_method.full_name.split("$") + return "nitunit.{a[0]}.{a[1]}" + end - # Is the method a "after_module"? - private fun is_after_module: Bool do return mproperty.is_toplevel and name == "after_module" + redef fun xml_name do + var a = test_method.full_name.split("$") + return a[2] + end end redef class MClassDef - # Is the class a TestClass? - # i.e. begins with "Test" - private fun is_test: Bool do - for sup in in_hierarchy.greaters do - if sup.name == "TestSuite" then return true + # Methods tagged with `before` in this class definition + private fun before: Array[MMethodDef] do + var res = new ArraySet[MMethodDef] + for mpropdef in mpropdefs do + if mpropdef isa MMethodDef and mpropdef.is_before then + res.add mpropdef + end end - return false + var in_hierarchy = self.in_hierarchy + if in_hierarchy == null then return res.to_a + for mclassdef in in_hierarchy.direct_greaters do + res.add_all mclassdef.before + end + var lin = res.to_a + mmodule.linearize_mpropdefs(lin) + return lin + end + + # Methods tagged with `before_all` in this class definition + private fun before_all: Array[MMethodDef] do + var res = new ArraySet[MMethodDef] + for mpropdef in mpropdefs do + if mpropdef isa MMethodDef and mpropdef.is_before_all then + res.add mpropdef + end + end + var in_hierarchy = self.in_hierarchy + if in_hierarchy == null then return res.to_a + for mclassdef in in_hierarchy.direct_greaters do + res.add_all mclassdef.before_all + end + var lin = res.to_a + mmodule.linearize_mpropdefs(lin) + return lin end - # "before_test" method for this classdef. - private fun before_test: nullable MMethodDef do + # Methods tagged with `after` in this class definition + private fun after: Array[MMethodDef] do + var res = new ArraySet[MMethodDef] for mpropdef in mpropdefs do - if mpropdef isa MMethodDef and mpropdef.is_before then return mpropdef + if mpropdef isa MMethodDef and mpropdef.is_after then + res.add mpropdef + end + end + var in_hierarchy = self.in_hierarchy + if in_hierarchy == null then return res.to_a + for mclassdef in in_hierarchy.direct_greaters do + res.add_all mclassdef.after end - return null + var lin = res.to_a + mmodule.linearize_mpropdefs(lin) + return lin.reversed end - # "after_test" method for this classdef. - private fun after_test: nullable MMethodDef do + # Methods tagged with `after_all` in this class definition + private fun after_all: Array[MMethodDef] do + var res = new ArraySet[MMethodDef] for mpropdef in mpropdefs do - if mpropdef isa MMethodDef and mpropdef.is_after then return mpropdef + if mpropdef isa MMethodDef and mpropdef.is_after_all then + res.add mpropdef + end + end + var in_hierarchy = self.in_hierarchy + if in_hierarchy == null then return res.to_a + for mclassdef in in_hierarchy.direct_greaters do + res.add_all mclassdef.after_all end - return null + var lin = res.to_a + mmodule.linearize_mpropdefs(lin) + return lin.reversed end end redef class MModule - # "before_module" method for this module. - private fun before_test: nullable MMethodDef do - for mclassdef in mclassdefs do - if not mclassdef.name == "Object" then continue - for mpropdef in mclassdef.mpropdefs do - if mpropdef isa MMethodDef and mpropdef.is_before_module then return mpropdef + # Methods tagged with `before_all` at the module level (in `Sys`) + private fun before_all: Array[MMethodDef] do + var res = new Array[MMethodDef] + for mmodule in in_importation.greaters do + for mclassdef in mmodule.mclassdefs do + if mclassdef.name != "Sys" then continue + for mpropdef in mclassdef.mpropdefs do + if not mpropdef isa MMethodDef or not mpropdef.is_before_all then continue + res.add mpropdef + end end end - return null + var lin = res.to_a + linearize_mpropdefs(lin) + return lin end - # "after_module" method for this module. - private fun after_test: nullable MMethodDef do - for mclassdef in mclassdefs do - if not mclassdef.name == "Object" then continue - for mpropdef in mclassdef.mpropdefs do - if mpropdef isa MMethodDef and mpropdef.is_after_module then return mpropdef + # Methods tagged with `after_all` at the module level (in `Sys`) + private fun after_all: Array[MMethodDef] do + var res = new Array[MMethodDef] + for mmodule in in_importation.greaters do + for mclassdef in mmodule.mclassdefs do + if mclassdef.name != "Sys" then continue + for mpropdef in mclassdef.mpropdefs do + if not mpropdef isa MMethodDef or not mpropdef.is_after_all then continue + res.add mpropdef + end end end - return null + var lin = res.to_a + linearize_mpropdefs(lin) + return lin.reversed end end @@ -380,28 +587,14 @@ redef class ModelBuilder # 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", 2) - 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 not mmodule.is_test 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", 2) - return ts - end + var res = tester.test_module_unit(mmodule) return res.to_xml end end