1870 lines
75 KiB
Python
1870 lines
75 KiB
Python
# Copyright (c) 2014 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 __future__ import print_function
|
|
from __future__ import division
|
|
from __future__ import absolute_import
|
|
|
|
import mock
|
|
from oslo_log import log as logging
|
|
|
|
from congress.datalog import base as datalog_base
|
|
from congress.datalog import compile
|
|
from congress.datalog import database
|
|
from congress.datalog import materialized
|
|
from congress.datalog import nonrecursive
|
|
from congress.datalog import utility
|
|
from congress.db import db_policy_rules
|
|
from congress import exception
|
|
from congress.policy_engines import agnostic
|
|
from congress.tests import base
|
|
from congress.tests import helper
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
NREC_THEORY = 'non-recursive theory'
|
|
|
|
|
|
class TestRuntime(base.TestCase):
|
|
"""Tests for Runtime that are not specific to any theory."""
|
|
|
|
def check_equal(self, actual_string, correct_string, msg):
|
|
self.assertTrue(helper.datalog_equal(
|
|
actual_string, correct_string, msg))
|
|
|
|
def test_theory_inclusion(self):
|
|
"""Test evaluation routines when one theory includes another."""
|
|
# spread out across inclusions
|
|
th1 = nonrecursive.NonrecursiveRuleTheory()
|
|
th2 = nonrecursive.NonrecursiveRuleTheory()
|
|
th3 = nonrecursive.NonrecursiveRuleTheory()
|
|
th1.includes.append(th2)
|
|
th2.includes.append(th3)
|
|
|
|
th1.insert(helper.str2form('p(x) :- q(x), r(x), s(2)'))
|
|
th2.insert(helper.str2form('q(1)'))
|
|
th1.insert(helper.str2form('r(1)'))
|
|
th3.insert(helper.str2form('s(2)'))
|
|
|
|
self.check_equal(
|
|
helper.pol2str(th1.select(helper.str2form('p(x)'))),
|
|
'p(1)', 'Data spread across inclusions')
|
|
|
|
def test_multi_policy_update(self):
|
|
"""Test updates that apply to multiple policies."""
|
|
def check_equal(actual, correct):
|
|
e = helper.datalog_equal(actual, correct)
|
|
self.assertTrue(e)
|
|
|
|
run = agnostic.Runtime()
|
|
run.create_policy('th1')
|
|
run.create_policy('th2')
|
|
|
|
events1 = [compile.Event(formula=x, insert=True, target='th1')
|
|
for x in helper.str2pol("p(1) p(2) q(1) q(3)")]
|
|
events2 = [compile.Event(formula=x, insert=True, target='th2')
|
|
for x in helper.str2pol("r(1) r(2) t(1) t(4)")]
|
|
run.update(events1 + events2)
|
|
|
|
check_equal(run.select('p(x)', 'th1'), 'p(1) p(2)')
|
|
check_equal(run.select('q(x)', 'th1'), 'q(1) q(3)')
|
|
check_equal(run.select('r(x)', 'th2'), 'r(1) r(2)')
|
|
check_equal(run.select('t(x)', 'th2'), 't(1) t(4)')
|
|
|
|
def test_initialize_tables(self):
|
|
"""Test initialize_tables() functionality of agnostic."""
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.insert('p(1) p(2)')
|
|
facts = [compile.Fact('p', (3,)), compile.Fact('p', (4,))]
|
|
run.initialize_tables(['p'], facts)
|
|
e = helper.datalog_equal(run.select('p(x)'), 'p(3) p(4)')
|
|
self.assertTrue(e)
|
|
|
|
def test_single_policy(self):
|
|
"""Test ability to create/delete single policies."""
|
|
# single policy
|
|
run = agnostic.Runtime()
|
|
original = run.policy_names()
|
|
run.create_policy('test1')
|
|
run.insert('p(x) :- q(x)', 'test1')
|
|
run.insert('q(1)', 'test1')
|
|
self.assertEqual(
|
|
run.select('p(x)', 'test1'), 'p(1)', 'Policy creation')
|
|
self.assertEqual(
|
|
run.select('p(x)', 'test1'), 'p(1)', 'Policy creation')
|
|
run.delete_policy('test1')
|
|
self.assertEqual(
|
|
set(run.policy_names()), set(original), 'Policy deletion')
|
|
|
|
def test_multi_policy(self):
|
|
"""Test ability to create/delete multiple policies."""
|
|
# multiple policies
|
|
run = agnostic.Runtime()
|
|
original = run.policy_names()
|
|
run.create_policy('test2')
|
|
run.create_policy('test3')
|
|
self.assertEqual(
|
|
set(run.policy_names()),
|
|
set(original + ['test2', 'test3']),
|
|
'Multi policy creation')
|
|
run.delete_policy('test2')
|
|
run.create_policy('test4')
|
|
self.assertEqual(
|
|
set(run.policy_names()),
|
|
set(original + ['test3', 'test4']),
|
|
'Multiple policy deletion')
|
|
run.insert('p(x) :- q(x) q(1)', 'test4')
|
|
self.assertEqual(
|
|
run.select('p(x)', 'test4'),
|
|
'p(1)',
|
|
'Multipolicy deletion select')
|
|
|
|
def test_policy_types(self):
|
|
"""Test types for multiple policies."""
|
|
# policy types
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test1', kind=datalog_base.NONRECURSIVE_POLICY_TYPE)
|
|
self.assertIsInstance(run.policy_object('test1'),
|
|
nonrecursive.NonrecursiveRuleTheory,
|
|
'Nonrecursive policy addition')
|
|
run.create_policy('test2', kind=datalog_base.ACTION_POLICY_TYPE)
|
|
self.assertIsInstance(run.policy_object('test2'),
|
|
nonrecursive.ActionTheory,
|
|
'Action policy addition')
|
|
run.create_policy('test3', kind=datalog_base.DATABASE_POLICY_TYPE)
|
|
self.assertIsInstance(run.policy_object('test3'),
|
|
database.Database,
|
|
'Database policy addition')
|
|
run.create_policy('test4', kind=datalog_base.MATERIALIZED_POLICY_TYPE)
|
|
self.assertIsInstance(run.policy_object('test4'),
|
|
materialized.MaterializedViewTheory,
|
|
'Materialized policy addition')
|
|
|
|
def test_policy_errors(self):
|
|
"""Test errors for multiple policies."""
|
|
# errors
|
|
run = agnostic.Runtime()
|
|
run.create_policy('existent')
|
|
self.assertRaises(KeyError, run.create_policy, 'existent')
|
|
self.assertRaises(KeyError, run.delete_policy, 'nonexistent')
|
|
self.assertRaises(KeyError, run.policy_object, 'nonexistent')
|
|
|
|
def test_wrong_arity_index(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test1')
|
|
run.insert('p(x) :- r(x), q(y, x)')
|
|
run.insert('r(1)')
|
|
run.insert('q(1,1)')
|
|
# run query first to build index
|
|
self.assertTrue(helper.datalog_equal(run.select('p(x)'), 'p(1)'))
|
|
# next insert causes an exceptions since the thing we indexed on
|
|
# doesn't exist
|
|
permitted, errs = run.insert('q(5)')
|
|
self.assertFalse(permitted)
|
|
self.assertEqual(len(errs), 1)
|
|
self.assertIsInstance(errs[0], exception.PolicyException)
|
|
# double-check that the error didn't result in an inconsistent state
|
|
self.assertEqual(run.select('q(5)'), '')
|
|
|
|
def test_get_tablename(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.insert('p(x) :- q(x)')
|
|
run.insert('q(x) :- r(x)')
|
|
run.insert('execute[nova:disconnect(x, y)] :- s(x, y)')
|
|
tables = run.get_tablename('test', 'p')
|
|
self.assertEqual({'p'}, set(tables))
|
|
|
|
tables = run.get_tablename('test', 't')
|
|
self.assertIsNone(tables)
|
|
|
|
tables = run.get_tablenames('test')
|
|
self.assertEqual({'p', 'q', 'r', 's'}, set(tables))
|
|
|
|
tables = run.get_tablename('test', 'nova:disconnect')
|
|
self.assertIsNone(tables)
|
|
|
|
def test_tablenames(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.insert('p(x) :- q(x,y)')
|
|
run.insert('q(x,y) :- r(x,y)')
|
|
run.insert('t(x) :- q(x,y), r(x,z), equal(y, z)')
|
|
run.insert('execute[nova:disconnect(x, y)] :- s(x, y)')
|
|
tables = run.tablenames()
|
|
self.assertEqual({'p', 'q', 'r', 's', 't', 'nova:disconnect'},
|
|
set(tables))
|
|
tables = run.tablenames(include_builtin=True)
|
|
self.assertEqual({'p', 'q', 'r', 's', 't', 'nova:disconnect', 'equal'},
|
|
set(tables))
|
|
tables = run.tablenames(body_only=True)
|
|
self.assertEqual({'q', 'r', 's'}, set(tables))
|
|
tables = run.tablenames(include_modal=False)
|
|
self.assertEqual({'p', 'q', 'r', 's', 't'}, set(tables))
|
|
|
|
@mock.patch.object(db_policy_rules, 'add_policy', side_effect=Exception())
|
|
def test_persistent_create_policy_with_db_exception(self, mock_add):
|
|
run = agnostic.Runtime()
|
|
with mock.patch.object(run, 'delete_policy') as mock_delete:
|
|
policy_name = 'test_policy'
|
|
self.assertRaises(exception.PolicyException,
|
|
run.persistent_create_policy,
|
|
policy_name)
|
|
mock_add.assert_called_once_with(mock.ANY,
|
|
policy_name,
|
|
policy_name[:5],
|
|
mock.ANY,
|
|
'user',
|
|
'nonrecursive')
|
|
mock_delete.assert_called_once_with(policy_name)
|
|
|
|
def test_tablenames_theory_name(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.create_policy('test2')
|
|
run.insert('p(x) :- q(x)', 'test')
|
|
run.insert('r(x) :- s(x)', 'test2')
|
|
|
|
tables = run.tablenames()
|
|
self.assertEqual(set(tables), set(['p', 'q', 'r', 's']))
|
|
|
|
tables = run.tablenames(theory_name='test')
|
|
self.assertEqual(set(tables), set(['p', 'q']))
|
|
|
|
|
|
class TestArity(base.TestCase):
|
|
def test_same_table_diff_policies(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('alice')
|
|
run.create_policy('bob')
|
|
run.insert('p(x) :- q(x, y)', 'alice')
|
|
run.insert('p(x, y) :- r(x, y, z)', 'bob')
|
|
self.assertEqual(1, run.arity('p', 'alice'))
|
|
self.assertEqual(2, run.arity('p', 'bob'))
|
|
|
|
def test_complex_table(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('alice')
|
|
run.create_policy('bob')
|
|
run.insert('p(x) :- q(x, y)', 'alice')
|
|
run.insert('p(x, y) :- r(x, y, z)', 'bob')
|
|
self.assertEqual(1, run.arity('alice:p', 'bob'))
|
|
self.assertEqual(1, run.arity('alice:p', 'alice'))
|
|
|
|
def test_modals(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('alice')
|
|
run.insert('execute[nova:p(x)] :- q(x, y)', 'alice')
|
|
self.assertEqual(1, run.arity('nova:p', 'alice', 'execute'))
|
|
|
|
|
|
class TestTriggerRegistry(base.TestCase):
|
|
def setUp(self):
|
|
super(TestTriggerRegistry, self).setUp()
|
|
self.f = lambda tbl, old, new: old
|
|
|
|
def test_trigger(self):
|
|
trigger1 = agnostic.Trigger('table', 'policy', self.f)
|
|
trigger2 = agnostic.Trigger('table', 'policy', self.f)
|
|
trigger3 = agnostic.Trigger('table2', 'policy', self.f)
|
|
trigger4 = agnostic.Trigger('table', 'policy', lambda x: x)
|
|
|
|
s = set()
|
|
s.add(trigger1)
|
|
s.add(trigger2)
|
|
s.add(trigger3)
|
|
s.add(trigger4)
|
|
self.assertEqual(4, len(s))
|
|
s.discard(trigger1)
|
|
self.assertEqual(3, len(s))
|
|
s.discard(trigger2)
|
|
self.assertEqual(2, len(s))
|
|
s.discard(trigger3)
|
|
self.assertEqual(1, len(s))
|
|
s.discard(trigger4)
|
|
self.assertEqual(0, len(s))
|
|
|
|
def test_register(self):
|
|
g = compile.RuleDependencyGraph()
|
|
reg = agnostic.TriggerRegistry(g)
|
|
|
|
# register
|
|
p_trigger = reg.register_table('p', 'alice', self.f)
|
|
triggers = reg.relevant_triggers(['alice:p'])
|
|
self.assertEqual(triggers, set([p_trigger]))
|
|
|
|
# register 2nd table
|
|
q_trigger = reg.register_table('q', 'alice', self.f)
|
|
p_triggers = reg.relevant_triggers(['alice:p'])
|
|
self.assertEqual(p_triggers, set([p_trigger]))
|
|
q_triggers = reg.relevant_triggers(['alice:q'])
|
|
self.assertEqual(q_triggers, set([q_trigger]))
|
|
|
|
# register again with table p
|
|
p2_trigger = reg.register_table('p', 'alice', self.f)
|
|
p_triggers = reg.relevant_triggers(['alice:p'])
|
|
self.assertEqual(p_triggers, set([p_trigger, p2_trigger]))
|
|
q_triggers = reg.relevant_triggers(['alice:q'])
|
|
self.assertEqual(q_triggers, set([q_trigger]))
|
|
|
|
def test_unregister(self):
|
|
g = compile.RuleDependencyGraph()
|
|
reg = agnostic.TriggerRegistry(g)
|
|
p_trigger = reg.register_table('p', 'alice', self.f)
|
|
q_trigger = reg.register_table('q', 'alice', self.f)
|
|
self.assertEqual(reg.relevant_triggers(['alice:p']),
|
|
set([p_trigger]))
|
|
self.assertEqual(reg.relevant_triggers(['alice:q']),
|
|
set([q_trigger]))
|
|
# unregister p
|
|
reg.unregister(p_trigger)
|
|
self.assertEqual(reg.relevant_triggers(['alice:p']), set())
|
|
self.assertEqual(reg.relevant_triggers(['alice:q']),
|
|
set([q_trigger]))
|
|
# unregister q
|
|
reg.unregister(q_trigger)
|
|
self.assertEqual(reg.relevant_triggers(['alice:p']), set())
|
|
self.assertEqual(reg.relevant_triggers(['alice:q']), set())
|
|
# unregister nonexistent trigger
|
|
self.assertRaises(KeyError, reg.unregister, p_trigger)
|
|
self.assertEqual(reg.relevant_triggers(['alice:p']), set())
|
|
self.assertEqual(reg.relevant_triggers(['alice:q']), set())
|
|
|
|
def test_basic_dependency(self):
|
|
g = compile.RuleDependencyGraph()
|
|
reg = agnostic.TriggerRegistry(g)
|
|
g.formula_insert(compile.parse1('p(x) :- q(x)'), 'alice')
|
|
# register p
|
|
p_trigger = reg.register_table('p', 'alice', self.f)
|
|
self.assertEqual(reg.relevant_triggers(['alice:q']), set([p_trigger]))
|
|
self.assertEqual(reg.relevant_triggers(['alice:p']), set([p_trigger]))
|
|
|
|
# register q
|
|
q_trigger = reg.register_table('q', 'alice', self.f)
|
|
self.assertEqual(reg.relevant_triggers(['alice:q']),
|
|
set([p_trigger, q_trigger]))
|
|
self.assertEqual(reg.relevant_triggers(['alice:p']),
|
|
set([p_trigger]))
|
|
|
|
def test_complex_dependency(self):
|
|
g = compile.RuleDependencyGraph()
|
|
reg = agnostic.TriggerRegistry(g)
|
|
g.formula_insert(compile.parse1('p(x) :- q(x)'), 'alice')
|
|
g.formula_insert(compile.parse1('q(x) :- r(x), s(x)'), 'alice')
|
|
g.formula_insert(compile.parse1('r(x) :- t(x, y), u(y)'), 'alice')
|
|
g.formula_insert(compile.parse1('separate(x) :- separate2(x)'),
|
|
'alice')
|
|
g.formula_insert(compile.parse1('notrig(x) :- notrig2(x)'), 'alice')
|
|
p_trigger = reg.register_table('p', 'alice', self.f)
|
|
sep_trigger = reg.register_table('separate', 'alice', self.f)
|
|
|
|
# individual tables
|
|
self.assertEqual(reg.relevant_triggers(['alice:p']), set([p_trigger]))
|
|
self.assertEqual(reg.relevant_triggers(['alice:q']), set([p_trigger]))
|
|
self.assertEqual(reg.relevant_triggers(['alice:r']), set([p_trigger]))
|
|
self.assertEqual(reg.relevant_triggers(['alice:s']), set([p_trigger]))
|
|
self.assertEqual(reg.relevant_triggers(['alice:t']), set([p_trigger]))
|
|
self.assertEqual(reg.relevant_triggers(['alice:u']), set([p_trigger]))
|
|
self.assertEqual(reg.relevant_triggers(['alice:notrig']), set())
|
|
self.assertEqual(reg.relevant_triggers(['alice:notrig2']), set([]))
|
|
self.assertEqual(reg.relevant_triggers(['alice:separate']),
|
|
set([sep_trigger]))
|
|
self.assertEqual(reg.relevant_triggers(['alice:separate2']),
|
|
set([sep_trigger]))
|
|
|
|
# groups of tables
|
|
self.assertEqual(reg.relevant_triggers(['alice:p', 'alice:q']),
|
|
set([p_trigger]))
|
|
self.assertEqual(reg.relevant_triggers(['alice:separate', 'alice:p']),
|
|
set([p_trigger, sep_trigger]))
|
|
self.assertEqual(reg.relevant_triggers(['alice:notrig', 'alice:p']),
|
|
set([p_trigger]))
|
|
|
|
# events: data
|
|
event = compile.Event(compile.parse1('q(1)'), target='alice')
|
|
self.assertEqual(reg.relevant_triggers([event]), set([p_trigger]))
|
|
|
|
event = compile.Event(compile.parse1('u(1)'), target='alice')
|
|
self.assertEqual(reg.relevant_triggers([event]), set([p_trigger]))
|
|
|
|
event = compile.Event(compile.parse1('separate2(1)'), target='alice')
|
|
self.assertEqual(reg.relevant_triggers([event]), set([sep_trigger]))
|
|
|
|
event = compile.Event(compile.parse1('notrig2(1)'), target='alice')
|
|
self.assertEqual(reg.relevant_triggers([event]), set([]))
|
|
|
|
# events: rules
|
|
event = compile.Event(compile.parse1('separate(x) :- q(x)'),
|
|
target='alice')
|
|
self.assertEqual(reg.relevant_triggers([event]), set([sep_trigger]))
|
|
|
|
event = compile.Event(compile.parse1('notrig(x) :- q(x)'),
|
|
target='alice')
|
|
self.assertEqual(reg.relevant_triggers([event]), set([]))
|
|
|
|
event = compile.Event(compile.parse1('r(x) :- q(x)'), target='alice')
|
|
self.assertEqual(reg.relevant_triggers([event]), set([p_trigger]))
|
|
|
|
# events: multiple rules and data
|
|
event1 = compile.Event(compile.parse1('r(x) :- q(x)'), target='alice')
|
|
event2 = compile.Event(compile.parse1('separate2(1)'), target='alice')
|
|
self.assertEqual(reg.relevant_triggers([event1, event2]),
|
|
set([p_trigger, sep_trigger]))
|
|
|
|
event1 = compile.Event(compile.parse1('r(x) :- q(x)'), target='alice')
|
|
event2 = compile.Event(compile.parse1('notrigger2(1)'), target='alice')
|
|
self.assertEqual(reg.relevant_triggers([event1, event2]),
|
|
set([p_trigger]))
|
|
|
|
def test_triggers_by_table(self):
|
|
t1 = agnostic.Trigger('p', 'alice', lambda x: x)
|
|
t2 = agnostic.Trigger('p', 'alice', lambda x, y: x)
|
|
t3 = agnostic.Trigger('q', 'alice', lambda x: x)
|
|
triggers = [t1, t2, t3]
|
|
table_triggers = agnostic.TriggerRegistry.triggers_by_table(triggers)
|
|
self.assertEqual(2, len(table_triggers))
|
|
self.assertEqual(set(table_triggers[('p', 'alice', None)]),
|
|
set([t1, t2]))
|
|
self.assertEqual(set(table_triggers[('q', 'alice', None)]),
|
|
set([t3]))
|
|
|
|
def test_modals(self):
|
|
g = compile.RuleDependencyGraph()
|
|
reg = agnostic.TriggerRegistry(g)
|
|
|
|
# register
|
|
p_trigger = reg.register_table('p', 'alice', self.f, modal='exec')
|
|
triggers = reg.relevant_triggers(['alice:p'])
|
|
self.assertEqual(triggers, set([p_trigger]))
|
|
|
|
# register 2nd table
|
|
q_trigger = reg.register_table('q', 'alice', self.f)
|
|
p_triggers = reg.relevant_triggers(['alice:p'])
|
|
self.assertEqual(p_triggers, set([p_trigger]))
|
|
q_triggers = reg.relevant_triggers(['alice:q'])
|
|
self.assertEqual(q_triggers, set([q_trigger]))
|
|
|
|
|
|
class TestTriggers(base.TestCase):
|
|
class MyObject(object):
|
|
"""A class with methods that have side-effects."""
|
|
|
|
def __init__(self):
|
|
self.value = 0
|
|
self.equals = False
|
|
|
|
def increment(self):
|
|
"""Used for counting number of times function invoked."""
|
|
self.value += 1
|
|
|
|
def equal(self, realold, realnew, old, new):
|
|
"""Used for checking if function is invoked with correct args."""
|
|
self.equals = (realold == old and realnew == new)
|
|
|
|
def test_empty(self):
|
|
obj = self.MyObject()
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.register_trigger('p', lambda tbl, old, new: obj.increment())
|
|
run.insert('p(1)')
|
|
self.assertEqual(1, obj.value)
|
|
|
|
def test_empty2(self):
|
|
obj = self.MyObject()
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.insert('p(1)')
|
|
run.register_trigger('p', lambda tbl, old, new: obj.increment())
|
|
run.delete('p(1)')
|
|
self.assertEqual(1, obj.value)
|
|
|
|
def test_empty3(self):
|
|
obj = self.MyObject()
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.insert('p(1)')
|
|
run.delete('p(1)')
|
|
run.register_trigger('p', lambda tbl, old, new: obj.increment())
|
|
run.delete('p(1)')
|
|
self.assertEqual(0, obj.value)
|
|
|
|
def test_nochange(self):
|
|
obj = self.MyObject()
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.insert('p(1)')
|
|
run.register_trigger('p', lambda tbl, old, new: obj.increment())
|
|
run.insert('p(1)')
|
|
self.assertEqual(0, obj.value)
|
|
|
|
def test_batch_change(self):
|
|
obj = self.MyObject()
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.register_trigger('p', lambda tbl, old, new: obj.increment())
|
|
p1 = compile.parse1('p(1)')
|
|
result = run.update([compile.Event(p1, target='test')])
|
|
self.assertTrue(result[0], ("Update failed with errors: " +
|
|
";".join(str(x) for x in result[1])))
|
|
self.assertEqual(1, obj.value)
|
|
|
|
def test_dependency(self):
|
|
obj = self.MyObject()
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.insert('p(x) :- q(x)')
|
|
run.register_trigger('p', lambda tbl, old, new: obj.increment())
|
|
run.insert('q(1)')
|
|
self.assertEqual(1, obj.value)
|
|
|
|
def test_dependency_batch_insert(self):
|
|
obj = self.MyObject()
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.register_trigger('p', lambda tbl, old, new: obj.increment())
|
|
run.insert('q(1) p(x) :- q(x)')
|
|
self.assertEqual(1, obj.value)
|
|
|
|
def test_dependency_batch(self):
|
|
obj = self.MyObject()
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.insert('p(x) :- q(x)')
|
|
run.register_trigger('p', lambda tbl, old, new: obj.increment())
|
|
rule = compile.parse1('q(x) :- r(x)')
|
|
data = compile.parse1('r(1)')
|
|
run.update([compile.Event(rule, target='test'),
|
|
compile.Event(data, target='test')])
|
|
self.assertEqual(1, obj.value)
|
|
|
|
def test_dependency_batch_delete(self):
|
|
obj = self.MyObject()
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.insert('p(x) :- q(x)')
|
|
run.insert('q(x) :- r(x)')
|
|
run.insert('r(1)')
|
|
run.register_trigger('p', lambda tbl, old, new: obj.increment())
|
|
run.delete('q(x) :- r(x)')
|
|
self.assertEqual(1, obj.value)
|
|
|
|
def test_multi_dependency(self):
|
|
obj = self.MyObject()
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.insert('p(x) :- q(x)')
|
|
run.insert('q(x) :- r(x), s(x)')
|
|
run.insert('s(1)')
|
|
run.register_trigger('p', lambda tbl, old, new: obj.increment())
|
|
run.insert('r(1)')
|
|
self.assertEqual(1, obj.value)
|
|
|
|
def test_negation(self):
|
|
obj = self.MyObject()
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.insert('p(x) :- q(x), not r(x)')
|
|
run.insert('q(1)')
|
|
run.insert('q(2)')
|
|
run.insert('r(2)')
|
|
run.register_trigger('p', lambda tbl, old, new: obj.increment())
|
|
run.insert('r(1)')
|
|
self.assertEqual(1, obj.value)
|
|
run.register_trigger('p', lambda tbl, old, new: obj.increment())
|
|
run.delete('r(1)')
|
|
self.assertEqual(3, obj.value)
|
|
|
|
def test_anti_dependency(self):
|
|
obj = self.MyObject()
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.insert('p(x) :- q(x)')
|
|
run.insert('r(1)')
|
|
run.register_trigger('r', lambda tbl, old, new: obj.increment())
|
|
run.insert('q(1)')
|
|
self.assertEqual(0, obj.value)
|
|
|
|
def test_old_new_correctness(self):
|
|
obj = self.MyObject()
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.insert('p(x) :- q(x)')
|
|
run.insert('q(x) :- r(x), not s(x)')
|
|
run.insert('r(1) r(2) r(3)')
|
|
run.insert('s(2)')
|
|
oldp = set(compile.parse('p(1) p(3)'))
|
|
newp = set(compile.parse('p(1) p(2)'))
|
|
run.register_trigger('p',
|
|
lambda tbl, old, new:
|
|
obj.equal(oldp, newp, old, new))
|
|
run.update([compile.Event(compile.parse1('s(3)')),
|
|
compile.Event(compile.parse1('s(2)'), insert=False)])
|
|
self.assertTrue(obj.equals)
|
|
|
|
def test_unregister(self):
|
|
obj = self.MyObject()
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
trigger = run.register_trigger('p',
|
|
lambda tbl, old, new: obj.increment())
|
|
run.insert('p(1)')
|
|
self.assertEqual(1, obj.value)
|
|
run.unregister_trigger(trigger)
|
|
self.assertEqual(1, obj.value)
|
|
run.insert('p(2)')
|
|
self.assertEqual(1, obj.value)
|
|
self.assertRaises(KeyError, run.unregister_trigger, trigger)
|
|
self.assertEqual(1, obj.value)
|
|
|
|
def test_sequence(self):
|
|
obj = self.MyObject()
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.register_trigger('p', lambda tbl, old, new: obj.increment())
|
|
run.insert('p(x) :- q(x)')
|
|
run.insert('q(1)')
|
|
self.assertEqual(1, obj.value)
|
|
|
|
def test_delete_data(self):
|
|
obj = self.MyObject()
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.register_trigger('p', lambda tbl, old, new: obj.increment())
|
|
run.insert('p(x) :- q(x, y), equal(y, 1)')
|
|
run.insert('q(1, 1)')
|
|
self.assertEqual(1, obj.value)
|
|
run.delete('q(1, 1)')
|
|
self.assertEqual(2, obj.value)
|
|
|
|
def test_multi_policies(self):
|
|
obj = self.MyObject()
|
|
run = agnostic.Runtime()
|
|
run.debug_mode()
|
|
run.create_policy('alice')
|
|
run.create_policy('bob')
|
|
run.register_trigger('p',
|
|
lambda tbl, old, new: obj.increment(), 'alice')
|
|
run.insert('p(x) :- bob:q(x)', target='alice')
|
|
run.insert('q(1)', target='bob')
|
|
self.assertEqual(1, obj.value)
|
|
run.delete('q(1)', target='bob')
|
|
self.assertEqual(2, obj.value)
|
|
|
|
def test_modal(self):
|
|
obj = self.MyObject()
|
|
run = agnostic.Runtime()
|
|
run.debug_mode()
|
|
run.create_policy('alice')
|
|
run.register_trigger('p', lambda tbl, old, new:
|
|
obj.increment(), 'alice', 'execute')
|
|
run.insert('execute[p(x)] :- q(x)')
|
|
self.assertEqual(0, obj.value)
|
|
run.insert('q(1)')
|
|
self.assertEqual(1, obj.value)
|
|
run.insert('q(2)')
|
|
self.assertEqual(2, obj.value)
|
|
|
|
def test_initialize(self):
|
|
obj = self.MyObject()
|
|
run = agnostic.Runtime()
|
|
run.debug_mode()
|
|
run.create_policy('alice')
|
|
run.register_trigger('p', lambda tbl, old, new:
|
|
obj.increment(), 'alice', 'execute')
|
|
run.insert('execute[p(x)] :- q(x)')
|
|
self.assertEqual(obj.value, 0)
|
|
run.initialize_tables(['q'], [compile.Fact('q', [1])], 'alice')
|
|
self.assertEqual(obj.value, 1)
|
|
|
|
|
|
class TestMultipolicyRules(base.TestCase):
|
|
def test_external(self):
|
|
"""Test ability to write rules that span multiple policies."""
|
|
# External theory
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test1')
|
|
run.insert('q(1)', target='test1')
|
|
run.insert('q(2)', target='test1')
|
|
run.create_policy('test2')
|
|
run.insert('p(x) :- test1:q(x)', target='test2')
|
|
actual = run.select('p(x)', target='test2')
|
|
e = helper.db_equal('p(1) p(2)', actual)
|
|
self.assertTrue(e, "Basic")
|
|
|
|
def test_multi_external(self):
|
|
"""Test multiple rules that span multiple policies."""
|
|
run = agnostic.Runtime()
|
|
run.debug_mode()
|
|
run.create_policy('test1')
|
|
run.create_policy('test2')
|
|
run.create_policy('test3')
|
|
run.insert('p(x) :- test2:p(x)', target='test1')
|
|
run.insert('p(x) :- test3:p(x)', target='test1')
|
|
run.insert('p(1)', target='test2')
|
|
run.insert('p(2)', target='test3')
|
|
actual = run.select('p(x)', target='test1')
|
|
e = helper.db_equal(actual, 'p(1) p(2)')
|
|
self.assertTrue(e, "Multiple external rules with multiple policies")
|
|
|
|
def test_external_current(self):
|
|
"""Test ability to write rules that span multiple policies."""
|
|
# External theory plus current theory
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test1')
|
|
run.insert('q(1)', target='test1')
|
|
run.insert('q(2)', target='test1')
|
|
run.create_policy('test2')
|
|
run.insert('p(x) :- test1:q(x), r(x)', target='test2')
|
|
run.insert('r(1)', target='test2')
|
|
run.insert('r(2)', target='test2')
|
|
actual = run.select('p(x)', target='test2')
|
|
e = helper.db_equal(actual, 'p(1) p(2)')
|
|
self.assertTrue(e, "Mixing external theories with current theory")
|
|
|
|
def test_ignore_local(self):
|
|
"""Test ability to write rules that span multiple policies."""
|
|
# Local table ignored
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test1')
|
|
run.insert('q(1)', target='test1')
|
|
run.insert('q(2)', target='test1')
|
|
run.create_policy('test2')
|
|
run.insert('p(x) :- test1:q(x), r(x)', target='test2')
|
|
run.insert('q(3)', 'test2')
|
|
run.insert('r(1)', target='test2')
|
|
run.insert('r(2)', target='test2')
|
|
run.insert('r(3)', target='test2')
|
|
actual = run.select('p(x)', target='test2')
|
|
e = helper.db_equal(actual, 'p(1) p(2)')
|
|
self.assertTrue(e, "Local table ignored")
|
|
|
|
def test_local(self):
|
|
"""Test ability to write rules that span multiple policies."""
|
|
# Local table used
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test1')
|
|
run.insert('q(1)', target='test1')
|
|
run.insert('q(2)', target='test1')
|
|
run.create_policy('test2')
|
|
run.insert('p(x) :- test1:q(x), q(x)', target='test2')
|
|
run.insert('q(2)', 'test2')
|
|
actual = run.select('p(x)', target='test2')
|
|
e = helper.db_equal(actual, 'p(2)')
|
|
self.assertTrue(e, "Local table used")
|
|
|
|
def test_multiple_external(self):
|
|
"""Test ability to write rules that span multiple policies."""
|
|
# Multiple external theories
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test1')
|
|
run.insert('q(1)', target='test1')
|
|
run.insert('q(2)', target='test1')
|
|
run.insert('q(3)', target='test1')
|
|
run.create_policy('test2')
|
|
run.insert('q(1)', target='test2')
|
|
run.insert('q(2)', target='test2')
|
|
run.insert('q(4)', target='test2')
|
|
run.create_policy('test3')
|
|
run.insert('p(x) :- test1:q(x), test2:q(x)', target='test3')
|
|
actual = run.select('p(x)', target='test3')
|
|
e = helper.db_equal(actual, 'p(1) p(2)')
|
|
self.assertTrue(e, "Multiple external theories")
|
|
|
|
def test_multiple_levels_external(self):
|
|
"""Test ability to write rules that span multiple policies."""
|
|
# Multiple levels of external theories
|
|
run = agnostic.Runtime()
|
|
run.debug_mode()
|
|
run.create_policy('test1')
|
|
run.insert('p(x) :- test2:q(x), test3:q(x)', target='test1')
|
|
run.insert('s(3) s(1) s(2) s(4)', target='test1')
|
|
run.create_policy('test2')
|
|
run.insert('q(x) :- test4:r(x)', target='test2')
|
|
run.create_policy('test3')
|
|
run.insert('q(x) :- test1:s(x)', target='test3')
|
|
run.create_policy('test4')
|
|
run.insert('r(1)', target='test4')
|
|
run.insert('r(2)', target='test4')
|
|
run.insert('r(5)', target='test4')
|
|
actual = run.select('p(x)', target='test1')
|
|
e = helper.db_equal(actual, 'p(1) p(2)')
|
|
self.assertTrue(e, "Multiple levels of external theories")
|
|
|
|
def test_multipolicy_head(self):
|
|
"""Test SELECT with different policy in the head."""
|
|
run = agnostic.Runtime()
|
|
run.debug_mode()
|
|
run.create_policy('test1', kind='action')
|
|
run.create_policy('test2', kind='action')
|
|
(permitted, errors) = run.insert('test2:p+(x) :- q(x)', 'test1')
|
|
self.assertTrue(permitted, "modals with policy names must be allowed")
|
|
run.insert('q(1)', 'test1')
|
|
run.insert('p(2)', 'test2')
|
|
actual = run.select('test2:p+(x)', 'test1')
|
|
e = helper.db_equal(actual, 'test2:p+(1)')
|
|
self.assertTrue(e, "Policy name in the head")
|
|
|
|
def test_multipolicy_normal_errors(self):
|
|
"""Test errors arising from rules in multiple policies."""
|
|
run = agnostic.Runtime()
|
|
run.debug_mode()
|
|
run.create_policy('test1')
|
|
|
|
# policy in head of rule
|
|
(permitted, errors) = run.insert('test2:p(x) :- q(x)', 'test1')
|
|
self.assertFalse(permitted)
|
|
self.assertTrue("should not reference any policy" in str(errors[0]))
|
|
|
|
# policy in head of rule with update
|
|
(permitted, errors) = run.insert('test2:p+(x) :- q(x)', 'test1')
|
|
self.assertFalse(permitted)
|
|
self.assertTrue("should not reference any policy" in str(errors[0]))
|
|
|
|
# policy in head of rule with update
|
|
(permitted, errors) = run.insert('test2:p-(x) :- q(x)', 'test1')
|
|
self.assertFalse(permitted)
|
|
self.assertTrue("should not reference any policy" in str(errors[0]))
|
|
|
|
# policy in head of fact
|
|
(permitted, errors) = run.insert('test2:p(1)', 'test1')
|
|
self.assertFalse(permitted)
|
|
self.assertTrue("should not reference any policy" in str(errors[0]))
|
|
|
|
# policy in head of fact
|
|
(permitted, errors) = run.insert('test2:p+(1)', 'test1')
|
|
self.assertFalse(permitted)
|
|
self.assertTrue("should not reference any policy" in str(errors[0]))
|
|
|
|
# policy in head of fact
|
|
(permitted, errors) = run.insert('test2:p-(1)', 'test1')
|
|
self.assertFalse(permitted)
|
|
self.assertTrue("should not reference any policy" in str(errors[0]))
|
|
|
|
# recursion across policies
|
|
run.insert('p(x) :- test2:q(x)', target='test1')
|
|
run.create_policy('test2')
|
|
(permit, errors) = run.insert('q(x) :- test1:p(x)', target='test2')
|
|
self.assertFalse(permit, "Recursion across theories should fail")
|
|
self.assertEqual(len(errors), 1)
|
|
self.assertTrue("Rules are recursive" in str(errors[0]))
|
|
|
|
def test_multipolicy_action_errors(self):
|
|
"""Test errors arising from rules in action policies."""
|
|
run = agnostic.Runtime()
|
|
run.debug_mode()
|
|
run.create_policy('test1', kind='action')
|
|
|
|
# policy in head of rule
|
|
(permitted, errors) = run.insert('test2:p(x) :- q(x)', 'test1')
|
|
self.assertFalse(permitted)
|
|
self.assertTrue("should not reference any policy" in str(errors[0]))
|
|
|
|
# policy in head of fact
|
|
(permitted, errors) = run.insert('test2:p(1)', 'test1')
|
|
self.assertFalse(permitted)
|
|
self.assertTrue("should not reference any policy" in str(errors[0]))
|
|
|
|
# recursion across policies
|
|
run.insert('p(x) :- test2:q(x)', target='test1')
|
|
run.create_policy('test2')
|
|
(permit, errors) = run.insert('q(x) :- test1:p(x)', target='test2')
|
|
self.assertFalse(permit, "Recursion across theories should fail")
|
|
self.assertEqual(len(errors), 1)
|
|
self.assertTrue("Rules are recursive" in str(errors[0]))
|
|
|
|
def test_dependency_graph(self):
|
|
"""Test that dependency graph gets updated correctly."""
|
|
run = agnostic.Runtime()
|
|
run.debug_mode()
|
|
g = run.global_dependency_graph
|
|
|
|
run.create_policy('test')
|
|
|
|
run.insert('p(x) :- q(x), nova:q(x)', target='test')
|
|
self.assertTrue(g.edge_in('test:p', 'nova:q', False))
|
|
self.assertTrue(g.edge_in('test:p', 'test:q', False))
|
|
|
|
run.insert('p(x) :- s(x)', target='test')
|
|
self.assertTrue(g.edge_in('test:p', 'nova:q', False))
|
|
self.assertTrue(g.edge_in('test:p', 'test:q', False))
|
|
self.assertTrue(g.edge_in('test:p', 'test:s', False))
|
|
|
|
run.insert('q(x) :- nova:r(x)', target='test')
|
|
self.assertTrue(g.edge_in('test:p', 'nova:q', False))
|
|
self.assertTrue(g.edge_in('test:p', 'test:q', False))
|
|
self.assertTrue(g.edge_in('test:p', 'test:s', False))
|
|
self.assertTrue(g.edge_in('test:q', 'nova:r', False))
|
|
|
|
run.delete('p(x) :- q(x), nova:q(x)', target='test')
|
|
self.assertTrue(g.edge_in('test:p', 'test:s', False))
|
|
self.assertTrue(g.edge_in('test:q', 'nova:r', False))
|
|
|
|
run.update([compile.Event(helper.str2form('p(x) :- q(x), nova:q(x)'),
|
|
target='test')])
|
|
self.assertTrue(g.edge_in('test:p', 'nova:q', False))
|
|
self.assertTrue(g.edge_in('test:p', 'test:q', False))
|
|
self.assertTrue(g.edge_in('test:p', 'test:s', False))
|
|
self.assertTrue(g.edge_in('test:q', 'nova:r', False))
|
|
|
|
def test_negation(self):
|
|
"""Test that negation when applied to a different policy works."""
|
|
run = agnostic.Runtime()
|
|
run.debug_mode()
|
|
run.create_policy('alpha')
|
|
run.create_policy('beta')
|
|
run.insert('p(x) :- beta:q(x), not beta:q(x)', 'alpha')
|
|
run.insert('q(1)', 'beta')
|
|
self.assertEqual(run.select('p(x)', 'alpha'), '')
|
|
|
|
def test_built_in(self):
|
|
"""Test that built_in function works."""
|
|
run = agnostic.Runtime()
|
|
run.debug_mode()
|
|
run.create_policy('alpha')
|
|
run.create_policy('beta')
|
|
run.create_policy('sigma')
|
|
run.insert('p(x1, x2) :- '
|
|
'beta:q(x1), sigma:r(x2), not equal(x1, x2)', 'alpha')
|
|
run.insert('q(1)', 'beta')
|
|
run.insert('r(1)', 'sigma')
|
|
run.insert('r(3)', 'sigma')
|
|
self.assertEqual(run.select('p(x1,x2)', 'alpha'), 'p(1, 3)')
|
|
|
|
def test_schema_check(self):
|
|
"""Test that schema check in multiple policies works."""
|
|
run = agnostic.Runtime()
|
|
run.debug_mode()
|
|
run.create_policy('alpha')
|
|
run.create_policy('beta')
|
|
run.insert('p(x,y) :- beta:q(x,y)', 'alpha')
|
|
permitted, changes = run.insert('q(x) :- r(x)', 'beta')
|
|
self.assertFalse(permitted)
|
|
self.assertEqual(len(changes), 1)
|
|
|
|
def test_same_rules(self):
|
|
"""Test that same rule insertion can be correctly dealt with."""
|
|
run = agnostic.Runtime()
|
|
run.debug_mode()
|
|
policy = 'alpha'
|
|
run.create_policy(policy)
|
|
rulestr = 'p(x,y) :- q(x,y)'
|
|
rule = compile.parse1(rulestr)
|
|
run.insert(rulestr, policy)
|
|
self.assertTrue(rule in run.policy_object(policy))
|
|
self.assertTrue(
|
|
rule.head.table.table in run.policy_object(policy).schema)
|
|
run.insert(rulestr, policy)
|
|
run.delete(rulestr, policy)
|
|
self.assertFalse(rule in run.policy_object(policy))
|
|
self.assertFalse(
|
|
rule.head.table.table in run.policy_object(policy).schema)
|
|
|
|
|
|
class TestSelect(base.TestCase):
|
|
def test_no_dups(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.insert('p(x) :- q(x)')
|
|
run.insert('p(x) :- r(x)')
|
|
run.insert('q(1)')
|
|
run.insert('r(1)')
|
|
self.assertEqual(run.select('p(x)'), 'p(1)')
|
|
|
|
|
|
class TestPolicyCreationDeletion(base.TestCase):
|
|
def test_policy_creation_after_ref(self):
|
|
"""Test ability to write rules that span multiple policies."""
|
|
# Local table used
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test1')
|
|
run.insert('p(x) :- test2:q(x)', 'test1')
|
|
run.create_policy('test2')
|
|
run.insert('q(1)', 'test2')
|
|
actual = run.select('p(x)', 'test1')
|
|
e = helper.db_equal(actual, 'p(1)')
|
|
self.assertTrue(e, "Creation after reference")
|
|
|
|
def test_policy_deletion_after_ref(self):
|
|
"""Test ability to write rules that span multiple policies."""
|
|
# Local table used
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test1')
|
|
run.insert('p(x) :- test2:q(x)', 'test1')
|
|
# ensuring this code runs, without causing an error
|
|
run.create_policy('test2')
|
|
run.delete_policy('test2')
|
|
# add the policy back, this time checking for dangling refs
|
|
run.create_policy('test2')
|
|
self.assertRaises(exception.DanglingReference, run.delete_policy,
|
|
'test2', disallow_dangling_refs=True)
|
|
|
|
def test_policy_deletion_dependency_graph(self):
|
|
"""Ensure dependency graph is properly updated when deleting policy."""
|
|
run = agnostic.Runtime()
|
|
run.create_policy('alice')
|
|
run.insert('p(x) :- q(x)')
|
|
LOG.info("graph: \n%s", run.global_dependency_graph)
|
|
self.assertTrue(run.global_dependency_graph.edge_in(
|
|
'alice:p', 'alice:q', False))
|
|
# don't delete rules first--just delete policy
|
|
run.delete_policy('alice')
|
|
self.assertEqual(len(run.global_dependency_graph), 0)
|
|
|
|
|
|
class TestDependencyGraph(base.TestCase):
|
|
def test_fact_insert(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
facts = [compile.Fact('p', [1])]
|
|
run.initialize_tables([], facts)
|
|
self.assertFalse(run.global_dependency_graph.node_in('test:p'))
|
|
|
|
def test_atom_insert(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.insert('p(1)')
|
|
self.assertFalse(run.global_dependency_graph.node_in('test:p'))
|
|
|
|
def test_rule_noop(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.insert('q(1) :- p(1)')
|
|
run.delete('q(2) :- p(2)')
|
|
self.assertTrue(run.global_dependency_graph.node_in('test:p'))
|
|
self.assertTrue(run.global_dependency_graph.node_in('test:q'))
|
|
self.assertTrue(run.global_dependency_graph.edge_in(
|
|
'test:q', 'test:p', False))
|
|
|
|
def test_atom_deletion(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.insert('q(x) :- p(x)')
|
|
run.delete('p(1)')
|
|
run.delete('p(1)')
|
|
# actually just testing that no error is thrown
|
|
self.assertFalse(run.global_dependency_graph.has_cycle())
|
|
|
|
|
|
class TestSimulate(base.TestCase):
|
|
DEFAULT_THEORY = 'test_default'
|
|
ACTION_THEORY = 'test_action'
|
|
|
|
def prep_runtime(self, code=None, msg=None, target=None, theories=None):
|
|
if code is None:
|
|
code = ""
|
|
if target is None:
|
|
target = self.DEFAULT_THEORY
|
|
run = agnostic.Runtime()
|
|
run.create_policy(self.DEFAULT_THEORY, abbr='default')
|
|
run.create_policy(self.ACTION_THEORY, abbr='action', kind='action')
|
|
if theories:
|
|
for theory in theories:
|
|
run.create_policy(theory)
|
|
run.debug_mode()
|
|
run.insert(code, target=target)
|
|
return run
|
|
|
|
def create(self, action_code, class_code, theories=None):
|
|
run = self.prep_runtime(theories=theories)
|
|
|
|
actth = self.ACTION_THEORY
|
|
permitted, errors = run.insert(action_code, target=actth)
|
|
self.assertTrue(permitted, "Error in action policy: {}".format(
|
|
utility.iterstr(errors)))
|
|
|
|
defth = self.DEFAULT_THEORY
|
|
permitted, errors = run.insert(class_code, target=defth)
|
|
self.assertTrue(permitted, "Error in classifier policy: {}".format(
|
|
utility.iterstr(errors)))
|
|
|
|
return run
|
|
|
|
def check(self, run, action_sequence, query, correct, msg, delta=False):
|
|
original_db = str(run.theory[self.DEFAULT_THEORY])
|
|
actual = run.simulate(
|
|
query, self.DEFAULT_THEORY, action_sequence,
|
|
self.ACTION_THEORY, delta=delta)
|
|
e = helper.datalog_equal(actual, correct)
|
|
self.assertTrue(e, msg + " (Query results not correct)")
|
|
e = helper.db_equal(
|
|
str(run.theory[self.DEFAULT_THEORY]), original_db)
|
|
self.assertTrue(e, msg + " (Rollback failed)")
|
|
|
|
def test_multipolicy_state_1(self):
|
|
"""Test update sequence affecting datasources."""
|
|
run = self.prep_runtime(theories=['nova', 'neutron'])
|
|
run.insert('p(x) :- nova:p(x)', self.DEFAULT_THEORY)
|
|
sequence = 'nova:p+(1) neutron:p+(2)'
|
|
self.check(run, sequence, 'p(x)', 'p(1)', 'Separate theories')
|
|
|
|
def test_multipolicy_state_2(self):
|
|
"""Test update sequence affecting datasources."""
|
|
run = self.prep_runtime(theories=['nova', 'neutron'])
|
|
run.insert('p(x) :- neutron:p(x)', self.DEFAULT_THEORY)
|
|
run.insert('p(x) :- nova:p(x)', self.DEFAULT_THEORY)
|
|
sequence = 'nova:p+(1) neutron:p+(2)'
|
|
self.check(run, sequence, 'p(x)', 'p(1) p(2)', 'Separate theories 2')
|
|
|
|
def test_multipolicy_state_3(self):
|
|
"""Test update sequence affecting datasources."""
|
|
run = self.prep_runtime(theories=['nova', 'neutron'])
|
|
run.insert('p(x) :- neutron:p(x)', self.DEFAULT_THEORY)
|
|
run.insert('p(x) :- nova:p(x)', self.DEFAULT_THEORY)
|
|
run.insert('p(1)', 'nova')
|
|
sequence = 'nova:p+(1) neutron:p+(2)'
|
|
self.check(run, sequence, 'p(x)', 'p(1) p(2)', 'Separate theories 3')
|
|
self.check(run, '', 'p(x)', 'p(1)', 'Existing data separate theories')
|
|
|
|
def test_multipolicy_action_sequence(self):
|
|
"""Test sequence updates with actions that impact multiple policies."""
|
|
action_code = ('nova:p+(x) :- q(x)'
|
|
'neutron:p+(y) :- q(x), plus(x, 1, y)'
|
|
'ceilometer:p+(y) :- q(x), plus(x, 5, y)'
|
|
'action("q")')
|
|
classify_code = 'p(x) :- nova:p(x) p(x) :- neutron:p(x) p(3)'
|
|
run = self.create(action_code, classify_code,
|
|
theories=['nova', 'neutron', 'ceilometer'])
|
|
action_sequence = 'q(1)'
|
|
self.check(run, action_sequence, 'p(x)', 'p(1) p(2) p(3)',
|
|
'Multi-policy actions')
|
|
|
|
def test_action_sequence(self):
|
|
"""Test sequence updates with actions."""
|
|
|
|
# Simple
|
|
action_code = ('p+(x) :- q(x) action("q")')
|
|
classify_code = 'p(2)' # just some other data present
|
|
run = self.create(action_code, classify_code)
|
|
action_sequence = 'q(1)'
|
|
self.check(run, action_sequence, 'p(x)', 'p(1) p(2)', 'Simple')
|
|
|
|
# Noop does not break rollback
|
|
action_code = ('p-(x) :- q(x)'
|
|
'action("q")')
|
|
classify_code = ('')
|
|
run = self.create(action_code, classify_code)
|
|
action_sequence = 'q(1)'
|
|
self.check(run, action_sequence, 'p(x)', '',
|
|
"Rollback handles Noop")
|
|
|
|
# Add and delete
|
|
action_code = ('action("act") '
|
|
'p+(x) :- act(x) '
|
|
'p-(y) :- act(x), r(x, y) ')
|
|
classify_code = 'p(2) r(1, 2)'
|
|
run = self.create(action_code, classify_code)
|
|
action_sequence = 'act(1)'
|
|
self.check(run, action_sequence, 'p(x)', 'p(1)', 'Add and delete')
|
|
|
|
# insertion takes precedence over deletion
|
|
action_code = ('p+(x) :- q(x)'
|
|
'p-(x) :- r(x)'
|
|
'action("q")')
|
|
classify_code = ('')
|
|
run = self.create(action_code, classify_code)
|
|
# ordered so that consequences will be p+(1) p-(1)
|
|
action_sequence = 'q(1), r(1) :- true'
|
|
self.check(run, action_sequence, 'p(x)', 'p(1)',
|
|
"Deletion before insertion")
|
|
|
|
# multiple action sequences 1
|
|
action_code = ('p+(x) :- q(x)'
|
|
'p-(x) :- r(x)'
|
|
'action("q")'
|
|
'action("r")')
|
|
classify_code = ('')
|
|
run = self.create(action_code, classify_code)
|
|
action_sequence = 'q(1) r(1)'
|
|
self.check(run, action_sequence, 'p(x)', '',
|
|
"Multiple actions: inversion from {}")
|
|
|
|
# multiple action sequences 2
|
|
action_code = ('p+(x) :- q(x)'
|
|
'p-(x) :- r(x)'
|
|
'action("q")'
|
|
'action("r")')
|
|
classify_code = ('p(1)')
|
|
run = self.create(action_code, classify_code)
|
|
action_sequence = 'q(1) r(1)'
|
|
self.check(run, action_sequence, 'p(x)', '',
|
|
"Multiple actions: inversion from p(1), first is noop")
|
|
|
|
# multiple action sequences 3
|
|
action_code = ('p+(x) :- q(x)'
|
|
'p-(x) :- r(x)'
|
|
'action("q")'
|
|
'action("r")')
|
|
classify_code = ('p(1)')
|
|
run = self.create(action_code, classify_code)
|
|
action_sequence = 'r(1) q(1)'
|
|
self.check(run, action_sequence, 'p(x)', 'p(1)',
|
|
"Multiple actions: inversion from p(1), first is not noop")
|
|
|
|
# multiple action sequences 4
|
|
action_code = ('p+(x) :- q(x)'
|
|
'p-(x) :- r(x)'
|
|
'action("q")'
|
|
'action("r")')
|
|
classify_code = ('')
|
|
run = self.create(action_code, classify_code)
|
|
action_sequence = 'r(1) q(1)'
|
|
self.check(run, action_sequence, 'p(x)', 'p(1)',
|
|
"Multiple actions: inversion from {}, first is not noop")
|
|
|
|
# Action with additional info
|
|
action_code = ('p+(x,z) :- q(x,y), r(y,z)'
|
|
'action("q") action("r")')
|
|
classify_code = 'p(1,2)'
|
|
run = self.create(action_code, classify_code)
|
|
action_sequence = 'q(1,2), r(2,3) :- true'
|
|
self.check(run, action_sequence, 'p(x,y)', 'p(1,2) p(1,3)',
|
|
'Action with additional info')
|
|
|
|
def test_state_rule_sequence(self):
|
|
"""Test state and rule update sequences."""
|
|
# State update
|
|
action_code = ''
|
|
classify_code = 'p(1)'
|
|
run = self.create(action_code, classify_code)
|
|
action_sequence = 'p+(2)'
|
|
self.check(run, action_sequence, 'p(x)', 'p(1) p(2)',
|
|
'State update')
|
|
|
|
# Rule update
|
|
action_code = ''
|
|
classify_code = 'q(1)'
|
|
run = self.create(action_code, classify_code)
|
|
action_sequence = 'p+(x) :- q(x)'
|
|
self.check(run, action_sequence, 'p(x)', 'p(1)',
|
|
'Rule update')
|
|
|
|
def test_complex_sequence(self):
|
|
"""Test more complex sequences of updates."""
|
|
# action with query
|
|
action_code = ('p+(x, y) :- q(x, y)'
|
|
'action("q")')
|
|
classify_code = ('r(1)')
|
|
run = self.create(action_code, classify_code)
|
|
action_sequence = 'q(x, 0) :- r(x)'
|
|
self.check(run, action_sequence, 'p(x,y)', 'p(1,0)',
|
|
'Action with query')
|
|
|
|
# action sequence with results
|
|
action_code = ('p+(id, val) :- create(val)'
|
|
'p+(id, val) :- update(id, val)'
|
|
'p-(id, val) :- update(id, newval), p(id, val)'
|
|
'action("create")'
|
|
'action("update")'
|
|
'result(x) :- create(val), p+(x,val)')
|
|
classify_code = 'hasval(val) :- p(x, val)'
|
|
run = self.create(action_code, classify_code)
|
|
action_sequence = 'create(0) update(x,1) :- result(x)'
|
|
self.check(run, action_sequence, 'hasval(x)', 'hasval(1)',
|
|
'Action sequence with results')
|
|
|
|
def test_delta_add(self):
|
|
"""Test when asking for changes in query."""
|
|
action_code = ('action("q") '
|
|
'p+(x) :- q(x) ')
|
|
classify_code = 'p(2)' # just some other data present
|
|
run = self.create(action_code, classify_code)
|
|
action_sequence = 'q(1)'
|
|
self.check(run, action_sequence, 'p(x)', 'p+(1)', 'Add',
|
|
delta=True)
|
|
|
|
def test_delta_delete(self):
|
|
"""Test when asking for changes in query."""
|
|
action_code = ('action("q") '
|
|
'p-(x) :- q(x) ')
|
|
classify_code = 'p(1) p(2)' # p(2): just some other data present
|
|
run = self.create(action_code, classify_code)
|
|
action_sequence = 'q(1)'
|
|
self.check(run, action_sequence, 'p(x)', 'p-(1)', 'Delete',
|
|
delta=True)
|
|
|
|
def test_delta_add_delete(self):
|
|
"""Test when asking for changes in query."""
|
|
action_code = ('action("act") '
|
|
'p+(x) :- act(x) '
|
|
'p-(y) :- act(x), r(x, y) ')
|
|
classify_code = 'p(2) r(1, 2) p(3)' # p(3): just other data present
|
|
run = self.create(action_code, classify_code)
|
|
action_sequence = 'act(1)'
|
|
self.check(run, action_sequence, 'p(x)', 'p+(1) p-(2)',
|
|
'Add and delete', delta=True)
|
|
|
|
def test_key_value_schema(self):
|
|
"""Test action of key/value updates."""
|
|
action_code = (
|
|
'action("changeAttribute")'
|
|
'server_attributes+(uid, name, newvalue) :- '
|
|
'changeAttribute(uid, name, newvalue) '
|
|
'server_attributes-(uid, name, oldvalue) :- '
|
|
' changeAttribute(uid, name, newvalue), '
|
|
' server_attributes(uid, name, oldvalue)')
|
|
policy = 'error(uid) :- server_attributes(uid, name, 0)'
|
|
|
|
run = self.create(action_code, policy)
|
|
seq = 'changeAttribute(101, "cpu", 0)'
|
|
self.check(run, seq, 'error(x)', 'error(101)',
|
|
'Basic error')
|
|
|
|
run = self.create(action_code, policy)
|
|
seq = 'changeAttribute(101, "cpu", 1)'
|
|
self.check(run, seq, 'error(x)', '',
|
|
'Basic non-error')
|
|
|
|
data = ('server_attributes(101, "cpu", 1)')
|
|
run = self.create(action_code, policy + data)
|
|
seq = 'changeAttribute(101, "cpu", 0)'
|
|
self.check(run, seq, 'error(x)', 'error(101)',
|
|
'Overwrite existing to cause error')
|
|
|
|
data = ('server_attributes(101, "cpu", 0)')
|
|
run = self.create(action_code, policy + data)
|
|
seq = 'changeAttribute(101, "cpu", 1)'
|
|
self.check(run, seq, 'error(x)', '',
|
|
'Overwrite existing to eliminate error')
|
|
|
|
data = ('server_attributes(101, "cpu", 0)'
|
|
'server_attributes(101, "disk", 0)')
|
|
run = self.create(action_code, policy + data)
|
|
seq = 'changeAttribute(101, "cpu", 1)'
|
|
self.check(run, seq, 'error(x)', 'error(101)',
|
|
'Overwrite existing but still error')
|
|
|
|
def test_duplicates(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
run.insert('p(x) :- q(x)')
|
|
run.insert('p(x) :- r(x)')
|
|
run.insert('q(1)')
|
|
run.insert('r(1)')
|
|
self.assertEqual(run.simulate('p(x)', 'test', '', 'test'), 'p(1)')
|
|
|
|
|
|
class TestActionExecution(base.TestCase):
|
|
class FakeCage(object):
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
def service_object(self, name):
|
|
if self.name == name:
|
|
return self
|
|
else:
|
|
return None
|
|
|
|
def test_insert_rule_insert_data(self):
|
|
args = {}
|
|
args['d6cage'] = TestActionExecution.FakeCage('test')
|
|
args['rootdir'] = None
|
|
args['log_actions_only'] = True
|
|
run = agnostic.DseRuntime(
|
|
name='test', keys='', inbox=None, datapath=None, args=args)
|
|
|
|
run.request = mock.Mock()
|
|
run.request.return_value = 'mocked request'
|
|
|
|
run.create_policy('test')
|
|
run.debug_mode()
|
|
run.insert('execute[p(x)] :- q(x)')
|
|
self.assertEqual(len(run.logger.messages), 0, "Improper action logged")
|
|
run.insert('q(1)')
|
|
self.assertEqual(len(run.logger.messages), 1, "No action logged")
|
|
self.assertEqual(run.logger.messages[0], 'Executing test:p(1)')
|
|
|
|
expected_args = ('test', 'p')
|
|
expected_kwargs = {'args': {'positional': [1]}}
|
|
args, kwargs = run.request.call_args_list[0]
|
|
|
|
self.assertEqual(expected_args, args)
|
|
self.assertEqual(expected_kwargs, kwargs)
|
|
|
|
def test_insert_data_insert_rule(self):
|
|
args = {}
|
|
args['d6cage'] = TestActionExecution.FakeCage('test')
|
|
args['rootdir'] = None
|
|
args['log_actions_only'] = True
|
|
run = agnostic.DseRuntime(
|
|
name='test', keys='', inbox=None, datapath=None, args=args)
|
|
|
|
run.request = mock.Mock()
|
|
run.request.return_value = 'mocked request'
|
|
|
|
run.create_policy('test')
|
|
run.debug_mode()
|
|
run.insert('q(1)')
|
|
self.assertEqual(len(run.logger.messages), 0, "Improper action logged")
|
|
run.insert('execute[p(x)] :- q(x)')
|
|
self.assertEqual(len(run.logger.messages), 1, "No action logged")
|
|
self.assertEqual(run.logger.messages[0], 'Executing test:p(1)')
|
|
|
|
expected_args = ('test', 'p')
|
|
expected_kwargs = {'args': {'positional': [1]}}
|
|
args, kwargs = run.request.call_args_list[0]
|
|
|
|
self.assertEqual(expected_args, args)
|
|
self.assertEqual(expected_kwargs, kwargs)
|
|
|
|
def test_insert_data_insert_rule_delete_data(self):
|
|
args = {}
|
|
args['d6cage'] = TestActionExecution.FakeCage('test')
|
|
args['rootdir'] = None
|
|
args['log_actions_only'] = True
|
|
run = agnostic.DseRuntime(
|
|
name='test', keys='', inbox=None, datapath=None, args=args)
|
|
|
|
run.request = mock.Mock()
|
|
run.request.return_value = 'mocked request'
|
|
|
|
run.create_policy('test')
|
|
run.debug_mode()
|
|
run.insert('q(1)')
|
|
self.assertEqual(len(run.logger.messages), 0, "Improper action logged")
|
|
run.insert('execute[p(x)] :- q(x)')
|
|
self.assertEqual(len(run.logger.messages), 1, "No action logged")
|
|
self.assertEqual(run.logger.messages[0], 'Executing test:p(1)')
|
|
run.delete('q(1)')
|
|
self.assertEqual(len(run.logger.messages), 1, "Delete failure")
|
|
self.assertEqual(run.logger.messages[0], 'Executing test:p(1)')
|
|
|
|
expected_args = ('test', 'p')
|
|
expected_kwargs = {'args': {'positional': [1]}}
|
|
args, kwargs = run.request.call_args_list[0]
|
|
|
|
self.assertEqual(expected_args, args)
|
|
self.assertEqual(expected_kwargs, kwargs)
|
|
|
|
def test_insert_data_insert_rule_delete_rule(self):
|
|
args = {}
|
|
args['d6cage'] = TestActionExecution.FakeCage('test')
|
|
args['rootdir'] = None
|
|
args['log_actions_only'] = True
|
|
run = agnostic.DseRuntime(
|
|
name='test', keys='', inbox=None, datapath=None, args=args)
|
|
|
|
run.request = mock.Mock()
|
|
run.request.return_value = 'mocked request'
|
|
|
|
run.create_policy('test')
|
|
run.debug_mode()
|
|
run.insert('q(1)')
|
|
self.assertEqual(len(run.logger.messages), 0, "Improper action logged")
|
|
run.insert('execute[p(x)] :- q(x)')
|
|
self.assertEqual(len(run.logger.messages), 1, "No action logged")
|
|
self.assertEqual(run.logger.messages[0], 'Executing test:p(1)')
|
|
run.delete('execute[p(x)] :- q(x)')
|
|
self.assertEqual(len(run.logger.messages), 1, "Delete failure")
|
|
self.assertEqual(run.logger.messages[0], 'Executing test:p(1)')
|
|
|
|
expected_args = ('test', 'p')
|
|
expected_kwargs = {'args': {'positional': [1]}}
|
|
args, kwargs = run.request.call_args_list[0]
|
|
|
|
self.assertEqual(expected_args, args)
|
|
self.assertEqual(expected_kwargs, kwargs)
|
|
|
|
def test_insert_data_insert_rule_noop_insert(self):
|
|
args = {}
|
|
args['d6cage'] = TestActionExecution.FakeCage('test')
|
|
args['rootdir'] = None
|
|
args['log_actions_only'] = True
|
|
run = agnostic.DseRuntime(
|
|
name='test', keys='', inbox=None, datapath=None, args=args)
|
|
|
|
run.request = mock.Mock()
|
|
run.request.return_value = 'mocked request'
|
|
|
|
run.create_policy('test')
|
|
run.debug_mode()
|
|
run.insert('q(1)')
|
|
self.assertEqual(len(run.logger.messages), 0, "Improper action logged")
|
|
run.insert('execute[p(x)] :- q(x)')
|
|
self.assertEqual(len(run.logger.messages), 1, "No action logged")
|
|
self.assertEqual(run.logger.messages[0], 'Executing test:p(1)')
|
|
run.insert('q(1)')
|
|
self.assertEqual(len(run.logger.messages), 1, "Improper action logged")
|
|
self.assertEqual(run.logger.messages[0], 'Executing test:p(1)')
|
|
|
|
expected_args = ('test', 'p')
|
|
expected_kwargs = {'args': {'positional': [1]}}
|
|
args, kwargs = run.request.call_args_list[0]
|
|
|
|
self.assertEqual(expected_args, args)
|
|
self.assertEqual(expected_kwargs, kwargs)
|
|
|
|
def test_disjunction(self):
|
|
args = {}
|
|
args['d6cage'] = TestActionExecution.FakeCage('test')
|
|
args['rootdir'] = None
|
|
args['log_actions_only'] = True
|
|
run = agnostic.DseRuntime(
|
|
name='test', keys='', inbox=None, datapath=None, args=args)
|
|
|
|
run.request = mock.Mock()
|
|
run.request.return_value = 'mocked request'
|
|
|
|
run.create_policy('test')
|
|
run.debug_mode()
|
|
run.insert('execute[p(x)] :- q(x)')
|
|
run.insert('execute[p(x)] :- r(x)')
|
|
self.assertEqual(len(run.logger.messages), 0, "Improper action logged")
|
|
run.insert('q(1)')
|
|
self.assertEqual(len(run.logger.messages), 1, "No action logged")
|
|
self.assertEqual(run.logger.messages[0], 'Executing test:p(1)')
|
|
run.insert('r(1)')
|
|
self.assertEqual(len(run.logger.messages), 1, "Improper action logged")
|
|
self.assertEqual(run.logger.messages[0], 'Executing test:p(1)')
|
|
|
|
expected_args = ('test', 'p')
|
|
expected_kwargs = {'args': {'positional': [1]}}
|
|
args, kwargs = run.request.call_args_list[0]
|
|
|
|
self.assertEqual(expected_args, args)
|
|
self.assertEqual(expected_kwargs, kwargs)
|
|
|
|
def test_multiple_instances(self):
|
|
args = {}
|
|
args['d6cage'] = TestActionExecution.FakeCage('test')
|
|
args['rootdir'] = None
|
|
args['log_actions_only'] = True
|
|
run = agnostic.DseRuntime(
|
|
name='test', keys='', inbox=None, datapath=None, args=args)
|
|
|
|
run.request = mock.Mock()
|
|
run.request.return_value = 'mocked request'
|
|
|
|
run.create_policy('test')
|
|
run.debug_mode()
|
|
run.insert('q(1)')
|
|
run.insert('q(2)')
|
|
self.assertEqual(len(run.logger.messages), 0, "Improper action logged")
|
|
run.insert('execute[p(x)] :- q(x)')
|
|
self.assertEqual(len(run.logger.messages), 2, "No action logged")
|
|
actualset = set([u'Executing test:p(1)', u'Executing test:p(2)'])
|
|
self.assertEqual(actualset, set(run.logger.messages))
|
|
|
|
expected_args_list = [
|
|
[('test', 'p'), {'args': {'positional': [1]}}],
|
|
[('test', 'p'), {'args': {'positional': [2]}}],
|
|
]
|
|
|
|
for args, kwargs in run.request.call_args_list:
|
|
self.assertTrue([args, kwargs] in expected_args_list)
|
|
expected_args_list.remove([args, kwargs])
|
|
|
|
def test_disabled_execute_action(self):
|
|
args = {}
|
|
args['d6cage'] = None
|
|
args['rootdir'] = None
|
|
args['log_actions_only'] = False
|
|
run = agnostic.DseRuntime(
|
|
name='test', keys='', inbox=None, datapath=None, args=args)
|
|
|
|
run.request = mock.Mock()
|
|
run.request.return_value = 'mocked request'
|
|
|
|
service_name = 'test-service'
|
|
action = 'non_executable_action'
|
|
action_args = {'positional': ['p_arg1'],
|
|
'named': {'key1': 'value1'}}
|
|
|
|
run.execute_action(service_name, action, action_args)
|
|
self.assertFalse(run.request.called)
|
|
|
|
|
|
class TestDisabledRules(base.SqlTestCase):
|
|
"""Tests for Runtime's ability to enable/disable rules."""
|
|
# insertions
|
|
def test_insert_enabled(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
|
|
schema = compile.Schema({'q': ('id', 'name', 'status')})
|
|
run.set_schema('test', schema)
|
|
obj = run.policy_object('test')
|
|
run.insert('p(x) :- q(id=x)')
|
|
self.assertEqual(len(run.error_events), 0)
|
|
self.assertEqual(len(run.disabled_events), 0)
|
|
self.assertEqual(len(obj.content()), 1)
|
|
|
|
def test_insert_disabled(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
|
|
obj = run.policy_object('test')
|
|
run.insert('p(x) :- q(id=x)')
|
|
self.assertEqual(len(run.disabled_events), 1)
|
|
self.assertEqual(len(obj.content()), 0)
|
|
|
|
def test_persistent_insert_disabled(self):
|
|
"""Test that persistent_insert_rule errors on IncompleteSchemaException
|
|
|
|
When a table schema is not available, named column references are
|
|
permitted but disabled in non-persistent rule insert to allow for
|
|
late-arriving schema when importing rules already in DB.
|
|
This behavior is not necessary in persistent_insert.
|
|
"""
|
|
run = agnostic.Runtime()
|
|
run.create_policy('data', kind=datalog_base.DATASOURCE_POLICY_TYPE)
|
|
run.persistent_create_policy('policy')
|
|
obj = run.policy_object('policy')
|
|
run.insert('p(x) :- data:q(id=x)')
|
|
try:
|
|
run.persistent_insert_rule('policy', 'p(x) :- data:q(id=x)',
|
|
'', '')
|
|
except exception.PolicyException as e:
|
|
self.assertTrue(
|
|
'Literal data:q(id=x) uses unknown table q'
|
|
in str(e),
|
|
'Named column reference on unknown table '
|
|
'should be disallowed in persistent insert')
|
|
self.assertEqual(len(run.disabled_events), 0)
|
|
self.assertEqual(len(obj.content()), 0)
|
|
|
|
try:
|
|
run.persistent_insert_rule('policy', 'p(x) :- unknown:q(id=x)',
|
|
'', '')
|
|
except exception.PolicyException as e:
|
|
self.assertTrue(
|
|
'Literal unknown:q(id=x) uses named arguments, but the '
|
|
'schema is unknown.'
|
|
in str(e),
|
|
'Named column reference on unknown table '
|
|
'should be disallowed in persistent insert')
|
|
self.assertEqual(len(run.disabled_events), 0)
|
|
self.assertEqual(len(obj.content()), 0)
|
|
|
|
def test_insert_errors(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
|
|
schema = compile.Schema({'q': ('name', 'status')})
|
|
run.set_schema('test', schema)
|
|
obj = run.policy_object('test')
|
|
permitted, errors = run.insert('p(x) :- q(id=x)')
|
|
self.assertFalse(permitted)
|
|
errstring = " ".join(str(x) for x in errors)
|
|
self.assertTrue("column name id does not exist" in errstring)
|
|
self.assertEqual(len(run.error_events), 0)
|
|
self.assertEqual(len(run.disabled_events), 0)
|
|
self.assertEqual(len(obj.content()), 0)
|
|
|
|
def test_insert_set_schema_disabled(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
|
|
obj = run.policy_object('test')
|
|
run.insert('p(x) :- q(id=x)') # rule is disabled
|
|
self.assertEqual(len(run.disabled_events), 1)
|
|
schema = compile.Schema({'q': ('id', 'name', 'status')})
|
|
run.set_schema('test', schema)
|
|
self.assertEqual(len(run.error_events), 0)
|
|
self.assertEqual(len(run.disabled_events), 0)
|
|
self.assertEqual(len(obj.content()), 1)
|
|
|
|
def test_insert_set_schema_disabled_multiple(self):
|
|
# insert rule that gets disabled
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
|
|
run.create_policy('nova', kind=datalog_base.DATASOURCE_POLICY_TYPE)
|
|
obj = run.policy_object('test')
|
|
run.insert('p(x) :- q(id=x), nova:r(id=x)', 'test')
|
|
self.assertEqual(len(run.disabled_events), 1)
|
|
# set first schema
|
|
schema = compile.Schema({'q': ('id', 'name', 'status')})
|
|
run.set_schema('test', schema)
|
|
self.assertEqual(len(run.error_events), 0)
|
|
self.assertEqual(len(run.disabled_events), 1)
|
|
self.assertEqual(len(obj.content()), 0)
|
|
# set second schema
|
|
schema = compile.Schema({'r': ('id', 'name', 'status')})
|
|
run.set_schema('nova', schema)
|
|
self.assertEqual(len(run.error_events), 0)
|
|
self.assertEqual(len(run.disabled_events), 0)
|
|
self.assertEqual(len(obj.content()), 1)
|
|
|
|
def test_insert_set_schema_errors(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
|
|
obj = run.policy_object('test')
|
|
run.insert('p(x) :- q(id=x)') # rule is disabled
|
|
self.assertEqual(len(run.disabled_events), 1)
|
|
schema = compile.Schema({'q': ('name', 'status')},)
|
|
run.set_schema('test', schema)
|
|
self.assertEqual(len(run.error_events), 1)
|
|
self.assertEqual(len(run.disabled_events), 0)
|
|
self.assertEqual(len(obj.content()), 0)
|
|
|
|
def test_insert_inferred_schema_errors(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
|
|
run.insert('p(x) :- q(x)')
|
|
permitted, errs = run.insert('q(1,2)')
|
|
self.assertFalse(permitted)
|
|
|
|
# deletions
|
|
def test_delete_enabled(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
|
|
schema = compile.Schema({'q': ('id', 'name', 'status')})
|
|
run.set_schema('test', schema)
|
|
obj = run.policy_object('test')
|
|
run.insert('p(x) :- q(id=x)')
|
|
self.assertEqual(len(obj.content()), 1)
|
|
run.delete('p(x) :- q(id=x)')
|
|
self.assertEqual(len(run.error_events), 0)
|
|
self.assertEqual(len(run.disabled_events), 0)
|
|
self.assertEqual(len(obj.content()), 0)
|
|
|
|
def test_delete_set_schema_disabled(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
|
|
obj = run.policy_object('test')
|
|
run.insert('p(x) :- q(id=x)')
|
|
run.delete('p(x) :- q(id=x)')
|
|
self.assertEqual(len(run.disabled_events), 2)
|
|
self.assertEqual(len(obj.content()), 0)
|
|
schema = compile.Schema({'q': ('id', 'name', 'status')})
|
|
run.set_schema('test', schema)
|
|
self.assertEqual(len(run.disabled_events), 0)
|
|
self.assertEqual(len(obj.content()), 0)
|
|
|
|
def test_delete_errors(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
|
|
schema = compile.Schema({'q': ('name', 'status')})
|
|
run.set_schema('test', schema)
|
|
obj = run.policy_object('test')
|
|
permitted, errors = run.delete('p(x) :- q(id=x)')
|
|
self.assertFalse(permitted)
|
|
errstring = " ".join(str(x) for x in errors)
|
|
self.assertTrue("column name id does not exist" in errstring)
|
|
self.assertEqual(len(run.error_events), 0)
|
|
self.assertEqual(len(run.disabled_events), 0)
|
|
self.assertEqual(len(obj.content()), 0)
|
|
|
|
def test_delete_set_schema_errors(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
|
|
obj = run.policy_object('test')
|
|
run.delete('p(x) :- q(id=x)') # rule is disabled
|
|
self.assertEqual(len(run.disabled_events), 1)
|
|
schema = compile.Schema({'q': ('name', 'status')})
|
|
run.set_schema('test', schema)
|
|
self.assertEqual(len(run.error_events), 1)
|
|
self.assertEqual(len(run.disabled_events), 0)
|
|
self.assertEqual(len(obj.content()), 0)
|
|
|
|
# errors in set_schema
|
|
def test_set_schema_unknown_policy(self):
|
|
run = agnostic.Runtime()
|
|
schema = compile.Schema({'q': ('name', 'status')})
|
|
try:
|
|
run.set_schema('test', schema)
|
|
self.fail("Error not thrown on unknown policy")
|
|
except exception.CongressException as e:
|
|
self.assertTrue("not been created" in str(e))
|
|
|
|
def test_disallow_schema_change(self):
|
|
# Ensures that cannot change schema once it is set.
|
|
# Can be removed once we support schema changes (e.g. for upgrade).
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
|
|
schema = compile.Schema({'q': ('name', 'status')})
|
|
run.set_schema('test', schema)
|
|
schema = compile.Schema({'q': ('id', 'name', 'status')})
|
|
try:
|
|
run.set_schema('test', schema)
|
|
self.fail("Error not thrown on schema change")
|
|
except exception.CongressException as e:
|
|
self.assertTrue("Schema for test already set" in str(e))
|
|
|
|
def test_insert_without_datasource_policy(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('beta') # not datasource policy
|
|
# exception because col refs over non-datasource policy
|
|
permitted, errors = run.insert('p(x) :- beta:q(name=x)')
|
|
self.assertFalse(permitted)
|
|
self.assertTrue(
|
|
any("does not reference a datasource policy" in str(e)
|
|
for e in errors))
|
|
|
|
def test_delete_policy_while_disabled_events_outstanding(self):
|
|
"""Test deleting policy while there are disabled_events outstanding."""
|
|
run = agnostic.Runtime()
|
|
|
|
# generate disabled event
|
|
run.create_policy('test', kind=datalog_base.DATASOURCE_POLICY_TYPE)
|
|
obj = run.policy_object('test')
|
|
run.insert('p(x) :- q(id=x)')
|
|
self.assertEqual(len(run.disabled_events), 1)
|
|
self.assertEqual(len(obj.content()), 0)
|
|
|
|
# create and delete another policy
|
|
run.create_policy('to_delete')
|
|
run.delete_policy('to_delete')
|
|
|
|
|
|
class TestDelegation(base.TestCase):
|
|
"""Tests for Runtime's delegation functionality."""
|
|
|
|
def test_subpolicy(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
policy = 'error(x) :- q(x), r(x)'
|
|
run.insert(policy)
|
|
subpolicy = run.find_subpolicy(
|
|
set(['q']), set(), set(['error', 'warning']))
|
|
e = helper.datalog_equal(subpolicy, policy)
|
|
self.assertTrue(e)
|
|
|
|
def test_subpolicy_multiple(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
policy = ('error(x) :- q(x), r(x) '
|
|
'error(x) :- q(x), s(x) '
|
|
'warning(x) :- t(x), q(x)')
|
|
run.insert(policy)
|
|
subpolicy = run.find_subpolicy(
|
|
set(['q']), set(), set(['error', 'warning']))
|
|
e = helper.datalog_equal(subpolicy, policy)
|
|
self.assertTrue(e)
|
|
|
|
def test_subpolicy_prohibited(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
policy1 = 'error(x) :- q(x), r(x) '
|
|
policy2 = 'error(x) :- q(x), s(x) '
|
|
policy3 = 'error(x) :- q(x), prohibit(x, y) '
|
|
policy4 = 'warning(x) :- t(x), q(x)'
|
|
run.insert(policy1 + policy2 + policy3 + policy4)
|
|
subpolicy = run.find_subpolicy(
|
|
set(['q']), set(['prohibit']), set(['error', 'warning']))
|
|
e = helper.datalog_equal(subpolicy, policy1 + policy2 + policy4)
|
|
self.assertTrue(e)
|
|
|
|
def test_subpolicy_layers(self):
|
|
run = agnostic.Runtime()
|
|
run.create_policy('test')
|
|
policy1 = 'error(x) :- t(x), u(x) '
|
|
policy2 = ' t(x) :- q(x), s(x) '
|
|
policy3 = 'error(x) :- p(x) '
|
|
policy4 = ' p(x) :- prohibit(x, y)'
|
|
policy5 = 'warning(x) :- t(x), q(x)'
|
|
run.insert(policy1 + policy2 + policy3 + policy4 + policy5)
|
|
subpolicy = run.find_subpolicy(
|
|
set(['q']), set(['prohibit']), set(['error', 'warning']))
|
|
e = helper.datalog_equal(subpolicy, policy1 + policy2 + policy5)
|
|
self.assertTrue(e)
|