def test_positional_args_padding_multiple_atoms(self): """Test positional args padding on a single atom.""" def check(code, correct, msg, no_theory=False): actual = compile.parse1( code).eliminate_column_references_and_pad_positional( {} if no_theory else theories) eq = helper.datalog_same(str(actual), correct) self.assertTrue(eq, msg) run = agnostic.Runtime() run.create_policy('nova') schema = compile.Schema({'q': ('id', 'name', 'status'), 'r': ('id', 'age', 'weight')}) theories = {'nova': self.SchemaWrapper(schema)} # Multiple atoms, no shared variable code = ("p(x) :- nova:q(x, y), nova:r(w)") correct = "p(x) :- nova:q(x, y, z0), nova:r(w, y0, y1)" check(code, correct, 'Multiple atoms') # Multiple atoms, some shared variable code = ("p(x) :- nova:q(x, y), nova:r(x)") correct = "p(x) :- nova:q(x, y, z0), nova:r(x, y0, y1)" check(code, correct, 'Multiple atoms') # Multiple atoms, same table code = ("p(x) :- nova:q(x, y), nova:q(x)") correct = "p(x) :- nova:q(x, y, z0), nova:q(x, w0, w1)" check(code, correct, 'Multiple atoms, same table')
def test_column_references_multiple_atoms(self): """Test column references occurring in multiple atoms in a rule.""" def check(code, correct, msg): actual = compile.parse1(code).eliminate_column_references(theories) eq = helper.datalog_same(str(actual), correct) self.assertTrue(eq, msg) run = agnostic.Runtime() run.create_policy('nova') schema = compile.Schema({ 'q': ('id', 'name', 'status'), 'r': ('id', 'age', 'weight') }) theories = {'nova': self.SchemaWrapper(schema)} # Multiple atoms code = ("p(x) :- nova:q(id=x, 2=y), nova:r(id=x)") correct = "p(x) :- nova:q(x, x0, y), nova:r(x, y0, y1)" check(code, correct, 'Multiple atoms') # Multiple atoms sharing column name but different variables code = ("p(x) :- nova:q(id=x), nova:r(id=y)") correct = "p(x) :- nova:q(x, x0, x1), nova:r(y, y0, y1)" check(code, correct, 'Multiple atoms shared column name') # Multiple atoms, same table code = ("p(x) :- nova:q(id=x, 2=y), nova:q(id=x)") correct = "p(x) :- nova:q(x, x0, y), nova:q(x, y0, y1)" check(code, correct, 'Multiple atoms, same table')
def test_policy_data(self): """Test policy properly inserts data and processes it normally.""" cage = congress.dse.d6cage.d6Cage() cage.loadModule( "TestDriver", helper.data_module_path("../tests/datasources/test_driver.py")) cage.loadModule("TestPolicy", helper.policy_module_path()) cage.createservice(name="data", moduleName="TestDriver", args=helper.datasource_openstack_args()) cage.createservice(name="policy", moduleName="TestPolicy", args={ 'd6cage': cage, 'rootdir': '' }) data = cage.services['data']['object'] policy = cage.services['policy']['object'] # turn off module-schema syntax checking policy.create_policy('data') policy.set_schema('data', compile.Schema({'p': (1, )})) policy.subscribe('data', 'p', callback=policy.receive_data) formula = policy.parse1('p(1)') # sending a single Insert. (Default for Event is Insert.) data.publish('p', [agnostic.Event(formula)]) helper.retry_check_db_equal(policy, 'data:p(x)', 'data:p(1)')
def test_inject(self): theory = mock.MagicMock(spec=topdown.TopDownTheory) world = {'t': theory} theory.name = 't' theory.schema = ast.Schema() theory.schema.map['l'] = [mkc('Int'), mkc('Int')] theory.select.return_value = ast.parse('l(1,2). l(3,4). l(5,6)') context = z3theory.Z3Context() # An external theory world context.theories = world # inject the declaration of external relation without rules param_types = [ context.type_registry.get_type(typ) for typ in ['Int', 'Int', 'Bool'] ] relation = z3.Function('t:l', *param_types) context.context.register_relation(relation) context.relations['t:l'] = relation # the test context.inject('t', 'l') rules = context.context._rules # pylint: disable=E1101 self.assertIs(True, all(r.decl().name() == 't:l' for r in rules)) self.assertEqual([[1, 2], [3, 4], [5, 6]], sorted([[c.as_long() for c in r.children()] for r in rules]))
def test_policy_table_publish(self): """Policy table result publish Test basic DSE functionality with policy engine and table result publish. """ node = helper.make_dsenode_new_partition('testnode') data = fake_datasource.FakeDataSource('data') policy = agnostic.DseRuntime('policy') policy2 = agnostic.DseRuntime('policy2') node.register_service(data) node.register_service(policy) node.register_service(policy2) policy.synchronizer = mock.MagicMock() policy2.synchronizer = mock.MagicMock() policy.create_policy('data', kind=datalog_base.DATASOURCE_POLICY_TYPE) policy.create_policy('classification') policy.set_schema('data', compile.Schema({'q': (1, )})) policy.insert('p(x):-data:q(x),gt(x,2)', target='classification') policy.insert('q(3)', target='data') # TODO(ekcs): test that no publish triggered (because no subscribers) policy2.create_policy('policy') policy2.subscribe('policy', 'classification:p') helper.retry_check_function_return_value( lambda: 'classification:p' in policy. _published_tables_with_subscriber, True) self.assertEqual(list(policy.policySubData.keys()), [('p', 'classification', None)]) helper.retry_check_db_equal(policy2, 'policy:classification:p(x)', 'policy:classification:p(3)') policy.insert('q(4)', target='data') helper.retry_check_db_equal(policy2, 'policy:classification:p(x)', ('policy:classification:p(3)' ' policy:classification:p(4)')) # test that no change to p means no publish triggered policy.insert('q(2)', target='data') # TODO(ekcs): test no publish triggered policy.delete('q(4)', target='data') helper.retry_check_db_equal(policy2, 'policy:classification:p(x)', 'policy:classification:p(3)') policy2.unsubscribe('policy', 'classification:p') # trigger removed helper.retry_check_function_return_value( lambda: len(policy._published_tables_with_subscriber) == 0, True) self.assertEqual(list(policy.policySubData.keys()), []) policy.insert('q(4)', target='data') # TODO(ekcs): test that no publish triggered (because no subscribers) node.stop()
def test_communication(self): """Test for communication. Test the module's ability to be loaded into the DSE by checking its ability to communicate on the message bus. """ cage = d6cage.d6Cage() # Create modules. # Turn off polling so we don't need to deal with real data. args = helper.datasource_openstack_args() args['poll_time'] = 0 cage.loadModule("NovaDriver", helper.data_module_path("nova_driver.py")) cage.loadModule("PolicyDriver", helper.policy_module_path()) cage.createservice(name="policy", moduleName="PolicyDriver", args={ 'd6cage': cage, 'rootdir': helper.data_module_path('') }) cage.createservice(name="nova", moduleName="NovaDriver", args=args) # Check that data gets sent from nova to policy as expected nova = cage.service_object('nova') policy = cage.service_object('policy') policy.debug_mode() policy.create_policy('nova') policy.set_schema('nova', compile.Schema({'server': (1, )})) policy.subscribe('nova', 'server', callback=policy.receive_data) # publishing is slightly convoluted b/c deltas are computed # automatically. (Not just convenient--useful so that DSE # properly handles the initial state problem.) # Need to set nova.state and nova.prior_state and then publish # anything. # publish server(1), server(2), server(3) helper.retry_check_subscribers(nova, [(policy.name, 'server')]) nova.prior_state = {} nova.state['server'] = set([(1, ), (2, ), (3, )]) nova.publish('server', None) helper.retry_check_db_equal( policy, 'nova:server(x)', 'nova:server(1) nova:server(2) nova:server(3)') # publish server(1), server(4), server(5) nova.prior_state['server'] = nova.state['server'] nova.state['server'] = set([(1, ), (4, ), (5, )]) nova.publish('server', None) helper.retry_check_db_equal( policy, 'nova:server(x)', 'nova:server(1) nova:server(4) nova:server(5)')
def test_rule_api_model_extended(self): """Test extended rule syntax.""" api = self.api engine = self.engine engine.set_schema('nova', compile.Schema({'q': ("name", "status", "year")})) # insert/retrieve rule with column references # just testing that no errors are thrown--correctness tested elsewhere # Assuming that api-models are pass-throughs to functionality context = {'policy_id': engine.DEFAULT_THEORY} (id1, rule) = api['rule'].add_item({'rule': 'p(x) :- nova:q(name=x)'}, {}, context=context) api['rule'].get_item(id1, {}, context=context)
def test_schema_columns(self): test_schema = compile.Schema({ 'p': (1, 2, 3), 'q': ({'name': 'a', 'type': 'Str'}, {'name': 'b', 'nullable': False})}, complete=True) self.assertEqual(test_schema.columns('p'), [1, 2, 3]) self.assertEqual(test_schema.columns('q'), ['a', 'b']) self.assertEqual([(data_types.Scalar, True), (data_types.Scalar, True), (data_types.Scalar, True)], test_schema.types('p')) self.assertEqual([(data_types.Str, True), (data_types.Scalar, False)], test_schema.types('q'))
def test_column_references_validation_errors(self): """Test invalid column references occurring in a single atom.""" schema = compile.Schema({'q': ('id', 'name', 'status'), 'r': ('id', 'age', 'weight')}, complete=True) theories = {'nova': self.SchemaWrapper(schema)} def check_err(rule, errmsg, msg): rule = compile.parse1(rule) try: rule.eliminate_column_references_and_pad_positional(theories) self.fail("Failed to throw error {}".format(errmsg)) except (exception.PolicyException, exception.IncompleteSchemaException) as e: emsg = "Err messages '{}' should include '{}'".format( str(e), errmsg) self.assertIn(errmsg, str(e), msg + ": " + emsg) check_err( 'p(x) :- nova:missing(id=x)', 'uses unknown table missing', 'Unknown table') check_err( 'p(x) :- nova:q(id=x, birthday=y)', 'column name birthday does not exist', 'Unknown column name') check_err( 'p(x) :- nova:q(4=y)', 'column index 4 is too large', 'Large column number') check_err( 'p(x) :- nova:q(id=x, 0=y)', 'index 0 references column id, which is also referenced by name', 'Conflict between name and number references') check_err( 'p(x) :- nova:q(x, y, id=z)', 'already provided by position', 'Conflict between name and position') theories = {} check_err( 'p(x) :- nova:missing(id=x)', 'schema is unknown', 'Missing schema')
def test_eliminate_column_references_body_order(self): """Test eliminate_column_references preserves order insensitivity.""" run = agnostic.Runtime() run.create_policy('nova') schema = compile.Schema({'q': ('id', 'name', 'status'), 'r': ('id', 'age', 'weight')}) theories = {'nova': self.SchemaWrapper(schema)} rule1 = compile.parse1( "p(x) :- nova:q(id=x, 2=y), nova:r(id=x)" ).eliminate_column_references_and_pad_positional(theories) rule2 = compile.parse1( "p(x) :- nova:r(id=x), nova:q(id=x, 2=y)" ).eliminate_column_references_and_pad_positional(theories) self.assertEqual(rule1, rule2, 'eliminate_column_references failed to ' 'preserve order insensitivity')
def test_policy_tables(self): """Test basic DSE functionality with policy engine and the API.""" cage = congress.dse.d6cage.d6Cage() cage.loadModule( "TestDriver", helper.data_module_path("../tests/datasources/test_driver.py")) cage.loadModule("TestPolicy", helper.policy_module_path()) cage.createservice(name="data", moduleName="TestDriver", args=helper.datasource_openstack_args()) # using regular testdriver as API for now cage.createservice(name="api", moduleName="TestDriver", args=helper.datasource_openstack_args()) cage.createservice(name="policy", moduleName="TestPolicy", args={ 'd6cage': cage, 'rootdir': '' }) data = cage.services['data']['object'] api = cage.services['api']['object'] policy = cage.services['policy']['object'] policy.create_policy('data') policy.set_schema('data', compile.Schema({'q': (1, )})) policy.subscribe('api', 'policy-update', callback=policy.receive_policy_update) # simulate API call for insertion of policy statements formula = policy.parse1('p(x) :- data:q(x)') api.publish('policy-update', [agnostic.Event(formula)]) helper.retry_check_nonempty_last_policy_change(policy) # simulate data source publishing to q formula = policy.parse1('q(1)') data.publish('q', [agnostic.Event(formula)]) helper.retry_check_db_equal(policy, 'data:q(x)', 'data:q(1)') # check that policy did the right thing with data e = helper.db_equal(policy.select('p(x)'), 'p(1)') self.assertTrue(e, 'Policy insert') # check that publishing into 'p' does not work formula = policy.parse1('p(3)') data.publish('p', [agnostic.Event(formula)]) # can't actually check that the update for p does not arrive # so instead wait a bit and check helper.pause() e = helper.db_equal(policy.select('p(x)'), 'p(1)') self.assertTrue(e, 'Policy non-insert')
def test_positional_args_padding_atom(self): """Test positional args padding on a single atom.""" def check_err(rule, errmsg, msg): rule = compile.parse1(rule) try: rule.eliminate_column_references_and_pad_positional(theories) self.fail("Failed to throw error {}".format(errmsg)) except (exception.PolicyException, exception.IncompleteSchemaException) as e: emsg = "Err messages '{}' should include '{}'".format( str(e), errmsg) self.assertIn(errmsg, str(e), msg + ": " + emsg) def check(code, correct, msg, no_theory=False): actual = compile.parse1( code).eliminate_column_references_and_pad_positional( {} if no_theory else theories) eq = helper.datalog_same(str(actual), correct) self.assertTrue(eq, msg) run = agnostic.Runtime() run.create_policy('nova') schema = compile.Schema({'q': ('id', 'name', 'status')}) theories = {'nova': self.SchemaWrapper(schema)} # Too few positional args code = ("p(x) :- nova:q(w, y)") correct = "p(x) :- nova:q(w, y, x3)" check(code, correct, 'Too few positional args') code = ("p(x) :- nova:q(w)") correct = "p(x) :- nova:q(w, y, x3)" check(code, correct, 'Too few positional args') code = ("p(x) :- nova:q()") correct = "p(x) :- nova:q(w, y, x3)" check(code, correct, 'Too few (no) positional args') # No schema provided, no change code = ("p(x) :- nova:q(w, y)") correct = "p(x) :- nova:q(w, y)" check(code, correct, 'No schema provided', True) code = ("p(x) :- nova:q(w, x, y, z)") correct = "p(x) :- nova:q(w, x, y, z)" check(code, correct, 'No schema provided', True)
def __init__(self, name=None, abbr=None, schema=None, theories=None, desc=None, owner=None): super(NonrecursiveRuleTheory, self).__init__(name=name, abbr=abbr, theories=theories, schema=schema, desc=desc, owner=owner) # dictionary from table name to list of rules with that table in head self.rules = ruleset.RuleSet() self.kind = base.NONRECURSIVE_POLICY_TYPE if schema is None: self.schema = compile.Schema()
def __init__(self, name=None, abbr=None, schema=None, theories=None, desc=None, owner=None): super(Z3Theory, self).__init__(name=name, abbr=abbr, theories=theories, schema=ast.Schema() if schema is None else schema, desc=desc, owner=owner) LOG.info('z3theory: create %s', name) self.kind = base.Z3_POLICY_TYPE self.rules = ruleset.RuleSet() self.dirty = False self.z3context = None Z3Context.get_context().register(self)
def __init__(self, name=None, abbr=None, schema=None, theories=None, desc=None, owner=None): super(NonrecursiveRuleTheory, self).__init__(name=name, abbr=abbr, theories=theories, schema=schema, desc=desc, owner=owner) # dictionary from table name to list of rules with that table in head self.rules = ruleset.RuleSet() self.kind = base.NONRECURSIVE_POLICY_TYPE if schema is None: self.schema = compile.Schema() # Indicates that a rule was added/removed # Used by the compiler to know if a theory should be recompiled. self.dirty = False
def test_module_schemas(self): """Test that rules are properly checked against module schemas.""" run = agnostic.Runtime() run.create_policy('mod1') run.create_policy('mod2') run.set_schema('mod1', compile.Schema({ 'p': (1, 2, 3), 'q': (1, ) }), complete=True) run.set_schema('mod2', compile.Schema({ 'p': (1, ), 'q': (1, 2) }), complete=True) def check_err(code_string, theory, emsg, msg, f=compile.rule_errors): rule = compile.parse1(code_string) errs = f(rule, run.theory, theory) self.assertTrue( any(emsg in str(err) for err in errs), msg + ":: Failed to find error message '" + emsg + "' in: " + ";".join(str(e) for e in errs)) # no errors rule = compile.parse1('p(x) :- q(x), mod1:p(x, y, z), mod2:q(x, y), ' 'mod1:q(t), mod2:p(t)') errs = compile.rule_errors(rule, run.theory) self.assertEqual(len(errs), 0, "Should not have found any errors") # unknown table within module check_err('p(x) :- q(x), mod1:r(x), r(x)', 'mod3', 'unknown table', 'Unknown table for rule') # wrong number of arguments check_err('p(x) :- q(x), mod1:p(x,y,z,w), r(x)', 'mod3', 'only 3 arguments are permitted', 'Wrong number of arguments for rule') # same tests for an atom # no errors atom = compile.parse1('p(1, 2, 2)') errs = compile.fact_errors(atom, run.theory, 'mod1') self.assertEqual(len(errs), 0, "Should not have found any errors") # unknown table within module check_err('r(1)', 'mod1', 'unknown table', 'Unknown table for atom', f=compile.fact_errors) # wrong number of arguments check_err('p(1, 2, 3, 4)', 'mod1', 'only 3 arguments are permitted', 'Wrong number of arguments for atom', f=compile.fact_errors) # schema update schema = compile.Schema() rule1 = compile.parse1('p(x) :- q(x, y)') change1 = schema.update(rule1.head, True) rule2 = compile.parse1('p(x) :- r(x, y)') change2 = schema.update(rule2.head, True) self.assertEqual(schema.count['p'], 2) schema.revert(change2) self.assertEqual(schema.count['p'], 1) schema.revert(change1) self.assertEqual('p' in schema.count, False) schema.update(rule1.head, True) schema.update(rule2.head, True) change1 = schema.update(rule1.head, False) change2 = schema.update(rule2.head, False) self.assertEqual('p' in schema.count, False) schema.revert(change2) self.assertEqual(schema.count['p'], 1) schema.revert(change1) self.assertEqual(schema.count['p'], 2)
def test_column_references_atom(self): """Test column references occurring in a single atom in a rule.""" def check(code, correct, msg): actual = compile.parse1(code).eliminate_column_references(theories) eq = helper.datalog_same(str(actual), correct) self.assertTrue(eq, msg) run = agnostic.Runtime() run.create_policy('nova') schema = compile.Schema({'q': ('id', 'name', 'status')}) theories = {'nova': self.SchemaWrapper(schema)} # Multiple column names code = ("p(x) :- nova:q(id=x, status=y)") correct = "p(x) :- nova:q(x, w, y)" check(code, correct, 'Multiple column names') # Multiple column numbers code = ("p(x) :- nova:q(0=x, 1=y, 2=z)") correct = "p(x) :- nova:q(x, y, z)" check(code, correct, 'Multiple column numbers') # Mix column names and numbers code = ("p(x) :- nova:q(id=x, 2=y)") correct = "p(x) :- nova:q(x, w, y)" check(code, correct, 'Mix names and numbers') # Object constants code = ("p(x) :- nova:q(id=3, 2=2)") correct = "p(x) :- nova:q(3, w, 2)" check(code, correct, 'Object constants') # Out of order code = ("p(x, y) :- nova:q(status=y, id=x)") correct = "p(x, y) :- nova:q(x, z, y)" check(code, correct, 'Out of order') # Out of order with numbers code = ("p(x, y) :- nova:q(1=y, 0=x)") correct = "p(x, y) :- nova:q(x, y, z)" check(code, correct, 'Out of order with numbers') # Positional plus named code = ("p(x, y) :- nova:q(x, status=y)") correct = "p(x, y) :- nova:q(x, z, y)" check(code, correct, 'Positional plus named') # Positional plus named 2 code = ("p(x, y, z) :- nova:q(x, y, 2=z)") correct = "p(x, y, z) :- nova:q(x, y, z)" check(code, correct, 'Positional plus named 2') # Pure positional (different since we are providing schema) code = ("p(x, y, z) :- nova:q(x, y, z)") correct = "p(x, y, z) :- nova:q(x, y, z)" check(code, correct, 'Pure positional') # Pure positional (without schema) code = ("p(x) :- nova:q(x, y, z)") run.delete_policy('nova') correct = "p(x) :- nova:q(x, y, z)" check(code, correct, 'Pure positional without schema')
def __init__(self, name, theories): super(MinTheory, self).__init__(name=name, theories=theories) self.rules = ruleset.RuleSet() self.schema = ast.Schema()
def test_schema(self): th = nonrecursive.NonrecursiveRuleTheory(name='alice') th.schema = compile.Schema({'p': ('id', 'status', 'name')}) self.assertEqual(th.arity('p'), 3) self.assertEqual(th.arity('alice:p'), 3)