blob: 97eea530955ea07ec39f4e64a3b55a0a54e6521d [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
Adam Israelc3e6c2e2018-03-01 09:31:50 -050024
Adam Israeldcdf82b2017-08-15 15:26:43 -040025bootstrapped = pytest.mark.skipif(
26 not is_bootstrapped(),
27 reason='bootstrapped Juju environment required')
28
Adam Israelc3e6c2e2018-03-01 09:31:50 -050029test_run_nonce = uuid.uuid4().hex[-4:]
30
Adam Israeldcdf82b2017-08-15 15:26:43 -040031
32class CleanController():
Adam Israelb0943662018-08-02 15:32:00 -040033 """
34 Context manager that automatically connects and disconnects from
35 the currently active controller.
36
37 Note: Unlike CleanModel, this will not create a new controller for you,
38 and an active controller must already be available.
39 """
Adam Israeldcdf82b2017-08-15 15:26:43 -040040 def __init__(self):
Adam Israelc3e6c2e2018-03-01 09:31:50 -050041 self._controller = None
Adam Israeldcdf82b2017-08-15 15:26:43 -040042
43 async def __aenter__(self):
Adam Israelc3e6c2e2018-03-01 09:31:50 -050044 self._controller = Controller()
45 await self._controller.connect()
46 return self._controller
Adam Israeldcdf82b2017-08-15 15:26:43 -040047
48 async def __aexit__(self, exc_type, exc, tb):
Adam Israelc3e6c2e2018-03-01 09:31:50 -050049 await self._controller.disconnect()
Adam Israeldcdf82b2017-08-15 15:26:43 -040050
51
52class CleanModel():
Adam Israelb0943662018-08-02 15:32:00 -040053 """
54 Context manager that automatically connects to the currently active
55 controller, adds a fresh model, returns the connection to that model,
56 and automatically disconnects and cleans up the model.
57
58 The new model is also set as the current default for the controller
59 connection.
60 """
Adam Israelc3e6c2e2018-03-01 09:31:50 -050061 def __init__(self, bakery_client=None):
62 self._controller = None
63 self._model = None
64 self._model_uuid = None
65 self._bakery_client = bakery_client
Adam Israeldcdf82b2017-08-15 15:26:43 -040066
67 async def __aenter__(self):
Adam Israelc3e6c2e2018-03-01 09:31:50 -050068 model_nonce = uuid.uuid4().hex[-4:]
69 frame = inspect.stack()[1]
70 test_name = frame.function.replace('_', '-')
71 jujudata = TestJujuData()
72 self._controller = Controller(
73 jujudata=jujudata,
74 bakery_client=self._bakery_client,
75 )
76 controller_name = jujudata.current_controller()
77 user_name = jujudata.accounts()[controller_name]['user']
78 await self._controller.connect(controller_name)
Adam Israeldcdf82b2017-08-15 15:26:43 -040079
Adam Israelc3e6c2e2018-03-01 09:31:50 -050080 model_name = 'test-{}-{}-{}'.format(
81 test_run_nonce,
82 test_name,
83 model_nonce,
84 )
85 self._model = await self._controller.add_model(model_name)
86
87 # Change the JujuData instance so that it will return the new
88 # model as the current model name, so that we'll connect
89 # to it by default.
90 jujudata.set_model(
91 controller_name,
92 user_name + "/" + model_name,
93 self._model.info.uuid,
94 )
Adam Israeldcdf82b2017-08-15 15:26:43 -040095
96 # save the model UUID in case test closes model
Adam Israelc3e6c2e2018-03-01 09:31:50 -050097 self._model_uuid = self._model.info.uuid
Adam Israeldcdf82b2017-08-15 15:26:43 -040098
Adam Israelc3e6c2e2018-03-01 09:31:50 -050099 return self._model
Adam Israeldcdf82b2017-08-15 15:26:43 -0400100
101 async def __aexit__(self, exc_type, exc, tb):
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500102 await self._model.disconnect()
103 await self._controller.destroy_model(self._model_uuid)
104 await self._controller.disconnect()
105
106
107class TestJujuData(FileJujuData):
108 def __init__(self):
109 self.__controller_name = None
110 self.__model_name = None
111 self.__model_uuid = None
112 super().__init__()
113
114 def set_model(self, controller_name, model_name, model_uuid):
115 self.__controller_name = controller_name
116 self.__model_name = model_name
117 self.__model_uuid = model_uuid
118
119 def current_model(self, *args, **kwargs):
120 return self.__model_name or super().current_model(*args, **kwargs)
121
122 def models(self):
123 all_models = super().models()
124 if self.__model_name is None:
125 return all_models
126 all_models.setdefault(self.__controller_name, {})
127 all_models[self.__controller_name].setdefault('models', {})
128 cmodels = all_models[self.__controller_name]['models']
129 cmodels[self.__model_name] = {'uuid': self.__model_uuid}
130 return all_models
Adam Israeldcdf82b2017-08-15 15:26:43 -0400131
132
133class AsyncMock(mock.MagicMock):
134 async def __call__(self, *args, **kwargs):
135 return super().__call__(*args, **kwargs)
Adam Israelb0943662018-08-02 15:32:00 -0400136
137
138@contextmanager
139def patch_file(filename):
140 """
141 "Patch" a file so that its current contents are automatically restored
142 when the context is exited.
143 """
144 filepath = Path(filename).expanduser()
145 data = filepath.read_bytes()
146 try:
147 yield
148 finally:
149 filepath.write_bytes(data)