Skip to content

Commit 939ec39

Browse files
committed
fix: add isomorphic check, migrate tests from unittest to pytest, and merger with FSA_min, remove pngs
1 parent 17bd3f0 commit 939ec39

10 files changed

Lines changed: 469 additions & 607 deletions

File tree

evaluation_function/schemas/utils.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from graphviz import Digraph
21
from .fsa import FSA, Transition
32

43
import matplotlib.pyplot as plt

evaluation_function/test/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ test/
99
├── __init__.py # Test package init
1010
├── conftest.py # Shared fixtures and configuration
1111
├── test_epsilon_closure.py # ε-closure computation tests
12-
├── test_nfa_to_dfa.py # NFA→DFA conversion tests
13-
└── test_minimization.py # DFA minimization tests
12+
├── test_nfa_to_dfa.py # NFA→DFA conversion tests
13+
├── test_minimization.py # DFA minimization tests
14+
└── test_validation.py # validation tests
1415
```
1516

1617
## Running Tests
@@ -27,6 +28,7 @@ pytest
2728
pytest evaluation_function/test/test_epsilon_closure.py
2829
pytest evaluation_function/test/test_nfa_to_dfa.py
2930
pytest evaluation_function/test/test_minimization.py
31+
pytest evaluation_function/test/test_validation.py
3032
```
3133

3234
### Run specific test class or function
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
"""
2+
Comprehensive tests for FSA validation, analysis, and comparison.
3+
4+
Covers validation rules, determinism, completeness, reachability,
5+
dead states, string acceptance, language equivalence, and DFA isomorphism.
6+
"""
7+
8+
import pytest
9+
10+
from evaluation_function.validation.validation import *
11+
from evaluation_function.schemas.utils import make_fsa
12+
13+
14+
class TestFSAValidation:
15+
"""Tests for basic FSA validation rules."""
16+
17+
def test_valid_fsa_basic(self):
18+
fsa = make_fsa(
19+
states=["q0", "q1"],
20+
alphabet=["a"],
21+
transitions=[{"from_state": "q0", "to_state": "q1", "symbol": "a"}],
22+
initial="q0",
23+
accept=["q1"],
24+
)
25+
assert is_valid_fsa(fsa) == []
26+
27+
def test_invalid_initial_state(self):
28+
fsa = make_fsa(
29+
states=["q1"],
30+
alphabet=["a"],
31+
transitions=[],
32+
initial="q0",
33+
accept=[],
34+
)
35+
errors = is_valid_fsa(fsa)
36+
assert len(errors) > 0
37+
assert ErrorCode.INVALID_INITIAL in [e.code for e in errors]
38+
39+
def test_invalid_accept_state(self):
40+
fsa = make_fsa(
41+
states=["q0"],
42+
alphabet=["a"],
43+
transitions=[],
44+
initial="q0",
45+
accept=["q1"],
46+
)
47+
errors = is_valid_fsa(fsa)
48+
assert len(errors) > 0
49+
assert ErrorCode.INVALID_ACCEPT in [e.code for e in errors]
50+
51+
def test_invalid_transition_source(self):
52+
fsa = make_fsa(
53+
states=["q1"],
54+
alphabet=["a"],
55+
transitions=[{"from_state": "q0", "to_state": "q1", "symbol": "a"}],
56+
initial="q1",
57+
accept=[],
58+
)
59+
errors = is_valid_fsa(fsa)
60+
assert ErrorCode.INVALID_TRANSITION_SOURCE in [e.code for e in errors]
61+
62+
def test_invalid_transition_destination(self):
63+
fsa = make_fsa(
64+
states=["q0"],
65+
alphabet=["a"],
66+
transitions=[{"from_state": "q0", "to_state": "q1", "symbol": "a"}],
67+
initial="q0",
68+
accept=[],
69+
)
70+
errors = is_valid_fsa(fsa)
71+
assert ErrorCode.INVALID_TRANSITION_DEST in [e.code for e in errors]
72+
73+
def test_invalid_transition_symbol(self):
74+
fsa = make_fsa(
75+
states=["q0", "q1"],
76+
alphabet=["a"],
77+
transitions=[{"from_state": "q0", "to_state": "q1", "symbol": "b"}],
78+
initial="q0",
79+
accept=["q1"],
80+
)
81+
errors = is_valid_fsa(fsa)
82+
assert ErrorCode.INVALID_SYMBOL in [e.code for e in errors]
83+
84+
85+
class TestDeterminism:
86+
"""Tests for determinism checking."""
87+
88+
def test_deterministic_fsa(self):
89+
fsa = make_fsa(
90+
states=["q0", "q1"],
91+
alphabet=["a", "b"],
92+
transitions=[
93+
{"from_state": "q0", "to_state": "q1", "symbol": "a"},
94+
{"from_state": "q0", "to_state": "q0", "symbol": "b"},
95+
{"from_state": "q1", "to_state": "q1", "symbol": "a"},
96+
{"from_state": "q1", "to_state": "q0", "symbol": "b"},
97+
],
98+
initial="q0",
99+
accept=["q1"],
100+
)
101+
assert is_deterministic(fsa) == []
102+
103+
def test_nondeterministic_fsa(self):
104+
fsa = make_fsa(
105+
states=["q0", "q1", "q2"],
106+
alphabet=["a"],
107+
transitions=[
108+
{"from_state": "q0", "to_state": "q1", "symbol": "a"},
109+
{"from_state": "q0", "to_state": "q2", "symbol": "a"},
110+
],
111+
initial="q0",
112+
accept=["q2"],
113+
)
114+
errors = is_deterministic(fsa)
115+
assert ErrorCode.DUPLICATE_TRANSITION in [e.code for e in errors]
116+
117+
118+
class TestCompleteness:
119+
"""Tests for DFA completeness."""
120+
121+
def test_complete_dfa(self):
122+
fsa = make_fsa(
123+
states=["q0", "q1"],
124+
alphabet=["a", "b"],
125+
transitions=[
126+
{"from_state": "q0", "to_state": "q1", "symbol": "a"},
127+
{"from_state": "q0", "to_state": "q0", "symbol": "b"},
128+
{"from_state": "q1", "to_state": "q1", "symbol": "a"},
129+
{"from_state": "q1", "to_state": "q0", "symbol": "b"},
130+
],
131+
initial="q0",
132+
accept=["q1"],
133+
)
134+
assert is_complete(fsa) == []
135+
136+
def test_incomplete_dfa(self):
137+
fsa = make_fsa(
138+
states=["q0", "q1"],
139+
alphabet=["a", "b"],
140+
transitions=[{"from_state": "q0", "to_state": "q1", "symbol": "a"}],
141+
initial="q0",
142+
accept=["q1"],
143+
)
144+
errors = is_complete(fsa)
145+
assert len(errors) == 3
146+
assert ErrorCode.MISSING_TRANSITION in [e.code for e in errors]
147+
148+
def test_complete_requires_deterministic(self):
149+
fsa = make_fsa(
150+
states=["q0", "q1"],
151+
alphabet=["a"],
152+
transitions=[
153+
{"from_state": "q0", "to_state": "q0", "symbol": "a"},
154+
{"from_state": "q0", "to_state": "q1", "symbol": "a"},
155+
],
156+
initial="q0",
157+
accept=["q1"],
158+
)
159+
errors = is_complete(fsa)
160+
codes = [e.code for e in errors]
161+
assert ErrorCode.NOT_DETERMINISTIC in codes
162+
assert ErrorCode.DUPLICATE_TRANSITION in codes
163+
164+
165+
class TestReachabilityAndDeadStates:
166+
"""Tests for unreachable and dead states."""
167+
168+
def test_find_unreachable_states(self):
169+
fsa = make_fsa(
170+
states=["q0", "q1", "q2"],
171+
alphabet=["a"],
172+
transitions=[{"from_state": "q0", "to_state": "q1", "symbol": "a"}],
173+
initial="q0",
174+
accept=["q1"],
175+
)
176+
errors = find_unreachable_states(fsa)
177+
assert ErrorCode.UNREACHABLE_STATE in [e.code for e in errors]
178+
assert any("q2" in e.message for e in errors)
179+
180+
def test_find_dead_states(self):
181+
fsa = make_fsa(
182+
states=["q0", "q1", "q2"],
183+
alphabet=["a", "b"],
184+
transitions=[
185+
{"from_state": "q0", "to_state": "q1", "symbol": "a"},
186+
{"from_state": "q1", "to_state": "q2", "symbol": "b"},
187+
{"from_state": "q2", "to_state": "q2", "symbol": "a"},
188+
{"from_state": "q2", "to_state": "q2", "symbol": "b"},
189+
],
190+
initial="q0",
191+
accept=["q1"],
192+
)
193+
errors = find_dead_states(fsa)
194+
assert ErrorCode.DEAD_STATE in [e.code for e in errors]
195+
assert any("q2" in e.message for e in errors)
196+
197+
def test_dead_states_no_accept_states(self):
198+
fsa = make_fsa(
199+
states=["q0", "q1"],
200+
alphabet=["a"],
201+
transitions=[
202+
{"from_state": "q0", "to_state": "q1", "symbol": "a"},
203+
{"from_state": "q1", "to_state": "q0", "symbol": "a"},
204+
],
205+
initial="q0",
206+
accept=[],
207+
)
208+
errors = find_dead_states(fsa)
209+
assert len(errors) == 2
210+
211+
212+
class TestStringAcceptance:
213+
"""Tests for string acceptance."""
214+
215+
def test_accepts_string(self):
216+
fsa = make_fsa(
217+
states=["q0", "q1"],
218+
alphabet=["a"],
219+
transitions=[{"from_state": "q0", "to_state": "q1", "symbol": "a"}],
220+
initial="q0",
221+
accept=["q1"],
222+
)
223+
assert accepts_string(fsa, "a") == []
224+
225+
def test_rejected_no_transition(self):
226+
fsa = make_fsa(
227+
states=["q0", "q1"],
228+
alphabet=["a"],
229+
transitions=[{"from_state": "q0", "to_state": "q1", "symbol": "a"}],
230+
initial="q0",
231+
accept=["q1"],
232+
)
233+
errors = accepts_string(fsa, "aa")
234+
assert ErrorCode.TEST_CASE_FAILED in [e.code for e in errors]
235+
236+
def test_empty_string(self):
237+
fsa = make_fsa(
238+
states=["q0"],
239+
alphabet=["a"],
240+
transitions=[{"from_state": "q0", "to_state": "q0", "symbol": "a"}],
241+
initial="q0",
242+
accept=["q0"],
243+
)
244+
assert accepts_string(fsa, "") == []
245+
246+
247+
class TestLanguageEquivalence:
248+
"""Tests for language equivalence."""
249+
250+
def test_accept_same_string(self):
251+
fsa1 = make_fsa(
252+
states=["q0", "q1"],
253+
alphabet=["a"],
254+
transitions=[{"from_state": "q0", "to_state": "q1", "symbol": "a"}],
255+
initial="q0",
256+
accept=["q1"],
257+
)
258+
fsa2 = make_fsa(
259+
states=["s0", "s1"],
260+
alphabet=["a"],
261+
transitions=[{"from_state": "s0", "to_state": "s1", "symbol": "a"}],
262+
initial="s0",
263+
accept=["s1"],
264+
)
265+
assert fsas_accept_same_string(fsa1, fsa2, "a") == []
266+
267+
def test_language_mismatch(self):
268+
fsa1 = make_fsa(
269+
states=["q0"],
270+
alphabet=["a"],
271+
transitions=[{"from_state": "q0", "to_state": "q0", "symbol": "a"}],
272+
initial="q0",
273+
accept=["q0"],
274+
)
275+
fsa2 = make_fsa(
276+
states=["s0"],
277+
alphabet=["b"],
278+
transitions=[{"from_state": "s0", "to_state": "s0", "symbol": "b"}],
279+
initial="s0",
280+
accept=["s0"],
281+
)
282+
errors = fsas_accept_same_language(fsa1, fsa2)
283+
assert ErrorCode.LANGUAGE_MISMATCH in [e.code for e in errors]
284+
285+
286+
class TestIsomorphism:
287+
"""Tests for DFA isomorphism checking."""
288+
289+
def test_isomorphic_dfas(self):
290+
fsa_user = make_fsa(
291+
states=["q0", "q1"],
292+
alphabet=["a", "b"],
293+
transitions=[
294+
{"from_state": "q0", "to_state": "q1", "symbol": "a"},
295+
{"from_state": "q0", "to_state": "q0", "symbol": "b"},
296+
{"from_state": "q1", "to_state": "q0", "symbol": "a"},
297+
{"from_state": "q1", "to_state": "q1", "symbol": "b"},
298+
],
299+
initial="q0",
300+
accept=["q1"],
301+
)
302+
fsa_sol = make_fsa(
303+
states=["s0", "s1"],
304+
alphabet=["a", "b"],
305+
transitions=[
306+
{"from_state": "s0", "to_state": "s1", "symbol": "a"},
307+
{"from_state": "s0", "to_state": "s0", "symbol": "b"},
308+
{"from_state": "s1", "to_state": "s0", "symbol": "a"},
309+
{"from_state": "s1", "to_state": "s1", "symbol": "b"},
310+
],
311+
initial="s0",
312+
accept=["s1"],
313+
)
314+
assert are_isomorphic(fsa_user, fsa_sol) == []
315+
316+
317+
if __name__ == "__main__":
318+
pytest.main([__file__, "-v"])

evaluation_function/validation/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ A Python-based utility module for the structural validation, property analysis,
4444
| `accepts_string(fsa, string)` | Tests if the FSA accepts a specific string. Supports non-determinism. |
4545
| `fsas_accept_same_language(fsa1, fsa2)` | Compares two FSAs for equivalence up to a specific string length. |
4646

47+
### 4. Isomorphism
48+
| Function | Description |
49+
| --- | --- |
50+
| `are_isomorphism(fsa1, fsa2)`| Check if two FSA are isomorphic |
51+
4752
---
4853

4954
## 📋 Data Structure Support

0 commit comments

Comments
 (0)