Merge lp:~vila/selenium-simple-test/test-loader into lp:selenium-simple-test

Proposed by Vincent Ladeuil
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
Reviewer Review Type Date Requested Status
Leo Arias (community) code review Approve
Review via email: mp+162765@code.launchpad.net

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_find_cases.py and test_runtests_get_suites.py.

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.

To post a comment you must log in.
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

Revision history for this message
Leo Arias (elopio) 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.
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...

Revision history for this message
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.shared_directory
1887 + == os.path.abspath(os.path.join(thisdir, 'shared')))

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://www.python.org/dev/peps/pep-0257/#multi-line-docstrings is 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'm not done yet...)

Revision history for this message
Leo Arias (elopio) wrote :

2084 + # command.get_opts_run and friends relies on optparse defaulting so

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(self, expected, condition, actual):

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.assertFiltered(
    expected=['foo', 'foobar', 'barfoo'], pattern=['*foo*'],
    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("'foo'^'bar'\n")

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.

Revision history for this message
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://pad.lv/1170389

>
> 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.SSTTestCase and runtests.SSTScriptTestCase
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://pad.lv/1179797

>
> 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.

Revision history for this message
Vincent Ladeuil (vila) wrote :
Download full text (3.5 KiB)

> 2084 + # command.get_opts_run and friends relies on optparse defaulting so
>
> 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(self, expected, condition, actual):
>
> 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.assertFiltered(
> 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_privates ?

>
> 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("'foo'^'bar'\n")
>
> 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...

Read more...

Revision history for this message
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_directory which will search directories above for a '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_directory and just leave the '-m' in place for now.

>
> 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.shared_directory
> 1887 + == os.path.abspath(os.path.join(thisdir, 'shared')))
>

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://www.python.org/dev/peps/pep-0257/#multi-line-docstrings is
> 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://stackoverflow.com/questions/5334531/python-documentation-standard-for-docstring
  http://sphinx-doc.org/markup/desc.html#info-field-lists

> (I'm not done yet...)

Thanks for that already ;)

448. By Vincent Ladeuil

Stop adding the script directory to sys.path

Revision history for this message
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.SSTTestCase and runtests.SSTScriptTestCase
> 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.

Revision history for this message
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.assertFiltered(
> > 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_privates ?

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.

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

> > Since we will deprecate runtests.SSTTestCase and runtests.SSTScriptTestCase
> > 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.

Revision history for this message
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 :)

review: Approve (code review)
449. By Vincent Ladeuil

Remerge result branch

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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+

Subscribers

People subscribed via source and target branches