@@ -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+
317494if __name__ == "__main__" :
318495 pytest .main ([__file__ , "-v" ])
0 commit comments