Assertions now use isinstance() on AST nodes (Concat, Alt, Plus, Optional, Star, Symbol, Empty) instead of comparing SORE strings.
204 lines
5.6 KiB
Python
204 lines
5.6 KiB
Python
"""Tests for grammar AST, matching, counting, and GBNF rendering."""
|
|
|
|
import pytest
|
|
from bex.grammar import (
|
|
Symbol, Concat, Alt, Plus, Optional, Star, Epsilon, Empty,
|
|
alphabet, count_words, lang_size, model_cost, match,
|
|
)
|
|
from bex.gbnf import to_gbnf, to_gbnf_with_rules, grammar_structure_score
|
|
|
|
|
|
# ── AST Construction ──
|
|
|
|
class TestASTConstruction:
|
|
def test_symbol(self):
|
|
assert Symbol('a') == Symbol('a')
|
|
assert Symbol('a') != Symbol('b')
|
|
|
|
def test_concat(self):
|
|
c = Concat([Symbol('a'), Symbol('b')])
|
|
assert c.parts == [Symbol('a'), Symbol('b')]
|
|
|
|
def test_alt(self):
|
|
a = Alt([Symbol('a'), Symbol('b')])
|
|
assert a.parts == [Symbol('a'), Symbol('b')]
|
|
|
|
def test_plus(self):
|
|
p = Plus(Symbol('a'))
|
|
assert p.child == Symbol('a')
|
|
|
|
def test_optional(self):
|
|
o = Optional(Symbol('a'))
|
|
assert o.child == Symbol('a')
|
|
|
|
def test_star(self):
|
|
s = Star(Symbol('a'))
|
|
assert s.child == Symbol('a')
|
|
|
|
def test_epsilon(self):
|
|
assert Epsilon() == Epsilon()
|
|
|
|
def test_empty(self):
|
|
assert Empty() == Empty()
|
|
|
|
|
|
# ── Alphabet ──
|
|
|
|
class TestAlphabet:
|
|
def test_symbol(self):
|
|
assert alphabet(Symbol('a')) == {'a'}
|
|
|
|
def test_concat(self):
|
|
assert alphabet(Concat([Symbol('a'), Symbol('b')])) == {'a', 'b'}
|
|
|
|
def test_nested(self):
|
|
g = Concat([Symbol('a'), Plus(Alt([Symbol('b'), Symbol('c')]))])
|
|
assert alphabet(g) == {'a', 'b', 'c'}
|
|
|
|
|
|
# ── Matching ──
|
|
|
|
class TestMatch:
|
|
def test_symbol(self):
|
|
assert match(Symbol('a'), ['a'])
|
|
assert not match(Symbol('a'), ['b'])
|
|
assert not match(Symbol('a'), [])
|
|
|
|
def test_concat(self):
|
|
g = Concat([Symbol('a'), Symbol('b'), Symbol('c')])
|
|
assert match(g, ['a', 'b', 'c'])
|
|
assert not match(g, ['a', 'b'])
|
|
assert not match(g, ['a', 'b', 'c', 'd'])
|
|
|
|
def test_alt(self):
|
|
g = Alt([Symbol('a'), Symbol('b')])
|
|
assert match(g, ['a'])
|
|
assert match(g, ['b'])
|
|
assert not match(g, ['c'])
|
|
|
|
def test_plus(self):
|
|
g = Plus(Symbol('a'))
|
|
assert match(g, ['a'])
|
|
assert match(g, ['a', 'a'])
|
|
assert match(g, ['a', 'a', 'a'])
|
|
assert not match(g, [])
|
|
assert not match(g, ['b'])
|
|
|
|
def test_optional(self):
|
|
g = Optional(Symbol('a'))
|
|
assert match(g, ['a'])
|
|
assert match(g, [])
|
|
assert not match(g, ['b'])
|
|
|
|
def test_star(self):
|
|
g = Star(Symbol('a'))
|
|
assert match(g, [])
|
|
assert match(g, ['a'])
|
|
assert match(g, ['a', 'a'])
|
|
assert not match(g, ['b'])
|
|
|
|
def test_complex_grammar(self):
|
|
g = Concat([Plus(Symbol('init')), Plus(Symbol('capability')),
|
|
Concat([Plus(Symbol('invoke')), Symbol('request')])])
|
|
assert match(g, ['init', 'capability', 'invoke', 'request'])
|
|
assert match(g, ['init', 'init', 'capability', 'capability', 'invoke', 'request'])
|
|
assert not match(g, ['init'])
|
|
|
|
|
|
# ── Counting ──
|
|
|
|
class TestCountWords:
|
|
def test_symbol(self):
|
|
assert count_words(Symbol('a'), 1) == 1
|
|
assert count_words(Symbol('a'), 0) == 0
|
|
|
|
def test_epsilon(self):
|
|
assert count_words(Epsilon(), 0) == 1
|
|
assert count_words(Epsilon(), 1) == 0
|
|
|
|
def test_empty(self):
|
|
assert count_words(Empty(), 0) == 0
|
|
|
|
def test_concat(self):
|
|
g = Concat([Symbol('a'), Symbol('b'), Symbol('c')])
|
|
assert count_words(g, 3) == 1
|
|
assert count_words(g, 2) == 0
|
|
|
|
def test_alt(self):
|
|
g = Alt([Symbol('a'), Symbol('b'), Symbol('c')])
|
|
assert count_words(g, 1) == 3
|
|
|
|
def test_plus(self):
|
|
g = Plus(Symbol('a'))
|
|
assert count_words(g, 1) == 1
|
|
assert count_words(g, 2) == 1
|
|
assert count_words(g, 0) == 0
|
|
|
|
def test_optional(self):
|
|
g = Optional(Symbol('a'))
|
|
assert count_words(g, 0) == 1
|
|
assert count_words(g, 1) == 1
|
|
|
|
def test_star(self):
|
|
g = Star(Symbol('a'))
|
|
assert count_words(g, 0) == 1
|
|
assert count_words(g, 1) == 1
|
|
|
|
def test_disjunction_plus(self):
|
|
g = Plus(Alt([Symbol('a'), Symbol('b'), Symbol('c')]))
|
|
assert count_words(g, 1) == 3
|
|
assert count_words(g, 2) == 9
|
|
|
|
def test_info_plus(self):
|
|
g = Plus(Symbol('info'))
|
|
for l in range(1, 8):
|
|
assert count_words(g, l) == 1
|
|
|
|
|
|
# ── Model cost ──
|
|
|
|
class TestModelCost:
|
|
def test_symbol(self):
|
|
assert model_cost(Symbol('a')) == 1
|
|
|
|
def test_concat(self):
|
|
assert model_cost(Concat([Symbol('a'), Symbol('b'), Symbol('c')])) == 3
|
|
|
|
def test_plus(self):
|
|
assert model_cost(Plus(Symbol('a'))) == 1
|
|
|
|
def test_nested(self):
|
|
g = Concat([Symbol('a'), Plus(Alt([Symbol('b'), Symbol('c')]))])
|
|
assert model_cost(g) == 3
|
|
|
|
|
|
# ── GBNF Rendering ──
|
|
|
|
def test_to_gbnf():
|
|
g = Plus(Symbol('init'))
|
|
result = to_gbnf(g)
|
|
assert result == 'root ::= "init"+'
|
|
|
|
|
|
def test_to_gbnf_with_rules():
|
|
g = Alt([Symbol('a'), Symbol('b')])
|
|
result = to_gbnf_with_rules(g, name='choice')
|
|
assert result == 'choice ::= "a" | "b"'
|
|
|
|
|
|
# ── Structure score ──
|
|
|
|
class TestStructureScore:
|
|
def test_empty(self):
|
|
assert grammar_structure_score(Empty()) == 0.0
|
|
|
|
def test_symbol(self):
|
|
assert grammar_structure_score(Symbol('a')) == 0.0
|
|
|
|
def test_concat(self):
|
|
g = Concat([Symbol('a'), Symbol('b'), Symbol('c')])
|
|
assert grammar_structure_score(g) > 0.0
|
|
|
|
def test_plus(self):
|
|
g = Plus(Symbol('a'))
|
|
assert grammar_structure_score(g) > 0.0
|