Coverage for lobster/common/yaml_policy_reader.py: 88%
72 statements
« prev ^ index » next coverage.py v7.17.3, created at 2026-11-13 15:05 +0000
« prev ^ index » next coverage.py v7.17.3, created at 2026-11-13 15:05 +0000
1#!/usr/bin/env python3
2#
3# LOBSTER - Lightweight Open BMW Software Traceability Evidence Report
4# Copyright (C) 2026 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU Affero General Public License as
8# published by the Free Software Foundation, either version 3 of the
9# License, or (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful, but
12# WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14# Affero General Public License for more details.
15#
16# You should have received a copy of the GNU Affero General Public
17# License along with this program. If not, see
18# <https://www.gnu.org/licenses/>.
20# Reads a YAML tracing policy file into a RawPolicy, validating it against
21# lobster/common/tracing_policy.yamale first (same precedent as
22# lobster/tools/trlc/lobster_trlc_config.py).
24from pathlib import Path
25from typing import Optional
27import yaml
28from yamale import YamaleError
30from lobster.common.exceptions import LOBSTER_Exception
31from lobster.common.location import File_Reference
32from lobster.common.raw_policy import (
33 RawLevel, RawPolicy, RawRequiresCandidate, RawSource, RawTraceTo,
34)
35from lobster.common.yaml_validation import validate_yaml_file
37SCHEMA_FILE = Path(__file__).parent / "tracing_policy.yamale"
40# pylint: disable-next=too-many-ancestors
41class UniqueKeyLoader(yaml.SafeLoader):
42 """Safe loader that rejects duplicate mapping keys instead of keeping the last."""
44 def construct_mapping(self, node, deep=False):
45 seen = set()
46 for key_node, _ in node.value:
47 key = self.construct_object(key_node, deep=True)
48 if key in seen: 48 ↛ 49line 48 didn't jump to line 49 because the condition on line 48 was never true
49 raise yaml.constructor.ConstructorError(
50 None, None, f"duplicate key {key!r}", key_node.start_mark)
51 seen.add(key)
52 return super().construct_mapping(node, deep)
55def _line_of_path(filename: str, path: str) -> Optional[int]:
56 """Return the 1-based line of a yamale error path like 'levels.0.kind'.
58 Falls back to the deepest node that exists, e.g. for missing keys.
59 """
60 with open(filename, "r", encoding="UTF-8") as f:
61 node = yaml.compose(f, Loader=yaml.SafeLoader)
62 if node is None: 62 ↛ 63line 62 didn't jump to line 63 because the condition on line 62 was never true
63 return None
64 for part in path.split("."):
65 child = None
66 if isinstance(node, yaml.MappingNode):
67 for key_node, value_node in node.value: 67 ↛ 75line 67 didn't jump to line 75 because the loop on line 67 didn't complete
68 if key_node.value == part:
69 child = value_node
70 break
71 elif isinstance(node, yaml.SequenceNode) and part.isdigit(): 71 ↛ 75line 71 didn't jump to line 75 because the condition on line 71 was always true
72 index = int(part)
73 if index < len(node.value): 73 ↛ 75line 73 didn't jump to line 75 because the condition on line 73 was always true
74 child = node.value[index]
75 if child is None: 75 ↛ 76line 75 didn't jump to line 76 because the condition on line 75 was never true
76 break
77 node = child
78 return node.start_mark.line + 1
81def _describe_error(filename: str, err: Exception) -> str:
82 if isinstance(err, yaml.YAMLError): 82 ↛ 83line 82 didn't jump to line 83 because the condition on line 82 was never true
83 mark = getattr(err, "problem_mark", None)
84 where = f"{filename}:{mark.line + 1}" if mark else filename
85 return f"{where}: invalid tracing policy: {err}"
87 messages = []
88 for result in err.results:
89 for error in result.errors:
90 path, _, text = str(error).partition(": ")
91 line = _line_of_path(filename, path) if text else None
92 where = f"{filename}:{line}" if line else filename
93 messages.append(f"{where}: invalid tracing policy: {error}")
94 return "\n".join(messages) or f"{filename}: invalid tracing policy: {err}"
97def read_yaml_policy(filename: str) -> RawPolicy:
98 if not Path(filename).is_file():
99 raise FileNotFoundError(f"Config file not found: {filename}")
101 try:
102 with open(filename, "r", encoding="UTF-8") as f:
103 data = yaml.load(f, Loader=UniqueKeyLoader)
104 validate_yaml_file(schema_file=SCHEMA_FILE, filename=filename)
105 except (YamaleError, yaml.YAMLError) as err:
106 raise LOBSTER_Exception(_describe_error(filename, err)) from err
108 loc = File_Reference(filename)
109 levels = []
110 for level_data in data.get("levels", []):
111 level = RawLevel(name=level_data["name"],
112 name_loc=loc,
113 kind=level_data["kind"])
114 for source_file in level_data.get("source") or []:
115 level.source.append(RawSource(file=source_file, loc=loc))
116 for target in level_data.get("trace-to") or []:
117 level.trace_to.append(RawTraceTo(target=target, loc=loc))
118 for or_group in level_data.get("requires") or []:
119 level.requires.append(
120 [RawRequiresCandidate(name=name, loc=loc)
121 for name in or_group])
122 levels.append(level)
124 return RawPolicy(levels=levels)