tlaplus-cli 0.3.3__tar.gz → 0.3.4__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.3.3/src/tlaplus_cli.egg-info → tlaplus_cli-0.3.4}/PKG-INFO +1 -1
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/pyproject.toml +1 -1
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/src/tlaplus_cli/tools_manager.py +21 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/src/tlaplus_cli/version_manager.py +119 -1
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4/src/tlaplus_cli.egg-info}/PKG-INFO +1 -1
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/tests/test_tools_manager.py +194 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/tests/test_version_manager.py +129 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/LICENSE +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/MANIFEST.in +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/README.md +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/setup.cfg +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/src/tlaplus_cli/__init__.py +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/src/tlaplus_cli/build_tlc_module.py +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/src/tlaplus_cli/check_java.py +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/src/tlaplus_cli/cli.py +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/src/tlaplus_cli/config.py +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/src/tlaplus_cli/project.py +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/src/tlaplus_cli/py.typed +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/src/tlaplus_cli/resources/__init__.py +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/src/tlaplus_cli/resources/default_config.yaml +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/src/tlaplus_cli/run_tlc.py +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/src/tlaplus_cli/settings.py +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/src/tlaplus_cli.egg-info/SOURCES.txt +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/src/tlaplus_cli.egg-info/dependency_links.txt +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/src/tlaplus_cli.egg-info/entry_points.txt +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/src/tlaplus_cli.egg-info/requires.txt +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/src/tlaplus_cli.egg-info/top_level.txt +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/tests/test_build_tlc_module.py +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/tests/test_check_java.py +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/tests/test_cli.py +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/tests/test_java_module_naming.py +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/tests/test_project.py +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/tests/test_run_tlc.py +0 -0
- {tlaplus_cli-0.3.3 → tlaplus_cli-0.3.4}/tests/test_spec_resolution.py +0 -0
|
@@ -12,9 +12,11 @@ from tlaplus_cli.version_manager import (
|
|
|
12
12
|
clear_cache,
|
|
13
13
|
clear_pin,
|
|
14
14
|
download_version,
|
|
15
|
+
download_version_from_url,
|
|
15
16
|
fetch_remote_versions,
|
|
16
17
|
get_pinned_version_dir,
|
|
17
18
|
get_tools_dir,
|
|
19
|
+
is_url,
|
|
18
20
|
list_local_versions,
|
|
19
21
|
read_version_metadata,
|
|
20
22
|
resolve_latest_version,
|
|
@@ -100,6 +102,25 @@ def install(
|
|
|
100
102
|
version: str = typer.Argument(None),
|
|
101
103
|
force: bool = typer.Option(False, "--force", "-f", help="Force re-download even if already installed."),
|
|
102
104
|
) -> None:
|
|
105
|
+
# --- URL install branch ---
|
|
106
|
+
if version and is_url(version):
|
|
107
|
+
try:
|
|
108
|
+
version_dir = download_version_from_url(version)
|
|
109
|
+
typer.echo("Download complete.")
|
|
110
|
+
except ValueError as e:
|
|
111
|
+
typer.echo(f"Error: {e}", err=True)
|
|
112
|
+
raise typer.Exit(1) from e
|
|
113
|
+
except Exception as e:
|
|
114
|
+
typer.echo(f"Error: Failed to download: {e}", err=True)
|
|
115
|
+
raise typer.Exit(1) from e
|
|
116
|
+
|
|
117
|
+
pinned_dir = get_pinned_version_dir()
|
|
118
|
+
if pinned_dir is None:
|
|
119
|
+
typer.echo(f"Auto-pinning newly installed version {version_dir.name}")
|
|
120
|
+
set_pin(version_dir)
|
|
121
|
+
return
|
|
122
|
+
# --- end URL branch ---
|
|
123
|
+
|
|
103
124
|
config = load_config()
|
|
104
125
|
versions, _ = fetch_remote_versions(config.tla.urls.tags, config.tla.urls.releases, config.tla.urls.per_page)
|
|
105
126
|
if not versions:
|
|
@@ -6,8 +6,10 @@ import shutil
|
|
|
6
6
|
import subprocess
|
|
7
7
|
import time
|
|
8
8
|
from dataclasses import asdict, dataclass
|
|
9
|
+
from datetime import UTC, datetime
|
|
9
10
|
from enum import Enum
|
|
10
11
|
from typing import TYPE_CHECKING, Any
|
|
12
|
+
from urllib.parse import urlparse
|
|
11
13
|
|
|
12
14
|
import requests
|
|
13
15
|
import typer
|
|
@@ -22,6 +24,121 @@ if TYPE_CHECKING:
|
|
|
22
24
|
_SEMVER_RE = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)")
|
|
23
25
|
|
|
24
26
|
|
|
27
|
+
def is_url(text: str) -> bool:
|
|
28
|
+
"""Return True if *text* looks like an HTTP(S) URL."""
|
|
29
|
+
return text.lower().startswith(("http://", "https://"))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def extract_version_from_url(url: str) -> str | None:
|
|
33
|
+
"""Scan URL path segments for a semver-like segment (e.g. 'v1.8.0').
|
|
34
|
+
|
|
35
|
+
Returns the first matching segment, or None if none is found.
|
|
36
|
+
"""
|
|
37
|
+
path = urlparse(url).path
|
|
38
|
+
for segment in path.split("/"):
|
|
39
|
+
if _SEMVER_RE.match(segment):
|
|
40
|
+
return segment
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _utc_now_iso() -> str:
|
|
45
|
+
"""Return the current UTC time in ISO 8601 format, e.g. '2026-04-06T12:51:28Z'."""
|
|
46
|
+
now = datetime.now(tz=UTC)
|
|
47
|
+
return now.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def download_version_from_url(url: str) -> Path:
|
|
51
|
+
"""Download tla2tools.jar from *url* and store it in a timestamped version directory.
|
|
52
|
+
|
|
53
|
+
The version name is extracted from URL path segments. The tag (directory suffix) is
|
|
54
|
+
the ISO 8601 download timestamp.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
ValueError: if no semver segment can be found in the URL.
|
|
58
|
+
"""
|
|
59
|
+
version_name = extract_version_from_url(url)
|
|
60
|
+
if version_name is None:
|
|
61
|
+
msg = (
|
|
62
|
+
'could not extract a version name from the URL. '
|
|
63
|
+
'The URL must contain a version segment (e.g. "v1.8.0").'
|
|
64
|
+
)
|
|
65
|
+
raise ValueError(msg)
|
|
66
|
+
|
|
67
|
+
tag = _utc_now_iso()
|
|
68
|
+
tools_dir = get_tools_dir()
|
|
69
|
+
version_dir = tools_dir / f"{version_name}-{tag}"
|
|
70
|
+
version_dir.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
jar_path = version_dir / "tla2tools.jar"
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
response = requests.get(
|
|
75
|
+
url,
|
|
76
|
+
stream=True,
|
|
77
|
+
timeout=30,
|
|
78
|
+
headers={"User-Agent": "tlaplus-cli"},
|
|
79
|
+
)
|
|
80
|
+
response.raise_for_status()
|
|
81
|
+
total = int(response.headers.get("content-length", 0))
|
|
82
|
+
|
|
83
|
+
with Progress(
|
|
84
|
+
"[progress.description]{task.description}",
|
|
85
|
+
BarColumn(),
|
|
86
|
+
DownloadColumn(),
|
|
87
|
+
TransferSpeedColumn(),
|
|
88
|
+
) as progress:
|
|
89
|
+
task = progress.add_task(f"Downloading {version_name}...", total=total or None)
|
|
90
|
+
with jar_path.open("wb") as f:
|
|
91
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
92
|
+
f.write(chunk)
|
|
93
|
+
progress.update(task, advance=len(chunk))
|
|
94
|
+
except Exception:
|
|
95
|
+
shutil.rmtree(version_dir, ignore_errors=True)
|
|
96
|
+
raise
|
|
97
|
+
|
|
98
|
+
write_version_metadata_from_url(version_dir, version_name=version_name, tag=tag, url=url)
|
|
99
|
+
return version_dir
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def write_version_metadata_from_url(
|
|
103
|
+
version_dir: Path,
|
|
104
|
+
*,
|
|
105
|
+
version_name: str,
|
|
106
|
+
tag: str,
|
|
107
|
+
url: str,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Write meta-tla2tools.json for a URL-sourced install."""
|
|
110
|
+
tlc2_version_string = ""
|
|
111
|
+
try:
|
|
112
|
+
result = subprocess.run(
|
|
113
|
+
["java", "-cp", "tla2tools.jar", "tlc2.TLC", "-version"],
|
|
114
|
+
cwd=version_dir,
|
|
115
|
+
capture_output=True,
|
|
116
|
+
text=True,
|
|
117
|
+
check=False,
|
|
118
|
+
)
|
|
119
|
+
if result.stdout:
|
|
120
|
+
tlc2_version_string = result.stdout.strip().split("\n")[0]
|
|
121
|
+
except Exception as e:
|
|
122
|
+
typer.echo(f"Warning: Failed to extract TLC version string: {e}", err=True)
|
|
123
|
+
|
|
124
|
+
meta_file = version_dir / "meta-tla2tools.json"
|
|
125
|
+
metadata = {
|
|
126
|
+
"tag_name": version_name,
|
|
127
|
+
"sha": "",
|
|
128
|
+
"published_at": "",
|
|
129
|
+
"tlc2_version_string": tlc2_version_string,
|
|
130
|
+
"prerelease": False,
|
|
131
|
+
"download_url": url,
|
|
132
|
+
"tag": tag,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
with meta_file.open("w") as f:
|
|
137
|
+
json.dump(metadata, f, indent=2)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
typer.echo(f"Warning: Failed to write metadata: {e}", err=True)
|
|
140
|
+
|
|
141
|
+
|
|
25
142
|
def _parse_semver(name: str) -> tuple[int, int, int] | None:
|
|
26
143
|
"""Try to extract (major, minor, patch) from a version name like 'v1.8.0'."""
|
|
27
144
|
m = _SEMVER_RE.match(name)
|
|
@@ -173,7 +290,8 @@ def list_local_versions() -> list[LocalVersion]:
|
|
|
173
290
|
result = []
|
|
174
291
|
for d in tools_dir.iterdir():
|
|
175
292
|
if d.is_dir() and not d.is_symlink():
|
|
176
|
-
|
|
293
|
+
# Split on the FIRST hyphen so timestamp suffixes are preserved whole
|
|
294
|
+
parts = d.name.split("-", 1)
|
|
177
295
|
if len(parts) == 2:
|
|
178
296
|
result.append(LocalVersion(name=parts[0], short_sha=parts[1], path=d))
|
|
179
297
|
return result
|
|
@@ -7,6 +7,7 @@ import pytest
|
|
|
7
7
|
from typer.testing import CliRunner
|
|
8
8
|
|
|
9
9
|
from tlaplus_cli.cli import app
|
|
10
|
+
from tlaplus_cli.version_manager import download_version_from_url
|
|
10
11
|
|
|
11
12
|
runner = CliRunner()
|
|
12
13
|
|
|
@@ -634,3 +635,196 @@ def test_uninstall_all_flag(mock_cache):
|
|
|
634
635
|
assert result.exit_code == 0
|
|
635
636
|
assert not v1.exists()
|
|
636
637
|
assert not v2.exists()
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
# ---------------------------------------------------------------------------
|
|
641
|
+
# Custom URL install — unit tests for download_version_from_url
|
|
642
|
+
# ---------------------------------------------------------------------------
|
|
643
|
+
|
|
644
|
+
def test_download_version_from_url_creates_dir(mocker, mock_cache):
|
|
645
|
+
"""A valid URL creates the expected version directory."""
|
|
646
|
+
|
|
647
|
+
url = "https://example.com/v1.9.0/tla2tools.jar"
|
|
648
|
+
fake_ts = "2026-04-06T12:51:28Z"
|
|
649
|
+
|
|
650
|
+
mocker.patch("tlaplus_cli.version_manager._utc_now_iso", return_value=fake_ts)
|
|
651
|
+
mock_response = mocker.MagicMock()
|
|
652
|
+
mock_response.raise_for_status = mocker.MagicMock()
|
|
653
|
+
mock_response.headers = {"content-length": "16"}
|
|
654
|
+
mock_response.iter_content.return_value = [b"fake jar content"]
|
|
655
|
+
mocker.patch("tlaplus_cli.version_manager.requests.get", return_value=mock_response)
|
|
656
|
+
mocker.patch("tlaplus_cli.version_manager.write_version_metadata_from_url")
|
|
657
|
+
|
|
658
|
+
result_dir = download_version_from_url(url)
|
|
659
|
+
tools_dir = mock_cache / "tools"
|
|
660
|
+
assert result_dir == tools_dir / f"v1.9.0-{fake_ts}"
|
|
661
|
+
assert result_dir.exists()
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def test_download_version_from_url_writes_jar(mocker, mock_cache):
|
|
665
|
+
"""The jar file is written into the version directory."""
|
|
666
|
+
|
|
667
|
+
url = "https://example.com/v1.9.0/tla2tools.jar"
|
|
668
|
+
fake_ts = "2026-04-06T12:51:28Z"
|
|
669
|
+
mocker.patch("tlaplus_cli.version_manager._utc_now_iso", return_value=fake_ts)
|
|
670
|
+
mock_response = mocker.MagicMock()
|
|
671
|
+
mock_response.raise_for_status = mocker.MagicMock()
|
|
672
|
+
mock_response.headers = {"content-length": "16"}
|
|
673
|
+
mock_response.iter_content.return_value = [b"fake jar content"]
|
|
674
|
+
mocker.patch("tlaplus_cli.version_manager.requests.get", return_value=mock_response)
|
|
675
|
+
mocker.patch("tlaplus_cli.version_manager.write_version_metadata_from_url")
|
|
676
|
+
|
|
677
|
+
result_dir = download_version_from_url(url)
|
|
678
|
+
jar = result_dir / "tla2tools.jar"
|
|
679
|
+
assert jar.exists()
|
|
680
|
+
assert jar.read_bytes() == b"fake jar content"
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def test_download_version_from_url_no_version_raises(mocker, mock_cache):
|
|
684
|
+
"""A URL with no semver segment raises ValueError."""
|
|
685
|
+
|
|
686
|
+
url = "https://example.com/latest/tla2tools.jar"
|
|
687
|
+
with pytest.raises(ValueError, match="version"):
|
|
688
|
+
download_version_from_url(url)
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def test_download_version_from_url_cleans_up_on_failure(mocker, mock_cache):
|
|
692
|
+
"""The version directory is removed if the download fails."""
|
|
693
|
+
|
|
694
|
+
url = "https://example.com/v1.9.0/tla2tools.jar"
|
|
695
|
+
fake_ts = "2026-04-06T12:51:28Z"
|
|
696
|
+
mocker.patch("tlaplus_cli.version_manager._utc_now_iso", return_value=fake_ts)
|
|
697
|
+
mocker.patch(
|
|
698
|
+
"tlaplus_cli.version_manager.requests.get",
|
|
699
|
+
side_effect=Exception("network error"),
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
with pytest.raises(Exception, match="network error"):
|
|
703
|
+
download_version_from_url(url)
|
|
704
|
+
|
|
705
|
+
tools_dir = mock_cache / "tools"
|
|
706
|
+
version_dir = tools_dir / f"v1.9.0-{fake_ts}"
|
|
707
|
+
assert not version_dir.exists()
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
# ---------------------------------------------------------------------------
|
|
711
|
+
# Custom URL install — CLI integration tests
|
|
712
|
+
# ---------------------------------------------------------------------------
|
|
713
|
+
|
|
714
|
+
def test_install_from_url_success(mock_cache, mock_load_config, mocker):
|
|
715
|
+
"""Installing from a valid URL echoes 'Download complete' and auto-pins."""
|
|
716
|
+
url = "https://example.com/v1.9.0/tla2tools.jar"
|
|
717
|
+
fake_ts = "2026-04-06T12:51:28Z"
|
|
718
|
+
|
|
719
|
+
def _fake_download_url(u):
|
|
720
|
+
tools_dir = mock_cache / "tools"
|
|
721
|
+
version_dir = tools_dir / f"v1.9.0-{fake_ts}"
|
|
722
|
+
version_dir.mkdir(parents=True, exist_ok=True)
|
|
723
|
+
(version_dir / "tla2tools.jar").write_bytes(b"jar")
|
|
724
|
+
return version_dir
|
|
725
|
+
|
|
726
|
+
mocker.patch(
|
|
727
|
+
"tlaplus_cli.tools_manager.download_version_from_url",
|
|
728
|
+
side_effect=_fake_download_url,
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
result = runner.invoke(app, ["tools", "install", url])
|
|
732
|
+
assert result.exit_code == 0
|
|
733
|
+
assert "Download complete" in result.stdout
|
|
734
|
+
assert "Auto-pinning" in result.stdout
|
|
735
|
+
|
|
736
|
+
pin_file = mock_cache / "tools" / "tools-pinned-version.txt"
|
|
737
|
+
assert pin_file.read_text().strip() == f"v1.9.0-{fake_ts}"
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def test_install_from_url_no_version_segment(mock_cache, mock_load_config, mocker):
|
|
741
|
+
"""A URL without a semver segment prints an error and exits with code 1."""
|
|
742
|
+
url = "https://example.com/latest/tla2tools.jar"
|
|
743
|
+
|
|
744
|
+
mocker.patch(
|
|
745
|
+
"tlaplus_cli.tools_manager.download_version_from_url",
|
|
746
|
+
side_effect=ValueError(
|
|
747
|
+
'could not extract a version name from the URL. '
|
|
748
|
+
'The URL must contain a version segment (e.g. "v1.8.0").'
|
|
749
|
+
),
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
result = runner.invoke(app, ["tools", "install", url])
|
|
753
|
+
assert result.exit_code == 1
|
|
754
|
+
assert "could not extract a version name" in result.output
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def test_install_from_url_does_not_move_existing_pin(mock_cache, mock_load_config, mocker):
|
|
758
|
+
"""URL install does NOT auto-pin when a pin already exists."""
|
|
759
|
+
tools_dir = mock_cache / "tools"
|
|
760
|
+
tools_dir.mkdir(parents=True, exist_ok=True)
|
|
761
|
+
pin_file = tools_dir / "tools-pinned-version.txt"
|
|
762
|
+
pin_file.write_text("v1.8.0-aaaaaaa")
|
|
763
|
+
(tools_dir / "v1.8.0-aaaaaaa").mkdir()
|
|
764
|
+
|
|
765
|
+
url = "https://example.com/v1.9.0/tla2tools.jar"
|
|
766
|
+
fake_ts = "2026-04-06T12:51:28Z"
|
|
767
|
+
|
|
768
|
+
def _fake_download_url(u):
|
|
769
|
+
version_dir = tools_dir / f"v1.9.0-{fake_ts}"
|
|
770
|
+
version_dir.mkdir(parents=True, exist_ok=True)
|
|
771
|
+
(version_dir / "tla2tools.jar").write_bytes(b"jar")
|
|
772
|
+
return version_dir
|
|
773
|
+
|
|
774
|
+
mocker.patch(
|
|
775
|
+
"tlaplus_cli.tools_manager.download_version_from_url",
|
|
776
|
+
side_effect=_fake_download_url,
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
result = runner.invoke(app, ["tools", "install", url])
|
|
780
|
+
assert result.exit_code == 0
|
|
781
|
+
assert pin_file.read_text().strip() == "v1.8.0-aaaaaaa"
|
|
782
|
+
assert "Auto-pinning" not in result.stdout
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def test_install_from_url_network_error(mock_cache, mock_load_config, mocker):
|
|
786
|
+
"""A network error during URL download prints the error and exits with code 1."""
|
|
787
|
+
url = "https://example.com/v1.9.0/tla2tools.jar"
|
|
788
|
+
|
|
789
|
+
mocker.patch(
|
|
790
|
+
"tlaplus_cli.tools_manager.download_version_from_url",
|
|
791
|
+
side_effect=Exception("connection refused"),
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
result = runner.invoke(app, ["tools", "install", url])
|
|
795
|
+
assert result.exit_code == 1
|
|
796
|
+
assert "Failed to download" in result.output
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def test_list_shows_url_installed_version(mock_github_api, mock_cache, mock_load_config):
|
|
800
|
+
"""URL-installed version (timestamp tag) appears correctly in tla tools list."""
|
|
801
|
+
tools_dir = mock_cache / "tools"
|
|
802
|
+
tools_dir.mkdir(parents=True, exist_ok=True)
|
|
803
|
+
|
|
804
|
+
ts = "2026-04-06T12:51:28Z"
|
|
805
|
+
url_dir = tools_dir / f"v1.9.0-{ts}"
|
|
806
|
+
url_dir.mkdir()
|
|
807
|
+
(url_dir / "tla2tools.jar").write_bytes(b"jar")
|
|
808
|
+
|
|
809
|
+
result = runner.invoke(app, ["tools", "list"])
|
|
810
|
+
assert result.exit_code == 0
|
|
811
|
+
assert "v1.9.0" in result.stdout
|
|
812
|
+
assert ts in result.stdout
|
|
813
|
+
assert "local only" in result.stdout
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def test_meta_sync_skips_url_installed_version(mock_github_api, mock_cache, mock_load_config, mocker):
|
|
817
|
+
"""meta sync gracefully skips versions not found in the remote list."""
|
|
818
|
+
tools_dir = mock_cache / "tools"
|
|
819
|
+
tools_dir.mkdir(parents=True, exist_ok=True)
|
|
820
|
+
|
|
821
|
+
ts = "2026-04-06T12:51:28Z"
|
|
822
|
+
url_dir = tools_dir / f"v1.9.0-{ts}"
|
|
823
|
+
url_dir.mkdir()
|
|
824
|
+
(url_dir / "tla2tools.jar").write_bytes(b"jar")
|
|
825
|
+
|
|
826
|
+
mocker.patch("tlaplus_cli.tools_manager.write_version_metadata")
|
|
827
|
+
|
|
828
|
+
result = runner.invoke(app, ["tools", "meta", "sync"])
|
|
829
|
+
assert result.exit_code == 0
|
|
830
|
+
assert "Could not find remote data for v1.9.0" in result.output
|
|
@@ -7,8 +7,12 @@ from tlaplus_cli.version_manager import (
|
|
|
7
7
|
LocalVersion,
|
|
8
8
|
RemoteVersion,
|
|
9
9
|
_process_remote_versions,
|
|
10
|
+
extract_version_from_url,
|
|
11
|
+
is_url,
|
|
12
|
+
list_local_versions,
|
|
10
13
|
resolve_latest_version,
|
|
11
14
|
write_version_metadata,
|
|
15
|
+
write_version_metadata_from_url,
|
|
12
16
|
)
|
|
13
17
|
|
|
14
18
|
|
|
@@ -191,3 +195,128 @@ def test_write_version_metadata(tmp_path, mocker):
|
|
|
191
195
|
assert data["tlc2_version_string"] == "TLC2 Version 2.18 of 27 March 2026 (revision 1.8.0)"
|
|
192
196
|
assert data["prerelease"] is True
|
|
193
197
|
assert data["download_url"] == "http://example.com/tla2tools.jar"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# --- is_url ---
|
|
201
|
+
|
|
202
|
+
def test_is_url_https():
|
|
203
|
+
assert is_url("https://example.com/v1.8.0/tla2tools.jar") is True
|
|
204
|
+
|
|
205
|
+
def test_is_url_http():
|
|
206
|
+
assert is_url("http://example.com/tla2tools.jar") is True
|
|
207
|
+
|
|
208
|
+
def test_is_url_version_string():
|
|
209
|
+
assert is_url("v1.8.0") is False
|
|
210
|
+
|
|
211
|
+
def test_is_url_empty():
|
|
212
|
+
assert is_url("") is False
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# --- extract_version_from_url ---
|
|
216
|
+
|
|
217
|
+
def test_extract_version_direct_parent():
|
|
218
|
+
url = "https://example.com/v1.8.0/tla2tools.jar"
|
|
219
|
+
assert extract_version_from_url(url) == "v1.8.0"
|
|
220
|
+
|
|
221
|
+
def test_extract_version_nested():
|
|
222
|
+
url = "https://example.com/custom/v1.8.0/build/tla2tools.jar"
|
|
223
|
+
assert extract_version_from_url(url) == "v1.8.0"
|
|
224
|
+
|
|
225
|
+
def test_extract_version_no_v_prefix():
|
|
226
|
+
url = "https://example.com/1.8.0/tla2tools.jar"
|
|
227
|
+
assert extract_version_from_url(url) == "1.8.0"
|
|
228
|
+
|
|
229
|
+
def test_extract_version_not_found():
|
|
230
|
+
url = "https://example.com/custom/latest/tla2tools.jar"
|
|
231
|
+
assert extract_version_from_url(url) is None
|
|
232
|
+
|
|
233
|
+
def test_extract_version_returns_first_match():
|
|
234
|
+
# Two version-like segments — must return the first one encountered
|
|
235
|
+
url = "https://example.com/v1.8.0/dist/v2.0.0/tla2tools.jar"
|
|
236
|
+
assert extract_version_from_url(url) == "v1.8.0"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# --- write_version_metadata_from_url ---
|
|
240
|
+
|
|
241
|
+
def test_write_version_metadata_from_url_creates_file(tmp_path, mocker):
|
|
242
|
+
|
|
243
|
+
mock_run = mocker.patch("tlaplus_cli.version_manager.subprocess.run")
|
|
244
|
+
mock_run.return_value.stdout = "TLC2 Version 2.20\nSome extra line"
|
|
245
|
+
|
|
246
|
+
write_version_metadata_from_url(
|
|
247
|
+
tmp_path,
|
|
248
|
+
version_name="v1.9.0",
|
|
249
|
+
tag="2026-04-06T12:51:28Z",
|
|
250
|
+
url="https://example.com/v1.9.0/tla2tools.jar",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
meta_file = tmp_path / "meta-tla2tools.json"
|
|
254
|
+
assert meta_file.exists()
|
|
255
|
+
|
|
256
|
+
with meta_file.open() as f:
|
|
257
|
+
data = json.load(f)
|
|
258
|
+
|
|
259
|
+
assert data["tag_name"] == "v1.9.0"
|
|
260
|
+
assert data["tag"] == "2026-04-06T12:51:28Z"
|
|
261
|
+
assert data["download_url"] == "https://example.com/v1.9.0/tla2tools.jar"
|
|
262
|
+
assert data["sha"] == ""
|
|
263
|
+
assert data["published_at"] == ""
|
|
264
|
+
assert data["prerelease"] is False
|
|
265
|
+
assert data["tlc2_version_string"] == "TLC2 Version 2.20"
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_write_version_metadata_from_url_handles_subprocess_failure(tmp_path, mocker):
|
|
269
|
+
|
|
270
|
+
mocker.patch(
|
|
271
|
+
"tlaplus_cli.version_manager.subprocess.run",
|
|
272
|
+
side_effect=Exception("java not found"),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
write_version_metadata_from_url(
|
|
276
|
+
tmp_path,
|
|
277
|
+
version_name="v1.9.0",
|
|
278
|
+
tag="2026-04-06T12:51:28Z",
|
|
279
|
+
url="https://example.com/v1.9.0/tla2tools.jar",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
meta_file = tmp_path / "meta-tla2tools.json"
|
|
283
|
+
with meta_file.open() as f:
|
|
284
|
+
data = json.load(f)
|
|
285
|
+
assert data["tlc2_version_string"] == ""
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# --- list_local_versions with timestamp tag ---
|
|
289
|
+
|
|
290
|
+
def test_list_local_versions_timestamp_tag(tmp_path, mocker):
|
|
291
|
+
"""URL-installed versions whose suffix is a timestamp are parsed correctly."""
|
|
292
|
+
|
|
293
|
+
mocker.patch("tlaplus_cli.version_manager.cache_dir", return_value=tmp_path)
|
|
294
|
+
tools_dir = tmp_path / "tools"
|
|
295
|
+
tools_dir.mkdir()
|
|
296
|
+
|
|
297
|
+
ts_dir = tools_dir / "v1.9.0-2026-04-06T12:51:28Z"
|
|
298
|
+
ts_dir.mkdir()
|
|
299
|
+
(ts_dir / "tla2tools.jar").write_bytes(b"jar")
|
|
300
|
+
|
|
301
|
+
versions = list_local_versions()
|
|
302
|
+
assert len(versions) == 1
|
|
303
|
+
lv = versions[0]
|
|
304
|
+
assert lv.name == "v1.9.0"
|
|
305
|
+
assert lv.short_sha == "2026-04-06T12:51:28Z"
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_list_local_versions_sha_tag(tmp_path, mocker):
|
|
309
|
+
"""Standard SHA-tagged versions still parse correctly after the split change."""
|
|
310
|
+
|
|
311
|
+
mocker.patch("tlaplus_cli.version_manager.cache_dir", return_value=tmp_path)
|
|
312
|
+
tools_dir = tmp_path / "tools"
|
|
313
|
+
tools_dir.mkdir()
|
|
314
|
+
|
|
315
|
+
sha_dir = tools_dir / "v1.8.0-aaaaaaa"
|
|
316
|
+
sha_dir.mkdir()
|
|
317
|
+
(sha_dir / "tla2tools.jar").write_bytes(b"jar")
|
|
318
|
+
|
|
319
|
+
versions = list_local_versions()
|
|
320
|
+
assert len(versions) == 1
|
|
321
|
+
assert versions[0].name == "v1.8.0"
|
|
322
|
+
assert versions[0].short_sha == "aaaaaaa"
|
|
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
|