def _read_and_clear_state(self): if self.CHARM_STATE_FILE.stat().st_size: storage = SQLiteStorage(self.CHARM_STATE_FILE) with (self.JUJU_CHARM_DIR / 'metadata.yaml').open() as m: af = (self.JUJU_CHARM_DIR / 'actions.yaml') if af.exists(): with af.open() as a: meta = CharmMeta.from_yaml(m, a) else: meta = CharmMeta.from_yaml(m) framework = Framework(storage, self.JUJU_CHARM_DIR, meta, None) class ThisCharmEvents(CharmEvents): pass class Charm(self.charm_module.Charm): on = ThisCharmEvents() mycharm = Charm(framework) stored = mycharm._stored # Override the saved data with a cleared state storage.save_snapshot(stored._data.handle.path, {}) storage.commit() framework.close() else: stored = StoredStateData(None, None) return stored
def setUp(self): def restore_env(env): os.environ.clear() os.environ.update(env) self.addCleanup(restore_env, os.environ.copy()) os.environ['PATH'] = "{}:{}".format( Path(__file__).parent / 'bin', os.environ['PATH']) os.environ['JUJU_UNIT_NAME'] = 'local/0' self.tmpdir = Path(tempfile.mkdtemp()) self.addCleanup(shutil.rmtree, str(self.tmpdir)) self.meta = CharmMeta() class CustomEvent(EventBase): pass class TestCharmEvents(CharmEvents): custom = EventSource(CustomEvent) # Relations events are defined dynamically and modify the class attributes. # We use a subclass temporarily to prevent these side effects from leaking. CharmBase.on = TestCharmEvents() def cleanup(): CharmBase.on = CharmEvents() self.addCleanup(cleanup)
def setUp(self): self._path = os.environ['PATH'] os.environ['PATH'] = str(Path(__file__).parent / 'bin') os.environ['JUJU_UNIT_NAME'] = 'local/0' self.tmpdir = Path(tempfile.mkdtemp()) self.addCleanup(shutil.rmtree, self.tmpdir) self.meta = CharmMeta() class CustomEvent(EventBase): pass class TestCharmEvents(CharmEvents): custom = EventSource(CustomEvent) # Relations events are defined dynamically and modify the class attributes. # We use a subclass temporarily to prevent these side effects from leaking. CharmBase.on = TestCharmEvents() def cleanup(): os.environ['PATH'] = self._path del os.environ['JUJU_UNIT_NAME'] CharmBase.on = CharmEvents() self.addCleanup(cleanup)
def test_relation_events(self): class MyCharm(CharmBase): def __init__(self, *args): super().__init__(*args) self.seen = [] for rel in ('req1', 'req-2', 'pro1', 'pro-2', 'peer1', 'peer-2'): # Hook up relation events to generic handler. self.framework.observe(self.on[rel].relation_joined, self.on_any_relation) self.framework.observe(self.on[rel].relation_changed, self.on_any_relation) self.framework.observe(self.on[rel].relation_departed, self.on_any_relation) self.framework.observe(self.on[rel].relation_broken, self.on_any_relation) def on_any_relation(self, event): assert event.relation.name == 'req1' assert event.relation.app.name == 'remote' self.seen.append(type(event).__name__) # language=YAML self.meta = CharmMeta.from_yaml(metadata=''' name: my-charm requires: req1: interface: req1 req-2: interface: req2 provides: pro1: interface: pro1 pro-2: interface: pro2 peers: peer1: interface: peer1 peer-2: interface: peer2 ''') charm = MyCharm(self.create_framework(), None) rel = charm.framework.model.get_relation('req1', 1) unit = charm.framework.model.get_unit('remote/0') charm.on['req1'].relation_joined.emit(rel, unit) charm.on['req1'].relation_changed.emit(rel, unit) charm.on['req-2'].relation_changed.emit(rel, unit) charm.on['pro1'].relation_departed.emit(rel, unit) charm.on['pro-2'].relation_departed.emit(rel, unit) charm.on['peer1'].relation_broken.emit(rel) charm.on['peer-2'].relation_broken.emit(rel) self.assertEqual(charm.seen, [ 'RelationJoinedEvent', 'RelationChangedEvent', 'RelationChangedEvent', 'RelationDepartedEvent', 'RelationDepartedEvent', 'RelationBrokenEvent', 'RelationBrokenEvent', ])
def create_framework(self): framework = Framework(self.tmpdir / "framework.data", self.tmpdir, CharmMeta(), None) # Ensure that the Framework object is closed and cleaned up even # when the test fails or errors out. self.addCleanup(framework.close) return framework
def test_not_if_already_local(self): meta = CharmMeta.from_yaml("series: [kubernetes]") with patch.dict( os.environ, {"JUJU_VERSION": "2.8"}), tempfile.NamedTemporaryFile() as fd: self.assertFalse( _should_use_controller_storage(Path(fd.name), meta)) self.assertLogged('Using local storage: {} already exists'.format( fd.name))
def test_relations_meta_limit_type_validation(self): with self.assertRaisesRegex(TypeError, "limit should be an int, not <class 'str'>"): # language=YAML self.meta = CharmMeta.from_yaml(''' name: my-charm requires: database: interface: mongodb limit: foobar ''')
def create_model(self): """Create a Model object.""" unit_name = 'myapp/0' patcher = patch.dict(os.environ, {'JUJU_UNIT_NAME': unit_name}) patcher.start() self.addCleanup(patcher.stop) backend = _ModelBackend() meta = CharmMeta() model = Model('myapp/0', meta, backend) return model
def test_relations_meta_scope_type_validation(self): with self.assertRaisesRegex(TypeError, "scope should be one of 'global', 'container'; not 'foobar'"): # language=YAML self.meta = CharmMeta.from_yaml(''' name: my-charm requires: database: interface: mongodb scope: foobar ''')
def test_containers(self): meta = CharmMeta.from_yaml(""" name: k8s-charm containers: test1: k: v test2: k: v """) self.assertIsInstance(meta.containers['test1'], ContainerMeta) self.assertIsInstance(meta.containers['test2'], ContainerMeta) self.assertEqual(meta.containers['test1'].name, 'test1') self.assertEqual(meta.containers['test2'].name, 'test2')
class MRRMTestCharm(CharmBase): __name__ = self.app_name on = MRRMTestEvents() meta = CharmMeta({ self.ROLE: { self._default_endpoint: { "role": self.ROLE, "interface": self.INTERFACE, "limit": self.LIMIT, }, }, }) app = harness.model.get_app(self.app_name) unit = harness.model.get_unit(self.unit_name)
def create_framework(self): model = create_autospec(Model) model.unit = create_autospec(Unit) model.unit.is_leader = MagicMock(return_value=False) model.app = create_autospec(Application) model.pod = create_autospec(Pod) model.config = create_autospec(ConfigData) raw_meta = {'provides': {'mongo': {"interface": "mongodb"}}} framework = Framework( self.tmpdir / "framework.data.{}".format(str(uuid4)), self.tmpdir, CharmMeta(raw=raw_meta), model) framework.model.app.name = "test-app" self.addCleanup(framework.close) return framework
def _get_action_test_meta(cls): # language=YAML return CharmMeta.from_yaml(metadata=''' name: my-charm ''', actions=''' foo-bar: description: "Foos the bar." params: foo-name: description: "A foo name to bar" type: string silent: default: false description: "" type: boolean required: foo-bar title: foo-bar start: description: "Start the unit." ''')
def test_relations_meta(self): # language=YAML self.meta = CharmMeta.from_yaml(''' name: my-charm requires: database: interface: mongodb limit: 1 scope: container metrics: interface: prometheus-scraping ''') self.assertEqual(self.meta.requires['database'].interface_name, 'mongodb') self.assertEqual(self.meta.requires['database'].limit, 1) self.assertEqual(self.meta.requires['database'].scope, 'container') self.assertEqual(self.meta.requires['metrics'].interface_name, 'prometheus-scraping') self.assertIsNone(self.meta.requires['metrics'].limit) self.assertEqual(self.meta.requires['metrics'].scope, 'global') # Default value
def test_workload_events(self): class MyCharm(CharmBase): def __init__(self, *args): super().__init__(*args) self.seen = [] self.count = 0 for workload in ('container-a', 'containerb'): # Hook up relation events to generic handler. self.framework.observe( self.on[workload].pebble_ready, self.on_any_pebble_ready) def on_any_pebble_ready(self, event): self.seen.append(type(event).__name__) self.count += 1 # language=YAML self.meta = CharmMeta.from_yaml(metadata=''' name: my-charm containers: container-a: containerb: ''') charm = MyCharm(self.create_framework()) self.assertIn('container_a_pebble_ready', repr(charm.on)) self.assertIn('containerb_pebble_ready', repr(charm.on)) charm.on['container-a'].pebble_ready.emit( charm.framework.model.unit.get_container('container-a')) charm.on['containerb'].pebble_ready.emit( charm.framework.model.unit.get_container('containerb')) self.assertEqual(charm.seen, [ 'PebbleReadyEvent', 'PebbleReadyEvent' ]) self.assertEqual(charm.count, 2)
def test_containers_storage(self): meta = CharmMeta.from_yaml(""" name: k8s-charm storage: data: type: filesystem location: /test/storage other: type: filesystem location: /test/other containers: test1: mounts: - storage: data location: /test/storagemount - storage: other location: /test/otherdata """) self.assertIsInstance(meta.containers['test1'], ContainerMeta) self.assertIsInstance(meta.containers['test1'].mounts["data"], ContainerStorageMeta) self.assertEqual(meta.containers['test1'].mounts["data"].location, '/test/storagemount') self.assertEqual(meta.containers['test1'].mounts["other"].location, '/test/otherdata')
def _get_function_test_meta(cls): return CharmMeta({'name': 'my-charm'}, { 'foo-bar': { 'description': 'Foos the bar.', 'title': 'foo-bar', 'required': 'foo-bar', 'params': { 'foo-name': { 'type': 'string', 'description': 'A foo name to bar', }, 'silent': { 'type': 'boolean', 'description': '', 'default': False, }, }, }, 'start': { 'description': 'Start the unit.' } })
def test_containers_storage_multiple_mounts(self): meta = CharmMeta.from_yaml(""" name: k8s-charm storage: data: type: filesystem location: /test/storage containers: test1: mounts: - storage: data location: /test/storagemount - storage: data location: /test/otherdata """) self.assertIsInstance(meta.containers['test1'], ContainerMeta) self.assertIsInstance(meta.containers['test1'].mounts["data"], ContainerStorageMeta) self.assertEqual( meta.containers['test1'].mounts["data"].locations[0], '/test/storagemount') self.assertEqual(meta.containers['test1'].mounts["data"].locations[1], '/test/otherdata') with self.assertRaises(RuntimeError): meta.containers["test1"].mounts["data"].location
def test_fallback_to_current_juju_version__new_enough(self): meta = CharmMeta.from_yaml("series: [kubernetes]") with patch.dict(os.environ, {"JUJU_VERSION": "2.8"}): self.assertTrue(_should_use_controller_storage(Path("/xyzzy"), meta)) self.assertLogged('Using controller storage: JUJU_VERSION=2.8.0')
def test_storage_events(self): class MyCharm(CharmBase): def __init__(self, *args): super().__init__(*args) self.seen = [] self.framework.observe(self.on['stor1'].storage_attached, self._on_stor1_attach) self.framework.observe(self.on['stor2'].storage_detaching, self._on_stor2_detach) self.framework.observe(self.on['stor3'].storage_attached, self._on_stor3_attach) self.framework.observe(self.on['stor-4'].storage_attached, self._on_stor4_attach) def _on_stor1_attach(self, event): self.seen.append(type(event).__name__) def _on_stor2_detach(self, event): self.seen.append(type(event).__name__) def _on_stor3_attach(self, event): self.seen.append(type(event).__name__) def _on_stor4_attach(self, event): self.seen.append(type(event).__name__) # language=YAML self.meta = CharmMeta.from_yaml(''' name: my-charm storage: stor-4: multiple: range: 2-4 type: filesystem stor1: type: filesystem stor2: multiple: range: "2" type: filesystem stor3: multiple: range: 2- type: filesystem ''') self.assertIsNone(self.meta.storages['stor1'].multiple_range) self.assertEqual(self.meta.storages['stor2'].multiple_range, (2, 2)) self.assertEqual(self.meta.storages['stor3'].multiple_range, (2, None)) self.assertEqual(self.meta.storages['stor-4'].multiple_range, (2, 4)) charm = MyCharm(self.create_framework()) charm.on['stor1'].storage_attached.emit() charm.on['stor2'].storage_detaching.emit() charm.on['stor3'].storage_attached.emit() charm.on['stor-4'].storage_attached.emit() self.assertEqual(charm.seen, [ 'StorageAttachedEvent', 'StorageDetachingEvent', 'StorageAttachedEvent', 'StorageAttachedEvent', ])
def create_model(self): """Create a Model object.""" backend = _ModelBackend(unit_name='myapp/0') meta = CharmMeta() model = Model(meta, backend) return model
def test_empty_action(self): meta = CharmMeta.from_yaml('name: my-charm', '') self.assertEqual(meta.actions, {})
def test_storage_events(self): class MyCharm(CharmBase): def __init__(self, *args): super().__init__(*args) self.seen = [] self.framework.observe(self.on['stor1'].storage_attached, self) self.framework.observe(self.on['stor2'].storage_detaching, self) self.framework.observe(self.on['stor3'].storage_attached, self) self.framework.observe(self.on['stor-4'].storage_attached, self) def on_stor1_storage_attached(self, event): self.seen.append(f'{type(event).__name__}') def on_stor2_storage_detaching(self, event): self.seen.append(f'{type(event).__name__}') def on_stor3_storage_attached(self, event): self.seen.append(f'{type(event).__name__}') def on_stor_4_storage_attached(self, event): self.seen.append(f'{type(event).__name__}') self.meta = CharmMeta({ 'name': 'my-charm', 'storage': { 'stor1': { 'type': 'filesystem' }, 'stor2': { 'type': 'filesystem', 'multiple': { 'range': '2', }, }, 'stor3': { 'type': 'filesystem', 'multiple': { 'range': '2-', }, }, 'stor-4': { 'type': 'filesystem', 'multiple': { 'range': '2-4', }, }, }, }) self.assertIsNone(self.meta.storages['stor1'].multiple_range) self.assertEqual(self.meta.storages['stor2'].multiple_range, (2, 2)) self.assertEqual(self.meta.storages['stor3'].multiple_range, (2, None)) self.assertEqual(self.meta.storages['stor-4'].multiple_range, (2, 4)) charm = MyCharm(self.create_framework(), None) charm.on['stor1'].storage_attached.emit() charm.on['stor2'].storage_detaching.emit() charm.on['stor3'].storage_attached.emit() charm.on['stor-4'].storage_attached.emit() self.assertEqual(charm.seen, [ 'StorageAttachedEvent', 'StorageDetachingEvent', 'StorageAttachedEvent', 'StorageAttachedEvent', ])
def test_not_if_not_in_k8s(self): meta = CharmMeta.from_yaml("series: [ecs]") with patch.dict(os.environ, {"JUJU_VERSION": "2.8"}): self.assertFalse(_should_use_controller_storage(Path("/xyzzy"), meta)) self.assertLogged('Using local storage: not a kubernetes charm')
def test_fallback_to_current_juju_version__too_old(self): meta = CharmMeta.from_yaml("series: [kubernetes]") with patch.dict(os.environ, {"JUJU_VERSION": "1.0"}): self.assertFalse(_should_use_controller_storage(Path("/xyzzy"), meta)) self.assertLogged('Using local storage: JUJU_VERSION=1.0.0')
def test_storage_events(self): this = self class MyCharm(CharmBase): def __init__(self, *args): super().__init__(*args) self.seen = [] self.framework.observe(self.on['stor1'].storage_attached, self._on_stor1_attach) self.framework.observe(self.on['stor2'].storage_detaching, self._on_stor2_detach) self.framework.observe(self.on['stor3'].storage_attached, self._on_stor3_attach) self.framework.observe(self.on['stor-4'].storage_attached, self._on_stor4_attach) def _on_stor1_attach(self, event): self.seen.append(type(event).__name__) this.assertEqual(event.storage.location, Path("/var/srv/stor1/0")) def _on_stor2_detach(self, event): self.seen.append(type(event).__name__) def _on_stor3_attach(self, event): self.seen.append(type(event).__name__) def _on_stor4_attach(self, event): self.seen.append(type(event).__name__) # language=YAML self.meta = CharmMeta.from_yaml(''' name: my-charm storage: stor-4: multiple: range: 2-4 type: filesystem stor1: type: filesystem stor2: multiple: range: "2" type: filesystem stor3: multiple: range: 2- type: filesystem stor-multiple-dashes: multiple: range: 2- type: filesystem ''') fake_script( self, "storage-get", """ if [ "$1" = "-s" ]; then id=${2#*/} key=${2%/*} echo "\\"/var/srv/${key}/${id}\\"" # NOQA: test_quote_backslashes elif [ "$1" = '--help' ]; then printf '%s\\n' \\ 'Usage: storage-get [options] [<key>]' \\ ' ' \\ 'Summary:' \\ 'print information for storage instance with specified id' \\ ' ' \\ 'Options:' \\ '--format (= smart)' \\ ' Specify output format (json|smart|yaml)' \\ '-o, --output (= "")' \\ ' Specify an output file' \\ '-s (= test-stor/0)' \\ ' specify a storage instance by id' \\ ' ' \\ 'Details:' \\ 'When no <key> is supplied, all keys values are printed.' else # Return the same path for all disks since `storage-get` # on attach and detach takes no parameters and is not # deterministically faked with fake_script exit 1 fi """, ) fake_script( self, "storage-list", """ echo '["disks/0"]' """, ) self.assertIsNone(self.meta.storages['stor1'].multiple_range) self.assertEqual(self.meta.storages['stor2'].multiple_range, (2, 2)) self.assertEqual(self.meta.storages['stor3'].multiple_range, (2, None)) self.assertEqual(self.meta.storages['stor-4'].multiple_range, (2, 4)) charm = MyCharm(self.create_framework()) charm.on['stor1'].storage_attached.emit(Storage("stor1", 0, charm.model._backend)) charm.on['stor2'].storage_detaching.emit(Storage("stor2", 0, charm.model._backend)) charm.on['stor3'].storage_attached.emit(Storage("stor3", 0, charm.model._backend)) charm.on['stor-4'].storage_attached.emit(Storage("stor-4", 0, charm.model._backend)) charm.on['stor-multiple-dashes'].storage_attached.emit( Storage("stor-multiple-dashes", 0, charm.model._backend)) self.assertEqual(charm.seen, [ 'StorageAttachedEvent', 'StorageDetachingEvent', 'StorageAttachedEvent', 'StorageAttachedEvent', ])