def __init__(self, name=None, abbr=None, schema=None, theories=None): super(NonrecursiveRuleTheory, self).__init__( name=name, abbr=abbr, theories=theories, schema=schema) # dictionary from table name to list of rules with that table in head self.rules = RuleSet() self.kind = NONRECURSIVE_POLICY_TYPE
def setUp(self): super(TestRuleSet, self).setUp() self.ruleset = RuleSet()
class NonrecursiveRuleTheory(TopDownTheory): """A non-recursive collection of Rules.""" def __init__(self, name=None, abbr=None, schema=None, theories=None): super(NonrecursiveRuleTheory, self).__init__( name=name, abbr=abbr, theories=theories, schema=schema) # dictionary from table name to list of rules with that table in head self.rules = RuleSet() self.kind = NONRECURSIVE_POLICY_TYPE # External Interface # SELECT implemented by TopDownTheory def initialize_tables(self, tablenames, facts): """Event handler for (re)initializing a collection of tables @facts must be an iterable containing compile.Fact objects. """ LOG.info("initialize_tables") cleared_tables = set(tablenames) for t in tablenames: self.rules.clear_table(t) count = 0 for f in facts: if f.table not in cleared_tables: self.rules.clear_table(f.table) cleared_tables.add(f.table) self.rules.add_rule(f.table, f) count += 1 LOG.info("initialized %d tables with %d facts", len(cleared_tables), count) def insert(self, rule): changes = self.update([Event(formula=rule, insert=True)]) return [event.formula for event in changes] def delete(self, rule): changes = self.update([Event(formula=rule, insert=False)]) return [event.formula for event in changes] def update(self, events): """Apply EVENTS. And return the list of EVENTS that actually changed the theory. Each event is the insert or delete of a policy statement. """ changes = [] self.log(None, "Update %s", iterstr(events)) try: for event in events: formula = compile.reorder_for_safety(event.formula) if event.insert: if self._insert_actual(formula): changes.append(event) else: if self._delete_actual(formula): changes.append(event) except Exception as e: LOG.exception("runtime caught an exception") raise e return changes def update_would_cause_errors(self, events): """Return a list of PolicyException. Return a list of PolicyException if we were to apply the insert/deletes of policy statements dictated by EVENTS to the current policy. """ self.log(None, "update_would_cause_errors %s", iterstr(events)) errors = [] for event in events: if not compile.is_datalog(event.formula): errors.append(PolicyException( "Non-formula found: {}".format( str(event.formula)))) else: if event.formula.is_atom(): errors.extend(compile.fact_errors( event.formula, self.theories, self.name)) else: errors.extend(compile.rule_errors( event.formula, self.theories, self.name)) # Would also check that rules are non-recursive, but that # is currently being handled by Runtime. The current implementation # disallows recursion in all theories. return errors def define(self, rules): """Empties and then inserts RULES.""" self.empty() return self.update([Event(formula=rule, insert=True) for rule in rules]) def empty(self, tablenames=None, invert=False): """Deletes contents of theory. If provided, TABLENAMES causes only the removal of all rules that help define one of the tables in TABLENAMES. If INVERT is true, all rules defining anything other than a table in TABLENAMES is deleted. """ if tablenames is None: self.rules.clear() return if invert: to_clear = set(self.defined_tablenames()) - set(tablenames) else: to_clear = tablenames for table in to_clear: self.rules.clear_table(table) def policy(self): # eliminate all rules with empty bodies return [p for p in self.content() if len(p.body) > 0] def get_arity_self(self, tablename, theory): if tablename not in self.rules: return None rules = self.rules.get_rules(tablename) if len(rules) == 0: return None try: rule = next(rule for rule in rules if rule.head.theory == theory) except StopIteration: return None return len(rule.head.arguments) def __contains__(self, formula): if compile.is_atom(formula): return self.rules.contains(formula.table, formula) else: return self.rules.contains(formula.head.table, formula) # Internal Interface def _insert_actual(self, rule): """Insert RULE and return True if there was a change.""" if compile.is_atom(rule): rule = compile.Rule(rule, [], rule.location) self.log(rule.head.table, "Insert: %s", repr(rule)) return self.rules.add_rule(rule.head.table, rule) def _delete_actual(self, rule): """Delete RULE and return True if there was a change.""" if compile.is_atom(rule): rule = compile.Rule(rule, [], rule.location) self.log(rule.head.table, "Delete: %s", rule) return self.rules.discard_rule(rule.head.table, rule) def content(self, tablenames=None): if tablenames is None: tablenames = self.rules.keys() results = [] for table in tablenames: if table in self.rules: results.extend(self.rules.get_rules(table)) return results def head_index(self, table, match_literal=None): """Return head index. This routine must return all the formulas pertinent for top-down evaluation when a literal with TABLE is at the top of the stack. """ if table in self.rules: return self.rules.get_rules(table, match_literal) return [] def arity(self, tablename): """Return the number of arguments TABLENAME takes. None if unknown because TABLENAME is not defined here. """ # assuming a fixed arity for all tables formulas = self.head_index(tablename) if len(formulas) == 0: return None first = formulas[0] # should probably have an overridable function for computing # the arguments of a head. Instead we assume heads have .arguments return len(self.head(first).arguments) def defined_tablenames(self): """Returns list of table names defined in/written to this theory.""" return self.rules.keys() def head(self, formula): """Given the output from head_index(), return the formula head. Given a FORMULA, return the thing to unify against. Usually, FORMULA is a compile.Rule, but it could be anything returned by HEAD_INDEX. """ return formula.head def body(self, formula): """Return formula body. Given a FORMULA, return a list of things to push onto the top-down eval stack. """ return formula.body
class TestRuleSet(base.TestCase): def setUp(self): super(TestRuleSet, self).setUp() self.ruleset = RuleSet() def test_empty_ruleset(self): self.assertFalse('p' in self.ruleset) self.assertEqual([], self.ruleset.keys()) def test_clear_ruleset(self): rule1 = compile.parse1('p(x,y) :- q(x), r(y)') self.ruleset.add_rule('p', rule1) self.ruleset.clear() self.assertFalse('p' in self.ruleset) self.assertEqual([], self.ruleset.keys()) def test_add_rule(self): rule1 = compile.parse1('p(x,y) :- q(x), r(y)') self.assertTrue(self.ruleset.add_rule('p', rule1)) self.assertTrue('p' in self.ruleset) self.assertEqual([rule1], self.ruleset.get_rules('p')) self.assertEqual(['p'], self.ruleset.keys()) def test_add_existing_rule(self): rule1 = compile.parse1('p(x,y) :- q(x), r(y)') self.assertTrue(self.ruleset.add_rule('p', rule1)) self.assertTrue('p' in self.ruleset) self.assertEqual([rule1], self.ruleset.get_rules('p')) self.assertEqual(['p'], self.ruleset.keys()) self.assertFalse(self.ruleset.add_rule('p', rule1)) self.assertTrue('p' in self.ruleset) self.assertEqual([rule1], self.ruleset.get_rules('p')) self.assertEqual(['p'], self.ruleset.keys()) def test_add_rules_with_same_head(self): rule1 = compile.parse1('p(x,y) :- q(x), r(y)') rule2 = compile.parse1('p(x,y) :- s(x), t(y)') self.assertTrue(self.ruleset.add_rule('p', rule1)) self.assertTrue('p' in self.ruleset) self.assertEqual([rule1], self.ruleset.get_rules('p')) self.assertEqual(['p'], self.ruleset.keys()) self.assertTrue(self.ruleset.add_rule('p', rule2)) self.assertTrue('p' in self.ruleset) self.assertTrue(rule1 in self.ruleset.get_rules('p')) self.assertTrue(rule2 in self.ruleset.get_rules('p')) self.assertEqual(['p'], self.ruleset.keys()) def test_add_rules_with_different_head(self): rule1 = compile.parse1('p1(x,y) :- q(x), r(y)') rule2 = compile.parse1('p2(x,y) :- s(x), t(y)') self.assertTrue(self.ruleset.add_rule('p1', rule1)) self.assertTrue(self.ruleset.add_rule('p2', rule2)) self.assertTrue('p1' in self.ruleset) self.assertEqual([rule1], self.ruleset.get_rules('p1')) self.assertTrue('p1' in self.ruleset.keys()) self.assertTrue('p2' in self.ruleset) self.assertEqual([rule2], self.ruleset.get_rules('p2')) self.assertTrue('p2' in self.ruleset.keys()) def test_add_fact(self): fact1 = Fact('p', (1, 2, 3)) equivalent_rule = compile.Rule(compile.parse1('p(1,2,3)'), ()) self.assertTrue(self.ruleset.add_rule('p', fact1)) self.assertTrue('p' in self.ruleset) self.assertEqual([equivalent_rule], self.ruleset.get_rules('p')) self.assertEqual(['p'], self.ruleset.keys()) def test_add_equivalent_rule(self): # equivalent_rule could be a fact because it has no body, and is # ground. equivalent_rule = compile.Rule(compile.parse1('p(1,2,3)'), ()) self.assertTrue(self.ruleset.add_rule('p', equivalent_rule)) self.assertTrue('p' in self.ruleset) self.assertEqual([equivalent_rule], self.ruleset.get_rules('p')) self.assertEqual(['p'], self.ruleset.keys()) def test_discard_rule(self): rule1 = compile.parse1('p(x,y) :- q(x), r(y)') self.assertTrue(self.ruleset.add_rule('p', rule1)) self.assertTrue('p' in self.ruleset) self.assertEqual([rule1], self.ruleset.get_rules('p')) self.assertTrue(self.ruleset.discard_rule('p', rule1)) self.assertFalse('p' in self.ruleset) self.assertEqual([], self.ruleset.keys()) def test_discard_nonexistent_rule(self): rule1 = compile.parse1('p(x,y) :- q(x), r(y)') self.assertFalse(self.ruleset.discard_rule('p', rule1)) self.assertFalse('p' in self.ruleset) self.assertEqual([], self.ruleset.keys()) def test_discard_rules_with_same_head(self): rule1 = compile.parse1('p(x,y) :- q(x), r(y)') rule2 = compile.parse1('p(x,y) :- s(x), t(y)') self.assertTrue(self.ruleset.add_rule('p', rule1)) self.assertTrue(self.ruleset.add_rule('p', rule2)) self.assertTrue('p' in self.ruleset) self.assertTrue(rule1 in self.ruleset.get_rules('p')) self.assertTrue(rule2 in self.ruleset.get_rules('p')) self.assertTrue(self.ruleset.discard_rule('p', rule1)) self.assertTrue(self.ruleset.discard_rule('p', rule2)) self.assertFalse('p' in self.ruleset) self.assertEqual([], self.ruleset.keys()) def test_discard_rules_with_different_head(self): rule1 = compile.parse1('p1(x,y) :- q(x), r(y)') rule2 = compile.parse1('p2(x,y) :- s(x), t(y)') self.assertTrue(self.ruleset.add_rule('p1', rule1)) self.assertTrue(self.ruleset.add_rule('p2', rule2)) self.assertTrue('p1' in self.ruleset) self.assertTrue('p2' in self.ruleset) self.assertTrue(rule1 in self.ruleset.get_rules('p1')) self.assertTrue(rule2 in self.ruleset.get_rules('p2')) self.assertTrue(self.ruleset.discard_rule('p1', rule1)) self.assertTrue(self.ruleset.discard_rule('p2', rule2)) self.assertFalse('p1' in self.ruleset) self.assertFalse('p2' in self.ruleset) self.assertEqual([], self.ruleset.keys()) def test_discard_fact(self): fact = Fact('p', (1, 2, 3)) equivalent_rule = compile.Rule(compile.parse1('p(1,2,3)'), ()) self.assertTrue(self.ruleset.add_rule('p', fact)) self.assertTrue('p' in self.ruleset) self.assertEqual([equivalent_rule], self.ruleset.get_rules('p')) self.assertTrue(self.ruleset.discard_rule('p', fact)) self.assertFalse('p' in self.ruleset) self.assertEqual([], self.ruleset.keys()) def test_discard_equivalent_rule(self): fact = Fact('p', (1, 2, 3)) equivalent_rule = compile.Rule(compile.parse1('p(1,2,3)'), ()) self.assertTrue(self.ruleset.add_rule('p', fact)) self.assertTrue('p' in self.ruleset) self.assertEqual([equivalent_rule], self.ruleset.get_rules('p')) self.assertTrue(self.ruleset.discard_rule('p', equivalent_rule)) self.assertFalse('p' in self.ruleset) self.assertEqual([], self.ruleset.keys()) def test_contains(self): fact = Fact('p', (1, 2, 3)) rule = compile.parse1('p(x) :- q(x)') self.ruleset.add_rule('p', fact) self.ruleset.add_rule('p', rule) # positive tests equivalent_fact1 = Fact('p', (1, 2, 3)) equivalent_fact2 = compile.parse1('p(1,2,3)') equivalent_fact3 = compile.Rule(compile.parse1('p(1,2,3)'), ()) equivalent_rule = compile.parse1('p(x) :- q(x)') self.assertTrue(self.ruleset.contains('p', equivalent_fact1)) self.assertTrue(self.ruleset.contains('p', equivalent_fact2)) self.assertTrue(self.ruleset.contains('p', equivalent_fact3)) self.assertTrue(self.ruleset.contains('p', equivalent_rule)) # negative tests nonequiv_fact = compile.parse1('p(4, 5, 6)') nonequiv_rule = compile.parse1('p(x) :- r(x)') self.assertFalse(self.ruleset.contains('p', nonequiv_fact)) self.assertFalse(self.ruleset.contains('p', nonequiv_rule))