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
This commit is contained in:
@@ -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 = []
|
||||
|
||||
@@ -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)."""
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
148
congress/policy/factset.py
Normal file
148
congress/policy/factset.py
Normal file
@@ -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]
|
||||
@@ -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 []
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)."""
|
||||
|
||||
118
congress/tests/policy/test_factset.py
Normal file
118
congress/tests/policy/test_factset.py
Normal file
@@ -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),)))
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user