Coverage for lobster/common/policy_builder.py: 87%
43 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-09-22 04:43 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-09-22 04:43 +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# Format-agnostic validation + construction layer: turns a RawPolicy (built
21# by a format-specific reader) into the OrderedDict[str, LevelDefinition]
22# consumed by lobster/common/report.py. This is where all semantic checks
23# (duplicate names, missing source files, self-trace, unknown trace
24# targets/requires levels) live, so they apply identically regardless of
25# which config file format was parsed.
27import os.path
28import collections
29from typing import OrderedDict
31from lobster.common.errors import LOBSTER_Error
32from lobster.common.level_definition import LevelDefinition
33from lobster.common.raw_policy import RawPolicy
36def build_tracing_policy(
37 raw_policy: RawPolicy,
38) -> OrderedDict[str, LevelDefinition]:
39 levels = collections.OrderedDict()
41 # First pass: create every level up front, so later passes can refer to
42 # a level regardless of whether it is declared before or after the
43 # level referencing it.
44 for raw_level in raw_policy.levels:
45 if raw_level.name in levels: 45 ↛ 46line 45 didn't jump to line 46 because the condition on line 45 was never true
46 raise LOBSTER_Error(raw_level.name_loc, "duplicate declaration")
47 levels[raw_level.name] = LevelDefinition(
48 name=raw_level.name,
49 kind=raw_level.kind,
50 )
52 # Second pass: validate and populate source files and trace-to targets.
53 for raw_level in raw_policy.levels:
54 item = levels[raw_level.name]
56 for raw_source in raw_level.source:
57 if not os.path.isfile(raw_source.file):
58 raise LOBSTER_Error(raw_source.loc,
59 f"cannot find file {raw_source.file}")
60 item.source.append({"file": raw_source.file})
62 for raw_trace in raw_level.trace_to:
63 if raw_trace.target == raw_level.name: 63 ↛ 64line 63 didn't jump to line 64 because the condition on line 63 was never true
64 raise LOBSTER_Error(raw_trace.loc, "cannot trace to yourself")
65 if raw_trace.target not in levels: 65 ↛ 66line 65 didn't jump to line 66 because the condition on line 65 was never true
66 raise LOBSTER_Error(raw_trace.loc,
67 f"unknown item {raw_trace.target}")
68 levels[raw_trace.target].needs_tracing_down = True
69 item.traces.append(raw_trace.target)
70 item.needs_tracing_up = True
72 # Third pass: resolve "requires" links now that every level's `traces`
73 # list (populated above) is complete.
74 for raw_level in raw_policy.levels:
75 item = levels[raw_level.name]
76 item.breakdown_requirements = []
77 if raw_level.requires:
78 for or_group in raw_level.requires:
79 new_or_group = []
80 for alt in or_group:
81 if alt.name not in levels: 81 ↛ 82line 81 didn't jump to line 82 because the condition on line 81 was never true
82 raise LOBSTER_Error(alt.loc, f"unknown level {alt.name}")
83 if item.name not in levels[alt.name].traces: 83 ↛ 84line 83 didn't jump to line 84 because the condition on line 83 was never true
84 raise LOBSTER_Error(
85 alt.loc,
86 f"{alt.name} cannot trace to {item.name} items",
87 )
88 new_or_group.append(alt.name)
89 item.breakdown_requirements.append(new_or_group)
90 else:
91 for src in levels.values():
92 if item.name in src.traces:
93 item.breakdown_requirements.append([src.name])
95 return levels