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