ReForma 0.1.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.
- reforma-0.1.0/MANIFEST.in +1 -0
- reforma-0.1.0/PKG-INFO +217 -0
- reforma-0.1.0/README.md +202 -0
- reforma-0.1.0/ReForma.egg-info/PKG-INFO +217 -0
- reforma-0.1.0/ReForma.egg-info/SOURCES.txt +9 -0
- reforma-0.1.0/ReForma.egg-info/dependency_links.txt +1 -0
- reforma-0.1.0/ReForma.egg-info/requires.txt +2 -0
- reforma-0.1.0/ReForma.egg-info/top_level.txt +1 -0
- reforma-0.1.0/pyproject.toml +31 -0
- reforma-0.1.0/setup.cfg +4 -0
- reforma-0.1.0/tests/test_pyRe.py +285 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include ReForma/bin/ReFormaTool.jar
|
reforma-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ReForma
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python bindings for the RePA/ReForma probabilistic automaton tool
|
|
5
|
+
Author-email: Joshua Dourado <joshuadourado@ua.pt>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: networkx
|
|
14
|
+
Requires-Dist: matplotlib
|
|
15
|
+
|
|
16
|
+
# ReForma — Python bindings for the RePA/ReForma tool
|
|
17
|
+
|
|
18
|
+
A clean Python library that wraps the `ReFormaTool.jar` CLI via subprocess,
|
|
19
|
+
giving you a Pythonic interface for simulation, training, PDL/PCTL
|
|
20
|
+
verification, and export.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# From the project root (where pyproject.toml lives)
|
|
28
|
+
pip install -e .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Requires **Python 3.10+** and a working `java` on your PATH.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from ReForma import ReForma
|
|
39
|
+
|
|
40
|
+
# Point to your compiled JAR
|
|
41
|
+
ReForma = ReForma("path/to/ReFormaTool.jar")
|
|
42
|
+
|
|
43
|
+
# Load a model
|
|
44
|
+
state = ReForma.load_file("examples/recommender.r")
|
|
45
|
+
|
|
46
|
+
print(state.current_states) # ['Home']
|
|
47
|
+
print(state.enabled) # [Transition('go_work': Home → Office, p=0.500), ...]
|
|
48
|
+
|
|
49
|
+
# Simulate
|
|
50
|
+
state = ReForma.step("go_work")
|
|
51
|
+
state = ReForma.step("easy_task")
|
|
52
|
+
state = ReForma.undo() # undo last step
|
|
53
|
+
|
|
54
|
+
# Reset to initial state
|
|
55
|
+
ReForma.reset()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Simulation
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
state = ReForma.load_file("model.r")
|
|
64
|
+
|
|
65
|
+
# Check what's enabled
|
|
66
|
+
for t in state.enabled:
|
|
67
|
+
print(f"{t.label}: {t.from_state} → {t.to_state} (p={t.probability:.3f})")
|
|
68
|
+
|
|
69
|
+
# Take a step by label
|
|
70
|
+
state = ReForma.step("go_work")
|
|
71
|
+
|
|
72
|
+
# Undo / reset
|
|
73
|
+
state = ReForma.undo()
|
|
74
|
+
state = ReForma.reset()
|
|
75
|
+
|
|
76
|
+
# Inspect variables
|
|
77
|
+
print(state.variables) # {'counter': 0, 'flag': 1}
|
|
78
|
+
|
|
79
|
+
# History of labels taken
|
|
80
|
+
print(ReForma.history) # ['go_work']
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Training
|
|
86
|
+
|
|
87
|
+
Train the model on a batch of sessions (lists of event labels):
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
ReForma.train([
|
|
91
|
+
["go_work", "easy_task", "easy_task", "go_home"],
|
|
92
|
+
["battery_low", "go_charge", "finish_charge", "socialize"],
|
|
93
|
+
["no_money", "go_work", "go_home"],
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
# Or train directly from a log file (one session per line, comma-separated)
|
|
97
|
+
ReForma.train_from_file("logs/sessions.txt")
|
|
98
|
+
|
|
99
|
+
# sessions.txt format:
|
|
100
|
+
# go_work,easy_task,go_home
|
|
101
|
+
# battery_low,go_charge,finish_charge
|
|
102
|
+
|
|
103
|
+
# Save the updated model with new weights
|
|
104
|
+
ReForma.save_source("model_trained.r")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## PDL / PCTL Verification
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
# Quantitative: probability of eventually reaching Office
|
|
113
|
+
prob = ReForma.check_pdl_value("Home", "{P=?[F Office]}")
|
|
114
|
+
print(f"P(reach Office from Home) = {prob:.4f}")
|
|
115
|
+
|
|
116
|
+
# Qualitative: is it probable?
|
|
117
|
+
holds = ReForma.check_pdl_value("Home", "{P>=0.4[F Office]}")
|
|
118
|
+
print(f"P>=0.4? {holds}") # True / False
|
|
119
|
+
|
|
120
|
+
# PDL: is there a path via go_work to Office?
|
|
121
|
+
holds = ReForma.check_pdl_value("Home", "<go_work>Office")
|
|
122
|
+
print(holds) # True
|
|
123
|
+
|
|
124
|
+
# Get the raw string result
|
|
125
|
+
raw = ReForma.check_pdl("Home", "{P=?[F Office]}")
|
|
126
|
+
print(raw) # "Result: 0.50000"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Formula syntax reference
|
|
130
|
+
|
|
131
|
+
| Formula | Meaning |
|
|
132
|
+
|-----------------------------|------------------------------------------------|
|
|
133
|
+
| `{P=?[F target]}` | Probability of eventually reaching `target` |
|
|
134
|
+
| `{P=?[G safe]}` | Probability of staying in `safe` forever |
|
|
135
|
+
| `{P=?[X next]}` | Probability of reaching `next` in one step |
|
|
136
|
+
| `{P=?[a U b]}` | Probability of `a` until `b` |
|
|
137
|
+
| `{P>=0.5[F target]}` | Is probability of reaching target ≥ 0.5? |
|
|
138
|
+
| `<action>state` | There exists a path via `action` to `state` |
|
|
139
|
+
| `[action]state` | All paths via `action` lead to `state` |
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Export
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
# PRISM DTMC
|
|
147
|
+
prism_code = ReForma.export_prism()
|
|
148
|
+
ReForma.save_prism("output/model.pm")
|
|
149
|
+
|
|
150
|
+
# mCRL2
|
|
151
|
+
mcrl2_code = ReForma.export_mcrl2()
|
|
152
|
+
|
|
153
|
+
# GLTS (imperative translation)
|
|
154
|
+
glts_code = ReForma.export_glts()
|
|
155
|
+
|
|
156
|
+
# Mermaid diagram (initial state)
|
|
157
|
+
diagram = ReForma.export_mermaid()
|
|
158
|
+
|
|
159
|
+
# Mermaid diagram (full LTS — all reachable states)
|
|
160
|
+
full_diagram = ReForma.export_mermaid(full_lts=True)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Loading from a string
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
source = """
|
|
169
|
+
name MyModel
|
|
170
|
+
init s0
|
|
171
|
+
s0 ---> s1: a (0.6)
|
|
172
|
+
s0 ---> s2: b (0.4)
|
|
173
|
+
s1 ---> s0: back (1.0)
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
state = ReForma.load(source, name="MyModel")
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Running the tests
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
pip install pytest
|
|
185
|
+
pytest tests/ -v
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Project structure
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
ReForma/
|
|
194
|
+
├── __init__.py # Public API exports
|
|
195
|
+
├── client.py # ReForma — high-level Python API
|
|
196
|
+
├── jar_bridge.py # JarBridge — low-level subprocess wrapper
|
|
197
|
+
└── model.py # ReFormaModel, SimulationState, Transition data classes
|
|
198
|
+
tests/
|
|
199
|
+
└── test_ReForma.py # Full test suite (mocked, no JAR needed)
|
|
200
|
+
pyproject.toml
|
|
201
|
+
README.md
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Error handling
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from ReForma.jar_bridge import JarError
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
result = ReForma.check_pdl("Home", "{P=?[F Office]}")
|
|
213
|
+
except JarError as e:
|
|
214
|
+
print(f"JAR error: {e}")
|
|
215
|
+
except RuntimeError as e:
|
|
216
|
+
print(f"Usage error: {e}") # e.g. no model loaded, invalid transition
|
|
217
|
+
```
|
reforma-0.1.0/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# ReForma — Python bindings for the RePA/ReForma tool
|
|
2
|
+
|
|
3
|
+
A clean Python library that wraps the `ReFormaTool.jar` CLI via subprocess,
|
|
4
|
+
giving you a Pythonic interface for simulation, training, PDL/PCTL
|
|
5
|
+
verification, and export.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# From the project root (where pyproject.toml lives)
|
|
13
|
+
pip install -e .
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Requires **Python 3.10+** and a working `java` on your PATH.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from ReForma import ReForma
|
|
24
|
+
|
|
25
|
+
# Point to your compiled JAR
|
|
26
|
+
ReForma = ReForma("path/to/ReFormaTool.jar")
|
|
27
|
+
|
|
28
|
+
# Load a model
|
|
29
|
+
state = ReForma.load_file("examples/recommender.r")
|
|
30
|
+
|
|
31
|
+
print(state.current_states) # ['Home']
|
|
32
|
+
print(state.enabled) # [Transition('go_work': Home → Office, p=0.500), ...]
|
|
33
|
+
|
|
34
|
+
# Simulate
|
|
35
|
+
state = ReForma.step("go_work")
|
|
36
|
+
state = ReForma.step("easy_task")
|
|
37
|
+
state = ReForma.undo() # undo last step
|
|
38
|
+
|
|
39
|
+
# Reset to initial state
|
|
40
|
+
ReForma.reset()
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Simulation
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
state = ReForma.load_file("model.r")
|
|
49
|
+
|
|
50
|
+
# Check what's enabled
|
|
51
|
+
for t in state.enabled:
|
|
52
|
+
print(f"{t.label}: {t.from_state} → {t.to_state} (p={t.probability:.3f})")
|
|
53
|
+
|
|
54
|
+
# Take a step by label
|
|
55
|
+
state = ReForma.step("go_work")
|
|
56
|
+
|
|
57
|
+
# Undo / reset
|
|
58
|
+
state = ReForma.undo()
|
|
59
|
+
state = ReForma.reset()
|
|
60
|
+
|
|
61
|
+
# Inspect variables
|
|
62
|
+
print(state.variables) # {'counter': 0, 'flag': 1}
|
|
63
|
+
|
|
64
|
+
# History of labels taken
|
|
65
|
+
print(ReForma.history) # ['go_work']
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Training
|
|
71
|
+
|
|
72
|
+
Train the model on a batch of sessions (lists of event labels):
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
ReForma.train([
|
|
76
|
+
["go_work", "easy_task", "easy_task", "go_home"],
|
|
77
|
+
["battery_low", "go_charge", "finish_charge", "socialize"],
|
|
78
|
+
["no_money", "go_work", "go_home"],
|
|
79
|
+
])
|
|
80
|
+
|
|
81
|
+
# Or train directly from a log file (one session per line, comma-separated)
|
|
82
|
+
ReForma.train_from_file("logs/sessions.txt")
|
|
83
|
+
|
|
84
|
+
# sessions.txt format:
|
|
85
|
+
# go_work,easy_task,go_home
|
|
86
|
+
# battery_low,go_charge,finish_charge
|
|
87
|
+
|
|
88
|
+
# Save the updated model with new weights
|
|
89
|
+
ReForma.save_source("model_trained.r")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## PDL / PCTL Verification
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# Quantitative: probability of eventually reaching Office
|
|
98
|
+
prob = ReForma.check_pdl_value("Home", "{P=?[F Office]}")
|
|
99
|
+
print(f"P(reach Office from Home) = {prob:.4f}")
|
|
100
|
+
|
|
101
|
+
# Qualitative: is it probable?
|
|
102
|
+
holds = ReForma.check_pdl_value("Home", "{P>=0.4[F Office]}")
|
|
103
|
+
print(f"P>=0.4? {holds}") # True / False
|
|
104
|
+
|
|
105
|
+
# PDL: is there a path via go_work to Office?
|
|
106
|
+
holds = ReForma.check_pdl_value("Home", "<go_work>Office")
|
|
107
|
+
print(holds) # True
|
|
108
|
+
|
|
109
|
+
# Get the raw string result
|
|
110
|
+
raw = ReForma.check_pdl("Home", "{P=?[F Office]}")
|
|
111
|
+
print(raw) # "Result: 0.50000"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Formula syntax reference
|
|
115
|
+
|
|
116
|
+
| Formula | Meaning |
|
|
117
|
+
|-----------------------------|------------------------------------------------|
|
|
118
|
+
| `{P=?[F target]}` | Probability of eventually reaching `target` |
|
|
119
|
+
| `{P=?[G safe]}` | Probability of staying in `safe` forever |
|
|
120
|
+
| `{P=?[X next]}` | Probability of reaching `next` in one step |
|
|
121
|
+
| `{P=?[a U b]}` | Probability of `a` until `b` |
|
|
122
|
+
| `{P>=0.5[F target]}` | Is probability of reaching target ≥ 0.5? |
|
|
123
|
+
| `<action>state` | There exists a path via `action` to `state` |
|
|
124
|
+
| `[action]state` | All paths via `action` lead to `state` |
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Export
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
# PRISM DTMC
|
|
132
|
+
prism_code = ReForma.export_prism()
|
|
133
|
+
ReForma.save_prism("output/model.pm")
|
|
134
|
+
|
|
135
|
+
# mCRL2
|
|
136
|
+
mcrl2_code = ReForma.export_mcrl2()
|
|
137
|
+
|
|
138
|
+
# GLTS (imperative translation)
|
|
139
|
+
glts_code = ReForma.export_glts()
|
|
140
|
+
|
|
141
|
+
# Mermaid diagram (initial state)
|
|
142
|
+
diagram = ReForma.export_mermaid()
|
|
143
|
+
|
|
144
|
+
# Mermaid diagram (full LTS — all reachable states)
|
|
145
|
+
full_diagram = ReForma.export_mermaid(full_lts=True)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Loading from a string
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
source = """
|
|
154
|
+
name MyModel
|
|
155
|
+
init s0
|
|
156
|
+
s0 ---> s1: a (0.6)
|
|
157
|
+
s0 ---> s2: b (0.4)
|
|
158
|
+
s1 ---> s0: back (1.0)
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
state = ReForma.load(source, name="MyModel")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Running the tests
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
pip install pytest
|
|
170
|
+
pytest tests/ -v
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Project structure
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
ReForma/
|
|
179
|
+
├── __init__.py # Public API exports
|
|
180
|
+
├── client.py # ReForma — high-level Python API
|
|
181
|
+
├── jar_bridge.py # JarBridge — low-level subprocess wrapper
|
|
182
|
+
└── model.py # ReFormaModel, SimulationState, Transition data classes
|
|
183
|
+
tests/
|
|
184
|
+
└── test_ReForma.py # Full test suite (mocked, no JAR needed)
|
|
185
|
+
pyproject.toml
|
|
186
|
+
README.md
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Error handling
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from ReForma.jar_bridge import JarError
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
result = ReForma.check_pdl("Home", "{P=?[F Office]}")
|
|
198
|
+
except JarError as e:
|
|
199
|
+
print(f"JAR error: {e}")
|
|
200
|
+
except RuntimeError as e:
|
|
201
|
+
print(f"Usage error: {e}") # e.g. no model loaded, invalid transition
|
|
202
|
+
```
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ReForma
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python bindings for the RePA/ReForma probabilistic automaton tool
|
|
5
|
+
Author-email: Joshua Dourado <joshuadourado@ua.pt>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: networkx
|
|
14
|
+
Requires-Dist: matplotlib
|
|
15
|
+
|
|
16
|
+
# ReForma — Python bindings for the RePA/ReForma tool
|
|
17
|
+
|
|
18
|
+
A clean Python library that wraps the `ReFormaTool.jar` CLI via subprocess,
|
|
19
|
+
giving you a Pythonic interface for simulation, training, PDL/PCTL
|
|
20
|
+
verification, and export.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# From the project root (where pyproject.toml lives)
|
|
28
|
+
pip install -e .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Requires **Python 3.10+** and a working `java` on your PATH.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from ReForma import ReForma
|
|
39
|
+
|
|
40
|
+
# Point to your compiled JAR
|
|
41
|
+
ReForma = ReForma("path/to/ReFormaTool.jar")
|
|
42
|
+
|
|
43
|
+
# Load a model
|
|
44
|
+
state = ReForma.load_file("examples/recommender.r")
|
|
45
|
+
|
|
46
|
+
print(state.current_states) # ['Home']
|
|
47
|
+
print(state.enabled) # [Transition('go_work': Home → Office, p=0.500), ...]
|
|
48
|
+
|
|
49
|
+
# Simulate
|
|
50
|
+
state = ReForma.step("go_work")
|
|
51
|
+
state = ReForma.step("easy_task")
|
|
52
|
+
state = ReForma.undo() # undo last step
|
|
53
|
+
|
|
54
|
+
# Reset to initial state
|
|
55
|
+
ReForma.reset()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Simulation
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
state = ReForma.load_file("model.r")
|
|
64
|
+
|
|
65
|
+
# Check what's enabled
|
|
66
|
+
for t in state.enabled:
|
|
67
|
+
print(f"{t.label}: {t.from_state} → {t.to_state} (p={t.probability:.3f})")
|
|
68
|
+
|
|
69
|
+
# Take a step by label
|
|
70
|
+
state = ReForma.step("go_work")
|
|
71
|
+
|
|
72
|
+
# Undo / reset
|
|
73
|
+
state = ReForma.undo()
|
|
74
|
+
state = ReForma.reset()
|
|
75
|
+
|
|
76
|
+
# Inspect variables
|
|
77
|
+
print(state.variables) # {'counter': 0, 'flag': 1}
|
|
78
|
+
|
|
79
|
+
# History of labels taken
|
|
80
|
+
print(ReForma.history) # ['go_work']
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Training
|
|
86
|
+
|
|
87
|
+
Train the model on a batch of sessions (lists of event labels):
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
ReForma.train([
|
|
91
|
+
["go_work", "easy_task", "easy_task", "go_home"],
|
|
92
|
+
["battery_low", "go_charge", "finish_charge", "socialize"],
|
|
93
|
+
["no_money", "go_work", "go_home"],
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
# Or train directly from a log file (one session per line, comma-separated)
|
|
97
|
+
ReForma.train_from_file("logs/sessions.txt")
|
|
98
|
+
|
|
99
|
+
# sessions.txt format:
|
|
100
|
+
# go_work,easy_task,go_home
|
|
101
|
+
# battery_low,go_charge,finish_charge
|
|
102
|
+
|
|
103
|
+
# Save the updated model with new weights
|
|
104
|
+
ReForma.save_source("model_trained.r")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## PDL / PCTL Verification
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
# Quantitative: probability of eventually reaching Office
|
|
113
|
+
prob = ReForma.check_pdl_value("Home", "{P=?[F Office]}")
|
|
114
|
+
print(f"P(reach Office from Home) = {prob:.4f}")
|
|
115
|
+
|
|
116
|
+
# Qualitative: is it probable?
|
|
117
|
+
holds = ReForma.check_pdl_value("Home", "{P>=0.4[F Office]}")
|
|
118
|
+
print(f"P>=0.4? {holds}") # True / False
|
|
119
|
+
|
|
120
|
+
# PDL: is there a path via go_work to Office?
|
|
121
|
+
holds = ReForma.check_pdl_value("Home", "<go_work>Office")
|
|
122
|
+
print(holds) # True
|
|
123
|
+
|
|
124
|
+
# Get the raw string result
|
|
125
|
+
raw = ReForma.check_pdl("Home", "{P=?[F Office]}")
|
|
126
|
+
print(raw) # "Result: 0.50000"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Formula syntax reference
|
|
130
|
+
|
|
131
|
+
| Formula | Meaning |
|
|
132
|
+
|-----------------------------|------------------------------------------------|
|
|
133
|
+
| `{P=?[F target]}` | Probability of eventually reaching `target` |
|
|
134
|
+
| `{P=?[G safe]}` | Probability of staying in `safe` forever |
|
|
135
|
+
| `{P=?[X next]}` | Probability of reaching `next` in one step |
|
|
136
|
+
| `{P=?[a U b]}` | Probability of `a` until `b` |
|
|
137
|
+
| `{P>=0.5[F target]}` | Is probability of reaching target ≥ 0.5? |
|
|
138
|
+
| `<action>state` | There exists a path via `action` to `state` |
|
|
139
|
+
| `[action]state` | All paths via `action` lead to `state` |
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Export
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
# PRISM DTMC
|
|
147
|
+
prism_code = ReForma.export_prism()
|
|
148
|
+
ReForma.save_prism("output/model.pm")
|
|
149
|
+
|
|
150
|
+
# mCRL2
|
|
151
|
+
mcrl2_code = ReForma.export_mcrl2()
|
|
152
|
+
|
|
153
|
+
# GLTS (imperative translation)
|
|
154
|
+
glts_code = ReForma.export_glts()
|
|
155
|
+
|
|
156
|
+
# Mermaid diagram (initial state)
|
|
157
|
+
diagram = ReForma.export_mermaid()
|
|
158
|
+
|
|
159
|
+
# Mermaid diagram (full LTS — all reachable states)
|
|
160
|
+
full_diagram = ReForma.export_mermaid(full_lts=True)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Loading from a string
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
source = """
|
|
169
|
+
name MyModel
|
|
170
|
+
init s0
|
|
171
|
+
s0 ---> s1: a (0.6)
|
|
172
|
+
s0 ---> s2: b (0.4)
|
|
173
|
+
s1 ---> s0: back (1.0)
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
state = ReForma.load(source, name="MyModel")
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Running the tests
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
pip install pytest
|
|
185
|
+
pytest tests/ -v
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Project structure
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
ReForma/
|
|
194
|
+
├── __init__.py # Public API exports
|
|
195
|
+
├── client.py # ReForma — high-level Python API
|
|
196
|
+
├── jar_bridge.py # JarBridge — low-level subprocess wrapper
|
|
197
|
+
└── model.py # ReFormaModel, SimulationState, Transition data classes
|
|
198
|
+
tests/
|
|
199
|
+
└── test_ReForma.py # Full test suite (mocked, no JAR needed)
|
|
200
|
+
pyproject.toml
|
|
201
|
+
README.md
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Error handling
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from ReForma.jar_bridge import JarError
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
result = ReForma.check_pdl("Home", "{P=?[F Office]}")
|
|
213
|
+
except JarError as e:
|
|
214
|
+
print(f"JAR error: {e}")
|
|
215
|
+
except RuntimeError as e:
|
|
216
|
+
print(f"Usage error: {e}") # e.g. no model loaded, invalid transition
|
|
217
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=42", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ReForma"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python bindings for the RePA/ReForma probabilistic automaton tool"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Joshua Dourado", email = "joshuadourado@ua.pt" }
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Topic :: Scientific/Engineering",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"networkx",
|
|
23
|
+
"matplotlib"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["."]
|
|
28
|
+
include = ["ReForma*"]
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.package-data]
|
|
31
|
+
ReForma = ["bin/RePATool.jar"]
|
reforma-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tests/test_reforma.py — full test suite (mocked, no real JAR needed).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
7
|
+
|
|
8
|
+
from reforma import reforma
|
|
9
|
+
|
|
10
|
+
from reforma.model import reformaModel, Transition, SimulationState
|
|
11
|
+
from reforma.jar_bridge import JarBridge, JarError
|
|
12
|
+
from reforma.client import _parse_pdl_result
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
SIMPLE_SOURCE = """\
|
|
16
|
+
name Simple
|
|
17
|
+
init Home
|
|
18
|
+
Home ---> Office: go_work (0.5)
|
|
19
|
+
Home ---> Station: go_charge (0.5)
|
|
20
|
+
Office ---> Home: go_home (1.0)
|
|
21
|
+
"""
|
|
22
|
+
STEP_OUTPUT = """\
|
|
23
|
+
Estado Atual: Home
|
|
24
|
+
Transicoes Habilitadas:
|
|
25
|
+
- [go_work] de Home para Office (P=0.500)
|
|
26
|
+
- [go_charge] de Home para Station (P=0.500)
|
|
27
|
+
"""
|
|
28
|
+
STEP_OUTPUT_OFFICE = """\
|
|
29
|
+
Estado Atual: Office
|
|
30
|
+
Transicoes Habilitadas:
|
|
31
|
+
- [go_home] de Office para Home (P=1.000)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def make_reforma(step_output: str = STEP_OUTPUT) -> reforma:
|
|
36
|
+
"""reforma with a fully mocked JarBridge — no JAR or Java needed."""
|
|
37
|
+
with patch("reforma.client.JarBridge") as MockBridge:
|
|
38
|
+
instance = MockBridge.return_value
|
|
39
|
+
instance.list_transitions.return_value = step_output
|
|
40
|
+
reforma = reforma() # no jar_path — uses bundled (mocked away)
|
|
41
|
+
reforma._bridge = instance
|
|
42
|
+
reforma.load(SIMPLE_SOURCE)
|
|
43
|
+
return reforma
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# reformaModel
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
class TestreformaModel:
|
|
50
|
+
def test_from_string(self):
|
|
51
|
+
m = reformaModel.from_string("init s0\ns0 ---> s1: a", name="test")
|
|
52
|
+
assert m.name == "test"
|
|
53
|
+
|
|
54
|
+
def test_from_file(self, tmp_path):
|
|
55
|
+
p = tmp_path / "model.r"
|
|
56
|
+
p.write_text(SIMPLE_SOURCE)
|
|
57
|
+
m = reformaModel.from_file(str(p))
|
|
58
|
+
assert m.name == "model"
|
|
59
|
+
assert "Home" in m.source
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# SimulationState
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
class TestSimulationState:
|
|
66
|
+
def test_transition_named_found(self):
|
|
67
|
+
t = Transition("Home", "Office", "go_work", "go_work", 0.5)
|
|
68
|
+
s = SimulationState(["Home"], [t], {}, False, None)
|
|
69
|
+
assert s.transition_named("go_work") is t
|
|
70
|
+
|
|
71
|
+
def test_transition_named_not_found(self):
|
|
72
|
+
s = SimulationState(["Home"], [], {}, False, None)
|
|
73
|
+
assert s.transition_named("missing") is None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# JarBridge — bundled JAR discovery
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
class TestJarBridge:
|
|
80
|
+
def test_explicit_missing_raises(self):
|
|
81
|
+
with pytest.raises(FileNotFoundError):
|
|
82
|
+
JarBridge("/nonexistent/RePATool.jar")
|
|
83
|
+
|
|
84
|
+
def test_no_bundled_jar_raises(self):
|
|
85
|
+
with patch("reforma.jar_bridge._bundled_jar_path", return_value=None):
|
|
86
|
+
with pytest.raises(FileNotFoundError, match="bundled"):
|
|
87
|
+
JarBridge() # no explicit path, no bundled jar
|
|
88
|
+
|
|
89
|
+
def test_bundled_jar_used_when_present(self, tmp_path):
|
|
90
|
+
fake_jar = tmp_path / "RePATool.jar"
|
|
91
|
+
fake_jar.write_bytes(b"PK") # minimal fake
|
|
92
|
+
with patch("reforma.jar_bridge._bundled_jar_path", return_value=fake_jar):
|
|
93
|
+
bridge = JarBridge()
|
|
94
|
+
assert bridge.jar_path == str(fake_jar)
|
|
95
|
+
|
|
96
|
+
def test_explicit_jar_overrides_bundled(self, tmp_path):
|
|
97
|
+
custom = tmp_path / "custom.jar"
|
|
98
|
+
custom.write_bytes(b"PK")
|
|
99
|
+
bridge = JarBridge(str(custom))
|
|
100
|
+
assert "custom" in bridge.jar_path
|
|
101
|
+
|
|
102
|
+
def test_run_nonzero_raises(self, tmp_path):
|
|
103
|
+
fake = tmp_path / "f.jar"
|
|
104
|
+
fake.write_bytes(b"")
|
|
105
|
+
bridge = JarBridge.__new__(JarBridge)
|
|
106
|
+
bridge.jar_path = str(fake)
|
|
107
|
+
bridge.java_bin = "java"
|
|
108
|
+
import subprocess
|
|
109
|
+
with patch("reforma.jar_bridge.subprocess.run") as mock_run:
|
|
110
|
+
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="err")
|
|
111
|
+
with pytest.raises(JarError):
|
|
112
|
+
bridge._run("-step", "x.r")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# reforma.bundled_jar()
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
class TestBundledJar:
|
|
119
|
+
def test_returns_none_when_absent(self):
|
|
120
|
+
with patch("reforma.jar_bridge._bundled_jar_path", return_value=None):
|
|
121
|
+
assert reforma.bundled_jar() is None
|
|
122
|
+
|
|
123
|
+
def test_returns_path_when_present(self, tmp_path):
|
|
124
|
+
fake = tmp_path / "RePATool.jar"
|
|
125
|
+
fake.write_bytes(b"PK")
|
|
126
|
+
with patch("reforma.jar_bridge._bundled_jar_path", return_value=fake):
|
|
127
|
+
assert reforma.bundled_jar() == str(fake)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
# reforma high-level
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
class TestreformaLoad:
|
|
134
|
+
def test_load_parses_state(self):
|
|
135
|
+
reforma = make_reforma()
|
|
136
|
+
assert reforma.state.current_states == ["Home"]
|
|
137
|
+
|
|
138
|
+
def test_enabled_transitions(self):
|
|
139
|
+
reforma = make_reforma()
|
|
140
|
+
labels = {t.label for t in reforma.state.enabled}
|
|
141
|
+
assert {"go_work", "go_charge"} == labels
|
|
142
|
+
|
|
143
|
+
def test_probabilities(self):
|
|
144
|
+
reforma = make_reforma()
|
|
145
|
+
t = reforma.state.transition_named("go_work")
|
|
146
|
+
assert abs(t.probability - 0.5) < 1e-6
|
|
147
|
+
|
|
148
|
+
def test_no_model_raises(self):
|
|
149
|
+
with patch("reforma.client.JarBridge"):
|
|
150
|
+
reforma = reforma()
|
|
151
|
+
with pytest.raises(RuntimeError, match="No model loaded"):
|
|
152
|
+
reforma.step("x")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class TestreformaSimulation:
|
|
156
|
+
def test_step_moves_state(self):
|
|
157
|
+
reforma = make_reforma()
|
|
158
|
+
reforma._bridge.list_transitions.return_value = STEP_OUTPUT_OFFICE
|
|
159
|
+
state = reforma.step("go_work")
|
|
160
|
+
assert state.current_states == ["Office"]
|
|
161
|
+
|
|
162
|
+
def test_step_invalid_raises(self):
|
|
163
|
+
reforma = make_reforma()
|
|
164
|
+
with pytest.raises(RuntimeError, match="not enabled"):
|
|
165
|
+
reforma.step("fly_to_moon")
|
|
166
|
+
|
|
167
|
+
def test_history_tracks_steps(self):
|
|
168
|
+
reforma = make_reforma()
|
|
169
|
+
reforma._bridge.list_transitions.return_value = STEP_OUTPUT_OFFICE
|
|
170
|
+
reforma.step("go_work")
|
|
171
|
+
assert reforma.history == ["go_work"]
|
|
172
|
+
|
|
173
|
+
def test_undo_pops_history(self):
|
|
174
|
+
reforma = make_reforma()
|
|
175
|
+
reforma._bridge.list_transitions.return_value = STEP_OUTPUT_OFFICE
|
|
176
|
+
reforma.step("go_work")
|
|
177
|
+
reforma._bridge.list_transitions.return_value = STEP_OUTPUT
|
|
178
|
+
reforma.undo()
|
|
179
|
+
assert reforma.history == []
|
|
180
|
+
|
|
181
|
+
def test_undo_empty_raises(self):
|
|
182
|
+
reforma = make_reforma()
|
|
183
|
+
with pytest.raises(RuntimeError, match="Nothing to undo"):
|
|
184
|
+
reforma.undo()
|
|
185
|
+
|
|
186
|
+
def test_reset_clears_history(self):
|
|
187
|
+
reforma = make_reforma()
|
|
188
|
+
reforma._bridge.list_transitions.return_value = STEP_OUTPUT_OFFICE
|
|
189
|
+
reforma.step("go_work")
|
|
190
|
+
reforma.reset()
|
|
191
|
+
assert reforma.history == []
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class TestreformaTraining:
|
|
195
|
+
def test_train_updates_source(self):
|
|
196
|
+
reforma = make_reforma()
|
|
197
|
+
updated = SIMPLE_SOURCE.replace("(0.5)", "(0.7)")
|
|
198
|
+
reforma._bridge.train.return_value = updated
|
|
199
|
+
reforma._bridge.list_transitions.return_value = STEP_OUTPUT
|
|
200
|
+
reforma.train([["go_work", "go_home"]])
|
|
201
|
+
assert "(0.7)" in reforma.source
|
|
202
|
+
|
|
203
|
+
def test_train_resets_history(self):
|
|
204
|
+
reforma = make_reforma()
|
|
205
|
+
reforma._bridge.list_transitions.return_value = STEP_OUTPUT_OFFICE
|
|
206
|
+
reforma.step("go_work")
|
|
207
|
+
reforma._bridge.train.return_value = SIMPLE_SOURCE
|
|
208
|
+
reforma._bridge.list_transitions.return_value = STEP_OUTPUT
|
|
209
|
+
reforma.train([["go_work"]])
|
|
210
|
+
assert reforma.history == []
|
|
211
|
+
|
|
212
|
+
def test_train_from_file(self, tmp_path):
|
|
213
|
+
log = tmp_path / "log.txt"
|
|
214
|
+
log.write_text("go_work,go_home\ngo_charge\n")
|
|
215
|
+
reforma = make_reforma()
|
|
216
|
+
reforma._bridge.train.return_value = SIMPLE_SOURCE
|
|
217
|
+
reforma._bridge.list_transitions.return_value = STEP_OUTPUT
|
|
218
|
+
reforma.train_from_file(str(log))
|
|
219
|
+
assert reforma._bridge.train.call_args[0][1] == ["go_work,go_home", "go_charge"]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class TestreformaVerification:
|
|
223
|
+
def test_check_pdl_string(self):
|
|
224
|
+
reforma = make_reforma()
|
|
225
|
+
reforma._bridge.check_pdl.return_value = "Result: 0.50000"
|
|
226
|
+
assert "0.50000" in reforma.check_pdl("Home", "{P=?[F Office]}")
|
|
227
|
+
|
|
228
|
+
def test_check_pdl_value_float(self):
|
|
229
|
+
reforma = make_reforma()
|
|
230
|
+
reforma._bridge.check_pdl.return_value = "Result: 0.75000"
|
|
231
|
+
assert abs(reforma.check_pdl_value("Home", "{P=?[F Office]}") - 0.75) < 1e-6
|
|
232
|
+
|
|
233
|
+
def test_check_pdl_true(self):
|
|
234
|
+
reforma = make_reforma()
|
|
235
|
+
reforma._bridge.check_pdl.return_value = "Result: true"
|
|
236
|
+
assert reforma.check_pdl_value("Home", "<go_work>Office") is True
|
|
237
|
+
|
|
238
|
+
def test_check_pdl_false(self):
|
|
239
|
+
reforma = make_reforma()
|
|
240
|
+
reforma._bridge.check_pdl.return_value = "Result: false"
|
|
241
|
+
assert reforma.check_pdl_value("Home", "[]false") is False
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class TestreformaExport:
|
|
245
|
+
def test_export_prism(self):
|
|
246
|
+
reforma = make_reforma()
|
|
247
|
+
reforma._bridge.get_prism.return_value = "dtmc\n..."
|
|
248
|
+
assert reforma.export_prism().startswith("dtmc")
|
|
249
|
+
|
|
250
|
+
def test_export_mcrl2(self):
|
|
251
|
+
reforma = make_reforma()
|
|
252
|
+
reforma._bridge.get_mcrl2.return_value = "act\n..."
|
|
253
|
+
assert reforma.export_mcrl2().startswith("act")
|
|
254
|
+
|
|
255
|
+
def test_export_glts(self):
|
|
256
|
+
reforma = make_reforma()
|
|
257
|
+
reforma._bridge.get_glts.return_value = "int go_work_active = 1"
|
|
258
|
+
assert "go_work_active" in reforma.export_glts()
|
|
259
|
+
|
|
260
|
+
def test_save_prism(self, tmp_path):
|
|
261
|
+
reforma = make_reforma()
|
|
262
|
+
reforma._bridge.get_prism.return_value = "dtmc\n..."
|
|
263
|
+
out = tmp_path / "model.pm"
|
|
264
|
+
reforma.save_prism(str(out))
|
|
265
|
+
assert out.read_text().startswith("dtmc")
|
|
266
|
+
|
|
267
|
+
def test_save_source(self, tmp_path):
|
|
268
|
+
reforma = make_reforma()
|
|
269
|
+
out = tmp_path / "out.r"
|
|
270
|
+
reforma.save_source(str(out))
|
|
271
|
+
assert "Home" in out.read_text()
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class TestParsePdlResult:
|
|
275
|
+
def test_float(self):
|
|
276
|
+
assert abs(_parse_pdl_result("Result: 0.33333") - 0.33333) < 1e-4
|
|
277
|
+
|
|
278
|
+
def test_true(self):
|
|
279
|
+
assert _parse_pdl_result("Result: true") is True
|
|
280
|
+
|
|
281
|
+
def test_false(self):
|
|
282
|
+
assert _parse_pdl_result("Result: false") is False
|
|
283
|
+
|
|
284
|
+
def test_unparseable(self):
|
|
285
|
+
assert isinstance(_parse_pdl_result("Deadlock found"), str)
|