dynamic-systems-testing-framework 1.0.1__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.
- dstf/__init__.py +41 -0
- dstf/__main__.py +10 -0
- dstf/authoring/__init__.py +13 -0
- dstf/authoring/builtin/__init__.py +32 -0
- dstf/authoring/builtin/accept.py +205 -0
- dstf/authoring/builtin/edit_criteria.py +479 -0
- dstf/authoring/builtin/edit_family.py +228 -0
- dstf/authoring/builtin/edit_on_plot.py +1354 -0
- dstf/authoring/builtin/edit_tree.py +1399 -0
- dstf/authoring/builtin/enqueue.py +144 -0
- dstf/authoring/capabilities.py +463 -0
- dstf/authoring/errors.py +49 -0
- dstf/authoring/family_rows.py +526 -0
- dstf/authoring/forms.py +631 -0
- dstf/authoring/layout.py +104 -0
- dstf/authoring/live_rows.py +337 -0
- dstf/authoring/live_runs.py +385 -0
- dstf/authoring/needle.py +127 -0
- dstf/authoring/picker_rows.py +92 -0
- dstf/authoring/queue_rows.py +354 -0
- dstf/authoring/session.py +3016 -0
- dstf/authoring/shell/__init__.py +9 -0
- dstf/authoring/shell/actions.py +702 -0
- dstf/authoring/shell/app.py +53 -0
- dstf/authoring/shell/drawer_controls.py +190 -0
- dstf/authoring/shell/family_fold.py +468 -0
- dstf/authoring/shell/figure_hook.py +249 -0
- dstf/authoring/shell/grips.js +1000 -0
- dstf/authoring/shell/header.py +783 -0
- dstf/authoring/shell/live_view.py +93 -0
- dstf/authoring/shell/picker.py +267 -0
- dstf/authoring/shell/placing.py +122 -0
- dstf/authoring/shell/plot_card.py +1171 -0
- dstf/authoring/shell/queue_panel.py +433 -0
- dstf/authoring/shell/rail.py +593 -0
- dstf/authoring/shell/shell.py +528 -0
- dstf/authoring/shell/stats_card.py +501 -0
- dstf/authoring/shell/strip.py +204 -0
- dstf/authoring/shell/style.py +721 -0
- dstf/authoring/shell/suite.py +277 -0
- dstf/authoring/shell/suite_grid.js +414 -0
- dstf/authoring/shell/suite_grid.py +373 -0
- dstf/authoring/shell/tab_theme.py +110 -0
- dstf/authoring/shell/tabstate.py +146 -0
- dstf/authoring/shell/test_list.py +179 -0
- dstf/authoring/shell/test_page.py +1404 -0
- dstf/authoring/shell/tree_card.py +1171 -0
- dstf/authoring/shell/variables_drawer.py +589 -0
- dstf/authoring/shell/widgets.py +188 -0
- dstf/authoring/situation.py +287 -0
- dstf/authoring/spec_document.py +215 -0
- dstf/authoring/spec_preview.py +83 -0
- dstf/authoring/suite_rows.py +99 -0
- dstf/authoring/tree_move.py +516 -0
- dstf/authoring/tree_pointer.py +33 -0
- dstf/authoring/tree_text.py +61 -0
- dstf/authoring/typed_number.js +64 -0
- dstf/authoring/typed_number.py +76 -0
- dstf/authoring/unscorable.py +141 -0
- dstf/authoring/variable_rows.py +216 -0
- dstf/backends/__init__.py +283 -0
- dstf/backends/base.py +1117 -0
- dstf/backends/common/__init__.py +1 -0
- dstf/backends/common/json_driver.py +484 -0
- dstf/backends/common/live_mat.py +369 -0
- dstf/backends/common/mat_reader.py +632 -0
- dstf/backends/common/proc_kill.py +238 -0
- dstf/backends/common/proc_output.py +20 -0
- dstf/backends/common/result_files.py +109 -0
- dstf/backends/common/result_variables.py +329 -0
- dstf/backends/common/statistics.py +39 -0
- dstf/backends/cross_backend.py +193 -0
- dstf/backends/dymola/__init__.py +5 -0
- dstf/backends/dymola/backend.py +776 -0
- dstf/backends/dymola/interface_loader.py +241 -0
- dstf/backends/dymola/log_parser.py +396 -0
- dstf/backends/dymola/persistent_backend.py +949 -0
- dstf/backends/fmpy/__init__.py +11 -0
- dstf/backends/fmpy/backend.py +443 -0
- dstf/backends/julia/__init__.py +27 -0
- dstf/backends/julia/backend.py +156 -0
- dstf/backends/julia/persistent_backend.py +541 -0
- dstf/backends/julia/run_persistent.jl +167 -0
- dstf/backends/julia/run_test.jl +146 -0
- dstf/backends/openmodelica/__init__.py +21 -0
- dstf/backends/openmodelica/backend.py +368 -0
- dstf/backends/openmodelica/init_xml.py +76 -0
- dstf/backends/openmodelica/log_parser.py +119 -0
- dstf/backends/openmodelica/mos_generator.py +228 -0
- dstf/backends/openmodelica/persistent_backend.py +1045 -0
- dstf/backends/openmodelica/session_loader.py +63 -0
- dstf/backends/persistent.py +1811 -0
- dstf/backends/pool.py +602 -0
- dstf/backends/progress.py +395 -0
- dstf/backends/python/__init__.py +11 -0
- dstf/backends/python/backend.py +141 -0
- dstf/backends/python/run_test.py +141 -0
- dstf/backends/run_metadata.py +103 -0
- dstf/backends/types.py +310 -0
- dstf/cli/__init__.py +12 -0
- dstf/cli/author_cmd.py +61 -0
- dstf/cli/baseline_cmds.py +341 -0
- dstf/cli/common.py +231 -0
- dstf/cli/diag_cmds.py +350 -0
- dstf/cli/main.py +561 -0
- dstf/cli/review.py +398 -0
- dstf/cli/run_cmds.py +589 -0
- dstf/cli/spec_cmds.py +123 -0
- dstf/comparison/__init__.py +13 -0
- dstf/comparison/algorithms.py +2127 -0
- dstf/comparison/builtins.py +291 -0
- dstf/comparison/comparator.py +1189 -0
- dstf/comparison/geometry.py +164 -0
- dstf/comparison/handles.py +1194 -0
- dstf/comparison/metric_tree.py +332 -0
- dstf/comparison/mode_schema.py +176 -0
- dstf/comparison/modes.py +788 -0
- dstf/comparison/registry.py +230 -0
- dstf/comparison/seeds.py +281 -0
- dstf/comparison/sides.py +158 -0
- dstf/comparison/snap.py +62 -0
- dstf/comparison/strict_keys.py +246 -0
- dstf/comparison/summaries.py +354 -0
- dstf/comparison/tree_eval.py +868 -0
- dstf/comparison/tree_spec.py +678 -0
- dstf/comparison/types.py +194 -0
- dstf/comparison/validator.py +232 -0
- dstf/config.py +815 -0
- dstf/discovery/__init__.py +0 -0
- dstf/discovery/json_recognizer.py +702 -0
- dstf/discovery/library_state.py +248 -0
- dstf/discovery/mo_parser.py +794 -0
- dstf/discovery/patch_apply.py +308 -0
- dstf/discovery/patch_envelope.py +63 -0
- dstf/discovery/plots.py +318 -0
- dstf/discovery/recognizer.py +103 -0
- dstf/discovery/scan_cache.py +154 -0
- dstf/discovery/spec_parser.py +538 -0
- dstf/discovery/spec_shape.py +122 -0
- dstf/discovery/spec_update.py +493 -0
- dstf/discovery/test_registry.py +488 -0
- dstf/discovery/unit_tests.py +95 -0
- dstf/figures/__init__.py +14 -0
- dstf/figures/assemble.py +48 -0
- dstf/figures/decimate.py +150 -0
- dstf/figures/families.py +289 -0
- dstf/figures/palette.py +170 -0
- dstf/figures/plotly.py +1413 -0
- dstf/figures/view.py +233 -0
- dstf/pipeline.py +435 -0
- dstf/plugins.py +104 -0
- dstf/reporting/__init__.py +0 -0
- dstf/reporting/console_report.py +187 -0
- dstf/reporting/context_builder.py +979 -0
- dstf/reporting/dashboard_render.py +534 -0
- dstf/reporting/junit_report.py +192 -0
- dstf/reporting/overlay_loader.py +708 -0
- dstf/reporting/report_suite.py +1059 -0
- dstf/reporting/rows.py +458 -0
- dstf/reporting/schema_export.py +413 -0
- dstf/reporting/stats_rows.py +321 -0
- dstf/reporting/status.py +388 -0
- dstf/reporting/templates/axes.js +61 -0
- dstf/reporting/templates/dashboard.html +284 -0
- dstf/reporting/templates/dashboard.js +604 -0
- dstf/reporting/templates/filters.js +100 -0
- dstf/reporting/templates/interactive.html +420 -0
- dstf/reporting/templates/report.js +116 -0
- dstf/reporting/templates/vendor/plotly.min.js +8 -0
- dstf/reporting/theme.py +324 -0
- dstf/reporting/tree_rows.py +73 -0
- dstf/reporting/verdicts.py +124 -0
- dstf/storage/__init__.py +0 -0
- dstf/storage/accept.py +216 -0
- dstf/storage/baseline.py +43 -0
- dstf/storage/batch_manifest.py +204 -0
- dstf/storage/ref_index.py +295 -0
- dstf/storage/reference_store.py +661 -0
- dstf/storage/roles.py +576 -0
- dstf/storage/run_stats.py +85 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/METADATA +266 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/RECORD +185 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/WHEEL +4 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/entry_points.txt +2 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Pluggable test-recognition system.
|
|
2
|
+
|
|
3
|
+
A ``Recognizer`` inspects a source file (a Modelica ``.mo``, an FMU, a Julia
|
|
4
|
+
script, ...) and emits a ``RecognizerResult`` describing a test it found.
|
|
5
|
+
Discovery runs every registered recognizer that applies to the configured
|
|
6
|
+
``source_type`` and merges results by ``model_id``. This lets users layer
|
|
7
|
+
their own test-tagging conventions on top of (or in place of) the bundled
|
|
8
|
+
defaults — a library that can't adopt the bundled ``UnitTests`` component
|
|
9
|
+
provides its own recognizer (PTA.2 onward: via a JSON map) instead.
|
|
10
|
+
|
|
11
|
+
PTA.1 wires the registry; only the bundled recognizer registers today, so
|
|
12
|
+
behavior is identical to the pre-PTA hardcoded path.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class RecognizerResult:
|
|
24
|
+
"""One recognizer's contribution toward a ``TestModel`` for one source file.
|
|
25
|
+
|
|
26
|
+
Fields left ``None`` / empty are not contributed by this recognizer; the
|
|
27
|
+
framework defaults fill them in (or another recognizer overrides, when
|
|
28
|
+
multiple recognizers emit for the same ``model_id``).
|
|
29
|
+
|
|
30
|
+
The shape today mirrors what the bundled Modelica recognizer extracts.
|
|
31
|
+
PTA.4 broadens this with runtime-behavior fields (``simulate_only``,
|
|
32
|
+
``requested_baselines``, ...).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
model_id: str
|
|
36
|
+
source_file: Path | None = None
|
|
37
|
+
# Variables-to-track contribution (was UnitTestInfo)
|
|
38
|
+
n_vars: int | None = None
|
|
39
|
+
# Instance name of the component whose ``x[i]`` the variables are: the
|
|
40
|
+
# result file names them ``<instance>.x[i]``. Contributed by a recognizer
|
|
41
|
+
# that takes those variables out of a matched component — their count
|
|
42
|
+
# (``n_vars``) or their expressions, either one; a recognizer that
|
|
43
|
+
# contributes neither leaves it None and the conventional default stands.
|
|
44
|
+
unit_tests_instance: str | None = None
|
|
45
|
+
x_expressions: list[str] = field(default_factory=list)
|
|
46
|
+
x_raw: str = ""
|
|
47
|
+
x_reference: list[float] | None = None
|
|
48
|
+
error_expected: float | None = None
|
|
49
|
+
# Simulation-parameters contribution (was ExperimentInfo)
|
|
50
|
+
stop_time: float | None = None
|
|
51
|
+
tolerance: float | None = None
|
|
52
|
+
method: str | None = None
|
|
53
|
+
number_of_intervals: int | None = None
|
|
54
|
+
output_interval: float | None = None
|
|
55
|
+
# PTA.4 — richer-contract runtime-behavior fields. Recognizers can set
|
|
56
|
+
# these; TestModel carries them through. Consumed by PTA.5 (simulate_only)
|
|
57
|
+
# and 4.B's cross-backend chain (requested_baselines).
|
|
58
|
+
simulate_only: bool | None = None
|
|
59
|
+
requested_baselines: list[str] | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Recognizer(ABC):
|
|
63
|
+
"""Base class for source-file recognizers."""
|
|
64
|
+
|
|
65
|
+
#: Unique recognizer name; used in diagnostics and as a registry key.
|
|
66
|
+
name: str = "<unnamed>"
|
|
67
|
+
#: ``Config.source_type`` values this recognizer applies to.
|
|
68
|
+
applies_to: frozenset[str] = frozenset()
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def recognize(self, source_file: Path) -> RecognizerResult | None:
|
|
72
|
+
"""Inspect a source file. Return a result if a test is recognized, else None."""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
def applies_to_path(self, source_file: Path, base: Path) -> bool:
|
|
76
|
+
"""Optional pre-filter: return False to skip the file before recognize().
|
|
77
|
+
|
|
78
|
+
Default: True (no filtering). Subclasses can override to honor a
|
|
79
|
+
per-recognizer paths_include / paths_exclude config (PTA-follow.1).
|
|
80
|
+
"""
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Registration-ordered list. The bundled recognizer registers first on import;
|
|
85
|
+
# user-provided recognizers (PTA.3) append, and discovery's per-model merge
|
|
86
|
+
# applies later writers as overrides — bundled defaults, user values win.
|
|
87
|
+
_REGISTRY: list[Recognizer] = []
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def register(recognizer: Recognizer) -> None:
|
|
91
|
+
"""Add a recognizer to the registry (idempotent on identity)."""
|
|
92
|
+
if recognizer not in _REGISTRY:
|
|
93
|
+
_REGISTRY.append(recognizer)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_recognizers(source_type: str) -> list[Recognizer]:
|
|
97
|
+
"""Return registered recognizers that apply to ``source_type``."""
|
|
98
|
+
return [r for r in _REGISTRY if source_type in r.applies_to]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _reset_for_tests() -> None:
|
|
102
|
+
"""Clear the registry — only for unit tests that need a clean slate."""
|
|
103
|
+
_REGISTRY.clear()
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""The source scan, cached across tool restarts (brief §3.8).
|
|
2
|
+
|
|
3
|
+
Under ``$XDG_CACHE_HOME/dstf/scan/`` (default ``~/.cache/dstf/scan/``),
|
|
4
|
+
one JSON file per (library path, recognizer set) — a file another DSTF
|
|
5
|
+
version, or other discovery code, wrote is ignored and replaced — never in
|
|
6
|
+
the work dir, which is a run's artifact directory that other tools read,
|
|
7
|
+
and never validated per file: on a 9P mount (``/mnt/d``) a ``stat`` sweep
|
|
8
|
+
over TRANSFORM's 3 269 ``.mo`` files measured 27 s, longer than parsing
|
|
9
|
+
them. The Session opens from the cache and rescans in the background.
|
|
10
|
+
Only ``source_type == "modelica"`` scans anything, so only those cache.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import dataclasses
|
|
16
|
+
import functools
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
import threading
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from .. import __version__
|
|
25
|
+
from ..config import Config
|
|
26
|
+
from . import (
|
|
27
|
+
mo_parser, # noqa: F401 — registers the bundled recognizer; the key names it
|
|
28
|
+
)
|
|
29
|
+
from .recognizer import get_recognizers
|
|
30
|
+
from .test_registry import TestModel
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
# The FORMAT of a cache file. What a scan MEANS — what the parser extracts
|
|
35
|
+
# and how it spells it — is not versioned by hand: see _discovery_digest.
|
|
36
|
+
CACHE_VERSION = 2
|
|
37
|
+
|
|
38
|
+
#: The modules that decide what a scan produces. A cache holds parsed
|
|
39
|
+
#: ``TestModel``s, so a change to any of them can make a cached entry mean
|
|
40
|
+
#: something the current code would never have written, and no source file
|
|
41
|
+
#: changed for a staleness check to see. Bumping a constant by hand is a
|
|
42
|
+
#: step people forget — twice already, once for the instance name and once
|
|
43
|
+
#: for expression whitespace — so the code itself is the version.
|
|
44
|
+
_DISCOVERY_SOURCES = (
|
|
45
|
+
"json_recognizer.py",
|
|
46
|
+
"mo_parser.py",
|
|
47
|
+
"recognizer.py",
|
|
48
|
+
"test_registry.py",
|
|
49
|
+
"unit_tests.py",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@functools.lru_cache(maxsize=1)
|
|
54
|
+
def _discovery_digest() -> str:
|
|
55
|
+
"""A digest of the discovery code's source bytes.
|
|
56
|
+
|
|
57
|
+
Read once per process: these files do not change under a running tool,
|
|
58
|
+
and a warm ``dstf author`` open asks for this before anything else.
|
|
59
|
+
"""
|
|
60
|
+
here = Path(__file__).parent
|
|
61
|
+
h = hashlib.sha256()
|
|
62
|
+
for name in _DISCOVERY_SOURCES:
|
|
63
|
+
h.update(name.encode("utf-8"))
|
|
64
|
+
try:
|
|
65
|
+
h.update((here / name).read_bytes())
|
|
66
|
+
except OSError as e: # a zipped / stripped install: fall back
|
|
67
|
+
logger.warning("cannot digest the discovery source %s: %s", name, e)
|
|
68
|
+
h.update(b"<unreadable>")
|
|
69
|
+
return h.hexdigest()[:16]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def cache_dir() -> Path:
|
|
73
|
+
root = os.environ.get("XDG_CACHE_HOME") or str(Path.home() / ".cache")
|
|
74
|
+
return Path(root) / "dstf" / "scan"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _key(config: Config) -> str:
|
|
78
|
+
recognizers = sorted(
|
|
79
|
+
r.name
|
|
80
|
+
for r in get_recognizers("modelica")
|
|
81
|
+
if r.name not in config.disabled_bundled
|
|
82
|
+
) + sorted(r.name for r in config.recognizers if "modelica" in r.applies_to)
|
|
83
|
+
payload = json.dumps(
|
|
84
|
+
[str(config.library_dir.resolve()), recognizers, CACHE_VERSION], sort_keys=True
|
|
85
|
+
)
|
|
86
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:24]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _path(config: Config) -> Path:
|
|
90
|
+
return cache_dir() / f"{_key(config)}.json"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _to_json(test: TestModel) -> dict:
|
|
94
|
+
if test.metric_tree_spec is not None:
|
|
95
|
+
raise ValueError(f"{test.model_id}: a scanned test carries no metric tree")
|
|
96
|
+
data = dataclasses.asdict(test)
|
|
97
|
+
data["source_file"] = str(test.source_file)
|
|
98
|
+
return data
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _from_json(data: dict) -> TestModel:
|
|
102
|
+
data = dict(data)
|
|
103
|
+
data["source_file"] = Path(data["source_file"])
|
|
104
|
+
return TestModel(**data)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def load_scan(config: Config) -> dict[str, TestModel] | None:
|
|
108
|
+
"""The cached scan for ``config``, or None (no cache, another DSTF
|
|
109
|
+
version, other discovery code, unreadable, or not a Modelica source)."""
|
|
110
|
+
if config.source_type != "modelica":
|
|
111
|
+
return None
|
|
112
|
+
path = _path(config)
|
|
113
|
+
try:
|
|
114
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
115
|
+
if data.get("dstf_version") != __version__:
|
|
116
|
+
return None
|
|
117
|
+
if data.get("discovery_digest") != _discovery_digest():
|
|
118
|
+
return None
|
|
119
|
+
return {m: _from_json(d) for m, d in data["tests"].items()}
|
|
120
|
+
except FileNotFoundError:
|
|
121
|
+
return None
|
|
122
|
+
except (OSError, ValueError, TypeError, KeyError) as e:
|
|
123
|
+
logger.warning("ignoring the scan cache %s: %s", path, e)
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def store_scan(config: Config, scanned: dict[str, TestModel]) -> Path | None:
|
|
128
|
+
"""Write the scan; returns the path, or None for a non-Modelica source
|
|
129
|
+
or when the cache cannot be written (a read-only home, a full disk).
|
|
130
|
+
The cache is an accelerator: a failed write is a warning, never an
|
|
131
|
+
error — an open that cannot cache still opens, and a rescan that cannot
|
|
132
|
+
cache still re-merges and finishes."""
|
|
133
|
+
if config.source_type != "modelica":
|
|
134
|
+
return None
|
|
135
|
+
path = _path(config)
|
|
136
|
+
payload = {
|
|
137
|
+
"dstf_version": __version__,
|
|
138
|
+
"discovery_digest": _discovery_digest(),
|
|
139
|
+
"library_dir": str(config.library_dir.resolve()),
|
|
140
|
+
"tests": {m: _to_json(t) for m, t in scanned.items()},
|
|
141
|
+
}
|
|
142
|
+
# Unique per thread as well as per process: a cached open's rescan
|
|
143
|
+
# thread and a second Session's in the same process write the same
|
|
144
|
+
# key, and with one name the second write replaced the first's file
|
|
145
|
+
# from under it and its own rename failed.
|
|
146
|
+
tmp = path.with_name(f"{path.stem}.{os.getpid()}.{threading.get_ident()}.tmp")
|
|
147
|
+
try:
|
|
148
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
tmp.write_text(json.dumps(payload), encoding="utf-8", newline="\n")
|
|
150
|
+
os.replace(tmp, path)
|
|
151
|
+
except OSError as e:
|
|
152
|
+
logger.warning("not caching the scan at %s: %s", path, e)
|
|
153
|
+
return None
|
|
154
|
+
return path
|