"""Tests for CRX Refined (cluster-then-infer).""" import pytest from bex.crx_refined import crx_refined, crx_with_confidence, _cluster_by_structure from bex.crx import CRX class TestClusterByStructure: def test_empty(self): assert _cluster_by_structure([]) == {} def test_single_sequence(self): result = _cluster_by_structure([['a', 'b', 'c']]) assert len(result) == 1 assert ('a', 'c', 'short') in result def test_same_start_end_same_length(self): seqs = [['a', 'b', 'c'], ['a', 'd', 'c']] result = _cluster_by_structure(seqs) # Both start with 'a', end with 'c', length 3 (short) assert len(result) == 1 def test_different_start_end(self): seqs = [['a', 'b', 'c'], ['x', 'y', 'z']] result = _cluster_by_structure(seqs) assert len(result) == 2 def test_length_buckets(self): seqs = [ ['a', 'b', 'a'], # short ['a', 'b', 'c', 'a'], # short (different last) ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'a'], # med (8, same first/last) ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'a'], # long (9, same first/last) ] result = _cluster_by_structure(seqs) keys = set(result.keys()) # 'a'→'a' short, 'a'→'a' med, 'a'→'a' long = 3 clusters for same first/last # 'a'→'a' short has 1 seq, med has 1, long has 1 assert ('a', 'a', 'short') in keys assert ('a', 'a', 'med') in keys assert ('a', 'a', 'long') in keys class TestCrxRefined: def test_empty(self): assert crx_refined([]) == 'ε' def test_single_sequence(self): result = crx_refined([['a', 'b', 'c']]) assert result is not None assert 'a' in result def test_identical_sequences(self): seqs = [['a', 'b', 'c']] * 5 result = crx_refined(seqs) assert 'a' in result assert 'b' in result def test_linear_pattern(self): seqs = [['a', 'b', 'c'], ['a', 'b', 'c'], ['a', 'b', 'c']] result = crx_refined(seqs) # All same → should be tight assert result == CRX().infer(seqs) def test_branching_pattern(self): # Paper counterexample seqs = [['a', 'b', 'c'], ['a', 'd', 'e'], ['a', 'b', 'e']] result = crx_refined(seqs) refined_g = crx_refined(seqs) # Refined should produce SOMETHING (not crash) assert refined_g is not None assert 'a' in refined_g def test_falls_back_to_standard(self): # Very diverse sequences — no cluster large enough seqs = [['a'], ['b'], ['c'], ['d']] result = crx_refined(seqs, min_cluster=2) # Should fall back to standard CRX assert result == CRX().infer(seqs) class TestCrxWithConfidence: def test_empty(self): result = crx_with_confidence([]) assert result['grammar'] == 'ε' assert result['confidence'] == 1.0 assert result['n_clusters'] == 0 def test_returns_all_fields(self): seqs = [['a', 'b', 'c'], ['a', 'b', 'c']] result = crx_with_confidence(seqs) assert 'grammar' in result assert 'confidence' in result assert 'n_clusters' in result assert 'largest_cluster' in result assert 'n_sequences' in result def test_confidence_range(self): seqs = [['a', 'b', 'c'], ['a', 'd', 'e']] result = crx_with_confidence(seqs) assert 0.0 <= result['confidence'] <= 1.0 def test_confident_when_tight(self): seqs = [['a', 'b', 'c']] * 10 result = crx_with_confidence(seqs) assert result['confidence'] >= 0.9 def test_less_confident_when_diverse(self): # Many sequences sharing first/last but with different internals seqs = [ ['a', 'b', 'c', 'z'], ['a', 'x', 'y', 'z'], ['a', 'p', 'q', 'z'], ['a', 'r', 's', 'z'], ] result = crx_with_confidence(seqs) # All share first/last → one cluster, but internals differ # Confidence depends on how many pairs the cluster grammar captures assert result['n_clusters'] >= 1 class TestComparisonWithStandard: """Compare refined vs standard CRX on various inputs.""" def test_linear_same_result(self): seqs = [['a', 'b', 'c']] * 5 assert crx_refined(seqs) == CRX().infer(seqs) def test_single_symbol(self): seqs = [['a']] * 5 assert crx_refined(seqs) == 'a' def test_two_symbols(self): seqs = [['a', 'b']] * 5 assert crx_refined(seqs) == 'a.b' def test_disjunction(self): seqs = [['a', 'b'], ['a', 'c']] result = crx_refined(seqs) # Should have a disjunction somewhere assert '+' in result