blob: 600372cfba017a8e0cef83cdc3465281e7fb4472 [file] [log] [blame]
Adam Israelc3e6c2e2018-03-01 09:31:50 -05001import inspect
Adam Israeldcdf82b2017-08-15 15:26:43 -04002import subprocess
3import uuid
Adam Israelb0943662018-08-02 15:32:00 -04004from contextlib import contextmanager
5from pathlib import Path
Adam Israeldcdf82b2017-08-15 15:26:43 -04006
Adam Israelc3e6c2e2018-03-01 09:31:50 -05007import mock
8from juju.client.jujudata import FileJujuData
Adam Israeldcdf82b2017-08-15 15:26:43 -04009from juju.controller import Controller
Adam Israelc3e6c2e2018-03-01 09:31:50 -050010
11import pytest
Adam Israeldcdf82b2017-08-15 15:26:43 -040012
13
14def is_bootstrapped():
Adam Israelb0943662018-08-02 15:32:00 -040015 try:
16 result = subprocess.run(['juju', 'switch'], stdout=subprocess.PIPE)
17 return (
18 result.returncode == 0 and
19 len(result.stdout.decode().strip()) > 0)
20 except FileNotFoundError:
21 return False
22
Adam Israeldcdf82b2017-08-15 15:26:43 -040023
24bootstrapped = pytest.mark.skipif(
25 not is_bootstrapped(),
26 reason='bootstrapped Juju environment required')
27
Adam Israelc3e6c2e2018-03-01 09:31:50 -050028test_run_nonce = uuid.uuid4().hex[-4:]
29
Adam Israeldcdf82b2017-08-15 15:26:43 -040030
31class CleanController():
Adam Israelb0943662018-08-02 15:32:00 -040032 """
33 Context manager that automatically connects and disconnects from
34 the currently active controller.
35
36 Note: Unlike CleanModel, this will not create a new controller for you,
37 and an active controller must already be available.
38 """
Adam Israeldcdf82b2017-08-15 15:26:43 -040039 def __init__(self):
Adam Israelc3e6c2e2018-03-01 09:31:50 -050040 self._controller = None
Adam Israeldcdf82b2017-08-15 15:26:43 -040041
42 async def __aenter__(self):
Adam Israelc3e6c2e2018-03-01 09:31:50 -050043 self._controller = Controller()
44 await self._controller.connect()
45 return self._controller
Adam Israeldcdf82b2017-08-15 15:26:43 -040046
47 async def __aexit__(self, exc_type, exc, tb):
Adam Israelc3e6c2e2018-03-01 09:31:50 -050048 await self._controller.disconnect()
Adam Israeldcdf82b2017-08-15 15:26:43 -040049
50
51class CleanModel():
Adam Israelb0943662018-08-02 15:32:00 -040052 """
53 Context manager that automatically connects to the currently active
54 controller, adds a fresh model, returns the connection to that model,
55 and automatically disconnects and cleans up the model.
56
57 The new model is also set as the current default for the controller
58 connection.
59 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -050060 def __init__(self, bakery_client=None):
61 self._controller = None
62 self._model = None
63 self._model_uuid = None
64 self._bakery_client = bakery_client
Adam Israeldcdf82b2017-08-15 15:26:43 -040065
66 async def __aenter__(self):
Adam Israelc3e6c2e2018-03-01 09:31:50 -050067 model_nonce = uuid.uuid4().hex[-4:]
68 frame = inspect.stack()[1]
69 test_name = frame.function.replace('_', '-')
70 jujudata = TestJujuData()
71 self._controller = Controller(
72 jujudata=jujudata,
73 bakery_client=self._bakery_client,
74 )
75 controller_name = jujudata.current_controller()
76 user_name = jujudata.accounts()[controller_name]['user']
77 await self._controller.connect(controller_name)
Adam Israeldcdf82b2017-08-15 15:26:43 -040078
Adam Israelc3e6c2e2018-03-01 09:31:50 -050079 model_name = 'test-{}-{}-{}'.format(
80 test_run_nonce,
81 test_name,
82 model_nonce,
83 )
84 self._model = await self._controller.add_model(model_name)
85
86 # Change the JujuData instance so that it will return the new
87 # model as the current model name, so that we'll connect
88 # to it by default.
89 jujudata.set_model(
90 controller_name,
91 user_name + "/" + model_name,
92 self._model.info.uuid,
93 )
Adam Israeldcdf82b2017-08-15 15:26:43 -040094
95 # save the model UUID in case test closes model
Adam Israelc3e6c2e2018-03-01 09:31:50 -050096 self._model_uuid = self._model.info.uuid
Adam Israeldcdf82b2017-08-15 15:26:43 -040097
Adam Israelc3e6c2e2018-03-01 09:31:50 -050098 return self._model
Adam Israeldcdf82b2017-08-15 15:26:43 -040099
100 async def __aexit__(self, exc_type, exc, tb):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500101 await self._model.disconnect()
102 await self._controller.destroy_model(self._model_uuid)
103 await self._controller.disconnect()
104
105
106class TestJujuData(FileJujuData):
107 def __init__(self):
108 self.__controller_name = None
109 self.__model_name = None
110 self.__model_uuid = None
111 super().__init__()
112
113 def set_model(self, controller_name, model_name, model_uuid):
114 self.__controller_name = controller_name
115 self.__model_name = model_name
116 self.__model_uuid = model_uuid
117
118 def current_model(self, *args, **kwargs):
119 return self.__model_name or super().current_model(*args, **kwargs)
120
121 def models(self):
122 all_models = super().models()
123 if self.__model_name is None:
124 return all_models
125 all_models.setdefault(self.__controller_name, {})
126 all_models[self.__controller_name].setdefault('models', {})
127 cmodels = all_models[self.__controller_name]['models']
128 cmodels[self.__model_name] = {'uuid': self.__model_uuid}
129 return all_models
Adam Israeldcdf82b2017-08-15 15:26:43 -0400130
131
132class AsyncMock(mock.MagicMock):
133 async def __call__(self, *args, **kwargs):
134 return super().__call__(*args, **kwargs)
Adam Israelb0943662018-08-02 15:32:00 -0400135
136
137@contextmanager
138def patch_file(filename):
139 """
140 "Patch" a file so that its current contents are automatically restored
141 when the context is exited.
142 """
143 filepath = Path(filename).expanduser()
144 data = filepath.read_bytes()
145 try:
146 yield
147 finally:
148 filepath.write_bytes(data)