nitunit: allow test-suite skeleton generation.
authorAlexandre Terrasa <alexandre@moz-code.org>
Wed, 27 Aug 2014 06:10:11 +0000 (02:10 -0400)
committerAlexandre Terrasa <alexandre@moz-code.org>
Thu, 28 Aug 2014 04:19:45 +0000 (00:19 -0400)
Signed-off-by: Alexandre Terrasa <alexandre@moz-code.org>

src/nitunit.nit
src/testing/testing.nit
src/testing/testing_gen.nit [new file with mode: 0644]

index c1464a6..7eb90bb 100644 (file)
@@ -19,18 +19,47 @@ import testing
 
 var toolcontext = new ToolContext
 
-toolcontext.option_context.add_option(toolcontext.opt_full, toolcontext.opt_output, toolcontext.opt_dir, toolcontext.opt_noact, toolcontext.opt_pattern, toolcontext.opt_file)
+toolcontext.option_context.add_option(toolcontext.opt_full, toolcontext.opt_output, toolcontext.opt_dir, toolcontext.opt_noact, toolcontext.opt_pattern, toolcontext.opt_file, toolcontext.opt_gen_unit, toolcontext.opt_gen_force, toolcontext.opt_gen_private, toolcontext.opt_gen_show)
 toolcontext.tooldescription = "Usage: nitunit [OPTION]... <file.nit>...\nExecutes the unit tests from Nit source files."
 
 toolcontext.process_options(args)
 var args = toolcontext.option_context.rest
 
+if toolcontext.opt_gen_unit.value then
+       if toolcontext.opt_pattern.value != null then
+               print "Option --pattern cannot be used with --gen-suite"
+               exit(0)
+       end
+       if toolcontext.opt_file.value != null then
+               print "Option --target-file cannot be used with --gen-suite"
+               exit(0)
+       end
+else
+       if toolcontext.opt_gen_force.value then
+               print "Option --force must be used with --gen-suite"
+               exit(0)
+       end
+       if toolcontext.opt_gen_private.value then
+               print "Option --private must be used with --gen-suite"
+               exit(0)
+       end
+       if toolcontext.opt_gen_show.value then
+               print "Option --only-show must be used with --gen-suite"
+               exit(0)
+       end
+end
+
 var model = new Model
 var modelbuilder = new ModelBuilder(model, toolcontext)
 
 var mmodules = modelbuilder.parse(args)
 modelbuilder.run_phases
 
+if toolcontext.opt_gen_unit.value then
+       modelbuilder.gen_test_unit(mmodules.first)
+       exit(0)
+end
+
 var page = new HTMLTag("testsuites")
 
 if toolcontext.opt_full.value then mmodules = model.mmodules
index 759e0dd..24251ae 100644 (file)
@@ -17,3 +17,4 @@ module testing
 
 import testing_doc
 import testing_suite
+import testing_gen
diff --git a/src/testing/testing_gen.nit b/src/testing/testing_gen.nit
new file mode 100644 (file)
index 0000000..19532c6
--- /dev/null
@@ -0,0 +1,181 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Test Suites generation.
+module testing_gen
+
+import testing_base
+import template
+
+# Used to generate a nitunit test file skeleton.
+class NitUnitGenerator
+
+       var toolcontext: ToolContext
+
+       # Generate the NitUnit test file skeleton for `mmodule` in `test_file`.
+       fun gen_unit(mmodule: MModule, test_file: String): Template do
+               var with_private = toolcontext.opt_gen_private.value
+               var tpl = new Template
+               tpl.addn "module test_{mmodule.name} is test_suite"
+               tpl.addn ""
+               tpl.addn "import test_suite"
+               if with_private then
+                       tpl.addn "intrude import {mmodule.name}"
+               else
+                       tpl.addn "import {mmodule.name}"
+               end
+               for mclassdef in mmodule.mclassdefs do
+                       if mclassdef.mclass.kind != concrete_kind then continue
+                       tpl.addn ""
+                       tpl.addn "class Test{mclassdef.name}"
+                       tpl.addn "\tsuper TestSuite"
+                       for mpropdef in mclassdef.mpropdefs do
+                               if not mpropdef isa MMethodDef then continue
+                               var mproperty = mpropdef.mproperty
+                               if mpropdef.is_abstract then continue
+                               if mproperty.is_init then continue
+                               if not with_private and mproperty.visibility <= protected_visibility then continue
+                               var case_name = case_name(mpropdef)
+                               tpl.addn ""
+                               tpl.addn "\tfun {case_name} do"
+                               tpl.addn "\t\tassert not_implemented: false # TODO remove once implemented"
+                               tpl.addn ""
+                               tpl.addn gen_init(mclassdef)
+                               var args = new Array[String]
+                               for mparameter in mpropdef.msignature.mparameters do
+                                       tpl.addn gen_decl(mparameter.name, mparameter.mtype, mclassdef)
+                                       args.add mparameter.name
+                               end
+                               var return_mtype = mpropdef.msignature.return_mtype
+                               if return_mtype != null then
+                                       tpl.addn gen_decl("exp", return_mtype, mclassdef)
+                                       tpl.add "\t\tvar res = "
+                               else
+                                       tpl.add "\t\t"
+                               end
+                               tpl.addn gen_call(mpropdef, args)
+                               if return_mtype != null then
+                                       tpl.addn "\t\tassert exp == res"
+                               end
+                               tpl.addn "\tend"
+                       end
+                       tpl.addn "end"
+               end
+               return tpl
+       end
+
+       # Generate case name based on `MMethodDef`.
+       # special method name like "[]", "+"... are filtered
+       private fun case_name(mmethoddef: MMethodDef): String do
+               var name = mmethoddef.name
+               if name == "[]" then return "test_bra"
+               if name == "[]=" then return "test_bra_assign"
+               if name == "+" then return "test_plus"
+               if name == "-" then return "test_minus"
+               if name == "*" then return "test_star"
+               if name == "/" then return "test_slash"
+               if name == "%" then return "test_percent"
+               if name == "unary -" then return "test_unary_minus"
+               if name == "==" then return "test_equals"
+               if name == "!=" then return "test_not_equals"
+               if name == "<" then return "test_lt"
+               if name == "<=" then return "test_le"
+               if name == "<=>" then return "test_compare"
+               if name == ">=" then return "test_ge"
+               if name == ">" then return "test_gt"
+               return "test_{name}"
+       end
+
+       # Method names that do not need a "." in call.
+       var nodot: Array[String] = ["+", "-", "*", "/", "%", "==", "!=", "<", "<=", "<=>", ">", ">=", ">"]
+
+       # Generate subject init.
+       private fun gen_init(mclassdef: MClassDef): Streamable do
+               if mclassdef.mclass.arity == 0 then
+                       return "\t\tvar subject: {mclassdef.name}"
+               end
+               return "\t\tvar subject: {mclassdef.name}[{mclassdef.bound_mtype.arguments.join(", ")}]"
+       end
+
+       private fun gen_decl(name: String, mtype: MType, mclassdef: MClassDef): String do
+               if mtype.need_anchor then
+                       mtype = mtype.anchor_to(mclassdef.mmodule, mclassdef.bound_mtype)
+               end
+               return "\t\tvar {name}: {mtype.to_s}"
+       end
+
+       # Generate call to `method` using `args`.
+       private fun gen_call(method: MMethodDef, args: Array[String]): Streamable do
+               # Here we handle the magic of the Nit syntax, have fun :)
+               var name = method.name
+               if name == "[]" then return "subject[{args.join(", ")}]"
+               if name == "[]=" then
+                       var last = args.pop
+                       return "subject[{args.join(", ")}] = {last}"
+               end
+               if name == "unary -" then return "-subject"
+               var tpl = new Template
+               if nodot.has(name) then
+                       tpl.add "subject {name}"
+                       if args.length == 1 then
+                               tpl.add " {args.first}"
+                       else if args.length > 1 then
+                               tpl.add " ({args.join(", ")})"
+                       end
+                       return tpl
+               end
+               if name.has_suffix("=") then
+                       name = name.substring(0, name.length - 1)
+                       var last = args.pop
+                       tpl.add "subject.{name}"
+                       if not args.is_empty then
+                               tpl.add "({args.join(", ")})"
+                       end
+                       tpl.add " = {last}"
+                       return tpl
+               end
+               tpl.add "subject.{name}"
+               if args.length == 1 then
+                       tpl.add " {args.first}"
+               else if args.length > 1 then
+                       tpl.add "({args.join(", ")})"
+               end
+               return tpl
+       end
+end
+
+redef class ModelBuilder
+       # Generate NitUnit test file skeleton for `mmodule`.
+       fun gen_test_unit(mmodule: MModule) do
+               var test_file = "test_{mmodule.name}.nit"
+               if test_file.file_exists and not toolcontext.opt_gen_force.value and not toolcontext.opt_gen_show.value then
+                       toolcontext.info("Skip generation for {mmodule}, file {test_file} already exists", 1)
+                       return
+               end
+               var generator = new NitUnitGenerator(toolcontext)
+               var tpl = generator.gen_unit(mmodule, test_file)
+               if toolcontext.opt_gen_show.value then
+                       tpl.write_to(sys.stdout)
+               else
+                       tpl.write_to_file(test_file)
+               end
+       end
+end
+
+redef class ToolContext
+       var opt_gen_unit = new OptionBool("Generate test suite skeleton for a module", "--gen-suite")
+       var opt_gen_force = new OptionBool("Force test generation even if file exists", "-f", "--force")
+       var opt_gen_private = new OptionBool("Also generate test case for private methods", "--private")
+       var opt_gen_show = new OptionBool("Only display skeleton, do not write file", "--only-show")
+end