Merge lp:~cprov/uci-engine/charm-tests into lp:uci-engine
- charm-tests
- Merge into trunk
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 |
Related bugs: |
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/
PS Jenkins bot (ps-jenkins) wrote : | # |
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.
File "/home/
options.
File "/home/
suite.
File "/home/
s.charm_root = '/'.join(
IndexError: list index out of range
Marking the MP, needs fixing. Please feel free to ignore if i'm doing abs stupid :)
Joe Talbott (joetalbott) wrote : | # |
LGTM with one minor nit.
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:/
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.
- 814. By Celso Providelo
-
Addressing review comments.
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.
> File "/home/
> 565, in main
> options.
> File "/home/
> 448, in load_charm_tests
> suite.setCharmR
> File "/home/
> 356, in setCharmRoots
> s.charm_root = '/'.join(
> IndexError: list index out of range
>
>
> Marking the MP, needs fixing. Please feel free to ignore if i'm doing abs
> stupid :)
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.
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:814
http://
Executed test runs:
Click here to trigger a rebuild:
http://
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:/
> 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.
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.
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.
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_
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.
Para Siva (psivaa) wrote : | # |
Thanks Celso for the fix to handle empty suite. Approving before disappearing for the day.
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`)
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
1 | === modified file 'charms/precise/wsgi-app/unit_tests/test_hooks.py' | |||
2 | --- charms/precise/wsgi-app/unit_tests/test_hooks.py 2014-09-25 20:38:15 +0000 | |||
3 | +++ charms/precise/wsgi-app/unit_tests/test_hooks.py 2014-09-29 18:45:52 +0000 | |||
4 | @@ -21,8 +21,8 @@ | |||
5 | 21 | 21 | ||
6 | 22 | import yaml | 22 | import yaml |
7 | 23 | 23 | ||
10 | 24 | from intercom import hooks as intercom_hooks | 24 | import hooks |
11 | 25 | from hooks import hooks | 25 | import intercom |
12 | 26 | 26 | ||
13 | 27 | 27 | ||
14 | 28 | HERE = os.path.abspath(os.path.dirname(__file__)) | 28 | HERE = os.path.abspath(os.path.dirname(__file__)) |
15 | @@ -135,13 +135,13 @@ | |||
16 | 135 | self.revno = 'r1234_foo' | 135 | self.revno = 'r1234_foo' |
17 | 136 | 136 | ||
18 | 137 | def test_framework_restish(self): | 137 | def test_framework_restish(self): |
20 | 138 | hooks.execute(['hooks/install']) | 138 | hooks.hooks.execute(['hooks/install']) |
21 | 139 | pkgs = self.apt_install.call_args_list[0][0][0] | 139 | pkgs = self.apt_install.call_args_list[0][0][0] |
22 | 140 | self.assertIn('python-restish', pkgs) | 140 | self.assertIn('python-restish', pkgs) |
23 | 141 | 141 | ||
24 | 142 | def test_framework_pyramid(self): | 142 | def test_framework_pyramid(self): |
25 | 143 | self.config.return_value['framework'] = 'pyramid' | 143 | self.config.return_value['framework'] = 'pyramid' |
27 | 144 | hooks.execute(['hooks/install']) | 144 | hooks.hooks.execute(['hooks/install']) |
28 | 145 | pkgs = self.apt_install.call_args_list[0][0][0] | 145 | pkgs = self.apt_install.call_args_list[0][0][0] |
29 | 146 | self.assertIn('python3-pyramid', pkgs) | 146 | self.assertIn('python3-pyramid', pkgs) |
30 | 147 | 147 | ||
31 | @@ -194,7 +194,7 @@ | |||
32 | 194 | cron_file_path = os.path.join(self._cron_root(), 'local-test') | 194 | cron_file_path = os.path.join(self._cron_root(), 'local-test') |
33 | 195 | with open(cron_file_path, 'w') as fd: | 195 | with open(cron_file_path, 'w') as fd: |
34 | 196 | fd.write('removed') | 196 | fd.write('removed') |
36 | 197 | hooks.execute(['hooks/config-changed']) | 197 | hooks.hooks.execute(['hooks/config-changed']) |
37 | 198 | self.assertEqual([], os.listdir(self._cron_root())) | 198 | self.assertEqual([], os.listdir(self._cron_root())) |
38 | 199 | self.service_restart.assert_called_once_with('cron') | 199 | self.service_restart.assert_called_once_with('cron') |
39 | 200 | 200 | ||
40 | @@ -203,7 +203,7 @@ | |||
41 | 203 | self.config.return_value['cron_schedule'] = '*/10 * * * *' | 203 | self.config.return_value['cron_schedule'] = '*/10 * * * *' |
42 | 204 | self.config.return_value['cron_cmd'] = 'foo.sh' | 204 | self.config.return_value['cron_cmd'] = 'foo.sh' |
43 | 205 | self.config.return_value['wsgi_user'] = 'bar' | 205 | self.config.return_value['wsgi_user'] = 'bar' |
45 | 206 | hooks.execute(['hooks/config-changed']) | 206 | hooks.hooks.execute(['hooks/config-changed']) |
46 | 207 | self.assertEqual(['local-test'], os.listdir(self._cron_root())) | 207 | self.assertEqual(['local-test'], os.listdir(self._cron_root())) |
47 | 208 | code_dir = os.path.join(self.tmpdir, 'code') | 208 | code_dir = os.path.join(self.tmpdir, 'code') |
48 | 209 | logs_dir = os.path.join(self.tmpdir, 'logs') | 209 | logs_dir = os.path.join(self.tmpdir, 'logs') |
49 | @@ -221,7 +221,7 @@ | |||
50 | 221 | self.service_restart.assert_called_once_with('cron') | 221 | self.service_restart.assert_called_once_with('cron') |
51 | 222 | 222 | ||
52 | 223 | def test_allowed_hosts(self): | 223 | def test_allowed_hosts(self): |
54 | 224 | hooks.execute(['hooks/config-changed']) | 224 | hooks.hooks.execute(['hooks/config-changed']) |
55 | 225 | self.assertTrue( | 225 | self.assertTrue( |
56 | 226 | os.path.exists(os.path.join(self.tmpdir, 'allowed_hosts.json'))) | 226 | os.path.exists(os.path.join(self.tmpdir, 'allowed_hosts.json'))) |
57 | 227 | 227 | ||
58 | @@ -265,7 +265,7 @@ | |||
59 | 265 | def test_intercom_changed(self): | 265 | def test_intercom_changed(self): |
60 | 266 | # New 'intercom' relations are stored in the JSON registry | 266 | # New 'intercom' relations are stored in the JSON registry |
61 | 267 | # as <hostname>:<port> | 267 | # as <hostname>:<port> |
63 | 268 | intercom_hooks.execute(['hooks/intercom-relation-changed']) | 268 | intercom.hooks.execute(['hooks/intercom-relation-changed']) |
64 | 269 | self.log.assert_called_with( | 269 | self.log.assert_called_with( |
65 | 270 | "Intercom unit 'remote-test' (remote.com) joined.") | 270 | "Intercom unit 'remote-test' (remote.com) joined.") |
66 | 271 | self.assertEquals( | 271 | self.assertEquals( |
67 | @@ -285,7 +285,7 @@ | |||
68 | 285 | '2': {'hostname': '10.0.0.2', | 285 | '2': {'hostname': '10.0.0.2', |
69 | 286 | 'port': '9000', | 286 | 'port': '9000', |
70 | 287 | 'unit': 'preserved'}}) | 287 | 'unit': 'preserved'}}) |
72 | 288 | intercom_hooks.execute(['hooks/intercom-relation-broken']) | 288 | intercom.hooks.execute(['hooks/intercom-relation-broken']) |
73 | 289 | self.log.assert_called_with("Intercom unit '' () departed.") | 289 | self.log.assert_called_with("Intercom unit '' () departed.") |
74 | 290 | self.assertEquals( | 290 | self.assertEquals( |
75 | 291 | {'2': {'hostname': '10.0.0.2', | 291 | {'2': {'hostname': '10.0.0.2', |
76 | @@ -314,7 +314,7 @@ | |||
77 | 314 | class TestWSGIHooks(RestishTestCase): | 314 | class TestWSGIHooks(RestishTestCase): |
78 | 315 | 315 | ||
79 | 316 | def test_wsgi_joined(self): | 316 | def test_wsgi_joined(self): |
81 | 317 | hooks.execute(['hooks/wsgi-relation-joined']) | 317 | hooks.hooks.execute(['hooks/wsgi-relation-joined']) |
82 | 318 | self.open_port.assert_called_once_with(8080) | 318 | self.open_port.assert_called_once_with(8080) |
83 | 319 | expected = { | 319 | expected = { |
84 | 320 | 'python_path': '', | 320 | 'python_path': '', |
85 | @@ -329,7 +329,7 @@ | |||
86 | 329 | '/srv/local-test/logs', 'www-data', 'www-data', 0755) | 329 | '/srv/local-test/logs', 'www-data', 'www-data', 0755) |
87 | 330 | 330 | ||
88 | 331 | def test_wsgi_broken(self): | 331 | def test_wsgi_broken(self): |
90 | 332 | hooks.execute(['hooks/wsgi-relation-broken']) | 332 | hooks.hooks.execute(['hooks/wsgi-relation-broken']) |
91 | 333 | self.close_port.assert_called_once_with(8080) | 333 | self.close_port.assert_called_once_with(8080) |
92 | 334 | 334 | ||
93 | 335 | 335 | ||
94 | @@ -337,7 +337,7 @@ | |||
95 | 337 | 337 | ||
96 | 338 | def test_json_status_joined(self): | 338 | def test_json_status_joined(self): |
97 | 339 | self.config.return_value['json_status_path'] = 'foo' | 339 | self.config.return_value['json_status_path'] = 'foo' |
99 | 340 | hooks.execute(['hooks/json_status-relation-joined']) | 340 | hooks.hooks.execute(['hooks/json_status-relation-joined']) |
100 | 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]) |
101 | 342 | 342 | ||
102 | 343 | 343 | ||
103 | @@ -358,7 +358,7 @@ | |||
104 | 358 | } | 358 | } |
105 | 359 | 359 | ||
106 | 360 | def test_changed(self): | 360 | def test_changed(self): |
108 | 361 | hooks.execute(['hooks/amqp-relation-changed']) | 361 | hooks.hooks.execute(['hooks/amqp-relation-changed']) |
109 | 362 | self.wsgi_reload.assert_called_once_with() | 362 | self.wsgi_reload.assert_called_once_with() |
110 | 363 | self.assertTrue( | 363 | self.assertTrue( |
111 | 364 | os.path.exists(os.path.join(self.tmpdir, 'amqp_config.py'))) | 364 | os.path.exists(os.path.join(self.tmpdir, 'amqp_config.py'))) |
112 | @@ -366,7 +366,7 @@ | |||
113 | 366 | def test_departed(self): | 366 | def test_departed(self): |
114 | 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: |
115 | 368 | f.write('') | 368 | f.write('') |
117 | 369 | hooks.execute(['hooks/amqp-relation-departed']) | 369 | hooks.hooks.execute(['hooks/amqp-relation-departed']) |
118 | 370 | self.wsgi_reload.assert_called_once_with() | 370 | self.wsgi_reload.assert_called_once_with() |
119 | 371 | 371 | ||
120 | 372 | 372 | ||
121 | @@ -390,7 +390,7 @@ | |||
122 | 390 | self.relation_get.side_effect = [{'user': 'data', 'host': 'h'}] | 390 | self.relation_get.side_effect = [{'user': 'data', 'host': 'h'}] |
123 | 391 | 391 | ||
124 | 392 | def test_pgsql_joined(self): | 392 | def test_pgsql_joined(self): |
126 | 393 | hooks.execute(['hooks/pgsql-relation-joined']) | 393 | hooks.hooks.execute(['hooks/pgsql-relation-joined']) |
127 | 394 | self.assertTrue( | 394 | self.assertTrue( |
128 | 395 | os.path.exists(os.path.join(self.tmpdir, 'pgsql.json'))) | 395 | os.path.exists(os.path.join(self.tmpdir, 'pgsql.json'))) |
129 | 396 | self.assertEqual(1, self.wsgi_reload.call_count) | 396 | self.assertEqual(1, self.wsgi_reload.call_count) |
130 | @@ -402,7 +402,7 @@ | |||
131 | 402 | f.write('rm $0 # just delete ourself') | 402 | f.write('rm $0 # just delete ourself') |
132 | 403 | f.flush() | 403 | f.flush() |
133 | 404 | self.config.return_value['db_migration_cmd'] = 'dash ./tmp.sh' | 404 | self.config.return_value['db_migration_cmd'] = 'dash ./tmp.sh' |
135 | 405 | hooks.execute([hook]) | 405 | hooks.hooks.execute([hook]) |
136 | 406 | if runs: | 406 | if runs: |
137 | 407 | self.assertFalse(os.path.exists(os.path.join(cdir, 'tmp.sh'))) | 407 | self.assertFalse(os.path.exists(os.path.join(cdir, 'tmp.sh'))) |
138 | 408 | else: | 408 | else: |
139 | @@ -423,5 +423,5 @@ | |||
140 | 423 | self._do_migration('hooks/config-changed') | 423 | self._do_migration('hooks/config-changed') |
141 | 424 | 424 | ||
142 | 425 | def test_pgsql_broken(self): | 425 | def test_pgsql_broken(self): |
144 | 426 | hooks.execute(['hooks/pgsql-relation-broken']) | 426 | hooks.hooks.execute(['hooks/pgsql-relation-broken']) |
145 | 427 | self.assertEqual(1, self.wsgi_reload.call_count) | 427 | self.assertEqual(1, self.wsgi_reload.call_count) |
146 | 428 | 428 | ||
147 | === modified file 'testing/run_tests.py' | |||
148 | --- testing/run_tests.py 2014-09-25 15:52:12 +0000 | |||
149 | +++ testing/run_tests.py 2014-09-29 18:45:52 +0000 | |||
150 | @@ -15,12 +15,14 @@ | |||
151 | 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/>. |
152 | 16 | """The CI Engine test runner.""" | 16 | """The CI Engine test runner.""" |
153 | 17 | 17 | ||
154 | 18 | import imp | ||
155 | 18 | import logging | 19 | import logging |
156 | 19 | import os | 20 | import os |
157 | 20 | import subunit | 21 | import subunit |
158 | 21 | import sys | 22 | import sys |
159 | 22 | import testtools | 23 | import testtools |
161 | 23 | 24 | import traceback | |
162 | 25 | import unittest | ||
163 | 24 | 26 | ||
164 | 25 | HERE = os.path.abspath(os.path.dirname(__file__)) | 27 | HERE = os.path.abspath(os.path.dirname(__file__)) |
165 | 26 | 28 | ||
166 | @@ -336,16 +338,109 @@ | |||
167 | 336 | return suite | 338 | return suite |
168 | 337 | 339 | ||
169 | 338 | 340 | ||
170 | 341 | class CharmTestSuite(unittest.TestSuite): | ||
171 | 342 | """Reloads charm modules before running the tests.""" | ||
172 | 343 | |||
173 | 344 | def __init__(self, tests=()): | ||
174 | 345 | self.charm_root = None | ||
175 | 346 | super(CharmTestSuite, self).__init__(tests) | ||
176 | 347 | |||
177 | 348 | def setCharmRoots(self): | ||
178 | 349 | """Set 'charm_roots' for contained suites. | ||
179 | 350 | |||
180 | 351 | Calculates the 'charm_root' from the contained test 'id's | ||
181 | 352 | (see `prefix_test_ids` for details). | ||
182 | 353 | """ | ||
183 | 354 | for s in self._tests: | ||
184 | 355 | flat_tests = list(filters.iter_flat(s)) | ||
185 | 356 | # Empty test suites, obviously do not need charm_root. | ||
186 | 357 | if not flat_tests: | ||
187 | 358 | continue | ||
188 | 359 | # The mangled test IDs encode the charm path, which is common | ||
189 | 360 | # for all tests within a `TestSuite`. We grab the first test | ||
190 | 361 | # ID (charms.<series>.<name>) and turn it into a path | ||
191 | 362 | # (charms/<series>/<name>) that is set as the suite 'charm_root'. | ||
192 | 363 | s.charm_root = '/'.join(flat_tests[0].id().split('.')[:3]) | ||
193 | 364 | |||
194 | 365 | def run(self, result): | ||
195 | 366 | """Reload local charm modules before running the inner tests. | ||
196 | 367 | |||
197 | 368 | Charm 'hooks' and 'charmhelpers' modules are reloaded in an | ||
198 | 369 | isolated environment before running the contained tests. | ||
199 | 370 | """ | ||
200 | 371 | # Upper level TestSuite has no 'charm_root', we should do the | ||
201 | 372 | # usual run business. | ||
202 | 373 | if self.charm_root is None: | ||
203 | 374 | return super(CharmTestSuite, self).run(result) | ||
204 | 375 | |||
205 | 376 | with SysPath([os.path.join(self.charm_root, 'hooks')]): | ||
206 | 377 | for mod_name in ['charmhelpers', 'hooks']: | ||
207 | 378 | fp, path, desc = imp.find_module(mod_name) | ||
208 | 379 | try: | ||
209 | 380 | imp.load_module(mod_name, fp, path, desc) | ||
210 | 381 | finally: | ||
211 | 382 | if fp: | ||
212 | 383 | fp.close() | ||
213 | 384 | |||
214 | 385 | return super(CharmTestSuite, self).run(result) | ||
215 | 386 | |||
216 | 387 | def __call__(self, *args, **kwds): | ||
217 | 388 | """Call local implementation of 'run' """ | ||
218 | 389 | return self.run(*args, **kwds) | ||
219 | 390 | |||
220 | 391 | |||
221 | 392 | class CharmTestLoader(loaders.Loader): | ||
222 | 393 | """Override ucitests.loaders.Loader fix tests importing conflicts. | ||
223 | 394 | |||
224 | 395 | All UCI-E charms contain python unittest cases in the <charm>/unit_tests/ | ||
225 | 396 | directory and since the default ucitest module importer only considers | ||
226 | 397 | the context tree for naming modules, they all look the same and only the | ||
227 | 398 | first import is really effective (all the other tests/modules are | ||
228 | 399 | shadowed). | ||
229 | 400 | |||
230 | 401 | This class re-implements `importFromPath` to load charms test cases with | ||
231 | 402 | a *unique* name (<charm>.<test>) in a way they will never conflict. | ||
232 | 403 | """ | ||
233 | 404 | |||
234 | 405 | # Loads tests with a *special* `CharmTestSuite` class, which is capable | ||
235 | 406 | # of re-importing local charm modules before running the tests. | ||
236 | 407 | suiteClass = CharmTestSuite | ||
237 | 408 | |||
238 | 409 | def importFromPath(self, path): | ||
239 | 410 | """Import and return the module for a given file path. | ||
240 | 411 | |||
241 | 412 | Modules (packages, really) are ignored and always result in an | ||
242 | 413 | empty module. Only files (*.py) are imported. | ||
243 | 414 | |||
244 | 415 | Raises ImportError if the given python file path could not be | ||
245 | 416 | imported. | ||
246 | 417 | """ | ||
247 | 418 | if not path.endswith('.py'): | ||
248 | 419 | return imp.new_module('always_empty') | ||
249 | 420 | |||
250 | 421 | imp_path = os.path.join(self.root, os.path.normpath(path)) | ||
251 | 422 | package = self.root.split('/')[-1].replace('-', '_') | ||
252 | 423 | module = os.path.basename(imp_path)[:-3] | ||
253 | 424 | mod_name = '{}.{}'.format(package, module) | ||
254 | 425 | |||
255 | 426 | try: | ||
256 | 427 | return imp.load_source(mod_name, imp_path) | ||
257 | 428 | except ImportError: | ||
258 | 429 | tb = traceback.format_exc() | ||
259 | 430 | msg = 'Failed to import {} at {}:\n{}'.format(mod_name, path, tb) | ||
260 | 431 | raise ImportError(msg) | ||
261 | 432 | |||
262 | 433 | |||
263 | 339 | def load_charm_tests(include_regexps, exclude_regexps=None): | 434 | def load_charm_tests(include_regexps, exclude_regexps=None): |
264 | 340 | """Load charm unittest from <charm>/unit_test.""" | 435 | """Load charm unittest from <charm>/unit_test.""" |
265 | 341 | charms = [ | 436 | charms = [ |
266 | 437 | 'charms/precise/key-secret-subordinate', | ||
267 | 342 | 'charms/precise/lander', | 438 | 'charms/precise/lander', |
268 | 343 | 'charms/precise/rabbitmq-worker', | 439 | 'charms/precise/rabbitmq-worker', |
269 | 440 | 'charms/precise/webui', | ||
270 | 344 | 'charms/precise/wsgi-app', | 441 | 'charms/precise/wsgi-app', |
271 | 345 | 'charms/precise/webui', | ||
272 | 346 | 'charms/precise/key-secret-subordinate', | ||
273 | 347 | ] | 442 | ] |
275 | 348 | loader = loaders.Loader() | 443 | loader = CharmTestLoader() |
276 | 349 | suite = loader.suiteClass() | 444 | suite = loader.suiteClass() |
277 | 350 | for c in charms: | 445 | for c in charms: |
278 | 351 | with SysPath([c, os.path.join(c, 'hooks')]): | 446 | with SysPath([c, os.path.join(c, 'hooks')]): |
279 | @@ -359,6 +454,9 @@ | |||
280 | 359 | suite.addTests(charm_suite) | 454 | suite.addTests(charm_suite) |
281 | 360 | suite = filters.include_regexps(include_regexps, suite) | 455 | suite = filters.include_regexps(include_regexps, suite) |
282 | 361 | suite = filters.exclude_regexps(exclude_regexps, suite) | 456 | suite = filters.exclude_regexps(exclude_regexps, suite) |
283 | 457 | |||
284 | 458 | suite.setCharmRoots() | ||
285 | 459 | |||
286 | 362 | return suite | 460 | return suite |
287 | 363 | 461 | ||
288 | 364 | 462 |
PASSED: Continuous integration, rev:813 s-jenkins. ubuntu- ci:8080/ job/uci- engine- ci/1483/
http://
Executed test runs:
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/uci- engine- ci/1483/ rebuild
http://