diff --git a/test/README.md b/test/README.md new file mode 100644 index 00000000..f5092dd2 --- /dev/null +++ b/test/README.md @@ -0,0 +1,69 @@ +# Tests: + +## Running Tests + +Our projects use both **`pytest`** and **`unittest`** frameworks. +You can run tests using either method. +The **`-vvv`** flag is used for **verbosity**, which provides more detailed output and is very helpful for debugging. + +### To run all tests, use one of the following command: +* **Using Pytest:** + ```bash + pytest unit -vvv + ``` + +* **Using Unittest:** + ```bash + python3 -m unittest discover unit -vvv + ``` + +### Running a Subset of Tests +Specify the directory containing your desired tests. For example, to run tests in `my_test_dir`: + +```bash +pytest unit/my_test_dir -vvv +# OR +python3 -m unittest discover unit/my_test_dir -vvv +``` + +## Adding New Unit Tests + +1. **Create a Test Folder** + Inside the `unit` directory, create a folder for your new test. This folder should contain: + - All source/header files needed for the test + - `BUILD` + - A python test script + - `__init__.py` + +2. **Creating the BUILD File** + - Make sure that all failing test targets get the `"manual"` tag. For example: + ``` + # This is a test I expect to fail + codechecker_test( + name = "codechecker_fail", + tags = [ + "manual", + ], + targets = [ + "test_fail", + ], + ) + ``` + +2. **Creating the Test File** + - Your test script must follow the naming convention: + ```text + test_*.py + ``` + - At the top of your test file, include the following snippet to correctly handle module imports: + ```python + from common.base import TestBase + ``` + - Create your test class by extending `TestBase` and implement your test methods. +> [!WARNING] +> You should include this line in your test class, this sets the current working directory: +> ```python +> __test_path__ = os.path.dirname(os.path.abspath(__file__)) +> ``` + +**For a test template look into unit/template** diff --git a/test/unit/__init__.py b/test/unit/__init__.py new file mode 100644 index 00000000..e6deb66a --- /dev/null +++ b/test/unit/__init__.py @@ -0,0 +1,24 @@ +# Copyright 2023 Ericsson AB +# +# 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. + +""" +Setup module paths and environment variables for the functional tests. +""" + +import os +import sys + +# Allow relative imports within the test project to work as expected +# Without it no module (test) would be able to import the common library +sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)))) diff --git a/test/unit/common/__init__.py b/test/unit/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/unit/common/base.py b/test/unit/common/base.py new file mode 100644 index 00000000..51766ac6 --- /dev/null +++ b/test/unit/common/base.py @@ -0,0 +1,126 @@ +# Copyright 2023 Ericsson AB +# +# 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. + +""" +Base for unit and functional tests +""" + +import logging +import os +import re +import shlex +import subprocess +import unittest +import sys + + +class TestBase(unittest.TestCase): + """Unittest base abstract class""" + + # This variable must be overwritten in each subclass! + __test_path__: str = None + BAZEL_BIN_DIR: str = None + BAZEL_TESTLOGS_DIR: str = None + + @classmethod + def setUpClass(cls): + """Load module, save environment""" + ErrorCollector: list[str] = [] + if cls.__test_path__ == None: + ErrorCollector.append( + "Test path must be overwritten! Use:" + "\n__test_path__ = os.path.dirname(os.path.abspath(__file__))" + ) + if cls.BAZEL_BIN_DIR == None: + ErrorCollector.append( + "Bazel bin directory must be overwritten! Use:" + "../../../bazel-bin/test/unit/my_test_folder" + ) + if cls.BAZEL_TESTLOGS_DIR == None: + ErrorCollector.append( + "Bazel test logs directory must be overwritten! Use:" + "../../../bazel-testlogs/test/unit/my_test_folder" + ) + if ErrorCollector: + raise NotImplementedError("\n".join(ErrorCollector)) + # Enable debug logs for tests if "super verbose" flag is provided + if "-vvv" in sys.argv: + logging.basicConfig( + level=logging.DEBUG, format="[TEST] %(levelname)5s: %(message)s" + ) + # Move to test dir + cls.test_dir = cls.__test_path__ + os.chdir(cls.test_dir) + # Save environment and location + cls.save_env = os.environ + cls.save_cwd = os.getcwd() + + @classmethod + def tearDownClass(cls): + """Restore environment""" + os.chdir(cls.save_cwd) + os.environ = cls.save_env + + def setUp(self): + """Before every test""" + logging.debug("\n%s", "-" * 70) + + @classmethod + def run_command(self, cmd: str, working_dir:str=None) -> tuple[int, str, str]: + """ + Run shell command. + returns: + - exit code + - stdout + - stderr + """ + logging.debug("Running: %s", cmd) + commands = shlex.split(cmd) + with subprocess.Popen( + commands, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=working_dir, + ) as process: + stdout, stderr = process.communicate() + return ( + process.returncode, + f"stdout: {stdout.decode('utf-8')}", + f"stderr: {stderr.decode('utf-8')}", + ) + + @classmethod + def grep_file(self, filename, regex): + """ + Grep given filename. + Returns list of matched lines. + Returns empty list if no match is found + """ + results : list[str] = [] + pattern = re.compile(regex) + logging.debug("RegEx = r'%s'", regex) + with open(filename, "r", encoding="utf-8") as fileobj: + for line in fileobj: + if pattern.search(line): + logging.debug(line) + results.append(line) + return results + + @classmethod + def contains_regex_in_file(self, file_path: str, regex: str) -> bool: + """ + Returns a boolean, whether the specified file contains the regex or not. + """ + return self.grep_file(file_path, regex) != [] diff --git a/test/unit/template/__init__.py b/test/unit/template/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/unit/template/test_template.py b/test/unit/template/test_template.py new file mode 100644 index 00000000..8113cdd2 --- /dev/null +++ b/test/unit/template/test_template.py @@ -0,0 +1,61 @@ +# Copyright 2023 Ericsson AB +# +# 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. + +""" +TODO: Describe what this file does +""" +import os +import unittest +from typing import final +from common.base import TestBase + + +class TestTemplate(TestBase): + """TODO: Add a description""" + # Set working directory + __test_path__ = os.path.dirname(os.path.abspath(__file__)) + # TODO: fix folder name + BAZEL_BIN_DIR = os.path.join("../../..", "bazel-bin", "test", + "unit", "my_test_folder") + BAZEL_TESTLOGS_DIR = os.path.join("../../..", "bazel-testlogs", "test", + "unit", "my_test_folder") + + @final + @classmethod + def setUpClass(cls): + """TODO: Define set up before the test suite""" + super().setUpClass() + cls.run_command("bazel clean") + + @final + @classmethod + def tearDownClass(cls): + """TODO: Define clean up after the test suite""" + super().tearDownClass() + + def setUp(self): + """TODO: Define clean up before every test""" + super().setUp() + + def tearDown(self): + """TODO: Define clean up after every test""" + return super().tearDown() + + def test_template(self): + """Test: TODO: describe your test""" + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main(buffer=True)