Merge ~hloeung/content-cache-charm:master into content-cache-charm:master
- Git
- lp:~hloeung/content-cache-charm
- master
- Merge into master
Status: | Merged |
---|---|
Approved by: | Haw Loeung |
Approved revision: | edb4467b113921a80de960c33be4096c73febd89 |
Merged at revision: | 6dc158dda107948ed5c41d42dbfe81a82f59b66d |
Proposed branch: | ~hloeung/content-cache-charm:master |
Merge into: | content-cache-charm:master |
Diff against target: |
689 lines (+512/-32) 16 files modified
Makefile (+2/-2) README.md (+9/-0) config.yaml (+12/-0) dev/null (+0/-18) layer.yaml (+7/-1) lib/nginx.py (+83/-0) metadata.yaml (+8/-4) pytest.ini (+6/-0) reactive/content_cache.py (+86/-0) tests/functional/test_content_cache.py (+5/-5) tests/unit/files/nginx_config_rendered_test_output-site1.local.txt (+19/-0) tests/unit/files/nginx_config_rendered_test_output-site2.local.txt (+14/-0) tests/unit/files/nginx_config_test_config.txt (+25/-0) tests/unit/test_content_cache.py (+152/-0) tests/unit/test_nginx.py (+83/-0) tox.ini (+1/-2) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stuart Bishop (community) | Approve | ||
Haw Loeung | Needs Resubmitting | ||
Review via email: mp+363822@code.launchpad.net |
Commit message
Initial charm
Description of the change
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Haw Loeung (hloeung) : | # |
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change cannot be self approved, setting status to needs review.
Stuart Bishop (stub) wrote : | # |
This all looks good, and nice to see someone chasing complete test coverage.
I think I found two bugs, commented on inline, but neither dificult to fix.
The rest is stylistic stuff. Most of them are just suggestions on alternative unittest.mock asserts; it is a very fat API and the docs take some getting used to.
I do think it is worth adding layer:status to your layer.yaml, as it will make the charm easier to maintain and, since this is a rare example of a tested charm, avoids some false positives in the tests. Particularly, there will be no need to ensure that an important status like 'blocked' was the last status set, and instead we can rely on layer:status ensuring that the important status is presented to the user when the hook finishes.
In general, I think it is its better for tests to focus on particular important results and only test those, rather than that the implementation exactly matches what the test expects. A good test is one where the implementation can be changed, and if the results are the same, the test passes unmodified. Mocks don't make this easy, and it is worth thinking about if you are best off with exact matches (eg. these flags where set, and only these flags, and in this order), or less fragile tests (eg. these flags were set, and maybe some others, and order doesn't matter).
Assuming this is still a work in progress, it is worth creating a new branch. Future merge proposals can use this branch as the dependent branch, and only the changes displayed for review.
Haw Loeung (hloeung) wrote : | # |
First round of fixes. Will continue with more.
Haw Loeung (hloeung) wrote : | # |
Second found of fixes done.
Haw Loeung (hloeung) wrote : | # |
Another round of fixes.
Haw Loeung (hloeung) : | # |
Haw Loeung (hloeung) wrote : | # |
Last round of fixes to address all of Stuart's points.
Haw Loeung (hloeung) : | # |
Stuart Bishop (stub) wrote : | # |
Yup, all good.
You may be able to replace the status reset_mocks with a single one, doing charms.
It would be possible to target the status mock by installing the MagicMock at charms.layer.status instead of charms.layer. However, what you have may be more useful as pretty much anything that comes from charms.layer will need to be mocked.
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change successfully merged at revision 6dc158dda107948
Preview Diff
1 | diff --git a/Makefile b/Makefile |
2 | index cbd8b5c..e592bf7 100644 |
3 | --- a/Makefile |
4 | +++ b/Makefile |
5 | @@ -23,8 +23,8 @@ functionaltest: |
6 | |
7 | clean: |
8 | @echo "Cleaning files" |
9 | - @if [ -d ./.tox ] ; then rm -r ./.tox ; fi |
10 | - @if [ -d ./.pytest_cache ] ; then rm -r ./.pytest_cache ; fi |
11 | + @rm -rf ./.tox |
12 | + @rm -rf ./.pytest_cache |
13 | |
14 | # The targets below don't depend on a file |
15 | .PHONY: lint test unittest functionaltest clean help |
16 | diff --git a/README.md b/README.md |
17 | index e69de29..004a714 100644 |
18 | --- a/README.md |
19 | +++ b/README.md |
20 | @@ -0,0 +1,9 @@ |
21 | +# Overview |
22 | + |
23 | +Deploy your own content distribution network (CDN). |
24 | + |
25 | + |
26 | +# Usage |
27 | + |
28 | + |
29 | +# TODO |
30 | diff --git a/config.yaml b/config.yaml |
31 | index 8d4f5d1..36d48c3 100644 |
32 | --- a/config.yaml |
33 | +++ b/config.yaml |
34 | @@ -11,3 +11,15 @@ options: |
35 | description: | |
36 | A comma-separated list of nagios servicegroups. |
37 | If left empty, the nagios_context will be used as the servicegroup |
38 | + sites: |
39 | + default: "" |
40 | + type: string |
41 | + description: | |
42 | + YAML-formatted virtual hosts/sites. e.g. |
43 | + site1.local: |
44 | + server: |
45 | + server_name: site1.local |
46 | + location: |
47 | + path: / |
48 | + proxy_pass: http://localhost:8081 |
49 | + proxy_set_header: Host "site1.local" |
50 | diff --git a/layer.yaml b/layer.yaml |
51 | index 3a60daa..253b93a 100644 |
52 | --- a/layer.yaml |
53 | +++ b/layer.yaml |
54 | @@ -2,4 +2,10 @@ includes: |
55 | - layer:basic |
56 | - layer:apt |
57 | - layer:nagios |
58 | -repo: lp:cdn-charm |
59 | + - layer:status |
60 | +repo: lp:content-cache-charm |
61 | +options: |
62 | + basic: |
63 | + packages: |
64 | + - haproxy |
65 | + - nginx |
66 | diff --git a/lib/nginx.py b/lib/nginx.py |
67 | new file mode 100644 |
68 | index 0000000..ac2f0eb |
69 | --- /dev/null |
70 | +++ b/lib/nginx.py |
71 | @@ -0,0 +1,83 @@ |
72 | +import os |
73 | + |
74 | +NGINX_SITE_BASE_PATH = '/etc/nginx' |
75 | +INDENT = ' '*4 |
76 | + |
77 | + |
78 | +class NginxConf: |
79 | + |
80 | + def __init__(self, sites_base_path=NGINX_SITE_BASE_PATH): |
81 | + self._sites_path = os.path.join(sites_base_path, 'sites-available') |
82 | + |
83 | + # Expose sites_path as a property to allow mocking in indirect calls to |
84 | + # this class. |
85 | + @property |
86 | + def sites_path(self): |
87 | + return self._sites_path |
88 | + |
89 | + def write_site(self, site, new): |
90 | + fname = os.path.join(self.sites_path, site) |
91 | + # Check if contents changed |
92 | + try: |
93 | + with open(fname, 'r', encoding='utf-8') as f: |
94 | + current = f.read() |
95 | + except FileNotFoundError: |
96 | + current = '' |
97 | + if new == current: |
98 | + return False |
99 | + with open(fname, 'w', encoding='utf-8') as f: |
100 | + f.write(new) |
101 | + return True |
102 | + |
103 | + def sync_sites(self, sites): |
104 | + changed = False |
105 | + for site in os.listdir(self.sites_path): |
106 | + available = os.path.join(self.sites_path, site) |
107 | + enabled = os.path.join(os.path.dirname(self.sites_path), 'sites-enabled', site) |
108 | + if site not in sites: |
109 | + changed = True |
110 | + try: |
111 | + os.remove(available) |
112 | + os.remove(enabled) |
113 | + except FileNotFoundError: |
114 | + pass |
115 | + elif not os.path.exists(enabled): |
116 | + changed = True |
117 | + os.symlink(available, enabled) |
118 | + |
119 | + return changed |
120 | + |
121 | + def render(self, conf): |
122 | + output = [] |
123 | + for key in conf.keys(): |
124 | + if key == 'server': |
125 | + output.append(self._render_server(conf[key])) |
126 | + else: |
127 | + output.append('{key} {value};' |
128 | + .format(key=key, value=conf[key])) |
129 | + return '\n'.join(output) |
130 | + |
131 | + def _render_server(self, conf): |
132 | + output = ['\nserver {'] |
133 | + for key in conf.keys(): |
134 | + if key == 'location': |
135 | + output.append(self._render_location(conf[key])) |
136 | + else: |
137 | + output.append('{indent}{key} {value};' |
138 | + .format(indent=INDENT, key=key, value=conf[key])) |
139 | + for log in ['access_log', 'error_log']: |
140 | + if log not in conf: |
141 | + output.append('{indent}{key} /var/log/nginx/{site}-access.log;' |
142 | + .format(indent=INDENT, key=log, site=conf['server_name'])) |
143 | + output.append('}\n') |
144 | + return '\n'.join(output) |
145 | + |
146 | + def _render_location(self, conf): |
147 | + output = ['\n{}location {} {{'.format(INDENT, conf['path'])] |
148 | + for key in conf.keys(): |
149 | + if key == 'path': |
150 | + continue |
151 | + output.append('{indent}{indent}{key} {value};' |
152 | + .format(indent=INDENT, key=key, value=conf[key])) |
153 | + output.append('{}}}\n'.format(INDENT)) |
154 | + return '\n'.join(output) |
155 | diff --git a/metadata.yaml b/metadata.yaml |
156 | index 43eba49..ceba5c2 100644 |
157 | --- a/metadata.yaml |
158 | +++ b/metadata.yaml |
159 | @@ -1,10 +1,14 @@ |
160 | -name: cdn |
161 | -summary: Content distribution network (CDN) |
162 | -maintainer: CDN Charmers <cdn-charmers@lists.launchpad.net> |
163 | +name: content-cache |
164 | +summary: Content / Frontend Cache |
165 | +maintainer: Content Cache Charmers <content-cache-charmers@lists.launchpad.net> |
166 | description: | |
167 | - Deploy CDN |
168 | + Installs Nginx and HAProxy as a highly available web accelerator |
169 | + with TLS support. Useful for providing local mirrors of HTTP servers |
170 | + and building content delivery networks (CDN). |
171 | tags: |
172 | - cache-proxy |
173 | + - content-cache |
174 | + - web-cache |
175 | - ops |
176 | series: |
177 | - bionic |
178 | diff --git a/pytest.ini b/pytest.ini |
179 | new file mode 100644 |
180 | index 0000000..39da980 |
181 | --- /dev/null |
182 | +++ b/pytest.ini |
183 | @@ -0,0 +1,6 @@ |
184 | +[pytest] |
185 | +filterwarnings = |
186 | + # https://github.com/juju/charm-helpers/issues/294 |
187 | + ignore:.*inspect.getargspec\(\) is deprecated.*:DeprecationWarning |
188 | + # https://github.com/juju/charm-helpers/issues/293 |
189 | + ignore:.*dist\(\) and linux_distribution\(\) functions are deprecated:PendingDeprecationWarning |
190 | diff --git a/reactive/cdn.py b/reactive/cdn.py |
191 | deleted file mode 100644 |
192 | index e69de29..0000000 |
193 | --- a/reactive/cdn.py |
194 | +++ /dev/null |
195 | diff --git a/reactive/content_cache.py b/reactive/content_cache.py |
196 | new file mode 100644 |
197 | index 0000000..d620521 |
198 | --- /dev/null |
199 | +++ b/reactive/content_cache.py |
200 | @@ -0,0 +1,86 @@ |
201 | +import yaml |
202 | + |
203 | +from charms import reactive |
204 | +from charms.layer import status |
205 | +from charmhelpers.core import hookenv, host |
206 | +from lib import nginx |
207 | + |
208 | + |
209 | +@reactive.hook('upgrade-charm') |
210 | +def upgrade_charm(): |
211 | + status.maintenance('forcing reconfiguration on upgrade-charm') |
212 | + reactive.clear_flag('content_cache.active') |
213 | + reactive.clear_flag('content_cache.installed') |
214 | + reactive.clear_flag('content_cache.haproxy.configured') |
215 | + reactive.clear_flag('content_cache.nginx.configured') |
216 | + |
217 | + |
218 | +@reactive.when_not('content_cache.installed') |
219 | +def install(): |
220 | + reactive.clear_flag('content_cache.active') |
221 | + |
222 | + reactive.clear_flag('content_cache.haproxy.configured') |
223 | + reactive.clear_flag('content_cache.nginx.configured') |
224 | + reactive.set_flag('content_cache.installed') |
225 | + |
226 | + |
227 | +@reactive.when('config.changed') |
228 | +def config_changed(): |
229 | + reactive.clear_flag('content_cache.haproxy.configured') |
230 | + reactive.clear_flag('content_cache.nginx.configured') |
231 | + |
232 | + |
233 | +@reactive.when('content_cache.nginx.configured', 'content_cache.haproxy.configured') |
234 | +@reactive.when_not('content_cache.active') |
235 | +def set_active(): |
236 | + # XXX: Add more info such as nginx and haproxy status |
237 | + status.active('ready') |
238 | + reactive.set_flag('content_cache.active') |
239 | + |
240 | + |
241 | +def service_start_or_restart(name): |
242 | + if host.service_running(name): |
243 | + status.maintenance('Restarting {}...'.format(name)) |
244 | + host.service_restart(name) |
245 | + else: |
246 | + status.maintenance('Starting {}...'.format(name)) |
247 | + host.service_start(name) |
248 | + |
249 | + |
250 | +@reactive.when_not('content_cache.nginx.configured') |
251 | +def configure_nginx(): |
252 | + config = hookenv.config() |
253 | + |
254 | + if not config.get('sites'): |
255 | + status.blocked('requires list of sites to configure') |
256 | + reactive.clear_flag('content_cache.active') |
257 | + return |
258 | + |
259 | + ngx_conf = nginx.NginxConf() |
260 | + conf = yaml.safe_load(config.get('sites')) |
261 | + changed = False |
262 | + for site in conf.keys(): |
263 | + if ngx_conf.write_site(site, ngx_conf.render(conf[site])): |
264 | + hookenv.log('Wrote out new configs for site: {}'.format(site)) |
265 | + changed = True |
266 | + if ngx_conf.sync_sites(conf.keys()): |
267 | + hookenv.log('Enabled sites: {}'.format(' '.join(conf.keys()))) |
268 | + changed = True |
269 | + if changed: |
270 | + service_start_or_restart('nginx') |
271 | + |
272 | + reactive.set_flag('content_cache.nginx.configured') |
273 | + |
274 | + |
275 | +@reactive.when_not('content_cache.haproxy.configured') |
276 | +def configure_haproxy(): |
277 | + config = hookenv.config() |
278 | + |
279 | + if not config.get('sites'): |
280 | + status.blocked('requires list of sites to configure') |
281 | + reactive.clear_flag('content_cache.active') |
282 | + return |
283 | + |
284 | + # TODO: Configure up and start/restart HAProxy |
285 | + |
286 | + reactive.set_flag('content_cache.haproxy.configured') |
287 | diff --git a/tests/functional/test_cdn.py b/tests/functional/test_content_cache.py |
288 | similarity index 86% |
289 | rename from tests/functional/test_cdn.py |
290 | rename to tests/functional/test_content_cache.py |
291 | index 70a79da..e5660c3 100644 |
292 | --- a/tests/functional/test_cdn.py |
293 | +++ b/tests/functional/test_content_cache.py |
294 | @@ -21,7 +21,7 @@ async def model(): |
295 | async def apps(model): |
296 | apps = [] |
297 | for entry in series: |
298 | - app = model.applications['cdn-{}'.format(entry)] |
299 | + app = model.applications['content_cache-{}'.format(entry)] |
300 | apps.append(app) |
301 | return apps |
302 | |
303 | @@ -35,15 +35,15 @@ async def units(apps): |
304 | |
305 | |
306 | @pytest.mark.parametrize('series', series) |
307 | -async def test_cdn_deploy(model, series): |
308 | +async def test_content_cache_deploy(model, series): |
309 | # Starts a deploy for each series |
310 | - await model.deploy('{}/builds/cdn'.format(juju_repository), |
311 | + await model.deploy('{}/builds/content_cache'.format(juju_repository), |
312 | series=series, |
313 | - application_name='cdn-{}'.format(series)) |
314 | + application_name='content_cache-{}'.format(series)) |
315 | assert True |
316 | |
317 | |
318 | -async def test_cdn_status(apps, model): |
319 | +async def test_content_cache_status(apps, model): |
320 | # Verifies status for all deployed series of the charm |
321 | for app in apps: |
322 | await model.block_until(lambda: app.status == 'active') |
323 | diff --git a/tests/unit/files/nginx_config_rendered_test_output-site1.local.txt b/tests/unit/files/nginx_config_rendered_test_output-site1.local.txt |
324 | new file mode 100644 |
325 | index 0000000..f5b318d |
326 | --- /dev/null |
327 | +++ b/tests/unit/files/nginx_config_rendered_test_output-site1.local.txt |
328 | @@ -0,0 +1,19 @@ |
329 | +proxy_cache_path /var/cache/nginx/site1.local use_temp_path=off levels=1:2 keys_zone=site1-cache:10m max_size=1g; |
330 | + |
331 | +server { |
332 | + server_name site1.local; |
333 | + listen 6081; |
334 | + |
335 | + location / { |
336 | + proxy_pass http://localhost:8081; |
337 | + proxy_set_header Host "site1.local"; |
338 | + proxy_cache valid 200 1d; |
339 | + proxy_cache_lock on; |
340 | + proxy_cache_min_uses 5; |
341 | + proxy_cache_revalidate on; |
342 | + proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; |
343 | + } |
344 | + |
345 | + access_log /var/log/nginx/site1.local-access.log; |
346 | + error_log /var/log/nginx/site1.local-access.log; |
347 | +} |
348 | diff --git a/tests/unit/files/nginx_config_rendered_test_output-site2.local.txt b/tests/unit/files/nginx_config_rendered_test_output-site2.local.txt |
349 | new file mode 100644 |
350 | index 0000000..a76e005 |
351 | --- /dev/null |
352 | +++ b/tests/unit/files/nginx_config_rendered_test_output-site2.local.txt |
353 | @@ -0,0 +1,14 @@ |
354 | +proxy_cache_path /var/cache/nginx/site2.local use_temp_path=off levels=1:2 keys_zone=site2-cache:10m max_size=1g; |
355 | + |
356 | +server { |
357 | + server_name site2.local; |
358 | + listen 6082; |
359 | + access_log /var/log/nginx/site2.local-access.log; |
360 | + error_log /var/log/nginx/site2.local-error.log;; |
361 | + |
362 | + location / { |
363 | + proxy_pass http://localhost:8082; |
364 | + proxy_set_header Host "site2.local"; |
365 | + } |
366 | + |
367 | +} |
368 | diff --git a/tests/unit/files/nginx_config_test_config.txt b/tests/unit/files/nginx_config_test_config.txt |
369 | new file mode 100644 |
370 | index 0000000..9087fae |
371 | --- /dev/null |
372 | +++ b/tests/unit/files/nginx_config_test_config.txt |
373 | @@ -0,0 +1,25 @@ |
374 | +site1.local: |
375 | + proxy_cache_path: /var/cache/nginx/site1.local use_temp_path=off levels=1:2 keys_zone=site1-cache:10m max_size=1g |
376 | + server: |
377 | + server_name: site1.local |
378 | + listen: 6081 |
379 | + location: |
380 | + path: / |
381 | + proxy_pass: http://localhost:8081 |
382 | + proxy_set_header: Host "site1.local" |
383 | + proxy_cache: valid 200 1d |
384 | + proxy_cache_lock: 'on' |
385 | + proxy_cache_min_uses: 5 |
386 | + proxy_cache_revalidate: 'on' |
387 | + proxy_cache_use_stale: error timeout updating http_500 http_502 http_503 http_504 |
388 | +site2.local: |
389 | + proxy_cache_path: /var/cache/nginx/site2.local use_temp_path=off levels=1:2 keys_zone=site2-cache:10m max_size=1g |
390 | + server: |
391 | + server_name: site2.local |
392 | + listen: 6082 |
393 | + access_log: /var/log/nginx/site2.local-access.log |
394 | + error_log: /var/log/nginx/site2.local-error.log; |
395 | + location: |
396 | + path: / |
397 | + proxy_pass: http://localhost:8082 |
398 | + proxy_set_header: Host "site2.local" |
399 | diff --git a/tests/unit/test_cdn.py b/tests/unit/test_cdn.py |
400 | deleted file mode 100644 |
401 | index 6342df8..0000000 |
402 | --- a/tests/unit/test_cdn.py |
403 | +++ /dev/null |
404 | @@ -1,18 +0,0 @@ |
405 | -import shutil |
406 | -import tempfile |
407 | -import unittest |
408 | - |
409 | - |
410 | -class TestCDN(unittest.TestCase): |
411 | - def setUp(self): |
412 | - self.tmpdir = tempfile.mkdtemp(prefix='charm-unittests-') |
413 | - |
414 | - def tearDown(self): |
415 | - shutil.rmtree(self.tmpdir) |
416 | - |
417 | - def test_test(self): |
418 | - self.assertTrue(True) |
419 | - |
420 | - |
421 | -if __name__ == '__main__': |
422 | - unittest.main() |
423 | diff --git a/tests/unit/test_content_cache.py b/tests/unit/test_content_cache.py |
424 | new file mode 100644 |
425 | index 0000000..2366df8 |
426 | --- /dev/null |
427 | +++ b/tests/unit/test_content_cache.py |
428 | @@ -0,0 +1,152 @@ |
429 | +import os |
430 | +import shutil |
431 | +import sys |
432 | +import tempfile |
433 | +import unittest |
434 | +from unittest import mock |
435 | + |
436 | +# Add path to where our reactive layer lives and import. |
437 | +sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))) |
438 | +# We also need to mock up charms.layer so we can run unit tests without having |
439 | +# to build the charm and pull in layers such as layer-status. |
440 | +sys.modules['charms.layer'] = mock.MagicMock() |
441 | +from charms.layer import status # NOQA: E402 |
442 | +from reactive import content_cache # NOQA: E402 |
443 | + |
444 | + |
445 | +class TestCharm(unittest.TestCase): |
446 | + def setUp(self): |
447 | + self.tmpdir = tempfile.mkdtemp(prefix='charm-unittests-') |
448 | + self.addCleanup(shutil.rmtree, self.tmpdir) |
449 | + |
450 | + self.charm_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) |
451 | + |
452 | + patcher = mock.patch('charmhelpers.core.hookenv.log') |
453 | + self.mock_log = patcher.start() |
454 | + self.addCleanup(patcher.stop) |
455 | + self.mock_log.return_value = '' |
456 | + |
457 | + patcher = mock.patch('charmhelpers.core.hookenv.charm_dir') |
458 | + self.mock_charm_dir = patcher.start() |
459 | + self.addCleanup(patcher.stop) |
460 | + self.mock_charm_dir.return_value = self.charm_dir |
461 | + |
462 | + patcher = mock.patch('charmhelpers.core.hookenv.local_unit') |
463 | + self.mock_local_unit = patcher.start() |
464 | + self.addCleanup(patcher.stop) |
465 | + self.mock_local_unit.return_value = 'mock-content-cache/0' |
466 | + |
467 | + patcher = mock.patch('charmhelpers.core.hookenv.config') |
468 | + self.mock_config = patcher.start() |
469 | + self.addCleanup(patcher.stop) |
470 | + self.mock_config.return_value = {} |
471 | + |
472 | + @mock.patch('charms.reactive.clear_flag') |
473 | + def test_hook_upgrade_charm_flags(self, clear_flag): |
474 | + '''Test correct flags set via upgrade-charm hook''' |
475 | + status.maintenance.reset_mock() |
476 | + content_cache.upgrade_charm() |
477 | + self.assertFalse(status.maintenance.assert_called()) |
478 | + expected = [mock.call('content_cache.active'), |
479 | + mock.call('content_cache.installed'), |
480 | + mock.call('content_cache.haproxy.configured'), |
481 | + mock.call('content_cache.nginx.configured')] |
482 | + self.assertFalse(clear_flag.assert_has_calls(expected, any_order=True)) |
483 | + |
484 | + @mock.patch('charms.reactive.clear_flag') |
485 | + @mock.patch('charms.reactive.set_flag') |
486 | + def test_hook_install_flags(self, set_flag, clear_flag): |
487 | + '''Test correct flags are set via install charm hook''' |
488 | + content_cache.install() |
489 | + expected = [mock.call('content_cache.installed')] |
490 | + self.assertFalse(set_flag.assert_has_calls(expected, any_order=True)) |
491 | + |
492 | + expected = [mock.call('content_cache.active'), |
493 | + mock.call('content_cache.haproxy.configured'), |
494 | + mock.call('content_cache.nginx.configured')] |
495 | + self.assertFalse(clear_flag.assert_has_calls(expected, any_order=True)) |
496 | + |
497 | + @mock.patch('charms.reactive.clear_flag') |
498 | + def test_hook_config_changed_flags(self, clear_flag): |
499 | + '''Test correct flags are set via config-changed charm hook''' |
500 | + content_cache.config_changed() |
501 | + expected = [mock.call('content_cache.haproxy.configured'), |
502 | + mock.call('content_cache.nginx.configured')] |
503 | + self.assertFalse(clear_flag.assert_has_calls(expected, any_order=True)) |
504 | + |
505 | + @mock.patch('charms.reactive.set_flag') |
506 | + def test_hook_set_active(self, set_flag): |
507 | + status.active.reset_mock() |
508 | + content_cache.set_active() |
509 | + self.assertFalse(status.active.assert_called()) |
510 | + self.assertFalse(set_flag.assert_called_once_with('content_cache.active')) |
511 | + |
512 | + @mock.patch('charmhelpers.core.host.service_running') |
513 | + @mock.patch('charmhelpers.core.host.service_restart') |
514 | + @mock.patch('charmhelpers.core.host.service_start') |
515 | + def test_service_start_or_restart_running(self, service_start, service_restart, service_running): |
516 | + '''Test service restarted when already running''' |
517 | + service_running.return_value = True |
518 | + status.active.reset_mock() |
519 | + content_cache.service_start_or_restart('someservice') |
520 | + self.assertFalse(status.maintenance.assert_called()) |
521 | + self.assertFalse(service_start.assert_not_called()) |
522 | + self.assertFalse(service_restart.assert_called_once_with('someservice')) |
523 | + |
524 | + @mock.patch('charmhelpers.core.host.service_running') |
525 | + @mock.patch('charmhelpers.core.host.service_restart') |
526 | + @mock.patch('charmhelpers.core.host.service_start') |
527 | + def test_service_start_or_restart_stopped(self, service_start, service_restart, service_running): |
528 | + '''Test service started up when not running/stopped''' |
529 | + service_running.return_value = False |
530 | + status.active.reset_mock() |
531 | + content_cache.service_start_or_restart('someservice') |
532 | + self.assertFalse(status.maintenance.assert_called()) |
533 | + self.assertFalse(service_start.assert_called_once_with('someservice')) |
534 | + self.assertFalse(service_restart.assert_not_called()) |
535 | + |
536 | + @mock.patch('charms.reactive.clear_flag') |
537 | + def test_configure_nginx_no_sites(self, clear_flag): |
538 | + '''Test correct flags are set when no sites defined to configure Nginx''' |
539 | + status.blocked.reset_mock() |
540 | + content_cache.configure_nginx() |
541 | + self.assertFalse(status.blocked.assert_called()) |
542 | + self.assertFalse(clear_flag.assert_called_once_with('content_cache.active')) |
543 | + |
544 | + @mock.patch('reactive.content_cache.service_start_or_restart') |
545 | + def test_configure_nginx_sites(self, service_start_or_restart): |
546 | + '''Test configuration of Nginx sites''' |
547 | + with open('tests/unit/files/nginx_config_test_config.txt', 'r', encoding='utf-8') as f: |
548 | + ngx_config = f.read() |
549 | + self.mock_config.return_value = {'sites': ngx_config} |
550 | + |
551 | + with mock.patch('lib.nginx.NginxConf.sites_path', new_callable=mock.PropertyMock) as mock_site_path: |
552 | + mock_site_path.return_value = os.path.join(self.tmpdir, 'sites-available') |
553 | + # sites-available and sites-enabled won't exist in our temp dir |
554 | + os.mkdir(os.path.join(self.tmpdir, 'sites-available')) |
555 | + os.mkdir(os.path.join(self.tmpdir, 'sites-enabled')) |
556 | + content_cache.configure_nginx() |
557 | + self.assertFalse(service_start_or_restart.assert_called_once_with('nginx')) |
558 | + |
559 | + # Re-run with same set of sites, no change so shouldn't need to restart Nginx |
560 | + service_start_or_restart.reset_mock() |
561 | + content_cache.configure_nginx() |
562 | + self.assertFalse(service_start_or_restart.assert_not_called()) |
563 | + |
564 | + @mock.patch('charms.reactive.clear_flag') |
565 | + def test_configure_haproxy_no_sites(self, clear_flag): |
566 | + status.blocked.reset_mock() |
567 | + content_cache.configure_haproxy() |
568 | + self.assertFalse(status.blocked.assert_called()) |
569 | + self.assertFalse(clear_flag.assert_called_once_with('content_cache.active')) |
570 | + |
571 | + @mock.patch('reactive.content_cache.service_start_or_restart') |
572 | + def test_configure_haproxy_sites(self, service_start_or_restart): |
573 | + with open('tests/unit/files/nginx_config_test_config.txt', 'r', encoding='utf-8') as f: |
574 | + ngx_config = f.read() |
575 | + self.mock_config.return_value = {'sites': ngx_config} |
576 | + content_cache.configure_haproxy() |
577 | + |
578 | + |
579 | +if __name__ == '__main__': |
580 | + unittest.main() |
581 | diff --git a/tests/unit/test_nginx.py b/tests/unit/test_nginx.py |
582 | new file mode 100644 |
583 | index 0000000..7cec4e6 |
584 | --- /dev/null |
585 | +++ b/tests/unit/test_nginx.py |
586 | @@ -0,0 +1,83 @@ |
587 | +import os |
588 | +import shutil |
589 | +import sys |
590 | +import tempfile |
591 | +import unittest |
592 | +import yaml |
593 | + |
594 | +sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))) |
595 | +from lib import nginx # NOQA: E402 |
596 | + |
597 | + |
598 | +class TestLibNginx(unittest.TestCase): |
599 | + def setUp(self): |
600 | + self.tmpdir = tempfile.mkdtemp(prefix='charm-unittests-') |
601 | + self.addCleanup(shutil.rmtree, self.tmpdir) |
602 | + self.charm_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) |
603 | + |
604 | + def test_nginx_config_sites_path(self): |
605 | + sites_path = '/etc/nginx/sites-available' |
606 | + ngx_conf = nginx.NginxConf() |
607 | + self.assertEqual(ngx_conf.sites_path, sites_path) |
608 | + |
609 | + def test_nginx_config_render(self): |
610 | + '''Test parsing a YAML-formatted list of sites''' |
611 | + |
612 | + ngx_conf = nginx.NginxConf() |
613 | + |
614 | + with open('tests/unit/files/nginx_config_test_config.txt', 'r', encoding='utf-8') as f: |
615 | + conf = yaml.safe_load(f.read()) |
616 | + |
617 | + # From the given YAML-formatted list of sites, check that each individual |
618 | + # Nginx config rendered matches what's in tests/unit/files. |
619 | + for site in conf.keys(): |
620 | + output_file = 'tests/unit/files/nginx_config_rendered_test_output-{}.txt'.format(site) |
621 | + with open(output_file, 'r', encoding='utf-8') as f: |
622 | + output = f.read() |
623 | + self.assertEqual(output, ngx_conf.render(conf[site])) |
624 | + |
625 | + def test_nginx_config_write_sites(self): |
626 | + '''Test writing out sites to individual Nginx site config files''' |
627 | + ngx_conf = nginx.NginxConf(self.tmpdir) |
628 | + os.mkdir(os.path.join(self.tmpdir, 'sites-available')) |
629 | + os.mkdir(os.path.join(self.tmpdir, 'sites-enabled')) |
630 | + |
631 | + with open('tests/unit/files/nginx_config_rendered_test_output-site1.local.txt', 'r', encoding='utf-8') as f: |
632 | + conf = f.read() |
633 | + |
634 | + self.assertTrue(ngx_conf.write_site('site1.local', conf)) |
635 | + # Write again with same contents, this time it should return 'False' |
636 | + # as there's no change, thus no need to restart/reload Nginx. |
637 | + self.assertFalse(ngx_conf.write_site('site1.local', conf)) |
638 | + |
639 | + # Compare what's been written out matches what's in tests/unit/files. |
640 | + with open(os.path.join(self.tmpdir, 'sites-available', 'site1.local'), 'r', encoding='utf-8') as f: |
641 | + output = f.read() |
642 | + self.assertEqual(conf, output) |
643 | + |
644 | + def test_nginx_config_sync_sites(self): |
645 | + '''Test cleanup of stale sites and that sites are enabled''' |
646 | + ngx_conf = nginx.NginxConf(self.tmpdir) |
647 | + os.mkdir(os.path.join(self.tmpdir, 'sites-available')) |
648 | + os.mkdir(os.path.join(self.tmpdir, 'sites-enabled')) |
649 | + |
650 | + with open('tests/unit/files/nginx_config_rendered_test_output-site1.local.txt', 'r', encoding='utf-8') as f: |
651 | + conf = f.read() |
652 | + |
653 | + # Write out an extra site config to test cleaning it up. |
654 | + for site in ['site1.local', 'site2.local']: |
655 | + ngx_conf.write_site(site, conf) |
656 | + ngx_conf.write_site('site3.local', conf) |
657 | + |
658 | + # Clean up anything that's not site1 and site2. |
659 | + self.assertTrue(ngx_conf.sync_sites(['site1.local', 'site2.local'])) |
660 | + # Check to make sure site1 still exists and is symlinked in site-senabled. |
661 | + self.assertTrue(os.path.exists(os.path.join(self.tmpdir, 'sites-available', 'site1.local'))) |
662 | + self.assertTrue(os.path.islink(os.path.join(self.tmpdir, 'sites-enabled', 'site1.local'))) |
663 | + # Only two sites, site3.local shouldn't exist. |
664 | + self.assertFalse(os.path.exists(os.path.join(self.tmpdir, 'sites-available', 'site3.local'))) |
665 | + self.assertFalse(os.path.exists(os.path.join(self.tmpdir, 'sites-enabled', 'site3.local'))) |
666 | + |
667 | + |
668 | +if __name__ == '__main__': |
669 | + unittest.main() |
670 | diff --git a/tox.ini b/tox.ini |
671 | index ee5dbeb..d6b7675 100644 |
672 | --- a/tox.ini |
673 | +++ b/tox.ini |
674 | @@ -9,7 +9,7 @@ setenv = |
675 | PYTHONPATH = . |
676 | |
677 | [testenv:unit] |
678 | -commands = pytest -v --ignore {toxinidir}/tests/functional --cov=lib --cov=reactive --cov=actions --cov-report=term |
679 | +commands = pytest -v --ignore {toxinidir}/tests/functional --cov=lib --cov=reactive --cov=actions --cov-report=term-missing --cov-branch |
680 | deps = -r{toxinidir}/tests/unit/requirements.txt |
681 | -r{toxinidir}/requirements.txt |
682 | setenv = PYTHONPATH={toxinidir}/lib |
683 | @@ -32,6 +32,5 @@ exclude = |
684 | .git, |
685 | __pycache__, |
686 | .tox, |
687 | - hooks/install.d/, |
688 | max-line-length = 120 |
689 | max-complexity = 10 |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.