Merge lp:~cprov/uci-engine/charm-tests into lp:uci-engine

Proposed by Celso Providelo
Status: Needs review
Proposed branch: lp:~cprov/uci-engine/charm-tests
Merge into: lp:uci-engine
Diff against target: 286 lines (+119/-21)
2 files modified
charms/precise/wsgi-app/unit_tests/test_hooks.py (+17/-17)
testing/run_tests.py (+102/-4)
To merge this branch: bzr merge lp:~cprov/uci-engine/charm-tests
Reviewer Review Type Date Requested Status
Para Siva (community) Approve
PS Jenkins bot (community) continuous-integration Approve
Vincent Ladeuil (community) Needs Fixing
Joe Talbott (community) Approve
Review via email: mp+236214@code.launchpad.net

Commit message

Fixing project test runner to load and run charm tests properly.

Description of the change

Fixing project test runner to load and run charm tests properly.

On the bright side, there was no test failures per se, since there were probably run by developers via local Makefile. We have a 45/45, so DON'T PANIC!

It was required to create a custom test loader, so the tests with the same relative path (standard) don't shadow the next ones; and also a custom test suite that is capable of reloading 'hooks' and 'charmhelpers' module before running tests for each charm (suite really, it does more reloads than necessary :-/).

It's unfortunate that such a simple problem (aggregating tests) implies in that much complexity encoded in testing/run_tests.py ...

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:813
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1483/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1483/rebuild

review: Approve (continuous-integration)
Revision history for this message
Para Siva (psivaa) wrote :

I keep getting the following trace on with and without a deployment. (both with novarc file sourced.)

Traceback (most recent call last):
  File "./run-tests", line 32, in <module>
    retval = run_tests.main(sys.argv[1:], sys.stdout, sys.stderr)
  File "/home/psivaa/temp/code-reviews/charm-tests/testing/run_tests.py", line 565, in main
    options.include_regexps, options.exclude_regexps)
  File "/home/psivaa/temp/code-reviews/charm-tests/testing/run_tests.py", line 448, in load_charm_tests
    suite.setCharmRoots()
  File "/home/psivaa/temp/code-reviews/charm-tests/testing/run_tests.py", line 356, in setCharmRoots
    s.charm_root = '/'.join(flat_tests[0].id().split('.')[:3])
IndexError: list index out of range

Marking the MP, needs fixing. Please feel free to ignore if i'm doing abs stupid :)

review: Needs Fixing
Revision history for this message
Joe Talbott (joetalbott) wrote :

LGTM with one minor nit.

review: Approve
Revision history for this message
Vincent Ladeuil (vila) wrote :

There are many issues with the MP, the main ones are:
- it breaks --list

- it uses imp.load_module() whose doc says: "This function does more than importing the module: if the module was already imported, it is equivalent to a reload()!" (The question mark is theirs). See https://docs.python.org/2/library/functions.html?highlight=reload#reload and the endless list of issues associated with that function.

There is no way we can trust reload(), it's meant to be used interactively with a full understanding of the fallouts.

The root issue that you're pointing out here is that we're using hooks.py to mean different things, python doesn't allow that. So that's what need to be changed.

I had a look at it and the second similar issue is that we *also* do 'import charmhelpers' with various definitions across charms and also try to convince python that they are all the same. This one is slightly simpler to fix: as a project, we should use the *same* version in all charms (we do use the same revno but build a different python package via different charm-helpers.yaml files).

I think it's time to stop using bigger hammers that do more harm than good, step back to the original decision to rely on 'import hooks' and fix that.

review: Needs Fixing
lp:~cprov/uci-engine/charm-tests updated
814. By Celso Providelo

Addressing review comments.

Revision history for this message
Celso Providelo (cprov) wrote :

Sivaa,

Thanks for pointing this problem, it only happens when you run run-tests without filters (an empty test suite for charm tests shows up mysteriously). Fix added.

> I keep getting the following trace on with and without a deployment. (both
> with novarc file sourced.)
>
> Traceback (most recent call last):
> File "./run-tests", line 32, in <module>
> retval = run_tests.main(sys.argv[1:], sys.stdout, sys.stderr)
> File "/home/psivaa/temp/code-reviews/charm-tests/testing/run_tests.py", line
> 565, in main
> options.include_regexps, options.exclude_regexps)
> File "/home/psivaa/temp/code-reviews/charm-tests/testing/run_tests.py", line
> 448, in load_charm_tests
> suite.setCharmRoots()
> File "/home/psivaa/temp/code-reviews/charm-tests/testing/run_tests.py", line
> 356, in setCharmRoots
> s.charm_root = '/'.join(flat_tests[0].id().split('.')[:3])
> IndexError: list index out of range
>
>
> Marking the MP, needs fixing. Please feel free to ignore if i'm doing abs
> stupid :)

Revision history for this message
Celso Providelo (cprov) wrote :

Joe,

I've added an explanation for the line calculating the path from the mangled test id. Thanks for pointing it.

> LGTM with one minor nit.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:814
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1491/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1491/rebuild

review: Approve (continuous-integration)
Revision history for this message
Celso Providelo (cprov) wrote :

Vila,

Thanks for your points, let's discuss them specifically.

> There are many issues with the MP, the main ones are:
> - it breaks --list

--list was probably broken because the problem with running run-test w/o filters, it's fixed now.

> - it uses imp.load_module() whose doc says: "This function does more than
> importing the module: if the module was already imported, it is equivalent to
> a reload()!" (The question mark is theirs). See
> https://docs.python.org/2/library/functions.html?highlight=reload#reload and
> the endless list of issues associated with that function.
>
> There is no way we can trust reload(), it's meant to be used interactively
> with a full understanding of the fallouts.
>
> The root issue that you're pointing out here is that we're using hooks.py to
> mean different things, python doesn't allow that. So that's what need to be
> changed.
>
> I had a look at it and the second similar issue is that we *also* do 'import
> charmhelpers' with various definitions across charms and also try to convince
> python that they are all the same. This one is slightly simpler to fix: as a
> project, we should use the *same* version in all charms (we do use the same
> revno but build a different python package via different charm-helpers.yaml
> files).

Charms are designed to be standalone projects, they just happen to be in the same tree because it's convenient to us, the same goes for running their tests inside a single *super-project* test suite, it's *just* way more convenient than spawning isolated `make check`s and collect results. That said, in order to keep the wanted benefits, UCI-E has to compromise.

We are all aware of the risks involved in reloading modules, but if it is what it takes to assure charm tests (and only them) are run as part of our test suite, so be it. And by all means, I agree with you on keeping this hack as isolated and instrumented as possible, so we will notice if it is behaving badly, if it ever does.

> I think it's time to stop using bigger hammers that do more harm than good,
> step back to the original decision to rely on 'import hooks' and fix that.

It sounds good, but `import hooks` & `import charmhelpers` are not going to change, they make all sense at the charm-level; and we have to ensure we run charm tests in tarmac, today, not tomorrow.

Moreover, It's not a *big hammer*, since it's well isolated (custom loaders and suite are only used for charm tests) and the behaviour can be entirely overridden by tweaking 2 lines, if we find real, not hypothetical, evidences that it's not doing what it is supposed to do.

Revision history for this message
Francis Ginther (fginther) wrote :

Here's a bit of a strawman. Is it time move some of these charms out of the lp:uci-engine source tree? Our original intent was to do this once the level of change had dropped to an acceptable level. This is not going to solve the problem, but maybe we just avoid it or lessen the risks.

Even if this isn't the right solution to solve this specific problem, is it time to remove one or more of these from the source? We should be able to do some simply analysis to see which charms have had the fewest recent updates.

Revision history for this message
Vincent Ladeuil (vila) wrote :

> Charms are designed to be standalone projects, they just happen to be in the
> same tree because it's convenient to us,

Components are also designed to be stand alone, we know we have issues there
around the overall project layout but we've already taken some steps to
ensure they use different parts of the python name space so their code can
be loaded in a single python process.

It's not about being convenient it's just about using python and respecting
the language definition.

> the same goes for running their tests
> inside a single *super-project* test suite, it's *just* way more convenient
> than spawning isolated `make check`s and collect results.

... and allowing test listing/selection and displaying the timings and being
able to use subunit as an output and being able to run them concurrently

> That said, in order
> to keep the wanted benefits, UCI-E has to compromise.

Says who ?

There *is* a valid fix which is to fix the charms to use their own name space like we do for the components.

>
> We are all aware of the risks involved in reloading modules,

I have no idea about how tricking python into believing it can import
different modules in the same name space spot can manifest itself or mask
issues, that's the whole point.

> but if it is what
> it takes to assure charm tests (and only them) are run as part of our test
> suite, so be it. And by all means, I agree with you on keeping this hack as
> isolated and instrumented as possible, so we will notice if it is behaving
> badly, if it ever does.
>
> > I think it's time to stop using bigger hammers that do more harm than good,
> > step back to the original decision to rely on 'import hooks' and fix that.
>
> It sounds good, but `import hooks` & `import charmhelpers` are not going to
> change,

Of course they can change, I already have one charm fixed to define its own
name space and fix the root issue.

> they make all sense at the charm-level; and we have to ensure we run
> charm tests in tarmac, today, not tomorrow.

The charms should be fixed, not run-tests, changing python semantics is not
run-tests job.

>
> Moreover, It's not a *big hammer*, since it's well isolated (custom loaders
> and suite are only used for charm tests) and the behaviour can be entirely
> overridden by tweaking 2 lines,

Your patch is already ~300 lines long, that's far from 2 lines.

> if we find real, not hypothetical, evidences
> that it's not doing what it is supposed to do.

The real evidence is that python can't load different modules under the same
name.

That's a real issue, perfectly clear and understood. I see no point in
tricking python when there is a way to use it properly.

Revision history for this message
Evan (ev) wrote :

We have always said that charms should be stand-alone and not depend on behaviour that only exists in lp:uci-engine. I think by putting the namespace of the charm in the filename, we move away from that. Pretty much every other charm-helpers using charm has hooks/hooks.py and now we have hooks/wsgi_app_hooks.py.

I also think the risks that Vincent highlights are very real and if they were to bite us, would be extremely hard to pinpoint.

I am almost inclined to agree with Francis that it's time to split the charms out into their own branches. We understand the various pieces of this project enough now to be able to accomplish that without branch landing being delicate. However, it's no small amount of work. Each charm requires a new stanza in Tarmac, a new branch for everyone on the team to watch MPs on, and so on.

I am also convinced that it greatly reduces visibility on these ancillary branches. It means that we sacrifice consistency, which already needs to be carefully managed in a microservices architecture.

Instead, I'd like to propose that we isolate the process environment for the charm tests. We fork a child process under the charm directory and run through the tests in complete isolation. The parent process can take the piped output from the child and combine it with its own results.

We've got more opinions on this one than we have people. Let's take it down a level and understand that there are tradeoffs with all approaches.

Revision history for this message
Para Siva (psivaa) wrote :

Thanks Celso for the fix to handle empty suite. Approving before disappearing for the day.

review: Approve
Revision history for this message
Celso Providelo (cprov) wrote :

Evan and Francis,

I have no arguments against your proposal for spawning `make check` on charms as a clean transition to move them away from our tree.

It is certainly a good ending-point to this polarised discussion full of hypothetical problems and no concrete solutions.

Any arguments against running charms tests in called_by_tarmac.py before deploying the new environment ? (users would chdir into charms and run `make check`)

Revision history for this message
Joe Talbott (joetalbott) wrote :

On Tue, Sep 30, 2014 at 12:52:38PM -0000, Celso Providelo wrote:
> Evan and Francis,
>
> I have no arguments against your proposal for spawning `make check` on charms as a clean transition to move them away from our tree.
>
> It is certainly a good ending-point to this polarised discussion full of hypothetical problems and no concrete solutions.
>
> Any arguments against running charms tests in called_by_tarmac.py before deploying the new environment ? (users would chdir into charms and run `make check`)

I am strongly against changing our charms to make them work with
uci-engine. I am also strongly in favor of getting our charms out of
our uci-engine branch. I don't think tracking several branches for MPs
is too much effort.

I am in support of running charm tests in isolation until they can be
put into separate branches.

Joe

Unmerged revisions

814. By Celso Providelo

Addressing review comments.

813. By Celso Providelo

Using a custom test suite for charms tests for reloding hooks & charmhelpers before running tests.

812. By Celso Providelo

Fix wsgi-app tests to import modules in the same way the other charms are doing (top-level 'hooks' module).

811. By Celso Providelo

Override ucitests.loaders.Loader test import behavior to cope with our charms default testing topology (<charm>/unit_tests/*.py).

810. By Vincent Ladeuil

Rename the test files to make them unique in the unit_tests namespace, force unit_tests to be a package with multiple directories so all the tests can be properly loaded.

Outcome: 36 failures for 45 charm tests which is better than 8 passing tests (which in turn means all but one hidden charm tests are broken :-/)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'charms/precise/wsgi-app/unit_tests/test_hooks.py'
--- charms/precise/wsgi-app/unit_tests/test_hooks.py 2014-09-25 20:38:15 +0000
+++ charms/precise/wsgi-app/unit_tests/test_hooks.py 2014-09-29 18:45:52 +0000
@@ -21,8 +21,8 @@
2121
22import yaml22import yaml
2323
24from intercom import hooks as intercom_hooks24import hooks
25from hooks import hooks25import intercom
2626
2727
28HERE = os.path.abspath(os.path.dirname(__file__))28HERE = os.path.abspath(os.path.dirname(__file__))
@@ -135,13 +135,13 @@
135 self.revno = 'r1234_foo'135 self.revno = 'r1234_foo'
136136
137 def test_framework_restish(self):137 def test_framework_restish(self):
138 hooks.execute(['hooks/install'])138 hooks.hooks.execute(['hooks/install'])
139 pkgs = self.apt_install.call_args_list[0][0][0]139 pkgs = self.apt_install.call_args_list[0][0][0]
140 self.assertIn('python-restish', pkgs)140 self.assertIn('python-restish', pkgs)
141141
142 def test_framework_pyramid(self):142 def test_framework_pyramid(self):
143 self.config.return_value['framework'] = 'pyramid'143 self.config.return_value['framework'] = 'pyramid'
144 hooks.execute(['hooks/install'])144 hooks.hooks.execute(['hooks/install'])
145 pkgs = self.apt_install.call_args_list[0][0][0]145 pkgs = self.apt_install.call_args_list[0][0][0]
146 self.assertIn('python3-pyramid', pkgs)146 self.assertIn('python3-pyramid', pkgs)
147147
@@ -194,7 +194,7 @@
194 cron_file_path = os.path.join(self._cron_root(), 'local-test')194 cron_file_path = os.path.join(self._cron_root(), 'local-test')
195 with open(cron_file_path, 'w') as fd:195 with open(cron_file_path, 'w') as fd:
196 fd.write('removed')196 fd.write('removed')
197 hooks.execute(['hooks/config-changed'])197 hooks.hooks.execute(['hooks/config-changed'])
198 self.assertEqual([], os.listdir(self._cron_root()))198 self.assertEqual([], os.listdir(self._cron_root()))
199 self.service_restart.assert_called_once_with('cron')199 self.service_restart.assert_called_once_with('cron')
200200
@@ -203,7 +203,7 @@
203 self.config.return_value['cron_schedule'] = '*/10 * * * *'203 self.config.return_value['cron_schedule'] = '*/10 * * * *'
204 self.config.return_value['cron_cmd'] = 'foo.sh'204 self.config.return_value['cron_cmd'] = 'foo.sh'
205 self.config.return_value['wsgi_user'] = 'bar'205 self.config.return_value['wsgi_user'] = 'bar'
206 hooks.execute(['hooks/config-changed'])206 hooks.hooks.execute(['hooks/config-changed'])
207 self.assertEqual(['local-test'], os.listdir(self._cron_root()))207 self.assertEqual(['local-test'], os.listdir(self._cron_root()))
208 code_dir = os.path.join(self.tmpdir, 'code')208 code_dir = os.path.join(self.tmpdir, 'code')
209 logs_dir = os.path.join(self.tmpdir, 'logs')209 logs_dir = os.path.join(self.tmpdir, 'logs')
@@ -221,7 +221,7 @@
221 self.service_restart.assert_called_once_with('cron')221 self.service_restart.assert_called_once_with('cron')
222222
223 def test_allowed_hosts(self):223 def test_allowed_hosts(self):
224 hooks.execute(['hooks/config-changed'])224 hooks.hooks.execute(['hooks/config-changed'])
225 self.assertTrue(225 self.assertTrue(
226 os.path.exists(os.path.join(self.tmpdir, 'allowed_hosts.json')))226 os.path.exists(os.path.join(self.tmpdir, 'allowed_hosts.json')))
227227
@@ -265,7 +265,7 @@
265 def test_intercom_changed(self):265 def test_intercom_changed(self):
266 # New 'intercom' relations are stored in the JSON registry266 # New 'intercom' relations are stored in the JSON registry
267 # as <hostname>:<port>267 # as <hostname>:<port>
268 intercom_hooks.execute(['hooks/intercom-relation-changed'])268 intercom.hooks.execute(['hooks/intercom-relation-changed'])
269 self.log.assert_called_with(269 self.log.assert_called_with(
270 "Intercom unit 'remote-test' (remote.com) joined.")270 "Intercom unit 'remote-test' (remote.com) joined.")
271 self.assertEquals(271 self.assertEquals(
@@ -285,7 +285,7 @@
285 '2': {'hostname': '10.0.0.2',285 '2': {'hostname': '10.0.0.2',
286 'port': '9000',286 'port': '9000',
287 'unit': 'preserved'}})287 'unit': 'preserved'}})
288 intercom_hooks.execute(['hooks/intercom-relation-broken'])288 intercom.hooks.execute(['hooks/intercom-relation-broken'])
289 self.log.assert_called_with("Intercom unit '' () departed.")289 self.log.assert_called_with("Intercom unit '' () departed.")
290 self.assertEquals(290 self.assertEquals(
291 {'2': {'hostname': '10.0.0.2',291 {'2': {'hostname': '10.0.0.2',
@@ -314,7 +314,7 @@
314class TestWSGIHooks(RestishTestCase):314class TestWSGIHooks(RestishTestCase):
315315
316 def test_wsgi_joined(self):316 def test_wsgi_joined(self):
317 hooks.execute(['hooks/wsgi-relation-joined'])317 hooks.hooks.execute(['hooks/wsgi-relation-joined'])
318 self.open_port.assert_called_once_with(8080)318 self.open_port.assert_called_once_with(8080)
319 expected = {319 expected = {
320 'python_path': '',320 'python_path': '',
@@ -329,7 +329,7 @@
329 '/srv/local-test/logs', 'www-data', 'www-data', 0755)329 '/srv/local-test/logs', 'www-data', 'www-data', 0755)
330330
331 def test_wsgi_broken(self):331 def test_wsgi_broken(self):
332 hooks.execute(['hooks/wsgi-relation-broken'])332 hooks.hooks.execute(['hooks/wsgi-relation-broken'])
333 self.close_port.assert_called_once_with(8080)333 self.close_port.assert_called_once_with(8080)
334334
335335
@@ -337,7 +337,7 @@
337337
338 def test_json_status_joined(self):338 def test_json_status_joined(self):
339 self.config.return_value['json_status_path'] = 'foo'339 self.config.return_value['json_status_path'] = 'foo'
340 hooks.execute(['hooks/json_status-relation-joined'])340 hooks.hooks.execute(['hooks/json_status-relation-joined'])
341 self.assertIn('status-url', self.relation_set.call_args_list[0][0][1])341 self.assertIn('status-url', self.relation_set.call_args_list[0][0][1])
342342
343343
@@ -358,7 +358,7 @@
358 }358 }
359359
360 def test_changed(self):360 def test_changed(self):
361 hooks.execute(['hooks/amqp-relation-changed'])361 hooks.hooks.execute(['hooks/amqp-relation-changed'])
362 self.wsgi_reload.assert_called_once_with()362 self.wsgi_reload.assert_called_once_with()
363 self.assertTrue(363 self.assertTrue(
364 os.path.exists(os.path.join(self.tmpdir, 'amqp_config.py')))364 os.path.exists(os.path.join(self.tmpdir, 'amqp_config.py')))
@@ -366,7 +366,7 @@
366 def test_departed(self):366 def test_departed(self):
367 with open(os.path.join(self.tmpdir, 'amqp_config.py'), 'w') as f:367 with open(os.path.join(self.tmpdir, 'amqp_config.py'), 'w') as f:
368 f.write('')368 f.write('')
369 hooks.execute(['hooks/amqp-relation-departed'])369 hooks.hooks.execute(['hooks/amqp-relation-departed'])
370 self.wsgi_reload.assert_called_once_with()370 self.wsgi_reload.assert_called_once_with()
371371
372372
@@ -390,7 +390,7 @@
390 self.relation_get.side_effect = [{'user': 'data', 'host': 'h'}]390 self.relation_get.side_effect = [{'user': 'data', 'host': 'h'}]
391391
392 def test_pgsql_joined(self):392 def test_pgsql_joined(self):
393 hooks.execute(['hooks/pgsql-relation-joined'])393 hooks.hooks.execute(['hooks/pgsql-relation-joined'])
394 self.assertTrue(394 self.assertTrue(
395 os.path.exists(os.path.join(self.tmpdir, 'pgsql.json')))395 os.path.exists(os.path.join(self.tmpdir, 'pgsql.json')))
396 self.assertEqual(1, self.wsgi_reload.call_count)396 self.assertEqual(1, self.wsgi_reload.call_count)
@@ -402,7 +402,7 @@
402 f.write('rm $0 # just delete ourself')402 f.write('rm $0 # just delete ourself')
403 f.flush()403 f.flush()
404 self.config.return_value['db_migration_cmd'] = 'dash ./tmp.sh'404 self.config.return_value['db_migration_cmd'] = 'dash ./tmp.sh'
405 hooks.execute([hook])405 hooks.hooks.execute([hook])
406 if runs:406 if runs:
407 self.assertFalse(os.path.exists(os.path.join(cdir, 'tmp.sh')))407 self.assertFalse(os.path.exists(os.path.join(cdir, 'tmp.sh')))
408 else:408 else:
@@ -423,5 +423,5 @@
423 self._do_migration('hooks/config-changed')423 self._do_migration('hooks/config-changed')
424424
425 def test_pgsql_broken(self):425 def test_pgsql_broken(self):
426 hooks.execute(['hooks/pgsql-relation-broken'])426 hooks.hooks.execute(['hooks/pgsql-relation-broken'])
427 self.assertEqual(1, self.wsgi_reload.call_count)427 self.assertEqual(1, self.wsgi_reload.call_count)
428428
=== modified file 'testing/run_tests.py'
--- testing/run_tests.py 2014-09-25 15:52:12 +0000
+++ testing/run_tests.py 2014-09-29 18:45:52 +0000
@@ -15,12 +15,14 @@
15# along with this program. If not, see <http://www.gnu.org/licenses/>.15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16"""The CI Engine test runner."""16"""The CI Engine test runner."""
1717
18import imp
18import logging19import logging
19import os20import os
20import subunit21import subunit
21import sys22import sys
22import testtools23import testtools
2324import traceback
25import unittest
2426
25HERE = os.path.abspath(os.path.dirname(__file__))27HERE = os.path.abspath(os.path.dirname(__file__))
2628
@@ -336,16 +338,109 @@
336 return suite338 return suite
337339
338340
341class CharmTestSuite(unittest.TestSuite):
342 """Reloads charm modules before running the tests."""
343
344 def __init__(self, tests=()):
345 self.charm_root = None
346 super(CharmTestSuite, self).__init__(tests)
347
348 def setCharmRoots(self):
349 """Set 'charm_roots' for contained suites.
350
351 Calculates the 'charm_root' from the contained test 'id's
352 (see `prefix_test_ids` for details).
353 """
354 for s in self._tests:
355 flat_tests = list(filters.iter_flat(s))
356 # Empty test suites, obviously do not need charm_root.
357 if not flat_tests:
358 continue
359 # The mangled test IDs encode the charm path, which is common
360 # for all tests within a `TestSuite`. We grab the first test
361 # ID (charms.<series>.<name>) and turn it into a path
362 # (charms/<series>/<name>) that is set as the suite 'charm_root'.
363 s.charm_root = '/'.join(flat_tests[0].id().split('.')[:3])
364
365 def run(self, result):
366 """Reload local charm modules before running the inner tests.
367
368 Charm 'hooks' and 'charmhelpers' modules are reloaded in an
369 isolated environment before running the contained tests.
370 """
371 # Upper level TestSuite has no 'charm_root', we should do the
372 # usual run business.
373 if self.charm_root is None:
374 return super(CharmTestSuite, self).run(result)
375
376 with SysPath([os.path.join(self.charm_root, 'hooks')]):
377 for mod_name in ['charmhelpers', 'hooks']:
378 fp, path, desc = imp.find_module(mod_name)
379 try:
380 imp.load_module(mod_name, fp, path, desc)
381 finally:
382 if fp:
383 fp.close()
384
385 return super(CharmTestSuite, self).run(result)
386
387 def __call__(self, *args, **kwds):
388 """Call local implementation of 'run' """
389 return self.run(*args, **kwds)
390
391
392class CharmTestLoader(loaders.Loader):
393 """Override ucitests.loaders.Loader fix tests importing conflicts.
394
395 All UCI-E charms contain python unittest cases in the <charm>/unit_tests/
396 directory and since the default ucitest module importer only considers
397 the context tree for naming modules, they all look the same and only the
398 first import is really effective (all the other tests/modules are
399 shadowed).
400
401 This class re-implements `importFromPath` to load charms test cases with
402 a *unique* name (<charm>.<test>) in a way they will never conflict.
403 """
404
405 # Loads tests with a *special* `CharmTestSuite` class, which is capable
406 # of re-importing local charm modules before running the tests.
407 suiteClass = CharmTestSuite
408
409 def importFromPath(self, path):
410 """Import and return the module for a given file path.
411
412 Modules (packages, really) are ignored and always result in an
413 empty module. Only files (*.py) are imported.
414
415 Raises ImportError if the given python file path could not be
416 imported.
417 """
418 if not path.endswith('.py'):
419 return imp.new_module('always_empty')
420
421 imp_path = os.path.join(self.root, os.path.normpath(path))
422 package = self.root.split('/')[-1].replace('-', '_')
423 module = os.path.basename(imp_path)[:-3]
424 mod_name = '{}.{}'.format(package, module)
425
426 try:
427 return imp.load_source(mod_name, imp_path)
428 except ImportError:
429 tb = traceback.format_exc()
430 msg = 'Failed to import {} at {}:\n{}'.format(mod_name, path, tb)
431 raise ImportError(msg)
432
433
339def load_charm_tests(include_regexps, exclude_regexps=None):434def load_charm_tests(include_regexps, exclude_regexps=None):
340 """Load charm unittest from <charm>/unit_test."""435 """Load charm unittest from <charm>/unit_test."""
341 charms = [436 charms = [
437 'charms/precise/key-secret-subordinate',
342 'charms/precise/lander',438 'charms/precise/lander',
343 'charms/precise/rabbitmq-worker',439 'charms/precise/rabbitmq-worker',
440 'charms/precise/webui',
344 'charms/precise/wsgi-app',441 'charms/precise/wsgi-app',
345 'charms/precise/webui',
346 'charms/precise/key-secret-subordinate',
347 ]442 ]
348 loader = loaders.Loader()443 loader = CharmTestLoader()
349 suite = loader.suiteClass()444 suite = loader.suiteClass()
350 for c in charms:445 for c in charms:
351 with SysPath([c, os.path.join(c, 'hooks')]):446 with SysPath([c, os.path.join(c, 'hooks')]):
@@ -359,6 +454,9 @@
359 suite.addTests(charm_suite)454 suite.addTests(charm_suite)
360 suite = filters.include_regexps(include_regexps, suite)455 suite = filters.include_regexps(include_regexps, suite)
361 suite = filters.exclude_regexps(exclude_regexps, suite)456 suite = filters.exclude_regexps(exclude_regexps, suite)
457
458 suite.setCharmRoots()
459
362 return suite460 return suite
363461
364462

Subscribers

People subscribed via source and target branches