""" Tests to verify the boaconstructor functionality. Copyright 2011 Oisin Mulvihill 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. """ import pprint import unittest import boaconstructor from boaconstructor import utils from boaconstructor import Template from boaconstructor.utils import what_is_required class BoaConstructor(unittest.TestCase): def testDerviveFromNotFoundInsideListOfDicts(self): """ """ common = dict(timeout=30) machine = dict( items=[ dict(c='derivefrom.[common]'), ], host='<place holder>', port=12987, timeout='common.$.timeout', ) # Use machine as base and override the host and timeout: test1 = Template( 'test1', { 'This is a comment.': 'derivefrom.[machine]', 'host' : 'example.com', 'timeout': 10, 'beep': False, }, ) result = test1.render(dict(machine=machine, common=common)) correct = dict( host='example.com', port=12987, timeout=10, beep=False, items=[ dict(timeout=30) ] ) # aid visual debug: err_msg = """result != correct result: %s correct: %s """ % (pprint.pformat(result), pprint.pformat(correct)) self.assertEquals(result, correct, err_msg) # Check what_is_required finds the derivefrom references # inside the same dict-in-list situation. # # Use machine as base and override the host and timeout: test1 = Template( 'test1', { 'This is a comment.': 'derivefrom.[something]', 'host' : 'example.com', 'timeout': 10, 'items': [ dict( x='derivefrom.[common]', y=[ dict(x='derivefrom.[bob]') ] ), ], 'beep': False, }, ) correct = {'common':1, 'something':1, 'bob': 1} result = what_is_required(test1) err_msg = """result != correct result: <%s> correct: <%s> """ % (pprint.pformat(result), pprint.pformat(correct)) self.assertEquals(result, correct, err_msg) def testForMissingCoverage(self): """Provide calls to parts of the code nosetests --with-coverage highlighted as missing. """ ddict = dict(a=1) self.assertEquals(utils.get(ddict, 'a'), 1) self.assertRaises(utils.AttributeError, utils.get, ddict, 'b') test1 = Template('test1', dict( a=1 )) self.assertEquals(utils.has(test1, 'a'), True) self.assertEquals(utils.has(test1, 'b'), False) self.assertRaises(boaconstructor.TemplateError, lambda: Template('test1', 'not-a-dict-bad-value')) self.assertEquals(str(test1), "{'a': 1}") def testMultipleDeriveFromNotSupported(self): """Check I can't have more then one derivefrom in a template. """ common = dict(timeout=30) machine = dict( host='<place holder>', port=12987, timeout='common.$.timeout', ) # Use machine as base and override the host and timeout: test1 = Template( 'test1', { '1. a': 'derivefrom.[machine]', '2. b': 'derivefrom.[common]', }, ) self.assertRaises( boaconstructor.utils.MultipleDeriveFromError, test1.render, dict(machine=machine, common=common) ) # Also catch attempts at deriving from non templates, strings, numbers, etc. test1 = Template( 'test1', { 'bad': 'derivefrom.[abc]', }, ) bad_values = [ 1, None, 0, "", "hello there", u"", Exception, object() ] for bad in bad_values: #print "bad: ", bad self.assertRaises( boaconstructor.utils.DeriveFromError, test1.render, dict(abc=bad) ) def testDeriveFrom(self): """Test the derivefrom value and its use to override a 'base' dict. """ common = dict(timeout=30) machine = dict( host='<place holder>', port=12987, timeout='common.$.timeout', ) # Use machine as base and override the host and timeout: test1 = Template( 'test1', { 'This is a comment.': 'derivefrom.[machine]', 'host' : 'example.com', 'timeout': 10, 'beep': False, }, ) result = test1.render(dict(machine=machine, common=common)) correct = dict( host='example.com', port=12987, timeout=10, beep=False ) # aid visual debug: err_msg = """result != correct result: %s correct: %s """ % (pprint.pformat(result), pprint.pformat(correct)) self.assertEquals(result, correct, err_msg) def testDeriveFromNested(self): """Test the derivefrom being used in a nested fashion. """ buffer = dict(a=1, b=2) common = dict(timeout=30, c="buffer.$.a") # Machine derived from common: machine = dict( common='derivefrom.[common]', host='<place holder>', port=12987, ) # Test1 derives from machine: test1 = Template( 'test1', { 'This is a comment.': 'derivefrom.[machine]', 'host' : 'example.com', 'timeout': 10, 'beep': False, 'd': 'buffer.$.b' }, ) result = test1.render(dict( machine=machine, common=common, buffer=buffer )) # And it should look like: correct = dict( host='example.com', port=12987, timeout=10, beep=False, c=1, d=2, ) # aid visual debug: err_msg = """result != correct result: %s correct: %s """ % (pprint.pformat(result), pprint.pformat(correct)) self.assertEquals(result, correct, err_msg) def testAllInclusionOnNonDicts(self): """Test that all -inclusion on non dict simply puts the value the resolves in. """ common = u"some text" test1 = Template( 'test1', { 'description': 'common.*', 'abc' : 'count.*', 'data': 'data.$.a' }, ) result = test1.render( dict( common='some text', count=10, data=dict(a=1) ), ) correct = dict( description='some text', abc=10, data=1, ) # aid visual debug: err_msg = """result != correct result: %s correct: %s """ % (pprint.pformat(result), pprint.pformat(correct)) self.assertEquals(result, correct, err_msg) def testReferencePreview(self): """Test that the top-level references the template need are highlighted by what_is_required. """ test2 = Template( 'test2', dict(host='test1.*', keep='com.$.keep', value="frank.*"), ) correct = {'test1':1, 'com':1, "frank":1} result = what_is_required(test2) err_msg = """result != correct result: <%s> correct: <%s> """ % (pprint.pformat(result), pprint.pformat(correct)) self.assertEquals(result, correct, err_msg) # Try with a list of ref-attr / all inc # test2 = Template( 'test2', dict(host='test1.*', stuff=['com.$.keep', "frank.*"]), ) correct = {'test1':1, 'com':1, "frank":1} result = what_is_required(test2) err_msg = """result != correct result: <%s> correct: <%s> """ % (pprint.pformat(result), pprint.pformat(correct)) self.assertEquals(result, correct, err_msg) # Try with a nested list # test2 = Template( 'test2', dict(host='test1.*', stuff=['com.$.keep', ["frank.*",]]), ) correct = {'test1':1, 'com':1, "frank":1} result = what_is_required(test2) err_msg = """result != correct result: <%s> correct: <%s> """ % (pprint.pformat(result), pprint.pformat(correct)) self.assertEquals(result, correct, err_msg) # Make sure what is required looks at derivefrom commands: test2 = Template( 'test2', {'replace': 'derivefrom.[test1]'}, ) correct = {'test1':1} result = what_is_required(test2) err_msg = """result != correct result: <%s> correct: <%s> """ % (pprint.pformat(result), pprint.pformat(correct)) self.assertEquals(result, correct, err_msg) def testListsAndTemplateIncludes(self): """Test the 'reference.*' which includes all the content of a template in another and its use in lists. """ common = dict(keep='yes', buffer=4096) auth = dict(user='james', secret='11ed394') test1 = Template( 'test1', dict( port=2394, hostname='bob', user='auth.$.user', password='auth.$.secret', ), ) # This includes all of test1 as 'host' at render time. The test1 # template will also require the auth reference to be made available # in order for it to render without Attribute/Reference Errors. # test2 = Template( 'test2', dict(host='test1.*', keep='com.$.keep'), references=dict( com=common, test1=test1, ), ) # simulate only knowing authentication details at run/render time: result = test2.render(dict( auth=auth, )) correct = dict( host=dict( port=2394, hostname='bob', user='james', password='11ed394' ), keep='yes' ) # aid visual debug: err_msg = """result != correct result: %s correct: %s """ % (pprint.pformat(result), pprint.pformat(correct)) self.assertEquals(result, correct, err_msg) # Now, add list of ref-attr or all-inclusions into the mix and see # that these are correctly resolved and replaced. common = dict(keep='yes', buffer=4096) peter = dict(username='pstoppard', secret='11ed394') graham = dict(username='gturner', secret='54jsl31') test1 = Template( 'test1', dict( options='common.*', usernames=['peter.$.username','graham.$.username'], users=['peter.*', 'graham.*'], ), ) result = test1.render( references={ 'common': common, 'peter': peter, 'graham': graham, } ) # I need to sort so it will order as python would order the dicts. u = [ dict(username='pstoppard', secret='11ed394'), dict(username='gturner', secret='54jsl31'), ] u.sort() correct = dict( options=dict(keep='yes', buffer=4096), usernames=['pstoppard', 'gturner'], users=u, ) err_msg = """result != correct result: %s correct: %s """ % (pprint.pformat(result), pprint.pformat(correct)) self.assertEquals(result, correct, err_msg) def testRender(self): """Test the utils module render which is used by the Template class. """ common = dict(keep='yes', buffer=4096) test1 = Template( 'test1', dict(keep='yes', buffer='data.$.buffer', hostname='bob'), references={ 'data':common, } ) test2 = Template( 'test2', dict(buffer='test1.$.buffer', hostname='bob', keep='common.$.keep'), references={ 'test1':test1, } ) # 'Render' test2 into a dict using the internal utils function: state = utils.RenderState( test2, int_refs=test2.references, ext_refs={'common':common} ) result = utils.render(state) correct = dict( buffer=4096, hostname='bob', keep='yes' ) # aid visual debug: err_msg = """result != correct result: %s correct: %s """ % (pprint.pformat(result), pprint.pformat(correct)) self.assertEquals(result, correct, err_msg) def testBuildRefCache(self): """Test the build reference cache generation works as I expect it too. This is used by render when it preprocesses the references. """ common = dict(keep='yes', buffer=4096) test1 = Template( 'test1', dict(keep='yes', buffer='data.$.buffer', hostname='bob'), references={ 'data':common, } ) test2 = Template( 'test2', dict(buffer='test1.$.buffer', hostname='bob', keep='common.$.keep'), references={ 'test1':test1, } ) # The ref cache is a quick lookup used by the resolve process # to find references and child references. This is important # when finding reference-to-reference attributes # int_refs = { "test1":test1 } ext_refs = { "common": common } # This should correctly identify data test1 refers to.as an internal # reference: # # old approach: utils.build_ref_cache(int_refs, ext_refs) # new approach via render state: state = utils.RenderState({}, int_refs, ext_refs) result = state.referenceCache correct = { 'int': { 'test1':test1, 'data': common }, 'ext': { 'common': common, } } # aid visual debug: err_msg = """result != correct result: %s correct: %s """ % (pprint.pformat(result), pprint.pformat(correct)) self.assertEquals(result, correct, err_msg) def testHasGet(self): """Test the special reference-attribute has/getter. """ ref = dict(abc=123) att = 'abc' result = True self.assertEquals(utils.has(ref, att), result) self.assertEquals(utils.get(ref, att), 123) ref = Template('test', dict(abc=123)) att = 'abc' result = True self.assertEquals(utils.has(ref, att), result) self.assertEquals(utils.get(ref, att), 123) class Data: abc = 123 ref = Data att = 'abc' result = True self.assertEquals(utils.has(ref, att), result) self.assertEquals(utils.get(ref, att), 123) class Data: def __init__(self): self.abc = 123 ref = Data() att = 'abc' result = True self.assertEquals(utils.has(ref, att), result) self.assertEquals(utils.get(ref, att), 123) def testReferenceResolving(self): """Test the resolution of refrence,attributes. """ # Test reference not found: ref = "common" att = "" int_refs = dict() ext_refs = dict() self.assertRaises( utils.ReferenceError, utils.resolve_references, ref, att, int_refs, ext_refs ) # Test attribute not found: ref = "common" att = "hostname" int_refs = dict(common=dict(interface='1.2.3.4')) ext_refs = dict() self.assertRaises( utils.AttributeError, utils.resolve_references, ref, att, int_refs, ext_refs ) ref = "common" att = "hostname" int_refs = dict() ext_refs = dict(common=dict(interface='1.2.3.4')) self.assertRaises( utils.AttributeError, utils.resolve_references, ref, att, int_refs, ext_refs ) # Test attribute found in reference: ref = "common" att = "hostname" int_refs = dict() ext_refs = dict(common=dict(hostname='example.com')) results = utils.resolve_references(ref, att, int_refs, ext_refs) correct = "example.com" self.assertEquals(results, correct) # Dots are allowed in references, nothing is done yet with this. ref = "machines.common" att = "hostname" int_refs = {'machines.common': dict(hostname='example.com')} ext_refs = {} results = utils.resolve_references(ref, att, int_refs, ext_refs) correct = "example.com" self.assertEquals(results, correct) # Test ext_refs priority over int_refs: ref = "common" att = "hostname" int_refs = dict(common=dict(hostname='example.com')) ext_refs = dict(common=dict(hostname='localhost')) results = utils.resolve_references(ref, att, int_refs, ext_refs) correct = "localhost" self.assertEquals(results, correct) # Test all inclusion reference resolving i.e. attribute is None # and only the reference is important. # ref = "common" att = None int_refs = dict(common=dict(hostname='example.com')) ext_refs = dict(common=dict(hostname='localhost')) results = utils.resolve_references(ref, att, int_refs, ext_refs) correct = dict(hostname='localhost') self.assertEquals(results, correct) ref = "common" att = None int_refs = dict(common=dict(hostname='example.com')) ext_refs = {} results = utils.resolve_references(ref, att, int_refs, ext_refs) correct = dict(hostname='example.com') self.assertEquals(results, correct) def testMixedDictTemplateAndInstanceResolution(self): """Test the mixed use of dicts, template instances and class instances as references. This also tests basic recursive resolution. """ class SomeData(object): def __init__(self): self.packet_size = 2048 self.live = True somedata = SomeData() # Set up common point at somedata. This will need recursion in host2 # as it will refere to common.$.buffer. # common = Template('common', { "timeout": 42, "buffer": 'data.$.packet_size', }, references=dict(data=somedata), ) # Render the 'host1' dict which uses dict+instance references. This # doesn't need recursion as no reference refers to another. # host1 = Template('host1', { "host": "1.2.3.4", "flag": False, "timeout": 'common.$.timeout', "packet_size": 'data.$.packet_size', "live": 'data.$.live', }, references = { 'common':common, 'data': somedata, } ) result = host1.render() correct = { "host":"1.2.3.4","flag":False,"timeout":42, "packet_size":somedata.packet_size, "live": somedata.live, } for key in correct: self.assertEquals(result[key], correct[key]) # Render 'host2' which uses mixed dict+template. Recursion will also # be needed to work out that host.$.packet size comes from somedata i.e. # host1 -> common -> somedata. In the final rendered host2 result buffer # should be 2048 # host2 = Template('host2', { "host": "4.3.2.1", "flag": 'host.$.flag', "timeout": 'common.$.timeout', "buffer": 'common.$.buffer', }, references = { 'common': common, 'host': host1, } ) result = host2.render() self.assertEquals(result['host'], '4.3.2.1') self.assertEquals(result['flag'], False) self.assertEquals(result['timeout'], 42) # This should be a value and not a reference: self.assertEquals(result['buffer'], 2048) def testValueParsing(self): """Test parsing a dict entries value to recover the reference and attribute. """ # Test entries that are not reference-attribute values: # ignore = [ 1, None, '1', '', 'bob', object(), 'abc.efg', 1.02, Exception, dir(), "abc$efg", "abc$.efg", "abc.$efg", 'bob*','bob.', '.*', 'abc.*.stuff' ] for value in ignore: correct = dict(found=None,reference='',attribute='',allfrom='') result = utils.parse_value(value) self.assertEquals(result, correct) # Now check the recovery of valid reference-attributes: value = 'abc.$.efg' correct = dict(found='refatt',reference='abc',attribute='efg',allfrom='') result = utils.parse_value(value) self.assertEquals(result, correct) value = 'settings.host.$.timeout' correct = dict(found='refatt',reference='settings.host',attribute='timeout',allfrom='') result = utils.parse_value(value) self.assertEquals(result, correct) # Now try all inclusion entries: # value = 'settings.host.*' correct = dict(found='all',reference='',attribute='',allfrom='settings.host') result = utils.parse_value(value) self.assertEquals(result, correct) value = 'abc.*' correct = dict(found='all',reference='',attribute='',allfrom='abc') result = utils.parse_value(value) self.assertEquals(result, correct) def testBasicExampleUsage(self): """Test the reference lookup and basic template render used as an example. """ # Data to be used in host1 and host2 common = Template('common', { "timeout": 42 }) # Uses data from common: host1 = Template('host1', { "host": "1.2.3.4", "flag": False, "timeout": 'common.$.timeout' }, references = {'common':common} ) # Uses data from common and host1 host2 = Template('host2', { "host": "4.3.2.1", "flag": 'host.$.flag', "timeout": 'common.$.timeout', }, ) # Render the 'host1' dict: result = host1.render() correct = {"host":"1.2.3.4","flag":False,"timeout":42} self.assertEquals(result, correct) # Render the 'host2' dict: result = host2.render( references = { 'common': common, 'host': host1, } ) correct = {"host":"4.3.2.1","flag":False,"timeout":42} self.assertEquals(result, correct)