cande-wrapper 0.0.1__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.
- cande_wrapper-0.0.1/PKG-INFO +5 -0
- cande_wrapper-0.0.1/README.md +130 -0
- cande_wrapper-0.0.1/pyproject.toml +9 -0
- cande_wrapper-0.0.1/setup.cfg +4 -0
- cande_wrapper-0.0.1/src/cande_wrapper/__init__.py +5 -0
- cande_wrapper-0.0.1/src/cande_wrapper/engine.py +165 -0
- cande_wrapper-0.0.1/src/cande_wrapper.egg-info/PKG-INFO +5 -0
- cande_wrapper-0.0.1/src/cande_wrapper.egg-info/SOURCES.txt +11 -0
- cande_wrapper-0.0.1/src/cande_wrapper.egg-info/dependency_links.txt +1 -0
- cande_wrapper-0.0.1/src/cande_wrapper.egg-info/top_level.txt +2 -0
- cande_wrapper-0.0.1/tests/test_engine.py +92 -0
- cande_wrapper-0.0.1/tests/test_integration.py +39 -0
- cande_wrapper-0.0.1/tests/test_result.py +36 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# cande-wrapper
|
|
2
|
+
|
|
3
|
+
Python wrapper for the [CANDE](https://www.fhwa.dot.gov/engineering/hydraulics/software/culvertanalysis.cfm) (Culvert ANalysis and DEsign) finite element engine.
|
|
4
|
+
|
|
5
|
+
The CANDE-2025 Fortran source is compiled via [f2py](https://numpy.org/doc/stable/f2py/) into a native Python extension module, allowing direct calls from Python without subprocess overhead or DLL wrappers.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
| Dependency | Version | Purpose |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| Python | >= 3.10 | Runtime |
|
|
12
|
+
| gfortran | any recent | Compiles the CANDE Fortran engine |
|
|
13
|
+
| NumPy | >= 1.26 | f2py extension building and runtime |
|
|
14
|
+
| Meson | >= 1.1 | Build system |
|
|
15
|
+
| Ninja | any recent | Build backend for Meson |
|
|
16
|
+
|
|
17
|
+
### Installing gfortran on Windows
|
|
18
|
+
|
|
19
|
+
The easiest way is via MSYS2:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# In MSYS2 terminal
|
|
23
|
+
pacman -S mingw-w64-x86_64-gcc-fortran
|
|
24
|
+
|
|
25
|
+
# Add to PATH (PowerShell)
|
|
26
|
+
$env:PATH += ";C:\msys64\mingw64\bin"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or install via conda:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
conda install -c conda-forge gfortran
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Installing build tools
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install meson-python meson ninja numpy
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
From the project root:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install -e ".[dev]"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This compiles the Fortran engine via f2py and installs the Python package in editable mode. The `[dev]` extra includes pytest and jupyter for testing and notebooks.
|
|
50
|
+
|
|
51
|
+
If the build fails, see [Troubleshooting](#troubleshooting) below.
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from cande_wrapper import CandeEngine
|
|
57
|
+
|
|
58
|
+
engine = CandeEngine(work_dir="path/to/my/models")
|
|
59
|
+
result = engine.run("EX1") # reads EX1.cid, writes EX1.out
|
|
60
|
+
print(result.output_text)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## CANDE Input Files
|
|
64
|
+
|
|
65
|
+
CANDE reads `.cid` input files (CANDE Input Data). These are fixed-format text files with sections:
|
|
66
|
+
|
|
67
|
+
- **A-1**: Master control line (analysis/design mode, solution level, title)
|
|
68
|
+
- **A-2**: Pipe type and element counts
|
|
69
|
+
- **B-\***: Pipe material properties
|
|
70
|
+
- **C-\***: Mesh, node, and element definitions
|
|
71
|
+
- **D-\***: Soil properties and load steps
|
|
72
|
+
- **STOP**: End-of-problem marker
|
|
73
|
+
|
|
74
|
+
Multiple problems can be run back-to-back in a single `.cid` file, each terminated by `STOP`.
|
|
75
|
+
|
|
76
|
+
An example input file is included at `tests/example_data/MGK-IO.cid`.
|
|
77
|
+
|
|
78
|
+
## Output Files
|
|
79
|
+
|
|
80
|
+
After running `engine.run("prefix")`, these files are created in the working directory:
|
|
81
|
+
|
|
82
|
+
| File | Description |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `prefix.out` | Main analysis output report |
|
|
85
|
+
| `prefix.log` | Engine log messages |
|
|
86
|
+
| `prefix.ctc` | Table of contents for the output |
|
|
87
|
+
| `prefix_MeshGeom.xml` | Mesh geometry (for visualization) |
|
|
88
|
+
| `prefix_MeshResults.xml` | Mesh results (for visualization) |
|
|
89
|
+
| `prefix_BeamResults.xml` | Beam element results |
|
|
90
|
+
| `prefix_PLOT1.dat` | Plot data file 1 |
|
|
91
|
+
| `prefix_PLOT2.dat` | Plot data file 2 |
|
|
92
|
+
|
|
93
|
+
The `CandeResult` object returned by `engine.run()` gives convenient access to the main output files:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
result = engine.run("EX1")
|
|
97
|
+
print(result.output_file) # Path to EX1.out
|
|
98
|
+
print(result.log_file) # Path to EX1.log
|
|
99
|
+
print(result.output_text) # Full contents of EX1.out
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Testing
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pytest -v # unit tests (no build required)
|
|
106
|
+
pytest -m integration -v # integration tests (requires build)
|
|
107
|
+
pytest -m "" -v # all tests
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Troubleshooting
|
|
111
|
+
|
|
112
|
+
### `gfortran: command not found`
|
|
113
|
+
|
|
114
|
+
gfortran is not on your PATH. Verify with `gfortran --version`. On Windows, ensure the MSYS2/MinGW or conda bin directory is in your PATH.
|
|
115
|
+
|
|
116
|
+
### Linker errors about missing symbols
|
|
117
|
+
|
|
118
|
+
The `meson.build` file lists Engine source files explicitly. If you see unresolved symbols like `plasti_` or `prhero_`, the corresponding `.f` file needs to be added to the `engine_sources` list in `meson.build`.
|
|
119
|
+
|
|
120
|
+
### `ImportError: CANDE Fortran extension not found`
|
|
121
|
+
|
|
122
|
+
The package was imported but the compiled extension (`_cande.pyd` / `_cande.so`) was not found. Rebuild with:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pip install -e . --no-build-isolation
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Large memory usage
|
|
129
|
+
|
|
130
|
+
The CANDE engine statically allocates ~320 MB for the stiffness matrix (`REAL*8 A(20000,2000)` in `system.fi`). This is normal for FEA solvers and is mapped as virtual memory.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Python interface to the CANDE Fortran engine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _find_gfortran_dirs() -> list[str]:
|
|
11
|
+
"""Find directories containing gfortran runtime DLLs on Windows."""
|
|
12
|
+
import shutil
|
|
13
|
+
|
|
14
|
+
dirs = []
|
|
15
|
+
# Check if gfortran is on PATH and find its bin directory
|
|
16
|
+
gfortran = shutil.which("gfortran")
|
|
17
|
+
if gfortran:
|
|
18
|
+
dirs.append(str(Path(gfortran).parent))
|
|
19
|
+
# Common locations
|
|
20
|
+
for candidate in [
|
|
21
|
+
r"C:\Strawberry\c\bin",
|
|
22
|
+
r"C:\msys64\mingw64\bin",
|
|
23
|
+
r"C:\mingw64\bin",
|
|
24
|
+
]:
|
|
25
|
+
if Path(candidate).is_dir():
|
|
26
|
+
dirs.append(candidate)
|
|
27
|
+
# Conda environment
|
|
28
|
+
conda_prefix = os.environ.get("CONDA_PREFIX")
|
|
29
|
+
if conda_prefix:
|
|
30
|
+
lib_dir = Path(conda_prefix) / "Library" / "bin"
|
|
31
|
+
if lib_dir.is_dir():
|
|
32
|
+
dirs.append(str(lib_dir))
|
|
33
|
+
return dirs
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CandeEngine:
|
|
37
|
+
"""Wrapper around the CANDE FEA engine compiled via f2py.
|
|
38
|
+
|
|
39
|
+
Usage::
|
|
40
|
+
|
|
41
|
+
engine = CandeEngine(work_dir="path/to/working/directory")
|
|
42
|
+
result = engine.run("my_problem")
|
|
43
|
+
# Reads my_problem.cid, writes my_problem.out in work_dir
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
work_dir : str or Path, optional
|
|
48
|
+
Working directory where .cid input files live and output files
|
|
49
|
+
will be written. Defaults to current directory.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, work_dir: Optional[str | Path] = None):
|
|
53
|
+
self.work_dir = Path(work_dir) if work_dir else Path.cwd()
|
|
54
|
+
|
|
55
|
+
def run(self, prefix: str) -> "CandeResult":
|
|
56
|
+
"""Run a CANDE analysis.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
prefix : str
|
|
61
|
+
File prefix (without extension). The engine will read
|
|
62
|
+
``{prefix}.cid`` and produce ``{prefix}.out`` and other
|
|
63
|
+
output files in the working directory.
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
CandeResult
|
|
68
|
+
Object with paths to all output files and the return code.
|
|
69
|
+
|
|
70
|
+
Raises
|
|
71
|
+
------
|
|
72
|
+
FileNotFoundError
|
|
73
|
+
If the input .cid file does not exist.
|
|
74
|
+
RuntimeError
|
|
75
|
+
If the Fortran engine returns a nonzero error code.
|
|
76
|
+
"""
|
|
77
|
+
cid_file = self.work_dir / f"{prefix}.cid"
|
|
78
|
+
if not cid_file.exists():
|
|
79
|
+
raise FileNotFoundError(f"Input file not found: {cid_file}")
|
|
80
|
+
|
|
81
|
+
# Import the f2py-compiled extension
|
|
82
|
+
try:
|
|
83
|
+
from cande_wrapper._cande import run_cande
|
|
84
|
+
except ImportError:
|
|
85
|
+
# On Windows, gfortran runtime DLLs may not be on PATH.
|
|
86
|
+
# Try adding common gfortran bin directories.
|
|
87
|
+
if os.name == "nt":
|
|
88
|
+
for gfortran_dir in _find_gfortran_dirs():
|
|
89
|
+
os.add_dll_directory(gfortran_dir)
|
|
90
|
+
try:
|
|
91
|
+
from cande_wrapper._cande import run_cande
|
|
92
|
+
except ImportError as e:
|
|
93
|
+
raise ImportError(
|
|
94
|
+
"CANDE Fortran extension DLL load failed. "
|
|
95
|
+
"Ensure gfortran runtime DLLs (libgfortran-5.dll) "
|
|
96
|
+
"are on PATH or install via conda."
|
|
97
|
+
) from e
|
|
98
|
+
else:
|
|
99
|
+
raise ImportError(
|
|
100
|
+
"CANDE Fortran extension not found. "
|
|
101
|
+
"Build it with: pip install -e . "
|
|
102
|
+
"(requires gfortran and numpy)"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# CANDE reads/writes files relative to CWD, so we chdir
|
|
106
|
+
original_dir = os.getcwd()
|
|
107
|
+
try:
|
|
108
|
+
os.chdir(self.work_dir)
|
|
109
|
+
ierror = run_cande(prefix)
|
|
110
|
+
finally:
|
|
111
|
+
os.chdir(original_dir)
|
|
112
|
+
|
|
113
|
+
if ierror != 0:
|
|
114
|
+
raise RuntimeError(
|
|
115
|
+
f"CANDE engine returned error code {ierror}. "
|
|
116
|
+
f"Check {prefix}.out and {prefix}.log for details."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return CandeResult(self.work_dir, prefix)
|
|
120
|
+
|
|
121
|
+
def check_input(self, prefix: str) -> bool:
|
|
122
|
+
"""Validate that a .cid input file exists and is readable.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
prefix : str
|
|
127
|
+
File prefix (without extension).
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
bool
|
|
132
|
+
True if the input file exists.
|
|
133
|
+
"""
|
|
134
|
+
return (self.work_dir / f"{prefix}.cid").exists()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class CandeResult:
|
|
138
|
+
"""Container for CANDE analysis output file paths.
|
|
139
|
+
|
|
140
|
+
Attributes
|
|
141
|
+
----------
|
|
142
|
+
prefix : str
|
|
143
|
+
The file prefix used for this run.
|
|
144
|
+
output_file : Path
|
|
145
|
+
Path to the main .out output file.
|
|
146
|
+
log_file : Path
|
|
147
|
+
Path to the .log file.
|
|
148
|
+
toc_file : Path
|
|
149
|
+
Path to the .ctc table-of-contents file.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def __init__(self, work_dir: Path, prefix: str):
|
|
153
|
+
self.prefix = prefix
|
|
154
|
+
self.work_dir = work_dir
|
|
155
|
+
self.output_file = work_dir / f"{prefix}.out"
|
|
156
|
+
self.log_file = work_dir / f"{prefix}.log"
|
|
157
|
+
self.toc_file = work_dir / f"{prefix}.ctc"
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def output_text(self) -> str:
|
|
161
|
+
"""Read and return the full output file contents."""
|
|
162
|
+
return self.output_file.read_text()
|
|
163
|
+
|
|
164
|
+
def __repr__(self) -> str:
|
|
165
|
+
return f"CandeResult(prefix={self.prefix!r}, output={self.output_file})"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/cande_wrapper/__init__.py
|
|
4
|
+
src/cande_wrapper/engine.py
|
|
5
|
+
src/cande_wrapper.egg-info/PKG-INFO
|
|
6
|
+
src/cande_wrapper.egg-info/SOURCES.txt
|
|
7
|
+
src/cande_wrapper.egg-info/dependency_links.txt
|
|
8
|
+
src/cande_wrapper.egg-info/top_level.txt
|
|
9
|
+
tests/test_engine.py
|
|
10
|
+
tests/test_integration.py
|
|
11
|
+
tests/test_result.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Unit tests for CandeEngine."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from cande_wrapper.engine import CandeEngine
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestCandeEngineInit:
|
|
12
|
+
def test_default_workdir(self):
|
|
13
|
+
engine = CandeEngine()
|
|
14
|
+
assert engine.work_dir == Path.cwd()
|
|
15
|
+
|
|
16
|
+
def test_custom_workdir_string(self, tmp_path):
|
|
17
|
+
engine = CandeEngine(work_dir=str(tmp_path))
|
|
18
|
+
assert engine.work_dir == tmp_path
|
|
19
|
+
|
|
20
|
+
def test_custom_workdir_path(self, tmp_path):
|
|
21
|
+
engine = CandeEngine(work_dir=tmp_path)
|
|
22
|
+
assert engine.work_dir == tmp_path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestCheckInput:
|
|
26
|
+
def test_exists(self, tmp_workdir):
|
|
27
|
+
engine = CandeEngine(work_dir=tmp_workdir)
|
|
28
|
+
assert engine.check_input("MGK-IO") is True
|
|
29
|
+
|
|
30
|
+
def test_missing(self, tmp_path):
|
|
31
|
+
engine = CandeEngine(work_dir=tmp_path)
|
|
32
|
+
assert engine.check_input("nonexistent") is False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestRun:
|
|
36
|
+
def test_missing_cid_raises(self, tmp_path):
|
|
37
|
+
engine = CandeEngine(work_dir=tmp_path)
|
|
38
|
+
with pytest.raises(FileNotFoundError, match="Input file not found"):
|
|
39
|
+
engine.run("nonexistent")
|
|
40
|
+
|
|
41
|
+
def test_missing_extension_raises(self, tmp_workdir, monkeypatch):
|
|
42
|
+
"""Simulates missing _cande extension by blocking the import."""
|
|
43
|
+
import sys
|
|
44
|
+
|
|
45
|
+
# Remove cached module and make it unimportable
|
|
46
|
+
monkeypatch.setitem(sys.modules, "cande_wrapper._cande", None)
|
|
47
|
+
|
|
48
|
+
engine = CandeEngine(work_dir=tmp_workdir)
|
|
49
|
+
with pytest.raises(ImportError):
|
|
50
|
+
engine.run("MGK-IO")
|
|
51
|
+
|
|
52
|
+
def test_success_returns_result(self, tmp_workdir, mock_fortran):
|
|
53
|
+
# Create the .out file that CandeResult expects
|
|
54
|
+
(tmp_workdir / "MGK-IO.out").write_text("NORMAL EXIT FROM CANDE")
|
|
55
|
+
(tmp_workdir / "MGK-IO.log").write_text("")
|
|
56
|
+
(tmp_workdir / "MGK-IO.ctc").write_text("")
|
|
57
|
+
|
|
58
|
+
engine = CandeEngine(work_dir=tmp_workdir)
|
|
59
|
+
result = engine.run("MGK-IO")
|
|
60
|
+
|
|
61
|
+
mock_fortran.run_cande.assert_called_once_with("MGK-IO")
|
|
62
|
+
assert result.prefix == "MGK-IO"
|
|
63
|
+
assert result.output_file == tmp_workdir / "MGK-IO.out"
|
|
64
|
+
|
|
65
|
+
def test_engine_error_raises(self, tmp_workdir, mock_fortran):
|
|
66
|
+
mock_fortran.run_cande.return_value = 1
|
|
67
|
+
|
|
68
|
+
engine = CandeEngine(work_dir=tmp_workdir)
|
|
69
|
+
with pytest.raises(RuntimeError, match="error code 1"):
|
|
70
|
+
engine.run("MGK-IO")
|
|
71
|
+
|
|
72
|
+
def test_restores_cwd_on_success(self, tmp_workdir, mock_fortran):
|
|
73
|
+
original = os.getcwd()
|
|
74
|
+
engine = CandeEngine(work_dir=tmp_workdir)
|
|
75
|
+
engine.run("MGK-IO")
|
|
76
|
+
assert os.getcwd() == original
|
|
77
|
+
|
|
78
|
+
def test_restores_cwd_on_error(self, tmp_workdir, mock_fortran):
|
|
79
|
+
mock_fortran.run_cande.return_value = 1
|
|
80
|
+
original = os.getcwd()
|
|
81
|
+
engine = CandeEngine(work_dir=tmp_workdir)
|
|
82
|
+
with pytest.raises(RuntimeError):
|
|
83
|
+
engine.run("MGK-IO")
|
|
84
|
+
assert os.getcwd() == original
|
|
85
|
+
|
|
86
|
+
def test_restores_cwd_on_exception(self, tmp_workdir, mock_fortran):
|
|
87
|
+
mock_fortran.run_cande.side_effect = Exception("boom")
|
|
88
|
+
original = os.getcwd()
|
|
89
|
+
engine = CandeEngine(work_dir=tmp_workdir)
|
|
90
|
+
with pytest.raises(Exception, match="boom"):
|
|
91
|
+
engine.run("MGK-IO")
|
|
92
|
+
assert os.getcwd() == original
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Integration tests that require the compiled Fortran extension.
|
|
2
|
+
|
|
3
|
+
Run with: pytest -m integration
|
|
4
|
+
These are skipped by default in normal test runs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import shutil
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from cande_wrapper.engine import CandeEngine
|
|
13
|
+
|
|
14
|
+
EXAMPLE_DATA = Path(__file__).parent / "example_data"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.integration
|
|
18
|
+
class TestIntegration:
|
|
19
|
+
def test_run_example_problem(self, tmp_path):
|
|
20
|
+
"""Run the MGK-IO example and verify CANDE completes normally."""
|
|
21
|
+
shutil.copy2(EXAMPLE_DATA / "MGK-IO.cid", tmp_path / "MGK-IO.cid")
|
|
22
|
+
|
|
23
|
+
engine = CandeEngine(work_dir=tmp_path)
|
|
24
|
+
result = engine.run("MGK-IO")
|
|
25
|
+
|
|
26
|
+
assert result.output_file.exists()
|
|
27
|
+
output = result.output_text
|
|
28
|
+
assert "NORMAL EXIT FROM CANDE" in output
|
|
29
|
+
|
|
30
|
+
def test_output_files_created(self, tmp_path):
|
|
31
|
+
"""Verify all expected output files are created."""
|
|
32
|
+
shutil.copy2(EXAMPLE_DATA / "MGK-IO.cid", tmp_path / "MGK-IO.cid")
|
|
33
|
+
|
|
34
|
+
engine = CandeEngine(work_dir=tmp_path)
|
|
35
|
+
result = engine.run("MGK-IO")
|
|
36
|
+
|
|
37
|
+
assert result.output_file.exists()
|
|
38
|
+
assert result.log_file.exists()
|
|
39
|
+
assert result.toc_file.exists()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Unit tests for CandeResult."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from cande_wrapper.engine import CandeResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestCandeResult:
|
|
9
|
+
def test_paths(self, tmp_path):
|
|
10
|
+
result = CandeResult(tmp_path, "EX1")
|
|
11
|
+
assert result.output_file == tmp_path / "EX1.out"
|
|
12
|
+
assert result.log_file == tmp_path / "EX1.log"
|
|
13
|
+
assert result.toc_file == tmp_path / "EX1.ctc"
|
|
14
|
+
|
|
15
|
+
def test_prefix(self, tmp_path):
|
|
16
|
+
result = CandeResult(tmp_path, "test_problem")
|
|
17
|
+
assert result.prefix == "test_problem"
|
|
18
|
+
|
|
19
|
+
def test_repr(self, tmp_path):
|
|
20
|
+
result = CandeResult(tmp_path, "EX1")
|
|
21
|
+
r = repr(result)
|
|
22
|
+
assert "EX1" in r
|
|
23
|
+
assert "CandeResult" in r
|
|
24
|
+
|
|
25
|
+
def test_output_text(self, tmp_path):
|
|
26
|
+
(tmp_path / "EX1.out").write_text("line 1\nline 2\n")
|
|
27
|
+
result = CandeResult(tmp_path, "EX1")
|
|
28
|
+
assert result.output_text == "line 1\nline 2\n"
|
|
29
|
+
|
|
30
|
+
def test_output_text_missing_file(self, tmp_path):
|
|
31
|
+
result = CandeResult(tmp_path, "EX1")
|
|
32
|
+
try:
|
|
33
|
+
_ = result.output_text
|
|
34
|
+
assert False, "Should have raised FileNotFoundError"
|
|
35
|
+
except FileNotFoundError:
|
|
36
|
+
pass
|