diff --git a/jaclang/cli/cli.py b/jaclang/cli/cli.py index 52023921f..eca628e2a 100644 --- a/jaclang/cli/cli.py +++ b/jaclang/cli/cli.py @@ -1,7 +1,6 @@ """Command line interface tool for the Jac language.""" import os import shutil -import unittest from typing import Optional from jaclang import jac_import as __jac_import__ @@ -9,6 +8,7 @@ from jaclang.compiler.constant import Constants from jaclang.compiler.passes.tool.schedules import format_pass from jaclang.compiler.transpiler import jac_file_to_pass +from jaclang.plugin.feature import JacFeature as Jac from jaclang.utils.lang_tools import AstTool @@ -84,17 +84,7 @@ def test(filename: str) -> None: :param filename: The path to the .jac file. """ - if filename.endswith(".jac"): - base, mod_name = os.path.split(filename) - base = base if base else "./" - mod_name = mod_name[:-4] - mod = __jac_import__(target=mod_name, base_path=base) - if hasattr(mod, "__jac_suite__"): - unittest.TextTestRunner().run(getattr(mod, "__jac_suite__")) # noqa: B009 - else: - print("No tests found.") - else: - print("Not a .jac file.") + Jac.run_test(filename) @cmd_registry.register diff --git a/jaclang/compiler/passes/main/pyast_gen_pass.py b/jaclang/compiler/passes/main/pyast_gen_pass.py index 6fac1990f..41a23f0c2 100644 --- a/jaclang/compiler/passes/main/pyast_gen_pass.py +++ b/jaclang/compiler/passes/main/pyast_gen_pass.py @@ -113,21 +113,6 @@ def needs_jac_feature(self) -> None: ) self.already_added.append("jac_feature") - def needs_test(self) -> None: - """Check if test is needed.""" - if "test" in self.already_added: - return - test_code = ( - "import unittest as __jac_unittest__\n" - "__jac_tc__ = __jac_unittest__.TestCase()\n" - "__jac_suite__ = __jac_unittest__.TestSuite()\n" - "class __jac_check:\n" - " def __getattr__(self, name):\n" - " return getattr(__jac_tc__, 'assert'+name)" - ) - self.preamble += ast3.parse(test_code).body - self.already_added.append("test") - def flatten(self, body: list[T | list[T] | None]) -> list[T]: """Flatten ast list.""" new_body = [] @@ -279,7 +264,6 @@ def exit_test(self, node: ast.Test) -> None: body: SubNodeList[CodeBlockStmt], doc: Optional[String], """ - self.needs_test() test_name = node.name.sym_name func = self.sync( ast3.FunctionDef( @@ -287,7 +271,7 @@ def exit_test(self, node: ast.Test) -> None: args=self.sync( ast3.arguments( posonlyargs=[], - args=[], + args=[self.sync(ast3.arg(arg="check", annotation=None))], kwonlyargs=[], vararg=None, kwargs=None, @@ -296,22 +280,23 @@ def exit_test(self, node: ast.Test) -> None: ) ), body=self.resolve_stmt_block(node.body, doc=node.doc), - decorator_list=[], + decorator_list=[ + self.sync( + ast3.Attribute( + value=self.sync( + ast3.Name(id=Con.JAC_FEATURE.value, ctx=ast3.Load()) + ), + attr="create_test", + ctx=ast3.Load(), + ) + ) + ], returns=self.sync(ast3.Constant(value=None)), type_comment=None, type_params=[], ), ) - func.body.insert( - 0, - self.sync(ast3.parse("check = __jac_check()").body[0]), - ) - check = self.sync( - ast3.parse( - f"__jac_suite__.addTest(__jac_unittest__.FunctionTestCase({test_name}))" - ).body[0] - ) - node.gen.py_ast = [func, check] + node.gen.py_ast = func def exit_module_code(self, node: ast.ModuleCode) -> None: """Sub objects. diff --git a/jaclang/core/construct.py b/jaclang/core/construct.py index f4680d62a..709963d65 100644 --- a/jaclang/core/construct.py +++ b/jaclang/core/construct.py @@ -2,8 +2,9 @@ from __future__ import annotations import types +import unittest from dataclasses import dataclass, field -from typing import Any, Callable, Optional +from typing import Any, Callable, Optional, Union from jaclang.compiler.constant import EdgeDir @@ -299,4 +300,31 @@ def resolve(self, cls: type) -> None: self.func = getattr(cls, self.name) +class JacTestCheck: + """Jac Testing and Checking.""" + + test_case = unittest.TestCase() + test_suite = unittest.TestSuite() + + @staticmethod + def reset() -> None: + """Clear the test suite.""" + JacTestCheck.test_case = unittest.TestCase() + JacTestCheck.test_suite = unittest.TestSuite() + + @staticmethod + def run_test() -> None: + """Run the test suite.""" + unittest.TextTestRunner().run(JacTestCheck.test_suite) + + @staticmethod + def add_test(test_fun: Callable) -> None: + """Create a new test.""" + JacTestCheck.test_suite.addTest(unittest.FunctionTestCase(test_fun)) + + def __getattr__(self, name: str) -> Union[bool, Any]: + """Make convenient check.Equal(...) etc.""" + return getattr(JacTestCheck.test_case, "assert" + name) + + root = Root() diff --git a/jaclang/plugin/default.py b/jaclang/plugin/default.py index 9c6f2e51d..b064a15af 100644 --- a/jaclang/plugin/default.py +++ b/jaclang/plugin/default.py @@ -1,11 +1,12 @@ """Jac Language Features.""" from __future__ import annotations +import os from dataclasses import dataclass, field from functools import wraps from typing import Any, Callable, Optional, Type - +from jaclang import jac_import from jaclang.plugin.spec import ( ArchBound, Architype, @@ -14,6 +15,7 @@ EdgeArchitype, EdgeDir, GenericEdge, + JacTestCheck, NodeArchitype, T, WalkerArchitype, @@ -69,6 +71,36 @@ def new_init(self: ArchBound, *args: object, **kwargs: object) -> None: return decorator + @staticmethod + @hookimpl + def create_test(test_fun: Callable) -> Callable: + """Create a new test.""" + + def test_deco() -> None: + test_fun(JacTestCheck()) + + test_deco.__name__ = test_fun.__name__ + JacTestCheck.add_test(test_deco) + + return test_deco + + @staticmethod + @hookimpl + def run_test(filename: str) -> None: + """Run the test suite in the specified .jac file. + + :param filename: The path to the .jac file. + """ + if filename.endswith(".jac"): + base, mod_name = os.path.split(filename) + base = base if base else "./" + mod_name = mod_name[:-4] + JacTestCheck.reset() + jac_import(target=mod_name, base_path=base) + JacTestCheck.run_test() + else: + print("Not a .jac file.") + @staticmethod @hookimpl def elvis(op1: Optional[T], op2: T) -> T: diff --git a/jaclang/plugin/feature.py b/jaclang/plugin/feature.py index 927afca9b..762087742 100644 --- a/jaclang/plugin/feature.py +++ b/jaclang/plugin/feature.py @@ -40,6 +40,16 @@ def make_architype( arch_type=arch_type, on_entry=on_entry, on_exit=on_exit ) + @staticmethod + def create_test(test_fun: Callable) -> Callable: + """Create a test.""" + return JacFeature.pm.hook.create_test(test_fun=test_fun) + + @staticmethod + def run_test(filename: str) -> None: + """Run the test suite in the specified .jac file.""" + return JacFeature.pm.hook.run_test(filename=filename) + @staticmethod def elvis(op1: Optional[T], op2: T) -> T: """Jac's elvis operator feature.""" diff --git a/jaclang/plugin/spec.py b/jaclang/plugin/spec.py index 0cab0b3bd..ead62b8e9 100644 --- a/jaclang/plugin/spec.py +++ b/jaclang/plugin/spec.py @@ -11,6 +11,7 @@ EdgeArchitype, EdgeDir, GenericEdge, + JacTestCheck, NodeAnchor, NodeArchitype, ObjectAnchor, @@ -23,6 +24,7 @@ __all__ = [ "EdgeAnchor", "GenericEdge", + "JacTestCheck", "NodeAnchor", "ObjectAnchor", "WalkerAnchor", @@ -56,6 +58,21 @@ def make_architype( """Create a new architype.""" raise NotImplementedError + @staticmethod + @hookspec(firstresult=True) + def create_test(test_fun: Callable) -> Callable: + """Create a new test.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def run_test(filename: str) -> None: + """Run the test suite in the specified .jac file. + + :param filename: The path to the .jac file. + """ + raise NotImplementedError + @staticmethod @hookspec(firstresult=True) def elvis(op1: Optional[T], op2: T) -> T: