Merge lp:~vila/selenium-simple-test/test-loader into lp:selenium-simple-test
- test-loader
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Vincent Ladeuil |
Approved revision: | 448 |
Merged at revision: | 404 |
Proposed branch: | lp:~vila/selenium-simple-test/test-loader |
Merge into: | lp:selenium-simple-test |
Diff against target: |
4066 lines (+2573/-1001) 36 files modified
docs/changelog.rst (+1/-0) src/sst/actions.py (+1/-0) src/sst/browsers.py (+121/-0) src/sst/cases.py (+233/-0) src/sst/command.py (+19/-11) src/sst/filters.py (+97/-0) src/sst/loader.py (+409/-0) src/sst/result.py (+104/-0) src/sst/runtests.py (+76/-550) src/sst/scripts/remote.py (+3/-0) src/sst/scripts/run.py (+10/-2) src/sst/selftests/context.py (+3/-2) src/sst/selftests/importing.py (+0/-4) src/sst/selftests/regular/__init__.py (+4/-0) src/sst/selftests/regular/test_testtools_testcase.py (+0/-3) src/sst/selftests/regular/test_two_methods.py (+4/-4) src/sst/selftests/shared/helpers.py (+3/-1) src/sst/selftests/static_file.py (+2/-2) src/sst/tests/__init__.py (+88/-4) src/sst/tests/test_command.py (+62/-0) src/sst/tests/test_django_devserver.py (+10/-8) src/sst/tests/test_filters.py (+142/-0) src/sst/tests/test_loader.py (+577/-0) src/sst/tests/test_protect_imports.py (+86/-0) src/sst/tests/test_result.py (+254/-152) src/sst/tests/test_runtests.py (+92/-0) src/sst/tests/test_runtests_find_cases.py (+0/-124) src/sst/tests/test_runtests_get_suites.py (+0/-116) src/sst/tests/test_sst_run.py (+4/-6) src/sst/tests/test_sst_script_test_case.py (+6/-4) src/sst/tests/test_write_tree.py (+94/-0) src/sst/tests/test_xvfb.py (+2/-2) src/sst/xvfbdisplay.py (+15/-0) sst-remote (+6/-3) sst-run (+6/-3) test-loader.TODO (+39/-0) |
To merge this branch: | bzr merge lp:~vila/selenium-simple-test/test-loader |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Leo Arias (community) | code review | Approve | |
Review via email:
|
Commit message
Implement a test loader and options to include or exclude tests to run
Description of the change
This implements test filtering by adding two new options to sst-run: --includes and --excludes.
They allow one to run a subset of the tests by specifying one or several prefixes matching the test ids. Test being organized as a tree, using prefixes is simple and powerful way to reduce the scope of the test run.
This required a new test loader that address some limitations in the stock unittest one required for sst specific needs.
Those limitations are mainly two:
- scripts cannot be imported at load time (sst specifically compile them
instead),
- unittest.discover defines a single pattern for matching both directories
and files (sst has different rules for each).
Moreover, even providing separate patterns for dirs and files is not enough. sst allows scripts and regular tests to be mixed in the same tree.
The proposed loader offers a way to define specific rules for a subtree by providing them in the __init__.py file.
The limitation is that the same directory cannot contain both scripts and regular files unless the __init__.py file defines which are which and load them appropriately.
The import/not import behavior also clarifies some constraints on sys.path, while scripts do not require any special sys.path, regular tests generally can only be imported from a specific entry point. Since this cannot be guessed reliably, the user has to set it up properly before calling sst-run.
'shared' on the other hand still add itself to sys.path, we may want to revisit that too.
The sys.path requirements (surprisingly) triggered some more cleanups in the existing code where absolute paths were used instead of relative ones.
I've added a bunch of unit tests that should make it easier to refactor the test loader and its companion classes and some helpers that make it easier to test it.
I've also changed the test ids to reflect their path as a pre-requisite to filtering (we weren't doing that previously leading to duplicate test ids if a script was existing under the same base name in different directories.
I did some sprint cleanup on my way which explain partly the size of the proposal.
There are now 97 unit tests (vs the initial ~35) which should help ;) So I'm pretty confident that the introduced features are not too bogus. But since I rewrote code that didn't have corresponding tests, I may have break support for obscure use cases. By contrast, removing get_suites and friends was easier with the existing test_runtests_
I've put some aliases in runtests for the newly introduced 'case' and 'browsers' modules. We should decide how to publicize the new layout and how we want to deprecate these aliases.
This proposal is already quite big and while I left some items in the test-loader-TODO file, the bulk of it is ready for feedback.
Warning for reviewers: things may be clearer if review locally and inspect each commit in isolation.
- 435. By Vincent Ladeuil
-
Merge trunk resolving conflicts
- 436. By Vincent Ladeuil
-
Merge trunk
- 437. By Vincent Ladeuil
-
Merge fix for run_django
- 438. By Vincent Ladeuil
-
Merge fixes for result
- 439. By Vincent Ladeuil
-
Ensures that tests defined in the __init__ file are properly loaded.
- 440. By Vincent Ladeuil
-
Don't import variables from modules.
- 441. By Vincent Ladeuil
-
Simpler Loaders implementation.
- 442. By Vincent Ladeuil
-
Split filtering from loader.py into its own filters.py module, provide tested helpers to simplify runtests.
- 443. By Vincent Ladeuil
-
Define and use helpers that make it easier to define discover in __init__.py files. Change the discover signature to match the real needs. Update the TODO. All test ids now include the python path of the file where they are defined
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Leo Arias (elopio) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Leo Arias (elopio) wrote : | # |
726 + """Load tests for a tree containing scrits.
Typo: scriPts
736 + regexp = '^shared$|^_'
The name of the shared file can be parameterized with the -m command line option. Having it hard-coded seems wrong.
752 + # FIXME
Can you file a bug for that? Seems nice to have, not so hard.
787 + # FIXME: This swallows exceptions raise the by the user defined
Typos: raiseD _ by
788 + # 'load_test'. We may want to give awy to expose them instead (with
Typo: a _ wAy
822 + """Load test from an sst tree.
Typo: testS
829 + This also provide ways for packages to define the test loading as they see
Typo: provideS
1886 +assert (config.
1887 + == os.path.
According to pep8, the break should be after the operator.
2020 + :param description: A text where files and directories contents is
I'm curious about where did you get this style for documenting parameters. The example from http://
(I'm not done yet...)
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Leo Arias (elopio) wrote : | # |
2084 + # command.
I think that's a type. Maybe it should be s/so/to
2085 + # sys.argv[1:]. To omply with that, we add a dummy first arg to
Typo: *c*omply
2212 + def assertFiltered(
I'm not sure 'actual' is a good name here. When I first read it, I thought expected would be asserted as equal to actual.
So, maybe, call it 'tests'? Not sure if it's better though.
Also, the calls to this method are hard to read. I think it might be better to use keyword args on the tests that call it:
self.assertFilt
expected=
tests=['foo', 'foobar', 'barfoo', 'baz'])
I'm not sure if that's better, either :) Just a thought.
2342 +Because the tests themselves shares this module name space, care must be taken
Typo: tests (...) share_
2424 +class TestModuleLoader
You are missing the test for a file with an _ in front, right?
l. 2450 With textwrap.dedent, this strings might look prettier.
2607 + f.write(
Instead of calling write three times, I would prefer to define a variable csv_contents, and write it once.
I'm almost done...
Awesome job man! Makes me think that a lot of this loaders will be useful to hordes of testers around the world. All except the script loaders may be a good addition to testtools.
I'll finish tomorrow morning. I'm testing this on lp:~elopio/canonical-identity-provider/fix1175698-ubuntu_title that failed because of the wrong original discovery we had. It now finds the cases I'm interested in, but failes for other reasons. So I need to first get SSO to green, to compare. I'll look for someone to blame tomorrow too.
- 444. By Vincent Ladeuil
-
Fix typos mentioned in review.
- 445. By Vincent Ladeuil
-
Rename sst/case.py to sst/cases.py as mentioned in review and fix fallouts.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Vincent Ladeuil (vila) wrote : | # |
> l 44, 66, 355, 482, 504, 644, 664, 685, 731, 765, 806, 835, 890, 938, 948,
> 959, 995, 1961, 1981, 2022, 3958
> Pep8 recommeds to precede the quotes that end a multiline docstring with an
> empty line.
But the rationale given is:
The BDFL [3] recommends inserting a blank line between the last paragraph
in a multi-line docstring and its closing quotes, placing the closing
quotes on a line by themselves. This way, Emacs' fill-paragraph command
can be used on it.
That may have been true in the past but nowadays, fill-paragraph works
perfectly fine there.
I have several other reasons to disagree:
- this rule is not enforced by our pep8 conformance test so it's unlikely to
be used consistently,
- when generating doc, this extra newline may conflict with other layout
style and will just complicate matters if we need to special case the end
of string,
- it wastes space, the triple quote on its own line already provides a
visual break.
That being said, if you object strongly I'll fix it and will try to respect
it in the future but I think it's far less important than respecting that
rule that prescribes a single line summary at the start of the docstring.
> Not a real issue, and not all of them introduced by you. But I find nice the
> consistency that comes from following all pep8 recommendations.
>
> I have on my TODO to update all the comments in the actions module, btw.
Yeah, that's hhtp://
>
> 139 === added file 'src/sst/case.py'
> Isn't cases a better name?
>
I almost used it myself, fixed. I've found that using the plural form for
modules leads to less clashes with variable names but it's not an idiom that
I encounter often, so I've used it lightly so far.
On the topic of module and class names, should we take the opportunity to
get rid of the redundancy in the test case class names, sst means Selenium
Simple Test, so SSTTestCase means Selenium Simple Test Test Case and
SSTScriptTestCase means Selenium Simple Test Script Test Case. I think it
should be:
- SSTestCase: Selenium Simple Test Case
- SSTScriptCase: Selenium Simple Test Script Case
Since we will deprecate runtests.
at some point in the future, we may as well fix this so people only have to
update once.
What do you think ?
> 332 + # TODO: Adding script_dir to sys.path only make sense if we want to
> 333 + # allow scripts to import from their own dir. Do we really need that
> ?
>
> Typo: makeS
>
> To answer the question, I'd say no. We are trying to use the full path
> everywhere, so this only allows a behavior we are trying to avoid. If we
> remove it, it might break some tests, but that's fine as they should be
> updated anyway.
Ok, filed http://
>
> 353 + the first row (headers) match data_map key names.
> 354 + rows beneath are filled with data values.
>
> Not changed by you, but I think this has wrong identation.
Fixed. Using fill-paragraph even ;)
>
> to be continued...
Thanks, very much appreciated.
- 446. By Vincent Ladeuil
-
More typos mentioned in review.
- 447. By Vincent Ladeuil
-
More typos and other test refactorings mentioned in review.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Vincent Ladeuil (vila) wrote : | # |
> 2084 + # command.
>
> I think that's a type. Maybe it should be s/so/to
Tyop indeed, fixed.
>
> 2085 + # sys.argv[1:]. To omply with that, we add a dummy first arg to
>
> Typo: *c*omply
>
Fixed.
> 2212 + def assertFiltered(
>
> I'm not sure 'actual' is a good name here. When I first read it, I thought
> expected would be asserted as equal to actual.
> So, maybe, call it 'tests'? Not sure if it's better though.
You're right, I renamed it 'ids as tests are created from them, not
received as such and not compared to expected indeed.
> Also, the calls to this method are hard to read. I think it might be better to
> use keyword args on the tests that call it:
>
> self.assertFilt
> expected=['foo', 'foobar', 'barfoo'], pattern=['*foo*'],
> tests=['foo', 'foobar', 'barfoo', 'baz'])
>
> I'm not sure if that's better, either :) Just a thought.
Yeah, I don't think adding keywords here is worth the added noise. You need
to understand what the helper is doing in any case, the keywords won't give
you that knowledge. Each class is short enough that you can afford reading
the helper to understand single line tests IMHO.
I've added docstrings to the assertFiltered methods.
>
> 2342 +Because the tests themselves shares this module name space, care must
> be taken
>
> Typo: tests (...) share_
>
Fixed.
> 2424 +class TestModuleLoader
>
> You are missing the test for a file with an _ in front, right?
test_ignore_
>
> l. 2450 With textwrap.dedent, this strings might look prettier.
>
I had several discussions about that in the past on various projects.
While relying on dedent seems a good idea at first, when maintaining the
tests it proves to be more trouble than the initial supposed easier read:
- the added indentations are not always appropriate as it means the input
you type transformed before being used so you're not looking at the data
your test will process,
- this makes it hard to copy paste code or data you tuned somewhere else
(variation on the point above: better stick with the format that is
processed).
> 2607 + f.write(
>
> Instead of calling write three times, I would prefer to define a variable
> csv_contents, and write it once.
Hehe, perfect example that I should have followed my own advices
above. Rewriting this as a multi-line content is far clearer (some single
quotes are required there and it's easier to catch them while reading a
single description than three different lines using different quotes).
It's also clearly a case where I prefer to respect the format expected than
adding leading indentation.
>
> I'm almost done...
>
> Awesome job man! Makes me think that a lot of this loaders will be useful to
> hordes of testers around the world. All except the script loaders may be a
> good addition to testtools.
Yeah, that was more or less the idea, first make it works in sst then look
at what can be upstreamed to testtools and/or python, but there is no hurry
;) sst really introduces an unusual pattern because the scripts should not
be imported, a constra...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Vincent Ladeuil (vila) wrote : | # |
Dang, my answer to that has been lost :-(
> 726 + """Load tests for a tree containing scrits.
>
> Typo: scriPts
Fixed.
>
> 736 + regexp = '^shared$|^_'
>
> The name of the shared file can be parameterized with the -m command line
> option. Having it hard-coded seems wrong.
It was hard-coded in different places including:
- find_shared_
dir when -m is not used,
- in get_suite and friends functions I'm replacing.
So it should be ignored for compatibility.
Now, I think '-m' is just a convenience to add a directory to sys.path and
I'm not convinced it's better than letting the user properly setup
PYTHONPATH or sys.path before running sst-run. So my plan is to remove
find_shared_
>
> 752 + # FIXME
>
> Can you file a bug for that? Seems nice to have, not so hard.
That's part of the refactoring I plan to do so should be fixed in the final
proposal.
>
> 787 + # FIXME: This swallows exceptions raise the by the user defined
>
> Typos: raiseD _ by
>
Fixed.
> 788 + # 'load_test'. We may want to give awy to expose them instead (with
>
> Typo: a _ wAy
>
Fixed.
> 822 + """Load test from an sst tree.
>
> Typo: testS
>
Fixed.
> 829 + This also provide ways for packages to define the test loading as
> they see
>
> Typo: provideS
>
Fixed.
> 1886 +assert (config.
> 1887 + == os.path.
>
Rewritten to avoid the issue.
> According to pep8, the break should be after the operator.
>
I disagree with that, the operator is the important part, it's easier to
find at the beginning of the line than at the end.
And again, our pep8 test didn't catch it...
> 2020 + :param description: A text where files and directories contents is
>
> I'm curious about where did you get this style for documenting parameters. The
> example from http://
> different, but I'm not sure if there's a "right" or recommended way for this.
> Personally, I prefer the style from 257, but I can give you no reasons for
> that :)
>
I think adding markup here doesn't make it harder to read but pave the way
for better doc generation in the future.
Some references:
http://
http://
> (I'm not done yet...)
Thanks for that already ;)
- 448. By Vincent Ladeuil
-
Stop adding the script directory to sys.path
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Leo Arias (elopio) wrote : | # |
> That being said, if you object strongly I'll fix it and will try to respect
> it in the future but I think it's far less important than respecting that
> rule that prescribes a single line summary at the start of the docstring.
I'm not objecting strongly, you have better reasons than I do.
> On the topic of module and class names, should we take the opportunity to
> get rid of the redundancy in the test case class names, sst means Selenium
> Simple Test, so SSTTestCase means Selenium Simple Test Test Case and
> SSTScriptTestCase means Selenium Simple Test Script Test Case. I think it
> should be:
>
> - SSTestCase: Selenium Simple Test Case
> - SSTScriptCase: Selenium Simple Test Script Case
>
> Since we will deprecate runtests.
> at some point in the future, we may as well fix this so people only have to
> update once.
>
> What do you think ?
I think we should do both at the same time. I can take care of updating sso, pay and u1, it's not hard.
However, that was released on a pypi, might break tests from other users of SST. I'm not sure if there are lots of them, we probably should ask Corey.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Leo Arias (elopio) wrote : | # |
> > Also, the calls to this method are hard to read. I think it might be better
> to
> > use keyword args on the tests that call it:
> >
> > self.assertFilt
> > expected=['foo', 'foobar', 'barfoo'], pattern=['*foo*'],
> > tests=['foo', 'foobar', 'barfoo', 'baz'])
> >
> > I'm not sure if that's better, either :) Just a thought.
>
> Yeah, I don't think adding keywords here is worth the added noise. You need
> to understand what the helper is doing in any case, the keywords won't give
> you that knowledge. Each class is short enough that you can afford reading
> the helper to understand single line tests IMHO.
>
> I've added docstrings to the assertFiltered methods.
That's good enough. Thanks.
> > 2424 +class TestModuleLoader
> >
> > You are missing the test for a file with an _ in front, right?
>
> test_ignore_
That one is in TestScriptLoader. If you change the implementation of script loader to not use ignore privates from the module loader, you can end with a coverage hole.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Vincent Ladeuil (vila) wrote : | # |
> > Since we will deprecate runtests.
> > at some point in the future, we may as well fix this so people only have to
> > update once.
> >
> > What do you think ?
>
> I think we should do both at the same time. I can take care of updating sso,
> pay and u1, it's not hard.
> However, that was released on a pypi, might break tests from other users of
> SST. I'm not sure if there are lots of them, we probably should ask Corey.
To clarify: I've left the symbols defined in runtests.py so we won't break compatibility by landing this.
The day we want to deprecate them, we still won't break compatibility but emit warnings.
So, say, 0.2.4 will introduce the refactor but the old symbols are still available.
0.2.5 will emit warnings about the symbols being deprecated and pointing to the new ones.
0.2.6 will remove the old symbols.
Or we can just emit the warnings now but I'd rather do that in a different proposal.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Leo Arias (elopio) wrote : | # |
again, this is awesome.
I'll leave it to you to globally approve. If you agree with me about the missing test, then you can add it. If you don't agree, then don't :)
- 449. By Vincent Ladeuil
-
Remerge result branch
Preview Diff
1 | === modified file 'docs/changelog.rst' |
2 | --- docs/changelog.rst 2013-05-10 08:53:33 +0000 |
3 | +++ docs/changelog.rst 2013-05-14 11:43:29 +0000 |
4 | @@ -24,6 +24,7 @@ |
5 | * switched to `junitxml` dependency for XML report generation. |
6 | * refactored ``retry_on_stale_element`` to make a new more generic |
7 | ``retry_on_exception``. |
8 | +* the script directory is not added to sys.path implicitly anymore. |
9 | |
10 | |
11 | version **0.2.3** (2013 Apr 17) |
12 | |
13 | === modified file 'src/sst/actions.py' |
14 | --- src/sst/actions.py 2013-05-10 08:53:33 +0000 |
15 | +++ src/sst/actions.py 2013-05-14 11:43:29 +0000 |
16 | @@ -246,6 +246,7 @@ |
17 | try: |
18 | os.makedirs(config.results_directory) |
19 | except OSError: |
20 | + # FIXME: We should only catch the EEXIST errno -- vila 2013-04-29 |
21 | pass # already exists |
22 | |
23 | |
24 | |
25 | === added file 'src/sst/browsers.py' |
26 | --- src/sst/browsers.py 1970-01-01 00:00:00 +0000 |
27 | +++ src/sst/browsers.py 2013-05-14 11:43:29 +0000 |
28 | @@ -0,0 +1,121 @@ |
29 | +# |
30 | +# Copyright (c) 2011-2013 Canonical Ltd. |
31 | +# |
32 | +# This file is part of: SST (selenium-simple-test) |
33 | +# https://launchpad.net/selenium-simple-test |
34 | +# |
35 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
36 | +# you may not use this file except in compliance with the License. |
37 | +# You may obtain a copy of the License at |
38 | +# |
39 | +# http://www.apache.org/licenses/LICENSE-2.0 |
40 | +# |
41 | +# Unless required by applicable law or agreed to in writing, software |
42 | +# distributed under the License is distributed on an "AS IS" BASIS, |
43 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
44 | +# See the License for the specific language governing permissions and |
45 | +# limitations under the License. |
46 | +# |
47 | + |
48 | + |
49 | +from selenium import webdriver |
50 | + |
51 | + |
52 | +class BrowserFactory(object): |
53 | + """Handle browser creation for tests. |
54 | + |
55 | + One instance is used for a given test run. |
56 | + """ |
57 | + |
58 | + webdriver_class = None |
59 | + |
60 | + def __init__(self, javascript_disabled=False): |
61 | + super(BrowserFactory, self).__init__() |
62 | + self.javascript_disabled = javascript_disabled |
63 | + |
64 | + def setup_for_test(self, test): |
65 | + """Setup the browser for the given test. |
66 | + |
67 | + Some browsers accept more options that are test (and browser) specific. |
68 | + |
69 | + Daughter classes should redefine this method to capture them. |
70 | + """ |
71 | + pass |
72 | + |
73 | + def browser(self): |
74 | + """Create a browser based on previously collected options. |
75 | + |
76 | + Daughter classes should override this method if they need to provide |
77 | + more context. |
78 | + """ |
79 | + return self.webdriver_class() |
80 | + |
81 | + |
82 | +# MISSINGTEST: Exercise this class -- vila 2013-04-11 |
83 | +class RemoteBrowserFactory(BrowserFactory): |
84 | + |
85 | + webdriver_class = webdriver.Remote |
86 | + |
87 | + def __init__(self, capabilities, remote_url): |
88 | + super(RemoteBrowserFactory, self).__init__() |
89 | + self.capabilities = capabilities |
90 | + self.remote_url = remote_url |
91 | + |
92 | + def browser(self): |
93 | + return self.webdriver_class(self.capabilities, self.remote_url) |
94 | + |
95 | + |
96 | +# MISSINGTEST: Exercise this class -- vila 2013-04-11 |
97 | +class ChromeFactory(BrowserFactory): |
98 | + |
99 | + webdriver_class = webdriver.Chrome |
100 | + |
101 | + |
102 | +# MISSINGTEST: Exercise this class (requires windows) -- vila 2013-04-11 |
103 | +class IeFactory(BrowserFactory): |
104 | + |
105 | + webdriver_class = webdriver.Ie |
106 | + |
107 | + |
108 | +# MISSINGTEST: Exercise this class -- vila 2013-04-11 |
109 | +class PhantomJSFactory(BrowserFactory): |
110 | + |
111 | + webdriver_class = webdriver.PhantomJS |
112 | + |
113 | + |
114 | +# MISSINGTEST: Exercise this class -- vila 2013-04-11 |
115 | +class OperaFactory(BrowserFactory): |
116 | + |
117 | + webdriver_class = webdriver.Opera |
118 | + |
119 | + |
120 | +class FirefoxFactory(BrowserFactory): |
121 | + |
122 | + webdriver_class = webdriver.Firefox |
123 | + |
124 | + def setup_for_test(self, test): |
125 | + profile = webdriver.FirefoxProfile() |
126 | + profile.set_preference('intl.accept_languages', 'en') |
127 | + if test.assume_trusted_cert_issuer: |
128 | + profile.set_preference('webdriver_assume_untrusted_issuer', False) |
129 | + profile.set_preference( |
130 | + 'capability.policy.default.Window.QueryInterface', 'allAccess') |
131 | + profile.set_preference( |
132 | + 'capability.policy.default.Window.frameElement.get', |
133 | + 'allAccess') |
134 | + if test.javascript_disabled or self.javascript_disabled: |
135 | + profile.set_preference('javascript.enabled', False) |
136 | + self.profile = profile |
137 | + |
138 | + def browser(self): |
139 | + return self.webdriver_class(self.profile) |
140 | + |
141 | + |
142 | +# MISSINGTEST: Exercise this class -- vila 2013-04-11 |
143 | +browser_factories = { |
144 | + 'Chrome': ChromeFactory, |
145 | + 'Firefox': FirefoxFactory, |
146 | + 'Ie': IeFactory, |
147 | + 'Opera': OperaFactory, |
148 | + 'PhantomJS': PhantomJSFactory, |
149 | +} |
150 | |
151 | === added file 'src/sst/cases.py' |
152 | --- src/sst/cases.py 1970-01-01 00:00:00 +0000 |
153 | +++ src/sst/cases.py 2013-05-14 11:43:29 +0000 |
154 | @@ -0,0 +1,233 @@ |
155 | +# |
156 | +# Copyright (c) 2011-2013 Canonical Ltd. |
157 | +# |
158 | +# This file is part of: SST (selenium-simple-test) |
159 | +# https://launchpad.net/selenium-simple-test |
160 | +# |
161 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
162 | +# you may not use this file except in compliance with the License. |
163 | +# You may obtain a copy of the License at |
164 | +# |
165 | +# http://www.apache.org/licenses/LICENSE-2.0 |
166 | +# |
167 | +# Unless required by applicable law or agreed to in writing, software |
168 | +# distributed under the License is distributed on an "AS IS" BASIS, |
169 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
170 | +# See the License for the specific language governing permissions and |
171 | +# limitations under the License. |
172 | +# |
173 | + |
174 | +import ast |
175 | +import logging |
176 | +import os |
177 | +import pdb |
178 | +import sys |
179 | +import testtools |
180 | +import testtools.content |
181 | + |
182 | + |
183 | +from sst import ( |
184 | + actions, |
185 | + browsers, |
186 | + config, |
187 | + context, |
188 | + xvfbdisplay, |
189 | +) |
190 | +import traceback |
191 | + |
192 | + |
193 | +logger = logging.getLogger('SST') |
194 | + |
195 | + |
196 | +class SSTTestCase(testtools.TestCase): |
197 | + """A test case that can use the sst framework.""" |
198 | + |
199 | + xvfb = None |
200 | + xserver_headless = False |
201 | + |
202 | + browser_factory = browsers.FirefoxFactory() |
203 | + |
204 | + javascript_disabled = False |
205 | + assume_trusted_cert_issuer = False |
206 | + |
207 | + wait_timeout = 10 |
208 | + wait_poll = 0.1 |
209 | + base_url = None |
210 | + |
211 | + results_directory = os.path.abspath('results') |
212 | + screenshots_on = False |
213 | + debug_post_mortem = False |
214 | + extended_report = False |
215 | + |
216 | + def setUp(self): |
217 | + super(SSTTestCase, self).setUp() |
218 | + if self.base_url is not None: |
219 | + actions.set_base_url(self.base_url) |
220 | + actions._set_wait_timeout(self.wait_timeout, self.wait_poll) |
221 | + # Ensures sst.actions will find me |
222 | + actions._test = self |
223 | + if self.xserver_headless and self.xvfb is None: |
224 | + # If we need to run headless and no xvfb is already running, start |
225 | + # a new one for the current test, scheduling the shutdown for the |
226 | + # end of the test. |
227 | + self.xvfb = xvfbdisplay.use_xvfb_server(self) |
228 | + config.results_directory = self.results_directory |
229 | + actions._make_results_dir() |
230 | + self.start_browser() |
231 | + self.addCleanup(self.stop_browser) |
232 | + if self.screenshots_on: |
233 | + self.addOnException(self.take_screenshot_and_page_dump) |
234 | + if self.debug_post_mortem: |
235 | + self.addOnException( |
236 | + self.print_exception_and_enter_post_mortem) |
237 | + if self.extended_report: |
238 | + self.addOnException(self.report_extensively) |
239 | + |
240 | + def shortDescription(self): |
241 | + # testools wrongly defines this as returning self.id(). Since we're not |
242 | + # using the short description (aka the first line of the test |
243 | + # docstring) (who is ? should we ?), we revert to the default behavior |
244 | + # so runners and results don't get mad. |
245 | + return None |
246 | + |
247 | + def start_browser(self): |
248 | + logger.debug('\nStarting browser') |
249 | + self.browser_factory.setup_for_test(self) |
250 | + self.browser = self.browser_factory.browser() |
251 | + logger.debug('Browser started: %s' % (self.browser.name)) |
252 | + |
253 | + def stop_browser(self): |
254 | + logger.debug('Stopping browser') |
255 | + self.browser.quit() |
256 | + |
257 | + def take_screenshot_and_page_dump(self, exc_info): |
258 | + try: |
259 | + filename = 'screenshot-{0}.png'.format(self.id()) |
260 | + actions.take_screenshot(filename) |
261 | + except Exception: |
262 | + # FIXME: Needs to be reported somehow ? -- vila 2012-10-16 |
263 | + pass |
264 | + try: |
265 | + # also dump page source |
266 | + filename = 'pagesource-{0}.html'.format(self.id()) |
267 | + actions.save_page_source(filename) |
268 | + except Exception: |
269 | + # FIXME: Needs to be reported somehow ? -- vila 2012-10-16 |
270 | + pass |
271 | + |
272 | + def print_exception_and_enter_post_mortem(self, exc_info): |
273 | + exc_class, exc, tb = exc_info |
274 | + traceback.print_exception(exc_class, exc, tb) |
275 | + pdb.post_mortem(tb) |
276 | + |
277 | + def report_extensively(self, exc_info): |
278 | + exc_class, exc, tb = exc_info |
279 | + original_message = str(exc) |
280 | + try: |
281 | + current_url = actions.get_current_url() |
282 | + except Exception: |
283 | + current_url = 'unavailable' |
284 | + try: |
285 | + page_source = actions.get_page_source() |
286 | + except Exception: |
287 | + page_source = 'unavailable' |
288 | + self.addDetail( |
289 | + 'Original exception', |
290 | + testtools.content.text_content('{0} : {1}'.format( |
291 | + exc.__class__.__name__, original_message))) |
292 | + self.addDetail('Current url', |
293 | + testtools.content.text_content(current_url)) |
294 | + self.addDetail('Page source', |
295 | + testtools.content.text_content(page_source)) |
296 | + |
297 | + |
298 | +class SSTScriptTestCase(SSTTestCase): |
299 | + """Test case used internally by sst-run and sst-remote.""" |
300 | + |
301 | + def __init__(self, script_dir, script_name, context_row=None): |
302 | + super(SSTScriptTestCase, self).__init__('run_test_script') |
303 | + self.script_dir = script_dir |
304 | + self.script_name = script_name |
305 | + self.script_path = os.path.join(self.script_dir, self.script_name) |
306 | + |
307 | + # pythonify the script path into a python path |
308 | + test_id = self.script_path.replace('.py', '') |
309 | + if test_id.startswith('./'): |
310 | + test_id = test_id[2:] |
311 | + self.id = lambda: '%s' % (test_id.replace(os.sep, '.')) |
312 | + if context_row is None: |
313 | + context_row = {} |
314 | + self.context = context_row |
315 | + |
316 | + def __str__(self): |
317 | + # Since we use run_test_script to encapsulate the call to the |
318 | + # compiled code, we need to override __str__ to get a proper name |
319 | + # reported. |
320 | + return "%s" % (self.id(),) |
321 | + |
322 | + def setUp(self): |
323 | + self._compile_script() |
324 | + # The script may override some settings. The default value for |
325 | + # JAVASCRIPT_DISABLED and ASSUME_TRUSTED_CERT_ISSUER are False, so if |
326 | + # the user mentions them in his script, it's to turn them on. Also, |
327 | + # getting our hands on the values used in the script is too hackish ;) |
328 | + if 'JAVASCRIPT_DISABLED' in self.code.co_names: |
329 | + self.javascript_disabled = True |
330 | + if 'ASSUME_TRUSTED_CERT_ISSUER' in self.code.co_names: |
331 | + self.assume_trusted_cert_issuer = True |
332 | + super(SSTScriptTestCase, self).setUp() |
333 | + # Start with default values |
334 | + actions.reset_base_url() |
335 | + actions._set_wait_timeout(10, 0.1) |
336 | + # Possibly inject parametrization from associated .csv file |
337 | + previous_context = context.store_context() |
338 | + self.addCleanup(context.restore_context, previous_context) |
339 | + context.populate_context(self.context, self.script_path, |
340 | + self.browser.name, self.javascript_disabled) |
341 | + |
342 | + def _compile_script(self): |
343 | + self.script_path = os.path.join(self.script_dir, self.script_name) |
344 | + with open(self.script_path) as f: |
345 | + source = f.read() + '\n' |
346 | + self.code = compile(source, self.script_path, 'exec') |
347 | + |
348 | + def run_test_script(self, result=None): |
349 | + # Run the test catching exceptions sstnam style |
350 | + try: |
351 | + exec self.code in self.context |
352 | + except actions.EndTest: |
353 | + pass |
354 | + |
355 | + |
356 | +def get_data(csv_path): |
357 | + """ |
358 | + Return a list of data dicts for parameterized testing. |
359 | + |
360 | + The first row (headers) match data_map key names. rows beneath are filled |
361 | + with data values. |
362 | + """ |
363 | + rows = [] |
364 | + print ' Reading data from %r...' % os.path.split(csv_path)[-1], |
365 | + row_num = 0 |
366 | + with open(csv_path) as f: |
367 | + headers = f.readline().rstrip().split('^') |
368 | + headers = [header.replace('"', '') for header in headers] |
369 | + headers = [header.replace("'", '') for header in headers] |
370 | + for line in f: |
371 | + row = {} |
372 | + row_num += 1 |
373 | + row['_row_num'] = row_num |
374 | + fields = line.rstrip().split('^') |
375 | + for header, field in zip(headers, fields): |
376 | + try: |
377 | + value = ast.literal_eval(field) |
378 | + except ValueError: |
379 | + value = field |
380 | + if value.lower() == 'false': |
381 | + value = False |
382 | + if value.lower() == 'true': |
383 | + value = True |
384 | + row[header] = value |
385 | + rows.append(row) |
386 | + print 'found %s rows' % len(rows) |
387 | + return rows |
388 | |
389 | === modified file 'src/sst/command.py' |
390 | --- src/sst/command.py 2013-05-01 22:21:07 +0000 |
391 | +++ src/sst/command.py 2013-05-14 11:43:29 +0000 |
392 | @@ -28,6 +28,7 @@ |
393 | import sst |
394 | from sst import ( |
395 | actions, |
396 | + browsers, |
397 | config, |
398 | runtests, |
399 | ) |
400 | @@ -95,6 +96,13 @@ |
401 | parser.add_option('--collect-only', dest='collect_only', |
402 | action='store_true', default=False, |
403 | help='collect/print cases without running tests') |
404 | + parser.add_option('-i', '--include', dest='includes', |
405 | + action='append', |
406 | + help='all tests starting with this prefix will be run') |
407 | + parser.add_option( |
408 | + '-e', '--exclude', dest='excludes', |
409 | + action='append', |
410 | + help='all tests starting with this prefix will not be run') |
411 | return parser |
412 | |
413 | |
414 | @@ -132,17 +140,17 @@ |
415 | return parser |
416 | |
417 | |
418 | -def get_opts_run(): |
419 | - return get_opts(get_run_options) |
420 | - |
421 | - |
422 | -def get_opts_remote(): |
423 | - return get_opts(get_remote_options) |
424 | - |
425 | - |
426 | -def get_opts(get_options): |
427 | +def get_opts_run(args=None): |
428 | + return get_opts(get_run_options, args) |
429 | + |
430 | + |
431 | +def get_opts_remote(args=None): |
432 | + return get_opts(get_remote_options, args) |
433 | + |
434 | + |
435 | +def get_opts(get_options, args=None): |
436 | parser = get_options() |
437 | - (cmd_opts, args) = parser.parse_args() |
438 | + (cmd_opts, args) = parser.parse_args(args) |
439 | |
440 | if cmd_opts.print_version: |
441 | print 'SST version: %s' % sst.__version__ |
442 | @@ -156,7 +164,7 @@ |
443 | print 'run "%s -h" or "%s --help" to see run options.' % (prog, prog) |
444 | sys.exit(1) |
445 | |
446 | - if cmd_opts.browser_type not in runtests.browser_factories: |
447 | + if cmd_opts.browser_type not in browsers.browser_factories: |
448 | print ("Error: %s should be one of %s" |
449 | % (cmd_opts.browser_type, runtests.browser_factories.keys())) |
450 | sys.exit(1) |
451 | |
452 | === added file 'src/sst/filters.py' |
453 | --- src/sst/filters.py 1970-01-01 00:00:00 +0000 |
454 | +++ src/sst/filters.py 2013-05-14 11:43:29 +0000 |
455 | @@ -0,0 +1,97 @@ |
456 | +# |
457 | +# Copyright (c) 2013 Canonical Ltd. |
458 | +# |
459 | +# This file is part of: SST (selenium-simple-test) |
460 | +# https://launchpad.net/selenium-simple-test |
461 | +# |
462 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
463 | +# you may not use this file except in compliance with the License. |
464 | +# You may obtain a copy of the License at |
465 | +# |
466 | +# http://www.apache.org/licenses/LICENSE-2.0 |
467 | +# |
468 | +# Unless required by applicable law or agreed to in writing, software |
469 | +# distributed under the License is distributed on an "AS IS" BASIS, |
470 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
471 | +# See the License for the specific language governing permissions and |
472 | +# limitations under the License. |
473 | +# |
474 | +import fnmatch |
475 | +import unittest |
476 | + |
477 | + |
478 | +def filter_suite(condition, suite): |
479 | + """Return tests for which ``condition`` is True in ``suite``. |
480 | + |
481 | + :param condition: A callable receiving a test and returning True if the |
482 | + test should be kept. |
483 | + |
484 | + :param suite: A test suite that can be iterated. It contains either tests |
485 | + or suite inheriting from ``unittest.TestSuite``. |
486 | + |
487 | + ``suite`` is a tree of tests and suites, the returned suite respect the |
488 | + received suite layout, only removing empty suites. |
489 | + """ |
490 | + filtered_suite = suite.__class__() |
491 | + for test in suite: |
492 | + if issubclass(test.__class__, unittest.TestSuite): |
493 | + # We received a suite, we'll filter a suite |
494 | + filtered = filter_suite(condition, test) |
495 | + if filtered.countTestCases(): |
496 | + # Keep only non-empty suites |
497 | + filtered_suite.addTest(filtered) |
498 | + elif condition(test): |
499 | + # The test is kept |
500 | + filtered_suite.addTest(test) |
501 | + return filtered_suite |
502 | + |
503 | + |
504 | +def filter_by_patterns(patterns, suite): |
505 | + """Returns the tests that match one of ``patterns``. |
506 | + |
507 | + :param patterns: A list of test name globs to include. All tests are |
508 | + included if no patterns are provided. |
509 | + |
510 | + :param suite: The test suite to filter. |
511 | + """ |
512 | + if not patterns: |
513 | + return suite |
514 | + |
515 | + def filter_test_patterns(test): |
516 | + for pattern in patterns: |
517 | + if fnmatch.fnmatchcase(test.id(), pattern): |
518 | + return True |
519 | + return False |
520 | + return filter_suite(filter_test_patterns, suite) |
521 | + |
522 | + |
523 | +def include_prefixes(prefixes, suite): |
524 | + """Returns the tests whose id starts with one of the prefixes.""" |
525 | + if not prefixes: |
526 | + # No prefixes, no filtering |
527 | + return suite |
528 | + |
529 | + def starts_with_one_of(test): |
530 | + # A test is kept if its id starts with one of the prefixes |
531 | + tid = test.id() |
532 | + for prefix in prefixes: |
533 | + if tid.startswith(prefix): |
534 | + return True |
535 | + return False |
536 | + return filter_suite(starts_with_one_of, suite) |
537 | + |
538 | + |
539 | +def exclude_prefixes(prefixes, suite): |
540 | + """Returns the tests whose id does not start with any of the prefixes.""" |
541 | + if not prefixes: |
542 | + # No prefixes, no filtering |
543 | + return suite |
544 | + |
545 | + def starts_with_none_of(test): |
546 | + # A test is kept if its id matches none of the 'excludes' prefixes |
547 | + tid = test.id() |
548 | + for prefix in prefixes: |
549 | + if tid.startswith(prefix): |
550 | + return False |
551 | + return True |
552 | + return filter_suite(starts_with_none_of, suite) |
553 | |
554 | === added file 'src/sst/loader.py' |
555 | --- src/sst/loader.py 1970-01-01 00:00:00 +0000 |
556 | +++ src/sst/loader.py 2013-05-14 11:43:29 +0000 |
557 | @@ -0,0 +1,409 @@ |
558 | +# |
559 | +# Copyright (c) 2011-2013 Canonical Ltd. |
560 | +# |
561 | +# This file is part of: SST (selenium-simple-test) |
562 | +# https://launchpad.net/selenium-simple-test |
563 | +# |
564 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
565 | +# you may not use this file except in compliance with the License. |
566 | +# You may obtain a copy of the License at |
567 | +# |
568 | +# http://www.apache.org/licenses/LICENSE-2.0 |
569 | +# |
570 | +# Unless required by applicable law or agreed to in writing, software |
571 | +# distributed under the License is distributed on an "AS IS" BASIS, |
572 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
573 | +# See the License for the specific language governing permissions and |
574 | +# limitations under the License. |
575 | +# |
576 | +import contextlib |
577 | +import fnmatch |
578 | +import functools |
579 | +import os |
580 | +import re |
581 | +import sys |
582 | +import unittest |
583 | +import unittest.loader |
584 | + |
585 | +from sst import cases |
586 | + |
587 | + |
588 | +def matches_for_regexp(regexp): |
589 | + match_re = re.compile(regexp) |
590 | + |
591 | + def matches(path): |
592 | + return bool(match_re.match(path)) |
593 | + return matches |
594 | + |
595 | + |
596 | +def matches_for_glob(pattern): |
597 | + match_re = fnmatch.translate(pattern) |
598 | + return matches_for_regexp(match_re) |
599 | + |
600 | + |
601 | +class NameMatcher(object): |
602 | + |
603 | + def __init__(self, includes=None, excludes=None): |
604 | + if includes is not None: |
605 | + self.includes = includes |
606 | + if excludes is not None: |
607 | + self.excludes = excludes |
608 | + |
609 | + def includes(self, path): |
610 | + return True |
611 | + |
612 | + def excludes(self, path): |
613 | + return False |
614 | + |
615 | + def matches(self, name): |
616 | + return self.includes(name) and not self.excludes(name) |
617 | + |
618 | + |
619 | +class FileLoader(object): |
620 | + """Load tests from a file. |
621 | + |
622 | + This is an abstract class allowing daughter classes to enforce constraints |
623 | + including the ability to load tests from files that cannot be imported. |
624 | + """ |
625 | + |
626 | + def __init__(self, test_loader, matcher=None): |
627 | + """Load tests from a file.""" |
628 | + super(FileLoader, self).__init__() |
629 | + if matcher is None: |
630 | + self.matches = lambda name: True |
631 | + else: |
632 | + self.matches = matcher.matches |
633 | + self.test_loader = test_loader |
634 | + |
635 | + def discover(self, directory, name): |
636 | + """Return None to represent an empty test suite. |
637 | + |
638 | + This is mostly for documentation purposes, if a file contains material |
639 | + that can produce tests, a specific file loader should be defined to |
640 | + build tests from the file content. |
641 | + """ |
642 | + return None |
643 | + |
644 | + |
645 | +class ModuleLoader(FileLoader): |
646 | + """Load tests from a python module. |
647 | + |
648 | + This handles base name matching and loading tests defined in an importable |
649 | + python module. |
650 | + """ |
651 | + |
652 | + def __init__(self, test_loader, matcher=None): |
653 | + if matcher is None: |
654 | + # Default to python source files, excluding private ones |
655 | + matcher = NameMatcher(includes=matches_for_regexp('.*\.py$'), |
656 | + excludes=matches_for_regexp('^_')) |
657 | + super(ModuleLoader, self).__init__(test_loader, matcher=matcher) |
658 | + |
659 | + def discover(self, directory, name): |
660 | + if not self.matches(name): |
661 | + return None |
662 | + module = self.test_loader.importFromPath(os.path.join(directory, name)) |
663 | + return self.test_loader.loadTestsFromModule(module) |
664 | + |
665 | + |
666 | +class ScriptLoader(FileLoader): |
667 | + """Load tests from an sst script. |
668 | + |
669 | + This handles base name matching and loading tests defined in an sst script. |
670 | + """ |
671 | + |
672 | + def __init__(self, test_loader, matcher=None): |
673 | + if matcher is None: |
674 | + # Default to python source files, excluding private ones |
675 | + matcher = NameMatcher(includes=matches_for_regexp('.*\.py$'), |
676 | + excludes=matches_for_regexp('^_')) |
677 | + super(ScriptLoader, self).__init__(test_loader, matcher=matcher) |
678 | + |
679 | + def discover(self, directory, name): |
680 | + if not self.matches(name): |
681 | + return None |
682 | + return self.test_loader.loadTestsFromScript(directory, name) |
683 | + |
684 | + |
685 | +class DirLoader(object): |
686 | + """Load tests from a tree. |
687 | + |
688 | + This is an abstract class allowing daughter classes to enforce constraints |
689 | + including the ability to load tests from files and directories that cannot |
690 | + be imported. |
691 | + """ |
692 | + |
693 | + def __init__(self, test_loader, matcher=None): |
694 | + """Load tests from a directory.""" |
695 | + super(DirLoader, self).__init__() |
696 | + if matcher is None: |
697 | + # Accept everything |
698 | + self.matches = lambda name: True |
699 | + else: |
700 | + self.matches = matcher.matches |
701 | + self.test_loader = test_loader |
702 | + |
703 | + def discover(self, directory, name): |
704 | + if not self.matches(name): |
705 | + return None |
706 | + path = os.path.join(directory, name) |
707 | + names = os.listdir(path) |
708 | + names = self.test_loader.sortNames(names) |
709 | + return self.discover_names(path, names) |
710 | + |
711 | + def discover_names(self, directory, names): |
712 | + suite = self.test_loader.suiteClass() |
713 | + for name in names: |
714 | + tests = self.discover_path(directory, name) |
715 | + if tests is not None: |
716 | + suite.addTests(tests) |
717 | + return suite |
718 | + |
719 | + def discover_path(self, directory, name): |
720 | + loader = None |
721 | + path = os.path.join(directory, name) |
722 | + if os.path.isfile(path): |
723 | + loader = self.test_loader.fileLoaderClass(self.test_loader) |
724 | + elif os.path.isdir(path): |
725 | + loader = self.test_loader.dirLoaderClass(self.test_loader) |
726 | + if loader is not None: |
727 | + return loader.discover(directory, name) |
728 | + return None |
729 | + |
730 | + |
731 | +class ScriptDirLoader(DirLoader): |
732 | + """Load tests for a tree containing scripts. |
733 | + |
734 | + Scripts can be organized in a tree where directories are not python |
735 | + packages. Since scripts are not imported, they don't require the |
736 | + directories containing them to be packages. |
737 | + """ |
738 | + |
739 | + def __init__(self, test_loader, matcher=None): |
740 | + if matcher is None: |
741 | + # Excludes the 'shared' directory and the "private" directories |
742 | + regexp = '^shared$|^_' |
743 | + matcher = NameMatcher(excludes=matches_for_regexp(regexp)) |
744 | + super(ScriptDirLoader, self).__init__(test_loader, matcher=matcher) |
745 | + |
746 | + def discover_path(self, directory, name): |
747 | + # MISSINGTEST: The behavior is unclear when a module cannot be imported |
748 | + # because sys.path is incomplete. This makes it hard for the user to |
749 | + # understand it should update sys.path -- vila 2013-05-05 |
750 | + path = os.path.join(directory, name) |
751 | + if (os.path.isdir(path) and os.path.isfile( |
752 | + os.path.join(path, '__init__.py'))): |
753 | + # Hold on, we need to respect users wishes here (if it has some) |
754 | + loader = PackageLoader(self.test_loader) |
755 | + try: |
756 | + return loader.discover(directory, name) |
757 | + except ImportError: |
758 | + # FIXME: Nah, didn't work, should we report it to the user ? |
759 | + # (yes see MISSINGTEST above) How ? (By re-raising with a |
760 | + # proper message: if there is an __init__.py file here, it |
761 | + # should be importable, that's what we should explain to the |
762 | + # user) vila 2013-05-04 |
763 | + pass |
764 | + return super(ScriptDirLoader, self).discover_path(directory, name) |
765 | + |
766 | + |
767 | +class PackageLoader(DirLoader): |
768 | + """Load tests for a package. |
769 | + |
770 | + A package provides a way for the user to specify how tests are loaded. |
771 | + """ |
772 | + |
773 | + def discover(self, directory, name): |
774 | + if not self.matches(name): |
775 | + return None |
776 | + path = os.path.join(directory, name) |
777 | + try: |
778 | + package = self.test_loader.importFromPath(path) |
779 | + except ImportError: |
780 | + # Explicitly raise the full exception with its backtrace. This |
781 | + # could be overwritten by daughter classes to handle them |
782 | + # differently (swallowing included ;) |
783 | + raise |
784 | + # Can we delegate to the package ? |
785 | + discover = getattr(package, 'discover', None) |
786 | + if discover is not None: |
787 | + # Since the user defined it, the package knows better |
788 | + return discover(self.test_loader, package, |
789 | + os.path.join(directory, name)) |
790 | + # Can we use the load_tests protocol ? |
791 | + load_tests = getattr(package, 'load_tests', None) |
792 | + if load_tests is not None: |
793 | + # FIXME: This swallows exceptions raised the by the user defined |
794 | + # 'load_test'. We may want to give a way to expose them instead |
795 | + # (with or without stopping the test loading) -- vila 2013-04-27 |
796 | + return self.test_loader.loadTestsFromModule(package) |
797 | + # Anything else with that ? |
798 | + # Nothing for now, thanks |
799 | + |
800 | + names = os.listdir(path) |
801 | + names.remove('__init__.py') |
802 | + names = self.test_loader.sortNames(names) |
803 | + return self.discover_names(path, names) |
804 | + |
805 | + |
806 | +@contextlib.contextmanager |
807 | +def Loaders(test_loader, file_loader_class, dir_loader_class): |
808 | + """A context manager for loading tests from a tree. |
809 | + |
810 | + This is mainly used when walking a tree a requiring a different set of |
811 | + loaders for a subtree. |
812 | + """ |
813 | + if file_loader_class is None: |
814 | + file_loader_class = test_loader.fileLoaderClass |
815 | + if dir_loader_class is None: |
816 | + dir_loader_class = test_loader.dirLoaderClass |
817 | + orig = (test_loader.fileLoaderClass, test_loader.dirLoaderClass) |
818 | + try: |
819 | + test_loader.fileLoaderClass = file_loader_class |
820 | + test_loader.dirLoaderClass = dir_loader_class |
821 | + # 'test_loader' will now use the specified file/dir loader classes |
822 | + yield |
823 | + finally: |
824 | + (test_loader.fileLoaderClass, test_loader.dirLoaderClass) = orig |
825 | + |
826 | + |
827 | +class TestLoader(unittest.TestLoader): |
828 | + """Load tests from an sst tree. |
829 | + |
830 | + This loader is able to load sst scripts and create test cases with the |
831 | + right sst specific attributes (browser, error handling, reporting). |
832 | + |
833 | + This also allows test case based modules to be loaded when appropriate. |
834 | + |
835 | + This also provides ways for packages to define the test loading as they see |
836 | + fit. |
837 | + |
838 | + Sorting happens on base names inside a directory while walking the tree and |
839 | + on test classes and test method names when loading a module. Those sortings |
840 | + combined provide a test suite where test ids are sorted. |
841 | + """ |
842 | + |
843 | + dirLoaderClass = PackageLoader |
844 | + fileLoaderClass = ModuleLoader |
845 | + |
846 | + def __init__(self, browser_factory=None, |
847 | + screenshots_on=False, debug_post_mortem=False, |
848 | + extended_report=False): |
849 | + super(TestLoader, self).__init__() |
850 | + self.browser_factory = browser_factory |
851 | + self.screenshots_on = screenshots_on |
852 | + self.debug_post_mortem = debug_post_mortem |
853 | + self.extended_report = extended_report |
854 | + |
855 | + def discover(self, start_dir, pattern='test*.py', top_level_dir=None): |
856 | + if top_level_dir: |
857 | + # For backward compatibility we insert top_level_dir in |
858 | + # sys.path. More complex import rules are left to the caller to |
859 | + # setup properly |
860 | + sys.path.insert(0, top_level_dir) |
861 | + |
862 | + class ModuleLoaderFromPattern(ModuleLoader): |
863 | + |
864 | + def __init__(self, test_loader): |
865 | + matcher = NameMatcher(includes=matches_for_glob(pattern)) |
866 | + super(ModuleLoaderFromPattern, self).__init__( |
867 | + test_loader, matcher=matcher) |
868 | + |
869 | + return self.discoverTests(start_dir, |
870 | + file_loader_class=ModuleLoaderFromPattern) |
871 | + |
872 | + def discoverTests(self, start_dir, file_loader_class=None, |
873 | + dir_loader_class=None): |
874 | + with Loaders(self, file_loader_class, dir_loader_class): |
875 | + dir_loader = self.dirLoaderClass(self) |
876 | + return dir_loader.discover(*os.path.split(start_dir)) |
877 | + |
878 | + def discoverTestsFromPackage(self, package, path, file_loader_class=None, |
879 | + dir_loader_class=None): |
880 | + suite = self.suiteClass() |
881 | + suite.addTests(self.loadTestsFromModule(package)) |
882 | + names = os.listdir(path) |
883 | + names.remove('__init__.py') |
884 | + names = self.sortNames(names) |
885 | + with Loaders(self, file_loader_class, dir_loader_class): |
886 | + dir_loader = self.dirLoaderClass(self) |
887 | + suite.addTests(dir_loader.discover_names(path, names)) |
888 | + return suite |
889 | + |
890 | + def sortNames(self, names): |
891 | + """Return 'names' sorted as defined by sortTestMethodsUsing. |
892 | + |
893 | + It's a little abuse of sort*TestMethods*Using as we're sorting file |
894 | + names (or even module python paths) but it allows providing a |
895 | + consistent order for the whole suite. |
896 | + """ |
897 | + return sorted(names, |
898 | + key=functools.cmp_to_key(self.sortTestMethodsUsing)) |
899 | + |
900 | + def importFromPath(self, path): |
901 | + path = os.path.normpath(path) |
902 | + if path.endswith('.py'): |
903 | + path = path[:-3] # Remove the trailing '.py' |
904 | + mod_name = path.replace(os.path.sep, '.') |
905 | + __import__(mod_name) |
906 | + return sys.modules[mod_name] |
907 | + |
908 | + def loadTestsFromScript(self, dir_name, script_name): |
909 | + suite = self.suiteClass() |
910 | + path = os.path.join(dir_name, script_name) |
911 | + if not os.path.isfile(path): |
912 | + return suite |
913 | + # script specific test parametrization |
914 | + csv_path = path.replace('.py', '.csv') |
915 | + if os.path.isfile(csv_path): |
916 | + for row in cases.get_data(csv_path): |
917 | + # row is a dictionary of variables that will magically appear |
918 | + # as globals in the script. |
919 | + test = self.loadTestFromScript(dir_name, script_name, row) |
920 | + suite.addTest(test) |
921 | + else: |
922 | + test = self.loadTestFromScript(dir_name, script_name) |
923 | + suite.addTest(test) |
924 | + return suite |
925 | + |
926 | + def loadTestFromScript(self, dir_name, script_name, context=None): |
927 | + test = cases.SSTScriptTestCase(dir_name, script_name, context) |
928 | + |
929 | + # FIXME: We shouldn't have to set test attributes manually, something |
930 | + # smells wrong here. -- vila 2013-04-26 |
931 | + test.browser_factory = self.browser_factory |
932 | + |
933 | + test.screenshots_on = self.screenshots_on |
934 | + test.debug_post_mortem = self.debug_post_mortem |
935 | + test.extended_report = self.extended_report |
936 | + |
937 | + return test |
938 | + |
939 | + |
940 | +def discoverTestScripts(test_loader, package, directory): |
941 | + """``discover`` helper to load sst scripts. |
942 | + |
943 | + This can be used in a __init__.py file while walking a regular tests tree. |
944 | + """ |
945 | + return test_loader.discoverTestsFromPackage( |
946 | + package, directory, |
947 | + file_loader_class=ScriptLoader, dir_loader_class=ScriptDirLoader) |
948 | + |
949 | + |
950 | +def discoverRegularTests(test_loader, package, directory): |
951 | + """``discover`` helper to load regular python files defining tests. |
952 | + |
953 | + This can be used in a __init__.py file while walking an sst tests tree. |
954 | + """ |
955 | + return test_loader.discoverTestsFromPackage( |
956 | + package, directory, |
957 | + file_loader_class=ModuleLoader, dir_loader_class=PackageLoader) |
958 | + |
959 | + |
960 | +def discoverNoTests(test_loader, *args, **kwargs): |
961 | + """Returns an empty test suite. |
962 | + |
963 | + This can be used in a __init__.py file to prune the test loading for a |
964 | + given subtree. |
965 | + """ |
966 | + return test_loader.suiteClass() |
967 | |
968 | === added file 'src/sst/result.py' |
969 | --- src/sst/result.py 1970-01-01 00:00:00 +0000 |
970 | +++ src/sst/result.py 2013-05-14 11:43:29 +0000 |
971 | @@ -0,0 +1,104 @@ |
972 | +# |
973 | +# Copyright (c) 2011,2012,2013 Canonical Ltd. |
974 | +# |
975 | +# This file is part of: SST (selenium-simple-test) |
976 | +# https://launchpad.net/selenium-simple-test |
977 | +# |
978 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
979 | +# you may not use this file except in compliance with the License. |
980 | +# You may obtain a copy of the License at |
981 | +# |
982 | +# http://www.apache.org/licenses/LICENSE-2.0 |
983 | +# |
984 | +# Unless required by applicable law or agreed to in writing, software |
985 | +# distributed under the License is distributed on an "AS IS" BASIS, |
986 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
987 | +# See the License for the specific language governing permissions and |
988 | +# limitations under the License. |
989 | +# |
990 | + |
991 | +import timeit |
992 | + |
993 | + |
994 | +from testtools import testresult |
995 | + |
996 | + |
997 | +class TextTestResult(testresult.TextTestResult): |
998 | + """A TestResult which outputs activity to a text stream. |
999 | + |
1000 | + TODO: add the verbosity parameter. |
1001 | + """ |
1002 | + |
1003 | + def __init__(self, stream, failfast=False, verbosity=1, timer=None): |
1004 | + super(TextTestResult, self).__init__(stream, failfast) |
1005 | + if timer is None: |
1006 | + timer = timeit.default_timer |
1007 | + self.timer = timer |
1008 | + self.verbose = verbosity > 1 |
1009 | + |
1010 | + def startTestRun(self): |
1011 | + super(TextTestResult, self).startTestRun() |
1012 | + |
1013 | + def startTest(self, test): |
1014 | + if self.verbose: |
1015 | + self.stream.write(str(test)) |
1016 | + self.stream.write(' ... ') |
1017 | + self.start_time = self.timer() |
1018 | + super(TextTestResult, self).startTest(test) |
1019 | + |
1020 | + def stopTest(self, test): |
1021 | + if self.verbose: |
1022 | + elapsed_time = self.timer() - self.start_time |
1023 | + self.stream.write(' (%.3f secs)\n' % elapsed_time) |
1024 | + self.stream.flush() |
1025 | + super(TextTestResult, self).stopTest(test) |
1026 | + |
1027 | + def addExpectedFailure(self, test, err=None, details=None): |
1028 | + if self.verbose: |
1029 | + self.stream.write('XFAIL') |
1030 | + else: |
1031 | + self.stream.write('x') |
1032 | + super(TextTestResult, self).addExpectedFailure(test, err, details) |
1033 | + |
1034 | + def addError(self, test, err=None, details=None): |
1035 | + if self.verbose: |
1036 | + self.stream.write('ERROR') |
1037 | + else: |
1038 | + self.stream.write('E') |
1039 | + super(TextTestResult, self).addError(test, err, details) |
1040 | + |
1041 | + def addFailure(self, test, err=None, details=None): |
1042 | + if self.verbose: |
1043 | + self.stream.write('FAIL') |
1044 | + else: |
1045 | + self.stream.write('F') |
1046 | + super(TextTestResult, self).addFailure(test, err, details) |
1047 | + |
1048 | + def addSkip(self, test, details=None): |
1049 | + # FIXME: Something weird is going on with testtools, as we're supposed |
1050 | + # to use a (self, test, reason, details) signature but this is never |
1051 | + # called this way -- vila 2013-05-10 |
1052 | + reason = details.get('reason', '').as_text() |
1053 | + if not reason: |
1054 | + reason = '' |
1055 | + else: |
1056 | + reason = ' ' + reason |
1057 | + if self.verbose: |
1058 | + self.stream.write('SKIP%s' % reason) |
1059 | + else: |
1060 | + self.stream.write('s') |
1061 | + super(TextTestResult, self).addSkip(test, reason, details) |
1062 | + |
1063 | + def addSuccess(self, test, details=None): |
1064 | + if self.verbose: |
1065 | + self.stream.write('OK') |
1066 | + else: |
1067 | + self.stream.write('.') |
1068 | + super(TextTestResult, self).addSuccess(test, details) |
1069 | + |
1070 | + def addUnexpectedSuccess(self, test, details=None): |
1071 | + if self.verbose: |
1072 | + self.stream.write('NOTOK') |
1073 | + else: |
1074 | + self.stream.write('u') |
1075 | + super(TextTestResult, self).addUnexpectedSuccess(test, details) |
1076 | |
1077 | === modified file 'src/sst/runtests.py' |
1078 | --- src/sst/runtests.py 2013-05-06 19:19:05 +0000 |
1079 | +++ src/sst/runtests.py 2013-05-14 11:43:29 +0000 |
1080 | @@ -17,34 +17,35 @@ |
1081 | # limitations under the License. |
1082 | # |
1083 | |
1084 | -import ast |
1085 | -import fnmatch |
1086 | import junitxml |
1087 | import logging |
1088 | import os |
1089 | -import pdb |
1090 | import sys |
1091 | -import traceback |
1092 | - |
1093 | -from timeit import default_timer |
1094 | -from unittest import ( |
1095 | - defaultTestLoader, |
1096 | - TestSuite, |
1097 | -) |
1098 | - |
1099 | -from selenium import webdriver |
1100 | + |
1101 | + |
1102 | import testtools |
1103 | import testtools.content |
1104 | -import testtools.testresult |
1105 | from sst import ( |
1106 | actions, |
1107 | + browsers, |
1108 | + cases, |
1109 | config, |
1110 | - context, |
1111 | - xvfbdisplay, |
1112 | -) |
1113 | -from .actions import ( |
1114 | - EndTest |
1115 | -) |
1116 | + filters, |
1117 | + loader, |
1118 | + result, |
1119 | +) |
1120 | + |
1121 | +# Maintaining compatibility until we deprecate the followings |
1122 | +BrowserFactory = browsers.BrowserFactory |
1123 | +RemoteBrowserFactory = browsers.RemoteBrowserFactory |
1124 | +ChromeFactory = browsers.ChromeFactory |
1125 | +IeFactory = browsers.IeFactory |
1126 | +PhantomJSFactory = browsers.PhantomJSFactory |
1127 | +OperaFactory = browsers.OperaFactory |
1128 | +FirefoxFactory = browsers.FirefoxFactory |
1129 | +browser_factories = browsers.browser_factories |
1130 | +SSTTestCase = cases.SSTTestCase |
1131 | +SSTScriptTestCase = cases.SSTScriptTestCase |
1132 | |
1133 | |
1134 | __all__ = ['runtests'] |
1135 | @@ -52,45 +53,53 @@ |
1136 | logger = logging.getLogger('SST') |
1137 | |
1138 | |
1139 | +# MISSINGTEST: 'shared' relationship with test_dir, auto-added to sys.path or |
1140 | +# not -- vila 2013-05-05 |
1141 | +# MISSINGTEST: 'results' dir, created in current dir unconditionally conflicts |
1142 | +# with claim that 'shared' can be found somewhere up -- vila 2013-05-05 |
1143 | def runtests(test_names, test_dir='.', collect_only=False, |
1144 | browser_factory=None, |
1145 | report_format='console', |
1146 | shared_directory=None, screenshots_on=False, failfast=False, |
1147 | debug=False, |
1148 | - extended=False): |
1149 | + extended=False, |
1150 | + includes=None, |
1151 | + excludes=None): |
1152 | + |
1153 | + config.results_directory = os.path.abspath('results') |
1154 | + actions._make_results_dir() |
1155 | |
1156 | if test_dir == 'selftests': |
1157 | # XXXX horrible hardcoding |
1158 | # selftests should be a command instead |
1159 | package_dir = os.path.dirname(__file__) |
1160 | - test_dir = os.path.join(package_dir, 'selftests') |
1161 | - |
1162 | - test_dir = _get_full_path(test_dir) |
1163 | - if not os.path.isdir(test_dir): |
1164 | - msg = 'Specified directory %r does not exist' % test_dir |
1165 | - print msg |
1166 | - sys.exit(1) |
1167 | + os.chdir(os.path.dirname(package_dir)) |
1168 | + test_dir = os.path.join('.', 'sst', 'selftests') |
1169 | + else: |
1170 | + if not os.path.isdir(test_dir): |
1171 | + msg = 'Specified directory %r does not exist' % test_dir |
1172 | + print msg |
1173 | + sys.exit(1) |
1174 | + shared_directory = find_shared_directory(test_dir, shared_directory) |
1175 | + config.shared_directory = shared_directory |
1176 | + sys.path.append(shared_directory) |
1177 | |
1178 | if browser_factory is None: |
1179 | # TODO: We could raise an error instead as providing a default value |
1180 | # makes little sense here -- vila 2013-04-11 |
1181 | - browser_factory = FirefoxFactory() |
1182 | - |
1183 | - shared_directory = find_shared_directory(test_dir, shared_directory) |
1184 | - config.shared_directory = shared_directory |
1185 | - sys.path.append(shared_directory) |
1186 | - |
1187 | - config.results_directory = _get_full_path('results') |
1188 | - |
1189 | - test_names = set(test_names) |
1190 | - |
1191 | - suites = get_suites(test_names, test_dir, shared_directory, collect_only, |
1192 | - browser_factory, |
1193 | - screenshots_on, failfast, debug, |
1194 | - extended=extended, |
1195 | - ) |
1196 | - |
1197 | - alltests = TestSuite(suites) |
1198 | + browser_factory = browsers.FirefoxFactory() |
1199 | + |
1200 | + test_loader = loader.TestLoader(browser_factory, screenshots_on, |
1201 | + debug, extended) |
1202 | + alltests = test_loader.suiteClass() |
1203 | + alltests.addTests( |
1204 | + test_loader.discoverTests(test_dir, |
1205 | + file_loader_class=loader.ScriptLoader, |
1206 | + dir_loader_class=loader.ScriptDirLoader)) |
1207 | + |
1208 | + alltests = filters.filter_by_patterns(test_names, alltests) |
1209 | + alltests = filters.include_prefixes(includes, alltests) |
1210 | + alltests = filters.exclude_prefixes(excludes, alltests) |
1211 | |
1212 | print '' |
1213 | print ' %s test cases loaded\n' % alltests.countTestCases() |
1214 | @@ -104,46 +113,32 @@ |
1215 | print 'Collect-Only Enabled, Not Running Tests...\n' |
1216 | print 'Tests Collected:' |
1217 | print '-' * 16 |
1218 | - for t in sorted(testtools.testsuite.iterate_tests(alltests)): |
1219 | + for t in testtools.testsuite.iterate_tests(alltests): |
1220 | print t.id() |
1221 | - sys.exit(0) |
1222 | + return |
1223 | |
1224 | + text_result = result.TextTestResult(sys.stdout, failfast=failfast, |
1225 | + verbosity=2) |
1226 | if report_format == 'xml': |
1227 | - _make_results_dir() |
1228 | results_file = os.path.join(config.results_directory, 'results.xml') |
1229 | xml_stream = file(results_file, 'wb') |
1230 | - result = testtools.testresult.MultiTestResult( |
1231 | - TextTestResult(sys.stdout, failfast=failfast), |
1232 | + res = testtools.testresult.MultiTestResult( |
1233 | + text_result, |
1234 | junitxml.JUnitXmlResult(xml_stream), |
1235 | ) |
1236 | - result.failfast = failfast |
1237 | + res.failfast = failfast |
1238 | else: |
1239 | - result = TextTestResult(sys.stdout, failfast=failfast) |
1240 | + res = text_result |
1241 | |
1242 | - result.startTestRun() |
1243 | + res.startTestRun() |
1244 | try: |
1245 | - alltests.run(result) |
1246 | + alltests.run(res) |
1247 | except KeyboardInterrupt: |
1248 | print >> sys.stderr, 'Test run interrupted' |
1249 | finally: |
1250 | # XXX should warn on cases that were specified but not found |
1251 | pass |
1252 | - result.stopTestRun() |
1253 | - |
1254 | - |
1255 | -def _get_full_path(path): |
1256 | - return os.path.normpath( |
1257 | - os.path.abspath( |
1258 | - os.path.join(os.getcwd(), path) |
1259 | - ) |
1260 | - ) |
1261 | - |
1262 | - |
1263 | -def _make_results_dir(): |
1264 | - try: |
1265 | - os.makedirs(config.results_directory) |
1266 | - except OSError: |
1267 | - pass # already exists |
1268 | + res.stopTestRun() |
1269 | |
1270 | |
1271 | def find_shared_directory(test_dir, shared_directory): |
1272 | @@ -165,9 +160,17 @@ |
1273 | The intention is that if you have 'tests/shared' and 'tests/foo' you |
1274 | run `sst-run -d tests/foo` and 'tests/shared' will still be used as |
1275 | the shared directory. |
1276 | + |
1277 | + IMHO the above is only needed because we don't allow: |
1278 | + sst-run --start with tests.foo |
1279 | + |
1280 | + So I plan to remove the support for searching 'shared' upwards in favor of |
1281 | + allowing running a test subset and go with a sane layout and import |
1282 | + behavior. No test fail if this feature is removed so it's not supported |
1283 | + anyway. -- vila 2013-04-26 |
1284 | """ |
1285 | if shared_directory is not None: |
1286 | - return _get_full_path(shared_directory) |
1287 | + return os.path.abspath(shared_directory) |
1288 | |
1289 | cwd = os.getcwd() |
1290 | default_shared = os.path.join(test_dir, 'shared') |
1291 | @@ -180,483 +183,6 @@ |
1292 | if os.path.isdir(this_shared): |
1293 | shared_directory = this_shared |
1294 | break |
1295 | - relpath = os.path.split(relpath)[0] |
1296 | - |
1297 | - return _get_full_path(shared_directory) |
1298 | - |
1299 | - |
1300 | -def get_suites(test_names, test_dir, shared_dir, collect_only, |
1301 | - browser_factory, |
1302 | - screenshots_on, failfast, debug, |
1303 | - extended=False |
1304 | - ): |
1305 | - return [ |
1306 | - get_suite( |
1307 | - test_names, root, collect_only, |
1308 | - browser_factory, |
1309 | - screenshots_on, failfast, debug, |
1310 | - extended=extended, |
1311 | - ) |
1312 | - for root, _, _ in os.walk(test_dir, followlinks=True) |
1313 | - if os.path.abspath(root) != shared_dir and |
1314 | - not os.path.abspath(root).startswith(shared_dir + os.path.sep) |
1315 | - and not os.path.split(root)[1].startswith('_') |
1316 | - ] |
1317 | - |
1318 | - |
1319 | -def find_cases(test_names, test_dir): |
1320 | - found = set() |
1321 | - dir_list = os.listdir(test_dir) |
1322 | - filtered_dir_list = set() |
1323 | - if not test_names: |
1324 | - test_names = ['*', ] |
1325 | - for name_pattern in test_names: |
1326 | - if not name_pattern.endswith('.py'): |
1327 | - name_pattern += '.py' |
1328 | - matches = fnmatch.filter(dir_list, name_pattern) |
1329 | - if matches: |
1330 | - for match in matches: |
1331 | - if os.path.isfile(os.path.join(test_dir, match)): |
1332 | - filtered_dir_list.add(match) |
1333 | - for entry in filtered_dir_list: |
1334 | - # conditions for ignoring files |
1335 | - if not entry.endswith('.py'): |
1336 | - continue |
1337 | - if entry.startswith('_'): |
1338 | - continue |
1339 | - found.add(entry) |
1340 | - |
1341 | - return found |
1342 | - |
1343 | - |
1344 | -def get_suite(test_names, test_dir, collect_only, |
1345 | - browser_factory, |
1346 | - screenshots_on, failfast, debug, |
1347 | - extended=False): |
1348 | - |
1349 | - suite = TestSuite() |
1350 | - |
1351 | - for case in find_cases(test_names, test_dir): |
1352 | - csv_path = os.path.join(test_dir, case.replace('.py', '.csv')) |
1353 | - if os.path.isfile(csv_path): |
1354 | - # reading the csv file now |
1355 | - for row in get_data(csv_path): |
1356 | - # row is a dictionary of variables |
1357 | - suite.addTest( |
1358 | - get_case( |
1359 | - test_dir, case, browser_factory, screenshots_on, row, |
1360 | - failfast=failfast, debug=debug, extended=extended |
1361 | - ) |
1362 | - ) |
1363 | - else: |
1364 | - suite.addTest( |
1365 | - get_case( |
1366 | - test_dir, case, browser_factory, screenshots_on, |
1367 | - failfast=failfast, debug=debug, extended=extended |
1368 | - ) |
1369 | - ) |
1370 | - |
1371 | - return suite |
1372 | - |
1373 | - |
1374 | -def use_xvfb_server(test, xvfb=None): |
1375 | - """Setup an xvfb server for a given test. |
1376 | - |
1377 | - :param xvfb: An Xvfb object to use. If none is supplied, default values are |
1378 | - used to build it. |
1379 | - |
1380 | - :returns: The xvfb server used so tests can use the built one. |
1381 | - """ |
1382 | - if xvfb is None: |
1383 | - xvfb = xvfbdisplay.Xvfb() |
1384 | - xvfb.start() |
1385 | - test.addCleanup(xvfb.stop) |
1386 | - return xvfb |
1387 | - |
1388 | - |
1389 | -class TextTestResult(testtools.testresult.TextTestResult): |
1390 | - """A TestResult which outputs activity to a text stream. |
1391 | - |
1392 | - TODO: add the verbosity parameter. |
1393 | - """ |
1394 | - |
1395 | - def __init__(self, stream, failfast=False): |
1396 | - super(TextTestResult, self).__init__(stream, failfast) |
1397 | - |
1398 | - def startTestRun(self): |
1399 | - super(TextTestResult, self).startTestRun() |
1400 | - |
1401 | - def startTest(self, test): |
1402 | - self.stream.write(str(test)) |
1403 | - self.stream.write(' ...\n') |
1404 | - self.start_time = default_timer() |
1405 | - super(TextTestResult, self).startTest(test) |
1406 | - |
1407 | - def stopTest(self, test): |
1408 | - self.stream.write('\n') |
1409 | - self.stream.flush() |
1410 | - super(TextTestResult, self).stopTest(test) |
1411 | - |
1412 | - def addExpectedFailure(self, test, err=None, details=None): |
1413 | - self.stream.write('Expected Failure\n') |
1414 | - super(TextTestResult, self).addExpectedFailure(test, err, details) |
1415 | - |
1416 | - def addError(self, test, err=None, details=None): |
1417 | - self.stream.write('ERROR\n') |
1418 | - super(TextTestResult, self).addError(test, err, details) |
1419 | - |
1420 | - def addFailure(self, test, err=None, details=None): |
1421 | - self.stream.write('FAIL\n') |
1422 | - super(TextTestResult, self).addFailure(test, err, details) |
1423 | - |
1424 | - def addSkip(self, test, reason=None, details=None): |
1425 | - if reason is None: |
1426 | - self.stream.write('Skipped\n') |
1427 | - else: |
1428 | - self.stream.write('Skipped %r\n' % reason) |
1429 | - super(TextTestResult, self).addSkip(test, reason, details) |
1430 | - |
1431 | - def addSuccess(self, test, details=None): |
1432 | - elapsed_time = default_timer() - self.start_time |
1433 | - self.stream.write('OK (%.3f secs)' % elapsed_time) |
1434 | - super(TextTestResult, self).addSuccess(test, details) |
1435 | - |
1436 | - def addUnexpectedSuccess(self, test, details=None): |
1437 | - self.stream.write('Unexpected Success\n') |
1438 | - super(TextTestResult, self).addUnexpectedSuccess(test, details) |
1439 | - |
1440 | - |
1441 | -class BrowserFactory(object): |
1442 | - """Handle browser creation for tests. |
1443 | - |
1444 | - One instance is used for a given test run. |
1445 | - """ |
1446 | - |
1447 | - webdriver_class = None |
1448 | - |
1449 | - def __init__(self, javascript_disabled=False): |
1450 | - super(BrowserFactory, self).__init__() |
1451 | - self.javascript_disabled = javascript_disabled |
1452 | - |
1453 | - def setup_for_test(self, test): |
1454 | - """Setup the browser for the given test. |
1455 | - |
1456 | - Some browsers accept more options that are test (and browser) specific. |
1457 | - |
1458 | - Daughter classes should redefine this method to capture them. |
1459 | - """ |
1460 | - pass |
1461 | - |
1462 | - def browser(self): |
1463 | - """Create a browser based on previously collected options. |
1464 | - |
1465 | - Daughter classes should override this method if they need to provide |
1466 | - more context. |
1467 | - """ |
1468 | - return self.webdriver_class() |
1469 | - |
1470 | - |
1471 | -# FIXME: Missing tests -- vila 2013-04-11 |
1472 | -class RemoteBrowserFactory(BrowserFactory): |
1473 | - |
1474 | - webdriver_class = webdriver.Remote |
1475 | - |
1476 | - def __init__(self, capabilities, remote_url): |
1477 | - super(RemoteBrowserFactory, self).__init__() |
1478 | - self.capabilities = capabilities |
1479 | - self.remote_url = remote_url |
1480 | - |
1481 | - def browser(self): |
1482 | - return self.webdriver_class(self.capabilities, self.remote_url) |
1483 | - |
1484 | - |
1485 | -# FIXME: Missing tests -- vila 2013-04-11 |
1486 | -class ChromeFactory(BrowserFactory): |
1487 | - |
1488 | - webdriver_class = webdriver.Chrome |
1489 | - |
1490 | - |
1491 | -# FIXME: Missing tests -- vila 2013-04-11 |
1492 | -class IeFactory(BrowserFactory): |
1493 | - |
1494 | - webdriver_class = webdriver.Ie |
1495 | - |
1496 | - |
1497 | -# FIXME: Missing tests -- vila 2013-04-11 |
1498 | -class PhantomJSFactory(BrowserFactory): |
1499 | - |
1500 | - webdriver_class = webdriver.PhantomJS |
1501 | - |
1502 | - |
1503 | -# FIXME: Missing tests -- vila 2013-04-11 |
1504 | -class OperaFactory(BrowserFactory): |
1505 | - |
1506 | - webdriver_class = webdriver.Opera |
1507 | - |
1508 | - |
1509 | -class FirefoxFactory(BrowserFactory): |
1510 | - |
1511 | - webdriver_class = webdriver.Firefox |
1512 | - |
1513 | - def setup_for_test(self, test): |
1514 | - profile = webdriver.FirefoxProfile() |
1515 | - profile.set_preference('intl.accept_languages', 'en') |
1516 | - if test.assume_trusted_cert_issuer: |
1517 | - profile.set_preference('webdriver_assume_untrusted_issuer', False) |
1518 | - profile.set_preference( |
1519 | - 'capability.policy.default.Window.QueryInterface', 'allAccess') |
1520 | - profile.set_preference( |
1521 | - 'capability.policy.default.Window.frameElement.get', |
1522 | - 'allAccess') |
1523 | - if test.javascript_disabled or self.javascript_disabled: |
1524 | - profile.set_preference('javascript.enabled', False) |
1525 | - self.profile = profile |
1526 | - |
1527 | - def browser(self): |
1528 | - return self.webdriver_class(self.profile) |
1529 | - |
1530 | - |
1531 | -# FIXME: Missing tests -- vila 2013-04-11 |
1532 | -browser_factories = { |
1533 | - 'Chrome': ChromeFactory, |
1534 | - 'Firefox': FirefoxFactory, |
1535 | - 'Ie': IeFactory, |
1536 | - 'Opera': OperaFactory, |
1537 | - 'PhantomJS': PhantomJSFactory, |
1538 | -} |
1539 | - |
1540 | - |
1541 | -class SSTTestCase(testtools.TestCase): |
1542 | - """A test case that can use the sst framework.""" |
1543 | - |
1544 | - xvfb = None |
1545 | - xserver_headless = False |
1546 | - |
1547 | - browser_factory = FirefoxFactory() |
1548 | - |
1549 | - javascript_disabled = False |
1550 | - assume_trusted_cert_issuer = False |
1551 | - |
1552 | - wait_timeout = 10 |
1553 | - wait_poll = 0.1 |
1554 | - base_url = None |
1555 | - |
1556 | - results_directory = _get_full_path('results') |
1557 | - screenshots_on = False |
1558 | - debug_post_mortem = False |
1559 | - extended_report = False |
1560 | - |
1561 | - def shortDescription(self): |
1562 | - return None |
1563 | - |
1564 | - def setUp(self): |
1565 | - super(SSTTestCase, self).setUp() |
1566 | - if self.base_url is not None: |
1567 | - actions.set_base_url(self.base_url) |
1568 | - actions._set_wait_timeout(self.wait_timeout, self.wait_poll) |
1569 | - # Ensures sst.actions will find me |
1570 | - actions._test = self |
1571 | - if self.xserver_headless and self.xvfb is None: |
1572 | - # If we need to run headless and no xvfb is already running, start |
1573 | - # a new one for the current test, scheduling the shutdown for the |
1574 | - # end of the test. |
1575 | - self.xvfb = use_xvfb_server(self) |
1576 | - config.results_directory = self.results_directory |
1577 | - _make_results_dir() |
1578 | - self.start_browser() |
1579 | - self.addCleanup(self.stop_browser) |
1580 | - if self.screenshots_on: |
1581 | - self.addOnException(self.take_screenshot_and_page_dump) |
1582 | - if self.debug_post_mortem: |
1583 | - self.addOnException( |
1584 | - self.print_exception_and_enter_post_mortem) |
1585 | - if self.extended_report: |
1586 | - self.addOnException(self.report_extensively) |
1587 | - |
1588 | - def start_browser(self): |
1589 | - logger.debug('Starting browser') |
1590 | - self.browser_factory.setup_for_test(self) |
1591 | - self.browser = self.browser_factory.browser() |
1592 | - logger.debug('Browser started: %s' % (self.browser.name)) |
1593 | - |
1594 | - def stop_browser(self): |
1595 | - logger.debug('Stopping browser') |
1596 | - self.browser.quit() |
1597 | - |
1598 | - def take_screenshot_and_page_dump(self, exc_info): |
1599 | - try: |
1600 | - filename = 'screenshot-{0}.png'.format(self.id()) |
1601 | - actions.take_screenshot(filename) |
1602 | - except Exception: |
1603 | - # FIXME: Needs to be reported somehow ? -- vila 2012-10-16 |
1604 | - pass |
1605 | - try: |
1606 | - # also dump page source |
1607 | - filename = 'pagesource-{0}.html'.format(self.id()) |
1608 | - actions.save_page_source(filename) |
1609 | - except Exception: |
1610 | - # FIXME: Needs to be reported somehow ? -- vila 2012-10-16 |
1611 | - pass |
1612 | - |
1613 | - def print_exception_and_enter_post_mortem(self, exc_info): |
1614 | - exc_class, exc, tb = exc_info |
1615 | - traceback.print_exception(exc_class, exc, tb) |
1616 | - pdb.post_mortem(tb) |
1617 | - |
1618 | - def report_extensively(self, exc_info): |
1619 | - exc_class, exc, tb = exc_info |
1620 | - original_message = str(exc) |
1621 | - try: |
1622 | - current_url = actions.get_current_url() |
1623 | - except Exception: |
1624 | - current_url = 'unavailable' |
1625 | - try: |
1626 | - page_source = actions.get_page_source() |
1627 | - except Exception: |
1628 | - page_source = 'unavailable' |
1629 | - self.addDetail( |
1630 | - 'Original exception', |
1631 | - testtools.content.text_content('{0} : {1}'.format( |
1632 | - exc.__class__.__name__, original_message))) |
1633 | - self.addDetail('Current url', |
1634 | - testtools.content.text_content(current_url)) |
1635 | - self.addDetail('Page source', |
1636 | - testtools.content.text_content(page_source)) |
1637 | - |
1638 | - |
1639 | -class SSTScriptTestCase(SSTTestCase): |
1640 | - """Test case used internally by sst-run and sst-remote.""" |
1641 | - |
1642 | - script_dir = '.' |
1643 | - script_name = None |
1644 | - |
1645 | - def __init__(self, test_method, context_row=None): |
1646 | - super(SSTScriptTestCase, self).__init__('run_test_script') |
1647 | - self.test_method = test_method |
1648 | - self.id = lambda: '%s.%s.%s' % (self.__class__.__module__, |
1649 | - self.__class__.__name__, test_method) |
1650 | - if context_row is None: |
1651 | - context_row = {} |
1652 | - self.context = context_row |
1653 | - |
1654 | - def __str__(self): |
1655 | - # Since we use run_test_script to encapsulate the call to the |
1656 | - # compiled code, we need to override __str__ to get a proper name |
1657 | - # reported. |
1658 | - return "%s (%s)" % (self.test_method, self.id()) |
1659 | - |
1660 | - def shortDescription(self): |
1661 | - # The description should be first line of the test method's docstring. |
1662 | - # Since we have no real test method here, we override it to always |
1663 | - # return none. |
1664 | - return None |
1665 | - |
1666 | - def setUp(self): |
1667 | - self.script_path = os.path.join(self.script_dir, self.script_name) |
1668 | - sys.path.append(self.script_dir) |
1669 | - self.addCleanup(sys.path.remove, self.script_dir) |
1670 | - self._compile_script() |
1671 | - # The script may override some settings. The default value for |
1672 | - # JAVASCRIPT_DISABLED and ASSUME_TRUSTED_CERT_ISSUER are False, so if |
1673 | - # the user mentions them in his script, it's to turn them on. Also, |
1674 | - # getting our hands on the values used in the script is too hackish ;) |
1675 | - if 'JAVASCRIPT_DISABLED' in self.code.co_names: |
1676 | - self.javascript_disabled = True |
1677 | - if 'ASSUME_TRUSTED_CERT_ISSUER' in self.code.co_names: |
1678 | - self.assume_trusted_cert_issuer = True |
1679 | - super(SSTScriptTestCase, self).setUp() |
1680 | - # Start with default values |
1681 | - actions.reset_base_url() |
1682 | - actions._set_wait_timeout(10, 0.1) |
1683 | - # Possibly inject parametrization from associated .csv file |
1684 | - previous_context = context.store_context() |
1685 | - self.addCleanup(context.restore_context, previous_context) |
1686 | - context.populate_context(self.context, self.script_path, |
1687 | - self.browser.name, self.javascript_disabled) |
1688 | - |
1689 | - def _compile_script(self): |
1690 | - with open(self.script_path) as f: |
1691 | - source = f.read() + '\n' |
1692 | - self.code = compile(source, self.script_path, 'exec') |
1693 | - |
1694 | - def run_test_script(self, result=None): |
1695 | - # Run the test catching exceptions sstnam style |
1696 | - try: |
1697 | - exec self.code in self.context |
1698 | - except EndTest: |
1699 | - pass |
1700 | - |
1701 | - |
1702 | -def _has_classes(test_dir, entry): |
1703 | - """Scan Python source file and check for a class definition.""" |
1704 | - with open(os.path.join(test_dir, entry)) as f: |
1705 | - source = f.read() + '\n' |
1706 | - found_classes = [] |
1707 | - |
1708 | - def visit_class_def(node): |
1709 | - found_classes.append(True) |
1710 | - |
1711 | - node_visitor = ast.NodeVisitor() |
1712 | - node_visitor.visit_ClassDef = visit_class_def |
1713 | - node_visitor.visit(ast.parse(source)) |
1714 | - return bool(found_classes) |
1715 | - |
1716 | - |
1717 | -def get_case(test_dir, entry, browser_factory, screenshots_on, |
1718 | - context=None, failfast=False, debug=False, extended=False): |
1719 | - # our naming convention for tests requires that script-based tests must |
1720 | - # not begin with "test_*." SSTTestCase class-based or other |
1721 | - # unittest.TestCase based source files must begin with "test_*". |
1722 | - # we also scan the source file to see if it has class definitions, |
1723 | - # since script base cases normally don't, but TestCase class-based |
1724 | - # tests always will. |
1725 | - if entry.startswith('test_') and _has_classes(test_dir, entry): |
1726 | - # load just the individual file's tests |
1727 | - this_test = defaultTestLoader.discover(test_dir, pattern=entry) |
1728 | - else: # this is for script-based test |
1729 | - name = entry[:-3] |
1730 | - test_name = 'test_%s' % name |
1731 | - this_test = SSTScriptTestCase(test_name, context) |
1732 | - this_test.script_dir = test_dir |
1733 | - this_test.script_name = entry |
1734 | - this_test.browser_factory = browser_factory |
1735 | - |
1736 | - this_test.screenshots_on = screenshots_on |
1737 | - this_test.debug_post_mortem = debug |
1738 | - this_test.extended_report = extended |
1739 | - |
1740 | - return this_test |
1741 | - |
1742 | - |
1743 | -def get_data(csv_path): |
1744 | - """ |
1745 | - Return a list of data dicts for parameterized testing. |
1746 | - |
1747 | - the first row (headers) match data_map key names. |
1748 | - rows beneath are filled with data values. |
1749 | - """ |
1750 | - rows = [] |
1751 | - print ' Reading data from %r...' % os.path.split(csv_path)[-1], |
1752 | - row_num = 0 |
1753 | - with open(csv_path) as f: |
1754 | - headers = f.readline().rstrip().split('^') |
1755 | - headers = [header.replace('"', '') for header in headers] |
1756 | - headers = [header.replace("'", '') for header in headers] |
1757 | - for line in f: |
1758 | - row = {} |
1759 | - row_num += 1 |
1760 | - row['_row_num'] = row_num |
1761 | - fields = line.rstrip().split('^') |
1762 | - for header, field in zip(headers, fields): |
1763 | - try: |
1764 | - value = ast.literal_eval(field) |
1765 | - except ValueError: |
1766 | - value = field |
1767 | - if value.lower() == 'false': |
1768 | - value = False |
1769 | - if value.lower() == 'true': |
1770 | - value = True |
1771 | - row[header] = value |
1772 | - rows.append(row) |
1773 | - print 'found %s rows' % len(rows) |
1774 | - return rows |
1775 | + relpath = os.path.dirname(relpath) |
1776 | + |
1777 | + return os.path.abspath(shared_directory) |
1778 | |
1779 | === modified file 'src/sst/scripts/remote.py' |
1780 | --- src/sst/scripts/remote.py 2013-04-18 18:37:10 +0000 |
1781 | +++ src/sst/scripts/remote.py 2013-05-14 11:43:29 +0000 |
1782 | @@ -55,6 +55,9 @@ |
1783 | failfast=cmd_opts.failfast, |
1784 | debug=cmd_opts.debug, |
1785 | extended=cmd_opts.extended_tracebacks, |
1786 | + # FIXME: not tested -- vila 2013-05-07 |
1787 | + includes=cmd_opts.includes, |
1788 | + excludes=cmd_opts.excludes |
1789 | ) |
1790 | |
1791 | print '--------------------------------------------------------------' |
1792 | |
1793 | === modified file 'src/sst/scripts/run.py' |
1794 | --- src/sst/scripts/run.py 2013-04-23 11:01:12 +0000 |
1795 | +++ src/sst/scripts/run.py 2013-05-14 11:43:29 +0000 |
1796 | @@ -33,6 +33,7 @@ |
1797 | |
1798 | import sst |
1799 | from sst import ( |
1800 | + browsers, |
1801 | command, |
1802 | runtests, |
1803 | tests, |
1804 | @@ -81,7 +82,7 @@ |
1805 | |
1806 | try: |
1807 | command.clear_old_results() |
1808 | - factory = runtests.browser_factories.get(cmd_opts.browser_type) |
1809 | + factory = browsers.browser_factories.get(cmd_opts.browser_type) |
1810 | runtests.runtests( |
1811 | args, |
1812 | test_dir=cmd_opts.dir_name, |
1813 | @@ -93,6 +94,8 @@ |
1814 | failfast=cmd_opts.failfast, |
1815 | debug=cmd_opts.debug, |
1816 | extended=cmd_opts.extended_tracebacks, |
1817 | + includes=cmd_opts.includes, |
1818 | + excludes=cmd_opts.excludes |
1819 | ) |
1820 | finally: |
1821 | |
1822 | @@ -111,7 +114,9 @@ |
1823 | |
1824 | def run_django(port): |
1825 | """Start django server for running local self-tests.""" |
1826 | - manage_file = './src/testproject/manage.py' |
1827 | + here = os.path.abspath(os.path.dirname(__file__)) |
1828 | + manage_file = os.path.abspath( |
1829 | + os.path.join(here, '../../testproject/manage.py')) |
1830 | url = 'http://localhost:%s/' % port |
1831 | |
1832 | if not os.path.isfile(manage_file): |
1833 | @@ -124,6 +129,9 @@ |
1834 | if django is None: |
1835 | print 'Error: can not find django module.' |
1836 | print 'you must have django installed to run the test project.' |
1837 | + # FIXME: Using sys.exit() makes it hard to test in isolation. Moreover |
1838 | + # this error path is not covered by a test. Both points may be related |
1839 | + # ;) -- vila 2013-05-10 |
1840 | sys.exit(1) |
1841 | proc = subprocess.Popen([manage_file, 'runserver', port], |
1842 | stderr=open(os.devnull, 'w'), |
1843 | |
1844 | === modified file 'src/sst/selftests/context.py' |
1845 | --- src/sst/selftests/context.py 2011-08-02 13:11:07 +0000 |
1846 | +++ src/sst/selftests/context.py 2013-05-14 11:43:29 +0000 |
1847 | @@ -8,5 +8,6 @@ |
1848 | assert __name__ == 'context' |
1849 | assert __file__.endswith('context.py') |
1850 | |
1851 | -thisdir = os.path.dirname(__file__) |
1852 | -assert config.shared_directory == os.path.join(thisdir, 'shared') |
1853 | +this_dir = os.path.dirname(__file__) |
1854 | +this_shared = os.path.abspath(os.path.join(this_dir, 'shared')) |
1855 | +assert (config.shared_directory == this_shared) |
1856 | |
1857 | === removed file 'src/sst/selftests/importing.py' |
1858 | --- src/sst/selftests/importing.py 2012-12-16 15:25:23 +0000 |
1859 | +++ src/sst/selftests/importing.py 1970-01-01 00:00:00 +0000 |
1860 | @@ -1,4 +0,0 @@ |
1861 | -from _module import foo |
1862 | - |
1863 | -# the current test directory should be added to sys.path |
1864 | -assert foo == 3 |
1865 | |
1866 | === added directory 'src/sst/selftests/regular' |
1867 | === added file 'src/sst/selftests/regular/__init__.py' |
1868 | --- src/sst/selftests/regular/__init__.py 1970-01-01 00:00:00 +0000 |
1869 | +++ src/sst/selftests/regular/__init__.py 2013-05-14 11:43:29 +0000 |
1870 | @@ -0,0 +1,4 @@ |
1871 | +from sst import loader |
1872 | + |
1873 | + |
1874 | +discover = loader.discoverRegularTests |
1875 | |
1876 | === renamed file 'src/sst/selftests/test_testtools_testcase.py' => 'src/sst/selftests/regular/test_testtools_testcase.py' |
1877 | --- src/sst/selftests/test_testtools_testcase.py 2013-02-07 16:01:21 +0000 |
1878 | +++ src/sst/selftests/regular/test_testtools_testcase.py 2013-05-14 11:43:29 +0000 |
1879 | @@ -5,8 +5,5 @@ |
1880 | |
1881 | class TestTestToolsTestCase(TestCase): |
1882 | |
1883 | - def shortDescription(self): |
1884 | - return None |
1885 | - |
1886 | def test_true(self): |
1887 | self.assertTrue(True) |
1888 | |
1889 | === renamed file 'src/sst/selftests/test_two_methods.py' => 'src/sst/selftests/regular/test_two_methods.py' |
1890 | --- src/sst/selftests/test_two_methods.py 2013-04-11 14:34:47 +0000 |
1891 | +++ src/sst/selftests/regular/test_two_methods.py 2013-05-14 11:43:29 +0000 |
1892 | @@ -1,7 +1,7 @@ |
1893 | -from sst import runtests |
1894 | - |
1895 | - |
1896 | -class TestBoth(runtests.SSTTestCase): |
1897 | +from sst import cases |
1898 | + |
1899 | + |
1900 | +class TestBoth(cases.SSTTestCase): |
1901 | |
1902 | def test_one(self): |
1903 | assert True |
1904 | |
1905 | === renamed file 'src/sst/selftests/test_unittest_testcase.py' => 'src/sst/selftests/regular/test_unittest_testcase.py' |
1906 | === modified file 'src/sst/selftests/shared/helpers.py' |
1907 | --- src/sst/selftests/shared/helpers.py 2013-04-23 08:42:16 +0000 |
1908 | +++ src/sst/selftests/shared/helpers.py 2013-05-14 11:43:29 +0000 |
1909 | @@ -19,7 +19,9 @@ |
1910 | # this will copy testproj.db from testproj.db.orginal as setup |
1911 | # and remove database after test as a cleanup. |
1912 | # (this is used for all tests that access local django admin app only) |
1913 | - test_db = 'src/testproject/testproj.db' |
1914 | + here = os.path.abspath(os.path.dirname(__file__)) |
1915 | + test_db = os.path.abspath( |
1916 | + os.path.join(here, '..', '..', '..', 'testproject/testproj.db')) |
1917 | if os.path.isfile(test_db): |
1918 | os.remove(test_db) |
1919 | shutil.copyfile(test_db + '.original', test_db) |
1920 | |
1921 | === modified file 'src/sst/selftests/static_file.py' |
1922 | --- src/sst/selftests/static_file.py 2013-04-19 08:03:08 +0000 |
1923 | +++ src/sst/selftests/static_file.py 2013-05-14 11:43:29 +0000 |
1924 | @@ -6,8 +6,8 @@ |
1925 | import sst.actions |
1926 | |
1927 | |
1928 | -static_file = os.path.join(''.join(os.path.split(__file__)[:-1]), |
1929 | - 'static.html') |
1930 | +static_file = os.path.abspath( |
1931 | + os.path.join(os.path.dirname(__file__), 'static.html')) |
1932 | |
1933 | # using full path |
1934 | sst.actions.go_to('file:////%s' % static_file) |
1935 | |
1936 | === modified file 'src/sst/tests/__init__.py' |
1937 | --- src/sst/tests/__init__.py 2013-04-23 00:22:48 +0000 |
1938 | +++ src/sst/tests/__init__.py 2013-05-14 11:43:29 +0000 |
1939 | @@ -19,12 +19,14 @@ |
1940 | import os |
1941 | import shutil |
1942 | import socket |
1943 | +import sys |
1944 | import tempfile |
1945 | |
1946 | -from sst import runtests |
1947 | - |
1948 | - |
1949 | -class SSTBrowserLessTestCase(runtests.SSTTestCase): |
1950 | +import testtools |
1951 | +from sst import cases |
1952 | + |
1953 | + |
1954 | +class SSTBrowserLessTestCase(cases.SSTTestCase): |
1955 | """A specialized test class for tests that don't need a browser.""" |
1956 | |
1957 | # We don't use a browser here so disable its use to speed the tests |
1958 | @@ -36,6 +38,19 @@ |
1959 | pass |
1960 | |
1961 | |
1962 | +class ImportingLocalFilesTest(testtools.TestCase): |
1963 | + """Class for tests requiring import of locally generated files. |
1964 | + |
1965 | + This setup the tests working dir in a newly created temp dir and restore |
1966 | + sys.modules and sys.path at the end of the test. |
1967 | + """ |
1968 | + def setUp(self): |
1969 | + super(ImportingLocalFilesTest, self).setUp() |
1970 | + set_cwd_to_tmp(self) |
1971 | + protect_imports(self) |
1972 | + sys.path.insert(0, self.test_base_dir) |
1973 | + |
1974 | + |
1975 | def set_cwd_to_tmp(test): |
1976 | """Create a temp dir an cd into it for the test duration. |
1977 | |
1978 | @@ -48,6 +63,27 @@ |
1979 | os.chdir(test.test_base_dir) |
1980 | |
1981 | |
1982 | +def protect_imports(test): |
1983 | + """Protect sys.modules and sys.path for the test duration. |
1984 | + |
1985 | + This is useful to test imports which modifies sys.modules or requires |
1986 | + modifying sys.path. |
1987 | + """ |
1988 | + # Protect sys.modules and sys.path to be able to test imports |
1989 | + test.patch(sys, 'path', list(sys.path)) |
1990 | + orig_modules = sys.modules.copy() |
1991 | + |
1992 | + def cleanup_modules(): |
1993 | + # Remove all added modules |
1994 | + added = [m for m in sys.modules.keys() if m not in orig_modules] |
1995 | + if added: |
1996 | + for m in added: |
1997 | + del sys.modules[m] |
1998 | + # Restore deleted or modified modules |
1999 | + sys.modules.update(orig_modules) |
2000 | + test.addCleanup(cleanup_modules) |
2001 | + |
2002 | + |
2003 | def check_devserver_port_used(port): |
2004 | """check if port is ok to use for django devserver""" |
2005 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
2006 | @@ -61,3 +97,51 @@ |
2007 | finally: |
2008 | sock.close() |
2009 | return used |
2010 | + |
2011 | + |
2012 | +def write_tree_from_desc(description): |
2013 | + """Write a tree described in a textual form to disk. |
2014 | + |
2015 | + The textual form describes the file contents separated by file/dir names. |
2016 | + |
2017 | + 'file: <file name>' on a single line starts a file description. The file |
2018 | + name must be the relative path from the tree root. |
2019 | + |
2020 | + 'dir: <dir name>' on a single line starts a dir description. |
2021 | + |
2022 | + 'link: <link source> <link name>' on a single line describes a symlink to |
2023 | + <link source> named <link name>. The source may not exist, spaces are not |
2024 | + allowed. |
2025 | + |
2026 | + :param description: A text where files and directories contents is |
2027 | + described in a textual form separated by file/dir names. |
2028 | + """ |
2029 | + cur_file = None |
2030 | + for line in description.splitlines(): |
2031 | + if line.startswith('file: '): |
2032 | + # A new file begins |
2033 | + if cur_file: |
2034 | + cur_file.close() |
2035 | + cur_file = open(line[len('file: '):], 'w') |
2036 | + continue |
2037 | + if line.startswith('dir:'): |
2038 | + # A new dir begins |
2039 | + if cur_file: |
2040 | + cur_file.close() |
2041 | + cur_file = None |
2042 | + os.mkdir(line[len('dir: '):]) |
2043 | + continue |
2044 | + if line.startswith('link: '): |
2045 | + # We don't support spaces in names |
2046 | + link_desc = line[len('link: '):] |
2047 | + try: |
2048 | + source, link = link_desc.split() |
2049 | + except ValueError: |
2050 | + raise ValueError('Invalid link description: %s' % (link_desc,)) |
2051 | + os.symlink(source, link) |
2052 | + continue |
2053 | + if cur_file is not None: # If no file is declared, nothing is written |
2054 | + # splitlines() removed the \n, let's add it again |
2055 | + cur_file.write(line + '\n') |
2056 | + if cur_file: |
2057 | + cur_file.close() |
2058 | |
2059 | === added file 'src/sst/tests/test_command.py' |
2060 | --- src/sst/tests/test_command.py 1970-01-01 00:00:00 +0000 |
2061 | +++ src/sst/tests/test_command.py 2013-05-14 11:43:29 +0000 |
2062 | @@ -0,0 +1,62 @@ |
2063 | +# |
2064 | +# Copyright (c) 2013 Canonical Ltd. |
2065 | +# |
2066 | +# This file is part of: SST (selenium-simple-test) |
2067 | +# https://launchpad.net/selenium-simple-test |
2068 | +# |
2069 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
2070 | +# you may not use this file except in compliance with the License. |
2071 | +# You may obtain a copy of the License at |
2072 | +# |
2073 | +# http://www.apache.org/licenses/LICENSE-2.0 |
2074 | +# |
2075 | +# Unless required by applicable law or agreed to in writing, software |
2076 | +# distributed under the License is distributed on an "AS IS" BASIS, |
2077 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2078 | +# See the License for the specific language governing permissions and |
2079 | +# limitations under the License. |
2080 | +# |
2081 | + |
2082 | +import testtools |
2083 | + |
2084 | +from sst import command |
2085 | + |
2086 | + |
2087 | +class TestArgParsing(testtools.TestCase): |
2088 | + |
2089 | + def parse_args(self, provided_args): |
2090 | + # command.get_opts_run and friends relies on optparse defaulting to |
2091 | + # sys.argv[1:]. To comply with that, we add a dummy first arg to |
2092 | + # represent the script name and remove it from the returned args. |
2093 | + opts, remaining_args = command.get_opts_run( |
2094 | + ['dummy-for-tests'] + provided_args) |
2095 | + self.assertEqual('dummy-for-tests', remaining_args[0]) |
2096 | + return opts, remaining_args[1:] |
2097 | + |
2098 | + def test_default_values(self): |
2099 | + opts, args = self.parse_args([]) |
2100 | + self.assertIs(None, opts.includes) |
2101 | + self.assertIs(None, opts.excludes) |
2102 | + self.assertEqual([], args) |
2103 | + |
2104 | + def test_single_include(self): |
2105 | + opts, args = self.parse_args(['--include', 'foo']) |
2106 | + self.assertEquals(['foo'], opts.includes) |
2107 | + self.assertIs(None, opts.excludes) |
2108 | + |
2109 | + def test_multiple_includes(self): |
2110 | + opts, args = self.parse_args( |
2111 | + ['--include', 'foo', '-i', 'bar', '-ibaz']) |
2112 | + self.assertEquals(['foo', 'bar', 'baz'], opts.includes) |
2113 | + self.assertIs(None, opts.excludes) |
2114 | + |
2115 | + def test_single_exclude(self): |
2116 | + opts, args = self.parse_args(['--exclude', 'foo']) |
2117 | + self.assertEquals(['foo'], opts.excludes) |
2118 | + self.assertIs(None, opts.includes) |
2119 | + |
2120 | + def test_multiple_excludes(self): |
2121 | + opts, args = self.parse_args( |
2122 | + ['--exclude', 'foo', '-e', 'bar', '-ebaz']) |
2123 | + self.assertEquals(['foo', 'bar', 'baz'], opts.excludes) |
2124 | + self.assertIs(None, opts.includes) |
2125 | |
2126 | === modified file 'src/sst/tests/test_django_devserver.py' |
2127 | --- src/sst/tests/test_django_devserver.py 2013-04-23 08:42:16 +0000 |
2128 | +++ src/sst/tests/test_django_devserver.py 2013-05-14 11:43:29 +0000 |
2129 | @@ -23,9 +23,10 @@ |
2130 | |
2131 | import testtools |
2132 | |
2133 | + |
2134 | +import sst |
2135 | +from sst import tests |
2136 | from sst.scripts import run |
2137 | -from sst.tests import check_devserver_port_used |
2138 | -from sst import DEVSERVER_PORT |
2139 | |
2140 | |
2141 | class TestDjangoDevServer(testtools.TestCase): |
2142 | @@ -35,18 +36,19 @@ |
2143 | # capture test output so we don't pollute the test runs |
2144 | self.out = StringIO() |
2145 | self.patch(sys, 'stdout', self.out) |
2146 | + tests.set_cwd_to_tmp(self) |
2147 | |
2148 | def test_django_start(self): |
2149 | - self.addCleanup(run.kill_django, DEVSERVER_PORT) |
2150 | - proc = run.run_django(DEVSERVER_PORT) |
2151 | + self.addCleanup(run.kill_django, sst.DEVSERVER_PORT) |
2152 | + proc = run.run_django(sst.DEVSERVER_PORT) |
2153 | self.assertIsNotNone(proc) |
2154 | |
2155 | def test_django_devserver_port_used(self): |
2156 | - used = check_devserver_port_used(DEVSERVER_PORT) |
2157 | + used = tests.check_devserver_port_used(sst.DEVSERVER_PORT) |
2158 | self.assertFalse(used) |
2159 | |
2160 | - self.addCleanup(run.kill_django, DEVSERVER_PORT) |
2161 | - run.run_django(DEVSERVER_PORT) |
2162 | + self.addCleanup(run.kill_django, sst.DEVSERVER_PORT) |
2163 | + run.run_django(sst.DEVSERVER_PORT) |
2164 | |
2165 | - used = check_devserver_port_used(DEVSERVER_PORT) |
2166 | + used = tests.check_devserver_port_used(sst.DEVSERVER_PORT) |
2167 | self.assertTrue(used) |
2168 | |
2169 | === added file 'src/sst/tests/test_filters.py' |
2170 | --- src/sst/tests/test_filters.py 1970-01-01 00:00:00 +0000 |
2171 | +++ src/sst/tests/test_filters.py 2013-05-14 11:43:29 +0000 |
2172 | @@ -0,0 +1,142 @@ |
2173 | +# |
2174 | +# Copyright (c) 2013 Canonical Ltd. |
2175 | +# |
2176 | +# This file is part of: SST (selenium-simple-test) |
2177 | +# https://launchpad.net/selenium-simple-test |
2178 | +# |
2179 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
2180 | +# you may not use this file except in compliance with the License. |
2181 | +# You may obtain a copy of the License at |
2182 | +# |
2183 | +# http://www.apache.org/licenses/LICENSE-2.0 |
2184 | +# |
2185 | +# Unless required by applicable law or agreed to in writing, software |
2186 | +# distributed under the License is distributed on an "AS IS" BASIS, |
2187 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2188 | +# See the License for the specific language governing permissions and |
2189 | +# limitations under the License. |
2190 | +# |
2191 | +import re |
2192 | +import unittest |
2193 | + |
2194 | +import testtools |
2195 | + |
2196 | +from sst import filters |
2197 | + |
2198 | + |
2199 | +def create_tests_from_ids(ids): |
2200 | + suite = unittest.TestSuite() |
2201 | + |
2202 | + def test_id(name): |
2203 | + return lambda: name |
2204 | + |
2205 | + for tid in ids: |
2206 | + # We need an existing method to create a test. Arbitrarily, we use |
2207 | + # id(), that souldn't fail ;) We won't run the test anyway. |
2208 | + test = unittest.TestCase(methodName='id') |
2209 | + # We can't define the lambda here or 'name' stay bound to the |
2210 | + # variable instead of the value, use a proxy to capture the value. |
2211 | + test.id = test_id(tid) |
2212 | + suite.addTest(test) |
2213 | + return suite |
2214 | + |
2215 | + |
2216 | +class TestFilterTestsById(testtools.TestCase): |
2217 | + |
2218 | + def assertFiltered(self, expected, condition, ids): |
2219 | + """Check that ``condition`` filters tests created from ``ids``.""" |
2220 | + filtered = filters.filter_suite(condition, create_tests_from_ids(ids)) |
2221 | + self.assertEqual(expected, |
2222 | + [t.id() for t in testtools.iterate_tests(filtered)]) |
2223 | + |
2224 | + def test_filter_none(self): |
2225 | + test_names = ['foo', 'bar'] |
2226 | + self.assertFiltered(test_names, lambda t: True, test_names) |
2227 | + |
2228 | + def test_filter_all(self): |
2229 | + test_names = ['foo', 'bar'] |
2230 | + self.assertFiltered([], lambda t: False, test_names) |
2231 | + |
2232 | + def test_filter_start(self): |
2233 | + self.assertFiltered(['foo', 'footix'], |
2234 | + lambda t: t.id().startswith('foo'), |
2235 | + ['foo', 'footix', 'bar', 'baz', 'fo']) |
2236 | + |
2237 | + def test_filter_in(self): |
2238 | + self.assertFiltered(['bar', 'baz'], |
2239 | + lambda t: t.id() in ('bar', 'baz'), |
2240 | + ['foo', 'footix', 'bar', 'baz', 'fo']) |
2241 | + |
2242 | + def test_filter_single(self): |
2243 | + self.assertFiltered(['bar'], |
2244 | + lambda t: t.id() == 'bar', |
2245 | + ['foo', 'bar', 'baz']) |
2246 | + |
2247 | + def test_filter_regexp(self): |
2248 | + ba = re.compile('ba') |
2249 | + self.assertFiltered(['bar', 'baz', 'foobar'], |
2250 | + lambda t: bool(ba.search(t.id())), |
2251 | + ['foo', 'bar', 'baz', 'foobar', 'qux']) |
2252 | + |
2253 | + |
2254 | +class TestFilterTestsByPatterns(testtools.TestCase): |
2255 | + |
2256 | + def assertFiltered(self, expected, patterns, ids): |
2257 | + """Check that ``patterns`` filters tests created from ``ids``.""" |
2258 | + filtered = filters.filter_by_patterns(patterns, |
2259 | + create_tests_from_ids(ids)) |
2260 | + self.assertEqual(expected, |
2261 | + [t.id() for t in testtools.iterate_tests(filtered)]) |
2262 | + |
2263 | + def test_filter_none(self): |
2264 | + self.assertFiltered(['foo', 'bar'], [], ['foo', 'bar']) |
2265 | + |
2266 | + def test_filter_one_pattern(self): |
2267 | + self.assertFiltered(['foo', 'foobar', 'barfoo'], ['*foo*'], |
2268 | + ['foo', 'foobar', 'barfoo', 'baz']) |
2269 | + |
2270 | + def test_filter_several_patterns(self): |
2271 | + self.assertFiltered(['foo', 'foobar', 'barfoo'], ['foo*', '*arf*'], |
2272 | + ['foo', 'foobar', 'barfoo', 'baz']) |
2273 | + |
2274 | + |
2275 | +class TestFilterTestsByIncludedPrefixes(testtools.TestCase): |
2276 | + |
2277 | + def assertFiltered(self, expected, prefixes, ids): |
2278 | + """Check that ``prefixes`` filters tests created from ``ids``.""" |
2279 | + filtered = filters.include_prefixes(prefixes, |
2280 | + create_tests_from_ids(ids)) |
2281 | + self.assertEqual(expected, |
2282 | + [t.id() for t in testtools.iterate_tests(filtered)]) |
2283 | + |
2284 | + def test_no_includes(self): |
2285 | + self.assertFiltered(['foo', 'bar'], [], ['foo', 'bar']) |
2286 | + |
2287 | + def test_one_include(self): |
2288 | + self.assertFiltered(['foo.bar', 'foo.baz'], ['foo'], |
2289 | + ['foo.bar', 'bar', 'foo.baz']) |
2290 | + |
2291 | + def test_several_includes(self): |
2292 | + self.assertFiltered(['foo.bar', 'foo.baz', 'bar.baz'], ['foo', 'bar.'], |
2293 | + ['foo.bar', 'bar', 'foo.baz', 'bar.baz']) |
2294 | + |
2295 | + |
2296 | +class TestFilterTestsByExcludedPrefixes(testtools.TestCase): |
2297 | + |
2298 | + def assertFiltered(self, expected, prefixes, ids): |
2299 | + """Check that ``prefixes`` filters tests created from ``ids``.""" |
2300 | + filtered = filters.exclude_prefixes(prefixes, |
2301 | + create_tests_from_ids(ids)) |
2302 | + self.assertEqual(expected, |
2303 | + [t.id() for t in testtools.iterate_tests(filtered)]) |
2304 | + |
2305 | + def test_no_excludes(self): |
2306 | + self.assertFiltered(['foo', 'bar'], [], ['foo', 'bar']) |
2307 | + |
2308 | + def test_one_exclude(self): |
2309 | + self.assertFiltered(['bar'], ['foo'], |
2310 | + ['foo.bar', 'bar', 'foo.baz']) |
2311 | + |
2312 | + def test_several_excludes(self): |
2313 | + self.assertFiltered(['bar'], ['foo', 'bar.'], |
2314 | + ['foo.bar', 'bar', 'foo.baz', 'bar.baz']) |
2315 | |
2316 | === added file 'src/sst/tests/test_loader.py' |
2317 | --- src/sst/tests/test_loader.py 1970-01-01 00:00:00 +0000 |
2318 | +++ src/sst/tests/test_loader.py 2013-05-14 11:43:29 +0000 |
2319 | @@ -0,0 +1,577 @@ |
2320 | +# |
2321 | +# Copyright (c) 2013 Canonical Ltd. |
2322 | +# |
2323 | +# This file is part of: SST (selenium-simple-test) |
2324 | +# https://launchpad.net/selenium-simple-test |
2325 | +# |
2326 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
2327 | +# you may not use this file except in compliance with the License. |
2328 | +# You may obtain a copy of the License at |
2329 | +# |
2330 | +# http://www.apache.org/licenses/LICENSE-2.0 |
2331 | +# |
2332 | +# Unless required by applicable law or agreed to in writing, software |
2333 | +# distributed under the License is distributed on an "AS IS" BASIS, |
2334 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2335 | +# See the License for the specific language governing permissions and |
2336 | +# limitations under the License. |
2337 | +# |
2338 | +"""Test for sst test loader. |
2339 | + |
2340 | +Many tests below create a temporary file hierarchy including python code and/or |
2341 | +sst scripts. Loading tests imply importing python modules in a way that tests |
2342 | +can observe via sys.modules while preserving isolation. |
2343 | + |
2344 | +The isolation is provided via two means: |
2345 | + |
2346 | +- the file hierarchies are created in a temporary directory added to sys.path |
2347 | + so test can just import from their current directory, |
2348 | + |
2349 | +- tests.protect_imports will remove the loaded modules from sys.modules and |
2350 | + restore sys.path. |
2351 | + |
2352 | +Because the tests themselves share this module name space, care must be taken |
2353 | +by tests to not use module names already used in the module. Most of tests |
2354 | +below therefore use 't' as the main directory because: |
2355 | +- we use python not lisp so using 't' is ok ;) |
2356 | +- it's short, |
2357 | +- it's unlikely to be imported by the module. |
2358 | + |
2359 | +""" |
2360 | +import testtools |
2361 | + |
2362 | +from sst import ( |
2363 | + loader, |
2364 | + tests, |
2365 | +) |
2366 | + |
2367 | + |
2368 | +class TestMatchesForRegexp(testtools.TestCase): |
2369 | + |
2370 | + def test_matches(self): |
2371 | + matches = loader.matches_for_regexp('foo.*') |
2372 | + # All assertions should succeed, if one of them fails, we have a bigger |
2373 | + # problem than having one test for each assertion |
2374 | + self.assertTrue(matches('foo')) |
2375 | + self.assertFalse(matches('bar')) |
2376 | + self.assertTrue(matches('foobar')) |
2377 | + self.assertFalse(matches('barfoo')) |
2378 | + |
2379 | + |
2380 | +class TestMatchesForGlob(testtools.TestCase): |
2381 | + |
2382 | + def test_matches(self): |
2383 | + matches = loader.matches_for_glob('foo*') |
2384 | + # All assertions should succeed, if one of them fails, we have a bigger |
2385 | + # problem than having one test for each assertion |
2386 | + self.assertTrue(matches('foo')) |
2387 | + self.assertFalse(matches('fo')) |
2388 | + self.assertFalse(matches('bar')) |
2389 | + self.assertTrue(matches('foobar')) |
2390 | + self.assertFalse(matches('barfoo')) |
2391 | + |
2392 | + |
2393 | +class TestNameMatcher(testtools.TestCase): |
2394 | + |
2395 | + def test_default_includes(self): |
2396 | + name_matcher = loader.NameMatcher() |
2397 | + self.assertTrue(name_matcher.includes('foo')) |
2398 | + self.assertTrue(name_matcher.matches('foo')) |
2399 | + |
2400 | + def test_default_exclude(self): |
2401 | + name_matcher = loader.NameMatcher() |
2402 | + self.assertFalse(name_matcher.excludes('foo')) |
2403 | + self.assertTrue(name_matcher.matches('foo')) |
2404 | + |
2405 | + def test_provided_includes(self): |
2406 | + name_matcher = loader.NameMatcher( |
2407 | + includes=loader.matches_for_regexp('^.*foo$')) |
2408 | + self.assertTrue(name_matcher.includes('foo')) |
2409 | + self.assertTrue(name_matcher.includes('barfoo')) |
2410 | + self.assertFalse(name_matcher.includes('bar')) |
2411 | + self.assertFalse(name_matcher.includes('foobar')) |
2412 | + |
2413 | + def test_provided_excludes(self): |
2414 | + name_matcher = loader.NameMatcher( |
2415 | + excludes=loader.matches_for_regexp('^bar.*foo$')) |
2416 | + self.assertTrue(name_matcher.excludes('barfoo')) |
2417 | + self.assertFalse(name_matcher.excludes('foo')) |
2418 | + |
2419 | + |
2420 | +class TestFileLoader(testtools.TestCase): |
2421 | + |
2422 | + def get_test_loader(self): |
2423 | + return loader.TestLoader() |
2424 | + |
2425 | + def test_discover_nothing(self): |
2426 | + tests.set_cwd_to_tmp(self) |
2427 | + with open('foo', 'w') as f: |
2428 | + f.write('bar\n') |
2429 | + file_loader = loader.FileLoader(self.get_test_loader()) |
2430 | + suite = file_loader.discover('.', 'foo') |
2431 | + self.assertIs(None, suite) |
2432 | + |
2433 | + |
2434 | +class TestModuleLoader(tests.ImportingLocalFilesTest): |
2435 | + |
2436 | + def get_test_loader(self): |
2437 | + return loader.TestLoader() |
2438 | + |
2439 | + def test_default_includes(self): |
2440 | + mod_loader = loader.ModuleLoader(self.get_test_loader()) |
2441 | + self.assertTrue(mod_loader.matches('foo.py')) |
2442 | + # But we won't try to import a random file |
2443 | + self.assertFalse(mod_loader.matches('foopy')) |
2444 | + |
2445 | + def test_discover_empty_file(self): |
2446 | + with open('foo.py', 'w') as f: |
2447 | + f.write('') |
2448 | + mod_loader = loader.ModuleLoader(self.get_test_loader()) |
2449 | + suite = mod_loader.discover('.', 'foo.py') |
2450 | + self.assertEqual(0, suite.countTestCases()) |
2451 | + |
2452 | + def test_discover_invalid_file(self): |
2453 | + with open('foo.py', 'w') as f: |
2454 | + f.write("I'm no python code") |
2455 | + mod_loader = loader.ModuleLoader(self.get_test_loader()) |
2456 | + self.assertRaises(SyntaxError, mod_loader.discover, '.', 'foo.py') |
2457 | + |
2458 | + def test_discover_valid_file(self): |
2459 | + with open('foo.py', 'w') as f: |
2460 | + f.write(''' |
2461 | +import unittest |
2462 | + |
2463 | +class Test(unittest.TestCase): |
2464 | + |
2465 | + def test_it(self): |
2466 | + self.assertTrue(True) |
2467 | +''') |
2468 | + mod_loader = loader.ModuleLoader(self.get_test_loader()) |
2469 | + suite = mod_loader.discover('.', 'foo.py') |
2470 | + self.assertEqual(1, suite.countTestCases()) |
2471 | + |
2472 | + |
2473 | +class TestDirLoaderDiscoverPath(tests.ImportingLocalFilesTest): |
2474 | + |
2475 | + def get_test_loader(self): |
2476 | + test_loader = loader.TestLoader() |
2477 | + # We don't use the default PackageLoader for unit testing DirLoader |
2478 | + # behavior. But we still leave ModuleLoader for the file loader. |
2479 | + test_loader.dirLoaderClass = loader.DirLoader |
2480 | + return test_loader |
2481 | + |
2482 | + def test_discover_path_for_file_without_package(self): |
2483 | + tests.write_tree_from_desc('''dir: t |
2484 | +file: t/foo.py |
2485 | +I'm not even python code |
2486 | +''') |
2487 | + # Since 'foo' can't be imported, discover_path should not be invoked, |
2488 | + # ensure we still get some meaningful error message. |
2489 | + dir_loader = loader.DirLoader(self.get_test_loader()) |
2490 | + e = self.assertRaises(ImportError, |
2491 | + dir_loader.discover_path, 't', 'foo.py') |
2492 | + self.assertEqual('No module named t.foo', e.message) |
2493 | + |
2494 | + def test_discover_path_for_valid_file(self): |
2495 | + tests.write_tree_from_desc('''dir: t |
2496 | +file: t/__init__.py |
2497 | +file: t/foo.py |
2498 | +import unittest |
2499 | + |
2500 | +class Test(unittest.TestCase): |
2501 | + |
2502 | + def test_it(self): |
2503 | + self.assertTrue(True) |
2504 | +''') |
2505 | + dir_loader = loader.DirLoader(self.get_test_loader()) |
2506 | + suite = dir_loader.discover_path('t', 'foo.py') |
2507 | + self.assertEqual(1, suite.countTestCases()) |
2508 | + |
2509 | + def test_discover_path_for_dir(self): |
2510 | + tests.write_tree_from_desc('''dir: t |
2511 | +file: t/__init__.py |
2512 | +dir: t/dir |
2513 | +file: t/dir/foo.py |
2514 | +import unittest |
2515 | + |
2516 | +class Test(unittest.TestCase): |
2517 | + |
2518 | + def test_it(self): |
2519 | + self.assertTrue(True) |
2520 | +''') |
2521 | + dir_loader = loader.DirLoader(self.get_test_loader()) |
2522 | + e = self.assertRaises(ImportError, |
2523 | + dir_loader.discover_path, 't', 'dir') |
2524 | + # 't' is a module but 'dir' is not, hence, 'dir.foo' is not either, |
2525 | + # blame python for the approximate message ;-/ |
2526 | + self.assertEqual('No module named dir.foo', e.message) |
2527 | + |
2528 | + def test_discover_path_for_not_matching_symlink(self): |
2529 | + tests.write_tree_from_desc('''dir: t |
2530 | +file: t/foo |
2531 | +tagada |
2532 | +link: t/foo t/bar.py |
2533 | +''') |
2534 | + dir_loader = loader.DirLoader(self.get_test_loader()) |
2535 | + suite = dir_loader.discover_path('t', 'bar.py') |
2536 | + self.assertIs(None, suite) |
2537 | + |
2538 | + def test_discover_path_for_broken_symlink(self): |
2539 | + tests.write_tree_from_desc('''dir: t |
2540 | +file: t/foo |
2541 | +tagada |
2542 | +link: bar t/qux |
2543 | +''') |
2544 | + dir_loader = loader.DirLoader(self.get_test_loader()) |
2545 | + suite = dir_loader.discover_path('t', 'qux') |
2546 | + self.assertIs(None, suite) |
2547 | + |
2548 | + def test_discover_simple_file_in_dir(self): |
2549 | + tests.write_tree_from_desc('''dir: t |
2550 | +file: t/__init__.py |
2551 | +file: t/foo.py |
2552 | +import unittest |
2553 | + |
2554 | +class Test(unittest.TestCase): |
2555 | + |
2556 | + def test_it(self): |
2557 | + self.assertTrue(True) |
2558 | +''') |
2559 | + dir_loader = loader.DirLoader(self.get_test_loader()) |
2560 | + suite = dir_loader.discover('.', 't') |
2561 | + # Despite using DirLoader, python triggers the 't' import so we are |
2562 | + # able to import foo.py and all is well |
2563 | + self.assertEqual(1, suite.countTestCases()) |
2564 | + |
2565 | + |
2566 | +class TestPackageLoader(tests.ImportingLocalFilesTest): |
2567 | + |
2568 | + def get_test_loader(self): |
2569 | + test_loader = loader.TestLoader() |
2570 | + return test_loader |
2571 | + |
2572 | + def test_discover_package_with_invalid_file(self): |
2573 | + tests.write_tree_from_desc('''dir: dir |
2574 | +file: dir/__init__.py |
2575 | +file: dir/foo.py |
2576 | +I'm not even python code |
2577 | +''') |
2578 | + pkg_loader = loader.PackageLoader(self.get_test_loader()) |
2579 | + e = self.assertRaises(SyntaxError, pkg_loader.discover, '.', 'dir') |
2580 | + self.assertEqual('EOL while scanning string literal', e.args[0]) |
2581 | + |
2582 | + def test_discover_simple_file_in_dir(self): |
2583 | + tests.write_tree_from_desc('''dir: t |
2584 | +file: t/__init__.py |
2585 | +file: t/foo.py |
2586 | +import unittest |
2587 | + |
2588 | +class Test(unittest.TestCase): |
2589 | + |
2590 | + def test_it(self): |
2591 | + self.assertTrue(True) |
2592 | +''') |
2593 | + dir_loader = loader.DirLoader(self.get_test_loader()) |
2594 | + suite = dir_loader.discover('.', 't') |
2595 | + self.assertEqual(1, suite.countTestCases()) |
2596 | + |
2597 | + |
2598 | +class TestLoadScript(testtools.TestCase): |
2599 | + |
2600 | + def setUp(self): |
2601 | + super(TestLoadScript, self).setUp() |
2602 | + tests.set_cwd_to_tmp(self) |
2603 | + |
2604 | + def create_script(self, path, content): |
2605 | + with open(path, 'w') as f: |
2606 | + f.write(content) |
2607 | + |
2608 | + def test_load_simple_script(self): |
2609 | + # A simple do nothing script with no imports |
2610 | + self.create_script('foo.py', 'pass') |
2611 | + suite = loader.TestLoader().loadTestsFromScript('.', 'foo.py') |
2612 | + self.assertEqual(1, suite.countTestCases()) |
2613 | + |
2614 | + def test_load_simple_script_with_csv(self): |
2615 | + self.create_script('foo.py', "pass") |
2616 | + with open('foo.csv', 'w') as f: |
2617 | + f.write('''\ |
2618 | +'foo'^'bar' |
2619 | +1^baz |
2620 | +2^qux |
2621 | +''') |
2622 | + suite = loader.TestLoader().loadTestsFromScript('.', 'foo.py') |
2623 | + self.assertEqual(2, suite.countTestCases()) |
2624 | + |
2625 | + def test_load_non_existing_script(self): |
2626 | + suite = loader.TestLoader().loadTestsFromScript('.', 'foo.py') |
2627 | + self.assertEqual(0, suite.countTestCases()) |
2628 | + |
2629 | + |
2630 | +class TestScriptLoader(tests.ImportingLocalFilesTest): |
2631 | + |
2632 | + def get_test_loader(self): |
2633 | + return loader.TestLoader() |
2634 | + |
2635 | + def test_simple_script(self): |
2636 | + tests.write_tree_from_desc('''dir: t |
2637 | +# no t/__init__.py required, we don't need to import the scripts |
2638 | +file: t/foo.py |
2639 | +from sst.actions import * |
2640 | + |
2641 | +raise AssertionError('Loading only, executing fails') |
2642 | +''') |
2643 | + script_loader = loader.ScriptLoader(self.get_test_loader()) |
2644 | + suite = script_loader.discover('t', 'foo.py') |
2645 | + self.assertEqual(1, suite.countTestCases()) |
2646 | + |
2647 | + def test_ignore_privates(self): |
2648 | + tests.write_tree_from_desc('''dir: t |
2649 | +file: t/_private.py |
2650 | +''') |
2651 | + script_loader = loader.ScriptLoader(self.get_test_loader()) |
2652 | + suite = script_loader.discover('t', '_private.py') |
2653 | + self.assertIs(None, suite) |
2654 | + |
2655 | + |
2656 | +class TesScriptDirLoader(tests.ImportingLocalFilesTest): |
2657 | + |
2658 | + def test_shared(self): |
2659 | + tests.write_tree_from_desc('''dir: t |
2660 | +# no t/__init__.py required, we don't need to import the scripts |
2661 | +file: t/foo.py |
2662 | +from sst.actions import * |
2663 | + |
2664 | +raise AssertionError('Loading only, executing fails') |
2665 | +dir: t/shared |
2666 | +file: t/shared/amodule.py |
2667 | +Don't look at me ! |
2668 | +''') |
2669 | + script_dir_loader = loader.ScriptDirLoader(loader.TestLoader()) |
2670 | + suite = script_dir_loader.discover('t', 'shared') |
2671 | + self.assertIs(None, suite) |
2672 | + |
2673 | + def test_regular(self): |
2674 | + tests.write_tree_from_desc('''dir: t |
2675 | +# no t/__init__.py required, we don't need to import the scripts |
2676 | +dir: t/subdir |
2677 | +file: t/subdir/foo.py |
2678 | +raise AssertionError('Loading only, executing fails') |
2679 | +dir: t/shared |
2680 | +file: t/shared/amodule.py |
2681 | +Don't look at me ! |
2682 | +''') |
2683 | + test_loader = loader.TestLoader() |
2684 | + suite = test_loader.discoverTests( |
2685 | + '.', file_loader_class=loader.ScriptLoader, |
2686 | + dir_loader_class=loader.ScriptDirLoader) |
2687 | + self.assertEqual(1, suite.countTestCases()) |
2688 | + |
2689 | + |
2690 | +class TestTestLoader(tests.ImportingLocalFilesTest): |
2691 | + |
2692 | + def test_simple_file_in_a_dir(self): |
2693 | + tests.write_tree_from_desc('''dir: t |
2694 | +file: t/__init__.py |
2695 | +file: t/foo.py |
2696 | +import unittest |
2697 | + |
2698 | +class Test(unittest.TestCase): |
2699 | + |
2700 | + def test_me(self): |
2701 | + self.assertTrue(True) |
2702 | +''') |
2703 | + test_loader = loader.TestLoader() |
2704 | + suite = test_loader.discoverTests('t') |
2705 | + self.assertEqual(1, suite.countTestCases()) |
2706 | + |
2707 | + def test_broken_file_in_a_dir(self): |
2708 | + tests.write_tree_from_desc('''dir: t |
2709 | +file: t/__init__.py |
2710 | +file: t/foo.py |
2711 | +I'm not even python code |
2712 | +''') |
2713 | + test_loader = loader.TestLoader() |
2714 | + e = self.assertRaises(SyntaxError, test_loader.discoverTests, 't') |
2715 | + self.assertEqual('EOL while scanning string literal', e.args[0]) |
2716 | + |
2717 | + def test_scripts_below_regular(self): |
2718 | + tests.write_tree_from_desc('''dir: t |
2719 | +file: t/__init__.py |
2720 | +file: t/foo.py |
2721 | +import unittest |
2722 | + |
2723 | +class Test(unittest.TestCase): |
2724 | + |
2725 | + def test_me(self): |
2726 | + self.assertTrue(True) |
2727 | +dir: t/scripts |
2728 | +file: t/scripts/__init__.py |
2729 | +from sst import loader |
2730 | + |
2731 | +discover = loader.discoverTestScripts |
2732 | +file: t/scripts/script.py |
2733 | +raise AssertionError('Loading only, executing fails') |
2734 | +''') |
2735 | + test_loader = loader.TestLoader() |
2736 | + suite = test_loader.discoverTests('t') |
2737 | + self.assertEqual(2, suite.countTestCases()) |
2738 | + # Check which kind of tests have been discovered or we may miss regular |
2739 | + # test cases seen as scripts. |
2740 | + self.assertEqual(['t.foo.Test.test_me', |
2741 | + 't.scripts.script'], |
2742 | + [t.id() for t in testtools.iterate_tests(suite)]) |
2743 | + |
2744 | + def test_regular_below_scripts(self): |
2745 | + tests.write_tree_from_desc('''dir: t |
2746 | +file: t/__init__.py |
2747 | +dir: t/regular |
2748 | +file: t/regular/__init__.py |
2749 | +from sst import loader |
2750 | +import unittest |
2751 | + |
2752 | +discover = loader.discoverRegularTests |
2753 | + |
2754 | +class Test(unittest.TestCase): |
2755 | + |
2756 | + def test_in_init(self): |
2757 | + self.assertTrue(True) |
2758 | +file: t/regular/foo.py |
2759 | +import unittest |
2760 | + |
2761 | +class Test(unittest.TestCase): |
2762 | + |
2763 | + def test_me(self): |
2764 | + self.assertTrue(True) |
2765 | +file: t/script.py |
2766 | +raise AssertionError('Loading only, executing fails') |
2767 | +''') |
2768 | + test_loader = loader.TestLoader() |
2769 | + suite = test_loader.discoverTests( |
2770 | + 't', |
2771 | + file_loader_class=loader.ScriptLoader, |
2772 | + dir_loader_class=loader.ScriptDirLoader) |
2773 | + # Check which kind of tests have been discovered or we may miss regular |
2774 | + # test cases seen as scripts. |
2775 | + self.assertEqual(['t.regular.Test.test_in_init', |
2776 | + 't.regular.foo.Test.test_me', |
2777 | + 't.script'], |
2778 | + [t.id() for t in testtools.iterate_tests(suite)]) |
2779 | + |
2780 | + def test_regular_and_scripts_mixed(self): |
2781 | + def regular(dir_name, name, suffix=None): |
2782 | + if suffix is None: |
2783 | + suffix = '' |
2784 | + return ''' |
2785 | +file: {dir_name}/{name}{suffix} |
2786 | +from sst import cases |
2787 | + |
2788 | +class Test_{name}(cases.SSTTestCase): |
2789 | + def test_{name}(self): |
2790 | + pass |
2791 | +'''.format(**locals()) |
2792 | + |
2793 | + tests.write_tree_from_desc('''dir: tests |
2794 | +file: tests/__init__.py |
2795 | +from sst import loader |
2796 | + |
2797 | +discover = loader.discoverRegularTests |
2798 | +''') |
2799 | + tests.write_tree_from_desc(regular('tests', 'test_real', '.py')) |
2800 | + tests.write_tree_from_desc(regular('tests', 'test_real1', '.py')) |
2801 | + tests.write_tree_from_desc(regular('tests', 'test_real2', '.py')) |
2802 | + # Leading '_' => ignored |
2803 | + tests.write_tree_from_desc(regular('tests', '_hidden', '.py')) |
2804 | + # Not a python file => ignored |
2805 | + tests.write_tree_from_desc(regular('tests', 'not-python')) |
2806 | + # Some empty files |
2807 | + tests.write_tree_from_desc(''' |
2808 | +file: script1.py |
2809 | +file: script2.py |
2810 | +file: not_a_test |
2811 | +# '.p' is intentional, not a typoed '.py' |
2812 | +file: test_not_a_test.p |
2813 | +_hidden_too.py |
2814 | +''') |
2815 | + test_loader = loader.TestLoader() |
2816 | + suite = test_loader.discoverTests( |
2817 | + '.', |
2818 | + file_loader_class=loader.ScriptLoader, |
2819 | + dir_loader_class=loader.ScriptDirLoader) |
2820 | + self.assertEqual(['script1', |
2821 | + 'script2', |
2822 | + 'tests.test_real.Test_test_real.test_test_real', |
2823 | + 'tests.test_real1.Test_test_real1.test_test_real1', |
2824 | + 'tests.test_real2.Test_test_real2.test_test_real2'], |
2825 | + [t.id() for t in testtools.iterate_tests(suite)]) |
2826 | + |
2827 | + |
2828 | +class TestTestLoaderPattern(tests.ImportingLocalFilesTest): |
2829 | + |
2830 | + def test_default_pattern(self): |
2831 | + tests.write_tree_from_desc('''dir: t |
2832 | +file: t/__init__.py |
2833 | +file: t/foo.py |
2834 | +Don't look at me ! |
2835 | +file: t/test_foo.py |
2836 | +import unittest |
2837 | + |
2838 | +class Test(unittest.TestCase): |
2839 | + |
2840 | + def test_me(self): |
2841 | + self.assertTrue(True) |
2842 | +''') |
2843 | + test_loader = loader.TestLoader() |
2844 | + suite = test_loader.discover('t') |
2845 | + self.assertEqual(1, suite.countTestCases()) |
2846 | + |
2847 | + def test_pattern(self): |
2848 | + tests.write_tree_from_desc('''dir: t |
2849 | +file: t/__init__.py |
2850 | +file: t/foo_foo.py |
2851 | +import unittest |
2852 | + |
2853 | +class Test(unittest.TestCase): |
2854 | + |
2855 | + def test_me(self): |
2856 | + self.assertTrue(True) |
2857 | +file: t/test_foo.py |
2858 | +Don't look at me ! |
2859 | +''') |
2860 | + test_loader = loader.TestLoader() |
2861 | + suite = test_loader.discover('t', pattern='foo*.py') |
2862 | + self.assertEqual(1, suite.countTestCases()) |
2863 | + |
2864 | + |
2865 | +class TestTestLoaderTopLevelDir(testtools.TestCase): |
2866 | + |
2867 | + def setUp(self): |
2868 | + super(TestTestLoaderTopLevelDir, self).setUp() |
2869 | + # We build trees rooted in test_base_dir from which we will import |
2870 | + tests.set_cwd_to_tmp(self) |
2871 | + tests.protect_imports(self) |
2872 | + |
2873 | + def _create_foo_in_tests(self): |
2874 | + tests.write_tree_from_desc('''dir: t |
2875 | +file: t/__init__.py |
2876 | +file: t/foo.py |
2877 | +import unittest |
2878 | + |
2879 | +class Test(unittest.TestCase): |
2880 | + |
2881 | + def test_me(self): |
2882 | + self.assertTrue(True) |
2883 | +''') |
2884 | + |
2885 | + def test_simple_file_in_a_dir(self): |
2886 | + self._create_foo_in_tests() |
2887 | + test_loader = loader.TestLoader() |
2888 | + suite = test_loader.discover('t', '*.py', self.test_base_dir) |
2889 | + self.assertEqual(1, suite.countTestCases()) |
2890 | + |
2891 | + def test_simple_file_in_a_dir_no_sys_path(self): |
2892 | + self._create_foo_in_tests() |
2893 | + test_loader = loader.TestLoader() |
2894 | + e = self.assertRaises(ImportError, |
2895 | + test_loader.discover, 't', '*.py') |
2896 | + self.assertEqual(e.message, 'No module named t') |
2897 | |
2898 | === added file 'src/sst/tests/test_protect_imports.py' |
2899 | --- src/sst/tests/test_protect_imports.py 1970-01-01 00:00:00 +0000 |
2900 | +++ src/sst/tests/test_protect_imports.py 2013-05-14 11:43:29 +0000 |
2901 | @@ -0,0 +1,86 @@ |
2902 | +# |
2903 | +# Copyright (c) 2013 Canonical Ltd. |
2904 | +# |
2905 | +# This file is part of: SST (selenium-simple-test) |
2906 | +# https://launchpad.net/selenium-simple-test |
2907 | +# |
2908 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
2909 | +# you may not use this file except in compliance with the License. |
2910 | +# You may obtain a copy of the License at |
2911 | +# |
2912 | +# http://www.apache.org/licenses/LICENSE-2.0 |
2913 | +# |
2914 | +# Unless required by applicable law or agreed to in writing, software |
2915 | +# distributed under the License is distributed on an "AS IS" BASIS, |
2916 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2917 | +# See the License for the specific language governing permissions and |
2918 | +# limitations under the License. |
2919 | +# |
2920 | + |
2921 | +import sys |
2922 | +import testtools |
2923 | + |
2924 | +from sst import tests |
2925 | + |
2926 | + |
2927 | +class TestProtectImports(testtools.TestCase): |
2928 | + |
2929 | + def setUp(self): |
2930 | + super(TestProtectImports, self).setUp() |
2931 | + tests.protect_imports(self) |
2932 | + |
2933 | + def run_successful_test(self, test): |
2934 | + result = testtools.TestResult() |
2935 | + test.run(result) |
2936 | + self.assertTrue(result.wasSuccessful()) |
2937 | + |
2938 | + def test_add_module(self): |
2939 | + self.assertIs(None, sys.modules.get('foo', None)) |
2940 | + |
2941 | + class Test(testtools.TestCase): |
2942 | + |
2943 | + def test_it(self): |
2944 | + tests.protect_imports(self) |
2945 | + sys.modules['foo'] = 'bar' |
2946 | + |
2947 | + self.run_successful_test(Test('test_it')) |
2948 | + self.assertIs(None, sys.modules.get('foo', None)) |
2949 | + |
2950 | + def test_remove_module(self): |
2951 | + self.assertIs(None, sys.modules.get('I_dont_exist', None)) |
2952 | + sys.modules['I_dont_exist'] = 'bar' |
2953 | + |
2954 | + class Test(testtools.TestCase): |
2955 | + |
2956 | + def test_it(self): |
2957 | + tests.protect_imports(self) |
2958 | + self.assertEqual('bar', sys.modules['I_dont_exist']) |
2959 | + del sys.modules['I_dont_exist'] |
2960 | + self.run_successful_test(Test('test_it')) |
2961 | + self.assertEqual('bar', sys.modules['I_dont_exist']) |
2962 | + |
2963 | + def test_modify_module(self): |
2964 | + self.assertIs(None, sys.modules.get('I_dont_exist', None)) |
2965 | + sys.modules['I_dont_exist'] = 'bar' |
2966 | + |
2967 | + class Test(testtools.TestCase): |
2968 | + |
2969 | + def test_it(self): |
2970 | + tests.protect_imports(self) |
2971 | + self.assertEqual('bar', sys.modules['I_dont_exist']) |
2972 | + sys.modules['I_dont_exist'] = 'qux' |
2973 | + self.run_successful_test(Test('test_it')) |
2974 | + self.assertEqual('bar', sys.modules['I_dont_exist']) |
2975 | + |
2976 | + def test_sys_path_restored(self): |
2977 | + tests.set_cwd_to_tmp(self) |
2978 | + inserted = self.test_base_dir |
2979 | + self.assertFalse(inserted in sys.path) |
2980 | + |
2981 | + class Test(testtools.TestCase): |
2982 | + |
2983 | + def test_it(self): |
2984 | + tests.protect_imports(self) |
2985 | + sys.path.insert(0, inserted) |
2986 | + self.run_successful_test(Test('test_it')) |
2987 | + self.assertFalse(inserted in sys.path) |
2988 | |
2989 | === renamed file 'src/sst/tests/test_results_output.py' => 'src/sst/tests/test_result.py' |
2990 | --- src/sst/tests/test_results_output.py 2013-05-07 13:58:29 +0000 |
2991 | +++ src/sst/tests/test_result.py 2013-05-14 11:43:29 +0000 |
2992 | @@ -18,166 +18,268 @@ |
2993 | # |
2994 | |
2995 | |
2996 | -import os |
2997 | -import unittest |
2998 | from cStringIO import StringIO |
2999 | +import sys |
3000 | |
3001 | import junitxml |
3002 | import testtools |
3003 | |
3004 | from sst import ( |
3005 | + result, |
3006 | tests, |
3007 | - runtests, |
3008 | ) |
3009 | |
3010 | |
3011 | -def _make_pass_test_suite(): |
3012 | - class InnerPassTestCase(unittest.TestCase): |
3013 | - def test_inner_pass(self): |
3014 | - self.assertTrue(True) |
3015 | - suite = unittest.TestSuite() |
3016 | - suite.addTest(InnerPassTestCase('test_inner_pass')) |
3017 | - return suite |
3018 | - |
3019 | - |
3020 | -def _make_fail_test_suite(): |
3021 | - class InnerFailTestCase(unittest.TestCase): |
3022 | - def test_inner_fail(self): |
3023 | +def get_case(kind): |
3024 | + # Define the class in a function so test loading don't try to load it as a |
3025 | + # regular test class. |
3026 | + class Test(testtools.TestCase): |
3027 | + |
3028 | + def test_pass(self): |
3029 | + pass |
3030 | + |
3031 | + def test_fail(self): |
3032 | self.assertTrue(False) |
3033 | - suite = unittest.TestSuite() |
3034 | - suite.addTest(InnerFailTestCase('test_inner_fail')) |
3035 | - return suite |
3036 | - |
3037 | - |
3038 | -def _make_error_test_suite(): |
3039 | - class InnerErrorTestCase(unittest.TestCase): |
3040 | - def test_inner_error(self): |
3041 | - raise |
3042 | - suite = unittest.TestSuite() |
3043 | - suite.addTest(InnerErrorTestCase('test_inner_error')) |
3044 | - return suite |
3045 | - |
3046 | - |
3047 | -def _make_skip_test_suite(): |
3048 | - class InnerSkipTestCase(unittest.TestCase): |
3049 | - @testtools.skip('skip me') |
3050 | - def test_inner_skip(self): |
3051 | - pass |
3052 | - suite = unittest.TestSuite() |
3053 | - suite.addTest(InnerSkipTestCase('test_inner_skip')) |
3054 | - return suite |
3055 | - |
3056 | - |
3057 | -class ConsoleOutputTestCase(testtools.TestCase): |
3058 | - |
3059 | - def setUp(self): |
3060 | - super(ConsoleOutputTestCase, self).setUp() |
3061 | - tests.set_cwd_to_tmp(self) |
3062 | - self.out = StringIO() |
3063 | - self.text_result = runtests.TextTestResult(self.out) |
3064 | - |
3065 | - def assert_output(self, suite, regex): |
3066 | - suite.run(self.text_result) |
3067 | - self.console_output = self.out.getvalue() |
3068 | - self.assertRegexpMatches(self.console_output, regex) |
3069 | - |
3070 | - def test_text_output_pass(self): |
3071 | - suite = _make_pass_test_suite() |
3072 | - regex = ( |
3073 | - r'test_inner_pass ' |
3074 | - r'\(sst.tests.test_results_output.InnerPassTestCase\) ...' |
3075 | - r'\nOK \([0-9]*.[0-9]{3} secs\)\n' |
3076 | - ) |
3077 | - self.assert_output(suite, regex) |
3078 | - |
3079 | - def test_text_output_fail(self): |
3080 | - suite = _make_fail_test_suite() |
3081 | - regex = ( |
3082 | - r'test_inner_fail ' |
3083 | - r'\(sst.tests.test_results_output.InnerFailTestCase\) ...' |
3084 | - r'\nFAIL\n' |
3085 | - ) |
3086 | - self.assert_output(suite, regex) |
3087 | - |
3088 | - def test_text_output_error(self): |
3089 | - suite = _make_error_test_suite() |
3090 | - regex = ( |
3091 | - r'test_inner_error ' |
3092 | - r'\(sst.tests.test_results_output.InnerErrorTestCase\) ...' |
3093 | - r'\nERROR\n' |
3094 | - ) |
3095 | - self.assert_output(suite, regex) |
3096 | - |
3097 | - def test_text_output_skip(self): |
3098 | - suite = _make_skip_test_suite() |
3099 | - regex = ( |
3100 | - r'test_inner_skip ' |
3101 | - r'\(sst.tests.test_results_output.InnerSkipTestCase\) ...' |
3102 | - r'\nSkipped \'skip me\'\n' |
3103 | - ) |
3104 | - self.assert_output(suite, regex) |
3105 | - |
3106 | - |
3107 | -class XmlOutputTestCase(testtools.TestCase): |
3108 | - |
3109 | - def setUp(self): |
3110 | - super(XmlOutputTestCase, self).setUp() |
3111 | - tests.set_cwd_to_tmp(self) |
3112 | - self.results_file = 'results.xml' |
3113 | - self.xml_stream = file(self.results_file, 'wb') |
3114 | - self.xml_result = junitxml.JUnitXmlResult(self.xml_stream) |
3115 | - self.addCleanup(os.remove, 'results.xml') |
3116 | - |
3117 | - def assert_output(self, suite, regex): |
3118 | - suite.run(self.xml_result) |
3119 | - # need this to signal junitxml or no results get written |
3120 | - self.xml_result.stopTestRun() |
3121 | - self.xml_stream.close() |
3122 | - self.assertIn(self.results_file, os.listdir(self.test_base_dir)) |
3123 | - with open(self.results_file) as f: |
3124 | - content = f.read() |
3125 | - self.assertRegexpMatches(content, regex) |
3126 | - |
3127 | - def test_xml_output_pass(self): |
3128 | - suite = _make_pass_test_suite() |
3129 | - regex = ( |
3130 | - r'<testsuite errors="0" failures="0" name="" tests="1" ' |
3131 | - r'time=".*">\n' |
3132 | - r'<testcase ' |
3133 | - r'classname="sst.tests.test_results_output.InnerPassTestCase" ' |
3134 | - r'name="test_inner_pass" time="[0-9]*.[0-9]{3}"' |
3135 | - ) |
3136 | - self.assert_output(suite, regex) |
3137 | - |
3138 | - def test_xml_output_fail(self): |
3139 | - suite = _make_fail_test_suite() |
3140 | - regex = ( |
3141 | - r'<testsuite errors="0" failures="1" name="" tests="1" ' |
3142 | - r'time=".*">\n' |
3143 | - r'<testcase ' |
3144 | - r'classname="sst.tests.test_results_output.InnerFailTestCase" ' |
3145 | - r'name="test_inner_fail" time="[0-9]*.[0-9]{3}"' |
3146 | - ) |
3147 | - self.assert_output(suite, regex) |
3148 | - |
3149 | - def test_xml_output_error(self): |
3150 | - suite = _make_error_test_suite() |
3151 | - regex = ( |
3152 | - r'<testsuite errors="1" failures="0" name="" tests="1" ' |
3153 | - r'time=".*">\n' |
3154 | - r'<testcase ' |
3155 | - r'classname="sst.tests.test_results_output.InnerErrorTestCase" ' |
3156 | - r'name="test_inner_error" time="[0-9]*.[0-9]{3}"' |
3157 | - ) |
3158 | - self.assert_output(suite, regex) |
3159 | - |
3160 | - def test_xml_output_skip(self): |
3161 | - suite = _make_skip_test_suite() |
3162 | - regex = ( |
3163 | - r'<testsuite errors="0" failures="0" name="" tests="1" ' |
3164 | - r'time=".*">\n<testcase classname="sst.tests.' |
3165 | - r'test_results_output.InnerSkipTestCase" ' |
3166 | - r'name="test_inner_skip" time="[0-9]*.[0-9]{3}"' |
3167 | - r'>\n<skipped>skip me</skipped>\n</testcase>\n</testsuite>' |
3168 | - ) |
3169 | - self.assert_output(suite, regex) |
3170 | + |
3171 | + def test_error(self): |
3172 | + raise SyntaxError |
3173 | + |
3174 | + def test_skip(self): |
3175 | + self.skipTest('') |
3176 | + |
3177 | + def test_skip_reason(self): |
3178 | + self.skipTest('Because') |
3179 | + |
3180 | + def test_expected_failure(self): |
3181 | + # We expect the test to fail and it does |
3182 | + self.expectFailure("1 should be 0", self.assertEqual, 1, 0) |
3183 | + |
3184 | + def test_unexpected_success(self): |
3185 | + # We expect the test to fail but it doesn't |
3186 | + self.expectFailure("1 is not 1", self.assertEqual, 1, 1) |
3187 | + |
3188 | + test_method = 'test_%s' % (kind,) |
3189 | + return Test(test_method) |
3190 | + |
3191 | + |
3192 | +class TestConsoleOutput(testtools.TestCase): |
3193 | + |
3194 | + def setUp(self): |
3195 | + super(TestConsoleOutput, self).setUp() |
3196 | + tests.set_cwd_to_tmp(self) |
3197 | + |
3198 | + def assertOutput(self, expected, kind): |
3199 | + # We don't care about timing here so we always return 0 which |
3200 | + # simplifies matching the expected result |
3201 | + test = get_case(kind) |
3202 | + out = StringIO() |
3203 | + res = result.TextTestResult(out, timer=lambda: 0.0) |
3204 | + test.run(res) |
3205 | + self.assertEquals(expected, res.stream.getvalue()) |
3206 | + |
3207 | + def test_pass_output(self): |
3208 | + self.assertOutput('.', 'pass') |
3209 | + |
3210 | + def test_fail_output(self): |
3211 | + self.assertOutput('F', 'fail') |
3212 | + |
3213 | + def test_error_output(self): |
3214 | + self.assertOutput('E', 'error') |
3215 | + |
3216 | + def test_skip_output(self): |
3217 | + self.assertOutput('s', 'skip') |
3218 | + |
3219 | + def test_skip_reason_output(self): |
3220 | + self.assertOutput('s', 'skip_reason') |
3221 | + |
3222 | + def test_expected_failure_output(self): |
3223 | + self.assertOutput('x', 'expected_failure') |
3224 | + |
3225 | + def test_unexpected_success_output(self): |
3226 | + self.assertOutput('u', 'unexpected_success') |
3227 | + |
3228 | + |
3229 | +class TestVerboseConsoleOutput(testtools.TestCase): |
3230 | + |
3231 | + def setUp(self): |
3232 | + super(TestVerboseConsoleOutput, self).setUp() |
3233 | + tests.set_cwd_to_tmp(self) |
3234 | + |
3235 | + def assertOutput(self, expected, kind): |
3236 | + # We don't care about timing here so we always return 0 which |
3237 | + # simplifies matching the expected result |
3238 | + test = get_case(kind) |
3239 | + out = StringIO() |
3240 | + res = result.TextTestResult(out, verbosity=2, timer=lambda: 0.0) |
3241 | + test.run(res) |
3242 | + self.assertEquals(expected, res.stream.getvalue()) |
3243 | + |
3244 | + def test_pass_output(self): |
3245 | + self.assertOutput('''\ |
3246 | +test_pass (sst.tests.test_result.Test) ... OK (0.000 secs) |
3247 | +''', |
3248 | + 'pass') |
3249 | + |
3250 | + def test_fail_output(self): |
3251 | + self.assertOutput('''\ |
3252 | +test_fail (sst.tests.test_result.Test) ... FAIL (0.000 secs) |
3253 | +''', |
3254 | + 'fail') |
3255 | + |
3256 | + def test_error_output(self): |
3257 | + self.assertOutput('''\ |
3258 | +test_error (sst.tests.test_result.Test) ... ERROR (0.000 secs) |
3259 | +''', |
3260 | + 'error') |
3261 | + |
3262 | + def test_skip_output(self): |
3263 | + self.assertOutput('''\ |
3264 | +test_skip (sst.tests.test_result.Test) ... SKIP (0.000 secs) |
3265 | +''', |
3266 | + 'skip') |
3267 | + |
3268 | + def test_skip_reason_output(self): |
3269 | + self.assertOutput('''\ |
3270 | +test_skip_reason (sst.tests.test_result.Test) ... SKIP Because (0.000 secs) |
3271 | +''', |
3272 | + 'skip_reason') |
3273 | + |
3274 | + def test_expected_failure_output(self): |
3275 | + self.assertOutput('''\ |
3276 | +test_expected_failure (sst.tests.test_result.Test) ... XFAIL (0.000 secs) |
3277 | +''', |
3278 | + 'expected_failure') |
3279 | + |
3280 | + def test_unexpected_success_output(self): |
3281 | + self.assertOutput('''\ |
3282 | +test_unexpected_success (sst.tests.test_result.Test) ... NOTOK (0.000 secs) |
3283 | +''', |
3284 | + 'unexpected_success') |
3285 | + |
3286 | + |
3287 | +class TestXmlOutput(testtools.TestCase): |
3288 | + |
3289 | + def setUp(self): |
3290 | + super(TestXmlOutput, self).setUp() |
3291 | + tests.set_cwd_to_tmp(self) |
3292 | + |
3293 | + def assertOutput(self, template, kind, kwargs=None): |
3294 | + """Assert the expected output from a run for a given test. |
3295 | + |
3296 | + :param template: A string where common strings have been replaced by a |
3297 | + keyword so we don't run into pep8 warnings for long lines. |
3298 | + |
3299 | + :param kind: A string used to select the kind of test. |
3300 | + |
3301 | + :param kwargs: A dict with more keywords for the template. This allows |
3302 | + some tests to add more keywords when they are test specific. |
3303 | + """ |
3304 | + if kwargs is None: |
3305 | + kwargs = dict() |
3306 | + out = StringIO() |
3307 | + res = junitxml.JUnitXmlResult(out) |
3308 | + # We don't care about timing here so we always return 0 which |
3309 | + # simplifies matching the expected result |
3310 | + res._now = lambda: 0.0 |
3311 | + res._duration = lambda f: 0.0 |
3312 | + test = get_case(kind) |
3313 | + test.run(res) |
3314 | + # due to the nature of JUnit XML output, nothing will be written to |
3315 | + # the stream until stopTestRun() is called. |
3316 | + res.stopTestRun() |
3317 | + # To allow easier reading for template, we format some known values |
3318 | + kwargs.update(dict(classname='%s.%s' % (test.__class__.__module__, |
3319 | + test.__class__.__name__), |
3320 | + name=test._testMethodName)) |
3321 | + expected = template.format(**kwargs) |
3322 | + self.assertEquals(expected, res._stream.getvalue()) |
3323 | + |
3324 | + def test_pass_output(self): |
3325 | + self.assertOutput('''\ |
3326 | +<testsuite errors="0" failures="0" name="" tests="1" time="0.000"> |
3327 | +<testcase classname="{classname}" name="{name}" time="0.000"/> |
3328 | +</testsuite> |
3329 | +''', |
3330 | + 'pass') |
3331 | + |
3332 | + def test_fail_output(self): |
3333 | + # Getting the file name right is tricky, depending on whether the |
3334 | + # module was just recompiled or not __file__ can be either .py or .pyc |
3335 | + # but when it appears in an exception, the .py is always used. |
3336 | + filename = __file__.replace('.pyc', '.py').replace('.pyo', '.py') |
3337 | + more = dict(exc_type='testtools.testresult.real._StringException', |
3338 | + filename=filename) |
3339 | + self.assertOutput('''\ |
3340 | +<testsuite errors="0" failures="1" name="" tests="1" time="0.000"> |
3341 | +<testcase classname="{classname}" name="{name}" time="0.000"> |
3342 | +<failure type="{exc_type}">_StringException: Traceback (most recent call last): |
3343 | + File "{filename}", line 42, in {name} |
3344 | + self.assertTrue(False) |
3345 | +AssertionError: False is not true |
3346 | + |
3347 | +</failure> |
3348 | +</testcase> |
3349 | +</testsuite> |
3350 | +''', |
3351 | + 'fail', |
3352 | + more) |
3353 | + |
3354 | + def test_error_output(self): |
3355 | + # Getting the file name right is tricky, depending on whether the |
3356 | + # module was just recompiled or not __file__ can be either .py or .pyc |
3357 | + # but when it appears in an exception, the .py is always used. |
3358 | + filename = __file__.replace('.pyc', '.py').replace('.pyo', '.py') |
3359 | + more = dict(exc_type='testtools.testresult.real._StringException', |
3360 | + filename=filename) |
3361 | + self.assertOutput('''\ |
3362 | +<testsuite errors="1" failures="0" name="" tests="1" time="0.000"> |
3363 | +<testcase classname="{classname}" name="{name}" time="0.000"> |
3364 | +<error type="{exc_type}">_StringException: Traceback (most recent call last): |
3365 | + File "{filename}", line 45, in {name} |
3366 | + raise SyntaxError |
3367 | +SyntaxError: None |
3368 | + |
3369 | +</error> |
3370 | +</testcase> |
3371 | +</testsuite> |
3372 | +''', |
3373 | + 'error', |
3374 | + more) |
3375 | + |
3376 | + def test_skip_output(self): |
3377 | + self.assertOutput('''\ |
3378 | +<testsuite errors="0" failures="0" name="" tests="1" time="0.000"> |
3379 | +<testcase classname="{classname}" name="{name}" time="0.000"> |
3380 | +<skipped></skipped> |
3381 | +</testcase> |
3382 | +</testsuite> |
3383 | +''', |
3384 | + 'skip') |
3385 | + |
3386 | + def test_skip_reason_output(self): |
3387 | + self.assertOutput('''\ |
3388 | +<testsuite errors="0" failures="0" name="" tests="1" time="0.000"> |
3389 | +<testcase classname="{classname}" name="{name}" time="0.000"> |
3390 | +<skipped>Because</skipped> |
3391 | +</testcase> |
3392 | +</testsuite> |
3393 | +''', |
3394 | + 'skip_reason') |
3395 | + |
3396 | + def test_expected_failure_output(self): |
3397 | + self.assertOutput('''\ |
3398 | +<testsuite errors="0" failures="0" name="" tests="1" time="0.000"> |
3399 | +<testcase classname="{classname}" name="{name}" time="0.000"/> |
3400 | +</testsuite> |
3401 | +''', |
3402 | + 'expected_failure') |
3403 | + |
3404 | + def test_unexpected_success_output(self): |
3405 | + self.assertOutput('''\ |
3406 | +<testsuite errors="0" failures="1" name="" tests="1" time="0.000"> |
3407 | +<testcase classname="{classname}" name="{name}" time="0.000"> |
3408 | +<failure type="unittest.case._UnexpectedSuccess"/> |
3409 | +</testcase> |
3410 | +</testsuite> |
3411 | +''', |
3412 | + 'unexpected_success') |
3413 | |
3414 | === added file 'src/sst/tests/test_runtests.py' |
3415 | --- src/sst/tests/test_runtests.py 1970-01-01 00:00:00 +0000 |
3416 | +++ src/sst/tests/test_runtests.py 2013-05-14 11:43:29 +0000 |
3417 | @@ -0,0 +1,92 @@ |
3418 | +# |
3419 | +# Copyright (c) 2013 Canonical Ltd. |
3420 | +# |
3421 | +# This file is part of: SST (selenium-simple-test) |
3422 | +# https://launchpad.net/selenium-simple-test |
3423 | +# |
3424 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
3425 | +# you may not use this file except in compliance with the License. |
3426 | +# You may obtain a copy of the License at |
3427 | +# |
3428 | +# http://www.apache.org/licenses/LICENSE-2.0 |
3429 | +# |
3430 | +# Unless required by applicable law or agreed to in writing, software |
3431 | +# distributed under the License is distributed on an "AS IS" BASIS, |
3432 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
3433 | +# See the License for the specific language governing permissions and |
3434 | +# limitations under the License. |
3435 | +# |
3436 | + |
3437 | +from cStringIO import StringIO |
3438 | +import sys |
3439 | + |
3440 | +from sst import ( |
3441 | + runtests, |
3442 | + tests, |
3443 | +) |
3444 | + |
3445 | + |
3446 | +class TestRunTestsFilteringByTestId(tests.ImportingLocalFilesTest): |
3447 | + |
3448 | + def setUp(self): |
3449 | + super(TestRunTestsFilteringByTestId, self).setUp() |
3450 | + tests.write_tree_from_desc('''dir: t |
3451 | +file: t/__init__.py |
3452 | +file: t/test_foo.py |
3453 | +import unittest |
3454 | + |
3455 | +class Test(unittest.TestCase): |
3456 | + |
3457 | + def test_me(self): |
3458 | + self.assertTrue(True) |
3459 | +file: t/bar.py |
3460 | +Don't look at me ! |
3461 | +file: t/too.py |
3462 | +Don't look at me ! |
3463 | +''') |
3464 | + |
3465 | + def run_tests(self, *args, **kwargs): |
3466 | + # FIXME: runtests use print, it should accept a stream instead. We also |
3467 | + # should be able to better focus the test filtering but that requires |
3468 | + # refactoring runtests. -- vila 2013-05-07 |
3469 | + self.out = StringIO() |
3470 | + self.patch(sys, 'stdout', self.out) |
3471 | + runtests.runtests(*args, test_dir='t', collect_only=True, **kwargs) |
3472 | + lines = self.out.getvalue().splitlines() |
3473 | + self.assertEqual('', lines[0]) |
3474 | + # We don't care about the number of tests, that will be checked later |
3475 | + # by the caller |
3476 | + self.assertTrue(lines[1].endswith('test cases loaded')) |
3477 | + self.assertEqual('', lines[2]) |
3478 | + self.assertEqual('-' * 62, lines[3]) |
3479 | + self.assertEqual('Collect-Only Enabled, Not Running Tests...', |
3480 | + lines[4]) |
3481 | + self.assertEqual('', lines[5]) |
3482 | + self.assertEqual('Tests Collected:', lines[6]) |
3483 | + self.assertEqual('----------------', lines[7]) |
3484 | + return lines[8:] |
3485 | + |
3486 | + def test_all(self): |
3487 | + self.assertEqual(['t.bar', 't.test_foo', 't.too'], |
3488 | + self.run_tests(None)) |
3489 | + |
3490 | + def test_single_include(self): |
3491 | + self.assertEqual(['t.bar'], |
3492 | + self.run_tests(None, includes=['t.b'])) |
3493 | + |
3494 | + def test_multiple_includes(self): |
3495 | + self.assertEqual(['t.bar', 't.too'], |
3496 | + self.run_tests(None, includes=['t.b', 't.to'])) |
3497 | + |
3498 | + def test_single_exclude(self): |
3499 | + self.assertEqual(['t.bar'], |
3500 | + self.run_tests(None, excludes=['t.t'])) |
3501 | + |
3502 | + def test_multiple_excludes(self): |
3503 | + self.assertEqual(['t.test_foo'], |
3504 | + self.run_tests(None, excludes=['t.to', 't.b'])) |
3505 | + |
3506 | + def test_mixed(self): |
3507 | + self.assertEqual(['t.test_foo'], |
3508 | + self.run_tests(None, includes=['t.t'], |
3509 | + excludes=['t.to'])) |
3510 | |
3511 | === removed file 'src/sst/tests/test_runtests_find_cases.py' |
3512 | --- src/sst/tests/test_runtests_find_cases.py 2013-04-23 08:42:16 +0000 |
3513 | +++ src/sst/tests/test_runtests_find_cases.py 1970-01-01 00:00:00 +0000 |
3514 | @@ -1,124 +0,0 @@ |
3515 | -# |
3516 | -# Copyright (c) 2013 Canonical Ltd. |
3517 | -# |
3518 | -# This file is part of: SST (selenium-simple-test) |
3519 | -# https://launchpad.net/selenium-simple-test |
3520 | -# |
3521 | -# Licensed under the Apache License, Version 2.0 (the "License"); |
3522 | -# you may not use this file except in compliance with the License. |
3523 | -# You may obtain a copy of the License at |
3524 | -# |
3525 | -# http://www.apache.org/licenses/LICENSE-2.0 |
3526 | -# |
3527 | -# Unless required by applicable law or agreed to in writing, software |
3528 | -# distributed under the License is distributed on an "AS IS" BASIS, |
3529 | -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
3530 | -# See the License for the specific language governing permissions and |
3531 | -# limitations under the License. |
3532 | -# |
3533 | - |
3534 | - |
3535 | -import os |
3536 | - |
3537 | -import testtools |
3538 | - |
3539 | -from sst import tests |
3540 | -from sst import runtests |
3541 | - |
3542 | - |
3543 | -def _make_empty_files(dir): |
3544 | - file_names = ( |
3545 | - 'test_a_real_test.py', |
3546 | - 'test_a_real_test2.py', |
3547 | - 'script.py', |
3548 | - 'not_a_test', |
3549 | - 'test_not_a_test.p', |
3550 | - '_hidden.py', |
3551 | - ) |
3552 | - os.mkdir(dir) |
3553 | - for fn in file_names: |
3554 | - with open(os.path.join(dir, fn), 'w'): |
3555 | - pass |
3556 | - |
3557 | - |
3558 | -class TestFindCases(testtools.TestCase): |
3559 | - |
3560 | - def setUp(self): |
3561 | - super(TestFindCases, self).setUp() |
3562 | - tests.set_cwd_to_tmp(self) |
3563 | - self.cases_dir = os.path.join(self.test_base_dir, 'cases') |
3564 | - |
3565 | - def test_runtests_find_cases_multi_name(self): |
3566 | - _make_empty_files(self.cases_dir) |
3567 | - args = ( |
3568 | - 'test_a_real_test', |
3569 | - 'script', |
3570 | - ) |
3571 | - matches = { |
3572 | - 'test_a_real_test.py', |
3573 | - 'script.py', |
3574 | - } |
3575 | - found = runtests.find_cases(args, self.cases_dir) |
3576 | - self.assertSetEqual(matches, found) |
3577 | - |
3578 | - def test_runtests_find_cases_single_name(self): |
3579 | - _make_empty_files(self.cases_dir) |
3580 | - args = ( |
3581 | - 'test_a_real_test', |
3582 | - ) |
3583 | - matches = { |
3584 | - 'test_a_real_test.py', |
3585 | - } |
3586 | - found = runtests.find_cases(args, self.cases_dir) |
3587 | - self.assertSetEqual(matches, found) |
3588 | - |
3589 | - def test_runtests_find_cases_glob(self): |
3590 | - _make_empty_files(self.cases_dir) |
3591 | - matches = { |
3592 | - 'test_a_real_test.py', |
3593 | - 'test_a_real_test2.py', |
3594 | - } |
3595 | - |
3596 | - args = ( |
3597 | - 'test_a_real_test*', |
3598 | - ) |
3599 | - found = runtests.find_cases(args, self.cases_dir) |
3600 | - self.assertSetEqual(matches, found) |
3601 | - |
3602 | - args = ( |
3603 | - 'test_*_test*', |
3604 | - ) |
3605 | - found = runtests.find_cases(args, self.cases_dir) |
3606 | - self.assertSetEqual(matches, found) |
3607 | - |
3608 | - def test_runtests_find_cases_glob_singlechar(self): |
3609 | - _make_empty_files(self.cases_dir) |
3610 | - args = ( |
3611 | - 'test_a_real_test?', |
3612 | - ) |
3613 | - matches = { |
3614 | - 'test_a_real_test2.py', |
3615 | - } |
3616 | - |
3617 | - found = runtests.find_cases(args, self.cases_dir) |
3618 | - self.assertSetEqual(matches, found) |
3619 | - |
3620 | - def test_runtests_find_cases_glob_and_name(self): |
3621 | - _make_empty_files(self.cases_dir) |
3622 | - args = ( |
3623 | - 'test_*', |
3624 | - 'script', |
3625 | - ) |
3626 | - matches = { |
3627 | - 'test_a_real_test.py', |
3628 | - 'test_a_real_test2.py', |
3629 | - 'script.py', |
3630 | - } |
3631 | - found = runtests.find_cases(args, self.cases_dir) |
3632 | - self.assertSetEqual(matches, found) |
3633 | - |
3634 | - def test_runtests_find_cases_none_found(self): |
3635 | - _make_empty_files(self.cases_dir) |
3636 | - args = ('xNOMATCHx',) |
3637 | - found = runtests.find_cases(args, self.cases_dir) |
3638 | - self.assertEqual(len(found), 0) |
3639 | |
3640 | === removed file 'src/sst/tests/test_runtests_get_suites.py' |
3641 | --- src/sst/tests/test_runtests_get_suites.py 2013-04-18 18:29:29 +0000 |
3642 | +++ src/sst/tests/test_runtests_get_suites.py 1970-01-01 00:00:00 +0000 |
3643 | @@ -1,116 +0,0 @@ |
3644 | -# |
3645 | -# Copyright (c) 2013 Canonical Ltd. |
3646 | -# |
3647 | -# This file is part of: SST (selenium-simple-test) |
3648 | -# https://launchpad.net/selenium-simple-test |
3649 | -# |
3650 | -# Licensed under the Apache License, Version 2.0 (the "License"); |
3651 | -# you may not use this file except in compliance with the License. |
3652 | -# You may obtain a copy of the License at |
3653 | -# |
3654 | -# http://www.apache.org/licenses/LICENSE-2.0 |
3655 | -# |
3656 | -# Unless required by applicable law or agreed to in writing, software |
3657 | -# distributed under the License is distributed on an "AS IS" BASIS, |
3658 | -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
3659 | -# See the License for the specific language governing permissions and |
3660 | -# limitations under the License. |
3661 | -# |
3662 | - |
3663 | - |
3664 | -import os |
3665 | - |
3666 | -import testtools |
3667 | -import unittest |
3668 | - |
3669 | -from sst import tests |
3670 | -from sst import runtests |
3671 | - |
3672 | - |
3673 | -def _make_test_files(dir): |
3674 | - |
3675 | - os.mkdir(dir) |
3676 | - |
3677 | - # generate files with TestCase classes |
3678 | - testclass_file_names = ( |
3679 | - 'test_a_real_test.py', |
3680 | - 'test_a_real_test1.py', |
3681 | - 'test_a_real_test2.py', |
3682 | - '_hidden_class.py', |
3683 | - 'class_hide_case', |
3684 | - 'class_hiding_case.py', |
3685 | - ) |
3686 | - for fn in testclass_file_names: |
3687 | - with open(os.path.join(dir, fn), 'w') as f: |
3688 | - f.write('from sst import runtests\n') |
3689 | - f.write('class Test_%s(runtests.SSTTestCase):\n' % fn[:-3]) |
3690 | - f.write(' def test_%s(self): pass\n' % fn[:-3]) |
3691 | - |
3692 | - # generate empty files |
3693 | - file_names = ( |
3694 | - '__init__.py', |
3695 | - 'script1.py', |
3696 | - 'script2.py', |
3697 | - 'not_a_test', |
3698 | - 'test_not_a_test.p', |
3699 | - '_hidden2.py' |
3700 | - ) |
3701 | - for fn in file_names: |
3702 | - with open(os.path.join(dir, fn), 'w') as f: |
3703 | - pass |
3704 | - |
3705 | - |
3706 | -class TestGetSuites(testtools.TestCase): |
3707 | - |
3708 | - def setUp(self): |
3709 | - super(TestGetSuites, self).setUp() |
3710 | - tests.set_cwd_to_tmp(self) |
3711 | - self.cases_dir = os.path.join(self.test_base_dir, 'cases') |
3712 | - |
3713 | - def test_runtests_get_suites(self): |
3714 | - _make_test_files(self.cases_dir) |
3715 | - |
3716 | - test_names = ('*', ) |
3717 | - test_dir = self.cases_dir |
3718 | - shared_dir = '.' |
3719 | - collect_only = False |
3720 | - screenshots_on = False |
3721 | - failfast = False, |
3722 | - debug = False |
3723 | - extended = False |
3724 | - |
3725 | - found = runtests.get_suites( |
3726 | - test_names, test_dir, shared_dir, collect_only, |
3727 | - runtests.FirefoxFactory(), |
3728 | - screenshots_on, failfast, debug, extended |
3729 | - ) |
3730 | - suite = found[0]._tests |
3731 | - |
3732 | - # assert we loaded correct number of cases |
3733 | - self.assertEquals(len(suite), 6) |
3734 | - |
3735 | - expected_scripted_tests = ( |
3736 | - 'test_script1', |
3737 | - 'test_script2', |
3738 | - 'test_class_hiding_case', |
3739 | - ) |
3740 | - expected_testcase_tests = ( |
3741 | - 'test_test_a_real_test', |
3742 | - 'test_test_a_real_test1', |
3743 | - 'test_test_a_real_test2', |
3744 | - ) |
3745 | - |
3746 | - for test in suite: |
3747 | - if issubclass(test.__class__, runtests.SSTTestCase): |
3748 | - self.assertIsInstance(test, runtests.SSTTestCase) |
3749 | - name = test.id().split('.')[-1] |
3750 | - self.assertIn(name, expected_scripted_tests) |
3751 | - elif issubclass(test.__class__, unittest.suite.TestSuite): |
3752 | - self.assertIsInstance(test, unittest.suite.TestSuite) |
3753 | - for test_class in test._tests: |
3754 | - for case in test_class._tests: |
3755 | - for t in case._tests: |
3756 | - name = t.id().split('.')[-1] |
3757 | - self.assertIn(name, expected_testcase_tests) |
3758 | - else: |
3759 | - raise Exception('Can not identify test') |
3760 | |
3761 | === modified file 'src/sst/tests/test_sst_run.py' |
3762 | --- src/sst/tests/test_sst_run.py 2013-04-23 11:05:53 +0000 |
3763 | +++ src/sst/tests/test_sst_run.py 2013-05-14 11:43:29 +0000 |
3764 | @@ -24,8 +24,8 @@ |
3765 | import testtools |
3766 | |
3767 | from sst import ( |
3768 | + cases, |
3769 | config, |
3770 | - runtests, |
3771 | tests, |
3772 | ) |
3773 | |
3774 | @@ -34,14 +34,14 @@ |
3775 | |
3776 | def setUp(self): |
3777 | super(TestSSTScriptTestCase, self).setUp() |
3778 | - self.test = runtests.SSTScriptTestCase('foo') |
3779 | + self.test = cases.SSTScriptTestCase('dir', 'foo.py') |
3780 | |
3781 | def test_id(self): |
3782 | """The test id mentions the python class path and the test name.""" |
3783 | # FIXME: This is a minimal test to cover http://pad.lv/1087606, it |
3784 | # would be better to check a results.xml file but we don't have the |
3785 | # test infrastructure for that (yet) -- vila 2012-12-07 |
3786 | - self.assertEqual('sst.runtests.SSTScriptTestCase.foo', self.test.id()) |
3787 | + self.assertEqual('dir.foo', self.test.id()) |
3788 | |
3789 | |
3790 | class TestSSTTestCase(testtools.TestCase): |
3791 | @@ -68,7 +68,6 @@ |
3792 | self.assertFalse(test.screenshots_on) |
3793 | self.assertEqual(test.wait_poll, 0.1) |
3794 | self.assertEqual(test.wait_timeout, 10) |
3795 | - self.assertIsNone(test.shortDescription()) |
3796 | self.assertEqual(test.id(), 'sst.tests.SSTBrowserLessTestCase.run') |
3797 | |
3798 | def test_config(self): |
3799 | @@ -77,5 +76,4 @@ |
3800 | self.assertEqual(config.cache, {}) |
3801 | self.assertEqual(config.flags, []) |
3802 | self.assertFalse(config.javascript_disabled) |
3803 | - self.assertEqual(os.path.split(config.results_directory)[-1], |
3804 | - 'results') |
3805 | + self.assertEqual(os.path.basename(config.results_directory), 'results') |
3806 | |
3807 | === modified file 'src/sst/tests/test_sst_script_test_case.py' |
3808 | --- src/sst/tests/test_sst_script_test_case.py 2013-04-11 14:59:48 +0000 |
3809 | +++ src/sst/tests/test_sst_script_test_case.py 2013-05-14 11:43:29 +0000 |
3810 | @@ -27,17 +27,19 @@ |
3811 | from testtools import matchers |
3812 | |
3813 | from sst import ( |
3814 | - runtests, |
3815 | + cases, |
3816 | tests, |
3817 | ) |
3818 | |
3819 | |
3820 | -class SSTStringTestCase(runtests.SSTScriptTestCase): |
3821 | +class SSTStringTestCase(cases.SSTScriptTestCase): |
3822 | |
3823 | - script_name = 'test_foo' |
3824 | - script_code = 'pass' |
3825 | xserver_headless = True |
3826 | |
3827 | + def __init__(self, code='pass'): |
3828 | + super(SSTStringTestCase, self).__init__('ignored_dir', 'ignored_name') |
3829 | + self.script_code = code |
3830 | + |
3831 | def setUp(self): |
3832 | # We don't need to compile the script because we have already define |
3833 | # the code to execute. |
3834 | |
3835 | === added file 'src/sst/tests/test_write_tree.py' |
3836 | --- src/sst/tests/test_write_tree.py 1970-01-01 00:00:00 +0000 |
3837 | +++ src/sst/tests/test_write_tree.py 2013-05-14 11:43:29 +0000 |
3838 | @@ -0,0 +1,94 @@ |
3839 | +# |
3840 | +# Copyright (c) 2013 Canonical Ltd. |
3841 | +# |
3842 | +# This file is part of: SST (selenium-simple-test) |
3843 | +# https://launchpad.net/selenium-simple-test |
3844 | +# |
3845 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
3846 | +# you may not use this file except in compliance with the License. |
3847 | +# You may obtain a copy of the License at |
3848 | +# |
3849 | +# http://www.apache.org/licenses/LICENSE-2.0 |
3850 | +# |
3851 | +# Unless required by applicable law or agreed to in writing, software |
3852 | +# distributed under the License is distributed on an "AS IS" BASIS, |
3853 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
3854 | +# See the License for the specific language governing permissions and |
3855 | +# limitations under the License. |
3856 | + |
3857 | +import os |
3858 | +import testtools |
3859 | + |
3860 | +from sst import tests |
3861 | + |
3862 | + |
3863 | +class TestWriteTree(testtools.TestCase): |
3864 | + |
3865 | + def setUp(self): |
3866 | + super(TestWriteTree, self).setUp() |
3867 | + tests.set_cwd_to_tmp(self) |
3868 | + |
3869 | + def test_empty_description(self): |
3870 | + self.assertEqual([], os.listdir('.')) |
3871 | + tests.write_tree_from_desc('') |
3872 | + self.assertEqual([], os.listdir('.')) |
3873 | + |
3874 | + def test_single_line_without_return(self): |
3875 | + self.assertEqual([], os.listdir('.')) |
3876 | + tests.write_tree_from_desc('file: foo') |
3877 | + self.assertEqual(['foo'], os.listdir('.')) |
3878 | + self.assertEqual('', file('foo').read()) |
3879 | + |
3880 | + def test_leading_line_is_ignored(self): |
3881 | + self.assertEqual([], os.listdir('.')) |
3882 | + tests.write_tree_from_desc('tagada\nfile: foo') |
3883 | + self.assertEqual(['foo'], os.listdir('.')) |
3884 | + self.assertEqual('', file('foo').read()) |
3885 | + |
3886 | + def test_orphan_line_is_ignored(self): |
3887 | + self.assertEqual([], os.listdir('.')) |
3888 | + tests.write_tree_from_desc(''' |
3889 | +dir: foo |
3890 | +orphan line |
3891 | +file: foo/bar.py |
3892 | +baz |
3893 | +''') |
3894 | + self.assertEqual(['foo'], os.listdir('.')) |
3895 | + self.assertEqual(['bar.py'], os.listdir('foo')) |
3896 | + self.assertEqual('baz\n', file('foo/bar.py').read()) |
3897 | + |
3898 | + def test_empty_file_content(self): |
3899 | + tests.write_tree_from_desc('''file: foo''') |
3900 | + self.assertEqual('', file('foo').read()) |
3901 | + |
3902 | + def test_simple_file_content(self): |
3903 | + tests.write_tree_from_desc('''file: foo |
3904 | +tagada |
3905 | +''') |
3906 | + self.assertEqual('tagada\n', file('foo').read()) |
3907 | + |
3908 | + def test_file_content_in_a_dir(self): |
3909 | + tests.write_tree_from_desc('''dir: dir |
3910 | +file: dir/foo |
3911 | +bar |
3912 | +''') |
3913 | + self.assertEqual('bar\n', file('dir/foo').read()) |
3914 | + |
3915 | + def test_simple_symlink_creation(self): |
3916 | + tests.write_tree_from_desc('''file: foo |
3917 | +tagada |
3918 | +link: foo bar |
3919 | +''') |
3920 | + self.assertEqual('tagada\n', file('foo').read()) |
3921 | + self.assertEqual('tagada\n', file('bar').read()) |
3922 | + |
3923 | + def test_broken_symlink_creation(self): |
3924 | + tests.write_tree_from_desc('''link: foo bar |
3925 | +''') |
3926 | + self.assertEqual('foo', os.readlink('bar')) |
3927 | + |
3928 | + def test_invalid_symlink_description_raises(self): |
3929 | + e = self.assertRaises(ValueError, |
3930 | + tests.write_tree_from_desc, '''link: foo |
3931 | +''') |
3932 | + self.assertEqual(e.message, 'Invalid link description: foo') |
3933 | |
3934 | === modified file 'src/sst/tests/test_xvfb.py' |
3935 | --- src/sst/tests/test_xvfb.py 2013-01-24 06:16:28 +0000 |
3936 | +++ src/sst/tests/test_xvfb.py 2013-05-14 11:43:29 +0000 |
3937 | @@ -26,7 +26,7 @@ |
3938 | |
3939 | |
3940 | from sst import ( |
3941 | - runtests, |
3942 | + cases, |
3943 | xvfbdisplay, |
3944 | ) |
3945 | |
3946 | @@ -49,7 +49,7 @@ |
3947 | self.assertEquals(orig, os.environ['DISPLAY']) |
3948 | |
3949 | |
3950 | -class Headless(runtests.SSTTestCase): |
3951 | +class Headless(cases.SSTTestCase): |
3952 | """A specialized test class for tests around xvfb.""" |
3953 | |
3954 | # We don't use a browser here so disable its use to speed the tests |
3955 | |
3956 | === modified file 'src/sst/xvfbdisplay.py' |
3957 | --- src/sst/xvfbdisplay.py 2013-01-24 06:02:56 +0000 |
3958 | +++ src/sst/xvfbdisplay.py 2013-05-14 11:43:29 +0000 |
3959 | @@ -85,6 +85,21 @@ |
3960 | os.environ['DISPLAY'] = ':%s' % display_num |
3961 | |
3962 | |
3963 | +def use_xvfb_server(test, xvfb=None): |
3964 | + """Setup an xvfb server for a given test. |
3965 | + |
3966 | + :param xvfb: An Xvfb object to use. If none is supplied, default values are |
3967 | + used to build it. |
3968 | + |
3969 | + :returns: The xvfb server used so tests can use the built one. |
3970 | + """ |
3971 | + if xvfb is None: |
3972 | + xvfb = Xvfb() |
3973 | + xvfb.start() |
3974 | + test.addCleanup(xvfb.stop) |
3975 | + return xvfb |
3976 | + |
3977 | + |
3978 | if __name__ == '__main__': |
3979 | # example: |
3980 | |
3981 | |
3982 | === modified file 'sst-remote' |
3983 | --- sst-remote 2012-09-27 11:17:49 +0000 |
3984 | +++ sst-remote 2013-05-14 11:43:29 +0000 |
3985 | @@ -1,10 +1,13 @@ |
3986 | #!/usr/bin/env python |
3987 | |
3988 | -from os.path import dirname, join |
3989 | -from sys import path |
3990 | +import os |
3991 | +import sys |
3992 | |
3993 | |
3994 | if __name__ == "__main__": |
3995 | - path.insert(0, join(dirname(__file__), "src")) |
3996 | + # We run from sources, we must ensure we won't import from an installed |
3997 | + # version so we insert 'src' in front of sys.path |
3998 | + cur_dir = os.path.abspath(os.path.dirname(__file__)) |
3999 | + sys.path.insert(0, os.path.join(cur_dir, 'src')) |
4000 | from sst.scripts import remote |
4001 | remote.main() |
4002 | |
4003 | === modified file 'sst-run' |
4004 | --- sst-run 2012-09-27 11:17:49 +0000 |
4005 | +++ sst-run 2013-05-14 11:43:29 +0000 |
4006 | @@ -1,10 +1,13 @@ |
4007 | #!/usr/bin/env python |
4008 | |
4009 | -from os.path import dirname, join |
4010 | -from sys import path |
4011 | +import os |
4012 | +import sys |
4013 | |
4014 | |
4015 | if __name__ == "__main__": |
4016 | - path.insert(0, join(dirname(__file__), "src")) |
4017 | + # We run from sources, we must ensure we won't import from an installed |
4018 | + # version so we insert 'src' in front of sys.path |
4019 | + cur_dir = os.path.abspath(os.path.dirname(__file__)) |
4020 | + sys.path.insert(0, os.path.join(cur_dir, 'src')) |
4021 | from sst.scripts import run |
4022 | run.main() |
4023 | |
4024 | === added file 'test-loader.TODO' |
4025 | --- test-loader.TODO 1970-01-01 00:00:00 +0000 |
4026 | +++ test-loader.TODO 2013-05-14 11:43:29 +0000 |
4027 | @@ -0,0 +1,39 @@ |
4028 | +* run all tests |
4029 | + |
4030 | + sst test |
4031 | + |
4032 | +* run acceptance tests |
4033 | + |
4034 | + sst test -i tests.acceptance |
4035 | + |
4036 | +* run unit tests |
4037 | + |
4038 | + sst test -e tests.acceptance |
4039 | + |
4040 | +* some test_loader tests are too big, this screams for better helpers so |
4041 | + they can be simplified. |
4042 | + |
4043 | +* fix the remaining grey areas in the design |
4044 | + |
4045 | +There are a few points that smell funny in the current implementation |
4046 | +including: |
4047 | + |
4048 | +- abusing sortTestMethodsUsing |
4049 | + |
4050 | +- having to call sortNames in several places |
4051 | + |
4052 | +This is probably caused by some less than ideal split of responsability |
4053 | +between TestLoader and the {File|Dir}Loader classes. |
4054 | + |
4055 | +The key points that need to be handled are: |
4056 | + |
4057 | +- whether directories are packages or not and as such require to be imported |
4058 | + |
4059 | +- if they are imported, respect user overriding |
4060 | + |
4061 | +- allowing the user to specify some matching on the base names encountered |
4062 | + while walking the tree (or a sub tree). This should remain separate for |
4063 | + files and directories (the rules are different). |
4064 | + |
4065 | +- how the sub tree is iterated |
4066 | + |
l 44, 66, 355, 482, 504, 644, 664, 685, 731, 765, 806, 835, 890, 938, 948, 959, 995, 1961, 1981, 2022, 3958
Pep8 recommeds to precede the quotes that end a multiline docstring with an empty line.
Not a real issue, and not all of them introduced by you. But I find nice the consistency that comes from following all pep8 recommendations.
I have on my TODO to update all the comments in the actions module, btw.
139 === added file 'src/sst/case.py'
Isn't cases a better name?
332 + # TODO: Adding script_dir to sys.path only make sense if we want to
333 + # allow scripts to import from their own dir. Do we really need that ?
Typo: makeS
To answer the question, I'd say no. We are trying to use the full path everywhere, so this only allows a behavior we are trying to avoid. If we remove it, it might break some tests, but that's fine as they should be updated anyway.
353 + the first row (headers) match data_map key names.
354 + rows beneath are filled with data values.
Not changed by you, but I think this has wrong identation.
to be continued...