From ef43aea498ffa4a512efd56f22decffb7ef8c060 Mon Sep 17 00:00:00 2001 From: Alexander Yip Date: Thu, 22 Jan 2015 16:43:59 -0800 Subject: [PATCH] Add and use Fact and FactSet Fact is a class used to store a single Fact. It replaces Rule in cases where the Fact has no negation and all the variables are bound. The Fact uses much less memory than a Rule for these situations since the Fact is really just a native tuple. FactSet is a container for Facts. RuleSet will now use a FactSet inside to store what we used to call literals. The RuleSet now converts Facts to Rules before returning the rules for get_rules(), so this change makes MaterializedView use OrderedSet directly since MaterializedView uses DeltaRules instead of Rules. Implements blueprint: fact-datastructure Change-Id: I5ac47075288f50fe027b94abc76724cb18370f3c --- congress/policy/base.py | 10 + congress/policy/compile.py | 17 ++ congress/policy/dsepolicy.py | 21 +-- congress/policy/factset.py | 148 +++++++++++++++ congress/policy/materialized.py | 14 +- congress/policy/nonrecursive.py | 21 +++ congress/policy/ruleset.py | 171 ++++++++++-------- congress/policy/runtime.py | 38 +--- congress/tests/policy/test_factset.py | 118 ++++++++++++ congress/tests/policy/test_ruleset.py | 44 +++++ congress/tests/policy/test_runtime.py | 4 +- .../tests/policy/test_runtime_performance.py | 9 +- 12 files changed, 482 insertions(+), 133 deletions(-) create mode 100644 congress/policy/factset.py create mode 100644 congress/tests/policy/test_factset.py diff --git a/congress/policy/base.py b/congress/policy/base.py index 28b00f254..e47d96c30 100644 --- a/congress/policy/base.py +++ b/congress/policy/base.py @@ -142,6 +142,16 @@ class Theory(object): else: self.trace_prefix = self.abbr + " " * (maxlength - len(self.abbr)) + def initialize_tables(self, tablenames, facts): + """initialize_tables + + Event handler for (re)initializing a collection of tables. Clears + tables befores assigning the new table content. + + @facts must be an iterable containing compile.Fact objects. + """ + raise NotImplementedError + def actual_events(self, events): """Returns subset of EVENTS that are not noops.""" actual = [] diff --git a/congress/policy/compile.py b/congress/policy/compile.py index 75b7f1b07..d2c5adc87 100644 --- a/congress/policy/compile.py +++ b/congress/policy/compile.py @@ -182,6 +182,23 @@ class ObjectConstant (Term): return True +class Fact (tuple): + """Represent a Fact (a ground literal) + + Use this class to represent a fact such as Foo(1,2,3). While one could + use a Rule to represent the same fact, this Fact datastructure is more + memory efficient than a Rule object since this Fact stores the information + as a native tuple, containing native values like ints and strings. Notes + that this subclasses from tuple. + """ + def __new__(cls, table, values): + return super(Fact, cls).__new__(cls, values) + + def __init__(self, table, values): + super(Fact, self).__init__(table, values) + self.table = table + + class Literal (object): """Represents a possibly negated atomic statement, e.g. p(a, 17, b).""" diff --git a/congress/policy/dsepolicy.py b/congress/policy/dsepolicy.py index c80c4b11d..ac8dafb13 100644 --- a/congress/policy/dsepolicy.py +++ b/congress/policy/dsepolicy.py @@ -76,23 +76,14 @@ class DseRuntime (runtime.Runtime, deepsix.deepSix): """Handler for when dataservice publishes full table.""" self.log("received full data msg for %s: %s", msg.header['dataindex'], runtime.iterstr(msg.body.data)) - literals = [] tablename = msg.header['dataindex'] service = msg.replyTo - for row in msg.body.data: - if not isinstance(row, tuple): - raise ValueError("Tuple expected, received: %s" % row) - # prefix tablename with data source - literals.append(compile.Literal.create_from_table_tuple( - tablename, row)) - (permitted, changes) = self.initialize_tables( - [tablename], literals, target=service) - if not permitted: - raise runtime.CongressRuntime( - "Update not permitted." + '\n'.join(str(x) for x in changes)) - else: - self.log("full data msg for %s caused %d changes: %s", - tablename, len(changes), runtime.iterstr(changes)) + + # Use a generator to avoid instantiating all these Facts at once. + literals = (compile.Fact(tablename, row) for row in msg.body.data) + + self.initialize_tables([tablename], literals, target=service) + self.log("full data msg for %s", tablename) def receive_data_update(self, msg): """Handler for when dataservice publishes a delta.""" diff --git a/congress/policy/factset.py b/congress/policy/factset.py new file mode 100644 index 000000000..4e1d7bb80 --- /dev/null +++ b/congress/policy/factset.py @@ -0,0 +1,148 @@ +# Copyright (c) 2015 VMware, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +from congress.openstack.common import log as logging +from congress.policy import utility + +LOG = logging.getLogger(__name__) + + +class FactSet(object): + """FactSet + + Maintains a set of facts, and provides indexing for efficient iteration, + given a partial or full match. Expects that all facts are the same width. + """ + + def __init__(self): + self._facts = utility.OrderedSet() + + # key is a sorted tuple of column indices, values are dict mapping a + # specific value for the key to a set of Facts. + self._indicies = {} + + def __contains__(self, fact): + return fact in self._facts + + def __len__(self): + return len(self._facts) + + def __iter__(self): + return self._facts.__iter__() + + def add(self, fact): + """Add a fact to the FactSet + + Returns True if the fact is absent from this FactSet and adds the + fact, otherwise returns False. + """ + assert isinstance(fact, tuple) + changed = self._facts.add(fact) + if changed: + # Add the fact to the indicies + for index in self._indicies.keys(): + self._add_fact_to_index(fact, index) + return changed + + def remove(self, fact): + """Remove a fact from the FactSet + + Returns True if the fact is in this FactSet and removes the fact, + otherwise returns False. + """ + changed = self._facts.discard(fact) + if changed: + # Remove from indices + for index in self._indicies.keys(): + self._remove_fact_from_index(fact, index) + return changed + + def create_index(self, columns): + """Create an index + + @columns is a tuple of column indicies that index into the facts in + self. @columns must be sorted in ascending order, and each column + index must be less than the width of a fact in self. If the index + exists, do nothing. + """ + assert sorted(columns) == list(columns) + assert len(columns) + + if columns in self._indicies: + return + + for f in self._facts: + self._add_fact_to_index(f, columns) + + def remove_index(self, columns): + """Remove an index + + @columns is a tuple of column indicies that index into the facts in + self. @columns must be sorted in ascending order, and each column + index must be less than the width of a fact in self. If the index + does not exists, raise KeyError. + """ + assert sorted(columns) == list(columns) + if columns in self._indicies: + del self._indicies[columns] + + def has_index(self, columns): + """Returns True if the index exists.""" + return columns in self._indicies + + def find(self, partial_fact): + """Find Facts given a partial fact + + @partial_fact is a tuple of pair tuples. The first item in each + pair tuple is an index into a fact, and the second item is a value to + match again self._facts. Expects the pairs to be sorted by index in + ascending order. + """ + index = tuple([i for i, v in partial_fact]) + k = tuple([v for i, v in partial_fact]) + if index in self._indicies and k in self._indicies[index]: + return self._indicies[index][k] + + # There is no index, so iterate. + matches = set() + for f in self._facts: + match = True + for i, v in partial_fact: + if f[i] != v: + match = False + break + if match: + matches.add(f) + return matches + + def _compute_key(self, columns, fact): + # assumes that @columns is sorted in ascending order. + return tuple([fact[i] for i in columns]) + + def _add_fact_to_index(self, fact, index): + if index not in self._indicies: + self._indicies[index] = {} + + k = self._compute_key(index, fact) + if k not in self._indicies[index]: + self._indicies[index][k] = set((fact,)) + else: + self._indicies[index][k].add(fact) + + def _remove_fact_from_index(self, fact, index): + k = self._compute_key(index, fact) + self._indicies[index][k].remove(fact) + if not len(self._indicies[index][k]): + del self._indicies[index][k] diff --git a/congress/policy/materialized.py b/congress/policy/materialized.py index ba6031873..c919f6cea 100644 --- a/congress/policy/materialized.py +++ b/congress/policy/materialized.py @@ -23,9 +23,9 @@ from congress.policy.builtin.congressbuiltin import builtin_registry from congress.policy import compile from congress.policy.compile import Event from congress.policy.database import Database -from congress.policy.ruleset import RuleSet from congress.policy.topdown import TopDownTheory from congress.policy.utility import iterstr +from congress.policy.utility import OrderedSet LOG = logging.getLogger(__name__) @@ -75,7 +75,7 @@ class DeltaRuleTheory (Theory): name=name, abbr=abbr, theories=theories) # dictionary from table name to list of rules with that table as # trigger - self.rules = RuleSet() + self.rules = {} # dictionary from delta_rule to the rule from which it was derived self.originals = set() # dictionary from table name to number of rules with that table in @@ -137,7 +137,9 @@ class DeltaRuleTheory (Theory): # contents # TODO(thinrichs): eliminate dups, maybe including # case where bodies are reorderings of each other - self.rules.add_rule(delta.trigger.table, delta) + if delta.trigger.table not in self.rules: + self.rules[delta.trigger.table] = OrderedSet() + self.rules[delta.trigger.table].add(delta) def delete(self, rule): """Delete a compile.Rule from theory. @@ -169,7 +171,9 @@ class DeltaRuleTheory (Theory): del self.all_tables[table] # contents - self.rules.discard_rule(delta.trigger.table, delta) + self.rules[delta.trigger.table].discard(delta) + if not len(self.rules[delta.trigger.table]): + del self.rules[delta.trigger.table] def policy(self): return self.originals @@ -189,7 +193,7 @@ class DeltaRuleTheory (Theory): def rules_with_trigger(self, table): """Return the list of DeltaRules that trigger on the given TABLE.""" if table in self.rules: - return self.rules.get_rules(table) + return self.rules[table] else: return [] diff --git a/congress/policy/nonrecursive.py b/congress/policy/nonrecursive.py index ad21ec206..67ffa3753 100644 --- a/congress/policy/nonrecursive.py +++ b/congress/policy/nonrecursive.py @@ -40,6 +40,27 @@ class NonrecursiveRuleTheory(TopDownTheory): # 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] diff --git a/congress/policy/ruleset.py b/congress/policy/ruleset.py index 5d5662eb7..0c0abe414 100644 --- a/congress/policy/ruleset.py +++ b/congress/policy/ruleset.py @@ -14,6 +14,10 @@ # from congress.openstack.common import log as logging +from congress.policy.compile import Fact +from congress.policy.compile import Literal +from congress.policy.compile import Rule +from congress.policy.factset import FactSet from congress.policy import utility LOG = logging.getLogger(__name__) @@ -22,8 +26,7 @@ LOG = logging.getLogger(__name__) class RuleSet(object): """RuleSet - Keeps track of all rules for all tables. Also manages indicies that allow - a caller to get all rules that match a certain literal pattern. + Keeps track of all rules for all tables. """ # Internally: @@ -34,106 +37,120 @@ class RuleSet(object): def __init__(self): self.rules = {} - self.literals = {} - self.indicies = {} + self.facts = {} def __str__(self): - return str(self.rules) + " " + str(self.literals) + return str(self.rules) + " " + str(self.facts) def add_rule(self, key, rule): - # rule can be a rule or a literal - # returns True on change + """Add a rule to the Ruleset - if len(rule.body): - dest = self.rules - else: - dest = self.literals - # Update indicies - for index_name in self.indicies.keys(): - if key == index_name[0]: - self._add_literal_to_index(rule, index_name) + @rule can be a Rule or a Fact. Returns True if add_rule() changes the + RuleSet. + """ + if isinstance(rule, Fact): + # If the rule is a Fact, then add it to self.facts. + if key not in self.facts: + self.facts[key] = FactSet() + return self.facts[key].add(rule) + + elif len(rule.body) == 0 and not rule.head.is_negated(): + # If the rule is a Rule, with no body, then it's a Fact, so + # convert the Rule to a Fact to a Fact and add to self.facts. + f = Fact(key, (a.name for a in rule.head.arguments)) + if key not in self.facts: + self.facts[key] = FactSet() + return self.facts[key].add(f) - if key in dest: - return dest[key].add(rule) else: - dest[key] = utility.OrderedSet([rule]) - return True + # else the rule is a regular rule, so add it to self.rules. + if key in self.rules: + return self.rules[key].add(rule) + else: + self.rules[key] = utility.OrderedSet([rule]) + return True def discard_rule(self, key, rule): - # rule can be a rule or a literal - # returns True on change + """Remove a rule from the Ruleset + + @rule can be a Rule or a Fact. Returns True if discard_rule() changes + the RuleSet. + """ + if isinstance(rule, Fact): + # rule is a Fact, so remove from self.facts + if key in self.facts: + changed = self.facts[key].remove(rule) + if len(self.facts[key]) == 0: + del self.facts[key] + return changed + return False + + elif not len(rule.body): + # rule is a Rule, but without a body so it will be in self.facts. + if key in self.facts: + fact = Fact(key, [a.name for a in rule.head.arguments]) + changed = self.facts[key].remove(fact) + if len(self.facts[key]) == 0: + del self.facts[key] + return changed + return False - if len(rule.body): - dest = self.rules else: - dest = self.literals - # Update indicies - for index_name in self.indicies.keys(): - if key == index_name[0]: - self._remove_literal_from_index(rule, index_name) - - if key in dest: - changed = dest[key].discard(rule) - if len(dest[key]) == 0: - del dest[key] - return changed - return False + # rule is a Rule with a body, so remove from self.rules. + if key in self.rules: + changed = self.rules[key].discard(rule) + if len(self.rules[key]) == 0: + del self.rules[key] + return changed + return False def keys(self): - return self.literals.keys() + self.rules.keys() + return self.facts.keys() + self.rules.keys() def __contains__(self, key): - return key in self.literals or key in self.rules + return key in self.facts or key in self.rules def get_rules(self, key, match_literal=None): - literals = [] + facts = [] - if match_literal and not match_literal.is_negated(): + if (match_literal and not match_literal.is_negated() and + key in self.facts): + # If the caller supplies a literal to match against, then use an + # index to find the matching rules. bound_arguments = tuple([i for i, arg in enumerate(match_literal.arguments) if not arg.is_variable()]) - index_name = (key,) + bound_arguments + if (bound_arguments and + not self.facts[key].has_index(bound_arguments)): + # The index does not exist, so create it. + self.facts[key].create_index(bound_arguments) - index_key = tuple([(i, arg.name) for i, arg - in enumerate(match_literal.arguments) - if not arg.is_variable()]) - index_key = (key,) + index_key - - if index_name not in self.indicies: - self._create_index(index_name) - - literals = list(self.indicies[index_name].get(index_key, ())) + partial_fact = tuple( + [(i, arg.name) + for i, arg in enumerate(match_literal.arguments) + if not arg.is_variable()]) + facts = list(self.facts[key].find(partial_fact)) else: - literals = list(self.literals.get(key, ())) + # There is no usable match_literal, so get all facts for the + # table. + facts = list(self.facts.get(key, ())) - return literals + list(self.rules.get(key, ())) + # Convert native tuples to Rule objects. + + # TODO(alex): This is inefficient because it creates Literal and Rule + # objects. It would be more efficient to change the TopDownTheory and + # unifier to handle Facts natively. + fact_rules = [] + for fact in facts: + literal = Literal.create_from_table_tuple(key, fact) + fact_rules.append(Rule(literal, ())) + + return fact_rules + list(self.rules.get(key, ())) def clear(self): self.rules = {} - self.literals = {} + self.facts = {} - def _create_index(self, index_name): - # Make an index over literals. An index is an OrderedSet of rules. - self.indicies[index_name] = {} # utility.OrderedSet() - if index_name[0] in self.literals: - for literal in self.literals[index_name[0]]: - self._add_literal_to_index(literal, index_name) - - def _add_literal_to_index(self, literal, index_name): - index_key = ((index_name[0],) + - tuple([(i, literal.head.arguments[i].name) - for i in index_name[1:]])) - - # Populate the index - if index_key not in self.indicies[index_name]: - self.indicies[index_name][index_key] = utility.OrderedSet() - self.indicies[index_name][index_key].add(literal) - - def _remove_literal_from_index(self, literal, index_name): - index_key = ((index_name[0],) + - tuple([(i, literal.head.arguments[i].name) - for i in index_name[1:]])) - - self.indicies[index_name][index_key].discard(literal) - if len(self.indicies[index_name][index_key]) == 0: - del self.indicies[index_name][index_key] + def clear_table(self, table): + self.rules[table] = utility.OrderedSet() + self.facts[table] = FactSet() diff --git a/congress/policy/runtime.py b/congress/policy/runtime.py index 31c63b095..d07a73e8f 100644 --- a/congress/policy/runtime.py +++ b/congress/policy/runtime.py @@ -298,39 +298,13 @@ class Runtime (object): else: return self.select_obj(query, self.get_target(target), trace) - def initialize_tables(self, tablenames, formulas, target=None): - """Event handler for (re)initializing a collection of tables.""" - # translate FORMULAS into list of formula objects - actual_formulas = [] - formula_tables = set() + def initialize_tables(self, tablenames, facts, target=None): + """Event handler for (re)initializing a collection of tables - if isinstance(formulas, basestring): - formulas = self.parse(formulas) - - for formula in formulas: - if isinstance(formula, basestring): - formula = self.parse1(formula) - elif isinstance(formula, tuple): - formula = compile.Literal.create_from_iter(formula) - assert formula.is_atom() - actual_formulas.append(formula) - formula_tables.add(formula.table) - - tablenames = set(tablenames) | formula_tables - self.table_log(None, "Initializing tables %s with %s", - iterstr(tablenames), iterstr(actual_formulas)) - # implement initialization by computing the requisite - # update. - theory = self.get_target(target) - old = set(theory.content(tablenames=tablenames)) - new = set(actual_formulas) - to_add = new - old - to_rem = old - new - to_add = [Event(formula_, insert=True) for formula_ in to_add] - to_rem = [Event(formula_, insert=False) for formula_ in to_rem] - self.table_log(None, "Initialize converted to update with %s and %s", - iterstr(to_add), iterstr(to_rem)) - return self.update(to_add + to_rem, target=target) + @facts must be an iterable containing compile.Fact objects. + """ + target_theory = self.get_target(target) + target_theory.initialize_tables(tablenames, facts) def insert(self, formula, target=None): """Event handler for arbitrary insertion (rules and facts).""" diff --git a/congress/tests/policy/test_factset.py b/congress/tests/policy/test_factset.py new file mode 100644 index 000000000..b80ac603c --- /dev/null +++ b/congress/tests/policy/test_factset.py @@ -0,0 +1,118 @@ +# Copyright (c) 2015 VMware, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from congress.policy.factset import FactSet +from congress.tests import base + + +class TestFactSet(base.TestCase): + def setUp(self): + super(TestFactSet, self).setUp() + self.factset = FactSet() + + def test_empty(self): + self.assertFalse((1, 2, 3) in self.factset) + self.assertEqual(0, len(self.factset)) + + def test_add_one(self): + f = (1, 2, 'a') + self.factset.add(f) + self.assertEqual(1, len(self.factset)) + self.assertEqual(set([f]), self.factset.find(((0, 1), (1, 2), + (2, 'a')))) + + def test_add_few(self): + f1 = (1, 200, 'a') + f2 = (2, 200, 'a') + f3 = (3, 200, 'c') + self.factset.add(f1) + self.factset.add(f2) + self.factset.add(f3) + + self.assertEqual(3, len(self.factset)) + self.assertEqual(set([f1, f2, f3]), self.factset.find(((1, 200),))) + self.assertEqual(set([f1, f2]), self.factset.find(((2, 'a'),))) + self.assertEqual(set([f1]), self.factset.find(((0, 1), (1, 200), + (2, 'a'),))) + self.assertEqual(set(), self.factset.find(((0, 8),))) + + def test_remove(self): + f1 = (1, 200, 'a') + f2 = (2, 200, 'a') + f3 = (3, 200, 'c') + self.factset.add(f1) + self.factset.add(f2) + self.factset.add(f3) + self.assertEqual(3, len(self.factset)) + + self.assertTrue(self.factset.remove(f1)) + self.assertEqual(2, len(self.factset)) + self.assertEqual(set([f2, f3]), self.factset.find(((1, 200),))) + + self.assertTrue(self.factset.remove(f3)) + self.assertEqual(1, len(self.factset)) + self.assertEqual(set([f2]), self.factset.find(((1, 200),))) + + self.assertFalse(self.factset.remove(f3)) + + self.assertTrue(self.factset.remove(f2)) + self.assertEqual(0, len(self.factset)) + self.assertEqual(set(), self.factset.find(((1, 200),))) + + def test_create_index(self): + f1 = (1, 200, 'a') + f2 = (2, 200, 'a') + f3 = (3, 200, 'c') + self.factset.add(f1) + self.factset.add(f2) + self.factset.add(f3) + + self.factset.create_index((1,)) + self.assertEqual(set([f1, f2, f3]), self.factset.find(((1, 200),))) + self.assertEqual(set([f1, f2]), self.factset.find(((2, 'a'),))) + self.assertEqual(set([f1, f2]), self.factset.find(((1, 200), + (2, 'a')))) + self.assertEqual(set([f1]), self.factset.find(((0, 1), (1, 200), + (2, 'a'),))) + self.assertEqual(set(), self.factset.find(((0, 8),))) + + self.factset.create_index((1, 2)) + self.assertEqual(set([f1, f2, f3]), self.factset.find(((1, 200),))) + self.assertEqual(set([f1, f2]), self.factset.find(((2, 'a'),))) + self.assertEqual(set([f1, f2]), self.factset.find(((1, 200), + (2, 'a')))) + self.assertEqual(set([f1]), self.factset.find(((0, 1), (1, 200), + (2, 'a'),))) + self.assertEqual(set(), self.factset.find(((0, 8),))) + + def test_remove_index(self): + f1 = (1, 200, 'a') + f2 = (2, 200, 'a') + f3 = (3, 200, 'c') + self.factset.add(f1) + self.factset.add(f2) + self.factset.add(f3) + + self.factset.create_index((1,)) + self.factset.create_index((1, 2)) + self.factset.remove_index((1,)) + self.factset.remove_index((1, 2)) + + self.assertEqual(set([f1, f2, f3]), self.factset.find(((1, 200),))) + self.assertEqual(set([f1, f2]), self.factset.find(((2, 'a'),))) + self.assertEqual(set([f1, f2]), self.factset.find(((1, 200), + (2, 'a')))) + self.assertEqual(set([f1]), self.factset.find(((0, 1), (1, 200), + (2, 'a'),))) + self.assertEqual(set(), self.factset.find(((0, 8),))) diff --git a/congress/tests/policy/test_ruleset.py b/congress/tests/policy/test_ruleset.py index a2152a5e1..b8f974bfd 100644 --- a/congress/tests/policy/test_ruleset.py +++ b/congress/tests/policy/test_ruleset.py @@ -13,6 +13,7 @@ # under the License. from congress.policy import compile +from congress.policy.compile import Fact from congress.policy.ruleset import RuleSet from congress.tests import base @@ -83,6 +84,25 @@ class TestRuleSet(base.TestCase): 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)) @@ -128,3 +148,27 @@ class TestRuleSet(base.TestCase): 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()) diff --git a/congress/tests/policy/test_runtime.py b/congress/tests/policy/test_runtime.py index c4bac2d01..a450a657a 100644 --- a/congress/tests/policy/test_runtime.py +++ b/congress/tests/policy/test_runtime.py @@ -20,6 +20,7 @@ from congress.policy.base import ACTION_POLICY_TYPE from congress.policy.base import DATABASE_POLICY_TYPE from congress.policy.base import MATERIALIZED_POLICY_TYPE from congress.policy.base import NONRECURSIVE_POLICY_TYPE +from congress.policy.compile import Fact from congress.policy import runtime from congress.tests import base from congress.tests import helper @@ -89,7 +90,8 @@ class TestRuntime(base.TestCase): run = runtime.Runtime() run.create_policy('test') run.insert('p(1) p(2)') - run.initialize_tables(['p'], ['p(3)', 'p(4)']) + facts = [Fact('p', (3,)), Fact('p', (4,))] + run.initialize_tables(['p'], facts) e = helper.datalog_equal(run.select('p(x)'), 'p(3) p(4)') self.assertTrue(e) diff --git a/congress/tests/policy/test_runtime_performance.py b/congress/tests/policy/test_runtime_performance.py index 784cd4e45..2e4718e91 100644 --- a/congress/tests/policy/test_runtime_performance.py +++ b/congress/tests/policy/test_runtime_performance.py @@ -14,6 +14,7 @@ # from congress.openstack.common import log as logging from congress.policy import base +from congress.policy.compile import Fact from congress.policy.compile import Literal from congress.policy import runtime from congress.tests import base as testbase @@ -113,8 +114,10 @@ class TestRuntimePerformance(testbase.TestCase): pass def test_runtime_initialize_tables(self): - MAX = 1000 - formulas = [('p', 1, 2, 'foo', 'bar', i) for i in range(MAX)] + MAX = 700 + longstring = 'a' * 100 + facts = (Fact('p', (1, 2, 'foo', 'bar', i, longstring)) + for i in range(MAX)) th = NREC_THEORY - self._runtime.initialize_tables(['p'], formulas, th) + self._runtime.initialize_tables(['p'], facts, th)