diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 6eea2606..1163df56 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -31,9 +31,7 @@ jobs: name: wheels path: | packages/*/dist/*.whl - packages/*/dist/*.gz packages/*/meta_dist/*.whl - packages/*/meta_dist/*.gz upload-test: name: PyPI Upload diff --git a/CHANGELOG.md b/CHANGELOG.md index 0be354b0..6216e94e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,16 @@ ## Changelog -### 0.9.18-dev + +### 0.9.19-dev + +* Fixes packaging of `bmw-lobster`. + +### 0.9.18 + +* Added a new tool `lobster-cpptest` which can extract references + from C++ unit tests using various regex patterns. + The references must be provided in a format similar to Doxygen comments. * Add support to `lobster-codebeamer` to generate output using the following schemas: - requirement diff --git a/CODING_GUIDELINE.md b/CODING_GUIDELINE.md new file mode 100644 index 00000000..bde06a21 --- /dev/null +++ b/CODING_GUIDELINE.md @@ -0,0 +1,208 @@ +# Coding Guideline + +Since the introduction of the coding guideline, +we mostly follow the [_Black_ Code Style](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html). + +Note that there is still older code around, +where the coding guideline has not yet been applied. + +## Classes + +The nomenclature of classes shall adhere to the following guideline: + +- Classes shall use the CamelCase pattern. +- File names shall be equal to the class names, but all lower case letters. +- Private members shall have at least one leading underscore. + Refer to [Private variables and name mangling](https://docs.python.org/3/tutorial/classes.html#private-variables) + to read more about it. + +Example: +```python +class CamelCase: + pass +``` +Here the file shall be named `camelcase.py`. + +In general, files should not be too lengthy. +We don't want to give a specific limit on the number of lines. +Consider to keep classes in separate files, +instead of having one large file for multiple classes. + +## Tests +We are using the `unittest` test framework for unit tests. +Test class names shall consist of the name of the class under test, +with a `Test` postfix. + +Example: +```python +from unittest import TestCase + +class CamelCaseTest(TestCase): + def test_function1(self): + instance_under_test = CamelCase() + result = instance_under_test.function1() + self.assertEqual(True, result) +``` + +The file name shall be `test_camelcase.py` + +## Line Length +The line length is 88 characters. + +## Architecture +Consider to create classes with dedicated properties over using dictionaries with keys. + +Example of the preferred approach: +```python +from dataclasses import dataclass + + +@dataclass +class Car: + color: str + +car = Car(color="red") +``` + +Counter-example of the disfavoured approach: +```python +car = {"color": "red"} +``` +The latter examples with a dictionary is easier to get started with, +but it is harder to maintain and refactor. +Also pylint will not be able to give warnings when accessing a key that does not exist. + + + +## Comments + +It's good to avoid inline comments and single line comments should be preferred. + +```python +def generate_lobster_report(): + + # Please prefer to use this single line commenting style for better readability. + lobster_report = "" + + return lobster_report # Avoid commenting style like this comment. +``` + + +## Documentation Strings a.k.a. Docstring + +### One line docstring + +One line docstrings as the name suggests, should fit in one line + +Example: +```python +def sum_of_two_numbers(number1, number2): + """Return the sum of two numbers.""" + return number1 + number2 +``` + + +### Multi-line docstring + +Multi-line docstring consists of a single line summary with a blank line followed by it and then followed by more +detailed description. To know more about it please visit official documentation [Python Multi-line docstrings](https://peps.python.org/pep-0257/#multi-line-docstrings). + +```python +def generate_lobster_report(data, metadata): + """ + Function to generate a lobster report. + + Keyword arguments: + data -- data contains the actual data used to generate the report + metadata -- metadata about the report + """ + + # some operations + lobster_report = "" + return lobster_report +``` + + +## Blank lines + +- Top-level functions and classes should be separated by 2 blank lines. +- A single blank line should be added between two methods of a class. +- Used blank lines inside a function/method based on logical sections and refrain from adding too much blank lines. + + +## Imports + +Imports should always be added on top of the file and there order should be as in the below mentioned sequence. +- Standard library imports +- Third-party imports +- Application imports + +Add a blank line between each type of imports. + + +- ### **Direct package/Module imports** + - ```python + # This is Correct + import sys + import os + + # Don't do this + import sys, os + + # This one is fine + from os import path + ``` +- ### **Absolute vs relative imports** + - As per the PEP 8 guidelines absolute imports are recommended over relative imports. + - ```python + import lobster.items + from lobster import items + from lobster.items import Implementation, TracingTag + + # Although in case of complex package layouts relative imports are acceptable + + from .items import Implementation + ``` + +- ### **Long import statements** + For long import statements i.e. line length more than 88 characters, use the below format for import statements + ```python + from lobster.items import Tracing_Tag, Requirement, Implementation, Activity, Tracing_Status + # The above import line is above 88 characters so surround the import statements using + # round brackets. + + from lobster.items import ( + Tracing_Tag, Requirement, Implementation, Activity, Tracing_Status + ) + ``` + +- ### Important points + - #### **Use relative imports only when the package structure is complex like one given in the example below** + - #### **Do not use wildcard imports as it makes unclear exactly what is used from the imported package/module** + - Example of wildcard imports + ```python + # Don't use wildcard imports + from lobster import * + ``` + + +## String Quotes + +Python supports single quoted as well as double-quoted strings. The PEP8 guideline doesn't have any guideline to use these. +But using one style would help the reader on focusing on the code and avoid any distraction. +The *Black coding style recommends to used double-quoted string as it match with the +docstring standard as per PEP8* + + +## Function Lengths + +There is no standard as such defined for the length of the functions. +However, if a functions is too long i.e lot of screen scrolling required to read as a whole then it is advisable to identify +and club related code and move them to new functions. Breaking the code into smaller functions would also help in code reusability. + + +## Constants + +Constants are generally defined at module level and in capital letters. For separation underscores are used. + +Example : `SCHEMA`, `ERROR_SEVERITY` diff --git a/README.md b/README.md index 905fe9cc..70bdc23b 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ The individual packages that `bmw-lobster` depends on are: * `bmw-lobster-core` the core API and various report generators. All other tools depend on this. * `bmw-lobster-tool-cpp` (for C/C++ code) +* `bmw-lobster-tool-cpptest` (for C/C++ code) * `bmw-lobster-tool-gtest` (for GoogleTest tests) * `bmw-lobster-tool-python` (for Python3 code) * `bmw-lobster-tool-beamer` (for requirements in Codebeamer) @@ -70,6 +71,7 @@ The individual packages that `bmw-lobster` depends on are: ### For LOBSTER developers * [Code Coverage Report](https://bmw-software-engineering.github.io/lobster/htmlcov/index.html) +* [Coding Guideline](CODING_GUIDELINE.md) ## Planned inputs diff --git a/lobster/tools/json/json.py b/lobster/tools/json/json.py index 6cd70142..b71b6355 100755 --- a/lobster/tools/json/json.py +++ b/lobster/tools/json/json.py @@ -53,14 +53,11 @@ def get_item(root, path, required): elif required: raise Malformed_Input("object does not contain %s" % field, root) - else: - return None + return None elif required: raise Malformed_Input("not an object", root) - - else: - return None + return None def syn_test_name(file_name): @@ -107,15 +104,15 @@ def process_tool_options(self, options, work_list): @classmethod def process(cls, options, file_name): - with open(file_name, "r", encoding="UTF-8") as fd: - data = json.load(fd) - - # First we follow the test-list items to get the actual data - # we're interested in. try: + with open(file_name, "r", encoding="UTF-8") as fd: + data = json.load(fd) data = get_item(root = data, path = options.test_list, required = True) + except UnicodeDecodeError as decode_error: + print("%s: File is not encoded in utf-8: %s" % (file_name, decode_error)) + return False, [] except Malformed_Input as err: pprint(err.data) print("%s: malformed input: %s" % (file_name, err.msg)) diff --git a/lobster/tools/python/python.py b/lobster/tools/python/python.py index 496bff68..24583057 100755 --- a/lobster/tools/python/python.py +++ b/lobster/tools/python/python.py @@ -500,7 +500,6 @@ def main(): if options.out: with open(options.out, "w", encoding="UTF-8") as fd: lobster_write(fd, schema, "lobster_python", items) - fd.write("\n") print("Written output for %u items to %s" % (len(items), options.out)) else: diff --git a/lobster/version.py b/lobster/version.py index 14b91bd9..b3d1603a 100644 --- a/lobster/version.py +++ b/lobster/version.py @@ -17,10 +17,11 @@ # License along with this program. If not, see # . -VERSION_TUPLE = (0, 9, 18) +VERSION_TUPLE = (0, 9, 19) VERSION_SUFFIX = "dev" -LOBSTER_VERSION = ("%u.%u.%u" % VERSION_TUPLE) + \ - ("-%s" % VERSION_SUFFIX if VERSION_SUFFIX else "") +LOBSTER_VERSION = ("%u.%u.%u" % VERSION_TUPLE) + ( + "-%s" % VERSION_SUFFIX if VERSION_SUFFIX else "" +) FULL_NAME = "LOBSTER %s" % LOBSTER_VERSION diff --git a/packages/lobster-core/Makefile b/packages/lobster-core/Makefile index 15c3a7ff..324da0e6 100644 --- a/packages/lobster-core/Makefile +++ b/packages/lobster-core/Makefile @@ -6,4 +6,4 @@ package: cp -Rv $(LOBSTER_ROOT)/lobster/html lobster cp $(LOBSTER_ROOT)/lobster/tools/*.py lobster/tools cp -Rv $(LOBSTER_ROOT)/lobster/tools/core lobster/tools - @python3 -m build + @python3 -m build --wheel diff --git a/packages/lobster-metapackage/Makefile b/packages/lobster-metapackage/Makefile index 8173277b..3a89d36b 100644 --- a/packages/lobster-metapackage/Makefile +++ b/packages/lobster-metapackage/Makefile @@ -1,2 +1,2 @@ package: - @python3 -m build + @python3 -m build --wheel diff --git a/packages/lobster-metapackage/README.md b/packages/lobster-metapackage/README.md index 2b2038e3..c5dd2ddc 100644 --- a/packages/lobster-metapackage/README.md +++ b/packages/lobster-metapackage/README.md @@ -11,6 +11,7 @@ LOBSTER packages as a convenience: * [bmw-lobster-core](https://pypi.org/project/bmw-lobster-core) * [bmw-lobster-tool-codebeamer](https://pypi.org/project/bmw-lobster-tool-codebeamer) * [bmw-lobster-tool-cpp](https://pypi.org/project/bmw-lobster-tool-cpp) +* [bmw-lobster-tool-cpptest](https://pypi.org/project/bmw-lobster-tool-cpptest) * [bmw-lobster-tool-gtest](https://pypi.org/project/bmw-lobster-tool-gtest) * [bmw-lobster-tool-json](https://pypi.org/project/bmw-lobster-tool-json) * [bmw-lobster-tool-python](https://pypi.org/project/bmw-lobster-tool-python) diff --git a/packages/lobster-monolithic/Makefile b/packages/lobster-monolithic/Makefile index 55030c70..26c9c9ff 100644 --- a/packages/lobster-monolithic/Makefile +++ b/packages/lobster-monolithic/Makefile @@ -1,5 +1,5 @@ package: rm -rf lobster dist meta_dist cp -Rv $(LOBSTER_ROOT)/lobster lobster - @python3 -m build + @python3 -m build --wheel mv dist meta_dist diff --git a/packages/lobster-tool-codebeamer/Makefile b/packages/lobster-tool-codebeamer/Makefile index a018b801..ca5e2eb3 100644 --- a/packages/lobster-tool-codebeamer/Makefile +++ b/packages/lobster-tool-codebeamer/Makefile @@ -2,4 +2,4 @@ package: rm -rf lobster mkdir -p lobster/tools cp -Rv $(LOBSTER_ROOT)/lobster/tools/codebeamer lobster/tools - @python3 -m build + @python3 -m build --wheel diff --git a/packages/lobster-tool-cpp/Makefile b/packages/lobster-tool-cpp/Makefile index 6bee7d0b..3bbd8a77 100644 --- a/packages/lobster-tool-cpp/Makefile +++ b/packages/lobster-tool-cpp/Makefile @@ -2,4 +2,4 @@ package: rm -rf lobster mkdir -p lobster/tools cp -Rv $(LOBSTER_ROOT)/lobster/tools/cpp lobster/tools - @python3 -m build + @python3 -m build --wheel diff --git a/packages/lobster-tool-cpptest/Makefile b/packages/lobster-tool-cpptest/Makefile index 29c612dc..0e44de82 100644 --- a/packages/lobster-tool-cpptest/Makefile +++ b/packages/lobster-tool-cpptest/Makefile @@ -2,4 +2,4 @@ package: rm -rf lobster mkdir -p lobster/tools cp -Rv $(LOBSTER_ROOT)/lobster/tools/cpptest lobster/tools - @python3 -m build + @python3 -m build --wheel diff --git a/packages/lobster-tool-gtest/Makefile b/packages/lobster-tool-gtest/Makefile index ce3ff551..4df97028 100644 --- a/packages/lobster-tool-gtest/Makefile +++ b/packages/lobster-tool-gtest/Makefile @@ -2,4 +2,4 @@ package: rm -rf lobster mkdir -p lobster/tools cp -Rv $(LOBSTER_ROOT)/lobster/tools/gtest lobster/tools - @python3 -m build + @python3 -m build --wheel diff --git a/packages/lobster-tool-json/Makefile b/packages/lobster-tool-json/Makefile index e3c3fd47..b21950be 100644 --- a/packages/lobster-tool-json/Makefile +++ b/packages/lobster-tool-json/Makefile @@ -2,4 +2,4 @@ package: rm -rf lobster mkdir -p lobster/tools cp -Rv $(LOBSTER_ROOT)/lobster/tools/json lobster/tools - @python3 -m build + @python3 -m build --wheel diff --git a/packages/lobster-tool-python/Makefile b/packages/lobster-tool-python/Makefile index 44038c66..d3df0fe1 100644 --- a/packages/lobster-tool-python/Makefile +++ b/packages/lobster-tool-python/Makefile @@ -2,4 +2,4 @@ package: rm -rf lobster mkdir -p lobster/tools cp -Rv $(LOBSTER_ROOT)/lobster/tools/python lobster/tools - @python3 -m build + @python3 -m build --wheel diff --git a/packages/lobster-tool-python/README.md b/packages/lobster-tool-python/README.md index ebe9acb9..151fb15a 100644 --- a/packages/lobster-tool-python/README.md +++ b/packages/lobster-tool-python/README.md @@ -66,6 +66,14 @@ Please note that the generated output json files always use `PyTest` as framewor } ``` +## Known Issues + +The resulting lobster file does not use the method names, but instead uses the class name together with an integer counter for consecutive methods. +This only affects methods in a class. +It does not affect functions. +For details see [issue 89](https://github.com/bmw-software-engineering/lobster/issues/89). +It will be fixed with the next release. + ## Copyright & License information The copyright holder of LOBSTER is the Bayerische Motoren Werke diff --git a/packages/lobster-tool-trlc/Makefile b/packages/lobster-tool-trlc/Makefile index 1ec3d5ee..7a4401b3 100644 --- a/packages/lobster-tool-trlc/Makefile +++ b/packages/lobster-tool-trlc/Makefile @@ -2,4 +2,4 @@ package: rm -rf lobster mkdir -p lobster/tools cp -Rv $(LOBSTER_ROOT)/lobster/tools/trlc lobster/tools - @python3 -m build + @python3 -m build --wheel diff --git a/pylint3.cfg b/pylint3.cfg index ac968f9b..eb41f88b 100644 --- a/pylint3.cfg +++ b/pylint3.cfg @@ -35,3 +35,6 @@ reports=yes [BASIC] class-rgx=([A-Z]+|[A-Z][a-z]+)(_[A-Z]+|[A-Z][a-z])* good-names=i,j,k,c,f,fd,zf,n,ap,rv,sm,mh,wp,ok,db,nc,cc,nt,ct + +[FORMAT] +max-line-length=88 diff --git a/setup.cfg b/setup.cfg index 15f2b3c4..905048ba 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,3 @@ [pycodestyle] ignore = E203, W504, E221, E251, E129, E266, E127 +max-line-length = 88 diff --git a/test-system/lobster-python/basic.output b/test-system/lobster-python/basic.output index e7751c8a..1f4d4a9c 100644 --- a/test-system/lobster-python/basic.output +++ b/test-system/lobster-python/basic.output @@ -63,4 +63,3 @@ Written output for 3 items to basic.lobster "schema": "lobster-imp-trace", "version": 3 } - diff --git a/test-system/lobster-python/multiple_identical_function_names.output b/test-system/lobster-python/multiple_identical_function_names.output index 31c4f397..b738e651 100644 --- a/test-system/lobster-python/multiple_identical_function_names.output +++ b/test-system/lobster-python/multiple_identical_function_names.output @@ -103,4 +103,3 @@ Written output for 6 items to multiple_identical_function_names.lobster "schema": "lobster-imp-trace", "version": 3 } - diff --git a/test-system/lobster-python/pytest_mark.output b/test-system/lobster-python/pytest_mark.output index df3ff3df..cccd045a 100644 --- a/test-system/lobster-python/pytest_mark.output +++ b/test-system/lobster-python/pytest_mark.output @@ -48,4 +48,3 @@ Written output for 2 items to pytest_mark.lobster "schema": "lobster-act-trace", "version": 3 } - diff --git a/test-unit/test_items.py b/test-unit/test_items.py new file mode 100644 index 00000000..d1673963 --- /dev/null +++ b/test-unit/test_items.py @@ -0,0 +1,517 @@ +import unittest +from unittest.mock import patch, MagicMock,create_autospec +from lobster.items import Tracing_Tag, Tracing_Status, Item, Requirement, Implementation, Activity +from hashlib import sha1 +from lobster.location import Location + +class ItemsTests(unittest.TestCase): + def setUp(self): + self.mock_namespace = "mock_namespace" + self.mock_tag = "mock_tag" + self.mock_framework = "mock_framework" + self.mock_kind = "mock_kind" + self.mock_name = "mock_name" + self.mock_text = "mock_text" + self.mock_status = "active" + self.mock_language = "mock_language" + self.mock_location = create_autospec(Location, instance=True) + self.tracing_tag = Tracing_Tag(self.mock_namespace, self.mock_tag) + self.item = Item(self.tracing_tag, self.mock_location) + self.requirement = Requirement(self.tracing_tag, self.mock_location, self.mock_framework,self. mock_kind, self.mock_name, self.mock_text, self.mock_status) + self.implementation = Implementation(self.tracing_tag, self.mock_location, self.mock_language, self.mock_kind, self.mock_name) + self.activity = Activity(self.tracing_tag, self.mock_location, self.mock_framework, self.mock_kind) + + def set_location_data(self, location_type): + if location_type == "file": + location_data = { + "kind": location_type, + "file": "example.txt" + } + elif location_type == "github": + location_data = { + "kind": location_type, + "gh_root": "https://mysuperserver.com", + "commit": "commit string", + "file": "example.txt", + "line": 1 + } + elif location_type == "codebeamer": + location_data = { + "kind": location_type, + "cb_root": "https://mysuperserver.com", + "tracker": 1, + "item": 1, + "version": 1, + "name": "name string" + } + elif location_type == "void": + location_data = { + "kind": location_type + } + return location_data + +class TestTracingTag(ItemsTests): + def setUp(self): + super().setUp() + + def test_key(self): + expected_key = "mock_namespace mock_tag" + actual_key = self.tracing_tag.key() + + self.assertEqual(expected_key, actual_key) + + def test_to_json(self): + expected_json = "mock_namespace mock_tag" + actual_json = self.tracing_tag.to_json() + + self.assertEqual(expected_json, actual_json) + + @patch('lobster.items.Tracing_Tag.from_text') + def test_from_json(self, mock_from_text): + json_input = "namespace string" + expected_namespace = "namespace" + expected_rest_value = "string" + expected_result = self.tracing_tag + mock_from_text.return_value = expected_result + result = self.tracing_tag.from_json(json_input) + + mock_from_text.assert_called_once_with(expected_namespace, expected_rest_value) + self.assertEqual(result, expected_result) + + def test_from_text_with_version(self): + text = "mock_tag@version" + expected_namespace = "mock_namespace" + expected_tag = "mock_tag" + expected_version = "version" + result = self.tracing_tag.from_text(self.tracing_tag.namespace, text) + + self.assertEqual(result.namespace, expected_namespace) + self.assertEqual(result.tag, expected_tag) + self.assertEqual(result.version, expected_version) + + def test_from_text_without_version(self): + namespace = "namespace" + text = "tag" + expected_tag = "tag" + expected_version = None + result = self.tracing_tag.from_text(namespace, text) + + self.assertEqual(result.namespace, namespace) + self.assertEqual(result.tag, expected_tag) + self.assertEqual(result.version, expected_version) + + def test_from_text_invalid_namespace(self): + with self.assertRaises(AssertionError): + self.tracing_tag.from_text(123, "tag@version") + + def test_from_text_invalid_text(self): + with self.assertRaises(AssertionError): + self.tracing_tag.from_text("namespace", 123) + + def test_hash(self): + hash_val = self.tracing_tag.hash() + hfunc = sha1() + hfunc.update(self.tracing_tag.key().encode("UTF-8")) + expected_hash = hfunc.hexdigest() + + self.assertEqual(hash_val, expected_hash) + +class TestItem(ItemsTests): + def setUp(self): + super().setUp() + + def test_set_level_valid_string(self): + mock_level = "mock_level" + self.item.set_level(mock_level) + + self.assertEqual(self.item.level, mock_level) + + def test_set_level_invalid_string(self): + invalid_level = 10 + + with self.assertRaises(AssertionError): + result = self.item.set_level(invalid_level) + + def test_error(self): + mock_message = "mock_message" + self.item.error(mock_message) + + self.assertIn(mock_message, self.item.messages) + self.assertTrue(self.item.messages) + + @patch("lobster.items.Tracing_Tag.key") + def test_add_tracing_target(self, mock_key): + mock_target = self.tracing_tag + expected_result = "mock_namespace mock_tag" + mock_key.return_value = expected_result + self.item.add_tracing_target(mock_target) + + self.assertIn(mock_target, self.item.unresolved_references) + self.assertIn(expected_result, self.item.unresolved_references_cache) + + def test_perform_source_checks(self): + mock_valid_source_info = {"key": "value"} + try: + self.item.perform_source_checks(mock_valid_source_info) + except AssertionError: + self.fail("perform_source_checks() raised AssertionError unexpectedly!") + + def test_perform_source_checks_with_invalid_type(self): + mock_invalid_source_info = ["not", "a", "dictionary"] + + with self.assertRaises(AssertionError): + self.item.perform_source_checks(mock_invalid_source_info) + + @patch("lobster.items.Tracing_Tag.key") + def test_determine_status_ok(self, mock_key): + self.item.ref_up = [] + self.item.ref_down = [self.tracing_tag] + self.item.just_up = [] + self.item.just_down = [] + self.item.just_global = [] + self.item.messages = [] + self.item.has_error = False + self.item.level = "level1" + expected_result = "mock_namespace mock_tag" + mock_key.return_value = expected_result + config = { + "level1": { + "needs_tracing_up": True, + "needs_tracing_down": True, + "traces": ["level1"], + "breakdown_requirements": [["level1"]] + } + } + stab = { + mock_key() : self.item + } + self.item.determine_status(config, stab) + + self.assertEqual(self.item.tracing_status, Tracing_Status.PARTIAL) + + def test_determine_status_missing_up_reference(self): + mock_namespace = "mock_namespace" + mock_tag = "mock_tag" + self.item.level = "level1" + self.item.just_up = [] + self.item.just_global = [] + self.item.ref_up = [] + config = { + "level1": { + "needs_tracing_up": True, + "needs_tracing_down": False, + "traces": ["level1"], + "breakdown_requirements": [["level1"]] + } + } + stab = { + Tracing_Tag(mock_namespace, mock_tag).key() : self.item + } + self.item.determine_status(config, stab) + + self.assertEqual(self.item.tracing_status, Tracing_Status.MISSING) + self.assertIn("missing up reference", self.item.messages) + + def test_determine_status_missing_down_reference(self): + mock_namespace = "mock_namespace" + mock_tag = "mock_tag" + self.item.level = "level1" + self.item.just_down = [] + self.item.just_global = [] + self.item.ref_down = [] + config = { + "level1": { + "needs_tracing_up": False, + "needs_tracing_down": True, + "traces": ["level1"], + "breakdown_requirements": [["level1"]] + } + } + stab = { + Tracing_Tag(mock_namespace, mock_tag).key() : self.item + } + self.item.determine_status(config, stab) + + self.assertEqual(self.item.tracing_status, Tracing_Status.MISSING) + self.assertIn("missing reference to level1", self.item.messages) + + @patch("lobster.items.Item.set_level") + def test_additional_data_from_json_valid_data(self, mock_set_level): + mock_level = "mock_level" + mock_set_level.return_value = "mock_level" + mock_data = { + "refs": ["mock_namespace mock_tag"], + "ref_up": ["mock refup"], + "ref_down": ["mock refdown"], + "messages": ["message1", "message2"], + "just_up": ["up1"], + "just_down": ["down1"], + "just_global": ["global1"], + "tracing_status": "OK" + } + schema_version = 3 + self.item.additional_data_from_json(mock_level, mock_data, schema_version) + + self.assertEqual(mock_set_level(), mock_level) + self.assertEqual([tag.namespace + " " + tag.tag for tag in self.item.ref_up], ["mock refup"]) + self.assertEqual([tag.namespace + " " + tag.tag for tag in self.item.ref_down], ["mock refdown"]) + self.assertEqual(self.item.messages, ["message1", "message2"]) + self.assertEqual(self.item.just_up, ["up1"]) + self.assertEqual(self.item.just_down, ["down1"]) + self.assertEqual(self.item.just_global, ["global1"]) + self.assertEqual(self.item.tracing_status, Tracing_Status.OK) + + def test_additional_data_from_json_invalid_level(self): + level = 123 + data = {} + schema_version = 3 + + with self.assertRaises(AssertionError): + self.item.additional_data_from_json(level, data, schema_version) + + def test_additional_data_from_json_invalid_data(self): + level = "info" + data = ["invalid", "list"] + schema_version = 3 + + with self.assertRaises(AssertionError): + self.item.additional_data_from_json(level, data, schema_version) + + def test_additional_data_from_json_invalid_schema_version(self): + level = "info" + data = {} + schema_version = 2 + + with self.assertRaises(AssertionError): + self.item.additional_data_from_json(level, data, schema_version) + + @patch("lobster.items.Tracing_Tag.to_json") + def test_to_json(self, mock_to_json): + mock_to_json.return_value = "mock_value" + self.item.name = "mock_name" + self.item.messages = ["message1", "message2"] + self.item.just_up = True + self.item.just_down = False + self.item.just_global = True + self.item.unresolved_references = [self.tracing_tag] + self.item.ref_up = [self.tracing_tag] + self.item.ref_down = [self.tracing_tag] + self.item.tracing_status = MagicMock() + self.item.tracing_status.name = "mock_status" + expected_json = { + "tag": "mock_value", + "location": self.mock_location.to_json(), + "name": "mock_name", + "messages": ["message1", "message2"], + "just_up": True, + "just_down": False, + "just_global": True, + "refs": ["mock_value"], + "ref_up": ["mock_value"], + "ref_down": ["mock_value"], + "tracing_status": "mock_status" + } + result = self.item.to_json() + + self.assertEqual(result, expected_json) + +class TestRequirement(ItemsTests): + def setUp(self): + super().setUp() + + @patch("lobster.items.Item.to_json") + def test_to_json(self, mock_super_to_json): + mock_super_to_json.return_value = { + "item_property": "item_value" + } + expected_result = { + "item_property": "item_value", + "framework": "mock_framework", + "kind": "mock_kind", + "text": "mock_text", + "status": "active" + } + result = self.requirement.to_json() + + mock_super_to_json.assert_called_once_with() + self.assertEqual(result, expected_result) + + @patch("lobster.items.Item.error") + def test_perform_source_checks_valid_status(self, mock_super_error): + mock_error_message = None + mock_super_error.return_value = mock_error_message + source_info = { + "valid_status": ["active", "inactive"] + } + self.requirement.perform_source_checks(source_info) + + self.assertIsNone(mock_super_error()) + + @patch("lobster.items.Item.error") + def test_perform_source_checks_invalid_status(self, mock_super_error): + mock_error_message = "status is active, expected closed or inactive" + mock_super_error.return_value = mock_error_message + source_info = { + "valid_status": ["inactive", "closed"] + } + self.requirement.perform_source_checks(source_info) + expected_error_message = "status is active, expected closed or inactive" + + self.assertEqual(mock_super_error(), expected_error_message) + + def test_perform_source_checks_invalid_source_info(self): + invalid_source_info = ["invalid", "list"] + + with self.assertRaises(AssertionError): + self.requirement.perform_source_checks(invalid_source_info) + + @patch("lobster.items.Tracing_Tag.from_json") + def test_from_json(self, mock_from_json): + mock_level = "mock_level" + mock_schema_version = 3 + mock_from_json.return_value = self.tracing_tag + for location_type in ["file", "github", "codebeamer", "void"]: + with self.subTest(location_type): + location_data = self.set_location_data(location_type) + mock_data = { + "tag": self.tracing_tag, + "location": location_data, + "framework": "framework_data", + "kind": "kind_data", + "name": "name_data", + "text": "text_data", + "status": "status_data" + } + result = self.requirement.from_json(mock_level, mock_data, mock_schema_version) + + self.assertEqual(result.tag, self.tracing_tag) + self.assertEqual(result.framework, "framework_data") + self.assertEqual(result.kind, "kind_data") + self.assertEqual(result.name, "name_data") + self.assertEqual(result.text, "text_data") + self.assertEqual(result.status, "status_data") + if location_type == "file": + self.assertEqual(result.location.filename, location_data["file"]) + elif location_type == "github": + self.assertEqual(result.location.gh_root, location_data["gh_root"]) + self.assertEqual(result.location.commit, location_data["commit"]) + self.assertEqual(result.location.filename, location_data["file"]) + self.assertEqual(result.location.line, location_data["line"]) + elif location_type == "codebeamer": + self.assertEqual(result.location.cb_root, location_data["cb_root"]) + self.assertEqual(result.location.tracker, location_data["tracker"]) + self.assertEqual(result.location.item, location_data["item"]) + self.assertEqual(result.location.version, location_data["version"]) + self.assertEqual(result.location.name, location_data["name"]) + +class TestImplementation(ItemsTests): + def setUp(self): + super().setUp() + + @patch("lobster.items.Item.to_json") + def test_to_json(self, mock_super_to_json): + mock_super_to_json.return_value = { + "item_property": "item_value" + } + expected_result = { + "item_property": "item_value", + "language": "mock_language", + "kind": "mock_kind" + } + result = self.implementation.to_json() + + mock_super_to_json.assert_called_once_with() + self.assertEqual(result, expected_result) + + @patch("lobster.items.Tracing_Tag.from_json") + def test_from_json(self, mock_from_json): + mock_level = "mock_level" + mock_schema_version = 3 + mock_from_json.return_value = self.tracing_tag + for location_type in ["file", "github", "codebeamer", "void"]: + with self.subTest(location_type): + location_data = self.set_location_data(location_type) + mock_data = { + "tag": self.tracing_tag, + "location": location_data, + "language": "Python", + "kind": "kind_data", + "name": "name_data", + } + result = self.implementation.from_json(mock_level, mock_data, mock_schema_version) + + self.assertEqual(result.tag, self.tracing_tag) + self.assertEqual(result.language, "Python") + self.assertEqual(result.kind, "kind_data") + self.assertEqual(result.name, "name_data") + if location_type == "file": + self.assertEqual(result.location.filename, location_data["file"]) + elif location_type == "github": + self.assertEqual(result.location.gh_root, location_data["gh_root"]) + self.assertEqual(result.location.commit, location_data["commit"]) + self.assertEqual(result.location.filename, location_data["file"]) + self.assertEqual(result.location.line, location_data["line"]) + elif location_type == "codebeamer": + self.assertEqual(result.location.cb_root, location_data["cb_root"]) + self.assertEqual(result.location.tracker, location_data["tracker"]) + self.assertEqual(result.location.item, location_data["item"]) + self.assertEqual(result.location.version, location_data["version"]) + self.assertEqual(result.location.name, location_data["name"]) + +class TestActivity(ItemsTests): + def setUp(self): + super().setUp() + + @patch("lobster.items.Item.to_json") + def test_to_json(self, mock_super_to_json): + mock_super_to_json.return_value = { + "item_property": "item_value" + } + expected_result = { + "item_property": "item_value", + "framework": "mock_framework", + "kind": "mock_kind", + "status": None + } + result = self.activity.to_json() + + mock_super_to_json.assert_called_once_with() + self.assertEqual(result, expected_result) + + @patch("lobster.items.Tracing_Tag.from_json") + def test_from_json(self, mock_from_json): + mock_level = "mock_level" + mock_schema_version = 3 + mock_from_json.return_value = self.tracing_tag + for location_type in ["file", "github", "codebeamer", "void"]: + with self.subTest(location_type): + location_data = self.set_location_data(location_type) + mock_data = { + "tag": self.tracing_tag, + "location": location_data, + "framework": "framework_data", + "kind": "kind_data", + "status": None + } + result = self.activity.from_json(mock_level, mock_data, mock_schema_version) + + self.assertEqual(result.tag, self.tracing_tag) + self.assertEqual(result.framework, "framework_data") + self.assertEqual(result.kind, "kind_data") + self.assertEqual(result.status, None) + if location_type == "file": + self.assertEqual(result.location.filename, location_data["file"]) + elif location_type == "github": + self.assertEqual(result.location.gh_root, location_data["gh_root"]) + self.assertEqual(result.location.commit, location_data["commit"]) + self.assertEqual(result.location.filename, location_data["file"]) + self.assertEqual(result.location.line, location_data["line"]) + elif location_type == "codebeamer": + self.assertEqual(result.location.cb_root, location_data["cb_root"]) + self.assertEqual(result.location.tracker, location_data["tracker"]) + self.assertEqual(result.location.item, location_data["item"]) + self.assertEqual(result.location.version, location_data["version"]) + self.assertEqual(result.location.name, location_data["name"]) + +if __name__ == '__main__': + unittest.main()