trlcdoc 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trlcdoc/__init__.py +37 -0
- trlcdoc/__main__.py +36 -0
- trlcdoc/cli.py +186 -0
- trlcdoc/config.py +433 -0
- trlcdoc/convert.py +95 -0
- trlcdoc/converters/LICENSE +17 -0
- trlcdoc/converters/create_test_report_in_markdown.py +137 -0
- trlcdoc/converters/create_test_report_in_rst.py +171 -0
- trlcdoc/converters/generic_rsl_markdown_converter.py +131 -0
- trlcdoc/converters/generic_rsl_rst_converter.py +131 -0
- trlcdoc/converters/image_processing.py +108 -0
- trlcdoc/converters/renderCfg.json +29 -0
- trlcdoc/converters/req2markdown.py +67 -0
- trlcdoc/converters/req2rst.py +67 -0
- trlcdoc/converters/tc2markdown.py +69 -0
- trlcdoc/converters/tc2rst.py +69 -0
- trlcdoc/converters/translation.json +31 -0
- trlcdoc/html.py +369 -0
- trlcdoc/plantuml.py +107 -0
- trlcdoc/schema/trlcdoc.schema.json +234 -0
- trlcdoc/sphinx/_static/custom.css +26 -0
- trlcdoc/sphinx/_static/s5defs.txt +68 -0
- trlcdoc/sphinx/conf.py +178 -0
- trlcdoc/sphinx_links.py +196 -0
- trlcdoc/testrun.py +188 -0
- trlcdoc/trace.py +212 -0
- trlcdoc/util.py +241 -0
- trlcdoc-0.3.0.dist-info/METADATA +110 -0
- trlcdoc-0.3.0.dist-info/RECORD +34 -0
- trlcdoc-0.3.0.dist-info/WHEEL +5 -0
- trlcdoc-0.3.0.dist-info/entry_points.txt +2 -0
- trlcdoc-0.3.0.dist-info/licenses/LICENSE +674 -0
- trlcdoc-0.3.0.dist-info/licenses/trlcdoc/converters/LICENSE +17 -0
- trlcdoc-0.3.0.dist-info/top_level.txt +1 -0
trlcdoc/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""trlcdoc - configurable documentation tooling for TRLC based projects.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# trlcdoc - A utility for producing project documentation from TRLC and LOBSTER contents.
|
|
6
|
+
# Copyright (c) 2026 - 2026 Norbert Schulz github@schulznorbert.de
|
|
7
|
+
#
|
|
8
|
+
# This file is part of trlcdoc program.
|
|
9
|
+
#
|
|
10
|
+
# The trlcdoc program is free software: you can redistribute it and/or modify it under
|
|
11
|
+
# the terms of the GNU General Public License as published by the Free Software Foundation,
|
|
12
|
+
# either version 3 of the License, or (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# The trlcdoc program is distributed in the hope that it will be useful, but
|
|
15
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
17
|
+
#
|
|
18
|
+
# You should have received a copy of the GNU General Public License along with pyTRLCConverter.
|
|
19
|
+
# If not, see <https://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
# Imports **********************************************************************
|
|
22
|
+
|
|
23
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
24
|
+
|
|
25
|
+
# Variables ********************************************************************
|
|
26
|
+
|
|
27
|
+
# Single source of truth is the version in pyproject.toml.
|
|
28
|
+
try:
|
|
29
|
+
__version__ = version("trlcdoc")
|
|
30
|
+
except PackageNotFoundError: # pragma: no cover - running from a source tree without install
|
|
31
|
+
__version__ = "0.0.0+unknown"
|
|
32
|
+
|
|
33
|
+
# Classes **********************************************************************
|
|
34
|
+
|
|
35
|
+
# Functions ********************************************************************
|
|
36
|
+
|
|
37
|
+
# Main *************************************************************************
|
trlcdoc/__main__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Entry point for 'python -m trlcdoc'.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# trlcdoc - A utility for producing project documentation from TRLC and LOBSTER contents.
|
|
6
|
+
# Copyright (c) 2026 - 2026 Norbert Schulz github@schulznorbert.de
|
|
7
|
+
#
|
|
8
|
+
# This file is part of trlcdoc program.
|
|
9
|
+
#
|
|
10
|
+
# The trlcdoc program is free software: you can redistribute it and/or modify it under
|
|
11
|
+
# the terms of the GNU General Public License as published by the Free Software Foundation,
|
|
12
|
+
# either version 3 of the License, or (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# The trlcdoc program is distributed in the hope that it will be useful, but
|
|
15
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
17
|
+
#
|
|
18
|
+
# You should have received a copy of the GNU General Public License along with pyTRLCConverter.
|
|
19
|
+
# If not, see <https://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
# Imports **********************************************************************
|
|
22
|
+
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
from .cli import main
|
|
26
|
+
|
|
27
|
+
# Variables ********************************************************************
|
|
28
|
+
|
|
29
|
+
# Classes **********************************************************************
|
|
30
|
+
|
|
31
|
+
# Functions ********************************************************************
|
|
32
|
+
|
|
33
|
+
# Main *************************************************************************
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
sys.exit(main())
|
trlcdoc/cli.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Command line interface of trlcdoc.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# trlcdoc - A utility for producing project documentation from TRLC and LOBSTER contents.
|
|
6
|
+
# Copyright (c) 2026 - 2026 Norbert Schulz github@schulznorbert.de
|
|
7
|
+
#
|
|
8
|
+
# This file is part of trlcdoc program.
|
|
9
|
+
#
|
|
10
|
+
# The trlcdoc program is free software: you can redistribute it and/or modify it under
|
|
11
|
+
# the terms of the GNU General Public License as published by the Free Software Foundation,
|
|
12
|
+
# either version 3 of the License, or (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# The trlcdoc program is distributed in the hope that it will be useful, but
|
|
15
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
17
|
+
#
|
|
18
|
+
# You should have received a copy of the GNU General Public License along with pyTRLCConverter.
|
|
19
|
+
# If not, see <https://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
# Imports **********************************************************************
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import List, Optional
|
|
26
|
+
|
|
27
|
+
from . import __version__
|
|
28
|
+
from .config import Config, find_config, load_config
|
|
29
|
+
from .convert import convert_documents
|
|
30
|
+
from .html import build_html
|
|
31
|
+
from .plantuml import provide_plantuml
|
|
32
|
+
from .testrun import run_tests
|
|
33
|
+
from .trace import build_trace_report
|
|
34
|
+
from .util import TrlcdocError, remove_output
|
|
35
|
+
|
|
36
|
+
# Variables ********************************************************************
|
|
37
|
+
|
|
38
|
+
# Steps in execution order. 'all' runs every step the configuration needs, except clean.
|
|
39
|
+
STEPS = ["clean", "check", "plantuml", "tests", "convert", "trace", "html"]
|
|
40
|
+
|
|
41
|
+
COPYRIGHT = "Copyright (c) 2026 Norbert Schulz"
|
|
42
|
+
REPOSITORY = "https://github.com/nhjschulz/trlc-doc"
|
|
43
|
+
DESCRIPTION = """Build project documentation for TRLC/LOBSTER based projects: requirements, architecture,
|
|
44
|
+
tests, test results, tracing report and HTML site."""
|
|
45
|
+
|
|
46
|
+
# Classes **********************************************************************
|
|
47
|
+
|
|
48
|
+
# Functions ********************************************************************
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _parse_args(argv: Optional[List[str]]) -> argparse.Namespace:
|
|
52
|
+
# lobster-trace: SwRequirements.sw_req_cli
|
|
53
|
+
# lobster-trace: SwRequirements.sw_req_cli_help
|
|
54
|
+
# lobster-trace: SwRequirements.sw_req_cli_version
|
|
55
|
+
# lobster-trace: SwRequirements.sw_req_steps
|
|
56
|
+
# lobster-trace: SwRequirements.sw_req_config_file
|
|
57
|
+
"""Parse the command line arguments.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
argv (Optional[List[str]]): Arguments, defaults to sys.argv[1:].
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
argparse.Namespace: The parsed arguments.
|
|
64
|
+
"""
|
|
65
|
+
parser = argparse.ArgumentParser(
|
|
66
|
+
prog="trlcdoc",
|
|
67
|
+
description=DESCRIPTION,
|
|
68
|
+
epilog=f"Version {__version__} - {COPYRIGHT} - GPL-3.0-or-later - see {REPOSITORY} for further information.",
|
|
69
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
70
|
+
|
|
71
|
+
parser.add_argument("steps", nargs="*", metavar="step",
|
|
72
|
+
help=f"steps to run: {', '.join(STEPS)} or all (default: all)")
|
|
73
|
+
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}",
|
|
74
|
+
help="show the trlcdoc version and exit")
|
|
75
|
+
parser.add_argument("-c", "--config", type=Path,
|
|
76
|
+
help="project configuration (default: trlcdoc.yaml in the current folder or a parent)")
|
|
77
|
+
parser.add_argument("--online", action="store_true",
|
|
78
|
+
help="link the tracing report to the repository instead of local files")
|
|
79
|
+
parser.add_argument("--format", choices=["rst", "markdown"], default="rst",
|
|
80
|
+
help="output format of the convert step (default: rst, needed for html)")
|
|
81
|
+
args = parser.parse_args(argv)
|
|
82
|
+
|
|
83
|
+
args.steps = args.steps or ["all"]
|
|
84
|
+
unknown = [step for step in args.steps if step not in (*STEPS, "all")]
|
|
85
|
+
if unknown:
|
|
86
|
+
parser.error(f"unknown step(s) {', '.join(unknown)}, choose from {', '.join(STEPS)}, all")
|
|
87
|
+
return args
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _selected_steps(cfg: Config, requested: List[str]) -> List[str]:
|
|
91
|
+
# lobster-trace: SwRequirements.sw_req_steps
|
|
92
|
+
# lobster-trace: SwRequirements.sw_req_step_order
|
|
93
|
+
"""Determine the steps to run, in execution order.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
cfg (Config): Project configuration.
|
|
97
|
+
requested (List[str]): Steps from the command line.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
List[str]: The steps to run.
|
|
101
|
+
"""
|
|
102
|
+
selected = {step for step in STEPS if step in requested}
|
|
103
|
+
if "all" in requested:
|
|
104
|
+
selected.update(step for step in STEPS
|
|
105
|
+
if step != "clean"
|
|
106
|
+
and not (step == "tests" and cfg.tests is None)
|
|
107
|
+
and not (step == "trace" and cfg.trace is None))
|
|
108
|
+
# Converting and rendering need the PlantUML jar.
|
|
109
|
+
if selected & {"convert", "html"}:
|
|
110
|
+
selected.add("plantuml")
|
|
111
|
+
return [step for step in STEPS if step in selected]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _run_step(step: str, cfg: Config, args: argparse.Namespace) -> bool:
|
|
115
|
+
# lobster-trace: SwRequirements.sw_req_steps
|
|
116
|
+
# lobster-trace: SwRequirements.sw_req_clean
|
|
117
|
+
# lobster-trace: SwRequirements.sw_req_exit_code
|
|
118
|
+
"""Run one step.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
step (str): Step name.
|
|
122
|
+
cfg (Config): Project configuration.
|
|
123
|
+
args (argparse.Namespace): Command line arguments.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
bool: True if the step found no problems in the project content
|
|
127
|
+
(failed tests, missing trace links). Build errors raise TrlcdocError.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
TrlcdocError: If the step cannot be completed.
|
|
131
|
+
"""
|
|
132
|
+
ok = True
|
|
133
|
+
print(f"\n===== trlcdoc {step} =====", flush=True)
|
|
134
|
+
if step == "clean":
|
|
135
|
+
print(f"Removing {cfg.output}" if remove_output(cfg.output) else f"Nothing to clean in {cfg.output}")
|
|
136
|
+
elif step == "check":
|
|
137
|
+
print(f"Configuration {cfg.config_file} is valid, output: {cfg.output}")
|
|
138
|
+
elif step == "plantuml":
|
|
139
|
+
provide_plantuml(cfg)
|
|
140
|
+
elif step == "tests":
|
|
141
|
+
if cfg.tests is None:
|
|
142
|
+
raise TrlcdocError("The configuration has no 'tests' section.")
|
|
143
|
+
ok = run_tests(cfg)
|
|
144
|
+
elif step == "convert":
|
|
145
|
+
convert_documents(cfg, args.format)
|
|
146
|
+
elif step == "trace":
|
|
147
|
+
if cfg.trace is None:
|
|
148
|
+
raise TrlcdocError("The configuration has no 'trace' section.")
|
|
149
|
+
ok = build_trace_report(cfg, args.online) == 0
|
|
150
|
+
else:
|
|
151
|
+
build_html(cfg)
|
|
152
|
+
return ok
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
156
|
+
# lobster-trace: SwRequirements.sw_req_cli
|
|
157
|
+
# lobster-trace: SwRequirements.sw_req_config_validation
|
|
158
|
+
# lobster-trace: SwRequirements.sw_req_exit_code
|
|
159
|
+
"""Run the requested trlcdoc steps.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
argv (Optional[List[str]]): Arguments, defaults to sys.argv[1:].
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
int: 0 if all steps succeeded and the content has no findings,
|
|
166
|
+
1 on findings (failed tests, missing trace links), 2 on build errors.
|
|
167
|
+
"""
|
|
168
|
+
args = _parse_args(argv)
|
|
169
|
+
exit_code = 0
|
|
170
|
+
try:
|
|
171
|
+
cfg = load_config(args.config if args.config is not None else find_config(Path.cwd()))
|
|
172
|
+
findings: List[str] = []
|
|
173
|
+
for step in _selected_steps(cfg, args.steps):
|
|
174
|
+
if not _run_step(step, cfg, args):
|
|
175
|
+
findings.append(step)
|
|
176
|
+
if findings:
|
|
177
|
+
print(f"\ntrlcdoc finished with findings in: {', '.join(findings)}")
|
|
178
|
+
exit_code = 1
|
|
179
|
+
else:
|
|
180
|
+
print("\ntrlcdoc finished successfully.")
|
|
181
|
+
except TrlcdocError as err:
|
|
182
|
+
print(f"\ntrlcdoc error: {err}")
|
|
183
|
+
exit_code = 2
|
|
184
|
+
return exit_code
|
|
185
|
+
|
|
186
|
+
# Main *************************************************************************
|
trlcdoc/config.py
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""Loading, validation and path resolution of the trlcdoc project configuration.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# trlcdoc - A utility for producing project documentation from TRLC and LOBSTER contents.
|
|
6
|
+
# Copyright (c) 2026 - 2026 Norbert Schulz github@schulznorbert.de
|
|
7
|
+
#
|
|
8
|
+
# This file is part of trlcdoc program.
|
|
9
|
+
#
|
|
10
|
+
# The trlcdoc program is free software: you can redistribute it and/or modify it under
|
|
11
|
+
# the terms of the GNU General Public License as published by the Free Software Foundation,
|
|
12
|
+
# either version 3 of the License, or (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# The trlcdoc program is distributed in the hope that it will be useful, but
|
|
15
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
17
|
+
#
|
|
18
|
+
# You should have received a copy of the GNU General Public License along with pyTRLCConverter.
|
|
19
|
+
# If not, see <https://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
# Imports **********************************************************************
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, Dict, List, Optional
|
|
26
|
+
|
|
27
|
+
import jsonschema
|
|
28
|
+
import yaml
|
|
29
|
+
|
|
30
|
+
from .util import TrlcdocError
|
|
31
|
+
|
|
32
|
+
# Variables ********************************************************************
|
|
33
|
+
|
|
34
|
+
PACKAGE_DIR = Path(__file__).resolve().parent
|
|
35
|
+
SCHEMA_FILE = PACKAGE_DIR / "schema" / "trlcdoc.schema.json"
|
|
36
|
+
BUILTIN_DIR = PACKAGE_DIR / "converters"
|
|
37
|
+
BUILTIN_PREFIX = "builtin:"
|
|
38
|
+
TESTS_COVERAGE = "tests:coverage"
|
|
39
|
+
|
|
40
|
+
# Document types whose content are TRLC records.
|
|
41
|
+
TRLC_DOCUMENT_TYPES = ("trlc", "test_results")
|
|
42
|
+
|
|
43
|
+
# Classes **********************************************************************
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Config:
|
|
47
|
+
"""Validated project configuration with resolved paths."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, data: Dict[str, Any], config_file: Path) -> None:
|
|
50
|
+
# lobster-trace: SwRequirements.sw_req_project_settings
|
|
51
|
+
# lobster-trace: SwRequirements.sw_req_documents
|
|
52
|
+
"""Construct the configuration.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
data (Dict[str, Any]): Schema validated configuration data.
|
|
56
|
+
config_file (Path): The configuration file. Its folder is the project root.
|
|
57
|
+
"""
|
|
58
|
+
self.data = data
|
|
59
|
+
self.config_file = config_file.resolve()
|
|
60
|
+
self.root = self.config_file.parent
|
|
61
|
+
self.documents: List[Dict[str, Any]] = data["documents"]
|
|
62
|
+
self.tests: Optional[Dict[str, Any]] = data.get("tests")
|
|
63
|
+
self.trace: Optional[Dict[str, Any]] = data.get("trace")
|
|
64
|
+
self.project: Dict[str, Any] = data["project"]
|
|
65
|
+
self.html: Dict[str, Any] = data.get("html", {})
|
|
66
|
+
self.plantuml: Dict[str, Any] = data.get("plantuml", {})
|
|
67
|
+
|
|
68
|
+
# Paths ********************************************************************
|
|
69
|
+
|
|
70
|
+
def path(self, value: str) -> Path:
|
|
71
|
+
# lobster-trace: SwRequirements.sw_req_config_file
|
|
72
|
+
"""Resolve a configured path against the project root.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
value (str): Absolute path or path relative to the project root.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Path: The absolute path.
|
|
79
|
+
"""
|
|
80
|
+
return (self.root / value).resolve()
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def output(self) -> Path:
|
|
84
|
+
# lobster-trace: SwRequirements.sw_req_output_folder
|
|
85
|
+
"""Folder for all generated files."""
|
|
86
|
+
return self.path(self.data["paths"].get("output", "build/trlcdoc"))
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def trlc_model(self) -> Path:
|
|
90
|
+
# lobster-trace: SwRequirements.sw_req_trlc_conversion
|
|
91
|
+
"""Folder with the TRLC model files."""
|
|
92
|
+
return self.path(self.data["paths"]["trlc_model"])
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def plantuml_dir(self) -> Path:
|
|
96
|
+
# lobster-trace: SwRequirements.sw_req_plantuml
|
|
97
|
+
# lobster-trace: SwRequirements.sw_req_output_folder
|
|
98
|
+
"""Folder for the downloaded PlantUML jar."""
|
|
99
|
+
return self.output / "plantuml"
|
|
100
|
+
|
|
101
|
+
def convert_dir(self, fmt: str, doc_id: str) -> Path:
|
|
102
|
+
# lobster-trace: SwRequirements.sw_req_trlc_conversion
|
|
103
|
+
# lobster-trace: SwRequirements.sw_req_output_folder
|
|
104
|
+
"""Output folder of a converted document.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
fmt (str): Output format, rst or markdown.
|
|
108
|
+
doc_id (str): Document id.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Path: The output folder.
|
|
112
|
+
"""
|
|
113
|
+
return self.output / "convert" / fmt / doc_id
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def tests_dir(self) -> Path:
|
|
117
|
+
# lobster-trace: SwRequirements.sw_req_test_run
|
|
118
|
+
# lobster-trace: SwRequirements.sw_req_output_folder
|
|
119
|
+
"""Output folder of the tests step."""
|
|
120
|
+
return self.output / "tests"
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def junit_file(self) -> Path:
|
|
124
|
+
# lobster-trace: SwRequirements.sw_req_test_run
|
|
125
|
+
"""JUnit XML file with the test results."""
|
|
126
|
+
result = self.tests_dir / "junit.xml"
|
|
127
|
+
if self.tests is not None and self.tests["runner"] == "command":
|
|
128
|
+
result = self.path(self.tests["junit"])
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def coverage_dir(self) -> Path:
|
|
133
|
+
# lobster-trace: SwRequirements.sw_req_test_run
|
|
134
|
+
"""HTML coverage report folder."""
|
|
135
|
+
result = self.tests_dir / "coverage"
|
|
136
|
+
if self.tests is not None and "coverage" in self.tests:
|
|
137
|
+
result = self.path(self.tests["coverage"])
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def test_results_dir(self) -> Path:
|
|
142
|
+
# lobster-trace: SwRequirements.sw_req_test_results
|
|
143
|
+
"""Folder with the TRLC test result records generated from the JUnit XML."""
|
|
144
|
+
return self.tests_dir / "results"
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def trace_dir(self) -> Path:
|
|
148
|
+
# lobster-trace: SwRequirements.sw_req_trace_report
|
|
149
|
+
# lobster-trace: SwRequirements.sw_req_output_folder
|
|
150
|
+
"""Output folder of the trace step."""
|
|
151
|
+
return self.output / "trace"
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def trace_report_html(self) -> Path:
|
|
155
|
+
# lobster-trace: SwRequirements.sw_req_trace_report
|
|
156
|
+
"""The HTML tracing report."""
|
|
157
|
+
return self.trace_dir / "tracing_report.html"
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def sphinx_dir(self) -> Path:
|
|
161
|
+
# lobster-trace: SwRequirements.sw_req_html_site
|
|
162
|
+
# lobster-trace: SwRequirements.sw_req_output_folder
|
|
163
|
+
"""Generated Sphinx source folder."""
|
|
164
|
+
return self.output / "sphinx"
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def html_dir(self) -> Path:
|
|
168
|
+
# lobster-trace: SwRequirements.sw_req_html_site
|
|
169
|
+
# lobster-trace: SwRequirements.sw_req_output_folder
|
|
170
|
+
"""Generated HTML site."""
|
|
171
|
+
return self.output / "html"
|
|
172
|
+
|
|
173
|
+
# Documents ****************************************************************
|
|
174
|
+
|
|
175
|
+
def document(self, doc_id: str) -> Dict[str, Any]:
|
|
176
|
+
# lobster-trace: SwRequirements.sw_req_documents
|
|
177
|
+
"""Get a document by its id.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
doc_id (str): Document id.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Dict[str, Any]: The document configuration.
|
|
184
|
+
"""
|
|
185
|
+
return next(doc for doc in self.documents if doc["id"] == doc_id)
|
|
186
|
+
|
|
187
|
+
def trlc_sources(self, doc: Dict[str, Any]) -> List[Path]:
|
|
188
|
+
# lobster-trace: SwRequirements.sw_req_trlc_conversion
|
|
189
|
+
"""Get the TRLC source folders of a TRLC document.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
doc (Dict[str, Any]): Document of type trlc or test_results.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
List[Path]: The source folders.
|
|
196
|
+
"""
|
|
197
|
+
result = [self.test_results_dir]
|
|
198
|
+
if doc["type"] == "trlc":
|
|
199
|
+
result = [self.path(doc["source"])]
|
|
200
|
+
return result
|
|
201
|
+
|
|
202
|
+
def included_documents(self, doc: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
203
|
+
# lobster-trace: SwRequirements.sw_req_trlc_conversion
|
|
204
|
+
"""Get all documents included by a document, transitively.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
doc (Dict[str, Any]): The including document.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
List[Dict[str, Any]]: Included documents, without the document itself.
|
|
211
|
+
"""
|
|
212
|
+
result: List[Dict[str, Any]] = []
|
|
213
|
+
pending = list(doc.get("include", []))
|
|
214
|
+
while pending:
|
|
215
|
+
doc_id = pending.pop(0)
|
|
216
|
+
if doc_id != doc["id"] and all(inc["id"] != doc_id for inc in result):
|
|
217
|
+
included = self.document(doc_id)
|
|
218
|
+
result.append(included)
|
|
219
|
+
pending.extend(included.get("include", []))
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
def included_sources(self, doc: Dict[str, Any]) -> List[Path]:
|
|
223
|
+
# lobster-trace: SwRequirements.sw_req_trlc_conversion
|
|
224
|
+
"""Get the TRLC source folders of all documents included by a document.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
doc (Dict[str, Any]): The including document.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
List[Path]: The source folders.
|
|
231
|
+
"""
|
|
232
|
+
result: List[Path] = []
|
|
233
|
+
for included in self.included_documents(doc):
|
|
234
|
+
result.extend(self.trlc_sources(included))
|
|
235
|
+
return result
|
|
236
|
+
|
|
237
|
+
def file_reference(self, value: str) -> Path:
|
|
238
|
+
# lobster-trace: SwRequirements.sw_req_trlc_conversion
|
|
239
|
+
# lobster-trace: SwRequirements.sw_req_builtin_converters
|
|
240
|
+
"""Resolve a converter, translation or render configuration file reference.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
value (str): 'builtin:<file>' or a path relative to the project root.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Path: The absolute file path.
|
|
247
|
+
"""
|
|
248
|
+
result = self.path(value)
|
|
249
|
+
if value.startswith(BUILTIN_PREFIX):
|
|
250
|
+
result = BUILTIN_DIR / value[len(BUILTIN_PREFIX):]
|
|
251
|
+
return result
|
|
252
|
+
|
|
253
|
+
def html_source(self, doc: Dict[str, Any]) -> Path:
|
|
254
|
+
# lobster-trace: SwRequirements.sw_req_document_types
|
|
255
|
+
"""Resolve the HTML folder of a document of type html or command.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
doc (Dict[str, Any]): Document of type html or command.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Path: The HTML folder.
|
|
262
|
+
"""
|
|
263
|
+
result = self.coverage_dir
|
|
264
|
+
if doc["type"] == "command":
|
|
265
|
+
result = self.path(doc["output"])
|
|
266
|
+
elif doc["source"] != TESTS_COVERAGE:
|
|
267
|
+
result = self.path(doc["source"])
|
|
268
|
+
return result
|
|
269
|
+
|
|
270
|
+
# Project ******************************************************************
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def repository(self) -> str:
|
|
274
|
+
# lobster-trace: SwRequirements.sw_req_project_settings
|
|
275
|
+
# lobster-trace: SwRequirements.sw_req_trace_report
|
|
276
|
+
"""Configured repository URL or 'auto'."""
|
|
277
|
+
return self.project.get("repository", "auto")
|
|
278
|
+
|
|
279
|
+
# Functions ********************************************************************
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _check_references(cfg: Config) -> List[str]:
|
|
283
|
+
# lobster-trace: SwRequirements.sw_req_config_validation
|
|
284
|
+
# lobster-trace: SwRequirements.sw_req_output_folder
|
|
285
|
+
"""Check the cross references, which the JSON schema cannot express.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
cfg (Config): Schema validated configuration.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
List[str]: Error messages, empty if all references are valid.
|
|
292
|
+
"""
|
|
293
|
+
errors: List[str] = []
|
|
294
|
+
doc_ids = [doc["id"] for doc in cfg.documents]
|
|
295
|
+
doc_types = {doc["id"]: doc["type"] for doc in cfg.documents}
|
|
296
|
+
|
|
297
|
+
for doc_id in sorted({doc_id for doc_id in doc_ids if doc_ids.count(doc_id) > 1}):
|
|
298
|
+
errors.append(f"documents: id '{doc_id}' is used more than once.")
|
|
299
|
+
|
|
300
|
+
for doc in cfg.documents:
|
|
301
|
+
errors.extend(_check_document(cfg, doc, doc_types))
|
|
302
|
+
|
|
303
|
+
if cfg.trace is not None:
|
|
304
|
+
errors.extend(_check_levels(cfg.trace["levels"], doc_types))
|
|
305
|
+
|
|
306
|
+
for key in ("conf", "index", "page_header"):
|
|
307
|
+
if key in cfg.html and not cfg.path(cfg.html[key]).is_file():
|
|
308
|
+
errors.append(f"html.{key}: file '{cfg.html[key]}' not found.")
|
|
309
|
+
|
|
310
|
+
if cfg.output.is_relative_to(PACKAGE_DIR):
|
|
311
|
+
errors.append("paths.output: must not be inside the trlcdoc package folder.")
|
|
312
|
+
# The clean step removes the output folder, so it must never contain the project.
|
|
313
|
+
if cfg.root.is_relative_to(cfg.output):
|
|
314
|
+
errors.append("paths.output: must not be the project root or one of its parents.")
|
|
315
|
+
|
|
316
|
+
return errors
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _check_document(cfg: Config, doc: Dict[str, Any], doc_types: Dict[str, str]) -> List[str]:
|
|
320
|
+
# lobster-trace: SwRequirements.sw_req_config_validation
|
|
321
|
+
"""Check the references of one document.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
cfg (Config): Schema validated configuration.
|
|
325
|
+
doc (Dict[str, Any]): The document.
|
|
326
|
+
doc_types (Dict[str, str]): Document type per document id.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
List[str]: Error messages, empty if all references are valid.
|
|
330
|
+
"""
|
|
331
|
+
errors: List[str] = []
|
|
332
|
+
where = f"documents[{doc['id']}]"
|
|
333
|
+
|
|
334
|
+
for inc in doc.get("include", []):
|
|
335
|
+
if doc_types.get(inc) not in TRLC_DOCUMENT_TYPES:
|
|
336
|
+
errors.append(f"{where}.include: '{inc}' is no document of type trlc or test_results.")
|
|
337
|
+
|
|
338
|
+
options = doc.get("pytrlcconverter", {})
|
|
339
|
+
files = {f"pytrlcconverter.converter.{fmt}": value for fmt, value in options.get("converter", {}).items()}
|
|
340
|
+
files.update({f"pytrlcconverter.{key}": options[key] for key in ("translation", "render_cfg") if key in options})
|
|
341
|
+
for key, value in files.items():
|
|
342
|
+
if not cfg.file_reference(value).is_file():
|
|
343
|
+
errors.append(f"{where}.{key}: file '{value}' not found.")
|
|
344
|
+
|
|
345
|
+
needs_tests = doc["type"] == "test_results" or doc.get("source") == TESTS_COVERAGE
|
|
346
|
+
if needs_tests and cfg.tests is None:
|
|
347
|
+
errors.append(f"{where}: needs a 'tests' section.")
|
|
348
|
+
if doc["type"] == "trace_report" and cfg.trace is None:
|
|
349
|
+
errors.append(f"{where}: type trace_report needs a 'trace' section.")
|
|
350
|
+
return errors
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _check_levels(levels: List[Dict[str, Any]], doc_types: Dict[str, str]) -> List[str]:
|
|
354
|
+
# lobster-trace: SwRequirements.sw_req_config_validation
|
|
355
|
+
# lobster-trace: SwRequirements.sw_req_trace_levels
|
|
356
|
+
"""Check the trace level references.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
levels (List[Dict[str, Any]]): Configured trace levels.
|
|
360
|
+
doc_types (Dict[str, str]): Document type per document id.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
List[str]: Error messages, empty if all references are valid.
|
|
364
|
+
"""
|
|
365
|
+
errors: List[str] = []
|
|
366
|
+
names = [level["name"] for level in levels]
|
|
367
|
+
|
|
368
|
+
for name in sorted({name for name in names if names.count(name) > 1}):
|
|
369
|
+
errors.append(f"trace.levels: name '{name}' is used more than once.")
|
|
370
|
+
|
|
371
|
+
for level in levels:
|
|
372
|
+
where = f"trace.levels[{level['name']}]"
|
|
373
|
+
if "document" in level and doc_types.get(level["document"]) not in TRLC_DOCUMENT_TYPES:
|
|
374
|
+
errors.append(f"{where}.document: '{level['document']}' is no document of type trlc or test_results.")
|
|
375
|
+
for key in ("trace_to", "requires"):
|
|
376
|
+
for target in level.get(key, []):
|
|
377
|
+
if target not in names:
|
|
378
|
+
errors.append(f"{where}.{key}: level '{target}' is not defined.")
|
|
379
|
+
return errors
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def load_config(config_file: Path) -> Config:
|
|
383
|
+
# lobster-trace: SwRequirements.sw_req_config_file
|
|
384
|
+
# lobster-trace: SwRequirements.sw_req_config_validation
|
|
385
|
+
"""Load and validate the project configuration.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
config_file (Path): The trlcdoc YAML configuration file.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
Config: The validated configuration.
|
|
392
|
+
|
|
393
|
+
Raises:
|
|
394
|
+
TrlcdocError: If the file is missing or invalid.
|
|
395
|
+
"""
|
|
396
|
+
if not config_file.is_file():
|
|
397
|
+
raise TrlcdocError(f"Configuration file {config_file} not found.")
|
|
398
|
+
|
|
399
|
+
with open(config_file, encoding="utf-8") as file:
|
|
400
|
+
data = yaml.safe_load(file)
|
|
401
|
+
|
|
402
|
+
with open(SCHEMA_FILE, encoding="utf-8") as file:
|
|
403
|
+
schema = json.load(file)
|
|
404
|
+
|
|
405
|
+
validator = jsonschema.Draft202012Validator(schema)
|
|
406
|
+
schema_errors = sorted(validator.iter_errors(data), key=lambda err: list(err.absolute_path))
|
|
407
|
+
if schema_errors:
|
|
408
|
+
messages = [f"{'/'.join(str(p) for p in err.absolute_path) or '<root>'}: {err.message}"
|
|
409
|
+
for err in schema_errors]
|
|
410
|
+
raise TrlcdocError(f"{config_file} does not match the schema:\n " + "\n ".join(messages))
|
|
411
|
+
|
|
412
|
+
cfg = Config(data, config_file)
|
|
413
|
+
reference_errors = _check_references(cfg)
|
|
414
|
+
if reference_errors:
|
|
415
|
+
raise TrlcdocError(f"{config_file} has invalid settings:\n " + "\n ".join(reference_errors))
|
|
416
|
+
|
|
417
|
+
return cfg
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def find_config(start: Path) -> Path:
|
|
421
|
+
# lobster-trace: SwRequirements.sw_req_config_file
|
|
422
|
+
"""Find the default configuration file trlcdoc.yaml in a folder or its parents.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
start (Path): Folder to start the search in.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Path: The configuration file, or start/trlcdoc.yaml if none was found.
|
|
429
|
+
"""
|
|
430
|
+
candidates = [folder / "trlcdoc.yaml" for folder in (start, *start.parents)]
|
|
431
|
+
return next((file for file in candidates if file.is_file()), start / "trlcdoc.yaml")
|
|
432
|
+
|
|
433
|
+
# Main *************************************************************************
|