tlaplus-cli 0.5.0__tar.gz → 0.6.0__tar.gz
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.
- {tlaplus_cli-0.5.0/src/tlaplus_cli.egg-info → tlaplus_cli-0.6.0}/PKG-INFO +2 -1
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/pyproject.toml +2 -1
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/tlc.py +51 -31
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/config/schema.py +1 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/resources/default_config.yaml +1 -0
- tlaplus_cli-0.6.0/src/tlaplus_cli/tlc/formatter.py +409 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/tlc/models.py +2 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/tlc/parser.py +2 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/tlc/runner.py +36 -12
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0/src/tlaplus_cli.egg-info}/PKG-INFO +2 -1
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli.egg-info/SOURCES.txt +1 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli.egg-info/requires.txt +1 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/LICENSE +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/MANIFEST.in +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/README.md +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/setup.cfg +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/__init__.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cache/__init__.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cache/github.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cli.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/__init__.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/check_java.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/config/__init__.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/config/edit.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/config/list.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/fetch_cache/__init__.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/fetch_cache/clear.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/modules/__init__.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/modules/build.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/modules/install.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/modules/lib.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/modules/path.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/tools/__init__.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/tools/dir.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/tools/install.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/tools/list.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/tools/meta/__init__.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/tools/meta/sync.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/tools/path.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/tools/pin.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/tools/uninstall.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/cmd/tools/upgrade.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/config/__init__.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/config/loader.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/java/__init__.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/java/classpath.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/java/inspector.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/project/__init__.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/project/core.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/py.typed +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/resources/__init__.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/tlc/__init__.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/tlc/codes.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/tlc/compiler.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/tlc/sany.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/tlc/values.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/ui.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/versioning/__init__.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/versioning/api.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/versioning/downloader.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/versioning/metadata.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/versioning/paths.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/versioning/resolver.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli/versioning/schema.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli.egg-info/dependency_links.txt +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli.egg-info/entry_points.txt +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/src/tlaplus_cli.egg-info/top_level.txt +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/tests/test_build_both.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/tests/test_build_modules_path.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/tests/test_cli.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/tests/test_config.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/tests/test_config_suite.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/tests/test_modules_build_lib.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/tests/test_modules_build_show_command.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/tests/test_modules_install_community.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/tests/test_modules_lib.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/tests/test_modules_path.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/tests/test_modules_path_resolved.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/tests/test_settings.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/tests/test_tlc_modules_path.py +0 -0
- {tlaplus_cli-0.5.0 → tlaplus_cli-0.6.0}/tests/test_tlc_show_command.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tlaplus-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: TLA+ tools: download TLC, compile custom modules, run model checker
|
|
5
5
|
Author-email: Denis Nikolskiy <codeomatics@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,6 +25,7 @@ Requires-Dist: pyyaml<7.0,>=6.0
|
|
|
25
25
|
Requires-Dist: platformdirs<5.0,>=4.0
|
|
26
26
|
Requires-Dist: pydantic<3.0.0,>=2.12.5
|
|
27
27
|
Requires-Dist: truststore<1.0.0,>=0.9.0
|
|
28
|
+
Requires-Dist: rich<14.0.0,>=13.7.0
|
|
28
29
|
Dynamic: license-file
|
|
29
30
|
|
|
30
31
|
# TLA+ CLI
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tlaplus-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "TLA+ tools: download TLC, compile custom modules, run model checker"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -15,6 +15,7 @@ dependencies = [
|
|
|
15
15
|
"platformdirs>=4.0,<5.0",
|
|
16
16
|
"pydantic>=2.12.5,<3.0.0",
|
|
17
17
|
"truststore>=0.9.0,<1.0.0",
|
|
18
|
+
"rich>=13.7.0,<14.0.0",
|
|
18
19
|
]
|
|
19
20
|
authors = [
|
|
20
21
|
{name = "Denis Nikolskiy", email = "codeomatics@gmail.com"}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.live import Live
|
|
2
4
|
|
|
3
5
|
from tlaplus_cli import ui
|
|
4
6
|
from tlaplus_cli.config.loader import load_config
|
|
5
7
|
from tlaplus_cli.java.classpath import ClasspathResolver
|
|
6
8
|
from tlaplus_cli.project import find_project_root
|
|
7
9
|
from tlaplus_cli.tlc.compiler import get_tlc_jar_path
|
|
8
|
-
from tlaplus_cli.tlc.
|
|
10
|
+
from tlaplus_cli.tlc.formatter import TlcFormatter
|
|
11
|
+
from tlaplus_cli.tlc.models import CheckState, ModelCheckResult
|
|
9
12
|
from tlaplus_cli.tlc.runner import (
|
|
10
13
|
build_tlc_command,
|
|
11
14
|
get_tlc_version,
|
|
@@ -31,18 +34,6 @@ def version_callback(value: bool) -> None:
|
|
|
31
34
|
raise typer.Exit(0)
|
|
32
35
|
|
|
33
36
|
|
|
34
|
-
def print_progress(result: ModelCheckResult) -> None:
|
|
35
|
-
"""Callback to print progress from ModelCheckResult."""
|
|
36
|
-
if result.status == CheckStatus.SuccessorStatesComputing and result.initial_states_stat:
|
|
37
|
-
last_stat = result.initial_states_stat[-1]
|
|
38
|
-
# Use carriage return to update the same line for progress
|
|
39
|
-
msg = f"Progress: {last_stat.total} states generated, {last_stat.distinct} distinct states found"
|
|
40
|
-
typer.echo(f"\r{msg}", err=True, nl=False)
|
|
41
|
-
elif result.status == CheckStatus.Finished:
|
|
42
|
-
# Final newline after progress
|
|
43
|
-
typer.echo("", err=True)
|
|
44
|
-
|
|
45
|
-
|
|
46
37
|
def _show_diagnostic_info(spec: str, spec_name: str) -> None:
|
|
47
38
|
"""Show diagnostic info before running TLC."""
|
|
48
39
|
version_str = get_tlc_version() or "unknown"
|
|
@@ -68,7 +59,8 @@ def _show_diagnostic_info(spec: str, spec_name: str) -> None:
|
|
|
68
59
|
|
|
69
60
|
# PLR0912: This command handler orchestrates multiple output types (errors, warnings, results, progress);
|
|
70
61
|
# splitting it would fragment the CLI's primary user-facing logic.
|
|
71
|
-
|
|
62
|
+
# PLR0915: Too many statements.
|
|
63
|
+
def tlc( # noqa: PLR0912, PLR0915
|
|
72
64
|
spec: str = typer.Argument(help="Name of the TLA+ specification (without .tla extension)."),
|
|
73
65
|
# Typer requires the parameter in the signature to register the CLI option;
|
|
74
66
|
# since the logic is in the eager callback, the value is not used here.
|
|
@@ -80,49 +72,69 @@ def tlc( # noqa: PLR0912
|
|
|
80
72
|
is_eager=True,
|
|
81
73
|
),
|
|
82
74
|
show_command: bool = typer.Option(False, "--show-command", help="Print the command instead of executing it."),
|
|
75
|
+
_refresh_interval: float | None = typer.Option(
|
|
76
|
+
None,
|
|
77
|
+
"--refresh-interval",
|
|
78
|
+
help="Refresh interval for the output (seconds).",
|
|
79
|
+
),
|
|
80
|
+
coverage: bool = typer.Option(False, "--coverage", help="Enable code coverage visualization."),
|
|
83
81
|
) -> None:
|
|
84
82
|
"""Run TLC model checker on a TLA+ specification."""
|
|
85
83
|
# version argument is handled by version_callback (is_eager=True)
|
|
86
84
|
try:
|
|
87
|
-
_,
|
|
85
|
+
_, _spec_name = resolve_spec_file(spec)
|
|
88
86
|
except FileNotFoundError as e:
|
|
89
87
|
ui.error(str(e))
|
|
90
88
|
raise typer.Exit(1) from None
|
|
91
89
|
|
|
92
90
|
if show_command:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
cmd = build_tlc_command(spec)
|
|
92
|
+
if coverage:
|
|
93
|
+
cmd.extend(["-coverage", "1"])
|
|
94
|
+
if not cmd:
|
|
97
95
|
raise typer.Exit(1) from None
|
|
98
96
|
typer.echo(" ".join(cmd))
|
|
99
97
|
raise typer.Exit(0)
|
|
100
98
|
|
|
101
|
-
|
|
99
|
+
tlc_version = get_tlc_version() or "unknown"
|
|
100
|
+
formatter = TlcFormatter(tlc_version=tlc_version)
|
|
101
|
+
layout = formatter.create_layout()
|
|
102
102
|
|
|
103
103
|
try:
|
|
104
|
-
# We need the parser to get the final result, so we'll wrap run_tlc
|
|
105
|
-
# or capture the result from the last callback.
|
|
106
104
|
final_result: list[ModelCheckResult] = []
|
|
107
105
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
106
|
+
with Live(layout, refresh_per_second=4, screen=False):
|
|
107
|
+
|
|
108
|
+
def wrapped_callback(res: ModelCheckResult) -> None:
|
|
109
|
+
formatter.update(layout, res)
|
|
110
|
+
|
|
111
|
+
if not final_result:
|
|
112
|
+
final_result.append(res)
|
|
113
|
+
else:
|
|
114
|
+
final_result[0] = res
|
|
114
115
|
|
|
115
|
-
|
|
116
|
+
exit_code = run_tlc(spec, callback=wrapped_callback, coverage=coverage)
|
|
116
117
|
|
|
117
118
|
if final_result:
|
|
118
119
|
res = final_result[0]
|
|
120
|
+
|
|
121
|
+
if res.sany_errors:
|
|
122
|
+
console = Console()
|
|
123
|
+
spec_file, _ = resolve_spec_file(spec)
|
|
124
|
+
for sany_err in res.sany_errors:
|
|
125
|
+
console.print(formatter.render_sany_error(sany_err, spec_file))
|
|
126
|
+
|
|
119
127
|
if res.state == CheckState.Success:
|
|
120
128
|
ui.success("Model checking completed successfully.")
|
|
121
129
|
elif res.state == CheckState.Error:
|
|
122
130
|
ui.error(f"Model checking failed with {len(res.errors)} error(s).")
|
|
123
131
|
for err in res.errors:
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
if err.error_trace:
|
|
133
|
+
console = Console()
|
|
134
|
+
console.print(formatter.render_error_trace(err.error_trace))
|
|
135
|
+
else:
|
|
136
|
+
for line in err.lines:
|
|
137
|
+
typer.echo(f" {line}", err=True)
|
|
126
138
|
|
|
127
139
|
if res.warnings:
|
|
128
140
|
ui.warn(f"Found {len(res.warnings)} warning(s).")
|
|
@@ -130,6 +142,14 @@ def tlc( # noqa: PLR0912
|
|
|
130
142
|
for line in warn_info.lines:
|
|
131
143
|
typer.echo(f" {line}", err=True)
|
|
132
144
|
|
|
145
|
+
if res.coverage_stat:
|
|
146
|
+
console = Console()
|
|
147
|
+
console.print(formatter.render_coverage_table(res.coverage_stat))
|
|
148
|
+
|
|
149
|
+
# Show inline coverage for the main spec
|
|
150
|
+
spec_file, _ = resolve_spec_file(spec)
|
|
151
|
+
console.print(formatter.render_inline_coverage(res.coverage_stat, spec_file))
|
|
152
|
+
|
|
133
153
|
if res.duration:
|
|
134
154
|
ui.info(f"Duration: {res.duration}ms")
|
|
135
155
|
except FileNotFoundError as e:
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from rich.align import Align
|
|
7
|
+
from rich.console import Group
|
|
8
|
+
from rich.layout import Layout
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
from rich.tree import Tree
|
|
12
|
+
|
|
13
|
+
from tlaplus_cli.tlc.models import (
|
|
14
|
+
CheckState,
|
|
15
|
+
CoverageItem,
|
|
16
|
+
ErrorTraceItem,
|
|
17
|
+
ModelCheckResult,
|
|
18
|
+
TlaValue,
|
|
19
|
+
)
|
|
20
|
+
from tlaplus_cli.tlc.sany import SanyMessage
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class _Section:
|
|
25
|
+
"""A named section in the TLC display."""
|
|
26
|
+
|
|
27
|
+
name: str
|
|
28
|
+
renderable: Any = field(default_factory=lambda: Text(""))
|
|
29
|
+
size: int | None = None
|
|
30
|
+
|
|
31
|
+
def update(self, renderable: Any) -> None:
|
|
32
|
+
"""Update the section content."""
|
|
33
|
+
self.renderable = renderable
|
|
34
|
+
|
|
35
|
+
def __rich__(self) -> Any:
|
|
36
|
+
return self.renderable
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TlcDisplay:
|
|
40
|
+
"""A dynamic display that grows as content is added."""
|
|
41
|
+
|
|
42
|
+
def __init__(self) -> None:
|
|
43
|
+
self.children: list[_Section] = []
|
|
44
|
+
self.renderable: Any = Text("")
|
|
45
|
+
|
|
46
|
+
def __getitem__(self, name: str) -> _Section:
|
|
47
|
+
for child in self.children:
|
|
48
|
+
if child.name == name:
|
|
49
|
+
return child
|
|
50
|
+
raise KeyError(name)
|
|
51
|
+
|
|
52
|
+
def update(self, renderable: Any) -> None:
|
|
53
|
+
"""Update root renderable (used before splitting)."""
|
|
54
|
+
self.renderable = renderable
|
|
55
|
+
|
|
56
|
+
def split_column(self, *sections: _Section) -> None:
|
|
57
|
+
"""Set the active sections."""
|
|
58
|
+
self.children = list(sections)
|
|
59
|
+
|
|
60
|
+
def __rich__(self) -> Group:
|
|
61
|
+
if not self.children:
|
|
62
|
+
return Group(self.renderable)
|
|
63
|
+
return Group(*self.children)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class ValueDiff:
|
|
68
|
+
key: str | int
|
|
69
|
+
value: str
|
|
70
|
+
tag: str # "added", "modified", "unchanged"
|
|
71
|
+
children: list["ValueDiff"] = field(default_factory=list)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class LogDeduplicator:
|
|
75
|
+
"""Groups consecutive identical logs into a single line with a count badge."""
|
|
76
|
+
|
|
77
|
+
def __init__(self) -> None:
|
|
78
|
+
self.last_line: str | None = None
|
|
79
|
+
self.count: int = 1
|
|
80
|
+
|
|
81
|
+
def process(self, line: str) -> str:
|
|
82
|
+
stripped_line = line.strip()
|
|
83
|
+
if stripped_line == self.last_line:
|
|
84
|
+
self.count += 1
|
|
85
|
+
return f"{stripped_line} ({self.count})"
|
|
86
|
+
|
|
87
|
+
self.last_line = stripped_line
|
|
88
|
+
self.count = 1
|
|
89
|
+
return stripped_line
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TlcFormatter:
|
|
93
|
+
"""Generates Rich layout for TLC model checking."""
|
|
94
|
+
|
|
95
|
+
def __init__(self, tlc_version: str = "unknown") -> None:
|
|
96
|
+
self.info_dedup = LogDeduplicator()
|
|
97
|
+
self.tlc_version = tlc_version
|
|
98
|
+
|
|
99
|
+
def render_sany_error(self, error: SanyMessage, spec_path: Path) -> Text:
|
|
100
|
+
"""Render SANY error with source code highlight."""
|
|
101
|
+
result = Text()
|
|
102
|
+
result.append(f"✖ SANY Error: {error.message}\n", style="bold red")
|
|
103
|
+
result.append(f" file://{spec_path}#L{error.range_start_line}\n\n", style="dim")
|
|
104
|
+
|
|
105
|
+
if error.range_start_line is not None:
|
|
106
|
+
try:
|
|
107
|
+
with spec_path.open() as f:
|
|
108
|
+
lines = f.readlines()
|
|
109
|
+
|
|
110
|
+
start_line = max(0, error.range_start_line - 3)
|
|
111
|
+
end_line = min(len(lines), (error.range_end_line or error.range_start_line) + 2)
|
|
112
|
+
|
|
113
|
+
for i in range(start_line, end_line):
|
|
114
|
+
line_num = i + 1
|
|
115
|
+
line_content = lines[i].rstrip()
|
|
116
|
+
if line_num == error.range_start_line:
|
|
117
|
+
result.append(f"{line_num:4} │ ", style="bold")
|
|
118
|
+
result.append(f"{line_content}\n")
|
|
119
|
+
# Add caret
|
|
120
|
+
col_start = error.range_start_col or 1
|
|
121
|
+
col_end = error.range_end_col or col_start + 1
|
|
122
|
+
caret_len = max(1, col_end - col_start)
|
|
123
|
+
caret_line = " │ " + " " * (col_start - 1) + "▲" * caret_len
|
|
124
|
+
result.append(f"{caret_line}\n", style="bold red")
|
|
125
|
+
msg = f"line {error.range_start_line}, col {col_start} to {col_end}"
|
|
126
|
+
result.append(f" │ {' ' * (col_start - 1)}└── Error here: {msg}\n", style="dim")
|
|
127
|
+
else:
|
|
128
|
+
result.append(f"{line_num:4} │ {line_content}\n", style="dim")
|
|
129
|
+
|
|
130
|
+
except OSError:
|
|
131
|
+
result.append(" (Source code unavailable)\n", style="dim")
|
|
132
|
+
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
def diff_values(self, old_val: TlaValue | None, new_val: TlaValue) -> ValueDiff:
|
|
136
|
+
"""Recursively diff two TlaValue trees."""
|
|
137
|
+
if old_val is None:
|
|
138
|
+
return ValueDiff(
|
|
139
|
+
key=new_val.key,
|
|
140
|
+
value=new_val.value,
|
|
141
|
+
tag="added",
|
|
142
|
+
children=[self.diff_values(None, child) for child in new_val.children],
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if old_val.value == new_val.value:
|
|
146
|
+
return ValueDiff(
|
|
147
|
+
key=new_val.key,
|
|
148
|
+
value=new_val.value,
|
|
149
|
+
tag="unchanged",
|
|
150
|
+
children=[
|
|
151
|
+
self.diff_values(old_child, new_child)
|
|
152
|
+
for old_child, new_child in zip(old_val.children, new_val.children, strict=False)
|
|
153
|
+
],
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Values are different, but maybe some children are the same
|
|
157
|
+
tag = "modified"
|
|
158
|
+
children = []
|
|
159
|
+
# Match children by key
|
|
160
|
+
old_children_map = {c.key: c for c in old_val.children}
|
|
161
|
+
for new_child in new_val.children:
|
|
162
|
+
old_child = old_children_map.get(new_child.key)
|
|
163
|
+
children.append(self.diff_values(old_child, new_child))
|
|
164
|
+
|
|
165
|
+
return ValueDiff(
|
|
166
|
+
key=new_val.key,
|
|
167
|
+
value=new_val.value,
|
|
168
|
+
tag=tag,
|
|
169
|
+
children=children,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def render_value_diff(self, diff: ValueDiff, tree: Tree | None = None) -> Tree:
|
|
173
|
+
"""Render a ValueDiff into a Rich Tree."""
|
|
174
|
+
style = ""
|
|
175
|
+
prefix = ""
|
|
176
|
+
if diff.tag == "added":
|
|
177
|
+
style = "green"
|
|
178
|
+
prefix = "[bold green]A[/] "
|
|
179
|
+
elif diff.tag == "modified":
|
|
180
|
+
style = "yellow"
|
|
181
|
+
prefix = "[bold yellow]M[/] "
|
|
182
|
+
elif diff.tag == "unchanged":
|
|
183
|
+
style = "dim"
|
|
184
|
+
|
|
185
|
+
label = Text()
|
|
186
|
+
if prefix:
|
|
187
|
+
label.append(Text.from_markup(prefix))
|
|
188
|
+
label.append(f"{diff.key} = ")
|
|
189
|
+
label.append(diff.value, style=style)
|
|
190
|
+
|
|
191
|
+
node = Tree(label) if tree is None else tree.add(label)
|
|
192
|
+
|
|
193
|
+
for child in diff.children:
|
|
194
|
+
self.render_value_diff(child, node)
|
|
195
|
+
|
|
196
|
+
return node
|
|
197
|
+
|
|
198
|
+
def render_error_trace(self, trace: list[ErrorTraceItem]) -> Tree:
|
|
199
|
+
"""Render a full error trace as a Tree."""
|
|
200
|
+
root = Tree("Counterexample (Error Trace)")
|
|
201
|
+
last_variables: dict[str | int, TlaValue] = {}
|
|
202
|
+
|
|
203
|
+
for item in trace:
|
|
204
|
+
state_label = Text(f"State {item.num}: {item.action}")
|
|
205
|
+
if item.module:
|
|
206
|
+
state_label.append(f" in {item.module}", style="italic")
|
|
207
|
+
|
|
208
|
+
state_node = root.add(state_label)
|
|
209
|
+
|
|
210
|
+
# Sort variables to ensure consistent order
|
|
211
|
+
sorted_vars = sorted(item.variables, key=lambda v: str(v.key))
|
|
212
|
+
|
|
213
|
+
for var in sorted_vars:
|
|
214
|
+
old_var = last_variables.get(var.key)
|
|
215
|
+
diff = self.diff_values(old_var, var)
|
|
216
|
+
self.render_value_diff(diff, state_node)
|
|
217
|
+
last_variables[var.key] = var
|
|
218
|
+
|
|
219
|
+
return root
|
|
220
|
+
|
|
221
|
+
def render_coverage_table(self, coverage: list[CoverageItem]) -> Table:
|
|
222
|
+
"""Render action coverage table."""
|
|
223
|
+
table = Table(title="Action Coverage", expand=True)
|
|
224
|
+
table.add_column("Module")
|
|
225
|
+
table.add_column("Action")
|
|
226
|
+
table.add_column("Total States", justify="right")
|
|
227
|
+
table.add_column("Unique States", justify="right")
|
|
228
|
+
|
|
229
|
+
for item in coverage:
|
|
230
|
+
table.add_row(
|
|
231
|
+
item.module,
|
|
232
|
+
item.action,
|
|
233
|
+
f"{item.total:,}",
|
|
234
|
+
f"{item.distinct:,}",
|
|
235
|
+
)
|
|
236
|
+
return table
|
|
237
|
+
|
|
238
|
+
def render_inline_coverage(self, coverage: list[CoverageItem], spec_path: Path) -> Text:
|
|
239
|
+
"""Render inline spec coverage."""
|
|
240
|
+
result = Text()
|
|
241
|
+
result.append("\nInline Coverage View:\n", style="bold")
|
|
242
|
+
result.append(" Coverage Line │ Source Code\n", style="dim")
|
|
243
|
+
result.append("─────────── ─────┼─────────────────────────────────────────────\n", style="dim")
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
with spec_path.open() as f:
|
|
247
|
+
lines = f.readlines()
|
|
248
|
+
|
|
249
|
+
# Map lines to coverage
|
|
250
|
+
line_coverage: dict[int, list[CoverageItem]] = {}
|
|
251
|
+
for item in coverage:
|
|
252
|
+
if item.module == spec_path.stem:
|
|
253
|
+
for line_idx in range(item.range_start_line, item.range_end_line + 1):
|
|
254
|
+
if line_idx not in line_coverage:
|
|
255
|
+
line_coverage[line_idx] = []
|
|
256
|
+
line_coverage[line_idx].append(item)
|
|
257
|
+
|
|
258
|
+
for i, line_content in enumerate(lines):
|
|
259
|
+
line_num = i + 1
|
|
260
|
+
cov_items = line_coverage.get(line_num, [])
|
|
261
|
+
|
|
262
|
+
cov_str = ""
|
|
263
|
+
style = ""
|
|
264
|
+
line_text = line_content.rstrip()
|
|
265
|
+
if cov_items:
|
|
266
|
+
max_distinct = max(item.distinct for item in cov_items)
|
|
267
|
+
cov_str = f"{max_distinct:,}"
|
|
268
|
+
if max_distinct == 0:
|
|
269
|
+
style = "bold red"
|
|
270
|
+
line_text += " <-- UNREACHED"
|
|
271
|
+
|
|
272
|
+
result.append(f"{cov_str:>11} {line_num:5} │ ", style="dim")
|
|
273
|
+
result.append(f"{line_text}\n", style=style)
|
|
274
|
+
|
|
275
|
+
except OSError:
|
|
276
|
+
result.append(" (Source code unavailable)\n", style="dim")
|
|
277
|
+
|
|
278
|
+
return result
|
|
279
|
+
|
|
280
|
+
def create_layout(self) -> Any:
|
|
281
|
+
"""Create the initial layout structure with only the header."""
|
|
282
|
+
layout = TlcDisplay()
|
|
283
|
+
layout.update(Text(f"Starting TLC model checker (version {self.tlc_version}).", style="bold"))
|
|
284
|
+
return layout
|
|
285
|
+
|
|
286
|
+
def update(self, layout: Any, result: ModelCheckResult) -> None:
|
|
287
|
+
"""Main update entry point that manages layout structure and content."""
|
|
288
|
+
self._ensure_layout_structure(layout, result)
|
|
289
|
+
self.update_header(layout, result)
|
|
290
|
+
if self._has_child(layout, "clock"):
|
|
291
|
+
self.update_clock(layout, result)
|
|
292
|
+
if self._has_child(layout, "logs"):
|
|
293
|
+
self.update_logs(layout, result)
|
|
294
|
+
if self._has_child(layout, "stats"):
|
|
295
|
+
self.update_stats(layout, result)
|
|
296
|
+
|
|
297
|
+
def _has_child(self, layout: Any, name: str) -> bool:
|
|
298
|
+
"""Check if a layout has a child with the given name."""
|
|
299
|
+
return any(child.name == name for child in layout.children)
|
|
300
|
+
|
|
301
|
+
def _ensure_layout_structure(self, layout: Any, result: ModelCheckResult) -> None:
|
|
302
|
+
"""Dynamically add sections as data becomes available."""
|
|
303
|
+
has_logs = bool(result.output_lines)
|
|
304
|
+
has_stats = bool(result.initial_states_stat)
|
|
305
|
+
has_clock = bool(result.start_date_time or result.process_info or result.duration)
|
|
306
|
+
|
|
307
|
+
# Build expected structure in desired order:
|
|
308
|
+
# 1. Header
|
|
309
|
+
# 2. Status Info (clock)
|
|
310
|
+
# 3. TLC instance info (logs)
|
|
311
|
+
# 4. State Space Progress (stats) - grows infinitely at the bottom
|
|
312
|
+
new_layouts = [_Section(name="header", size=2)]
|
|
313
|
+
if has_clock:
|
|
314
|
+
new_layouts.append(_Section(name="clock", size=7))
|
|
315
|
+
if has_logs:
|
|
316
|
+
new_layouts.append(_Section(name="logs"))
|
|
317
|
+
if has_stats:
|
|
318
|
+
new_layouts.append(_Section(name="stats"))
|
|
319
|
+
|
|
320
|
+
# Check if we need to update
|
|
321
|
+
current_names = [lay.name for lay in layout.children]
|
|
322
|
+
expected_names = [lay.name for lay in new_layouts]
|
|
323
|
+
|
|
324
|
+
if current_names != expected_names:
|
|
325
|
+
layout.split_column(*new_layouts)
|
|
326
|
+
|
|
327
|
+
def update_header(self, layout: Any, _result: ModelCheckResult) -> None:
|
|
328
|
+
"""Update header."""
|
|
329
|
+
# _result is unused for now but kept for signature consistency
|
|
330
|
+
header_text = Text(f"Starting TLC model checker (version {self.tlc_version}).", style="bold")
|
|
331
|
+
if self._has_child(layout, "header"):
|
|
332
|
+
layout["header"].update(header_text)
|
|
333
|
+
else:
|
|
334
|
+
layout.update(header_text)
|
|
335
|
+
|
|
336
|
+
def update_stats(self, layout: Layout, result: ModelCheckResult) -> None:
|
|
337
|
+
table = Table(title="State Space Progress", expand=True)
|
|
338
|
+
table.add_column("Time")
|
|
339
|
+
table.add_column("Diameter", justify="right")
|
|
340
|
+
table.add_column("Found", justify="right")
|
|
341
|
+
table.add_column("Distinct", justify="right")
|
|
342
|
+
table.add_column("Queue", justify="right")
|
|
343
|
+
|
|
344
|
+
for item in result.initial_states_stat:
|
|
345
|
+
table.add_row(
|
|
346
|
+
item.timestamp,
|
|
347
|
+
str(item.diameter),
|
|
348
|
+
f"{item.total:,}",
|
|
349
|
+
f"{item.distinct:,}",
|
|
350
|
+
f"{item.queue_size:,}",
|
|
351
|
+
)
|
|
352
|
+
layout["stats"].update(table)
|
|
353
|
+
|
|
354
|
+
def update_logs(self, layout: Layout, result: ModelCheckResult) -> None:
|
|
355
|
+
"""Update logs panel with TLC output."""
|
|
356
|
+
deduped_logs: list[str] = []
|
|
357
|
+
temp_dedup = LogDeduplicator()
|
|
358
|
+
for line in result.output_lines:
|
|
359
|
+
processed = temp_dedup.process(line)
|
|
360
|
+
if temp_dedup.count > 1 and deduped_logs:
|
|
361
|
+
deduped_logs[-1] = processed
|
|
362
|
+
else:
|
|
363
|
+
deduped_logs.append(processed)
|
|
364
|
+
|
|
365
|
+
log_lines = Text()
|
|
366
|
+
for line in deduped_logs[-10:]:
|
|
367
|
+
log_lines.append(f"{line}\n")
|
|
368
|
+
|
|
369
|
+
layout["logs"].update(Group(Align.center(Text("TLC instance info", style="bold")), log_lines))
|
|
370
|
+
|
|
371
|
+
def update_clock(self, layout: Layout, result: ModelCheckResult) -> None:
|
|
372
|
+
"""Update info panel at the bottom with clock and TLC stats."""
|
|
373
|
+
layout["clock"].update(StatusInfoRenderable(result))
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class StatusInfoRenderable:
|
|
377
|
+
"""Renderable that computes elapsed time dynamically."""
|
|
378
|
+
|
|
379
|
+
def __init__(self, result: ModelCheckResult) -> None:
|
|
380
|
+
self.result = result
|
|
381
|
+
|
|
382
|
+
def __rich__(self) -> Group:
|
|
383
|
+
"""Render the status info group."""
|
|
384
|
+
status_text = self.result.status.name
|
|
385
|
+
if self.result.state == CheckState.Stopped:
|
|
386
|
+
status_text = "canceled"
|
|
387
|
+
|
|
388
|
+
if not self.result.start_date_time:
|
|
389
|
+
elapsed_str = "00:00:00"
|
|
390
|
+
elapsed_style = "dim"
|
|
391
|
+
else:
|
|
392
|
+
if self.result.state == CheckState.Running:
|
|
393
|
+
elapsed = datetime.now() - self.result.start_date_time
|
|
394
|
+
else:
|
|
395
|
+
elapsed = (self.result.end_date_time or datetime.now()) - self.result.start_date_time
|
|
396
|
+
seconds = int(elapsed.total_seconds())
|
|
397
|
+
hours, remainder = divmod(seconds, 3600)
|
|
398
|
+
minutes, seconds = divmod(remainder, 60)
|
|
399
|
+
elapsed_str = f"{hours:02}:{minutes:02}:{seconds:02}"
|
|
400
|
+
elapsed_style = "bold cyan"
|
|
401
|
+
|
|
402
|
+
info_text = Text()
|
|
403
|
+
info_text.append(f"Elapsed time: {elapsed_str}\n", style=elapsed_style)
|
|
404
|
+
info_text.append(f"Status: {status_text}\n")
|
|
405
|
+
|
|
406
|
+
if self.result.state == CheckState.Running:
|
|
407
|
+
info_text.append("Stop with Ctrl + C", style="yellow")
|
|
408
|
+
|
|
409
|
+
return Group(Align.center(Text("Status Info", style="bold")), info_text)
|
|
@@ -6,6 +6,7 @@ Ported and adapted from ts-output-parser/model/check.ts.
|
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from enum import Enum
|
|
9
|
+
from typing import Any
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class CheckState(Enum):
|
|
@@ -127,4 +128,5 @@ class ModelCheckResult:
|
|
|
127
128
|
workers_count: int = 0
|
|
128
129
|
collision_probability: str | None = None
|
|
129
130
|
output_lines: list[str] = field(default_factory=list)
|
|
131
|
+
sany_errors: list[Any] = field(default_factory=list)
|
|
130
132
|
trace_file_path: str | None = None
|
|
@@ -139,6 +139,8 @@ class TlcParser:
|
|
|
139
139
|
if code == codes.TLC_STARTING.num:
|
|
140
140
|
self._parse_starting(message.lines)
|
|
141
141
|
self.result.status = CheckStatus.Starting
|
|
142
|
+
elif code == codes.TLC_VERSION.num:
|
|
143
|
+
self.result.process_info = "".join(message.lines).strip()
|
|
142
144
|
elif code == codes.TLC_PROGRESS_STATS.num:
|
|
143
145
|
self._parse_progress_stats(message.lines)
|
|
144
146
|
self.result.status = CheckStatus.SuccessorStatesComputing
|
|
@@ -8,8 +8,9 @@ from tlaplus_cli.java import validate_java_version
|
|
|
8
8
|
from tlaplus_cli.java.classpath import ClasspathResolver, ResolveMode
|
|
9
9
|
from tlaplus_cli.project import find_project_root
|
|
10
10
|
from tlaplus_cli.tlc.compiler import get_tlc_jar_path
|
|
11
|
-
from tlaplus_cli.tlc.models import ModelCheckResult
|
|
11
|
+
from tlaplus_cli.tlc.models import CheckState, ModelCheckResult
|
|
12
12
|
from tlaplus_cli.tlc.parser import TlcParser
|
|
13
|
+
from tlaplus_cli.tlc.sany import SanyParser
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def resolve_spec_file(spec: str) -> tuple[Path, str]:
|
|
@@ -66,7 +67,7 @@ def build_tlc_command(spec: str) -> list[str]:
|
|
|
66
67
|
]
|
|
67
68
|
|
|
68
69
|
|
|
69
|
-
def run_tlc(spec: str, callback: Callable[[ModelCheckResult], None] | None = None) -> int:
|
|
70
|
+
def run_tlc(spec: str, callback: Callable[[ModelCheckResult], None] | None = None, coverage: bool = False) -> int: # noqa: PLR0912
|
|
70
71
|
"""Run TLC model checker on a TLA+ specification. Returns exit code."""
|
|
71
72
|
config = load_config()
|
|
72
73
|
validate_java_version(config.java.min_version)
|
|
@@ -76,12 +77,15 @@ def run_tlc(spec: str, callback: Callable[[ModelCheckResult], None] | None = Non
|
|
|
76
77
|
|
|
77
78
|
# Ensure tool mode is enabled for structured output
|
|
78
79
|
if "-tool" not in cmd:
|
|
79
|
-
# Insert -tool after java class name or at some reasonable position
|
|
80
|
-
# TLC usually expects options before the spec file name.
|
|
81
|
-
# cmd is ["java", ..., "tlc2.TLC", "spec.tla"]
|
|
82
80
|
cmd.insert(-1, "-tool")
|
|
83
81
|
|
|
82
|
+
if coverage and "-coverage" not in cmd:
|
|
83
|
+
# coverage expects a number of minutes between stats, default to 1
|
|
84
|
+
cmd.insert(-1, "-coverage")
|
|
85
|
+
cmd.insert(-1, "1")
|
|
86
|
+
|
|
84
87
|
parser = TlcParser()
|
|
88
|
+
sany_parser = SanyParser()
|
|
85
89
|
|
|
86
90
|
try:
|
|
87
91
|
with subprocess.Popen(
|
|
@@ -92,15 +96,35 @@ def run_tlc(spec: str, callback: Callable[[ModelCheckResult], None] | None = Non
|
|
|
92
96
|
text=True,
|
|
93
97
|
bufsize=1,
|
|
94
98
|
) as process:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
try:
|
|
100
|
+
if process.stdout:
|
|
101
|
+
for line in process.stdout:
|
|
102
|
+
if line.startswith("@!@!@"):
|
|
103
|
+
parser.process_line(line)
|
|
104
|
+
else:
|
|
105
|
+
sany_parser.process_line(line)
|
|
106
|
+
# We also pass non-SANY, non-TLC-tool-mode lines to the TLC parser
|
|
107
|
+
# so they end up in output_lines
|
|
108
|
+
parser.process_line(line)
|
|
109
|
+
|
|
110
|
+
if callback:
|
|
111
|
+
res = parser.get_result()
|
|
112
|
+
res.sany_errors = sany_parser.get_errors()
|
|
113
|
+
callback(res)
|
|
114
|
+
except KeyboardInterrupt:
|
|
115
|
+
# Stop the TLC process gracefully
|
|
116
|
+
process.terminate()
|
|
117
|
+
# Parse remaining output if any
|
|
118
|
+
if process.stdout:
|
|
119
|
+
for line in process.stdout:
|
|
98
120
|
parser.process_line(line)
|
|
99
|
-
|
|
100
|
-
|
|
121
|
+
if callback:
|
|
122
|
+
callback(parser.get_result())
|
|
101
123
|
|
|
102
|
-
|
|
103
|
-
|
|
124
|
+
if callback:
|
|
125
|
+
res = parser.get_result()
|
|
126
|
+
res.state = CheckState.Stopped
|
|
127
|
+
callback(res)
|
|
104
128
|
|
|
105
129
|
process.wait()
|
|
106
130
|
return process.returncode
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tlaplus-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: TLA+ tools: download TLC, compile custom modules, run model checker
|
|
5
5
|
Author-email: Denis Nikolskiy <codeomatics@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,6 +25,7 @@ Requires-Dist: pyyaml<7.0,>=6.0
|
|
|
25
25
|
Requires-Dist: platformdirs<5.0,>=4.0
|
|
26
26
|
Requires-Dist: pydantic<3.0.0,>=2.12.5
|
|
27
27
|
Requires-Dist: truststore<1.0.0,>=0.9.0
|
|
28
|
+
Requires-Dist: rich<14.0.0,>=13.7.0
|
|
28
29
|
Dynamic: license-file
|
|
29
30
|
|
|
30
31
|
# TLA+ CLI
|
|
@@ -50,6 +50,7 @@ src/tlaplus_cli/resources/default_config.yaml
|
|
|
50
50
|
src/tlaplus_cli/tlc/__init__.py
|
|
51
51
|
src/tlaplus_cli/tlc/codes.py
|
|
52
52
|
src/tlaplus_cli/tlc/compiler.py
|
|
53
|
+
src/tlaplus_cli/tlc/formatter.py
|
|
53
54
|
src/tlaplus_cli/tlc/models.py
|
|
54
55
|
src/tlaplus_cli/tlc/parser.py
|
|
55
56
|
src/tlaplus_cli/tlc/runner.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|