Skip to content

Commit 6b0112c

Browse files
committed
feat: add comprehensive tests for epsilon transitions in FSA validation; enhance epsilon handling in validation logic
1 parent edead19 commit 6b0112c

3 files changed

Lines changed: 336 additions & 8 deletions

File tree

evaluation_function/test/test_correction.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,5 +196,121 @@ def test_non_minimal_fsa_fails_when_required(self, equivalent_dfa):
196196
assert result.fsa_feedback is not None
197197

198198

199+
# =============================================================================
200+
# Test Epsilon Transitions (End-to-End)
201+
# =============================================================================
202+
203+
class TestEpsilonTransitionCorrection:
204+
"""Test the full correction pipeline with ε-NFA inputs."""
205+
206+
def test_epsilon_nfa_vs_equivalent_dfa_correct(self):
207+
"""ε-NFA student answer equivalent to DFA expected should be correct."""
208+
# ε-NFA accepts exactly "a": q0 --ε--> q1 --a--> q2
209+
student_enfa = make_fsa(
210+
states=["q0", "q1", "q2"],
211+
alphabet=["a"],
212+
transitions=[
213+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
214+
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
215+
],
216+
initial="q0",
217+
accept=["q2"],
218+
)
219+
# DFA accepts exactly "a": s0 --a--> s1
220+
expected_dfa = make_fsa(
221+
states=["s0", "s1"],
222+
alphabet=["a"],
223+
transitions=[
224+
{"from_state": "s0", "to_state": "s1", "symbol": "a"},
225+
],
226+
initial="s0",
227+
accept=["s1"],
228+
)
229+
result = analyze_fsa_correction(student_enfa, expected_dfa)
230+
assert isinstance(result, Result)
231+
assert result.is_correct is True
232+
233+
def test_epsilon_nfa_vs_different_dfa_incorrect(self):
234+
"""ε-NFA accepting 'a' vs DFA accepting 'b' should be incorrect."""
235+
student_enfa = make_fsa(
236+
states=["q0", "q1", "q2"],
237+
alphabet=["a", "b"],
238+
transitions=[
239+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
240+
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
241+
],
242+
initial="q0",
243+
accept=["q2"],
244+
)
245+
expected_dfa = make_fsa(
246+
states=["s0", "s1"],
247+
alphabet=["a", "b"],
248+
transitions=[
249+
{"from_state": "s0", "to_state": "s1", "symbol": "b"},
250+
],
251+
initial="s0",
252+
accept=["s1"],
253+
)
254+
result = analyze_fsa_correction(student_enfa, expected_dfa)
255+
assert isinstance(result, Result)
256+
assert result.is_correct is False
257+
assert result.fsa_feedback is not None
258+
assert len(result.fsa_feedback.errors) > 0
259+
260+
def test_multi_epsilon_nfa_vs_dfa_correct(self):
261+
"""ε-NFA for (a|b) with branching epsilons should match equivalent DFA."""
262+
student_enfa = make_fsa(
263+
states=["q0", "q1", "q2", "q3"],
264+
alphabet=["a", "b"],
265+
transitions=[
266+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
267+
{"from_state": "q0", "to_state": "q2", "symbol": "ε"},
268+
{"from_state": "q1", "to_state": "q3", "symbol": "a"},
269+
{"from_state": "q2", "to_state": "q3", "symbol": "b"},
270+
],
271+
initial="q0",
272+
accept=["q3"],
273+
)
274+
expected_dfa = make_fsa(
275+
states=["s0", "s1"],
276+
alphabet=["a", "b"],
277+
transitions=[
278+
{"from_state": "s0", "to_state": "s1", "symbol": "a"},
279+
{"from_state": "s0", "to_state": "s1", "symbol": "b"},
280+
],
281+
initial="s0",
282+
accept=["s1"],
283+
)
284+
result = analyze_fsa_correction(student_enfa, expected_dfa)
285+
assert isinstance(result, Result)
286+
assert result.is_correct is True
287+
288+
def test_epsilon_nfa_structural_info_reports_nondeterministic(self):
289+
"""ε-NFA should have structural info reporting non-deterministic."""
290+
student_enfa = make_fsa(
291+
states=["q0", "q1", "q2"],
292+
alphabet=["a"],
293+
transitions=[
294+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
295+
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
296+
],
297+
initial="q0",
298+
accept=["q2"],
299+
)
300+
expected_dfa = make_fsa(
301+
states=["s0", "s1"],
302+
alphabet=["a"],
303+
transitions=[
304+
{"from_state": "s0", "to_state": "s1", "symbol": "a"},
305+
],
306+
initial="s0",
307+
accept=["s1"],
308+
)
309+
result = analyze_fsa_correction(student_enfa, expected_dfa)
310+
assert result.fsa_feedback is not None
311+
assert result.fsa_feedback.structural is not None
312+
assert result.fsa_feedback.structural.is_deterministic is False
313+
314+
199315
if __name__ == "__main__":
200316
pytest.main([__file__, "-v"])

evaluation_function/test/test_validation.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,5 +314,182 @@ def test_isomorphic_dfas(self):
314314
assert are_isomorphic(fsa_user, fsa_sol) == []
315315

316316

317+
class TestEpsilonTransitions:
318+
"""Tests for epsilon transition handling across the validation pipeline."""
319+
320+
def test_valid_fsa_with_epsilon_unicode(self):
321+
"""ε-NFA with Unicode ε should pass structural validation."""
322+
fsa = make_fsa(
323+
states=["q0", "q1", "q2"],
324+
alphabet=["a"],
325+
transitions=[
326+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
327+
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
328+
],
329+
initial="q0",
330+
accept=["q2"],
331+
)
332+
assert is_valid_fsa(fsa) == []
333+
334+
def test_valid_fsa_with_epsilon_string(self):
335+
"""ε-NFA with 'epsilon' string should pass structural validation."""
336+
fsa = make_fsa(
337+
states=["q0", "q1", "q2"],
338+
alphabet=["a"],
339+
transitions=[
340+
{"from_state": "q0", "to_state": "q1", "symbol": "epsilon"},
341+
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
342+
],
343+
initial="q0",
344+
accept=["q2"],
345+
)
346+
assert is_valid_fsa(fsa) == []
347+
348+
def test_valid_fsa_with_empty_string_epsilon(self):
349+
"""ε-NFA with empty string epsilon should pass structural validation."""
350+
fsa = make_fsa(
351+
states=["q0", "q1", "q2"],
352+
alphabet=["a"],
353+
transitions=[
354+
{"from_state": "q0", "to_state": "q1", "symbol": ""},
355+
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
356+
],
357+
initial="q0",
358+
accept=["q2"],
359+
)
360+
assert is_valid_fsa(fsa) == []
361+
362+
def test_epsilon_nfa_is_not_deterministic(self):
363+
"""ε-NFA should be flagged as non-deterministic."""
364+
fsa = make_fsa(
365+
states=["q0", "q1"],
366+
alphabet=["a"],
367+
transitions=[
368+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
369+
],
370+
initial="q0",
371+
accept=["q1"],
372+
)
373+
errors = is_deterministic(fsa)
374+
assert len(errors) > 0
375+
assert ErrorCode.NOT_DETERMINISTIC in [e.code for e in errors]
376+
377+
def test_accepts_string_via_epsilon_closure(self):
378+
"""ε-NFA should accept 'a' by following q0 --ε--> q1 --a--> q2."""
379+
fsa = make_fsa(
380+
states=["q0", "q1", "q2"],
381+
alphabet=["a"],
382+
transitions=[
383+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
384+
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
385+
],
386+
initial="q0",
387+
accept=["q2"],
388+
)
389+
assert accepts_string(fsa, "a") == []
390+
391+
def test_rejects_string_with_epsilon_nfa(self):
392+
"""ε-NFA that accepts 'a' should reject empty string."""
393+
fsa = make_fsa(
394+
states=["q0", "q1", "q2"],
395+
alphabet=["a"],
396+
transitions=[
397+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
398+
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
399+
],
400+
initial="q0",
401+
accept=["q2"],
402+
)
403+
errors = accepts_string(fsa, "")
404+
assert len(errors) > 0
405+
406+
def test_accepts_empty_string_via_epsilon(self):
407+
"""ε-NFA should accept empty string when initial reaches accept via ε."""
408+
fsa = make_fsa(
409+
states=["q0", "q1"],
410+
alphabet=["a"],
411+
transitions=[
412+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
413+
],
414+
initial="q0",
415+
accept=["q1"],
416+
)
417+
assert accepts_string(fsa, "") == []
418+
419+
def test_epsilon_nfa_equivalent_to_dfa(self):
420+
"""ε-NFA and DFA accepting the same language should be equivalent."""
421+
enfa = make_fsa(
422+
states=["q0", "q1", "q2"],
423+
alphabet=["a"],
424+
transitions=[
425+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
426+
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
427+
],
428+
initial="q0",
429+
accept=["q2"],
430+
)
431+
dfa = make_fsa(
432+
states=["s0", "s1"],
433+
alphabet=["a"],
434+
transitions=[
435+
{"from_state": "s0", "to_state": "s1", "symbol": "a"},
436+
],
437+
initial="s0",
438+
accept=["s1"],
439+
)
440+
assert fsas_accept_same_language(enfa, dfa) == []
441+
442+
def test_epsilon_nfa_not_equivalent_to_different_dfa(self):
443+
"""ε-NFA and DFA accepting different languages should not be equivalent."""
444+
enfa = make_fsa(
445+
states=["q0", "q1", "q2"],
446+
alphabet=["a", "b"],
447+
transitions=[
448+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
449+
{"from_state": "q1", "to_state": "q2", "symbol": "a"},
450+
],
451+
initial="q0",
452+
accept=["q2"],
453+
)
454+
dfa = make_fsa(
455+
states=["s0", "s1"],
456+
alphabet=["a", "b"],
457+
transitions=[
458+
{"from_state": "s0", "to_state": "s1", "symbol": "b"},
459+
],
460+
initial="s0",
461+
accept=["s1"],
462+
)
463+
errors = fsas_accept_same_language(enfa, dfa)
464+
assert len(errors) > 0
465+
466+
def test_multi_epsilon_nfa_equivalent_to_dfa(self):
467+
"""ε-NFA for (a|b) with branching epsilons should match equivalent DFA."""
468+
# q0 --ε--> q1, q0 --ε--> q2, q1 --a--> q3, q2 --b--> q3
469+
enfa = make_fsa(
470+
states=["q0", "q1", "q2", "q3"],
471+
alphabet=["a", "b"],
472+
transitions=[
473+
{"from_state": "q0", "to_state": "q1", "symbol": "ε"},
474+
{"from_state": "q0", "to_state": "q2", "symbol": "ε"},
475+
{"from_state": "q1", "to_state": "q3", "symbol": "a"},
476+
{"from_state": "q2", "to_state": "q3", "symbol": "b"},
477+
],
478+
initial="q0",
479+
accept=["q3"],
480+
)
481+
dfa = make_fsa(
482+
states=["s0", "s1"],
483+
alphabet=["a", "b"],
484+
transitions=[
485+
{"from_state": "s0", "to_state": "s1", "symbol": "a"},
486+
{"from_state": "s0", "to_state": "s1", "symbol": "b"},
487+
],
488+
initial="s0",
489+
accept=["s1"],
490+
)
491+
assert fsas_accept_same_language(enfa, dfa) == []
492+
493+
317494
if __name__ == "__main__":
318495
pytest.main([__file__, "-v"])

0 commit comments

Comments
 (0)