Merge lp:~javier.collado/mago/suite_discovery into lp:~mago-contributors/mago/mago-1.0
- suite_discovery
- Merge into mago-1.0
Status: | Rejected |
---|---|
Rejected by: | Javier Collado |
Proposed branch: | lp:~javier.collado/mago/suite_discovery |
Merge into: | lp:~mago-contributors/mago/mago-1.0 |
Diff against target: | None lines |
To merge this branch: | bzr merge lp:~javier.collado/mago/suite_discovery |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Joker Wild (community) | test | Approve | |
Eitan Isaacson | Needs Fixing | ||
Review via email:
|
Commit message
Description of the change
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Javier Collado (javier.collado) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eitan Isaacson (eeejay) wrote : | # |
Ambitious, I like it. It is a major change, but it is the obvious next step.
One thing: when you talk about the overlap, do you mean that text execution and discovery don't follow the exact same code path? I think they should before this goes into trunk.
Also, I think an environment variable (besides -d) would be useful.
> Hello,
>
> This branch is a refactoring of the code in ubuntu-desktop-test into a package
> called cmd that is part of the desktoptesting package. The code has been
> splitted in different modules to provide better maintainability.
>
> In addition to this, some effort has been spent in writing new test discovery
> code. The summary of the changes is the following:
> - Added -d, --directory option to append as many directories as needed where
> the test cases should be found
> - Added -i, --info option to just display the name of the test cases that
> would be executed.
> - Removed -f, --file options since it the functionality to filter by suite
> file is already covered by -s, --suite
>
> As you'll see that the discovery is based on generators which means that it's
> being performed on demand (i.e. test cases aren't discovered until they are
> going to be executed) and that the time that it takes to start executing the
> first test case should be reduced (specially in an installation with a big set
> of test cases). I believe that other test runners such as nose are written
> using this design.
>
> There's some overlapping between the process_suite_file function and the Case
> class (in discovery module) since both of them perform test case filtering.
> However, I expect to improve the integration of these two parts of code in the
> future since changing also that part would have made the change much bigger
> than I expected.
>
> I hope you find this change useful.
>
> Best regards,
> Javier
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Javier Collado (javier.collado) wrote : | # |
Hello,
The overlapping is because the code in the discovery.py module parses the xml file to discover the test cases in it. This is what makes possible to do the following:
for app in apps:
for suite in app.suites():
for case in suite.cases():
....
On the other hand, the process_suite_file also parses the suite file to run the test suite and apply the same filtering (using -c command line options) so for a complete integration of the discovery mechanism I need to make some changes so that a Suite object, that takes care of the xml parsing, is used instead of a suite filename.
I don't expect this additional change to be very big, but I wanted to know you opinion about the whole idea before spending too much time in it. So unless you have any concern, I may continue working on this and ask for another review when the work in the branch is finished.
Also, I will add the environment variable so that it's used if present unless an option is specified, which will take precedence. Given the change in the project name, I think that a good name for the variable would be MAGO_PATH.
Do you agree on this (continue working and ask for a new review later and the name for the environment variable)?
Best regards,
Javier
> Ambitious, I like it. It is a major change, but it is the obvious next step.
>
> One thing: when you talk about the overlap, do you mean that text execution
> and discovery don't follow the exact same code path? I think they should
> before this goes into trunk.
>
> Also, I think an environment variable (besides -d) would be useful.
>
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eitan Isaacson (eeejay) wrote : | # |
I agree that these changes would be positive. Ara, what do you say?
> Hello,
>
> The overlapping is because the code in the discovery.py module parses the xml
> file to discover the test cases in it. This is what makes possible to do the
> following:
> for app in apps:
> for suite in app.suites():
> for case in suite.cases():
> ....
>
> On the other hand, the process_suite_file also parses the suite file to run
> the test suite and apply the same filtering (using -c command line options) so
> for a complete integration of the discovery mechanism I need to make some
> changes so that a Suite object, that takes care of the xml parsing, is used
> instead of a suite filename.
>
> I don't expect this additional change to be very big, but I wanted to know you
> opinion about the whole idea before spending too much time in it. So unless
> you have any concern, I may continue working on this and ask for another
> review when the work in the branch is finished.
>
> Also, I will add the environment variable so that it's used if present unless
> an option is specified, which will take precedence. Given the change in the
> project name, I think that a good name for the variable would be MAGO_PATH.
>
> Do you agree on this (continue working and ask for a new review later and the
> name for the environment variable)?
>
> Best regards,
> Javier
>
> > Ambitious, I like it. It is a major change, but it is the obvious next step.
> >
> > One thing: when you talk about the overlap, do you mean that text execution
> > and discovery don't follow the exact same code path? I think they should
> > before this goes into trunk.
> >
> > Also, I think an environment variable (besides -d) would be useful.
> >
- 64. By Javier Collado
-
process_suite_file changed to use suite objects instead of suite filenames (renamed to process_suite)
run_suite_file changed to use suite objects instead of suite filenames (renamed to run_suite) - 65. By Javier Collado
-
Runner classes are not longer in charge of xml parsing and test case discovery
- 66. By Javier Collado
-
Fixed problem with result tags
- 67. By Javier Collado
-
Results storage moved to result.ResultDict class
- 68. By Javier Collado
-
Classes renamed to make it clear that they just contain data and not the implementation
- 69. By Javier Collado
-
Screenshot problem fixed
- 70. By Javier Collado
-
Removing cases parameter no longer used
- 71. By Javier Collado
-
New process_application function created to iterate directly over applications instead of over suites
- 72. By Javier Collado
-
If MAGO_PATH environment variable exists, it's used as the default list of directories to look for applications (unless -d option is passed explicitly)
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Javier Collado (javier.collado) wrote : | # |
Hello,
I've been worked a little more on the code and the integration is now complete:
- runner classes work with SuiteData or CaseData objects so they don't take care of discovery or xml parsing functionality
- MAGO_PATH environment variable is used as default list of directories (unless -d option is used)
Please take a look again at the code and let me know you opinion.
Best regards,
Javier
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eitan Isaacson (eeejay) wrote : | # |
I like it.. and need it.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eitan Isaacson (eeejay) wrote : | # |
Actually, logging seems to be broken. I am not seeing any logging output from safe_run_command()
debugging..
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eitan Isaacson (eeejay) wrote : | # |
Wasn't a logger bug, here is the fix:
=== modified file 'desktoptesting
--- desktoptesting/
+++ desktoptesting/
@@ -243,7 +243,7 @@
# command line
if cls.whitelist:
suites = (suite for suite in suites
- if suite in cls.whitelist)
+ if suite.name in cls.whitelist)
return suites
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eitan Isaacson (eeejay) wrote : | # |
Here is another regression that I think needs fixing before merge:
2009-06-16 14:50:13,260 ERROR XSL file `/home/
This has to do with the TESTS_SHARE global. I am not sure if it's value is a good choice. In any case, the XSL should probably be found using "-d" and MAGO_PATH.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Joker Wild (lajjr-deactivatedaccount) wrote : | # |
If logging in implemented.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Javier Collado (javier.collado) wrote : | # |
Hello Eitan,
Thanks for your comments and your feedback. I'm afraid that the code is not free from defects so I really appreciate your help.
I've taken a look at that piece of code and the original version still looks fine to me. Let me explain it as clear as possible:
- suite is a SuiteData object that implements the __eq__ method for comparisons
- suite.name is the name of the test suite as written in the xml file
- suite.filename is the name of the xml file that contains the suite description data
My understanding was that in the main branch the suite filtering isn't performed by the suite name, but by the suite filename (with or without extension). Is that correct? In such a case that piece of code seems to be right since it's working that way thanks to the __eq__ method. For example:
(Pydb) suite
<desktoptesting
(Pydb) suite.name
'gedit chains'
(Pydb) suite.filename
'gedit_chains.xml'
(Pydb) suite in ['gedit chains']
False
(Pydb) suite in ['gedit_
True
(Pydb) suite in ['gedit_chains']
True
Please let me know if this is the way suite filtering is expected to work?
Anyway, I'll take a look at the safe_run_command output to make sure what happened to the logs.
Best regards,
Javier
> Wasn't a logger bug, here is the fix:
>
> === modified file 'desktoptesting
> --- desktoptesting/
> +++ desktoptesting/
> @@ -243,7 +243,7 @@
> # command line
> if cls.whitelist:
> suites = (suite for suite in suites
> - if suite in cls.whitelist)
> + if suite.name in cls.whitelist)
>
> return suites
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Javier Collado (javier.collado) wrote : | # |
Hello Eitan,
Since there are multiple directories that may contain test cases, I made the assumption that the xsl file is located in a path relative to the test runner script. For example, in my installation:
$ which ubuntu-desktop-test
/usr/local/
$ locate report.xsl | grep /usr/local
/usr/local/
So what I did is, at first, set TEST_SHARE to (globals.py):
TESTS_SHARE = "share/
and once the script is called (main.py):
globals.
To make this work, it is needed that arg[0] contains a full path so the following change was needed (ubuntu-
args[0] = __file__
From what I see in your log, you called the ubuntu-desktop-test script from the development branch instead of from an installed path and that's the reason the XSL file wasn't found (XSL relative path is different in development than in installation).
I could change the code in main.py to use a fallback path for the XSL file in case it's not found where expected. However, I'm not very fond of making a change to make code in the development branch work when the same code is working after being installed. Maybe we should try to look for a cleaner solution for the problem. I don't know if trying to incorporate the buildout changes from Markus would make this mismatch easier to solve.
Does anybody have any idea?
Best regards,
Javier
> Here is another regression that I think needs fixing before merge:
>
> 2009-06-16 14:50:13,260 ERROR XSL file
> `/home/
> tests/report.xsl' does not exist.
>
> This has to do with the TESTS_SHARE global. I am not sure if it's value is a
> good choice. In any case, the XSL should probably be found using "-d" and
> MAGO_PATH.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Javier Collado (javier.collado) wrote : | # |
I've run the gedit test cases with debug log level and seen this:
2009-06-17 10:02:10,933 DEBUG Running command: xsltproc -o /home/javi/
Should I have seen something else? Maybe the problem you're having is related to the one with the XSL file.
> Actually, logging seems to be broken. I am not seeing any logging output from
> safe_run_command()
>
> debugging..
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eitan Isaacson (eeejay) wrote : | # |
> Hello Eitan,
>
> Thanks for your comments and your feedback. I'm afraid that the code is not
> free from defects so I really appreciate your help.
>
> I've taken a look at that piece of code and the original version still looks
> fine to me. Let me explain it as clear as possible:
> - suite is a SuiteData object that implements the __eq__ method for
> comparisons
> - suite.name is the name of the test suite as written in the xml file
> - suite.filename is the name of the xml file that contains the suite
> description data
>
> My understanding was that in the main branch the suite filtering isn't
> performed by the suite name, but by the suite filename (with or without
> extension). Is that correct?
This has been correct. The main problem is that the "-i" option does not show suite files, but suite names. This defeats the purpose of the suites being discoverable, a user should be able to do this:
$ ubuntu-desktop-test -i
*suite/case list*
$ ubuntu-desktop-test -c <suite from list above>
I think both options should be available, by file or by suite. The suite filter is the most easy to use, but the file filter will allow things to not break when more than one suite have the same name.
> Please let me know if this is the way suite filtering is expected to work?
overriding __eq__ is fine, just see above.
> Anyway, I'll take a look at the safe_run_command output to make sure what
> happened to the logs.
That was my mistake, the logger is fine. The suite was simply not being run because I was supplying suite names, not files, see above.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Javier Collado (javier.collado) wrote : | # |
To solve the problem, I think I could do the following:
- -s/--suite: Provides filtering for both file name and suite name
Display not only the name, but also the file name when displaying the suite information (also display the path for applications).
Would that be OK?
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eitan Isaacson (eeejay) wrote : | # |
>
> I could change the code in main.py to use a fallback path for the XSL file in
> case it's not found where expected. However, I'm not very fond of making a
> change to make code in the development branch work when the same code is
> working after being installed.
I think being able to run things out of the build path is very important. And yes, the solution is probably hackish. Maybe search for the XSL in the parent directory of the suite?
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eitan Isaacson (eeejay) wrote : | # |
> To solve the problem, I think I could do the following:
> - -s/--suite: Provides filtering for both file name and suite name
Maybe -f/--file? or -x/--xml? Having -s do both is confusing.
>
> Display not only the name, but also the file name when displaying the suite
> information (also display the path for applications).
>
That sounds great.
> Would that be OK?
medio :)
- 73. By Javier Collado
-
Suites may be filtered by either name or filename depending on the command line option used (-n/--suite_name, -f/--suite_file)
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eitan Isaacson (eeejay) wrote : | # |
Ok, I have been thinking about this, and here is how I think the directory mess could be simplified:
1. Get rid of -d, it is confusing that there is more than one way to set the directory.
2. globals.TESTS_SHARE turns into globals.MAGO_SHARE, unless a environment variable of the same name overrides it, it points to $(prefix)
3. A new variable called globals.MAGO_PATH is introduced that is a list of paths to search for tests in. By default it is: [os.path.curdir, os.path.
Some pseudo-code:
def _get_share_dir():
# Return installed share directory path, or current build directory if not installed.
MAGO_SHARE = os.env.
if os.env.
MAGO_PATH = os.env[
else:
MAGO_PATH = [os.path.curdir, os.path.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eitan Isaacson (eeejay) wrote : | # |
Just implemented in lp:~eeejay/mago/suite_discovery
Please review/
- 74. By Javier Collado
-
Taking advantage of the testing_
install_ scripts class to set TESTS_SHARE global variable properly in installed code, while the original source code works fine directly in the branch
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Javier Collado (javier.collado) wrote : | # |
Hello Eitan,
I'm afraid I still don't feel comfortable with having a piece of code whose aim is to check if we are in a branch or in an installed path.
When I was thinking about the questions I sent to the mail list regarding the additional code in setup.py, I saw that another solution was already there. What we really need is to have a global variable that has a value for the code that is in the branch and another one for the installed code. This way, the code is the same (which is property a like) in both cases despite the value of the TESTS_SHARE global variable isn't. This can be accomplished using the testing_
Best regards,
Javier
> Just implemented in lp:~eeejay/mago/suite_discovery
>
> Please review/
- 75. By Javier Collado
-
Removing code that is no longer needed
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eitan Isaacson (eeejay) wrote : | # |
> Hello Eitan,
>
> I'm afraid I still don't feel comfortable with having a piece of code whose
> aim is to check if we are in a branch or in an installed path.
>
I don't think we will agree here. The variable substitution you suggest seems error prone on many levels, and easily overlooked. While the solution I suggested might not be entirely bomb-proof, I think it is fairly robust, in one file, and easy to read.
Did you see my three points above? I think those are important. As to how the share dir is found, I will leave that up to you. In any case, I needed this in trunk last week, for the checkbox work.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eitan Isaacson (eeejay) wrote : | # |
Another suggestion: Let's not recursively search for tests. Since we could define arbitrary paths to search in, a user might put "/" in by mistake, or even just her home directory. I noticed that discovery takes a long time when running from $HOME. A depth of one is good enough, in my opinion.
Unmerged revisions
- 75. By Javier Collado
-
Removing code that is no longer needed
- 74. By Javier Collado
-
Taking advantage of the testing_
install_ scripts class to set TESTS_SHARE global variable properly in installed code, while the original source code works fine directly in the branch
Preview Diff
1 | === modified file 'bin/ubuntu-desktop-test' |
2 | --- bin/ubuntu-desktop-test 2009-05-05 11:10:25 +0000 |
3 | +++ bin/ubuntu-desktop-test 2009-06-03 10:12:35 +0000 |
4 | @@ -1,502 +1,9 @@ |
5 | #!/usr/bin/env python |
6 | - |
7 | -import os |
8 | - |
9 | -os.environ['NO_GAIL'] = '1' |
10 | -os.environ['NO_AT_BRIDGE'] = '1' |
11 | - |
12 | -import re |
13 | -import sys |
14 | -import logging |
15 | -import xml.dom.minidom |
16 | -import ldtp |
17 | -import ldtputils |
18 | -import traceback |
19 | - |
20 | -from logging import StreamHandler, FileHandler, Formatter |
21 | -from optparse import OptionParser |
22 | -from stat import ST_MODE, S_IMODE |
23 | -from subprocess import Popen, PIPE |
24 | -from time import time, gmtime, strftime |
25 | -from shutil import move |
26 | - |
27 | -# Globals |
28 | -TESTS_SHARE = "." |
29 | -TESTS_HOME = os.environ["HOME"] + "/.ubuntu-desktop-test" |
30 | -SCREENSHOTS_SHARE = "/tmp/ldtp-screenshots" |
31 | - |
32 | -# For the moment, all applications should be running in English |
33 | -ldtp.setlocale("C") |
34 | - |
35 | - |
36 | -def error(message, *args): |
37 | - message = "Error: %s\n" % message |
38 | - sys.stderr.write(message % args) |
39 | - sys.exit(1) |
40 | - |
41 | -def safe_change_mode(path, mode): |
42 | - if not os.path.exists(path): |
43 | - error("Path does not exist: %s", path) |
44 | - |
45 | - old_mode = os.stat(path)[ST_MODE] |
46 | - if mode != S_IMODE(old_mode): |
47 | - os.chmod(path, mode) |
48 | - |
49 | -def safe_make_directory(path, mode=0755): |
50 | - if os.path.exists(path): |
51 | - if not os.path.isdir(path): |
52 | - error("Path is not a directory: %s", path) |
53 | - |
54 | - safe_change_mode(path, mode) |
55 | - else: |
56 | - logging.debug("Creating directory: %s", path) |
57 | - os.makedirs(path, mode) |
58 | - |
59 | -def safe_run_command(command): |
60 | - logging.debug("Running command: %s" % command) |
61 | - p = Popen(command, stdout=PIPE, shell=True) |
62 | - (pid, status) = os.waitpid(p.pid, 0) |
63 | - if status: |
64 | - error("Command failed: %s", command) |
65 | - |
66 | - return p.stdout.read() |
67 | - |
68 | -def is_valid_application_directory(application_directory, applications): |
69 | - application = os.path.basename(application_directory) |
70 | - if applications is not None and application not in applications: |
71 | - logging.debug("Application name `%s' not in specified options: %s", |
72 | - application, ", ".join(applications)) |
73 | - return False |
74 | - |
75 | - pattern = r"[a-z0-9][-_a-z0-9+.]*" |
76 | - if not re.match(pattern, application, re.I): |
77 | - logging.debug("Application name `%s' does not match pattern: %s", |
78 | - application, pattern) |
79 | - return False |
80 | - |
81 | - if not os.path.isdir(application_directory): |
82 | - logging.debug("Application directory `%s' is not a directory", |
83 | - application_directory) |
84 | - return False |
85 | - |
86 | - return True |
87 | - |
88 | -def is_valid_suite(suite_file, suites, files): |
89 | - |
90 | - # If the user has specified a file, but not a suite, skip |
91 | - if suites is None and files is not None: |
92 | - return False |
93 | - |
94 | - # Support specifying suites with or without the extension |
95 | - # but do not include backup files LP#326177 |
96 | - if suites is not None: |
97 | - suites_clean = [] |
98 | - for s in suites: |
99 | - if os.path.splitext(s)[1] is '': |
100 | - suites_clean.append(s + ".xml") |
101 | - else: |
102 | - suites_clean.append(s) |
103 | - |
104 | - suites = suites_clean |
105 | - |
106 | - suite = os.path.basename(suite_file) |
107 | - if suites is not None and suite not in suites: |
108 | - logging.debug("Suite name `%s' not in specified options: %s", |
109 | - suite, ", ".join(suites)) |
110 | - return False |
111 | - |
112 | - pattern = r"[a-z0-9][-_a-z0-9+.]*.xml$" |
113 | - if not re.match(pattern, suite, re.I): |
114 | - logging.debug("Suite name `%s' does not match pattern: %s", |
115 | - suite, pattern) |
116 | - return False |
117 | - |
118 | - if not os.path.isfile(suite_file): |
119 | - logging.debug("Suite file `%s' is not a file", |
120 | - suite_file) |
121 | - return False |
122 | - |
123 | - return True |
124 | - |
125 | -def is_valid_suite_file(file, suites): |
126 | - |
127 | - suite = os.path.basename(file) |
128 | - |
129 | - pattern = r"[a-z0-9][-_a-z0-9+.]*.xml$" |
130 | - if not re.match(pattern, suite, re.I): |
131 | - logging.debug("Suite name `%s' does not match pattern: %s", |
132 | - file, pattern) |
133 | - return False |
134 | - |
135 | - if not os.path.isfile(file): |
136 | - logging.debug("Suite file `%s' is not a file", |
137 | - file) |
138 | - return False |
139 | - |
140 | - if file in suites: |
141 | - logging.debug("Suite file is already in the list", |
142 | - file) |
143 | - return False |
144 | - |
145 | - return True |
146 | - |
147 | - |
148 | -def filter_suite_files(applications, suites, files, folder): |
149 | - |
150 | - # Filter applications |
151 | - suite_files = [] |
152 | - |
153 | - if folder is not None: |
154 | - for application in os.listdir(folder): |
155 | - application_directory = os.path.join(folder, application) |
156 | - if not is_valid_application_directory(application_directory, applications): |
157 | - continue |
158 | - |
159 | - # Filter suites |
160 | - for suite in os.listdir(application_directory): |
161 | - suite_file = os.path.join(application_directory, suite) |
162 | - if not is_valid_suite(suite_file, suites, files): |
163 | - continue |
164 | - |
165 | - suite_files.append(suite_file) |
166 | - |
167 | - if files is not None and folder is None: |
168 | - for file in files: |
169 | - if not is_valid_suite_file(file, suite_files): |
170 | - continue |
171 | - suite_files.append(file) |
172 | - |
173 | - return suite_files |
174 | - |
175 | -def run_suite_file(suite_file, log_file, cases=None): |
176 | - conf_file = os.path.join(TESTS_SHARE, "conffile.ini") |
177 | - runner = TestSuiteRunner(suite_file) |
178 | - results = runner.run(cases=cases) |
179 | - f = open(log_file, 'w') |
180 | - f.write(results) |
181 | - f.close() |
182 | - |
183 | -def convert_log_file(log_file): |
184 | - |
185 | - if os.path.exists(SCREENSHOTS_SHARE): |
186 | - screenshot_dir = os.path.dirname(log_file) + "/screenshots" |
187 | - |
188 | - if not os.path.exists(screenshot_dir): |
189 | - safe_make_directory(screenshot_dir) |
190 | - |
191 | - if len(os.listdir(SCREENSHOTS_SHARE)) > 0: |
192 | - command = "mv " + SCREENSHOTS_SHARE + "/* " + screenshot_dir |
193 | - safe_run_command(command) |
194 | - |
195 | - log_file_tmp = log_file + ".tmp" |
196 | - o = open(log_file_tmp, "w") |
197 | - data = open(log_file).read() |
198 | - o.write(re.sub(SCREENSHOTS_SHARE, "screenshots", data)) |
199 | - o.flush() |
200 | - o.close() |
201 | - |
202 | - os.remove(log_file) |
203 | - os.rename(log_file_tmp, log_file) |
204 | - |
205 | - xsl_file = os.path.join(TESTS_SHARE, "report.xsl") |
206 | - if not os.path.exists(xsl_file): |
207 | - error("XSL file `%s' does not exist.", xsl_file) |
208 | - |
209 | - html_file = log_file.replace(".log", ".html") |
210 | - |
211 | - command = "xsltproc -o %s %s %s" \ |
212 | - % (html_file, xsl_file, log_file) |
213 | - safe_run_command(command) |
214 | - |
215 | -def process_suite_file(suite_file, target_directory, cases=None): |
216 | - application_name = os.path.basename(os.path.dirname(suite_file)) |
217 | - application_target = os.path.join(target_directory, application_name) |
218 | - safe_make_directory(application_target) |
219 | - |
220 | - suite_name = os.path.basename(suite_file) |
221 | - log_file = os.path.join(application_target, |
222 | - suite_name.replace(".xml", ".log")) |
223 | - run_suite_file(suite_file, log_file, cases) |
224 | - |
225 | - convert_log_file(log_file) |
226 | - |
227 | -def main(args=sys.argv): |
228 | - usage = "%prog [OPTIONS]" |
229 | - parser = OptionParser(usage=usage) |
230 | - |
231 | - default_target = "~/.ubuntu-desktop-test" |
232 | - default_log_level = "critical" |
233 | - |
234 | - parser.add_option("-l", "--log", |
235 | - metavar="FILE", |
236 | - help="The file to write the log to.") |
237 | - parser.add_option("--log-level", |
238 | - default=default_log_level, |
239 | - help="One of debug, info, warning, error or critical.") |
240 | - parser.add_option("-a", "--application", |
241 | - action="append", |
242 | - type="string", |
243 | - default=None, |
244 | - help="Application name to test. Option can be repeated " |
245 | - "and defaults to all applications") |
246 | - parser.add_option("-s", "--suite", |
247 | - action="append", |
248 | - type="string", |
249 | - default=None, |
250 | - help="Suite name to test within applications. Option " |
251 | - "can be repeated and default to all suites") |
252 | - parser.add_option("-f", "--file", |
253 | - action="append", |
254 | - type="string", |
255 | - default=None, |
256 | - help="XML file name of the suite to test within applications.") |
257 | - parser.add_option("-t", "--target", |
258 | - metavar="FILE", |
259 | - default=default_target, |
260 | - help="Target directory for logs and reports. Defaults " |
261 | - "to: %default") |
262 | - parser.add_option("-c", "--case", |
263 | - action="append", |
264 | - type="string", |
265 | - default=None, |
266 | - help="Test cases to run (all, if not specified).") |
267 | - |
268 | - (options, args) = parser.parse_args(args[1:]) |
269 | - |
270 | - # Set logging early |
271 | - log_level = logging.getLevelName(options.log_level.upper()) |
272 | - log_handlers = [] |
273 | - log_handlers.append(StreamHandler()) |
274 | - if options.log: |
275 | - log_filename = options.log |
276 | - log_handlers.append(FileHandler(log_filename)) |
277 | - |
278 | - format = ("%(asctime)s %(levelname)-8s %(message)s") |
279 | - if log_handlers: |
280 | - for handler in log_handlers: |
281 | - handler.setFormatter(Formatter(format)) |
282 | - logging.getLogger().addHandler(handler) |
283 | - if log_level: |
284 | - logging.getLogger().setLevel(log_level) |
285 | - elif not logging.getLogger().handlers: |
286 | - logging.disable(logging.CRITICAL) |
287 | - |
288 | - options.target = os.path.expanduser(options.target) |
289 | - if os.path.exists(options.target) and not os.path.isdir(options.target): |
290 | - parser.error("Target directory `%s' exists but is not a directory.", |
291 | - options.target) |
292 | - |
293 | - # Filter suite files from project directory |
294 | - |
295 | - if not os.path.isdir(TESTS_SHARE): |
296 | - error("Share directory `%s' is not a directory.", TESTS_SHARE) |
297 | - else: |
298 | - suite_files = filter_suite_files(options.application, options.suite, options.file, TESTS_SHARE) |
299 | - |
300 | - if os.path.isdir(TESTS_HOME): |
301 | - suite_files += filter_suite_files(options.application, options.suite, options.file, TESTS_HOME) |
302 | - |
303 | - suite_files += filter_suite_files(options.application, options.suite, options.file, None) |
304 | - |
305 | - # Run filtered suite file |
306 | - for suite_file in suite_files: |
307 | - process_suite_file(suite_file, options.target, options.case) |
308 | - |
309 | - |
310 | - return 0 |
311 | - |
312 | -class TestRunner: |
313 | - def __init__(self): |
314 | - self.result = {} |
315 | - self.args = {} |
316 | - |
317 | - def get_args(self, node): |
318 | - for n in node.childNodes: |
319 | - if n.nodeType != n.ELEMENT_NODE or not n.hasChildNodes(): |
320 | - continue |
321 | - self.args[n.tagName.encode('ascii')] = n.firstChild.data |
322 | - |
323 | - def add_results_to_node(self, node): |
324 | - if self.result == {}: return |
325 | - result = node.ownerDocument.createElement("result") |
326 | - for key, val in self.result.items(): |
327 | - if not isinstance(val, list): |
328 | - val = [val] |
329 | - for item in val: |
330 | - n = node.ownerDocument.createElement(key) |
331 | - n.appendChild(n.ownerDocument.createTextNode(str(item))) |
332 | - result.appendChild(n) |
333 | - |
334 | - node.appendChild(result) |
335 | - |
336 | - def append_result(self, key, value): |
337 | - l = self.result.get(key, []) |
338 | - l.append(value) |
339 | - self.result[key] = l |
340 | - |
341 | - def set_result(self, key, value): |
342 | - self.result[key] = value |
343 | - |
344 | - def append_screenshot(self, screenshot_file=None): |
345 | - _logFile = "%s/screenshot-%s.png" % (SCREENSHOTS_SHARE, |
346 | - strftime ("%m-%d-%Y-%H-%M-%s")) |
347 | - safe_make_directory(SCREENSHOTS_SHARE) |
348 | - if screenshot_file is None: |
349 | - ldtputils.imagecapture(outFile = _logFile) |
350 | - else: |
351 | - move(screenshot_file, _logFile) |
352 | - self.append_result('screenshot', _logFile) |
353 | - |
354 | - |
355 | -class TestCaseRunner(TestRunner): |
356 | - def __init__(self, obj, xml_node): |
357 | - TestRunner.__init__(self) |
358 | - self.xml_node = xml_node |
359 | - self.name = xml_node.getAttribute('name') |
360 | - self.test_func = getattr( |
361 | - obj, xml_node.getElementsByTagName('method')[0].firstChild.data) |
362 | - args_element = xml_node.getElementsByTagName('args') |
363 | - if args_element: |
364 | - self.get_args(args_element[0]) |
365 | - |
366 | - def run(self, logger): |
367 | - starttime = time() |
368 | - try: |
369 | - rv = self.test_func(**self.args) |
370 | - except AssertionError, e: |
371 | - # The test failed. |
372 | - if len(e.args) > 1: |
373 | - self.append_result('message', e.args[0]) |
374 | - self.append_screenshot(e.args[1]) |
375 | - else: |
376 | - self.append_result('message', str(e)) |
377 | - self.append_screenshot() |
378 | - self.append_result('stacktrace', traceback.format_exc()) |
379 | - self.set_result('pass', 0) |
380 | - except Exception, e: |
381 | - # There was an unrelated error. |
382 | - logging.warning(traceback.format_exc()) |
383 | - if len(e.args) > 1: |
384 | - self.append_result('message', e.args[0]) |
385 | - self.append_screenshot(e.args[1]) |
386 | - else: |
387 | - self.append_result('message', str(e)) |
388 | - self.append_screenshot() |
389 | - self.append_result('stacktrace', traceback.format_exc()) |
390 | - self.set_result('error', 1) |
391 | - else: |
392 | - self.set_result('pass', 1) |
393 | - try: |
394 | - message, screenshot = rv |
395 | - except: |
396 | - pass |
397 | - else: |
398 | - if message: |
399 | - self.append_result('message', message) |
400 | - if screenshot: |
401 | - self.append_screenshot(screenshot) |
402 | - finally: |
403 | - self.set_result('time', time() - starttime) |
404 | - |
405 | - self.add_results_to_node(self.xml_node) |
406 | - |
407 | -class TestSuiteRunner(TestRunner): |
408 | - def __init__(self, suite_file, loggerclass=None): |
409 | - TestRunner.__init__(self) |
410 | - self.dom = xml.dom.minidom.parse(suite_file) |
411 | - self._strip_whitespace(self.dom.documentElement) |
412 | - clsname, modname = None, None |
413 | - case_runners = [] |
414 | - for node in self.dom.documentElement.childNodes: |
415 | - if node.nodeType != node.ELEMENT_NODE: |
416 | - continue |
417 | - if node.tagName == 'class': |
418 | - modname, clsname = node.firstChild.data.rsplit('.', 1) |
419 | - elif node.tagName == 'case': |
420 | - logging.debug("Adding case %s to current test suite.", |
421 | - node.getAttribute("name")) |
422 | - case_runners.append(node) |
423 | - elif node.tagName == 'args': |
424 | - self.get_args(node) |
425 | - if None in (clsname, modname): |
426 | - raise Exception, "Missing a suite class" |
427 | - |
428 | - # Suite file and module are suppose to be in the same directory |
429 | - sys.path.insert(1, os.path.dirname(suite_file)) |
430 | - logging.debug("Modname: %s", modname) |
431 | - mod = __import__(modname) |
432 | - sys.path.pop(1) |
433 | - |
434 | - cls = getattr(mod, clsname) |
435 | - |
436 | - self.testsuite = cls(**self.args) |
437 | - |
438 | - self.case_runners = \ |
439 | - [TestCaseRunner(self.testsuite, c) for c in case_runners] |
440 | - |
441 | - def run(self, loggerclass=None, setup_once=True, cases=None): |
442 | - try: |
443 | - self._run(loggerclass, setup_once, cases) |
444 | - except Exception, e: |
445 | - logging.warning(traceback.format_exc()) |
446 | - # There was an unrelated error. |
447 | - self.append_result('message', str(e)) |
448 | - self.append_result('stacktrace', traceback.format_exc()) |
449 | - self.set_result('error', 1) |
450 | - try: |
451 | - self.testsuite.teardown() |
452 | - except: |
453 | - pass |
454 | - |
455 | - self.add_results_to_node(self.dom.documentElement) |
456 | - |
457 | - return self.dom.toprettyxml(' ') |
458 | - |
459 | - def _run(self, loggerclass, setup_once, cases): |
460 | - if loggerclass: |
461 | - logger = loggerclass() |
462 | - else: |
463 | - logger = ldtp |
464 | - |
465 | - if setup_once: |
466 | - # Set up the environment. |
467 | - self.testsuite.setup() |
468 | - |
469 | - firsttest = True |
470 | - for testcase in self.case_runners: |
471 | - if cases and testcase.name not in cases: |
472 | - continue |
473 | - if not setup_once: |
474 | - # Set up the app for each test, if requested. |
475 | - self.testsuite.setup() |
476 | - if not firsttest: |
477 | - # Clean up from previous run. |
478 | - self.testsuite.cleanup() |
479 | - firsttest = False |
480 | - testcase.run(logger) |
481 | - if not setup_once: |
482 | - # Teardown upthe app for each test, if requested. |
483 | - self.testsuite.teardown() |
484 | - |
485 | - if setup_once: |
486 | - # Tear down after entire suite. |
487 | - self.testsuite.teardown() |
488 | - |
489 | - def _strip_whitespace(self, element): |
490 | - if element is None: |
491 | - return |
492 | - sibling = element.firstChild |
493 | - while sibling: |
494 | - nextSibling = sibling.nextSibling |
495 | - if sibling.nodeType == sibling.TEXT_NODE: |
496 | - stripped = sibling.data.strip() |
497 | - if stripped == '': |
498 | - element.removeChild(sibling) |
499 | - else: |
500 | - sibling.data = stripped |
501 | - else: |
502 | - self._strip_whitespace(sibling) |
503 | - sibling = nextSibling |
504 | +import sys, os |
505 | +from desktoptesting.cmd.main import main |
506 | |
507 | if __name__ == "__main__": |
508 | - sys.exit(main()) |
509 | + # Pass complete file name as program name |
510 | + args = sys.argv |
511 | + args[0] = __file__ |
512 | + sys.exit(main(args)) |
513 | |
514 | === added directory 'desktoptesting/cmd' |
515 | === added file 'desktoptesting/cmd/__init__.py' |
516 | === added file 'desktoptesting/cmd/discovery.py' |
517 | --- desktoptesting/cmd/discovery.py 1970-01-01 00:00:00 +0000 |
518 | +++ desktoptesting/cmd/discovery.py 2009-06-04 04:18:42 +0000 |
519 | @@ -0,0 +1,226 @@ |
520 | +""" |
521 | +This module contains the functionality related to the discovery of the test suites |
522 | +""" |
523 | +import os, re, logging |
524 | +import xml.etree.ElementTree as etree |
525 | + |
526 | + |
527 | +class Application: |
528 | + """ |
529 | + Application description data |
530 | + """ |
531 | + name_pattern = r"[a-z0-9][-_a-z0-9+.]*" |
532 | + name_regex = re.compile(name_pattern) |
533 | + whitelist = None |
534 | + |
535 | + def __init__(self, path, filenames): |
536 | + self.path = path |
537 | + self.filenames = filenames |
538 | + |
539 | + self.name = os.path.basename(path) |
540 | + |
541 | + |
542 | + def __eq__(self, other): |
543 | + """ |
544 | + Two applications are considered to be equal if they have the |
545 | + same name |
546 | + """ |
547 | + return (type(self) == type(other) |
548 | + and self.name == other.name) |
549 | + |
550 | + |
551 | + def name_matches(self): |
552 | + """ |
553 | + Return True if the application name |
554 | + honors the expected pattern |
555 | + """ |
556 | + return self.name_regex.match(self.name) |
557 | + |
558 | + |
559 | + def suites(self): |
560 | + """ |
561 | + Return a generator for all suites |
562 | + """ |
563 | + return Suite.discover(self) |
564 | + |
565 | + |
566 | + @classmethod |
567 | + def discover(cls, base_dirpaths): |
568 | + """ |
569 | + Generator that discovers all applications under |
570 | + a list of top directories |
571 | + """ |
572 | + discovered_applications = [] |
573 | + |
574 | + for base_dirpath in base_dirpaths: |
575 | + for dirpath, dirnames, filenames in os.walk(base_dirpath): |
576 | + # Application directories are expected to honor |
577 | + # the specified name pattern |
578 | + app = cls(dirpath, filenames) |
579 | + if not app.name_matches(): |
580 | + logging.debug("Application name %s does not match pattern: %s" |
581 | + % (app.name, app.name_pattern)) |
582 | + continue |
583 | + |
584 | + # This check makes sure that the same application |
585 | + # isn't discovered twice. That is to say, when discovering |
586 | + # applications from multiple directories, the test cases |
587 | + # from the application that is first found will be |
588 | + # executed while the others will be discarded |
589 | + if app in discovered_applications: |
590 | + logging.debug("Application name %s has been already discovered" |
591 | + % app.name) |
592 | + continue |
593 | + |
594 | + # Return application only if there is no whitelist |
595 | + # or if it matches any of the whitelist names |
596 | + if cls.whitelist and not app.name in cls.whitelist: |
597 | + logging.debug("Application name %s has not been whitelisted" |
598 | + % app.name) |
599 | + continue |
600 | + |
601 | + # At least one '.xml' file with a 'suite' root tag |
602 | + # should be contained in the directory to be a valid application directory |
603 | + if not any(app.suites()): |
604 | + logging.debug("Application directory %s does't seem to contain a valid suite file" |
605 | + % app.path) |
606 | + continue |
607 | + |
608 | + discovered_applications.append(app) |
609 | + yield app |
610 | + |
611 | + |
612 | +class Suite: |
613 | + """ |
614 | + Suite description data |
615 | + """ |
616 | + whitelist = None |
617 | + |
618 | + def __init__(self, application, filename): |
619 | + self.application = application |
620 | + self.filename = filename |
621 | + self.fullname = os.path.join(application.path, |
622 | + filename) |
623 | + |
624 | + try: |
625 | + self.tree = etree.parse(self.fullname) |
626 | + except: |
627 | + self.tree = None |
628 | + |
629 | + |
630 | + @property |
631 | + def name(self): |
632 | + """ |
633 | + Return suite name as written in the xml file |
634 | + """ |
635 | + return self.tree.getroot().attrib['name'] |
636 | + |
637 | + |
638 | + def cases(self): |
639 | + """ |
640 | + Generator for all test cases in the suite |
641 | + """ |
642 | + return Case.discover(self) |
643 | + |
644 | + |
645 | + def __eq__(self, other): |
646 | + """ |
647 | + A suite is compared against its filename |
648 | + or against its own name (useful for filtering) |
649 | + """ |
650 | + if type(other) == str: |
651 | + other_name, other_ext = os.path.splitext(other) |
652 | + |
653 | + if other_ext: |
654 | + return other == self.filename |
655 | + else: |
656 | + return other_name == os.path.splitext(self.filename)[0] |
657 | + else: |
658 | + return self.id == other.id |
659 | + |
660 | + |
661 | + def has_valid_xml(self): |
662 | + """ |
663 | + Return true if xml could be parsed |
664 | + and the root tag is 'suite' |
665 | + """ |
666 | + return (self.tree |
667 | + and self.tree.getroot().tag == 'suite') |
668 | + |
669 | + |
670 | + @classmethod |
671 | + def discover(cls, app): |
672 | + """ |
673 | + Discover suites inside of an application |
674 | + """ |
675 | + # All test suites will be defined by an xml file |
676 | + xml_filenames = (filename |
677 | + for filename in app.filenames |
678 | + if filename.endswith('xml')) |
679 | + |
680 | + # Discovered suites must contain a valid xml content |
681 | + # and at least one test cases |
682 | + suites = (suite for suite in (cls(app, filename) |
683 | + for filename in xml_filenames) |
684 | + if suite.has_valid_xml() and any(suite.cases())) |
685 | + |
686 | + |
687 | + # Filter suites using the whitelist provide through the |
688 | + # command line |
689 | + if cls.whitelist: |
690 | + suites = (suite for suite in suites |
691 | + if suite in cls.whitelist) |
692 | + |
693 | + return suites |
694 | + |
695 | + |
696 | +class Case: |
697 | + """ |
698 | + Test case description data |
699 | + """ |
700 | + whitelist = None |
701 | + |
702 | + def __init__(self, suite, case_tag): |
703 | + self.suite = suite |
704 | + self.case_tag = case_tag |
705 | + |
706 | + |
707 | + @property |
708 | + def name(self): |
709 | + """ |
710 | + Return test case name |
711 | + """ |
712 | + return self.case_tag.attrib['name'] |
713 | + |
714 | + |
715 | + @classmethod |
716 | + def discover(cls, suite): |
717 | + """ |
718 | + Discover all test cases in a suite |
719 | + """ |
720 | + cases = (cls(suite, case_tag) |
721 | + for case_tag in suite.tree.findall('case')) |
722 | + |
723 | + if cls.whitelist: |
724 | + cases = (case for case in cases |
725 | + if case.name in cls.whitelist) |
726 | + |
727 | + return cases |
728 | + |
729 | + |
730 | +def discover_applications(top_directories, |
731 | + filtering_applications, |
732 | + filtering_suites, |
733 | + filtering_cases): |
734 | + """ |
735 | + Discover all applications and filter them properly |
736 | + """ |
737 | + # Configure filtering options |
738 | + Application.whitelist = filtering_applications |
739 | + Suite.whitelist = filtering_suites |
740 | + Case.whitelist = filtering_cases |
741 | + |
742 | + # Discover all applications under top directories |
743 | + discovered_apps = Application.discover(top_directories) |
744 | + |
745 | + return discovered_apps |
746 | |
747 | === added file 'desktoptesting/cmd/globals.py' |
748 | --- desktoptesting/cmd/globals.py 1970-01-01 00:00:00 +0000 |
749 | +++ desktoptesting/cmd/globals.py 2009-06-03 12:29:19 +0000 |
750 | @@ -0,0 +1,6 @@ |
751 | +""" |
752 | +This module just provides a namespace for global variables used across |
753 | +multiple modules in the package |
754 | +""" |
755 | +TESTS_SHARE = "share/ubuntu-desktop-tests" |
756 | +SCREENSHOTS_SHARE = "/tmp/ldtp-screenshots" |
757 | |
758 | === added file 'desktoptesting/cmd/main.py' |
759 | --- desktoptesting/cmd/main.py 1970-01-01 00:00:00 +0000 |
760 | +++ desktoptesting/cmd/main.py 2009-06-04 04:15:25 +0000 |
761 | @@ -0,0 +1,136 @@ |
762 | +""" |
763 | +This module provides the main entry point for the execution automated |
764 | +desktop test cases |
765 | +""" |
766 | + |
767 | +import os, re, sys, logging |
768 | +from logging import StreamHandler, FileHandler, Formatter |
769 | +from itertools import chain |
770 | +import ldtp |
771 | + |
772 | +from . import globals |
773 | +from .runner import TestSuiteRunner |
774 | +from .parser import parse_options |
775 | +from .utils import safe_make_directory, safe_run_command |
776 | +from .discovery import discover_applications |
777 | + |
778 | + |
779 | +def run_suite_file(suite_file, log_file, cases=None): |
780 | + logging.info("Running %s" % suite_file) |
781 | + runner = TestSuiteRunner(suite_file) |
782 | + results = runner.run(cases=cases) |
783 | + f = open(log_file, 'w') |
784 | + f.write(results) |
785 | + f.close() |
786 | + |
787 | + |
788 | +def convert_log_file(log_file, target_directory): |
789 | + if os.path.exists(globals.SCREENSHOTS_SHARE): |
790 | + screenshot_dir = os.path.dirname(log_file) + "/screenshots" |
791 | + |
792 | + if not os.path.exists(screenshot_dir): |
793 | + safe_make_directory(screenshot_dir) |
794 | + |
795 | + if len(os.listdir(globals.SCREENSHOTS_SHARE)) > 0: |
796 | + command = "mv " + globals.SCREENSHOTS_SHARE + "/* " + screenshot_dir |
797 | + safe_run_command(command) |
798 | + |
799 | + log_file_tmp = log_file + ".tmp" |
800 | + o = open(log_file_tmp, "w") |
801 | + data = open(log_file).read() |
802 | + o.write(re.sub(globals.SCREENSHOTS_SHARE, "screenshots", data)) |
803 | + o.flush() |
804 | + o.close() |
805 | + |
806 | + os.remove(log_file) |
807 | + os.rename(log_file_tmp, log_file) |
808 | + |
809 | + xsl_file = os.path.join(globals.TESTS_SHARE, "report.xsl") |
810 | + if not os.path.exists(xsl_file): |
811 | + logging.error("XSL file `%s' does not exist." % xsl_file) |
812 | + sys.exit(1) |
813 | + |
814 | + html_file = log_file.replace(".log", ".html") |
815 | + |
816 | + command = "xsltproc -o %s %s %s" \ |
817 | + % (html_file, xsl_file, log_file) |
818 | + safe_run_command(command) |
819 | + |
820 | + |
821 | +def process_suite_file(suite_file, target_directory, cases=None): |
822 | + application_name = os.path.basename(os.path.dirname(suite_file)) |
823 | + application_target = os.path.join(target_directory, application_name) |
824 | + safe_make_directory(application_target) |
825 | + |
826 | + suite_name = os.path.basename(suite_file) |
827 | + log_file = os.path.join(application_target, |
828 | + suite_name.replace(".xml", ".log")) |
829 | + run_suite_file(suite_file, log_file, cases) |
830 | + convert_log_file(log_file, target_directory) |
831 | + |
832 | + |
833 | +def configure_logging(log_level_str, log_filename): |
834 | + """ |
835 | + Configure log handlers |
836 | + """ |
837 | + log_level = logging.getLevelName(log_level_str.upper()) |
838 | + log_handlers = [] |
839 | + log_handlers.append(StreamHandler()) |
840 | + if log_filename: |
841 | + log_handlers.append(FileHandler(log_filename)) |
842 | + |
843 | + format = ("%(asctime)s %(levelname)-8s %(message)s") |
844 | + if log_handlers: |
845 | + for handler in log_handlers: |
846 | + handler.setFormatter(Formatter(format)) |
847 | + logging.getLogger().addHandler(handler) |
848 | + if log_level: |
849 | + logging.getLogger().setLevel(log_level) |
850 | + elif not logging.getLogger().handlers: |
851 | + logging.disable(logging.CRITICAL) |
852 | + |
853 | + |
854 | +def main(args=sys.argv): |
855 | + """ |
856 | + Execute automated tests |
857 | + """ |
858 | + os.environ['NO_GAIL'] = '1' |
859 | + os.environ['NO_AT_BRIDGE'] = '1' |
860 | + |
861 | + # For the moment, all applications should be running in English |
862 | + ldtp.setlocale("C") |
863 | + |
864 | + # Get shared directory based on the directory in which binary is located |
865 | + globals.TESTS_SHARE = os.path.realpath(os.path.join(os.path.dirname(args[0]), |
866 | + os.path.pardir, |
867 | + globals.TESTS_SHARE)) |
868 | + |
869 | + options = parse_options(args) |
870 | + configure_logging(options.log_level, options.log) |
871 | + |
872 | + apps = discover_applications(options.directories, |
873 | + options.applications, |
874 | + options.suites, |
875 | + options.cases) |
876 | + |
877 | + # Execute test cases |
878 | + if not apps: |
879 | + logging.warning("No test applications found") |
880 | + else: |
881 | + if not options.info: |
882 | + suites = chain.from_iterable(app.suites() for app in apps) |
883 | + for suite in suites: |
884 | + process_suite_file(suite.fullname, options.target, options.cases) |
885 | + else: |
886 | + for app in apps: |
887 | + print "Application: %s" % app.name |
888 | + for suite in app.suites(): |
889 | + print "- Suite: %s" % suite.name |
890 | + for case in suite.cases(): |
891 | + print " - Case: %s" % case.name |
892 | + |
893 | + return 0 |
894 | + |
895 | + |
896 | +if __name__ == "__main__": |
897 | + sys.exit(main()) |
898 | |
899 | === added file 'desktoptesting/cmd/parser.py' |
900 | --- desktoptesting/cmd/parser.py 1970-01-01 00:00:00 +0000 |
901 | +++ desktoptesting/cmd/parser.py 2009-06-04 04:15:25 +0000 |
902 | @@ -0,0 +1,118 @@ |
903 | +""" |
904 | +This module contains the code needed to parse all the options |
905 | +""" |
906 | +import os |
907 | +from copy import copy |
908 | +from optparse import OptionParser, OptionGroup, OptionValueError |
909 | +from optparse import Option as OldOption |
910 | + |
911 | +from . import globals |
912 | + |
913 | +def check_dir(option, opt, value): |
914 | + """ |
915 | + Check if an dir option string contains a real directory |
916 | + """ |
917 | + value = os.path.realpath(os.path.expanduser(value)) |
918 | + if not os.path.isdir(value): |
919 | + raise OptionValueError("option %s: " |
920 | + "Directory '%s' doesn't exist" |
921 | + % (opt, value)) |
922 | + return value |
923 | + |
924 | + |
925 | +def check_dirname(option, opt, value): |
926 | + """ |
927 | + Check if an dirname option string contains a valid directory name |
928 | + """ |
929 | + value = os.path.realpath(os.path.expanduser(value)) |
930 | + if os.path.exists(value) and not os.path.isdir(value): |
931 | + raise OptionValueError("option %s: " |
932 | + "Directory '%s' exists, but isn't a directory" |
933 | + % (opt, value)) |
934 | + return value |
935 | + |
936 | +class Option(OldOption): |
937 | + """ |
938 | + Extended option class that adds two directory types: |
939 | + - dirname: Valid directory name that isn't required to exist |
940 | + - dir: Valid directory name that must exist |
941 | + """ |
942 | + TYPES = OldOption.TYPES + ("dir", "dirname") |
943 | + TYPE_CHECKER = copy(OldOption.TYPE_CHECKER) |
944 | + TYPE_CHECKER["dir"] = check_dir |
945 | + TYPE_CHECKER["dirname"] = check_dirname |
946 | + |
947 | + |
948 | +def parse_options(args): |
949 | + """ |
950 | + Parse options passed through the command line |
951 | + """ |
952 | + default_target = "~/.ubuntu-desktop-test" |
953 | + default_log_level = "critical" |
954 | + default_directories = [os.getcwd(), globals.TESTS_SHARE] |
955 | + |
956 | + parser = OptionParser(description="Execute automated tests", |
957 | + option_class=Option) |
958 | + |
959 | + parser.add_option('-i', '--info', |
960 | + action="store_true", |
961 | + help=("Display information about test cases " |
962 | + "without executing them")) |
963 | + |
964 | + group = OptionGroup(parser, "Test selection options") |
965 | + group.add_option("-d", "--directory", |
966 | + dest="directories", |
967 | + # Default directories are used only if no directory |
968 | + # was passed through the command line |
969 | + default = [], |
970 | + metavar="DIR", |
971 | + action="append", |
972 | + type="dir", |
973 | + help=("Application directory locations. Option can be repeated " |
974 | + "and defaults to both shared test directory " |
975 | + "and current working directory")) |
976 | + group.add_option("-a", "--application", |
977 | + dest="applications", |
978 | + action="append", |
979 | + type="string", |
980 | + default=None, |
981 | + help=("Application name to test. Option can be repeated " |
982 | + "and defaults to all applications")) |
983 | + group.add_option("-s", "--suite", |
984 | + dest="suites", |
985 | + metavar="SUITE", |
986 | + action="append", |
987 | + type="string", |
988 | + default=None, |
989 | + help=("Suite name to test within applications. Option " |
990 | + "can be repeated and default to all suites")) |
991 | + group.add_option("-c", "--case", |
992 | + dest="cases", |
993 | + metavar="CASE", |
994 | + action="append", |
995 | + type="string", |
996 | + default=None, |
997 | + help="Test cases to run (all, if not specified).") |
998 | + parser.add_option_group(group) |
999 | + |
1000 | + group = OptionGroup(parser, "Logging options") |
1001 | + group.add_option("-l", "--log", |
1002 | + metavar="FILE", |
1003 | + help="The file to write the log to.") |
1004 | + group.add_option("--log-level", |
1005 | + default=default_log_level, |
1006 | + help="One of debug, info, warning, error or critical.") |
1007 | + group.add_option("-t", "--target", |
1008 | + metavar="DIR", |
1009 | + type="dirname", |
1010 | + default=default_target, |
1011 | + help=("Target directory for logs and reports. Defaults " |
1012 | + "to: %default")) |
1013 | + parser.add_option_group(group) |
1014 | + |
1015 | + (options, args) = parser.parse_args(args[1:]) |
1016 | + |
1017 | + if not options.directories: |
1018 | + options.directories = default_directories |
1019 | + |
1020 | + return options |
1021 | |
1022 | === added file 'desktoptesting/cmd/runner.py' |
1023 | --- desktoptesting/cmd/runner.py 1970-01-01 00:00:00 +0000 |
1024 | +++ desktoptesting/cmd/runner.py 2009-06-03 16:35:09 +0000 |
1025 | @@ -0,0 +1,206 @@ |
1026 | +""" |
1027 | +This module contains different kinds of runner classes needed to |
1028 | +execute test cases |
1029 | +""" |
1030 | +import os, sys, traceback, logging |
1031 | +import xml.dom.minidom |
1032 | +import ldtp, ldtputils |
1033 | +from time import time, gmtime, strftime |
1034 | +from shutil import move |
1035 | + |
1036 | +from . import globals |
1037 | +from .utils import safe_make_directory |
1038 | + |
1039 | +class TestRunner: |
1040 | + def __init__(self): |
1041 | + self.result = {} |
1042 | + self.args = {} |
1043 | + |
1044 | + def get_args(self, node): |
1045 | + for n in node.childNodes: |
1046 | + if n.nodeType != n.ELEMENT_NODE or not n.hasChildNodes(): |
1047 | + continue |
1048 | + self.args[n.tagName.encode('ascii')] = n.firstChild.data |
1049 | + |
1050 | + def add_results_to_node(self, node): |
1051 | + if self.result == {}: return |
1052 | + result = node.ownerDocument.createElement("result") |
1053 | + for key, val in self.result.items(): |
1054 | + if not isinstance(val, list): |
1055 | + val = [val] |
1056 | + for item in val: |
1057 | + n = node.ownerDocument.createElement(key) |
1058 | + n.appendChild(n.ownerDocument.createTextNode(str(item))) |
1059 | + result.appendChild(n) |
1060 | + |
1061 | + node.appendChild(result) |
1062 | + |
1063 | + def append_result(self, key, value): |
1064 | + l = self.result.get(key, []) |
1065 | + l.append(value) |
1066 | + self.result[key] = l |
1067 | + |
1068 | + def set_result(self, key, value): |
1069 | + self.result[key] = value |
1070 | + |
1071 | + def append_screenshot(self, screenshot_file=None): |
1072 | + _logFile = "%s/screenshot-%s.png" % (globals.SCREENSHOTS_SHARE, |
1073 | + strftime ("%m-%d-%Y-%H-%M-%s")) |
1074 | + safe_make_directory(globals.SCREENSHOTS_SHARE) |
1075 | + if not screenshot_file: |
1076 | + ldtputils.imagecapture(outFile = _logFile) |
1077 | + else: |
1078 | + move(screenshot_file, _logFile) |
1079 | + self.append_result('screenshot', _logFile) |
1080 | + |
1081 | + |
1082 | +class TestCaseRunner(TestRunner): |
1083 | + def __init__(self, obj, xml_node): |
1084 | + TestRunner.__init__(self) |
1085 | + self.xml_node = xml_node |
1086 | + self.name = xml_node.getAttribute('name') |
1087 | + self.test_func = getattr( |
1088 | + obj, xml_node.getElementsByTagName('method')[0].firstChild.data) |
1089 | + args_element = xml_node.getElementsByTagName('args') |
1090 | + if args_element: |
1091 | + self.get_args(args_element[0]) |
1092 | + |
1093 | + def run(self, logger): |
1094 | + starttime = time() |
1095 | + try: |
1096 | + rv = self.test_func(**self.args) |
1097 | + except AssertionError, e: |
1098 | + # The test failed. |
1099 | + if len(e.args) > 1: |
1100 | + self.append_result('message', e.args[0]) |
1101 | + self.append_screenshot(e.args[1]) |
1102 | + else: |
1103 | + self.append_result('message', str(e)) |
1104 | + self.append_screenshot() |
1105 | + self.append_result('stacktrace', traceback.format_exc()) |
1106 | + self.set_result('pass', 0) |
1107 | + except Exception, e: |
1108 | + # There was an unrelated error. |
1109 | + logging.warning(traceback.format_exc()) |
1110 | + if len(e.args) > 1: |
1111 | + self.append_result('message', e.args[0]) |
1112 | + self.append_screenshot(e.args[1]) |
1113 | + else: |
1114 | + self.append_result('message', str(e)) |
1115 | + self.append_screenshot() |
1116 | + self.append_result('stacktrace', traceback.format_exc()) |
1117 | + self.set_result('error', 1) |
1118 | + else: |
1119 | + self.set_result('pass', 1) |
1120 | + try: |
1121 | + message, screenshot = rv |
1122 | + except: |
1123 | + pass |
1124 | + else: |
1125 | + if message: |
1126 | + self.append_result('message', message) |
1127 | + if screenshot: |
1128 | + self.append_screenshot(screenshot) |
1129 | + finally: |
1130 | + self.set_result('time', time() - starttime) |
1131 | + |
1132 | + self.add_results_to_node(self.xml_node) |
1133 | + |
1134 | + |
1135 | +class TestSuiteRunner(TestRunner): |
1136 | + def __init__(self, suite_file, loggerclass=None): |
1137 | + TestRunner.__init__(self) |
1138 | + self.dom = xml.dom.minidom.parse(suite_file) |
1139 | + self._strip_whitespace(self.dom.documentElement) |
1140 | + clsname, modname = None, None |
1141 | + case_runners = [] |
1142 | + for node in self.dom.documentElement.childNodes: |
1143 | + if node.nodeType != node.ELEMENT_NODE: |
1144 | + continue |
1145 | + if node.tagName == 'class': |
1146 | + modname, clsname = node.firstChild.data.rsplit('.', 1) |
1147 | + elif node.tagName == 'case': |
1148 | + logging.debug("Adding case %s to current test suite.", |
1149 | + node.getAttribute("name")) |
1150 | + case_runners.append(node) |
1151 | + elif node.tagName == 'args': |
1152 | + self.get_args(node) |
1153 | + if None in (clsname, modname): |
1154 | + raise Exception, "Missing a suite class" |
1155 | + |
1156 | + # Suite file and module are suppose to be in the same directory |
1157 | + sys.path.insert(1, os.path.dirname(suite_file)) |
1158 | + logging.debug("Modname: %s", modname) |
1159 | + mod = __import__(modname) |
1160 | + sys.path.pop(1) |
1161 | + |
1162 | + cls = getattr(mod, clsname) |
1163 | + |
1164 | + self.testsuite = cls(**self.args) |
1165 | + |
1166 | + self.case_runners = \ |
1167 | + [TestCaseRunner(self.testsuite, c) for c in case_runners] |
1168 | + |
1169 | + def run(self, loggerclass=None, setup_once=True, cases=None): |
1170 | + try: |
1171 | + self._run(loggerclass, setup_once, cases) |
1172 | + except Exception, e: |
1173 | + logging.warning(traceback.format_exc()) |
1174 | + # There was an unrelated error. |
1175 | + self.append_result('message', str(e)) |
1176 | + self.append_result('stacktrace', traceback.format_exc()) |
1177 | + self.set_result('error', 1) |
1178 | + try: |
1179 | + self.testsuite.teardown() |
1180 | + except: |
1181 | + pass |
1182 | + |
1183 | + self.add_results_to_node(self.dom.documentElement) |
1184 | + |
1185 | + return self.dom.toprettyxml(' ') |
1186 | + |
1187 | + def _run(self, loggerclass, setup_once, cases): |
1188 | + if loggerclass: |
1189 | + logger = loggerclass() |
1190 | + else: |
1191 | + logger = ldtp |
1192 | + |
1193 | + if setup_once: |
1194 | + # Set up the environment. |
1195 | + self.testsuite.setup() |
1196 | + |
1197 | + firsttest = True |
1198 | + for testcase in self.case_runners: |
1199 | + if cases and testcase.name not in cases: |
1200 | + continue |
1201 | + if not setup_once: |
1202 | + # Set up the app for each test, if requested. |
1203 | + self.testsuite.setup() |
1204 | + if not firsttest: |
1205 | + # Clean up from previous run. |
1206 | + self.testsuite.cleanup() |
1207 | + firsttest = False |
1208 | + testcase.run(logger) |
1209 | + if not setup_once: |
1210 | + # Teardown upthe app for each test, if requested. |
1211 | + self.testsuite.teardown() |
1212 | + |
1213 | + if setup_once: |
1214 | + # Tear down after entire suite. |
1215 | + self.testsuite.teardown() |
1216 | + |
1217 | + def _strip_whitespace(self, element): |
1218 | + if not element: |
1219 | + return |
1220 | + sibling = element.firstChild |
1221 | + while sibling: |
1222 | + nextSibling = sibling.nextSibling |
1223 | + if sibling.nodeType == sibling.TEXT_NODE: |
1224 | + stripped = sibling.data.strip() |
1225 | + if stripped == '': |
1226 | + element.removeChild(sibling) |
1227 | + else: |
1228 | + sibling.data = stripped |
1229 | + else: |
1230 | + self._strip_whitespace(sibling) |
1231 | + sibling = nextSibling |
1232 | |
1233 | === added file 'desktoptesting/cmd/utils.py' |
1234 | --- desktoptesting/cmd/utils.py 1970-01-01 00:00:00 +0000 |
1235 | +++ desktoptesting/cmd/utils.py 2009-06-03 10:12:35 +0000 |
1236 | @@ -0,0 +1,38 @@ |
1237 | +""" |
1238 | +This module contains utility functions for file management and command |
1239 | +execution |
1240 | +""" |
1241 | +import os, logging |
1242 | +from stat import ST_MODE, S_IMODE |
1243 | +from subprocess import Popen, PIPE |
1244 | + |
1245 | +def safe_change_mode(path, mode): |
1246 | + if not os.path.exists(path): |
1247 | + logging.error("Path does not exist: %s" % path) |
1248 | + sys.exit(1) |
1249 | + |
1250 | + old_mode = os.stat(path)[ST_MODE] |
1251 | + if mode != S_IMODE(old_mode): |
1252 | + os.chmod(path, mode) |
1253 | + |
1254 | + |
1255 | +def safe_make_directory(path, mode=0755): |
1256 | + if os.path.exists(path): |
1257 | + if not os.path.isdir(path): |
1258 | + logging.error("Path is not a directory: %s" % path) |
1259 | + sys.exit(1) |
1260 | + |
1261 | + safe_change_mode(path, mode) |
1262 | + else: |
1263 | + logging.debug("Creating directory: %s" % path) |
1264 | + os.makedirs(path, mode) |
1265 | + |
1266 | + |
1267 | +def safe_run_command(command): |
1268 | + logging.debug("Running command: %s" % command) |
1269 | + p = Popen(command, stdout=PIPE, shell=True) |
1270 | + (pid, status) = os.waitpid(p.pid, 0) |
1271 | + if status: |
1272 | + logging.error("Command failed: %s" % command) |
1273 | + |
1274 | + return p.stdout.read() |
1275 | |
1276 | === modified file 'gedit/gedit_chains.py' |
1277 | --- gedit/gedit_chains.py 2009-04-21 10:15:09 +0000 |
1278 | +++ gedit/gedit_chains.py 2009-06-03 09:41:56 +0000 |
1279 | @@ -1,4 +1,5 @@ |
1280 | # -*- coding: utf-8 -*- |
1281 | +import os |
1282 | from time import time, gmtime, strftime |
1283 | |
1284 | from desktoptesting.test_suite.gnome import GEditTestSuite |
1285 | @@ -12,6 +13,11 @@ |
1286 | self.application.write_text(chain) |
1287 | self.application.save(test_file) |
1288 | |
1289 | + # oracle file path is assumed to be relative |
1290 | + # to test case code |
1291 | + oracle = os.path.join(os.path.dirname(__file__), |
1292 | + oracle) |
1293 | + |
1294 | testcheck = FileComparison(oracle, test_file) |
1295 | |
1296 | if testcheck.perform_test() == FAIL: |
1297 | |
1298 | === modified file 'gedit/gedit_chains.xml' |
1299 | --- gedit/gedit_chains.xml 2009-03-11 16:35:00 +0000 |
1300 | +++ gedit/gedit_chains.xml 2009-06-03 09:41:56 +0000 |
1301 | @@ -8,7 +8,7 @@ |
1302 | <method>testChain</method> |
1303 | <description>Test Unicode text saving.</description> |
1304 | <args> |
1305 | - <oracle>./gedit/data/utf8.txt</oracle> |
1306 | + <oracle>data/utf8.txt</oracle> |
1307 | <chain>This is a japanese string: 広告掲載 - ビジネス</chain> |
1308 | </args> |
1309 | </case> |
1310 | @@ -16,7 +16,7 @@ |
1311 | <method>testChain</method> |
1312 | <description>Test ASCII text saving.</description> |
1313 | <args> |
1314 | - <oracle>./gedit/data/ascii.txt</oracle> |
1315 | + <oracle>data/ascii.txt</oracle> |
1316 | <chain>This is a very basic string!</chain> |
1317 | </args> |
1318 | </case> |
1319 | |
1320 | === modified file 'setup.py' |
1321 | --- setup.py 2009-05-06 08:00:55 +0000 |
1322 | +++ setup.py 2009-06-03 09:41:56 +0000 |
1323 | @@ -113,7 +113,8 @@ |
1324 | scripts = ["bin/ubuntu-desktop-test"], |
1325 | packages = ["desktoptesting", |
1326 | "desktoptesting.application", |
1327 | - "desktoptesting.test_suite"], |
1328 | + "desktoptesting.test_suite", |
1329 | + "desktoptesting.cmd"], |
1330 | cmdclass = { |
1331 | "install_data": testing_install_data, |
1332 | "install_scripts": testing_install_scripts, |
Hello,
This branch is a refactoring of the code in ubuntu-desktop-test into a package called cmd that is part of the desktoptesting package. The code has been splitted in different modules to provide better maintainability.
In addition to this, some effort has been spent in writing new test discovery code. The summary of the changes is the following:
- Added -d, --directory option to append as many directories as needed where the test cases should be found
- Added -i, --info option to just display the name of the test cases that would be executed.
- Removed -f, --file options since it the functionality to filter by suite file is already covered by -s, --suite
As you'll see that the discovery is based on generators which means that it's being performed on demand (i.e. test cases aren't discovered until they are going to be executed) and that the time that it takes to start executing the first test case should be reduced (specially in an installation with a big set of test cases). I believe that other test runners such as nose are written using this design.
There's some overlapping between the process_suite_file function and the Case class (in discovery module) since both of them perform test case filtering. However, I expect to improve the integration of these two parts of code in the future since changing also that part would have made the change much bigger than I expected.
I hope you find this change useful.
Best regards,
Javier