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
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
6 import yaml
7
8-from intercom import hooks as intercom_hooks
9-from hooks import hooks
10+import hooks
11+import intercom
12
13
14 HERE = os.path.abspath(os.path.dirname(__file__))
15@@ -135,13 +135,13 @@
16 self.revno = 'r1234_foo'
17
18 def test_framework_restish(self):
19- hooks.execute(['hooks/install'])
20+ hooks.hooks.execute(['hooks/install'])
21 pkgs = self.apt_install.call_args_list[0][0][0]
22 self.assertIn('python-restish', pkgs)
23
24 def test_framework_pyramid(self):
25 self.config.return_value['framework'] = 'pyramid'
26- hooks.execute(['hooks/install'])
27+ hooks.hooks.execute(['hooks/install'])
28 pkgs = self.apt_install.call_args_list[0][0][0]
29 self.assertIn('python3-pyramid', pkgs)
30
31@@ -194,7 +194,7 @@
32 cron_file_path = os.path.join(self._cron_root(), 'local-test')
33 with open(cron_file_path, 'w') as fd:
34 fd.write('removed')
35- hooks.execute(['hooks/config-changed'])
36+ hooks.hooks.execute(['hooks/config-changed'])
37 self.assertEqual([], os.listdir(self._cron_root()))
38 self.service_restart.assert_called_once_with('cron')
39
40@@ -203,7 +203,7 @@
41 self.config.return_value['cron_schedule'] = '*/10 * * * *'
42 self.config.return_value['cron_cmd'] = 'foo.sh'
43 self.config.return_value['wsgi_user'] = 'bar'
44- hooks.execute(['hooks/config-changed'])
45+ hooks.hooks.execute(['hooks/config-changed'])
46 self.assertEqual(['local-test'], os.listdir(self._cron_root()))
47 code_dir = os.path.join(self.tmpdir, 'code')
48 logs_dir = os.path.join(self.tmpdir, 'logs')
49@@ -221,7 +221,7 @@
50 self.service_restart.assert_called_once_with('cron')
51
52 def test_allowed_hosts(self):
53- hooks.execute(['hooks/config-changed'])
54+ hooks.hooks.execute(['hooks/config-changed'])
55 self.assertTrue(
56 os.path.exists(os.path.join(self.tmpdir, 'allowed_hosts.json')))
57
58@@ -265,7 +265,7 @@
59 def test_intercom_changed(self):
60 # New 'intercom' relations are stored in the JSON registry
61 # as <hostname>:<port>
62- intercom_hooks.execute(['hooks/intercom-relation-changed'])
63+ intercom.hooks.execute(['hooks/intercom-relation-changed'])
64 self.log.assert_called_with(
65 "Intercom unit 'remote-test' (remote.com) joined.")
66 self.assertEquals(
67@@ -285,7 +285,7 @@
68 '2': {'hostname': '10.0.0.2',
69 'port': '9000',
70 'unit': 'preserved'}})
71- intercom_hooks.execute(['hooks/intercom-relation-broken'])
72+ intercom.hooks.execute(['hooks/intercom-relation-broken'])
73 self.log.assert_called_with("Intercom unit '' () departed.")
74 self.assertEquals(
75 {'2': {'hostname': '10.0.0.2',
76@@ -314,7 +314,7 @@
77 class TestWSGIHooks(RestishTestCase):
78
79 def test_wsgi_joined(self):
80- hooks.execute(['hooks/wsgi-relation-joined'])
81+ hooks.hooks.execute(['hooks/wsgi-relation-joined'])
82 self.open_port.assert_called_once_with(8080)
83 expected = {
84 'python_path': '',
85@@ -329,7 +329,7 @@
86 '/srv/local-test/logs', 'www-data', 'www-data', 0755)
87
88 def test_wsgi_broken(self):
89- hooks.execute(['hooks/wsgi-relation-broken'])
90+ hooks.hooks.execute(['hooks/wsgi-relation-broken'])
91 self.close_port.assert_called_once_with(8080)
92
93
94@@ -337,7 +337,7 @@
95
96 def test_json_status_joined(self):
97 self.config.return_value['json_status_path'] = 'foo'
98- hooks.execute(['hooks/json_status-relation-joined'])
99+ hooks.hooks.execute(['hooks/json_status-relation-joined'])
100 self.assertIn('status-url', self.relation_set.call_args_list[0][0][1])
101
102
103@@ -358,7 +358,7 @@
104 }
105
106 def test_changed(self):
107- hooks.execute(['hooks/amqp-relation-changed'])
108+ hooks.hooks.execute(['hooks/amqp-relation-changed'])
109 self.wsgi_reload.assert_called_once_with()
110 self.assertTrue(
111 os.path.exists(os.path.join(self.tmpdir, 'amqp_config.py')))
112@@ -366,7 +366,7 @@
113 def test_departed(self):
114 with open(os.path.join(self.tmpdir, 'amqp_config.py'), 'w') as f:
115 f.write('')
116- hooks.execute(['hooks/amqp-relation-departed'])
117+ hooks.hooks.execute(['hooks/amqp-relation-departed'])
118 self.wsgi_reload.assert_called_once_with()
119
120
121@@ -390,7 +390,7 @@
122 self.relation_get.side_effect = [{'user': 'data', 'host': 'h'}]
123
124 def test_pgsql_joined(self):
125- hooks.execute(['hooks/pgsql-relation-joined'])
126+ hooks.hooks.execute(['hooks/pgsql-relation-joined'])
127 self.assertTrue(
128 os.path.exists(os.path.join(self.tmpdir, 'pgsql.json')))
129 self.assertEqual(1, self.wsgi_reload.call_count)
130@@ -402,7 +402,7 @@
131 f.write('rm $0 # just delete ourself')
132 f.flush()
133 self.config.return_value['db_migration_cmd'] = 'dash ./tmp.sh'
134- hooks.execute([hook])
135+ hooks.hooks.execute([hook])
136 if runs:
137 self.assertFalse(os.path.exists(os.path.join(cdir, 'tmp.sh')))
138 else:
139@@ -423,5 +423,5 @@
140 self._do_migration('hooks/config-changed')
141
142 def test_pgsql_broken(self):
143- hooks.execute(['hooks/pgsql-relation-broken'])
144+ hooks.hooks.execute(['hooks/pgsql-relation-broken'])
145 self.assertEqual(1, self.wsgi_reload.call_count)
146
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 # along with this program. If not, see <http://www.gnu.org/licenses/>.
152 """The CI Engine test runner."""
153
154+import imp
155 import logging
156 import os
157 import subunit
158 import sys
159 import testtools
160-
161+import traceback
162+import unittest
163
164 HERE = os.path.abspath(os.path.dirname(__file__))
165
166@@ -336,16 +338,109 @@
167 return suite
168
169
170+class CharmTestSuite(unittest.TestSuite):
171+ """Reloads charm modules before running the tests."""
172+
173+ def __init__(self, tests=()):
174+ self.charm_root = None
175+ super(CharmTestSuite, self).__init__(tests)
176+
177+ def setCharmRoots(self):
178+ """Set 'charm_roots' for contained suites.
179+
180+ Calculates the 'charm_root' from the contained test 'id's
181+ (see `prefix_test_ids` for details).
182+ """
183+ for s in self._tests:
184+ flat_tests = list(filters.iter_flat(s))
185+ # Empty test suites, obviously do not need charm_root.
186+ if not flat_tests:
187+ continue
188+ # The mangled test IDs encode the charm path, which is common
189+ # for all tests within a `TestSuite`. We grab the first test
190+ # ID (charms.<series>.<name>) and turn it into a path
191+ # (charms/<series>/<name>) that is set as the suite 'charm_root'.
192+ s.charm_root = '/'.join(flat_tests[0].id().split('.')[:3])
193+
194+ def run(self, result):
195+ """Reload local charm modules before running the inner tests.
196+
197+ Charm 'hooks' and 'charmhelpers' modules are reloaded in an
198+ isolated environment before running the contained tests.
199+ """
200+ # Upper level TestSuite has no 'charm_root', we should do the
201+ # usual run business.
202+ if self.charm_root is None:
203+ return super(CharmTestSuite, self).run(result)
204+
205+ with SysPath([os.path.join(self.charm_root, 'hooks')]):
206+ for mod_name in ['charmhelpers', 'hooks']:
207+ fp, path, desc = imp.find_module(mod_name)
208+ try:
209+ imp.load_module(mod_name, fp, path, desc)
210+ finally:
211+ if fp:
212+ fp.close()
213+
214+ return super(CharmTestSuite, self).run(result)
215+
216+ def __call__(self, *args, **kwds):
217+ """Call local implementation of 'run' """
218+ return self.run(*args, **kwds)
219+
220+
221+class CharmTestLoader(loaders.Loader):
222+ """Override ucitests.loaders.Loader fix tests importing conflicts.
223+
224+ All UCI-E charms contain python unittest cases in the <charm>/unit_tests/
225+ directory and since the default ucitest module importer only considers
226+ the context tree for naming modules, they all look the same and only the
227+ first import is really effective (all the other tests/modules are
228+ shadowed).
229+
230+ This class re-implements `importFromPath` to load charms test cases with
231+ a *unique* name (<charm>.<test>) in a way they will never conflict.
232+ """
233+
234+ # Loads tests with a *special* `CharmTestSuite` class, which is capable
235+ # of re-importing local charm modules before running the tests.
236+ suiteClass = CharmTestSuite
237+
238+ def importFromPath(self, path):
239+ """Import and return the module for a given file path.
240+
241+ Modules (packages, really) are ignored and always result in an
242+ empty module. Only files (*.py) are imported.
243+
244+ Raises ImportError if the given python file path could not be
245+ imported.
246+ """
247+ if not path.endswith('.py'):
248+ return imp.new_module('always_empty')
249+
250+ imp_path = os.path.join(self.root, os.path.normpath(path))
251+ package = self.root.split('/')[-1].replace('-', '_')
252+ module = os.path.basename(imp_path)[:-3]
253+ mod_name = '{}.{}'.format(package, module)
254+
255+ try:
256+ return imp.load_source(mod_name, imp_path)
257+ except ImportError:
258+ tb = traceback.format_exc()
259+ msg = 'Failed to import {} at {}:\n{}'.format(mod_name, path, tb)
260+ raise ImportError(msg)
261+
262+
263 def load_charm_tests(include_regexps, exclude_regexps=None):
264 """Load charm unittest from <charm>/unit_test."""
265 charms = [
266+ 'charms/precise/key-secret-subordinate',
267 'charms/precise/lander',
268 'charms/precise/rabbitmq-worker',
269+ 'charms/precise/webui',
270 'charms/precise/wsgi-app',
271- 'charms/precise/webui',
272- 'charms/precise/key-secret-subordinate',
273 ]
274- loader = loaders.Loader()
275+ loader = CharmTestLoader()
276 suite = loader.suiteClass()
277 for c in charms:
278 with SysPath([c, os.path.join(c, 'hooks')]):
279@@ -359,6 +454,9 @@
280 suite.addTests(charm_suite)
281 suite = filters.include_regexps(include_regexps, suite)
282 suite = filters.exclude_regexps(exclude_regexps, suite)
283+
284+ suite.setCharmRoots()
285+
286 return suite
287
288

Subscribers

People subscribed via source and target branches