From ea91bb493a14036c56b6bb7490603a6e841ec55a Mon Sep 17 00:00:00 2001 From: TannazVhdBMWExt Date: Fri, 27 Sep 2024 12:10:24 +0200 Subject: [PATCH] lobster-report allow optional up-linking in tracing policies lobster.config accepts multiline keywords which considered to be "and" the "requires" keyword has been changed to "trace from" Resolves https://github.com/bmw-software-engineering/lobster/issues/10 --- CHANGELOG.md | 5 ++ docs/config_files.md | 24 ++++-- lobster/config/parser.py | 96 +++++++++------------ lobster/items.py | 41 +++++---- lobster/tools/core/html_report.py | 5 +- packages/lobster-core/README.md | 30 +++++++ test-unit/lobster-core/data/data1.lobster | 27 ++++++ test-unit/lobster-core/data/data2.lobster | 27 ++++++ test-unit/lobster-core/data/data3.lobster | 50 +++++++++++ test-unit/lobster-core/data/data4.lobster | 25 ++++++ test-unit/lobster-core/data/data5.lobster | 25 ++++++ test-unit/lobster-core/data/lobster1.config | 39 +++++++++ test-unit/lobster-core/test_core.py | 33 +++++++ 13 files changed, 349 insertions(+), 78 deletions(-) create mode 100644 test-unit/lobster-core/data/data1.lobster create mode 100644 test-unit/lobster-core/data/data2.lobster create mode 100644 test-unit/lobster-core/data/data3.lobster create mode 100644 test-unit/lobster-core/data/data4.lobster create mode 100644 test-unit/lobster-core/data/data5.lobster create mode 100644 test-unit/lobster-core/data/lobster1.config create mode 100644 test-unit/lobster-core/test_core.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b078200f..283995a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Changelog +### 1.0.0-dev + +* The `lobster-report` tool now supports optional "OR" in uplinks (`trace to`). In + the `lobster.config` file the keyword `requires` has been changed to `trace from`. + ### 0.9.18-dev * The `lobster-python` tool adds the counter logic to the function diff --git a/docs/config_files.md b/docs/config_files.md index cca8871e..b25e555d 100644 --- a/docs/config_files.md +++ b/docs/config_files.md @@ -69,11 +69,11 @@ requirements "Requirements" { implementation "Code" { source: "python.lobster"; - trace to: "Requirements"; + trace to: "Requirements" or "Code"; } ``` -#### requires +#### trace from Sometimes you might want alternatives. For example we could have two possibly ways to verify a requirement: by proof or by test. If we just @@ -101,15 +101,15 @@ activity "Formal Proof" { ``` Then we would get lots of errors as the tooling would require a -requirement to be broken down into all three. The `requires` +requirement to be broken down into all three. The `trace from` configuration can help here: ``` requirements "Requirements" { source: "trlc.lobster"; - requires: "Code"; - requires: "Unit Test" or "Formal Proof"; + trace from: "Code"; + trace from: "Unit Test" or "Formal Proof"; } ``` @@ -118,7 +118,17 @@ link to code, and either a link to a test or a link to a proof. **Note:** Don't forget that the `trace to` configuration is always mandatory. -You cannot build links with a configuration that uses only `requires`. +You cannot build links with a configuration that uses only `trace from`. + +**Note:** +Multiple lines of the same rule are treated as if they had an `and` between them. + +**Note:** +`or` is supported in each line, to specify that only one target is needed, not + all of them. + +**Note:** +Self-references are also allowed. # Examples @@ -148,7 +158,7 @@ our own custom LOBSTER trace tool). requirements "System Requirements" { source: "cbtrace.lobster" with valid_status {"Valid"}; - requires: "Integration Tests" or "Analysis"; + trace from: "Integration Tests" or "Analysis"; } requirements "Software Requirements" { diff --git a/lobster/config/parser.py b/lobster/config/parser.py index 4b83af00..6159f1d0 100644 --- a/lobster/config/parser.py +++ b/lobster/config/parser.py @@ -102,13 +102,11 @@ def parse_level_declaration(self): "duplicate declaration") item = { - "name" : level_name, - "kind" : level_kind, - "traces" : [], - "source" : [], - "needs_tracing_up" : False, - "needs_tracing_down" : False, - "raw_trace_requirements" : [] + "name" : level_name, + "kind" : level_kind, + "source" : [], + "trace_to" : [], + "trace_from" : [] } self.levels[level_name] = item @@ -175,39 +173,40 @@ def parse_level_declaration(self): elif self.peek("KEYWORD", "trace"): self.match("KEYWORD", "trace") - self.match("KEYWORD", "to") - self.match("COLON") - self.match("STRING") - if self.ct.value() == level_name: - self.error(self.ct.loc, - "cannot trace to yourself") - elif self.ct.value() not in self.levels: - self.error(self.ct.loc, - "unknown item %s" % self.ct.value()) - else: - self.levels[self.ct.value()]["needs_tracing_down"] = True - item["traces"].append(self.ct.value()) - item["needs_tracing_up"] = True - self.match("SEMI") + if self.peek("KEYWORD", "to"): + self.match("KEYWORD", "to") + self.match("COLON") - elif self.peek("KEYWORD", "requires"): - self.match("KEYWORD", "requires") - self.match("COLON") + req_list = [] + self.match("STRING") + req_list.append(self.ct.value()) - req_list = [] + while self.peek("KEYWORD", "or"): + self.match("KEYWORD", "or") + self.match("STRING") + req_list.append(self.ct.value()) - self.match("STRING") - req_list.append(self.ct) + self.match("SEMI") + + item["trace_to"].append(req_list) - while self.peek("KEYWORD", "or"): - self.match("KEYWORD", "or") + elif self.peek("KEYWORD", "from"): + self.match("KEYWORD", "from") + self.match("COLON") + + req_list = [] self.match("STRING") - req_list.append(self.ct) + req_list.append(self.ct.value()) - self.match("SEMI") + while self.peek("KEYWORD", "or"): + self.match("KEYWORD", "or") + self.match("STRING") + req_list.append(self.ct.value()) - item["raw_trace_requirements"].append(req_list) + self.match("SEMI") + + item["trace_from"].append(req_list) else: self.error(self.nt.loc, @@ -219,28 +218,19 @@ def parse_level_declaration(self): def load(mh, file_name): parser = Parser(mh, file_name) ast = parser.parse() - - # Resolve requires links now + item_names = list(ast.keys()) for item in ast.values(): - item["breakdown_requirements"] = [] - if len(item["raw_trace_requirements"]) > 0: - for chain in item["raw_trace_requirements"]: - new_chain = [] - for tok in chain: - if tok.value() not in ast: - mh.error(tok.loc, "unknown level %s" % tok.value()) - if item["name"] not in ast[tok.value()]["traces"]: - mh.error(tok.loc, - "%s cannot trace to %s items" % - (tok.value(), - item["name"])) - new_chain.append(tok.value()) - item["breakdown_requirements"].append(new_chain) - else: - for src in ast.values(): - if item["name"] in src["traces"]: - item["breakdown_requirements"].append([src["name"]]) - del item["raw_trace_requirements"] + if len(item["trace_to"]) > 0: + for trace_to in item["trace_to"]: + if not set(trace_to).issubset(item_names): + mh.error(set(trace_to).issubset(item_names), + "cannot trace to %s items" % ",".join(trace_to)) + + if len(item["trace_from"]) > 0: + for trace_from in item["trace_from"]: + if not set(trace_from).issubset(item_names): + mh.error("cannot trace from %s items" % + ",".join(trace_from)) return ast diff --git a/lobster/items.py b/lobster/items.py index 7fb1774a..0fd0d9fb 100644 --- a/lobster/items.py +++ b/lobster/items.py @@ -134,31 +134,42 @@ def determine_status(self, config, stab): level = config[self.level] has_up_ref = len(self.ref_up) > 0 + has_down_ref = len(self.ref_down) > 0 has_just_up = len(self.just_up) > 0 or len(self.just_global) > 0 has_just_down = len(self.just_down) > 0 or len(self.just_global) > 0 has_init_errors = len(self.messages) > 0 # Check up references ok_up = True - if level["needs_tracing_up"]: + if level["trace_to"]: if not has_up_ref and not has_just_up: ok_up = False self.messages.append("missing up reference") + for trace_to in level["trace_to"]: + # and + if not ok_up: + break + + # or + refs_levels = [stab[ref.key()].level for ref in self.ref_up] + ok_up = len(set(refs_levels).intersection(set(trace_to))) > 0 + # Check set of down references ok_down = True - if level["needs_tracing_down"]: - has_trace = {name : False - for name in config - if self.level in config[name]["traces"]} - for ref in self.ref_down: - has_trace[stab[ref.key()].level] = True - for chain in level["breakdown_requirements"]: - if not any(has_trace[src] for src in chain) and \ - not has_just_down: - ok_down = False - self.messages.append("missing reference to %s" % - " or ".join(sorted(chain))) + if level["trace_from"]: + if not has_down_ref and not has_just_down: + ok_down = False + self.messages.append("missing down reference") + + for trace_from in level["trace_from"]: + # and + if not ok_down: + break + # or + refs_levels = [stab[ref.key()].level for ref in self.ref_down] + ok_down = len(set(refs_levels) + .intersection(set(trace_from))) > 0 # Set status if self.has_error: @@ -168,9 +179,7 @@ def determine_status(self, config, stab): self.tracing_status = Tracing_Status.JUSTIFIED else: self.tracing_status = Tracing_Status.OK - elif (ok_up or ok_down) and \ - level["needs_tracing_up"] and \ - level["needs_tracing_down"]: + elif ok_up or ok_down: self.tracing_status = Tracing_Status.PARTIAL else: self.tracing_status = Tracing_Status.MISSING diff --git a/lobster/tools/core/html_report.py b/lobster/tools/core/html_report.py index a51e564c..11417386 100755 --- a/lobster/tools/core/html_report.py +++ b/lobster/tools/core/html_report.py @@ -135,10 +135,11 @@ def create_policy_diagram(doc, report, dot): for level in report.config.values(): source = name_hash(level["name"]) - for target in map(name_hash, level["traces"]): + all_trace_tos = sum(level["trace_to"], []) + for target in map(name_hash, all_trace_tos): # Not a mistake; we want to show the tracing down, whereas # in the config file we indicate how we trace up. - graph += ' n_%s -> n_%s;\n' % (target, source) + graph += ' n_%s -> n_%s;\n' % (source, target) graph += "}\n" with tempfile.TemporaryDirectory() as tmp_dir: diff --git a/packages/lobster-core/README.md b/packages/lobster-core/README.md index 27a28bc1..799405b7 100644 --- a/packages/lobster-core/README.md +++ b/packages/lobster-core/README.md @@ -14,11 +14,41 @@ You can generate a report linking everything together with `lobster-report`. The report is in JSON, but you can generate more readable versions of it with additional tools: +* `lobster-report`: Creation of JSON format report from provided config file * `lobster-online-report`: Preprocess a JSON report to contain github references instead of local file references * `lobster-html-report`: Generate a HTML report * `lobster-ci-report`: Generate a compiler-message like output, useful for CI +## Configuration +The `lobster-report` tool works with a config file. In it you can declare the +upstream, downstream and source of tracing policies. + +The configuration file follows the following rules: + +* This file is able to get multiple `trace to` and `trace from` keys. +* `trace to` specifies all the outgoing targets. +* `trace from` specifies all the incoming targets. +* Multiple lines of the same rule are treated as if they had an `and` between them. +* `or` is supported in each line, to specify that only one target is needed, not + all of them. +* Self-references are also allowed. + + +``` +requirements "Requirements" { + source: "file1.lobster"; +} + +requirements "System Requirements" { + source: "file2.lobster"; + trace to: "Models" or "Software Requirements"; + trace to: "Requirements"; + trace from: "System Requirements" or "Integration Tests"; + trace from: "Unit Tests"; +} +``` + ## Requirements * `lobster-online-report`: This tool needs `git 1.7.8` or higher to support git submodules diff --git a/test-unit/lobster-core/data/data1.lobster b/test-unit/lobster-core/data/data1.lobster new file mode 100644 index 00000000..a9a1d400 --- /dev/null +++ b/test-unit/lobster-core/data/data1.lobster @@ -0,0 +1,27 @@ +{ + "data": [ + { + "tag": "req 17045@11", + "location": { + "kind": "codebeamer", + "cb_root": "https://codebeamer.company.net/", + "tracker": 1111, + "item": 1111, + "version": 11, + "name": "test1" + }, + "name": "test1", + "messages": [], + "just_up": [], + "just_down": [], + "just_global": [], + "framework": "codebeamer", + "kind": "Technical Requirement", + "text": null, + "status": "Valid" + } + ], + "generator": "lobster_codebeamer", + "schema": "lobster-req-trace", + "version": 4 +} \ No newline at end of file diff --git a/test-unit/lobster-core/data/data2.lobster b/test-unit/lobster-core/data/data2.lobster new file mode 100644 index 00000000..9813a4f7 --- /dev/null +++ b/test-unit/lobster-core/data/data2.lobster @@ -0,0 +1,27 @@ +{ + "data": [ + { + "tag": "req 17044@22", + "location": { + "kind": "codebeamer", + "cb_root": "https://codebeamer.company.net/", + "tracker": 2222, + "item": 2222, + "version": 22, + "name": "test" + }, + "name": "test", + "messages": [], + "just_up": [], + "just_down": [], + "just_global": [], + "framework": "codebeamer", + "kind": "Technical Requirement", + "text": null, + "status": "Valid" + } + ], + "generator": "lobster_codebeamer", + "schema": "lobster-req-trace", + "version": 4 +} \ No newline at end of file diff --git a/test-unit/lobster-core/data/data3.lobster b/test-unit/lobster-core/data/data3.lobster new file mode 100644 index 00000000..70a06cac --- /dev/null +++ b/test-unit/lobster-core/data/data3.lobster @@ -0,0 +1,50 @@ +{ + "data": [ + { + "tag": "req test3.trlc", + "location": { + "kind": "file", + "file": "./main.trlc", + "line": 3, + "column": 13 + }, + "name": "test3.trlc", + "messages": [], + "just_up": [], + "just_down": [], + "just_global": [], + "ref_down": [ + "req 17044@22" + ], + "ref_up": [ + "req 17045@11", + "req test33.lobster" + ], + "framework": "TRLC", + "kind": "Requirement", + "text": "Treat Requirements Like Code", + "status": null + }, + { + "tag": "req test33.lobster", + "location": { + "kind": "file", + "file": "./main.trlc", + "line": 7, + "column": 13 + }, + "name": "test33.lobster", + "messages": [], + "just_up": [], + "just_down": [], + "just_global": [], + "framework": "TRLC", + "kind": "Requirement", + "text": "test", + "status": null + } + ], + "generator": "lobster-trlc", + "schema": "lobster-req-trace", + "version": 4 +} diff --git a/test-unit/lobster-core/data/data4.lobster b/test-unit/lobster-core/data/data4.lobster new file mode 100644 index 00000000..745e7c70 --- /dev/null +++ b/test-unit/lobster-core/data/data4.lobster @@ -0,0 +1,25 @@ +{ + "data": [ + { + "tag": "req test4.trlc", + "location": { + "kind": "file", + "file": "./main.trlc", + "line": 4, + "column": 44 + }, + "name": "test4.trlc", + "messages": [], + "just_up": [], + "just_down": [], + "just_global": [], + "framework": "TRLC", + "kind": "Requirement", + "text": "Treat Requirements Like Code", + "status": null + } + ], + "generator": "lobster-trlc", + "schema": "lobster-req-trace", + "version": 4 +} diff --git a/test-unit/lobster-core/data/data5.lobster b/test-unit/lobster-core/data/data5.lobster new file mode 100644 index 00000000..b9f9810a --- /dev/null +++ b/test-unit/lobster-core/data/data5.lobster @@ -0,0 +1,25 @@ +{ + "data": [ + { + "tag": "req test5.trlc", + "location": { + "kind": "file", + "file": "./main.trlc", + "line": 4, + "column": 44 + }, + "name": "test5.trlc", + "messages": [], + "just_up": [], + "just_down": [], + "just_global": [], + "framework": "TRLC", + "kind": "Requirement", + "text": "Treat Requirements Like Code", + "status": null + } + ], + "generator": "lobster-trlc", + "schema": "lobster-req-trace", + "version": 4 +} diff --git a/test-unit/lobster-core/data/lobster1.config b/test-unit/lobster-core/data/lobster1.config new file mode 100644 index 00000000..9b7683fe --- /dev/null +++ b/test-unit/lobster-core/data/lobster1.config @@ -0,0 +1,39 @@ +requirements "Laws" { + source: "data/data4.lobster"; +} + +requirements "System Requirements" { + source: "data/data3.lobster"; + trace to: "Implementation" or "System Requirements"; + trace from: "Software Requirements"; +} + +requirements "Software Requirements" { + source: "data/data2.lobster"; + trace to: "Software Requirements" or "System Requirements"; + trace to: "Laws"; + trace from: "Software Requirements" or "Implementation"; + trace from: "Component Test"; +} + +requirements "Another Test" { + source: "data/data5.lobster"; + trace to: "Software Requirements" or "Another Test"; + trace to: "Software Requirements"; +} + +requirements "Implementation" { + source: "data/data1.lobster"; + trace to: "Unit Test"; +} + +requirements "Unit Test" { + trace to: "Component Test"; + trace to: "Software Requirements"; +} + +requirements "Component Test" { + trace to: "Software Requirements"; +} + +requirements "Empty" {} diff --git a/test-unit/lobster-core/test_core.py b/test-unit/lobster-core/test_core.py new file mode 100644 index 00000000..2efa6eff --- /dev/null +++ b/test-unit/lobster-core/test_core.py @@ -0,0 +1,33 @@ +import unittest +from pathlib import Path + +from lobster.report import Report + + +class CoreTest(unittest.TestCase): + + def setUp(self): + self.lobster_config1 = str(Path('./data', 'lobster1.config')) + + def test_lobster_report_parse_config(self): + report = Report() + + report.parse_config(self.lobster_config1) + + self.assertEqual(8, len(report.config)) + self.assertEqual(8, len(report.coverage)) + self.assertEqual(6, len(report.items)) + + expect = {'Laws': {'items': 1, 'ok': 1, 'coverage': 100.0}, + 'System Requirements': {'items': 2, 'ok': 1, 'coverage': 50.0}, + 'Software Requirements': {'items': 1, 'ok': 0, 'coverage': 0.0}, + 'Implementation': {'items': 1, 'ok': 0, 'coverage': 0.0}, + 'Unit Test': {'items': 0, 'ok': 0, 'coverage': 100.0}, + 'Component Test': {'items': 0, 'ok': 0, 'coverage': 100.0}, + 'Empty': {'coverage': 100.0, 'items': 0, 'ok': 0}, + 'Another Test': {'coverage': 0.0, 'items': 1, 'ok': 0}} + self.assertEqual(expect, report.coverage) + + +if __name__ == '__main__': + unittest.main()