Merge lp:~javier.collado/mago/suite_discovery into lp:~mago-contributors/mago/mago-1.0

Proposed by Javier Collado
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
Reviewer Review Type Date Requested Status
Joker Wild (community) test Approve
Eitan Isaacson Needs Fixing
Review via email: mp+7055@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Javier Collado (javier.collado) wrote :

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

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

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

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

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

Revision history for this message
Eitan Isaacson (eeejay) wrote :

I like it.. and need it.

review: Approve
Revision history for this message
Eitan Isaacson (eeejay) wrote :

Actually, logging seems to be broken. I am not seeing any logging output from safe_run_command()

debugging..

review: Needs Fixing
Revision history for this message
Eitan Isaacson (eeejay) wrote :

Wasn't a logger bug, here is the fix:

=== modified file 'desktoptesting/cmd/discovery.py'
--- desktoptesting/cmd/discovery.py 2009-06-09 07:23:10 +0000
+++ desktoptesting/cmd/discovery.py 2009-06-16 21:21:15 +0000
@@ -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

Revision history for this message
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/eitan/svn/canonical/udt/suite_discovery/share/ubuntu-desktop-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.

Revision history for this message
Joker Wild (lajjr-deactivatedaccount) wrote :

If logging in implemented.

review: Approve (test)
Revision history for this message
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.cmd.discovery.SuiteData instance at 0x8aead4c>
(Pydb) suite.name
'gedit chains'
(Pydb) suite.filename
'gedit_chains.xml'
(Pydb) suite in ['gedit chains']
False
(Pydb) suite in ['gedit_chains.xml']
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/cmd/discovery.py'
> --- desktoptesting/cmd/discovery.py 2009-06-09 07:23:10 +0000
> +++ desktoptesting/cmd/discovery.py 2009-06-16 21:21:15 +0000
> @@ -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

Revision history for this message
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/bin/ubuntu-desktop-test
$ locate report.xsl | grep /usr/local
/usr/local/share/ubuntu-desktop-tests/report.xsl

So what I did is, at first, set TEST_SHARE to (globals.py):
TESTS_SHARE = "share/ubuntu-desktop-tests"

and once the script is called (main.py):
   globals.TESTS_SHARE = os.path.realpath(os.path.join(os.path.dirname(args[0]),
                                                        os.path.pardir,
                                                        globals.TESTS_SHARE))

To make this work, it is needed that arg[0] contains a full path so the following change was needed (ubuntu-desktop-test)
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/eitan/svn/canonical/udt/suite_discovery/share/ubuntu-desktop-
> 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.

Revision history for this message
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/.ubuntu-desktop-test/gedit/gedit_chains.html /usr/local/share/ubuntu-desktop-tests/report.xsl /home/javi/.ubuntu-desktop-test/gedit/gedit_chains.log

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

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

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

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

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

Revision history for this message
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)/share/mago.
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.join(MAGO_SHARE, 'tests')]. It could also be overridden with a colon-delimited environment variable with the same name.

Some pseudo-code:

def _get_share_dir():
  # Return installed share directory path, or current build directory if not installed.

MAGO_SHARE = os.env.get('MAGO_SHARE', _get_share_dir())

if os.env.get('MAGO_PATH', None):
  MAGO_PATH = os.env['MAGO_PATH'].split(':')
else:
  MAGO_PATH = [os.path.curdir, os.path.join(MAGO_SHARE, 'tests')]

Revision history for this message
Eitan Isaacson (eeejay) wrote :

Just implemented in lp:~eeejay/mago/suite_discovery

Please review/merge/comment here.

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

Revision history for this message
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_install_scripts class in setup.py. Please take a look at the last version of the branch and let me know what do you think.

Best regards,
   Javier

> Just implemented in lp:~eeejay/mago/suite_discovery
>
> Please review/merge/comment here.

75. By Javier Collado

Removing code that is no longer needed

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

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

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

Subscribers

People subscribed via source and target branches

to status/vote changes: