diff --git a/jac/jaclang/cli/cli.py b/jac/jaclang/cli/cli.py index a16c288443..3f2782e4df 100644 --- a/jac/jaclang/cli/cli.py +++ b/jac/jaclang/cli/cli.py @@ -307,6 +307,7 @@ def enter( @cmd_registry.register def test( filepath: str, + test_name: str = "", filter: str = "", xit: bool = False, maxfail: int = None, # type:ignore @@ -316,6 +317,7 @@ def test( """Run the test suite in the specified .jac file. :param filepath: Path/to/file.jac + :param test_name: Run a specific test. :param filter: Filter the files using Unix shell style conventions. :param xit(exit): Stop(exit) running tests as soon as finds an error. :param maxfail: Stop running tests after n failures. @@ -328,6 +330,7 @@ def test( failcount = Jac.run_test( filepath=filepath, + func_name=("test_" + test_name) if test_name else None, filter=filter, xit=xit, maxfail=maxfail, diff --git a/jac/jaclang/plugin/default.py b/jac/jaclang/plugin/default.py index 76fa0df859..7b276ffcc8 100644 --- a/jac/jaclang/plugin/default.py +++ b/jac/jaclang/plugin/default.py @@ -5,6 +5,7 @@ import ast as ast3 import fnmatch import html +import inspect import os import types from collections import OrderedDict @@ -818,12 +819,14 @@ def jac_import( @hookimpl def create_test(test_fun: Callable) -> Callable: """Create a new test.""" + file_path = inspect.getfile(test_fun) + func_name = test_fun.__name__ def test_deco() -> None: test_fun(JacTestCheck()) test_deco.__name__ = test_fun.__name__ - JacTestCheck.add_test(test_deco) + JacTestCheck.add_test(file_path, func_name, test_deco) return test_deco @@ -831,6 +834,7 @@ def test_deco() -> None: @hookimpl def run_test( filepath: str, + func_name: Optional[str], filter: Optional[str], xit: bool, maxfail: Optional[int], @@ -849,7 +853,9 @@ def run_test( mod_name = mod_name[:-5] JacTestCheck.reset() Jac.jac_import(target=mod_name, base_path=base, cachable=False) - JacTestCheck.run_test(xit, maxfail, verbose) + JacTestCheck.run_test( + xit, maxfail, verbose, os.path.abspath(filepath), func_name + ) ret_count = JacTestCheck.failcount else: print("Not a .jac file.") @@ -875,7 +881,9 @@ def run_test( print(f"\n\n\t\t* Inside {root_dir}" + "/" + f"{file} *") JacTestCheck.reset() Jac.jac_import(target=file[:-4], base_path=root_dir) - JacTestCheck.run_test(xit, maxfail, verbose) + JacTestCheck.run_test( + xit, maxfail, verbose, os.path.abspath(file), func_name + ) if JacTestCheck.breaker and (xit or maxfail): break diff --git a/jac/jaclang/plugin/feature.py b/jac/jaclang/plugin/feature.py index 808b254ae9..a2fb585fed 100644 --- a/jac/jaclang/plugin/feature.py +++ b/jac/jaclang/plugin/feature.py @@ -359,6 +359,7 @@ def create_test(test_fun: Callable) -> Callable: @staticmethod def run_test( filepath: str, + func_name: Optional[str] = None, filter: Optional[str] = None, xit: bool = False, maxfail: Optional[int] = None, @@ -368,6 +369,7 @@ def run_test( """Run the test suite in the specified .jac file.""" return plugin_manager.hook.run_test( filepath=filepath, + func_name=func_name, filter=filter, xit=xit, maxfail=maxfail, diff --git a/jac/jaclang/plugin/spec.py b/jac/jaclang/plugin/spec.py index bc1bf01259..aa7371cc85 100644 --- a/jac/jaclang/plugin/spec.py +++ b/jac/jaclang/plugin/spec.py @@ -345,6 +345,7 @@ def create_test(test_fun: Callable) -> Callable: @hookspec(firstresult=True) def run_test( filepath: str, + func_name: Optional[str], filter: Optional[str], xit: bool, maxfail: Optional[int], diff --git a/jac/jaclang/runtimelib/test.py b/jac/jaclang/runtimelib/test.py index f453b74c07..64a878d037 100644 --- a/jac/jaclang/runtimelib/test.py +++ b/jac/jaclang/runtimelib/test.py @@ -3,6 +3,7 @@ from __future__ import annotations import unittest +from dataclasses import dataclass from typing import Callable, Optional @@ -56,6 +57,16 @@ class JacTestCheck: test_case = unittest.TestCase() test_suite = unittest.TestSuite() + + @dataclass + class TestSuite: + """Test Suite.""" + + test_case: unittest.FunctionTestCase + func_name: str + + test_suite_path: dict[str, list[TestSuite]] = {} + breaker = False failcount = 0 @@ -64,13 +75,45 @@ def reset() -> None: """Clear the test suite.""" JacTestCheck.test_case = unittest.TestCase() JacTestCheck.test_suite = unittest.TestSuite() + JacTestCheck.test_suite_path = {} @staticmethod - def run_test(xit: bool, maxfail: int | None, verbose: bool) -> None: + def run_test( + xit: bool, + maxfail: int | None, + verbose: bool, + filepath: str | None, + func_name: str | None, + ) -> None: """Run the test suite.""" verb = 2 if verbose else 1 + test_suite = JacTestCheck.test_suite + + if filepath and filepath.endswith(".test.jac"): + filepath = filepath[:-9] + elif filepath and filepath.endswith(".jac"): + filepath = filepath[:-4] + + if filepath: + test_cases = JacTestCheck.test_suite_path.get(filepath) + if test_cases is not None: + test_suite = unittest.TestSuite() + for test_case in test_cases: + if func_name: + if test_case.func_name == func_name: + test_suite.addTest(test_case.test_case) + else: + test_suite.addTest(test_case.test_case) + + elif func_name: + test_suite = unittest.TestSuite() + for test_cases in JacTestCheck.test_suite_path.values(): + for test_case in test_cases: + if test_case.func_name == func_name: + test_suite.addTest(test_case.test_case) + runner = JacTextTestRunner(max_failures=maxfail, failfast=xit, verbosity=verb) - result = runner.run(JacTestCheck.test_suite) + result = runner.run(test_suite) if result.wasSuccessful(): print("Passed successfully.") else: @@ -81,9 +124,21 @@ def run_test(xit: bool, maxfail: int | None, verbose: bool) -> None: ) @staticmethod - def add_test(test_fun: Callable) -> None: + def add_test(filepath: str, func_name: str, test_func: Callable) -> None: """Create a new test.""" - JacTestCheck.test_suite.addTest(unittest.FunctionTestCase(test_fun)) + if filepath and filepath.endswith(".test.jac"): + filepath = filepath[:-9] + elif filepath and filepath.endswith(".jac"): + filepath = filepath[:-4] + + if filepath not in JacTestCheck.test_suite_path: + JacTestCheck.test_suite_path[filepath] = [] + + test_case = unittest.FunctionTestCase(test_func) + JacTestCheck.test_suite_path[filepath].append( + JacTestCheck.TestSuite(test_case=test_case, func_name=func_name) + ) + JacTestCheck.test_suite.addTest(test_case) def __getattr__(self, name: str) -> object: """Make convenient check.Equal(...) etc.""" diff --git a/jac/jaclang/tests/fixtures/jactest_imported.jac b/jac/jaclang/tests/fixtures/jactest_imported.jac new file mode 100644 index 0000000000..f9695e7962 --- /dev/null +++ b/jac/jaclang/tests/fixtures/jactest_imported.jac @@ -0,0 +1,6 @@ + + +test this_should_not_run { + print("This test should not run after import."); + assert False; +} diff --git a/jac/jaclang/tests/fixtures/jactest_main.jac b/jac/jaclang/tests/fixtures/jactest_main.jac new file mode 100644 index 0000000000..dc174dd898 --- /dev/null +++ b/jac/jaclang/tests/fixtures/jactest_main.jac @@ -0,0 +1,22 @@ +import jactest_imported; + +can fib(n: int) -> int { + if n <= 1 { + return n; + } + return fib(n - 1) + fib(n - 2); +} + + +test first_two { + print("Testing first 2 fibonacci numbers."); + assert fib(0) == 0; + assert fib(1) == 0; +} + +test from_2_to_10 { + print("Testing fibonacci numbers from 2 to 10."); + for i in range(2, 10) { + assert fib(i) == fib(i - 1) + fib(i - 2); + } +} diff --git a/jac/jaclang/tests/test_cli.py b/jac/jaclang/tests/test_cli.py index cf9b1cd183..8e20720a7d 100644 --- a/jac/jaclang/tests/test_cli.py +++ b/jac/jaclang/tests/test_cli.py @@ -393,6 +393,27 @@ def test_run_test(self) -> None: self.assertIn("...F", stderr) self.assertIn("F.F", stderr) + def test_run_specific_test_only(self) -> None: + """Test a specific test case.""" + process = subprocess.Popen( + [ + "jac", + "test", + "-t", + "from_2_to_10", + self.fixture_abs_path("jactest_main.jac"), + ], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + stdout, stderr = process.communicate() + self.assertIn("Ran 1 test", stderr) + self.assertIn("Testing fibonacci numbers from 2 to 10.", stdout) + self.assertNotIn("Testing first 2 fibonacci numbers.", stdout) + self.assertNotIn("This test should not run after import.", stdout) + def test_graph_coverage(self) -> None: """Test for coverage of graph cmd.""" graph_params = set(inspect.signature(cli.dot).parameters.keys())