tlaplus-cli 0.3.4__tar.gz → 0.3.5__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.
Files changed (39) hide show
  1. {tlaplus_cli-0.3.4/src/tlaplus_cli.egg-info → tlaplus_cli-0.3.5}/PKG-INFO +11 -1
  2. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/README.md +10 -0
  3. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/pyproject.toml +1 -1
  4. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/src/tlaplus_cli/build_tlc_module.py +16 -3
  5. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/src/tlaplus_cli/cli.py +2 -0
  6. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/src/tlaplus_cli/config.py +21 -1
  7. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/src/tlaplus_cli/run_tlc.py +9 -0
  8. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/src/tlaplus_cli/settings.py +1 -0
  9. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/src/tlaplus_cli/version_manager.py +3 -5
  10. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5/src/tlaplus_cli.egg-info}/PKG-INFO +11 -1
  11. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/src/tlaplus_cli.egg-info/SOURCES.txt +5 -8
  12. tlaplus_cli-0.3.5/tests/test_build_modules_path.py +50 -0
  13. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/tests/test_cli.py +1 -4
  14. tlaplus_cli-0.3.5/tests/test_config.py +99 -0
  15. tlaplus_cli-0.3.5/tests/test_modules_path.py +62 -0
  16. tlaplus_cli-0.3.5/tests/test_settings.py +38 -0
  17. tlaplus_cli-0.3.5/tests/test_tlc_modules_path.py +43 -0
  18. tlaplus_cli-0.3.4/tests/test_build_tlc_module.py +0 -161
  19. tlaplus_cli-0.3.4/tests/test_check_java.py +0 -69
  20. tlaplus_cli-0.3.4/tests/test_java_module_naming.py +0 -86
  21. tlaplus_cli-0.3.4/tests/test_project.py +0 -50
  22. tlaplus_cli-0.3.4/tests/test_run_tlc.py +0 -96
  23. tlaplus_cli-0.3.4/tests/test_spec_resolution.py +0 -200
  24. tlaplus_cli-0.3.4/tests/test_tools_manager.py +0 -830
  25. tlaplus_cli-0.3.4/tests/test_version_manager.py +0 -322
  26. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/LICENSE +0 -0
  27. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/MANIFEST.in +0 -0
  28. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/setup.cfg +0 -0
  29. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/src/tlaplus_cli/__init__.py +0 -0
  30. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/src/tlaplus_cli/check_java.py +0 -0
  31. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/src/tlaplus_cli/project.py +0 -0
  32. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/src/tlaplus_cli/py.typed +0 -0
  33. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/src/tlaplus_cli/resources/__init__.py +0 -0
  34. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/src/tlaplus_cli/resources/default_config.yaml +0 -0
  35. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/src/tlaplus_cli/tools_manager.py +0 -0
  36. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/src/tlaplus_cli.egg-info/dependency_links.txt +0 -0
  37. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/src/tlaplus_cli.egg-info/entry_points.txt +0 -0
  38. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/src/tlaplus_cli.egg-info/requires.txt +0 -0
  39. {tlaplus_cli-0.3.4 → tlaplus_cli-0.3.5}/src/tlaplus_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tlaplus-cli
3
- Version: 0.3.4
3
+ Version: 0.3.5
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
@@ -170,6 +170,14 @@ Compile modules in a specific project path:
170
170
  tla modules build path/to/project
171
171
  ```
172
172
 
173
+ Configure a persistent custom modules path:
174
+ ```bash
175
+ tla modules path /path/to/custom/modules
176
+ ```
177
+
178
+ > [!TIP]
179
+ > Setting a persistent `module_path` allows `tla modules build` and `tla tlc` to resolve your Java overrides regardless of where the commands are executed.
180
+
173
181
  Verbose output:
174
182
  ```bash
175
183
  tla modules build --verbose
@@ -221,6 +229,8 @@ tlc:
221
229
  java_class: tlc2.TLC
222
230
  overrides_class: tlc2.overrides.TLCOverrides
223
231
 
232
+ module_path: null # (Optional) Persistent custom modules path
233
+
224
234
  java:
225
235
  min_version: 11
226
236
  opts:
@@ -141,6 +141,14 @@ Compile modules in a specific project path:
141
141
  tla modules build path/to/project
142
142
  ```
143
143
 
144
+ Configure a persistent custom modules path:
145
+ ```bash
146
+ tla modules path /path/to/custom/modules
147
+ ```
148
+
149
+ > [!TIP]
150
+ > Setting a persistent `module_path` allows `tla modules build` and `tla tlc` to resolve your Java overrides regardless of where the commands are executed.
151
+
144
152
  Verbose output:
145
153
  ```bash
146
154
  tla modules build --verbose
@@ -192,6 +200,8 @@ tlc:
192
200
  java_class: tlc2.TLC
193
201
  overrides_class: tlc2.overrides.TLCOverrides
194
202
 
203
+ module_path: null # (Optional) Persistent custom modules path
204
+
195
205
  java:
196
206
  min_version: 11
197
207
  opts:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tlaplus-cli"
7
- version = "0.3.4"
7
+ version = "0.3.5"
8
8
  description = "TLA+ tools: download TLC, compile custom modules, run model checker"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
 
7
7
  import typer
8
8
 
9
- from tlaplus_cli.config import cache_dir, load_config, workspace_root
9
+ from tlaplus_cli.config import cache_dir, load_config, save_config, workspace_root
10
10
  from tlaplus_cli.version_manager import get_pinned_version_dir
11
11
 
12
12
 
@@ -28,9 +28,8 @@ def build(
28
28
  typer.echo("Error: tla2tools.jar not found.\nRun 'tla tools install' first.", err=True)
29
29
  raise typer.Exit(1)
30
30
 
31
- modules_dir = base_dir / config.workspace.modules_dir
31
+ modules_dir = Path(config.module_path) if config.module_path else base_dir / config.workspace.modules_dir
32
32
  classes_dir = base_dir / config.workspace.classes_dir
33
-
34
33
  lib_dir = base_dir / "lib"
35
34
  lib_jars = sorted(lib_dir.glob("*.jar")) if lib_dir.is_dir() else []
36
35
  classpath = os.pathsep.join([str(jar_path)] + [str(j) for j in lib_jars])
@@ -71,3 +70,17 @@ def build(
71
70
  except FileNotFoundError:
72
71
  typer.echo("Error: 'javac' not found. Ensure JDK is installed and in PATH.", err=True)
73
72
  raise typer.Exit(1) from None
73
+
74
+
75
+ def set_modules_path(path: str = typer.Argument(..., help="Path to the custom Java modules directory.")) -> None:
76
+ """Configure a persistent custom Java modules path."""
77
+ config_obj = load_config()
78
+ p = Path(path).resolve()
79
+
80
+ if not p.is_dir():
81
+ typer.echo(f"Error: Path does not exist or is not a directory: {p}", err=True)
82
+ raise typer.Exit(1)
83
+
84
+ config_obj.module_path = str(p)
85
+ save_config(config_obj)
86
+ typer.echo(f"Modules path updated to: {p}")
@@ -5,6 +5,7 @@ import importlib.metadata
5
5
  import typer
6
6
 
7
7
  from tlaplus_cli.build_tlc_module import build as build_tlc_cmd
8
+ from tlaplus_cli.build_tlc_module import set_modules_path
8
9
  from tlaplus_cli.check_java import check_java_version, get_java_version
9
10
  from tlaplus_cli.config import load_config
10
11
  from tlaplus_cli.run_tlc import tlc as run_tlc_cmd
@@ -50,6 +51,7 @@ def root(
50
51
  # Attach typer subgroups
51
52
  modules_app = typer.Typer(name="modules", help="Manage TLA+ Java modules.", no_args_is_help=True)
52
53
  modules_app.command(name="build")(build_tlc_cmd)
54
+ modules_app.command(name="path")(set_modules_path)
53
55
 
54
56
  app.add_typer(modules_app, name="modules")
55
57
  app.add_typer(tools_app, name="tools")
@@ -17,11 +17,31 @@ def load_config() -> Settings:
17
17
  Creates a default config on first run.
18
18
  """
19
19
  cp = _ensure_config()
20
- with cp.open() as f:
20
+ with cp.open(encoding="utf-8") as f:
21
21
  data = yaml.safe_load(f)
22
22
  return Settings.model_validate(data)
23
23
 
24
24
 
25
+ def save_config(settings: Settings) -> None:
26
+ """Save config to the user config directory.
27
+
28
+ Uses a defensive deep copy of settings.
29
+ """
30
+ cp = config_path()
31
+ # defensive deep copy as per plan
32
+ base_settings = settings.model_copy(deep=True)
33
+
34
+ # Convert to dict, excluding None values for a cleaner config if desired,
35
+ # but the plan doesn't specify exclusion. Pydantic's model_dump is good.
36
+ data = base_settings.model_dump(mode="json")
37
+
38
+ with cp.open("w", encoding="utf-8") as f:
39
+ yaml.safe_dump(data, f, default_flow_style=False)
40
+
41
+ # Clear cache to ensure subsequent loads get the new data
42
+ load_config.cache_clear()
43
+
44
+
25
45
  def config_dir() -> Path:
26
46
  """OS-standard user config directory for this app."""
27
47
  return Path(platformdirs.user_config_dir(_APP_NAME))
@@ -85,6 +85,15 @@ def tlc(
85
85
 
86
86
  classpath_parts = [str(jar_path)]
87
87
  extra_jvm_opts: list[str] = []
88
+
89
+ if config.module_path:
90
+ custom_path = Path(config.module_path)
91
+ if custom_path.is_dir():
92
+ # Phase 4: safely append the custom directory path to the Java -cp mechanism
93
+ classpath_parts.append(str(custom_path))
94
+ # Also add to TLA-Library for convenience if TLA modules are there
95
+ extra_jvm_opts.append(f"-DTLA-Library={custom_path}")
96
+
88
97
  if project_root:
89
98
  classes_path = project_root / config.workspace.classes_dir
90
99
  if classes_path.is_dir():
@@ -46,3 +46,4 @@ class Settings(BaseModel):
46
46
  workspace: WorkspaceConfig
47
47
  tlc: TlcConfig
48
48
  java: JavaConfig = Field(default_factory=JavaConfig)
49
+ module_path: str | None = None
@@ -58,10 +58,7 @@ def download_version_from_url(url: str) -> Path:
58
58
  """
59
59
  version_name = extract_version_from_url(url)
60
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
- )
61
+ msg = 'could not extract a version name from the URL. The URL must contain a version segment (e.g. "v1.8.0").'
65
62
  raise ValueError(msg)
66
63
 
67
64
  tag = _utc_now_iso()
@@ -419,7 +416,8 @@ def _fetch_from_api(
419
416
  releases_response = requests.get(releases_url, params=params, timeout=10)
420
417
  releases_response.raise_for_status()
421
418
  releases_data: list[dict[str, Any]] = releases_response.json()
422
- except requests.RequestException:
419
+ except Exception as e:
420
+ typer.echo(f"⚠ Warning: Failed to fetch remote versions: {e}", err=True)
423
421
  return None
424
422
  else:
425
423
  return tags_data, releases_data
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tlaplus-cli
3
- Version: 0.3.4
3
+ Version: 0.3.5
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
@@ -170,6 +170,14 @@ Compile modules in a specific project path:
170
170
  tla modules build path/to/project
171
171
  ```
172
172
 
173
+ Configure a persistent custom modules path:
174
+ ```bash
175
+ tla modules path /path/to/custom/modules
176
+ ```
177
+
178
+ > [!TIP]
179
+ > Setting a persistent `module_path` allows `tla modules build` and `tla tlc` to resolve your Java overrides regardless of where the commands are executed.
180
+
173
181
  Verbose output:
174
182
  ```bash
175
183
  tla modules build --verbose
@@ -221,6 +229,8 @@ tlc:
221
229
  java_class: tlc2.TLC
222
230
  overrides_class: tlc2.overrides.TLCOverrides
223
231
 
232
+ module_path: null # (Optional) Persistent custom modules path
233
+
224
234
  java:
225
235
  min_version: 11
226
236
  opts:
@@ -21,12 +21,9 @@ src/tlaplus_cli.egg-info/requires.txt
21
21
  src/tlaplus_cli.egg-info/top_level.txt
22
22
  src/tlaplus_cli/resources/__init__.py
23
23
  src/tlaplus_cli/resources/default_config.yaml
24
- tests/test_build_tlc_module.py
25
- tests/test_check_java.py
24
+ tests/test_build_modules_path.py
26
25
  tests/test_cli.py
27
- tests/test_java_module_naming.py
28
- tests/test_project.py
29
- tests/test_run_tlc.py
30
- tests/test_spec_resolution.py
31
- tests/test_tools_manager.py
32
- tests/test_version_manager.py
26
+ tests/test_config.py
27
+ tests/test_modules_path.py
28
+ tests/test_settings.py
29
+ tests/test_tlc_modules_path.py
@@ -0,0 +1,50 @@
1
+ from tlaplus_cli.cli import app
2
+
3
+
4
+ def test_build_uses_configured_module_path(mocker, tmp_path, base_settings, runner):
5
+ """Test that build uses module_path from config if available."""
6
+ # Custom module path
7
+ custom_modules = tmp_path / "custom_modules"
8
+ custom_modules.mkdir()
9
+ (custom_modules / "MyModule.java").write_text("class MyModule {}")
10
+
11
+ # Update settings
12
+ settings = base_settings.model_copy(deep=True)
13
+ settings.module_path = str(custom_modules)
14
+ mocker.patch("tlaplus_cli.build_tlc_module.load_config", return_value=settings)
15
+
16
+ # Mock subprocess.run
17
+ mock_run = mocker.patch("tlaplus_cli.build_tlc_module.subprocess.run")
18
+ mock_run.return_value.returncode = 0
19
+
20
+ # Mock jar path
21
+ pinned_dir = tmp_path / "tools" / "v1.8.0"
22
+ pinned_dir.mkdir(parents=True)
23
+ (pinned_dir / "tla2tools.jar").write_bytes(b"fake")
24
+ mocker.patch("tlaplus_cli.build_tlc_module.get_pinned_version_dir", return_value=pinned_dir)
25
+
26
+ result = runner.invoke(app, ["modules", "build"])
27
+
28
+ assert result.exit_code == 0
29
+ args, _ = mock_run.call_args
30
+ cmd = args[0]
31
+ # Source file should be from custom_modules
32
+ assert str(custom_modules / "MyModule.java") in cmd
33
+
34
+
35
+ def test_build_fails_if_module_path_missing(mocker, tmp_path, base_settings, runner):
36
+ """Test that build fails if the configured module_path does not exist."""
37
+ settings = base_settings.model_copy(deep=True)
38
+ settings.module_path = str(tmp_path / "non_existent")
39
+ mocker.patch("tlaplus_cli.build_tlc_module.load_config", return_value=settings)
40
+
41
+ # Mock jar path
42
+ pinned_dir = tmp_path / "tools" / "v1.8.0"
43
+ pinned_dir.mkdir(parents=True)
44
+ (pinned_dir / "tla2tools.jar").write_bytes(b"fake")
45
+ mocker.patch("tlaplus_cli.build_tlc_module.get_pinned_version_dir", return_value=pinned_dir)
46
+
47
+ result = runner.invoke(app, ["modules", "build"])
48
+
49
+ assert result.exit_code == 1
50
+ assert "Error: modules directory not found" in result.output
@@ -1,11 +1,8 @@
1
1
  import pytest
2
2
  import typer
3
- from typer.testing import CliRunner
4
3
 
5
4
  from tlaplus_cli.cli import app, version_callback
6
5
 
7
- runner = CliRunner()
8
-
9
6
 
10
7
  def test_version_callback_exits(mocker):
11
8
  """Test version callback exits after printing."""
@@ -22,7 +19,7 @@ def test_version_callback_exits(mocker):
22
19
  mock_metadata.assert_called_once_with("tlaplus-cli")
23
20
 
24
21
 
25
- def test_cli_version_flag(mocker):
22
+ def test_cli_version_flag(mocker, runner):
26
23
  """Test 'tla --version' command."""
27
24
  mock_metadata = mocker.patch("importlib.metadata.metadata")
28
25
  mock_metadata.return_value = {
@@ -0,0 +1,99 @@
1
+ from tlaplus_cli import config
2
+
3
+
4
+ def test_config_paths(mocker, tmp_path):
5
+ """Test config_dir, config_path, cache_dir return correct paths."""
6
+ mocker.patch("platformdirs.user_config_dir", return_value=str(tmp_path / "config"))
7
+ mocker.patch("platformdirs.user_cache_dir", return_value=str(tmp_path / "cache"))
8
+
9
+ assert config.config_dir() == tmp_path / "config"
10
+ assert config.config_path() == tmp_path / "config" / "config.yaml"
11
+ assert config.cache_dir() == tmp_path / "cache"
12
+
13
+
14
+ def test_ensure_config_creates_default(mocker, tmp_path):
15
+ """_ensure_config creates the config file if it doesn't exist."""
16
+ config_dir = tmp_path / "config"
17
+ mocker.patch("tlaplus_cli.config.config_dir", return_value=config_dir)
18
+ config_path = config_dir / "config.yaml"
19
+
20
+ assert not config_path.exists()
21
+ path = config._ensure_config()
22
+ assert path == config_path
23
+ assert config_path.exists()
24
+ assert "workspace:" in config_path.read_text()
25
+
26
+
27
+ def test_load_config_caching(mocker, tmp_path):
28
+ """load_config should be cached."""
29
+ config.load_config.cache_clear()
30
+
31
+ config_dir = tmp_path / "config"
32
+ mocker.patch("tlaplus_cli.config.config_dir", return_value=config_dir)
33
+ config._ensure_config()
34
+
35
+ # First call
36
+ c1 = config.load_config()
37
+ # Second call should return the same object from cache
38
+ c2 = config.load_config()
39
+ assert c1 is c2
40
+
41
+
42
+ def test_workspace_root_absolute(mocker, tmp_path):
43
+ """workspace_root returns absolute path when config has absolute path."""
44
+ abs_path = (tmp_path / "my_ws").absolute()
45
+ mock_settings = mocker.MagicMock()
46
+ mock_settings.workspace.root = str(abs_path)
47
+ mocker.patch("tlaplus_cli.config.load_config", return_value=mock_settings)
48
+
49
+ assert config.workspace_root() == abs_path
50
+
51
+
52
+ def test_load_module_path(mocker, tmp_path):
53
+ """Test that module_path can be loaded from config."""
54
+ config.load_config.cache_clear()
55
+ config_dir = tmp_path / "config"
56
+ mocker.patch("tlaplus_cli.config.config_dir", return_value=config_dir)
57
+ config_path = config_dir / "config.yaml"
58
+ config_path.parent.mkdir(parents=True, exist_ok=True)
59
+
60
+ config_content = """
61
+ tla:
62
+ urls:
63
+ tags: https://example.com/tags
64
+ releases: https://example.com/releases
65
+ per_page: 30
66
+ workspace:
67
+ root: .
68
+ spec_dir: spec
69
+ modules_dir: modules
70
+ classes_dir: classes
71
+ tlc:
72
+ java_class: tlc2.TLC
73
+ overrides_class: tlc2.overrides.TLCOverrides
74
+ module_path: /custom/path
75
+ """
76
+ config_path.write_text(config_content)
77
+
78
+ cfg = config.load_config()
79
+ assert cfg.module_path == "/custom/path"
80
+
81
+
82
+ def test_save_config(mocker, tmp_path):
83
+ """Test saving config preserves module_path and uses deep copy."""
84
+ config.load_config.cache_clear()
85
+ config_dir = tmp_path / "config"
86
+ mocker.patch("tlaplus_cli.config.config_dir", return_value=config_dir)
87
+ config_path = config_dir / "config.yaml"
88
+ config_dir.mkdir(parents=True, exist_ok=True)
89
+
90
+ cfg = config.load_config()
91
+ cfg.module_path = "/new/path"
92
+
93
+ config.save_config(cfg)
94
+
95
+ # Reload and check
96
+ config.load_config.cache_clear()
97
+ new_cfg = config.load_config()
98
+ assert new_cfg.module_path == "/new/path"
99
+ assert "module_path: /new/path" in config_path.read_text()
@@ -0,0 +1,62 @@
1
+ from tlaplus_cli import config
2
+ from tlaplus_cli.cli import app
3
+
4
+
5
+ def test_modules_path_success(mocker, tmp_path, runner):
6
+ """Test successful setting of modules path."""
7
+ config.load_config.cache_clear()
8
+ config_dir = tmp_path / "config"
9
+ mocker.patch("tlaplus_cli.config.config_dir", return_value=config_dir)
10
+
11
+ # Create a dummy module directory
12
+ module_dir = tmp_path / "my_modules"
13
+ module_dir.mkdir()
14
+
15
+ result = runner.invoke(app, ["modules", "path", str(module_dir)])
16
+
17
+ assert result.exit_code == 0
18
+ assert f"Modules path updated to: {module_dir}" in result.output
19
+
20
+ # Check config
21
+ config.load_config.cache_clear()
22
+ cfg = config.load_config()
23
+ assert cfg.module_path == str(module_dir)
24
+
25
+
26
+ def test_modules_path_invalid_dir(mocker, tmp_path, runner):
27
+ """Test setting modules path to non-existent directory fails."""
28
+ config.load_config.cache_clear()
29
+ config_dir = tmp_path / "config"
30
+ mocker.patch("tlaplus_cli.config.config_dir", return_value=config_dir)
31
+
32
+ invalid_dir = tmp_path / "does_not_exist"
33
+
34
+ result = runner.invoke(app, ["modules", "path", str(invalid_dir)])
35
+
36
+ assert result.exit_code == 1
37
+ assert "Error: Path does not exist or is not a directory" in result.output
38
+
39
+ # Check config was NOT updated
40
+ config.load_config.cache_clear()
41
+ cfg = config.load_config()
42
+ assert cfg.module_path is None
43
+
44
+
45
+ def test_modules_path_file_instead_of_dir(mocker, tmp_path, runner):
46
+ """Test setting modules path to a file fails."""
47
+ config.load_config.cache_clear()
48
+ config_dir = tmp_path / "config"
49
+ mocker.patch("tlaplus_cli.config.config_dir", return_value=config_dir)
50
+
51
+ some_file = tmp_path / "some_file.txt"
52
+ some_file.write_text("hello")
53
+
54
+ result = runner.invoke(app, ["modules", "path", str(some_file)])
55
+
56
+ assert result.exit_code == 1
57
+ assert "Error: Path does not exist or is not a directory" in result.output
58
+
59
+ # Check config was NOT updated
60
+ config.load_config.cache_clear()
61
+ cfg = config.load_config()
62
+ assert cfg.module_path is None
@@ -0,0 +1,38 @@
1
+ import os
2
+
3
+ from tlaplus_cli.settings import JavaConfig
4
+
5
+
6
+ def test_java_config_default_opts():
7
+ """Default opts are set if JAVA_OPTS is not present."""
8
+ if "JAVA_OPTS" in os.environ:
9
+ del os.environ["JAVA_OPTS"]
10
+
11
+ config = JavaConfig()
12
+ assert "-XX:+UseParallelGC" in config.opts
13
+
14
+
15
+ def test_java_config_env_opts_overrides(monkeypatch):
16
+ """JAVA_OPTS environment variable overrides the default opts."""
17
+ monkeypatch.setenv("JAVA_OPTS", "-Xmx4G -Xms2G")
18
+
19
+ # We must pass something to trigger the validator since it's a model_validator(mode="before")
20
+ # Actually, pydantic calls it even for default instantiation if not careful,
21
+ # but here it's called with 'data' which is the dict passed to __init__ or empty dict.
22
+
23
+ config = JavaConfig()
24
+ assert config.opts == ["-Xmx4G", "-Xms2G"]
25
+
26
+
27
+ def test_java_config_env_opts_empty(monkeypatch):
28
+ """Empty JAVA_OPTS should be ignored (keep defaults)."""
29
+ monkeypatch.setenv("JAVA_OPTS", "")
30
+ config = JavaConfig()
31
+ assert "-XX:+UseParallelGC" in config.opts
32
+
33
+
34
+ def test_java_config_explicit_opts_with_env(monkeypatch):
35
+ """JAVA_OPTS should override even explicitly passed opts in model_validator(mode='before')."""
36
+ monkeypatch.setenv("JAVA_OPTS", "-Xmx1G")
37
+ config = JavaConfig(opts=["-Xmx2G"])
38
+ assert config.opts == ["-Xmx1G"]
@@ -0,0 +1,43 @@
1
+ from tlaplus_cli.cli import app
2
+
3
+
4
+ def test_tlc_uses_custom_module_path(mocker, tmp_path, base_settings, runner):
5
+ """Test that tlc uses module_path for classpath and TLA-Library."""
6
+ # Custom module path
7
+ custom_modules = tmp_path / "custom_modules"
8
+ custom_modules.mkdir()
9
+
10
+ # Update settings
11
+ settings = base_settings.model_copy(deep=True)
12
+ settings.module_path = str(custom_modules)
13
+ mocker.patch("tlaplus_cli.run_tlc.load_config", return_value=settings)
14
+
15
+ # Mock jar path
16
+ pinned_dir = tmp_path / "tools" / "v1.8.0"
17
+ pinned_dir.mkdir(parents=True)
18
+ (pinned_dir / "tla2tools.jar").write_bytes(b"fake")
19
+ mocker.patch("tlaplus_cli.run_tlc.get_pinned_version_dir", return_value=pinned_dir)
20
+
21
+ # Create a dummy spec
22
+ spec_file = tmp_path / "MySpec.tla"
23
+ spec_file.write_text("---- MODULE MySpec ----\n====\n")
24
+
25
+ # Mock subprocess.run
26
+ mock_run = mocker.patch("tlaplus_cli.run_tlc.subprocess.run")
27
+ mock_run.return_value.returncode = 0
28
+
29
+ # Mock check_java_version to avoid potential Mock issues
30
+ mocker.patch("tlaplus_cli.run_tlc.check_java_version")
31
+
32
+ result = runner.invoke(app, ["tlc", str(spec_file)])
33
+
34
+ assert result.exit_code == 0
35
+ args, _kwargs = mock_run.call_args
36
+ cmd = args[0]
37
+ cmd_str = " ".join(cmd)
38
+
39
+ # Phase 4: append the custom directory path to the Java -cp mechanism
40
+ assert str(custom_modules) in cmd_str
41
+
42
+ # It should also probably be in TLA-Library
43
+ assert f"-DTLA-Library={custom_modules}" in cmd_str