From c07f02c7aab422ea4ce5580703a531ee6b4ee3b6 Mon Sep 17 00:00:00 2001 From: Mike Hommey Date: Thu, 25 Jul 2013 14:26:09 +0900 Subject: [PATCH] Backout changeset a13dafd65d1c (bug 895940) for breaking PGO builds on a CLOSED TREE --- testing/mozbase/README.md | 2 +- testing/mozbase/manifestdestiny/README.md | 414 ++++++++++++++++++ .../manifestparser/manifestparser.py | 25 +- testing/mozbase/manifestdestiny/setup.py | 17 +- .../manifestdestiny/tests/comment-example.ini | 11 - .../manifestdestiny/tests/manifest.ini | 1 - .../tests/test_manifestparser.py | 11 - .../manifestdestiny/tests/test_read_ini.py | 69 --- .../tests/test_testmanifest.py | 11 - testing/mozbase/mozb2g/mozb2g/b2gmixin.py | 82 ++-- testing/mozbase/mozb2g/setup.py | 16 +- testing/mozbase/mozcrash/setup.py | 4 +- testing/mozbase/mozcrash/tests/test.py | 2 +- .../mozbase/mozdevice/mozdevice/__init__.py | 2 +- .../mozdevice/mozdevice/devicemanager.py | 17 +- .../mozdevice/mozdevice/devicemanagerADB.py | 41 +- .../mozdevice/mozdevice/devicemanagerSUT.py | 31 +- testing/mozbase/mozdevice/setup.py | 2 +- .../mozdevice/sut_tests/test_fileExists.py | 37 -- testing/mozbase/mozdevice/tests/manifest.ini | 16 +- testing/mozbase/mozdevice/tests/sut_app.py | 20 - testing/mozbase/mozdevice/tests/sut_chmod.py | 21 - .../mozbase/mozdevice/tests/sut_fileExists.py | 29 -- .../mozdevice/tests/sut_fileMethods.py | 74 ---- testing/mozbase/mozdevice/tests/sut_info.py | 49 --- testing/mozbase/mozdevice/tests/sut_ip.py | 37 -- testing/mozbase/mozdevice/tests/sut_kill.py | 24 - testing/mozbase/mozdevice/tests/sut_list.py | 22 - testing/mozbase/mozdevice/tests/sut_logcat.py | 51 --- testing/mozbase/mozdevice/tests/sut_mkdir.py | 9 - testing/mozbase/mozdevice/tests/sut_push.py | 14 +- testing/mozbase/mozdevice/tests/sut_remove.py | 24 - testing/mozbase/mozdevice/tests/sut_time.py | 18 - .../mozbase/mozdevice/tests/sut_unpackfile.py | 24 - testing/mozbase/mozfile/mozfile/mozfile.py | 34 +- testing/mozbase/mozfile/setup.py | 4 +- testing/mozbase/mozfile/tests/manifest.ini | 4 +- testing/mozbase/mozfile/tests/stubs.py | 34 -- .../tests/{test_extract.py => test.py} | 69 ++- testing/mozbase/mozfile/tests/test_load.py | 5 +- testing/mozbase/mozfile/tests/test_rmtree.py | 59 --- testing/mozbase/mozfile/tests/test_tempdir.py | 42 -- .../mozbase/mozfile/tests/test_tempfile.py | 21 +- testing/mozbase/mozfile/tests/test_url.py | 1 - testing/mozbase/mozhttpd/README.md | 1 + testing/mozbase/mozhttpd/mozhttpd/__init__.py | 41 +- testing/mozbase/mozhttpd/mozhttpd/iface.py | 33 ++ testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py | 50 +-- testing/mozbase/mozhttpd/setup.py | 18 +- testing/mozbase/mozhttpd/tests/baseurl.py | 18 - testing/mozbase/mozhttpd/tests/basic.py | 46 -- testing/mozbase/mozhttpd/tests/manifest.ini | 4 +- testing/mozbase/mozinfo/mozinfo/__init__.py | 2 - testing/mozbase/mozinfo/mozinfo/mozinfo.py | 50 +-- testing/mozbase/mozinfo/setup.py | 4 +- testing/mozbase/mozinfo/tests/manifest.ini | 1 - testing/mozbase/mozinfo/tests/test.py | 88 ---- .../mozinstall/mozinstall/mozinstall.py | 5 +- testing/mozbase/mozinstall/setup.py | 4 +- .../tests/Installer-Stubs/firefox.dmg | Bin 13441 -> 0 bytes .../tests/Installer-Stubs/firefox.exe | Bin 55015 -> 0 bytes .../tests/Installer-Stubs/firefox.tar.bz2 | Bin 2882 -> 0 bytes .../tests/Installer-Stubs/firefox.zip | Bin 8707 -> 0 bytes testing/mozbase/mozinstall/tests/manifest.ini | 1 - testing/mozbase/mozinstall/tests/test.py | 151 ------- testing/mozbase/mozlog/README.md | 18 + testing/mozbase/mozlog/mozlog/__init__.py | 5 - testing/mozbase/mozlog/mozlog/logger.py | 92 +--- testing/mozbase/mozlog/mozlog/loglistener.py | 50 --- testing/mozbase/mozlog/setup.py | 25 +- testing/mozbase/mozlog/tests/manifest.ini | 1 - testing/mozbase/mozlog/tests/test_logger.py | 170 ------- .../mozbase/moznetwork/moznetwork/__init__.py | 19 - .../moznetwork/moznetwork/moznetwork.py | 16 +- testing/mozbase/moznetwork/setup.py | 6 +- testing/mozbase/moznetwork/tests/manifest.ini | 3 - testing/mozbase/moznetwork/tests/test.py | 78 ---- testing/mozbase/mozprocess/setup.py | 2 +- testing/mozbase/mozprocess/tests/Makefile | 2 +- .../mozbase/mozprofile/mozprofile/__init__.py | 3 +- .../mozprofile/mozprofile/permissions.py | 14 +- .../mozbase/mozprofile/mozprofile/prefs.py | 14 +- .../mozbase/mozprofile/mozprofile/profile.py | 47 +- testing/mozbase/mozprofile/setup.py | 2 +- .../tests/files/prefs_with_interpolation.js | 4 - .../mozprofile/tests/test_preferences.py | 18 - .../mozbase/mozrunner/mozrunner/__init__.py | 6 +- testing/mozbase/mozrunner/mozrunner/local.py | 386 ---------------- testing/mozbase/mozrunner/mozrunner/remote.py | 335 -------------- .../mozrunner/resources/metrotestharness.exe | Bin 63488 -> 0 bytes testing/mozbase/mozrunner/mozrunner/runner.py | 359 +++++++++++++-- testing/mozbase/mozrunner/setup.py | 12 +- testing/mozbase/moztest/README.md | 16 + testing/mozbase/moztest/setup.py | 15 +- testing/mozbase/setup_development.py | 14 +- testing/mozbase/test-manifest.ini | 4 - 96 files changed, 1111 insertions(+), 2642 deletions(-) create mode 100644 testing/mozbase/manifestdestiny/README.md delete mode 100644 testing/mozbase/manifestdestiny/tests/comment-example.ini delete mode 100755 testing/mozbase/manifestdestiny/tests/test_read_ini.py delete mode 100644 testing/mozbase/mozdevice/sut_tests/test_fileExists.py delete mode 100644 testing/mozbase/mozdevice/tests/sut_app.py delete mode 100644 testing/mozbase/mozdevice/tests/sut_chmod.py delete mode 100644 testing/mozbase/mozdevice/tests/sut_fileExists.py delete mode 100644 testing/mozbase/mozdevice/tests/sut_fileMethods.py delete mode 100644 testing/mozbase/mozdevice/tests/sut_info.py delete mode 100644 testing/mozbase/mozdevice/tests/sut_ip.py delete mode 100644 testing/mozbase/mozdevice/tests/sut_kill.py delete mode 100644 testing/mozbase/mozdevice/tests/sut_list.py delete mode 100644 testing/mozbase/mozdevice/tests/sut_logcat.py delete mode 100644 testing/mozbase/mozdevice/tests/sut_remove.py delete mode 100644 testing/mozbase/mozdevice/tests/sut_time.py delete mode 100644 testing/mozbase/mozdevice/tests/sut_unpackfile.py delete mode 100644 testing/mozbase/mozfile/tests/stubs.py rename testing/mozbase/mozfile/tests/{test_extract.py => test.py} (70%) mode change 100644 => 100755 delete mode 100644 testing/mozbase/mozfile/tests/test_rmtree.py delete mode 100644 testing/mozbase/mozfile/tests/test_tempdir.py create mode 100644 testing/mozbase/mozhttpd/README.md create mode 100644 testing/mozbase/mozhttpd/mozhttpd/iface.py delete mode 100644 testing/mozbase/mozhttpd/tests/baseurl.py delete mode 100644 testing/mozbase/mozhttpd/tests/basic.py delete mode 100644 testing/mozbase/mozinfo/tests/manifest.ini delete mode 100644 testing/mozbase/mozinfo/tests/test.py delete mode 100644 testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.dmg delete mode 100644 testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.exe delete mode 100644 testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.tar.bz2 delete mode 100644 testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.zip delete mode 100644 testing/mozbase/mozinstall/tests/manifest.ini delete mode 100644 testing/mozbase/mozinstall/tests/test.py create mode 100644 testing/mozbase/mozlog/README.md delete mode 100644 testing/mozbase/mozlog/mozlog/loglistener.py delete mode 100644 testing/mozbase/mozlog/tests/manifest.ini delete mode 100644 testing/mozbase/mozlog/tests/test_logger.py delete mode 100644 testing/mozbase/moznetwork/tests/manifest.ini delete mode 100644 testing/mozbase/moznetwork/tests/test.py delete mode 100644 testing/mozbase/mozprofile/tests/files/prefs_with_interpolation.js delete mode 100644 testing/mozbase/mozrunner/mozrunner/local.py delete mode 100644 testing/mozbase/mozrunner/mozrunner/remote.py delete mode 100644 testing/mozbase/mozrunner/mozrunner/resources/metrotestharness.exe create mode 100644 testing/mozbase/moztest/README.md diff --git a/testing/mozbase/README.md b/testing/mozbase/README.md index 7656598e9ea..4975039ff4d 100644 --- a/testing/mozbase/README.md +++ b/testing/mozbase/README.md @@ -12,7 +12,7 @@ the docs directory. Consult [open][] [bugs][] and feel free to file [new bugs][]. -[project page]: https://wiki.mozilla.org/Auto-tools/Projects/Mozbase +[project page]: https://wiki.mozilla.org/Auto-tools/Projects/MozBase [detailed docs]: http://mozbase.readthedocs.org/ [open]: https://bugzilla.mozilla.org/buglist.cgi?resolution=---&component=Mozbase&product=Testing [bugs]: https://bugzilla.mozilla.org/buglist.cgi?resolution=---&status_whiteboard_type=allwordssubstr&query_format=advanced&status_whiteboard=mozbase diff --git a/testing/mozbase/manifestdestiny/README.md b/testing/mozbase/manifestdestiny/README.md new file mode 100644 index 00000000000..b0527f0bdec --- /dev/null +++ b/testing/mozbase/manifestdestiny/README.md @@ -0,0 +1,414 @@ +Universal manifests for Mozilla test harnesses + +# What is ManifestDestiny? + +What ManifestDestiny gives you: + +* manifests are ordered lists of tests +* tests may have an arbitrary number of key, value pairs +* the parser returns an ordered list of test data structures, which + are just dicts with some keys. For example, a test with no + user-specified metadata looks like this: + + [{'expected': 'pass', + 'path': '/home/mozilla/mozmill/src/ManifestDestiny/manifestdestiny/tests/testToolbar/testBackForwardButtons.js', + 'relpath': 'testToolbar/testBackForwardButtons.js', + 'name': 'testBackForwardButtons.js', + 'here': '/home/mozilla/mozmill/src/ManifestDestiny/manifestdestiny/tests', + 'manifest': '/home/mozilla/mozmill/src/ManifestDestiny/manifestdestiny/tests/manifest.ini',}] + +The keys displayed here (path, relpath, name, here, and manifest) are reserved keys for ManifestDestiny and any consuming APIs. You can add additional key, value metadata to each test. + + +# Why have test manifests? + +It is desirable to have a unified format for test manifests for testing +[mozilla-central](http://hg.mozilla.org/mozilla-central), etc. + +* It is desirable to be able to selectively enable or disable tests based on platform or other conditions. This should be easy to do. Currently, since many of the harnesses just crawl directories, there is no effective way of disabling a test except for removal from mozilla-central +* It is desriable to do this in a universal way so that enabling and disabling tests as well as other tasks are easily accessible to a wider audience than just those intimately familiar with the specific test framework. +* It is desirable to have other metadata on top of the test. For instance, let's say a test is marked as skipped. It would be nice to give the reason why. + + +Most Mozilla test harnesses work by crawling a directory structure. +While this is straight-forward, manifests offer several practical +advantages:: + +* ability to turn a test off easily: if a test is broken on m-c + currently, the only way to turn it off, generally speaking, is just + removing the test. Often this is undesirable, as if the test should + be dismissed because other people want to land and it can't be + investigated in real time (is it a failure? is the test bad? is no + one around that knows the test?), then backing out a test is at best + problematic. With a manifest, a test may be disabled without + removing it from the tree and a bug filed with the appropriate + reason: + + [test_broken.js] + disabled = https://bugzilla.mozilla.org/show_bug.cgi?id=123456 + +* ability to run different (subsets of) tests on different + platforms. Traditionally, we've done a bit of magic or had the test + know what platform it would or would not run on. With manifests, you + can mark what platforms a test will or will not run on and change + these without changing the test. + + [test_works_on_windows_only.js] + run-if = os == 'win' + +* ability to markup tests with metadata. We have a large, complicated, + and always changing infrastructure. key, value metadata may be used + as an annotation to a test and appropriately curated and mined. For + instance, we could mark certain tests as randomorange with a bug + number, if it were desirable. + +* ability to have sane and well-defined test-runs. You can keep + different manifests for different test runs and ``[include:]`` + (sub)manifests as appropriate to your needs. + + +# Manifest Format + +Manifests are .ini file with the section names denoting the path +relative to the manifest: + + [foo.js] + [bar.js] + [fleem.js] + +The sections are read in order. In addition, tests may include +arbitrary key, value metadata to be used by the harness. You may also +have a `[DEFAULT]` section that will give key, value pairs that will +be inherited by each test unless overridden: + + [DEFAULT] + type = restart + + [lilies.js] + color = white + + [daffodils.js] + color = yellow + type = other + # override type from DEFAULT + + [roses.js] + color = red + +You can also include other manifests: + + [include:subdir/anothermanifest.ini] + +Manifests are included relative to the directory of the manifest with +the `[include:]` directive unless they are absolute paths. + + +# Data + +Manifest Destiny gives tests as a list of dictionaries (in python +terms). + +* path: full path to the test +* relpath: relative path starting from the root manifest location +* name: file name of the test +* here: the parent directory of the manifest +* manifest: the path to the manifest containing the test + +This data corresponds to a one-line manifest: + + [testToolbar/testBackForwardButtons.js] + +If additional key, values were specified, they would be in this dict +as well. + +Outside of the reserved keys, the remaining key, values +are up to convention to use. There is a (currently very minimal) +generic integration layer in ManifestDestiny for use of all harnesses, +`manifestparser.TestManifest`. +For instance, if the 'disabled' key is present, you can get the set of +tests without disabled (various other queries are doable as well). + +Since the system is convention-based, the harnesses may do whatever +they want with the data. They may ignore it completely, they may use +the provided integration layer, or they may provide their own +integration layer. This should allow whatever sort of logic is +desired. For instance, if in yourtestharness you wanted to run only on +mondays for a certain class of tests: + + tests = [] + for test in manifests.tests: + if 'runOnDay' in test: + if calendar.day_name[calendar.weekday(*datetime.datetime.now().timetuple()[:3])].lower() == test['runOnDay'].lower(): + tests.append(test) + else: + tests.append(test) + +To recap: +* the manifests allow you to specify test data +* the parser gives you this data +* you can use it however you want or process it further as you need + +Tests are denoted by sections in an .ini file (see +http://hg.mozilla.org/automation/ManifestDestiny/file/tip/manifestdestiny/tests/mozmill-example.ini). + +Additional manifest files may be included with an `[include:]` directive: + + [include:path-to-additional-file.manifest] + +The path to included files is relative to the current manifest. + +The `[DEFAULT]` section contains variables that all tests inherit from. + +Included files will inherit the top-level variables but may override +in their own `[DEFAULT]` section. + + +# ManifestDestiny Architecture + +There is a two- or three-layered approach to the ManifestDestiny +architecture, depending on your needs: + +1. ManifestParser: this is a generic parser for .ini manifests that +facilitates the `[include:]` logic and the inheritence of +metadata. Despite the internal variable being called `self.tests` +(an oversight), this layer has nothing in particular to do with tests. + +2. TestManifest: this is a harness-agnostic integration layer that is +test-specific. TestManifest faciliates `skip-if` and `run-if` logic. + +3. Optionally, a harness will have an integration layer than inherits +from TestManifest if more harness-specific customization is desired at +the manifest level. + +See the source code at https://github.com/mozilla/mozbase/tree/master/manifestdestiny +and +https://github.com/mozilla/mozbase/blob/master/manifestdestiny/manifestparser.py +in particular. + + +# Using Manifests + +A test harness will normally call `TestManifest.active_tests`: + + def active_tests(self, exists=True, disabled=True, **tags): + +The manifests are passed to the `__init__` or `read` methods with +appropriate arguments. `active_tests` then allows you to select the +tests you want: + +- exists : return only existing tests +- disabled : whether to return disabled tests; if not these will be + filtered out; if True (the default), the `disabled` key of a + test's metadata will be present and will be set to the reason that a + test is disabled +- tags : keys and values to filter on (e.g. `os='linux'`) + +`active_tests` looks for tests with `skip-if` +`run-if`. If the condition is or is not fulfilled, +respectively, the test is marked as disabled. For instance, if you +pass `**dict(os='linux')` as `**tags`, if a test contains a line +`skip-if = os == 'linux'` this test will be disabled, or +`run-if = os = 'win'` in which case the test will also be disabled. It +is up to the harness to pass in tags appropriate to its usage. + + +# Creating Manifests + +ManifestDestiny comes with a console script, `manifestparser create`, that +may be used to create a seed manifest structure from a directory of +files. Run `manifestparser help create` for usage information. + + +# Copying Manifests + +To copy tests and manifests from a source: + + manifestparser [options] copy from_manifest to_directory -tag1 -tag2 --key1=value1 key2=value2 ... + + +# Upating Tests + +To update the tests associated with with a manifest from a source +directory: + + manifestparser [options] update manifest from_directory -tag1 -tag2 --key1=value1 --key2=value2 ... + + +# Usage example + +Here is an example of how to create manifests for a directory tree and +update the tests listed in the manifests from an external source. + +## Creating Manifests + +Let's say you want to make a series of manifests for a given directory structure containing `.js` test files: + + testing/mozmill/tests/firefox/ + testing/mozmill/tests/firefox/testAwesomeBar/ + testing/mozmill/tests/firefox/testPreferences/ + testing/mozmill/tests/firefox/testPrivateBrowsing/ + testing/mozmill/tests/firefox/testSessionStore/ + testing/mozmill/tests/firefox/testTechnicalTools/ + testing/mozmill/tests/firefox/testToolbar/ + testing/mozmill/tests/firefox/restartTests + +You can use `manifestparser create` to do this: + + $ manifestparser help create + Usage: manifestparser.py [options] create directory <...> + + create a manifest from a list of directories + + Options: + -p PATTERN, --pattern=PATTERN + glob pattern for files + -i IGNORE, --ignore=IGNORE + directories to ignore + -w IN_PLACE, --in-place=IN_PLACE + Write .ini files in place; filename to write to + +We only want `.js` files and we want to skip the `restartTests` directory. +We also want to write a manifest per directory, so I use the `--in-place` +option to write the manifests: + + manifestparser create . -i restartTests -p '*.js' -w manifest.ini + +This creates a manifest.ini per directory that we care about with the JS test files: + + testing/mozmill/tests/firefox/manifest.ini + testing/mozmill/tests/firefox/testAwesomeBar/manifest.ini + testing/mozmill/tests/firefox/testPreferences/manifest.ini + testing/mozmill/tests/firefox/testPrivateBrowsing/manifest.ini + testing/mozmill/tests/firefox/testSessionStore/manifest.ini + testing/mozmill/tests/firefox/testTechnicalTools/manifest.ini + testing/mozmill/tests/firefox/testToolbar/manifest.ini + +The top-level `manifest.ini` merely has `[include:]` references to the sub manifests: + + [include:testAwesomeBar/manifest.ini] + [include:testPreferences/manifest.ini] + [include:testPrivateBrowsing/manifest.ini] + [include:testSessionStore/manifest.ini] + [include:testTechnicalTools/manifest.ini] + [include:testToolbar/manifest.ini] + +Each sub-level manifest contains the (`.js`) test files relative to it. + +## Updating the tests from manifests + +You may need to update tests as given in manifests from a different source directory. +`manifestparser update` was made for just this purpose: + + Usage: manifestparser [options] update manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ... + + update the tests as listed in a manifest from a directory + +To update from a directory of tests in `~/mozmill/src/mozmill-tests/firefox/` run: + + manifestparser update manifest.ini ~/mozmill/src/mozmill-tests/firefox/ + + +# Tests + +ManifestDestiny includes a suite of tests: + +https://github.com/mozilla/mozbase/tree/master/manifestdestiny/tests + +`test_manifest.txt` is a doctest that may be helpful in figuring out +how to use the API. Tests are run via `python test.py`. + + +# Bugs + +Please file any bugs or feature requests at + +https://bugzilla.mozilla.org/enter_bug.cgi?product=Testing&component=ManifestParser + +Or contact jhammel @mozilla.org or in #ateam on irc.mozilla.org + + +# CLI + +Run `manifestparser help` for usage information. + +To create a manifest from a set of directories: + + manifestparser [options] create directory <...> [create-options] + +To output a manifest of tests: + + manifestparser [options] write manifest <...> -tag1 -tag2 --key1=value1 --key2=value2 ... + +To copy tests and manifests from a source: + + manifestparser [options] copy from_manifest to_manifest -tag1 -tag2 --key1=value1 key2=value2 ... + +To update the tests associated with with a manifest from a source +directory: + + manifestparser [options] update manifest from_directory -tag1 -tag2 --key1=value1 --key2=value2 ... + + +# Design Considerations + +Contrary to some opinion, manifestparser.py and the associated .ini +format were not magically plucked from the sky but were descended upon +through several design considerations. + +* test manifests should be ordered. While python 2.6 and greater has + a ConfigParser that can use an ordered dictionary, it is a + requirement that we support python 2.4 for the build + testing + environment. To that end, a `read_ini` function was implemented + in manifestparser.py that should be the equivalent of the .ini + dialect used by ConfigParser. + +* the manifest format should be easily human readable/writable. While + there was initially some thought of using JSON, there was pushback + that JSON was not easily editable. An ideal manifest format would + degenerate to a line-separated list of files. While .ini format + requires an additional `[]` per line, and while there have been + complaints about this, hopefully this is good enough. + +* python does not have an in-built YAML parser. Since it was + undesirable for manifestparser.py to have any dependencies, YAML was + dismissed as a format. + +* we could have used a proprietary format but decided against it. + Everyone knows .ini and there are good tools to deal with it. + However, since read_ini is the only function that transforms a + manifest to a list of key, value pairs, while the implications for + changing the format impacts downstream code, doing so should be + programmatically simple. + +* there should be a single file that may easily be + transported. Traditionally, test harnesses have lived in + mozilla-central. This is less true these days and it is increasingly + likely that more tests will not live in mozilla-central going + forward. So `manifestparser.py` should be highly consumable. To + this end, it is a single file, as appropriate to mozilla-central, + which is also a working python package deployed to PyPI for easy + installation. + + +# Developing ManifestDestiny + +ManifestDestiny is developed and maintained by Mozilla's +[Automation and Testing Team](https://wiki.mozilla.org/Auto-tools). +The project page is located at +https://wiki.mozilla.org/Auto-tools/Projects/ManifestDestiny . + + +# Historical Reference + +Date-ordered list of links about how manifests came to be where they are today:: + +* https://wiki.mozilla.org/Auto-tools/Projects/UniversalManifest +* http://alice.nodelman.net/blog/post/2010/05/ +* http://alice.nodelman.net/blog/post/universal-manifest-for-unit-tests-a-proposal/ +* https://elvis314.wordpress.com/2010/07/05/improving-personal-hygiene-by-adjusting-mochitests/ +* https://elvis314.wordpress.com/2010/07/27/types-of-data-we-care-about-in-a-manifest/ +* https://bugzilla.mozilla.org/show_bug.cgi?id=585106 +* http://elvis314.wordpress.com/2011/05/20/converting-xpcshell-from-listing-directories-to-a-manifest/ +* https://bugzilla.mozilla.org/show_bug.cgi?id=616999 +* https://wiki.mozilla.org/Auto-tools/Projects/ManifestDestiny +* https://developer.mozilla.org/en/Writing_xpcshell-based_unit_tests#Adding_your_tests_to_the_xpcshell_manifest diff --git a/testing/mozbase/manifestdestiny/manifestparser/manifestparser.py b/testing/mozbase/manifestdestiny/manifestparser/manifestparser.py index 7afc3c4d997..5d2613c9e14 100755 --- a/testing/mozbase/manifestdestiny/manifestparser/manifestparser.py +++ b/testing/mozbase/manifestdestiny/manifestparser/manifestparser.py @@ -19,7 +19,30 @@ import sys from fnmatch import fnmatch from optparse import OptionParser -relpath = os.path.relpath +# we need relpath, but it is introduced in python 2.6 +# http://docs.python.org/library/os.path.html +try: + relpath = os.path.relpath +except AttributeError: + def relpath(path, start): + """ + Return a relative version of a path + from /usr/lib/python2.6/posixpath.py + """ + + if not path: + raise ValueError("no path specified") + + start_list = os.path.abspath(start).split(os.path.sep) + path_list = os.path.abspath(path).split(os.path.sep) + + # Work out how much of the filepath is shared by start and path. + i = len(os.path.commonprefix([start_list, path_list])) + + rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:] + if not rel_list: + return os.curdir + return os.path.join(*rel_list) # expr.py # from: diff --git a/testing/mozbase/manifestdestiny/setup.py b/testing/mozbase/manifestdestiny/setup.py index 8e5a2396edb..e58039d5127 100644 --- a/testing/mozbase/manifestdestiny/setup.py +++ b/testing/mozbase/manifestdestiny/setup.py @@ -3,19 +3,28 @@ # You can obtain one at http://mozilla.org/MPL/2.0/. from setuptools import setup +import sys +import os + +here = os.path.dirname(os.path.abspath(__file__)) +try: + filename = os.path.join(here, 'README.md') + description = file(filename).read() +except: + description = '' PACKAGE_NAME = "ManifestDestiny" -PACKAGE_VERSION = '0.5.7' +PACKAGE_VERSION = '0.5.6' setup(name=PACKAGE_NAME, version=PACKAGE_VERSION, - description="Library to create and manage test manifests", - long_description="see http://mozbase.readthedocs.org/", + description="Universal manifests for Mozilla test harnesses", + long_description=description, classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers keywords='mozilla manifests', author='Mozilla Automation and Testing Team', author_email='tools@lists.mozilla.org', - url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase', + url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase', license='MPL', zip_safe=False, packages=['manifestparser'], diff --git a/testing/mozbase/manifestdestiny/tests/comment-example.ini b/testing/mozbase/manifestdestiny/tests/comment-example.ini deleted file mode 100644 index 030ceffdb3b..00000000000 --- a/testing/mozbase/manifestdestiny/tests/comment-example.ini +++ /dev/null @@ -1,11 +0,0 @@ -; See https://bugzilla.mozilla.org/show_bug.cgi?id=813674 - -[test_0180_fileInUse_xp_win_complete.js] -[test_0181_fileInUse_xp_win_partial.js] -[test_0182_rmrfdirFileInUse_xp_win_complete.js] -[test_0183_rmrfdirFileInUse_xp_win_partial.js] -[test_0184_fileInUse_xp_win_complete.js] -[test_0185_fileInUse_xp_win_partial.js] -[test_0186_rmrfdirFileInUse_xp_win_complete.js] -[test_0187_rmrfdirFileInUse_xp_win_partial.js] -; [test_0202_app_launch_apply_update_dirlocked.js] # Test disabled, bug 757632 \ No newline at end of file diff --git a/testing/mozbase/manifestdestiny/tests/manifest.ini b/testing/mozbase/manifestdestiny/tests/manifest.ini index 6dad6e95935..b06b951ecc5 100644 --- a/testing/mozbase/manifestdestiny/tests/manifest.ini +++ b/testing/mozbase/manifestdestiny/tests/manifest.ini @@ -2,4 +2,3 @@ [test_expressionparser.py] [test_manifestparser.py] [test_testmanifest.py] -[test_read_ini.py] diff --git a/testing/mozbase/manifestdestiny/tests/test_manifestparser.py b/testing/mozbase/manifestdestiny/tests/test_manifestparser.py index ef58590e05e..cb87fff1dfb 100755 --- a/testing/mozbase/manifestdestiny/tests/test_manifestparser.py +++ b/testing/mozbase/manifestdestiny/tests/test_manifestparser.py @@ -215,16 +215,5 @@ class TestManifestparser(unittest.TestCase): os.path.join(here, 'fleem')) - def test_comments(self): - """ - ensure comments work, see - https://bugzilla.mozilla.org/show_bug.cgi?id=813674 - """ - comment_example = os.path.join(here, 'comment-example.ini') - manifest = ManifestParser(manifests=(comment_example,)) - self.assertEqual(len(manifest.tests), 8) - names = [i['name'] for i in manifest.tests] - self.assertFalse('test_0202_app_launch_apply_update_dirlocked.js' in names) - if __name__ == '__main__': unittest.main() diff --git a/testing/mozbase/manifestdestiny/tests/test_read_ini.py b/testing/mozbase/manifestdestiny/tests/test_read_ini.py deleted file mode 100755 index fb018fa8613..00000000000 --- a/testing/mozbase/manifestdestiny/tests/test_read_ini.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python - -""" -test .ini parsing - -ensure our .ini parser is doing what we want; to be deprecated for -python's standard ConfigParser when 2.7 is reality so OrderedDict -is the default: - -http://docs.python.org/2/library/configparser.html -""" - -import unittest -from manifestparser import read_ini -from ConfigParser import ConfigParser -from StringIO import StringIO - -class IniParserTest(unittest.TestCase): - - def test_inline_comments(self): - """ - We have no inline comments; so we're testing to ensure we don't: - https://bugzilla.mozilla.org/show_bug.cgi?id=855288 - """ - - # test '#' inline comments (really, the lack thereof) - string = """[test_felinicity.py] -kittens = true # This test requires kittens -""" - buffer = StringIO() - buffer.write(string) - buffer.seek(0) - result = read_ini(buffer)[0][1]['kittens'] - self.assertEqual(result, "true # This test requires kittens") - - # compare this to ConfigParser - # python 2.7 ConfigParser does not support '#' as an - # inline comment delimeter (for "backwards compatability"): - # http://docs.python.org/2/library/configparser.html - buffer.seek(0) - parser = ConfigParser() - parser.readfp(buffer) - control = parser.get('test_felinicity.py', 'kittens') - self.assertEqual(result, control) - - # test ';' inline comments (really, the lack thereof) - string = string.replace('#', ';') - buffer = StringIO() - buffer.write(string) - buffer.seek(0) - result = read_ini(buffer)[0][1]['kittens'] - self.assertEqual(result, "true ; This test requires kittens") - - # compare this to ConfigParser - # python 2.7 ConfigParser *does* support ';' as an - # inline comment delimeter (ibid). - # Python 3.x configparser, OTOH, does not support - # inline-comments by default. It does support their specification, - # though they are weakly discouraged: - # http://docs.python.org/dev/library/configparser.html - buffer.seek(0) - parser = ConfigParser() - parser.readfp(buffer) - control = parser.get('test_felinicity.py', 'kittens') - self.assertNotEqual(result, control) - - -if __name__ == '__main__': - unittest.main() diff --git a/testing/mozbase/manifestdestiny/tests/test_testmanifest.py b/testing/mozbase/manifestdestiny/tests/test_testmanifest.py index 898706f4d04..1a93df81ee0 100644 --- a/testing/mozbase/manifestdestiny/tests/test_testmanifest.py +++ b/testing/mozbase/manifestdestiny/tests/test_testmanifest.py @@ -29,17 +29,6 @@ class TestTestManifest(unittest.TestCase): last_test = manifest.active_tests(exists=False, toolkit='cocoa')[-1] self.assertEqual(last_test['expected'], 'fail') - def test_comments(self): - """ - ensure comments work, see - https://bugzilla.mozilla.org/show_bug.cgi?id=813674 - """ - comment_example = os.path.join(here, 'comment-example.ini') - manifest = TestManifest(manifests=(comment_example,)) - self.assertEqual(len(manifest.tests), 8) - names = [i['name'] for i in manifest.tests] - self.assertFalse('test_0202_app_launch_apply_update_dirlocked.js' in names) - if __name__ == '__main__': unittest.main() diff --git a/testing/mozbase/mozb2g/mozb2g/b2gmixin.py b/testing/mozbase/mozb2g/mozb2g/b2gmixin.py index e0f5c974f40..b17a320e91f 100644 --- a/testing/mozbase/mozb2g/mozb2g/b2gmixin.py +++ b/testing/mozbase/mozb2g/mozb2g/b2gmixin.py @@ -19,30 +19,18 @@ class B2GMixin(object): userJS = "/data/local/user.js" marionette = None - def __init__(self, host=None, marionetteHost=None, marionettePort=2828, - **kwargs): - - # (allowing marionneteHost to be specified seems a bit - # counter-intuitive since we normally get it below from the ip - # address, however we currently need it to be able to connect - # via adb port forwarding and localhost) - if marionetteHost: - self.marionetteHost = marionetteHost - elif host: - self.marionetteHost = host - self.marionettePort = marionettePort + def __init__(self, host=None, marionette_port=2828, **kwargs): + self.marionetteHost = host + self.marionettePort = marionette_port def cleanup(self): - """ - If a user profile was setup on the device, restore it to the original. - """ if self.profileDir: self.restoreProfile() def waitForPort(self, timeout): - """Waits for the marionette server to respond, until the timeout specified. - - :param timeout: Timeout parameter in seconds. + """ + Wait for the marionette server to respond. + Timeout parameter is in seconds """ print "waiting for port" starttime = datetime.datetime.now() @@ -62,11 +50,11 @@ class B2GMixin(object): time.sleep(1) raise DMError("Could not communicate with Marionette port") - def setupMarionette(self, scriptTimeout=60000): + def setupMarionette(self): """ - Starts a marionette session. - If no host was given at init, the ip of the device will be retrieved - and networking will be established. + Start a marionette session. + If no host is given, then this will get the ip + of the device, and set up networking if needed. """ if not self.marionetteHost: self.setupDHCP() @@ -77,11 +65,9 @@ class B2GMixin(object): self.waitForPort(30) self.marionette.start_session() - self.marionette.set_script_timeout(scriptTimeout) - def restartB2G(self): """ - Restarts the b2g process on the device. + Restarts the b2g process on the device """ #restart b2g so we start with a clean slate if self.marionette and self.marionette.session: @@ -97,16 +83,16 @@ class B2GMixin(object): self.shellCheckOutput(['start', 'b2g']) def setupProfile(self, prefs=None): - """Sets up the user profile on the device. - - :param prefs: String of user_prefs to add to the profile. Defaults to a standard b2g testing profile. """ - # currently we have no custom prefs to set (when bug 800138 is fixed, - # we will probably want to enable marionette on an external ip by - # default) + Sets up the user profile on the device, + The 'prefs' is a string of user_prefs to add to the profile. + If it is not set, it will default to a standard b2g testing profile. + """ if not prefs: - prefs = "" - + prefs = """ +user_pref("power.screen.timeout", 999999); +user_pref("devtools.debugger.force-local", false); + """ #remove previous user.js if there is one if not self.profileDir: self.profileDir = tempfile.mkdtemp() @@ -115,7 +101,7 @@ class B2GMixin(object): os.remove(our_userJS) #copy profile try: - self.getFile(self.userJS, our_userJS) + output = self.getFile(self.userJS, our_userJS) except subprocess.CalledProcessError: pass #if we successfully copied the profile, make a backup of the file @@ -123,39 +109,31 @@ class B2GMixin(object): self.shellCheckOutput(['dd', 'if=%s' % self.userJS, 'of=%s.orig' % self.userJS]) with open(our_userJS, 'a') as user_file: user_file.write("%s" % prefs) - self.pushFile(our_userJS, self.userJS) self.restartB2G() self.setupMarionette() - def setupDHCP(self, interfaces=['eth0', 'wlan0']): - """Sets up networking. - - :param interfaces: Network connection types to try. Defaults to eth0 and wlan0. + def setupDHCP(self, conn_type='eth0'): """ - all_interfaces = [line.split()[0] for line in \ - self.shellCheckOutput(['netcfg']).splitlines()[1:]] - interfaces_to_try = filter(lambda i: i in interfaces, all_interfaces) + Sets up networking. + If conn_type is not set, it will assume eth0. + """ tries = 5 - print "Setting up DHCP..." while tries > 0: print "attempts left: %d" % tries try: - for interface in interfaces_to_try: - self.shellCheckOutput(['netcfg', interface, 'dhcp'], - timeout=10) - if self.getIP(interfaces=[interface]): - return + self.shellCheckOutput(['netcfg', conn_type, 'dhcp'], timeout=10) + if self.getIP(): + return except DMError: pass - time.sleep(1) - tries -= 1 + tries = tries - 1 raise DMError("Could not set up network connection") def restoreProfile(self): """ - Restores the original user profile on the device. + Restores the original profile """ if not self.profileDir: raise DMError("There is no profile to restore") @@ -169,8 +147,6 @@ class B2GMixin(object): def getAppInfo(self): """ Returns the appinfo, with an additional "date" key. - - :rtype: dictionary """ if not self.marionette or not self.marionette.session: self.setupMarionette() diff --git a/testing/mozbase/mozb2g/setup.py b/testing/mozbase/mozb2g/setup.py index bf646199a0b..80155312cb5 100644 --- a/testing/mozbase/mozb2g/setup.py +++ b/testing/mozbase/mozb2g/setup.py @@ -2,21 +2,29 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. +import os from setuptools import setup -PACKAGE_VERSION = '0.3' +PACKAGE_VERSION = '0.1' -deps = ['mozdevice >= 0.16', 'marionette_client >= 0.5.2'] +# take description from README +here = os.path.dirname(os.path.abspath(__file__)) +try: + description = file(os.path.join(here, 'README.md')).read() +except (OSError, IOError): + description = '' + +deps = ['mozdevice', 'marionette_client'] setup(name='mozb2g', version=PACKAGE_VERSION, description="B2G specific code for device automation", - long_description="see http://mozbase.readthedocs.org/", + long_description=description, classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers keywords='', author='Mozilla Automation and Testing Team', author_email='tools@lists.mozilla.org', - url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase', + url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase', license='MPL', packages=['mozb2g'], include_package_data=True, diff --git a/testing/mozbase/mozcrash/setup.py b/testing/mozbase/mozcrash/setup.py index f40c4b3f657..0b20106d0e5 100644 --- a/testing/mozbase/mozcrash/setup.py +++ b/testing/mozbase/mozcrash/setup.py @@ -4,7 +4,7 @@ from setuptools import setup -PACKAGE_VERSION = '0.8' +PACKAGE_VERSION = '0.6' # dependencies deps = ['mozfile >= 0.3', @@ -18,7 +18,7 @@ setup(name='mozcrash', keywords='mozilla', author='Mozilla Automation and Tools team', author_email='tools@lists.mozilla.org', - url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase', + url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase', license='MPL', packages=['mozcrash'], include_package_data=True, diff --git a/testing/mozbase/mozcrash/tests/test.py b/testing/mozbase/mozcrash/tests/test.py index d14ebb938d9..40d9c7e8f8b 100644 --- a/testing/mozbase/mozcrash/tests/test.py +++ b/testing/mozbase/mozcrash/tests/test.py @@ -8,7 +8,7 @@ import os, unittest, subprocess, tempfile, shutil, urlparse, zipfile, StringIO import mozcrash, mozlog, mozhttpd # Make logs go away -log = mozlog.getLogger("mozcrash", handler=mozlog.FileHandler(os.devnull)) +log = mozlog.getLogger("mozcrash", os.devnull) def popen_factory(stdouts): """ diff --git a/testing/mozbase/mozdevice/mozdevice/__init__.py b/testing/mozbase/mozdevice/mozdevice/__init__.py index ea11ab985b7..5753225413d 100644 --- a/testing/mozbase/mozdevice/mozdevice/__init__.py +++ b/testing/mozbase/mozdevice/mozdevice/__init__.py @@ -2,7 +2,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. -from devicemanager import DeviceManager, DMError, ZeroconfListener +from devicemanager import DeviceManager, DMError from devicemanagerADB import DeviceManagerADB from devicemanagerSUT import DeviceManagerSUT from droid import DroidADB, DroidSUT, DroidConnectByHWID diff --git a/testing/mozbase/mozdevice/mozdevice/devicemanager.py b/testing/mozbase/mozdevice/mozdevice/devicemanager.py index 6e38f386f05..b6be6476669 100644 --- a/testing/mozbase/mozdevice/mozdevice/devicemanager.py +++ b/testing/mozbase/mozdevice/mozdevice/devicemanager.py @@ -6,7 +6,6 @@ import hashlib import mozlog import socket import os -import posixpath import re import struct import StringIO @@ -239,14 +238,15 @@ class DeviceManager(object): WARNING: does not create last part of the path. For example, if asked to create `/mnt/sdcard/foo/bar/baz`, it will only create `/mnt/sdcard/foo/bar` """ - filename = posixpath.normpath(filename) - containing = posixpath.dirname(filename) - if not self.dirExists(containing): + dirParts = filename.rsplit('/', 1) + if not self.dirExists(dirParts[0]): parts = filename.split('/') - name = "/" - for part in parts[:-1]: + name = "" + for part in parts: + if part is parts[-1]: + break if part != "": - name = posixpath.join(name, part) + name += '/' + part self.mkDir(name) # mkDir will check previous existence @abstractmethod @@ -258,8 +258,7 @@ class DeviceManager(object): @abstractmethod def fileExists(self, filepath): """ - Return whether filepath exists on the device file system, - regardless of file type. + Return whether filepath exists and is a file on the device file system. """ @abstractmethod diff --git a/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py b/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py index 94e12c4de93..afcf8ad8d45 100644 --- a/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py +++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py @@ -156,35 +156,6 @@ class DeviceManagerADB(DeviceManager): return None - def forward(self, local, remote): - """ - Forward socket connections. - - Forward specs are one of: - tcp: - localabstract: - localreserved: - localfilesystem: - dev: - jdwp: (remote only) - """ - return self._checkCmd(['forward', local, remote]) - - def remount(self): - "Remounts the /system partition on the device read-write." - return self._checkCmd(['remount']) - - def devices(self): - "Return a list of connected devices as (serial, status) tuples." - proc = self._runCmd(['devices']) - proc.stdout.readline() # ignore first line of output - devices = [] - for line in iter(proc.stdout.readline, ''): - result = re.match('(.*?)\t(.*)', line) - if result: - devices.append((result.group(1), result.group(2))) - return devices - def _connectRemoteADB(self): self._checkCmd(["connect", self.host + ":" + str(self.port)]) @@ -430,7 +401,7 @@ class DeviceManagerADB(DeviceManager): elif offset is not None: f.seek(offset) ret = f.read() - else: + else: ret = f.read() f.close() @@ -521,10 +492,12 @@ class DeviceManagerADB(DeviceManager): raise DMError("Failed to get application root for: %s" % packageName) def reboot(self, wait = False, **kwargs): - self._checkCmd(["reboot"]) - if not wait: + self._runCmd(["reboot"]) + if (not wait): return - self._checkCmd(["wait-for-device", "shell", "ls", "/sbin"]) + countdown = 40 + while (countdown > 0): + self._checkCmd(["wait-for-device", "shell", "ls", "/sbin"]) def updateApp(self, appBundlePath, **kwargs): return self._runCmd(["install", "-r", appBundlePath]).stdout.read() @@ -533,7 +506,7 @@ class DeviceManagerADB(DeviceManager): timestr = self._runCmd(["shell", "date", "+%s"]).stdout.read().strip() if (not timestr or not timestr.isdigit()): raise DMError("Unable to get current time using date (got: '%s')" % timestr) - return int(timestr)*1000 + return str(int(timestr)*1000) def getInfo(self, directive=None): ret = {} diff --git a/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py b/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py index 0e03115fbc9..acf4c8262c6 100644 --- a/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py +++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py @@ -368,13 +368,15 @@ class DeviceManagerSUT(DeviceManager): existentDirectories = [] for root, dirs, files in os.walk(localDir, followlinks=True): - _, subpath = root.split(localDir) - subpath = subpath.lstrip('/') - remoteRoot = posixpath.join(remoteDir, subpath) + parts = root.split(localDir) for f in files: - remoteName = posixpath.join(remoteRoot, f) + remoteRoot = remoteDir + '/' + parts[1] + if (remoteRoot.endswith('/')): + remoteName = remoteRoot + f + else: + remoteName = remoteRoot + '/' + f - if subpath == "": + if (parts[1] == ""): remoteRoot = remoteDir parent = os.path.dirname(remoteName) @@ -395,24 +397,19 @@ class DeviceManagerSUT(DeviceManager): def fileExists(self, filepath): # Because we always have / style paths we make this a lot easier with some # assumptions - filepath = posixpath.normpath(filepath) - # / should always exist but we can use this to check for things like - # having access to the filesystem - if filepath == '/': - return self.dirExists(filepath) - (containingpath, filename) = posixpath.split(filepath) - return filename in self.listFiles(containingpath) + s = filepath.split('/') + containingpath = '/'.join(s[:-1]) + return s[-1] in self.listFiles(containingpath) def listFiles(self, rootdir): - rootdir = posixpath.normpath(rootdir) - if not self.dirExists(rootdir): + rootdir = rootdir.rstrip('/') + if (self.dirExists(rootdir) == False): return [] data = self._runCmds([{ 'cmd': 'cd ' + rootdir }, { 'cmd': 'ls' }]) files = filter(lambda x: x, data.splitlines()) if len(files) == 1 and files[0] == '': - # special case on the agent: empty directories return just the - # string "" + # special case on the agent: empty directories return just the string "" return [] return files @@ -877,7 +874,7 @@ class DeviceManagerSUT(DeviceManager): self._logger.debug("updateApp: got status back: %s" % status) def getCurrentTime(self): - return int(self._runCmds([{ 'cmd': 'clok' }]).strip()) + return self._runCmds([{ 'cmd': 'clok' }]).strip() def _getCallbackIpAndPort(self, aIp, aPort): """ diff --git a/testing/mozbase/mozdevice/setup.py b/testing/mozbase/mozdevice/setup.py index 4d9386bc036..4b7ada51e81 100644 --- a/testing/mozbase/mozdevice/setup.py +++ b/testing/mozbase/mozdevice/setup.py @@ -4,7 +4,7 @@ from setuptools import setup -PACKAGE_VERSION = '0.28' +PACKAGE_VERSION = '0.27' setup(name='mozdevice', version=PACKAGE_VERSION, diff --git a/testing/mozbase/mozdevice/sut_tests/test_fileExists.py b/testing/mozbase/mozdevice/sut_tests/test_fileExists.py deleted file mode 100644 index d888306a255..00000000000 --- a/testing/mozbase/mozdevice/sut_tests/test_fileExists.py +++ /dev/null @@ -1,37 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -import tempfile -import posixpath - -from dmunit import DeviceManagerTestCase - -class FileExistsTestCase(DeviceManagerTestCase): - """This tests the "fileExists" command. - """ - - def testOnRoot(self): - self.assertTrue(self.dm.fileExists('/')) - - def testOnNonexistent(self): - self.assertFalse(self.dm.fileExists('/doesNotExist')) - - def testOnRegularFile(self): - remote_path = posixpath.join(self.dm.getDeviceRoot(), 'testFile') - self.assertFalse(self.dm.fileExists(remote_path)) - with tempfile.NamedTemporaryFile() as f: - self.dm.pushFile(f.name, remote_path) - self.assertTrue(self.dm.fileExists(remote_path)) - self.dm.removeFile(remote_path) - - def testOnDirectory(self): - remote_path = posixpath.join(self.dm.getDeviceRoot(), 'testDir') - remote_path_file = posixpath.join(remote_path, 'testFile') - self.assertFalse(self.dm.fileExists(remote_path)) - with tempfile.NamedTemporaryFile() as f: - self.dm.pushFile(f.name, remote_path_file) - self.assertTrue(self.dm.fileExists(remote_path)) - self.dm.removeFile(remote_path_file) - self.dm.removeDir(remote_path) - diff --git a/testing/mozbase/mozdevice/tests/manifest.ini b/testing/mozbase/mozdevice/tests/manifest.ini index 8e67f66a433..d9682256b9c 100644 --- a/testing/mozbase/mozdevice/tests/manifest.ini +++ b/testing/mozbase/mozdevice/tests/manifest.ini @@ -1,21 +1,9 @@ [DEFAULT] skip-if = os == 'win' -[sut_app.py] [sut_basic.py] -[sut_chmod.py] -[sut_fileExists.py] -[sut_fileMethods.py] -[sut_info.py] -[sut_ip.py] -[sut_kill.py] -[sut_list.py] -[sut_logcat.py] [sut_mkdir.py] -[sut_ps.py] [sut_push.py] [sut_pull.py] -[sut_remove.py] -[sut_time.py] -[sut_unpackfile.py] -[droidsut_launch.py] +[sut_ps.py] +[droidsut_launch.py] \ No newline at end of file diff --git a/testing/mozbase/mozdevice/tests/sut_app.py b/testing/mozbase/mozdevice/tests/sut_app.py deleted file mode 100644 index a28f753d0f9..00000000000 --- a/testing/mozbase/mozdevice/tests/sut_app.py +++ /dev/null @@ -1,20 +0,0 @@ -#/usr/bin/env python -import mozdevice -import mozlog -import unittest -from sut import MockAgent - - -class TestApp(unittest.TestCase): - - def test_getAppRoot(self): - command = [("getapproot org.mozilla.firefox", - "/data/data/org.mozilla.firefox")] - - m = MockAgent(self, commands=command) - d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG) - - self.assertEqual(command[0][1], d.getAppRoot('org.mozilla.firefox')) - -if __name__ == '__main__': - unittest.main() diff --git a/testing/mozbase/mozdevice/tests/sut_chmod.py b/testing/mozbase/mozdevice/tests/sut_chmod.py deleted file mode 100644 index d0d6694d370..00000000000 --- a/testing/mozbase/mozdevice/tests/sut_chmod.py +++ /dev/null @@ -1,21 +0,0 @@ -#/usr/bin/env python -import mozdevice -import mozlog -import unittest -from sut import MockAgent - - -class TestChmod(unittest.TestCase): - - def test_chmod(self): - - command = [('chmod /mnt/sdcard/test', 'Changing permissions for /storage/emulated/legacy/Test\n' - ' \n' - 'chmod /storage/emulated/legacy/Test ok\n')] - m = MockAgent(self, commands=command) - d = mozdevice.DroidSUT('127.0.0.1', port=m.port, logLevel=mozlog.DEBUG) - - self.assertEqual(None, d.chmodDir('/mnt/sdcard/test')) - -if __name__ == '__main__': - unittest.main() diff --git a/testing/mozbase/mozdevice/tests/sut_fileExists.py b/testing/mozbase/mozdevice/tests/sut_fileExists.py deleted file mode 100644 index aa7157b394c..00000000000 --- a/testing/mozbase/mozdevice/tests/sut_fileExists.py +++ /dev/null @@ -1,29 +0,0 @@ -from sut import MockAgent -import mozdevice -import unittest - -class FileExistsTest(unittest.TestCase): - - commands = [('isdir /', 'TRUE'), - ('cd /', ''), - ('ls', 'init')] - - def test_onRoot(self): - root_commands = [('isdir /', 'TRUE')] - a = MockAgent(self, commands=root_commands) - d = mozdevice.DroidSUT("127.0.0.1", port=a.port) - self.assertTrue(d.fileExists('/')) - - def test_onNonexistent(self): - a = MockAgent(self, commands=self.commands) - d = mozdevice.DroidSUT("127.0.0.1", port=a.port) - self.assertFalse(d.fileExists('/doesNotExist')) - - def test_onRegularFile(self): - a = MockAgent(self, commands=self.commands) - d = mozdevice.DroidSUT("127.0.0.1", port=a.port) - self.assertTrue(d.fileExists('/init')) - -if __name__ == '__main__': - unittest.main() - diff --git a/testing/mozbase/mozdevice/tests/sut_fileMethods.py b/testing/mozbase/mozdevice/tests/sut_fileMethods.py deleted file mode 100644 index cef24d91fc6..00000000000 --- a/testing/mozbase/mozdevice/tests/sut_fileMethods.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python - -import hashlib -import mock -import mozdevice -import mozlog -import shutil -import tempfile -import os -import unittest -from sut import MockAgent - - -class TestFileMethods(unittest.TestCase): - """ Class to test misc file methods """ - - content = "What is the answer to the life, universe and everything? 42" - h = hashlib.md5() - h.update(content) - temp_hash = h.hexdigest() - - def test_validateFile(self): - - with tempfile.NamedTemporaryFile() as f: - f.write(self.content) - f.flush() - - # Test Valid Hashes - commands_valid = [("hash /sdcard/test/file", self.temp_hash)] - - m = MockAgent(self, commands=commands_valid) - d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG) - self.assertTrue(d.validateFile('/sdcard/test/file', f.name)) - - # Test invalid hashes - commands_invalid = [("hash /sdcard/test/file", "0this0hash0is0completely0invalid")] - - m = MockAgent(self, commands=commands_invalid) - d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG) - self.assertFalse(d.validateFile('/sdcard/test/file', f.name)) - - def test_getFile(self): - - fname = "/mnt/sdcard/file" - commands = [("pull %s" % fname, "%s,%s\n%s" % (fname, len(self.content), self.content)), - ("hash %s" % fname, self.temp_hash)] - - with tempfile.NamedTemporaryFile() as f: - m = MockAgent(self, commands=commands) - d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG) - # No error means success - self.assertEqual(None, d.getFile(fname, f.name)) - - def test_getDirectory(self): - - fname = "/mnt/sdcard/file" - commands = [("isdir /mnt/sdcard", "TRUE"), - ("isdir /mnt/sdcard", "TRUE"), - ("cd /mnt/sdcard", ""), - ("ls", "file"), - ("isdir %s" % fname, "FALSE"), - ("pull %s" % fname, "%s,%s\n%s" % (fname, len(self.content), self.content)), - ("hash %s" % fname, self.temp_hash)] - - tmpdir = tempfile.mkdtemp() - m = MockAgent(self, commands=commands) - d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG) - self.assertEqual(None, d.getDirectory("/mnt/sdcard", tmpdir)) - - # Cleanup - shutil.rmtree(tmpdir) - -if __name__ == '__main__': - unittest.main() diff --git a/testing/mozbase/mozdevice/tests/sut_info.py b/testing/mozbase/mozdevice/tests/sut_info.py deleted file mode 100644 index bb13be9721a..00000000000 --- a/testing/mozbase/mozdevice/tests/sut_info.py +++ /dev/null @@ -1,49 +0,0 @@ -#/usr/bin/env python -import mozdevice -import mozlog -import re -import unittest -from sut import MockAgent - - -class TestGetInfo(unittest.TestCase): - - commands = {'os': ('info os', 'JDQ39'), - 'id': ('info id', '11:22:33:44:55:66'), - 'uptime': ('info uptime', '0 days 0 hours 7 minutes 0 seconds 0 ms'), - 'uptimemillis': ('info uptimemillis', '666'), - 'systime': ('info systime', '2013/04/2 12:42:00:007'), - 'screen': ('info screen', 'X:768 Y:1184'), - 'rotation': ('info rotation', 'ROTATION:0'), - 'memory': ('info memory', 'PA:1351032832, FREE: 878645248'), - 'process': ('info process', '1000 527 system\n' - '10091 3443 org.mozilla.firefox\n' - '10112 3137 com.mozilla.SUTAgentAndroid\n' - '10035 807 com.android.launcher'), - 'disk': ('info disk', '/data: 6084923392 total, 980922368 available\n' - '/system: 867999744 total, 332333056 available\n' - '/mnt/sdcard: 6084923392 total, 980922368 available'), - 'power': ('info power', 'Power status:\n' - ' AC power OFFLINE\n' - ' Battery charge LOW DISCHARGING\n' - ' Remaining charge: 20%\n' - ' Battery Temperature: 25.2 (c)'), - 'sutuserinfo': ('info sutuserinfo', 'User Serial:0'), - 'temperature': ('info temperature', 'Temperature: unknown') - } - - def test_getInfo(self): - - for directive in self.commands.keys(): - m = MockAgent(self, commands=[self.commands[directive]]) - d = mozdevice.DroidSUT('127.0.0.1', port=m.port, logLevel=mozlog.DEBUG) - - expected = re.sub(r'\ +', ' ', self.commands[directive][1]).split('\n') - # Account for slightly different return format for 'process' - if directive is 'process': - expected = [[x] for x in expected] - - self.assertEqual(d.getInfo(directive=directive)[directive], expected) - -if __name__ == '__main__': - unittest.main() diff --git a/testing/mozbase/mozdevice/tests/sut_ip.py b/testing/mozbase/mozdevice/tests/sut_ip.py deleted file mode 100644 index d8dafb953b7..00000000000 --- a/testing/mozbase/mozdevice/tests/sut_ip.py +++ /dev/null @@ -1,37 +0,0 @@ -#/usr/bin/env python -import mozdevice -import mozlog -import unittest -from sut import MockAgent - - -class TestGetIP(unittest.TestCase): - """ class to test IP methods """ - - commands = [('exec ifconfig eth0', 'eth0: ip 192.168.0.1 ' - 'mask 255.255.255.0 flags [up broadcast running multicast]\n' - 'return code [0]'), - ('exec ifconfig wlan0', 'wlan0: ip 10.1.39.126\n' - 'mask 255.255.0.0 flags [up broadcast running multicast]\n' - 'return code [0]'), - ('exec ifconfig fake0', '##AGENT-WARNING## [ifconfig] ' - 'command with arg(s) = [fake0] is currently not implemented.') - ] - - def test_getIP_eth0(self): - m = MockAgent(self, commands=[self.commands[0]]) - d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG) - self.assertEqual('192.168.0.1', d.getIP(interfaces=['eth0'])) - - def test_getIP_wlan0(self): - m = MockAgent(self, commands=[self.commands[1]]) - d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG) - self.assertEqual('10.1.39.126', d.getIP(interfaces=['wlan0'])) - - def test_getIP_error(self): - m = MockAgent(self, commands=[self.commands[2]]) - d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG) - self.assertRaises(mozdevice.DMError, d.getIP, interfaces=['fake0']) - -if __name__ == '__main__': - unittest.main() diff --git a/testing/mozbase/mozdevice/tests/sut_kill.py b/testing/mozbase/mozdevice/tests/sut_kill.py deleted file mode 100644 index c5cd44a8949..00000000000 --- a/testing/mozbase/mozdevice/tests/sut_kill.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python - -import mozdevice -import mozlog -import unittest -from sut import MockAgent - - -class TestKill(unittest.TestCase): - - def test_killprocess(self): - commands = [("ps", "1000 1486 com.android.settings\n" - "10016 420 com.android.location.fused\n" - "10023 335 com.android.systemui\n"), - ("kill com.android.settings", - "Successfully killed com.android.settings\n")] - m = MockAgent(self, commands=commands) - d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG) - # No error raised means success - self.assertEqual(None, d.killProcess("com.android.settings")) - - -if __name__ == '__main__': - unittest.main() diff --git a/testing/mozbase/mozdevice/tests/sut_list.py b/testing/mozbase/mozdevice/tests/sut_list.py deleted file mode 100644 index 6f76ea1c979..00000000000 --- a/testing/mozbase/mozdevice/tests/sut_list.py +++ /dev/null @@ -1,22 +0,0 @@ -#/usr/bin/env python -import mozdevice -import mozlog -import unittest -from sut import MockAgent - - -class TestListFiles(unittest.TestCase): - commands = [("isdir /mnt/sdcard", "TRUE"), - ("cd /mnt/sdcard", ""), - ("ls", "Android\nMusic\nPodcasts\nRingtones\nAlarms\n" - "Notifications\nPictures\nMovies\nDownload\nDCIM\n")] - - def test_listFiles(self): - m = MockAgent(self, commands=self.commands) - d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG) - - expected = (self.commands[2][1].strip()).split("\n") - self.assertEqual(expected, d.listFiles("/mnt/sdcard")) - -if __name__ == '__main__': - unittest.main() diff --git a/testing/mozbase/mozdevice/tests/sut_logcat.py b/testing/mozbase/mozdevice/tests/sut_logcat.py deleted file mode 100644 index 9cbb80cf178..00000000000 --- a/testing/mozbase/mozdevice/tests/sut_logcat.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python - -import mozdevice -import mozlog -import unittest -from sut import MockAgent - - -class TestLogCat(unittest.TestCase): - """ Class to test methods assosiated with logcat """ - - def test_getLogcat(self): - - logcat_output = ("07-17 00:51:10.377 I/SUTAgentAndroid( 2933): onCreate\n\r" - "07-17 00:51:10.457 D/dalvikvm( 2933): GC_CONCURRENT freed 351K, 17% free 2523K/3008K, paused 5ms+2ms, total 38ms\n\r" - "07-17 00:51:10.497 I/SUTAgentAndroid( 2933): Caught exception creating file in /data/local/tmp: open failed: EACCES (Permission denied)\n\r" - "07-17 00:51:10.507 E/SUTAgentAndroid( 2933): ERROR: Cannot access world writeable test root\n\r" - "07-17 00:51:10.547 D/GeckoHealthRec( 3253): Initializing profile cache.\n\r" - "07-17 00:51:10.607 D/GeckoHealthRec( 3253): Looking for /data/data/org.mozilla.fennec/files/mozilla/c09kfhne.default/times.json\n\r" - "07-17 00:51:10.637 D/GeckoHealthRec( 3253): Using times.json for profile creation time.\n\r" - "07-17 00:51:10.707 D/GeckoHealthRec( 3253): Incorporating environment: times.json profile creation = 1374026758604\n\r" - "07-17 00:51:10.507 D/GeckoHealthRec( 3253): Requested prefs.\n\r" - "07-17 06:50:54.907 I/SUTAgentAndroid( 3876): \n\r" - "07-17 06:50:54.907 I/SUTAgentAndroid( 3876): Total Private Dirty Memory 3176 kb\n\r" - "07-17 06:50:54.907 I/SUTAgentAndroid( 3876): Total Proportional Set Size Memory 5679 kb\n\r" - "07-17 06:50:54.907 I/SUTAgentAndroid( 3876): Total Shared Dirty Memory 9216 kb\n\r" - "07-17 06:55:21.627 I/SUTAgentAndroid( 3876): 127.0.0.1 : execsu /system/bin/logcat -v time -d dalvikvm:I " - "ConnectivityService:S WifiMonitor:S WifiStateTracker:S wpa_supplicant:S NetworkStateTracker:S\n\r" - "07-17 06:55:21.827 I/dalvikvm-heap( 3876): Grow heap (frag case) to 3.019MB for 102496-byte allocation\n\r" - "return code [0]") - - inp = ("execsu /system/bin/logcat -v time -d " - "dalvikvm:I ConnectivityService:S WifiMonitor:S " - "WifiStateTracker:S wpa_supplicant:S NetworkStateTracker:S") - - commands = [(inp, logcat_output)] - m = MockAgent(self, commands=commands) - d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG) - self.assertEqual(logcat_output[:-17].split('\r'), d.getLogcat()) - - def test_recordLogcat(self): - - commands = [("execsu /system/bin/logcat -c", "return code [0]")] - - m = MockAgent(self, commands=commands) - d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG) - # No error raised means success - self.assertEqual(None, d.recordLogcat()) - -if __name__ == '__main__': - unittest.main() diff --git a/testing/mozbase/mozdevice/tests/sut_mkdir.py b/testing/mozbase/mozdevice/tests/sut_mkdir.py index 43ab0d251d8..14cc658474f 100644 --- a/testing/mozbase/mozdevice/tests/sut_mkdir.py +++ b/testing/mozbase/mozdevice/tests/sut_mkdir.py @@ -59,15 +59,6 @@ class MkDirsTest(unittest.TestCase): d.mkDirs('/mnt/sdcard/foo/foo') a.wait() - def test_mkdirs_on_root(self): - cmds = [('isdir /', 'TRUE')] - a = MockAgent(self, commands=cmds) - d = mozdevice.DroidSUT('127.0.0.1', port=a.port, - logLevel=mozlog.DEBUG) - d.mkDirs('/foo') - - a.wait() - if __name__ == '__main__': unittest.main() diff --git a/testing/mozbase/mozdevice/tests/sut_push.py b/testing/mozbase/mozdevice/tests/sut_push.py index 8abd5a7c787..8f2e7b7269c 100644 --- a/testing/mozbase/mozdevice/tests/sut_push.py +++ b/testing/mozbase/mozdevice/tests/sut_push.py @@ -44,21 +44,21 @@ class PushTest(unittest.TestCase): f.write(pushfile) f.flush() - subTests = [ { 'cmds': [ ("isdir /mnt/sdcard/baz", "TRUE"), - ("isdir /mnt/sdcard/baz", "TRUE"), - ("push /mnt/sdcard/baz/%s %s\r\n%s" % + subTests = [ { 'cmds': [ ("isdir /mnt/sdcard//baz", "TRUE"), + ("isdir /mnt/sdcard//baz", "TRUE"), + ("push /mnt/sdcard//baz/%s %s\r\n%s" % (os.path.basename(f.name), len(pushfile), pushfile), expectedFileResponse) ], 'expectException': False }, - { 'cmds': [ ("isdir /mnt/sdcard/baz", "TRUE"), - ("isdir /mnt/sdcard/baz", "TRUE"), - ("push /mnt/sdcard/baz/%s %s\r\n%s" % + { 'cmds': [ ("isdir /mnt/sdcard//baz", "TRUE"), + ("isdir /mnt/sdcard//baz", "TRUE"), + ("push /mnt/sdcard//baz/%s %s\r\n%s" % (os.path.basename(f.name), len(pushfile), pushfile), "BADHASH") ], 'expectException': True }, - { 'cmds': [ ("isdir /mnt/sdcard/baz", "FALSE"), + { 'cmds': [ ("isdir /mnt/sdcard//baz", "FALSE"), ("isdir /mnt", "FALSE"), ("mkdr /mnt", "##AGENT-WARNING## Could not create the directory /mnt") ], diff --git a/testing/mozbase/mozdevice/tests/sut_remove.py b/testing/mozbase/mozdevice/tests/sut_remove.py deleted file mode 100644 index 65d289d35f5..00000000000 --- a/testing/mozbase/mozdevice/tests/sut_remove.py +++ /dev/null @@ -1,24 +0,0 @@ -#/usr/bin/env python -import mozdevice -import mozlog -import unittest -from sut import MockAgent - - -class TestRemove(unittest.TestCase): - - def test_removeDir(self): - commands = [("isdir /mnt/sdcard/test", "TRUE"), - ("rmdr /mnt/sdcard/test", "Deleting file(s) from " - "/storage/emulated/legacy/Moztest\n" - " \n" - "Deleting directory " - "/storage/emulated/legacy/Moztest\n")] - - m = MockAgent(self, commands=commands) - d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG) - # No error implies we're all good - self.assertEqual(None, d.removeDir("/mnt/sdcard/test")) - -if __name__ == '__main__': - unittest.main() diff --git a/testing/mozbase/mozdevice/tests/sut_time.py b/testing/mozbase/mozdevice/tests/sut_time.py deleted file mode 100644 index 0136d0e47bf..00000000000 --- a/testing/mozbase/mozdevice/tests/sut_time.py +++ /dev/null @@ -1,18 +0,0 @@ -#/usr/bin/env python -import mozdevice -import mozlog -import unittest -from sut import MockAgent - - -class TestGetCurrentTime(unittest.TestCase): - - def test_getCurrentTime(self): - command = [('clok', '1349980200')] - - m = MockAgent(self, commands=command) - d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG) - self.assertEqual(d.getCurrentTime(), int(command[0][1])) - -if __name__ == '__main__': - unittest.main() diff --git a/testing/mozbase/mozdevice/tests/sut_unpackfile.py b/testing/mozbase/mozdevice/tests/sut_unpackfile.py deleted file mode 100644 index 2235b597da4..00000000000 --- a/testing/mozbase/mozdevice/tests/sut_unpackfile.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python - -import mozdevice -import mozlog -import unittest -from sut import MockAgent - - -class TestUnpack(unittest.TestCase): - - def test_unpackFile(self): - - commands = [("isdir /mnt/sdcard/tests", "TRUE"), - ("unzp /data/test/sample.zip /data/test/", - "Checksum: 653400271\n" - "1 of 1 successfully extracted\n")] - m = MockAgent(self, commands=commands) - d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG) - # No error being thrown imples all is well - self.assertEqual(None, d.unpackFile("/data/test/sample.zip", - "/data/test/")) - -if __name__ == '__main__': - unittest.main() diff --git a/testing/mozbase/mozfile/mozfile/mozfile.py b/testing/mozbase/mozfile/mozfile/mozfile.py index 3405c3f432a..582db221865 100644 --- a/testing/mozbase/mozfile/mozfile/mozfile.py +++ b/testing/mozbase/mozfile/mozfile/mozfile.py @@ -2,9 +2,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -from contextlib import contextmanager import os -import shutil import tarfile import tempfile import urlparse @@ -17,8 +15,7 @@ __all__ = ['extract_tarball', 'is_url', 'load', 'rmtree', - 'NamedTemporaryFile', - 'TemporaryDirectory'] + 'NamedTemporaryFile'] ### utilities for extracting archives @@ -52,8 +49,7 @@ def extract_zip(src, dest): for name in namelist: filename = os.path.realpath(os.path.join(dest, name)) if name.endswith('/'): - if not os.path.isdir(filename): - os.makedirs(filename) + os.makedirs(filename) else: path = os.path.dirname(filename) if not os.path.isdir(path): @@ -93,16 +89,14 @@ def extract(src, dest=None): src) # namelist returns paths with forward slashes even in windows - top_level_files = [os.path.join(dest, name.rstrip('/')) for name in namelist + top_level_files = [os.path.join(dest, name) for name in namelist if len(name.rstrip('/').split('/')) == 1] # namelist doesn't include folders, append these to the list for name in namelist: - index = name.find('/') - if index != -1: - root = os.path.join(dest, name[:index]) - if root not in top_level_files: - top_level_files.append(root) + root = os.path.join(dest, name[:name.find('/')]) + if root not in top_level_files: + top_level_files.append(root) return top_level_files @@ -238,19 +232,3 @@ def load(resource): return file(resource) return urllib2.urlopen(resource) - -@contextmanager -def TemporaryDirectory(): - """ - create a temporary directory using tempfile.mkdtemp, and then clean it up. - - Example usage: - with TemporaryDirectory() as tmp: - open(os.path.join(tmp, "a_temp_file"), "w").write("data") - - """ - tempdir = tempfile.mkdtemp() - try: - yield tempdir - finally: - shutil.rmtree(tempdir) diff --git a/testing/mozbase/mozfile/setup.py b/testing/mozbase/mozfile/setup.py index bcd082a1132..6689995452c 100644 --- a/testing/mozbase/mozfile/setup.py +++ b/testing/mozbase/mozfile/setup.py @@ -4,7 +4,7 @@ from setuptools import setup -PACKAGE_VERSION = '0.10' +PACKAGE_VERSION = '0.7' setup(name='mozfile', version=PACKAGE_VERSION, @@ -14,7 +14,7 @@ setup(name='mozfile', keywords='mozilla', author='Mozilla Automation and Tools team', author_email='tools@lists.mozilla.org', - url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase', + url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase', license='MPL', packages=['mozfile'], include_package_data=True, diff --git a/testing/mozbase/mozfile/tests/manifest.ini b/testing/mozbase/mozfile/tests/manifest.ini index ce337e48fc8..008b38bfb1a 100644 --- a/testing/mozbase/mozfile/tests/manifest.ini +++ b/testing/mozbase/mozfile/tests/manifest.ini @@ -1,6 +1,4 @@ -[test_extract.py] +[test.py] [test_load.py] -[test_rmtree.py] -[test_tempdir.py] [test_tempfile.py] [test_url.py] diff --git a/testing/mozbase/mozfile/tests/stubs.py b/testing/mozbase/mozfile/tests/stubs.py deleted file mode 100644 index ee7c1168743..00000000000 --- a/testing/mozbase/mozfile/tests/stubs.py +++ /dev/null @@ -1,34 +0,0 @@ -import os -import shutil -import tempfile - - -# stub file paths -files = [('foo.txt',), - ('foo', 'bar.txt'), - ('foo', 'bar', 'fleem.txt'), - ('foobar', 'fleem.txt'), - ('bar.txt')] - - -def create_stub(): - """create a stub directory""" - - tempdir = tempfile.mkdtemp() - try: - for path in files: - fullpath = os.path.join(tempdir, *path) - dirname = os.path.dirname(fullpath) - if not os.path.exists(dirname): - os.makedirs(dirname) - contents = path[-1] - f = file(fullpath, 'w') - f.write(contents) - f.close() - return tempdir - except Exception, e: - try: - shutil.rmtree(tempdir) - except: - pass - raise e diff --git a/testing/mozbase/mozfile/tests/test_extract.py b/testing/mozbase/mozfile/tests/test.py old mode 100644 new mode 100755 similarity index 70% rename from testing/mozbase/mozfile/tests/test_extract.py rename to testing/mozbase/mozfile/tests/test.py index d9e6e0750b6..e2e344637d7 --- a/testing/mozbase/mozfile/tests/test_extract.py +++ b/testing/mozbase/mozfile/tests/test.py @@ -1,21 +1,53 @@ #!/usr/bin/env python +""" +tests for mozfile +""" + import mozfile import os import shutil import tarfile import tempfile -import stubs import unittest import zipfile +# stub file paths +files = [('foo.txt',), + ('foo', 'bar.txt'), + ('foo', 'bar', 'fleem.txt'), + ('foobar', 'fleem.txt'), + ('bar.txt')] + +def create_stub(): + """create a stub directory""" + + tempdir = tempfile.mkdtemp() + try: + for path in files: + fullpath = os.path.join(tempdir, *path) + dirname = os.path.dirname(fullpath) + if not os.path.exists(dirname): + os.makedirs(dirname) + contents = path[-1] + f = file(fullpath, 'w') + f.write(contents) + f.close() + return tempdir + except Exception, e: + try: + shutil.rmtree(tempdir) + except: + pass + raise e + class TestExtract(unittest.TestCase): """test extracting archives""" def ensure_directory_contents(self, directory): """ensure the directory contents match""" - for f in stubs.files: + for f in files: path = os.path.join(directory, *f) exists = os.path.exists(path) if not exists: @@ -101,11 +133,11 @@ class TestExtract(unittest.TestCase): def create_tarball(self): """create a stub tarball for testing""" - tempdir = stubs.create_stub() + tempdir = create_stub() filename = tempfile.mktemp(suffix='.tar') archive = tarfile.TarFile(filename, mode='w') try: - for path in stubs.files: + for path in files: archive.add(os.path.join(tempdir, *path), arcname=os.path.join(*path)) except: os.remove(archive) @@ -118,11 +150,11 @@ class TestExtract(unittest.TestCase): def create_zip(self): """create a stub zipfile for testing""" - tempdir = stubs.create_stub() + tempdir = create_stub() filename = tempfile.mktemp(suffix='.zip') archive = zipfile.ZipFile(filename, mode='w') try: - for path in stubs.files: + for path in files: archive.write(os.path.join(tempdir, *path), arcname=os.path.join(*path)) except: os.remove(filename) @@ -131,3 +163,28 @@ class TestExtract(unittest.TestCase): shutil.rmtree(tempdir) archive.close() return filename + +class TestRemoveTree(unittest.TestCase): + """test our ability to remove a directory tree""" + + def test_remove_directory(self): + tempdir = create_stub() + self.assertTrue(os.path.exists(tempdir)) + self.assertTrue(os.path.isdir(tempdir)) + try: + mozfile.rmtree(tempdir) + except: + shutil.rmtree(tempdir) + raise + self.assertFalse(os.path.exists(tempdir)) + +class TestNamedTemporaryFile(unittest.TestCase): + """test our fix for NamedTemporaryFile""" + + def test_named_temporary_file(self): + temp = mozfile.NamedTemporaryFile() + temp.write("A simple test") + del temp + +if __name__ == '__main__': + unittest.main() diff --git a/testing/mozbase/mozfile/tests/test_load.py b/testing/mozbase/mozfile/tests/test_load.py index 13a5b519c13..d1d6d62d691 100755 --- a/testing/mozbase/mozfile/tests/test_load.py +++ b/testing/mozbase/mozfile/tests/test_load.py @@ -10,7 +10,6 @@ import tempfile import unittest from mozfile import load - class TestLoad(unittest.TestCase): """test the load function""" @@ -26,16 +25,18 @@ class TestLoad(unittest.TestCase): host = '127.0.0.1' httpd = mozhttpd.MozHttpd(host=host, + port=8888, urlhandlers=[{'method': 'GET', 'path': '.*', 'function': example}]) try: httpd.start(block=False) - content = load(httpd.get_url()).read() + content = load('http://127.0.0.1:8888/foo').read() self.assertEqual(content, 'example') finally: httpd.stop() + def test_file_path(self): """test loading from file path""" try: diff --git a/testing/mozbase/mozfile/tests/test_rmtree.py b/testing/mozbase/mozfile/tests/test_rmtree.py deleted file mode 100644 index 689467afe5d..00000000000 --- a/testing/mozbase/mozfile/tests/test_rmtree.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python - -import mozfile -import mozinfo -import os -import shutil -import tempfile -import unittest -import stubs - - -class TestRemoveTree(unittest.TestCase): - """test our ability to remove a directory tree""" - - def setUp(self): - # Generate a stub - self.tempdir = stubs.create_stub() - - def tearDown(self): - # Cleanup the stub if it sill exists - if os.path.isdir(self.tempdir): - mozfile.rmtree(self.tempdir) - - def test_remove_directory(self): - self.assertTrue(os.path.isdir(self.tempdir)) - try: - mozfile.rmtree(self.tempdir) - except: - shutil.rmtree(self.tempdir) - raise - self.assertFalse(os.path.exists(self.tempdir)) - - def test_remove_directory_with_open_file(self): - """ Tests handling when removing a directory tree - which has a file in it is still open """ - # Open a file in the generated stub - filepath = os.path.join(self.tempdir, *stubs.files[1]) - f = file(filepath, 'w') - f.write('foo-bar') - # keep file open and then try removing the dir-tree - if mozinfo.isWin: - # On the Windows family WindowsError should be raised. - self.assertRaises(WindowsError, mozfile.rmtree, self.tempdir) - else: - # Folder should be deleted on all other platforms - mozfile.rmtree(self.tempdir) - self.assertFalse(os.path.exists(self.tempdir)) - - def test_remove_directory_after_closing_file(self): - """ Test that the call to mozfile.rmtree succeeds on - all platforms after file is closed """ - - filepath = os.path.join(self.tempdir, *stubs.files[1]) - with open(filepath, 'w') as f: - f.write('foo-bar') - # Delete directory tree - mozfile.rmtree(self.tempdir) - # Check deletion is successful - self.assertFalse(os.path.exists(self.tempdir)) diff --git a/testing/mozbase/mozfile/tests/test_tempdir.py b/testing/mozbase/mozfile/tests/test_tempdir.py deleted file mode 100644 index 81f03d095b5..00000000000 --- a/testing/mozbase/mozfile/tests/test_tempdir.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python - -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -""" -tests for mozfile.TemporaryDirectory -""" - -from mozfile import TemporaryDirectory -import os -import unittest - - -class TestTemporaryDirectory(unittest.TestCase): - - def test_removed(self): - """ensure that a TemporaryDirectory gets removed""" - path = None - with TemporaryDirectory() as tmp: - path = tmp - self.assertTrue(os.path.isdir(tmp)) - tmpfile = os.path.join(tmp, "a_temp_file") - open(tmpfile, "w").write("data") - self.assertTrue(os.path.isfile(tmpfile)) - self.assertFalse(os.path.isdir(path)) - self.assertFalse(os.path.exists(path)) - - def test_exception(self): - """ensure that TemporaryDirectory handles exceptions""" - path = None - with self.assertRaises(Exception): - with TemporaryDirectory() as tmp: - path = tmp - self.assertTrue(os.path.isdir(tmp)) - raise Exception("oops") - self.assertFalse(os.path.isdir(path)) - self.assertFalse(os.path.exists(path)) - -if __name__ == '__main__': - unittest.main() diff --git a/testing/mozbase/mozfile/tests/test_tempfile.py b/testing/mozbase/mozfile/tests/test_tempfile.py index 3c3d26d5d6f..5cabd89041f 100644 --- a/testing/mozbase/mozfile/tests/test_tempfile.py +++ b/testing/mozbase/mozfile/tests/test_tempfile.py @@ -12,26 +12,7 @@ import mozfile import os import unittest - class TestNamedTemporaryFile(unittest.TestCase): - """test our fix for NamedTemporaryFile""" - - def test_named_temporary_file(self): - """ Ensure the fix for re-opening a NamedTemporaryFile works - - Refer to https://bugzilla.mozilla.org/show_bug.cgi?id=818777 - and https://bugzilla.mozilla.org/show_bug.cgi?id=821362 - """ - - test_string = "A simple test" - with mozfile.NamedTemporaryFile() as temp: - # Test we can write to file - temp.write(test_string) - # Forced flush, so that we can read later - temp.flush() - - # Test we can open the file again on all platforms - self.assertEqual(open(temp.name).read(), test_string) def test_iteration(self): """ensure the line iterator works""" @@ -52,7 +33,7 @@ class TestNamedTemporaryFile(unittest.TestCase): lines = [] for line in tf: lines.append(line.strip()) - self.assertEqual(lines, []) # because we did not seek(0) + self.assertEqual(lines, []) # because we did not seek(0) tf.seek(0) lines = [] for line in tf: diff --git a/testing/mozbase/mozfile/tests/test_url.py b/testing/mozbase/mozfile/tests/test_url.py index 7d2b12b3997..fa2af3206b7 100755 --- a/testing/mozbase/mozfile/tests/test_url.py +++ b/testing/mozbase/mozfile/tests/test_url.py @@ -7,7 +7,6 @@ tests for is_url import unittest from mozfile import is_url - class TestIsUrl(unittest.TestCase): """test the is_url function""" diff --git a/testing/mozbase/mozhttpd/README.md b/testing/mozbase/mozhttpd/README.md new file mode 100644 index 00000000000..c2f69ddb176 --- /dev/null +++ b/testing/mozbase/mozhttpd/README.md @@ -0,0 +1 @@ +basic python webserver, tested with talos diff --git a/testing/mozbase/mozhttpd/mozhttpd/__init__.py b/testing/mozbase/mozhttpd/mozhttpd/__init__.py index a9c0b06e50e..c9b5d5ef3a2 100644 --- a/testing/mozbase/mozhttpd/mozhttpd/__init__.py +++ b/testing/mozbase/mozhttpd/mozhttpd/__init__.py @@ -2,45 +2,6 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. -""" -Mozhttpd is a simple http webserver written in python, designed expressly -for use in automated testing scenarios. It is designed to both serve static -content and provide simple web services. - -The server is based on python standard library modules such as -SimpleHttpServer, urlparse, etc. The ThreadingMixIn is used to -serve each request on a discrete thread. - -Some existing uses of mozhttpd include Peptest_, Eideticker_, and Talos_. - -.. _Peptest: https://github.com/mozilla/peptest/ - -.. _Eideticker: https://github.com/mozilla/eideticker/ - -.. _Talos: http://hg.mozilla.org/build/ - -The following simple example creates a basic HTTP server which serves -content from the current directory, defines a single API endpoint -`/api/resource/` and then serves requests indefinitely: - -:: - - import mozhttpd - - @mozhttpd.handlers.json_response - def resource_get(request, objid): - return (200, { 'id': objid, - 'query': request.query }) - - - httpd = mozhttpd.MozHttpd(port=8080, docroot='.', - urlhandlers = [ { 'method': 'GET', - 'path': '/api/resources/([^/]+)/?', - 'function': resource_get } ]) - print "Serving '%s' at %s:%s" % (httpd.docroot, httpd.host, httpd.port) - httpd.start(block=True) - -""" - from mozhttpd import MozHttpd, Request, RequestHandler, main from handlers import json_response +import iface diff --git a/testing/mozbase/mozhttpd/mozhttpd/iface.py b/testing/mozbase/mozhttpd/mozhttpd/iface.py new file mode 100644 index 00000000000..44f6d133abd --- /dev/null +++ b/testing/mozbase/mozhttpd/mozhttpd/iface.py @@ -0,0 +1,33 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import socket +if os.name != 'nt': + import fcntl + import struct + +def _get_interface_ip(ifname): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + return socket.inet_ntoa(fcntl.ioctl( + s.fileno(), + 0x8915, # SIOCGIFADDR + struct.pack('256s', ifname[:15]) + )[20:24]) + +def get_lan_ip(): + try: + ip = socket.gethostbyname(socket.gethostname()) + except socket.gaierror: # for Mac OS X + ip = socket.gethostbyname(socket.gethostname() + ".local") + + if ip.startswith("127.") and os.name != "nt": + interfaces = ["eth0", "eth1", "eth2", "wlan0", "wlan1", "wifi0", "ath0", "ath1", "ppp0"] + for ifname in interfaces: + try: + ip = _get_interface_ip(ifname) + break; + except IOError: + pass + return ip diff --git a/testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py b/testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py index c82adf8836a..b7ea21b0c08 100755 --- a/testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py +++ b/testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py @@ -16,7 +16,7 @@ import os import urllib import urlparse import re -import moznetwork +import iface import time from SocketServer import ThreadingMixIn @@ -163,23 +163,14 @@ class RequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): class MozHttpd(object): """ - :param host: Host from which to serve (default 127.0.0.1) - :param port: Port from which to serve (default 8888) - :param docroot: Server root (default os.getcwd()) - :param urlhandlers: Handlers to specify behavior against method and path match (default None) - :param proxy_host_dirs: Toggle proxy behavior (default False) - :param log_requests: Toggle logging behavior (default False) - Very basic HTTP server class. Takes a docroot (path on the filesystem) and a set of urlhandler dictionaries of the form: - :: - - { - 'method': HTTP method (string): GET, POST, or DEL, - 'path': PATH_INFO (regular expression string), - 'function': function of form fn(arg1, arg2, arg3, ..., request) - } + { + 'method': HTTP method (string): GET, POST, or DEL, + 'path': PATH_INFO (regular expression string), + 'function': function of form fn(arg1, arg2, arg3, ..., request) + } and serves HTTP. For each request, MozHttpd will either return a file off the docroot, or dispatch to a handler function (if both path and @@ -201,7 +192,7 @@ class MozHttpd(object): True. """ - def __init__(self, host="127.0.0.1", port=0, docroot=None, + def __init__(self, host="127.0.0.1", port=8888, docroot=None, urlhandlers=None, proxy_host_dirs=False, log_requests=False): self.host = host self.port = int(port) @@ -225,11 +216,9 @@ class MozHttpd(object): def start(self, block=False): """ - Starts the server. - - If `block` is True, the call will not return. If `block` is False, the - server will be started on a separate thread that can be terminated by - a call to stop(). + Start the server. If block is True, the call will not return. + If block is False, the server will be started on a separate thread that + can be terminated by a call to .stop() """ self.httpd = EasyServer((self.host, self.port), self.handler_class) if block: @@ -240,11 +229,6 @@ class MozHttpd(object): self.server.start() def stop(self): - """ - Stops the server. - - If the server is not running, this method has no effect. - """ if self.httpd: ### FIXME: There is no shutdown() method in Python 2.4... try: @@ -253,18 +237,6 @@ class MozHttpd(object): pass self.httpd = None - def get_url(self, path="/"): - """ - Returns a URL that can be used for accessing the server (e.g. http://192.168.1.3:4321/) - - :param path: Path to append to URL (e.g. if path were /foobar.html you would get a URL like - http://192.168.1.3:4321/foobar.html). Default is `/`. - """ - if not self.httpd: - return None - - return "http://%s:%s%s" % (self.host, self.httpd.server_port, path) - __del__ = stop @@ -290,7 +262,7 @@ def main(args=sys.argv[1:]): parser.error("mozhttpd does not take any arguments") if options.external_ip: - host = moznetwork.get_lan_ip() + host = iface.get_lan_ip() else: host = options.host diff --git a/testing/mozbase/mozhttpd/setup.py b/testing/mozbase/mozhttpd/setup.py index db8bb8f5e01..7f027d0c616 100644 --- a/testing/mozbase/mozhttpd/setup.py +++ b/testing/mozbase/mozhttpd/setup.py @@ -2,20 +2,28 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. +import os from setuptools import setup -PACKAGE_VERSION = '0.6' -deps = ['moznetwork >= 0.1'] +try: + here = os.path.dirname(os.path.abspath(__file__)) + description = file(os.path.join(here, 'README.md')).read() +except IOError: + description = None + +PACKAGE_VERSION = '0.5' + +deps = [] setup(name='mozhttpd', version=PACKAGE_VERSION, - description="Python webserver intended for use with Mozilla testing", - long_description="see http://mozbase.readthedocs.org/", + description="basic python webserver, tested with talos", + long_description=description, classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers keywords='mozilla', author='Mozilla Automation and Testing Team', author_email='tools@lists.mozilla.org', - url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase', + url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase', license='MPL', packages=['mozhttpd'], include_package_data=True, diff --git a/testing/mozbase/mozhttpd/tests/baseurl.py b/testing/mozbase/mozhttpd/tests/baseurl.py deleted file mode 100644 index 048a79b7550..00000000000 --- a/testing/mozbase/mozhttpd/tests/baseurl.py +++ /dev/null @@ -1,18 +0,0 @@ -import mozhttpd -import unittest - -class BaseUrlTest(unittest.TestCase): - - def test_base_url(self): - httpd = mozhttpd.MozHttpd(port=0) - self.assertEqual(httpd.get_url(), None) - httpd.start(block=False) - self.assertEqual("http://127.0.0.1:%s/" % httpd.httpd.server_port, - httpd.get_url()) - self.assertEqual("http://127.0.0.1:%s/cheezburgers.html" % \ - httpd.httpd.server_port, - httpd.get_url(path="/cheezburgers.html")) - httpd.stop() - -if __name__ == '__main__': - unittest.main() diff --git a/testing/mozbase/mozhttpd/tests/basic.py b/testing/mozbase/mozhttpd/tests/basic.py deleted file mode 100644 index 8d64b4332e0..00000000000 --- a/testing/mozbase/mozhttpd/tests/basic.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python - -import mozhttpd -import mozfile -import os -import tempfile -import unittest - - -class TestBasic(unittest.TestCase): - """ Test basic Mozhttpd capabilites """ - - def test_basic(self): - """ Test mozhttpd can serve files """ - - tempdir = tempfile.mkdtemp() - - # sizes is a dict of the form: name -> [size, binary_string, filepath] - sizes = {'small': [128], 'large': [16384]} - - for k in sizes.keys(): - # Generate random binary string - sizes[k].append(os.urandom(sizes[k][0])) - - # Add path of file with binary string to list - fpath = os.path.join(tempdir, k) - sizes[k].append(fpath) - - # Write binary string to file - with open(fpath, 'wb') as f: - f.write(sizes[k][1]) - - server = mozhttpd.MozHttpd(docroot=tempdir) - server.start() - server_url = server.get_url() - - # Retrieve file and check contents matchup - for k in sizes.keys(): - retrieved_content = mozfile.load(server_url + k).read() - self.assertEqual(retrieved_content, sizes[k][1]) - - # Cleanup tempdir and related files - mozfile.rmtree(tempdir) - -if __name__ == '__main__': - unittest.main() diff --git a/testing/mozbase/mozhttpd/tests/manifest.ini b/testing/mozbase/mozhttpd/tests/manifest.ini index 4d1eac07196..370125380b9 100644 --- a/testing/mozbase/mozhttpd/tests/manifest.ini +++ b/testing/mozbase/mozhttpd/tests/manifest.ini @@ -1,5 +1,3 @@ -[api.py] -[baseurl.py] -[basic.py] [filelisting.py] +[api.py] [requestlog.py] diff --git a/testing/mozbase/mozinfo/mozinfo/__init__.py b/testing/mozbase/mozinfo/mozinfo/__init__.py index 904dfef71a7..8d293c128ee 100644 --- a/testing/mozbase/mozinfo/mozinfo/__init__.py +++ b/testing/mozbase/mozinfo/mozinfo/__init__.py @@ -51,6 +51,4 @@ Module variables: """ -import mozinfo from mozinfo import * -__all__ = mozinfo.__all__ diff --git a/testing/mozbase/mozinfo/mozinfo/mozinfo.py b/testing/mozbase/mozinfo/mozinfo/mozinfo.py index d54f2bcf5ee..e77cebfb652 100755 --- a/testing/mozbase/mozinfo/mozinfo/mozinfo.py +++ b/testing/mozbase/mozinfo/mozinfo/mozinfo.py @@ -108,12 +108,9 @@ def sanitize(info): # method for updating information def update(new_info): - """ - Update the info. - - :param new_info: Either a dict containing the new info or a path/url - to a json file containing the new info. - """ + """Update the info. + new_info can either be a dict or a path/url + to a json file containing a dict.""" if isinstance(new_info, basestring): f = mozfile.load(new_info) @@ -131,50 +128,13 @@ def update(new_info): if isLinux or isBsd: globals()['isUnix'] = True -def find_and_update_from_json(*dirs): - """ - Find a mozinfo.json file, load it, and update the info with the - contents. - - :param dirs: Directories in which to look for the file. They will be - searched after first looking in the root of the objdir - if the current script is being run from a Mozilla objdir. - - Returns the full path to mozinfo.json if it was found, or None otherwise. - """ - # First, see if we're in an objdir - try: - from mozbuild.base import MozbuildObject - build = MozbuildObject.from_environment() - json_path = _os.path.join(build.topobjdir, "mozinfo.json") - if _os.path.isfile(json_path): - update(json_path) - return json_path - except ImportError: - pass - - for d in dirs: - d = _os.path.abspath(d) - json_path = _os.path.join(d, "mozinfo.json") - if _os.path.isfile(json_path): - update(json_path) - return json_path - - return None - update({}) # exports __all__ = info.keys() __all__ += ['is' + os_name.title() for os_name in choices['os']] -__all__ += [ - 'info', - 'unknown', - 'main', - 'choices', - 'update', - 'find_and_update_from_json', - ] +__all__ += ['info', 'unknown', 'main', 'choices', 'update'] + def main(args=None): diff --git a/testing/mozbase/mozinfo/setup.py b/testing/mozbase/mozinfo/setup.py index 582b0673d4a..0080ee059a7 100644 --- a/testing/mozbase/mozinfo/setup.py +++ b/testing/mozbase/mozinfo/setup.py @@ -4,7 +4,7 @@ from setuptools import setup -PACKAGE_VERSION = '0.6' +PACKAGE_VERSION = '0.5' # dependencies deps = ['mozfile >= 0.6'] @@ -21,7 +21,7 @@ setup(name='mozinfo', keywords='mozilla', author='Mozilla Automation and Testing Team', author_email='tools@lists.mozilla.org', - url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase', + url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase', license='MPL', packages=['mozinfo'], include_package_data=True, diff --git a/testing/mozbase/mozinfo/tests/manifest.ini b/testing/mozbase/mozinfo/tests/manifest.ini deleted file mode 100644 index 528fdea7b65..00000000000 --- a/testing/mozbase/mozinfo/tests/manifest.ini +++ /dev/null @@ -1 +0,0 @@ -[test.py] diff --git a/testing/mozbase/mozinfo/tests/test.py b/testing/mozbase/mozinfo/tests/test.py deleted file mode 100644 index a106f5d8329..00000000000 --- a/testing/mozbase/mozinfo/tests/test.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this file, -# You can obtain one at http://mozilla.org/MPL/2.0/. - -import json -import mock -import os -import shutil -import sys -import tempfile -import unittest -import mozinfo - -class TestMozinfo(unittest.TestCase): - def setUp(self): - reload(mozinfo) - self.tempdir = os.path.abspath(tempfile.mkdtemp()) - - # When running from an objdir mozinfo will use a build generated json file - # instead of the ones created for testing. Prevent that from happening. - # See bug 896038 for details. - sys.modules['mozbuild'] = None - - def tearDown(self): - shutil.rmtree(self.tempdir) - del sys.modules['mozbuild'] - - def test_basic(self): - """Test that mozinfo has a few attributes.""" - self.assertNotEqual(mozinfo.os, None) - # should have isFoo == True where os == "foo" - self.assertTrue(getattr(mozinfo, "is" + mozinfo.os[0].upper() + mozinfo.os[1:])) - - def test_update(self): - """Test that mozinfo.update works.""" - mozinfo.update({"foo": 123}) - self.assertEqual(mozinfo.info["foo"], 123) - - def test_update_file(self): - """Test that mozinfo.update can load a JSON file.""" - j = os.path.join(self.tempdir, "mozinfo.json") - with open(j, "w") as f: - f.write(json.dumps({"foo": "xyz"})) - mozinfo.update(j) - self.assertEqual(mozinfo.info["foo"], "xyz") - - def test_update_file_invalid_json(self): - """Test that mozinfo.update handles invalid JSON correctly""" - j = os.path.join(self.tempdir,'test.json') - with open(j, 'w') as f: - f.write('invalid{"json":') - self.assertRaises(ValueError,mozinfo.update,[j]) - - def test_find_and_update_file(self): - """Test that mozinfo.find_and_update_from_json can - find mozinfo.json in a directory passed to it.""" - j = os.path.join(self.tempdir, "mozinfo.json") - with open(j, "w") as f: - f.write(json.dumps({"foo": "abcdefg"})) - self.assertEqual(mozinfo.find_and_update_from_json(self.tempdir), j) - self.assertEqual(mozinfo.info["foo"], "abcdefg") - - def test_find_and_update_file_invalid_json(self): - """Test that mozinfo.find_and_update_from_json can - handle invalid JSON""" - j = os.path.join(self.tempdir, "mozinfo.json") - with open(j, 'w') as f: - f.write('invalid{"json":') - self.assertRaises(ValueError, mozinfo.find_and_update_from_json, self.tempdir) - - - def test_find_and_update_file_mozbuild(self): - """Test that mozinfo.find_and_update_from_json can - find mozinfo.json using the mozbuild module.""" - j = os.path.join(self.tempdir, "mozinfo.json") - with open(j, "w") as f: - f.write(json.dumps({"foo": "123456"})) - m = mock.MagicMock() - # Mock the value of MozbuildObject.from_environment().topobjdir. - m.MozbuildObject.from_environment.return_value.topobjdir = self.tempdir - with mock.patch.dict(sys.modules, {"mozbuild": m, "mozbuild.base": m}): - self.assertEqual(mozinfo.find_and_update_from_json(), j) - self.assertEqual(mozinfo.info["foo"], "123456") - -if __name__ == '__main__': - unittest.main() diff --git a/testing/mozbase/mozinstall/mozinstall/mozinstall.py b/testing/mozbase/mozinstall/mozinstall/mozinstall.py index 4ff258e5e55..cbbeeeea997 100755 --- a/testing/mozbase/mozinstall/mozinstall/mozinstall.py +++ b/testing/mozbase/mozinstall/mozinstall/mozinstall.py @@ -61,8 +61,7 @@ def get_binary(path, app_name): # On OS X we can get the real binary from the app bundle if mozinfo.isMac: plist = '%s/Contents/Info.plist' % path - if not os.path.isfile(plist): - raise InvalidBinary('%s/Contents/Info.plist not found' % path) + assert os.path.isfile(plist), '"%s" has not been found.' % plist binary = os.path.join(path, 'Contents/MacOS/', readPlist(plist)['CFBundleExecutable']) @@ -206,7 +205,7 @@ def uninstall(install_folder): # Ensure that we remove any trace of the installation. Even the uninstaller # on Windows leaves files behind we have to explicitely remove. - mozfile.rmtree(install_folder) + shutil.rmtree(install_folder) def _install_dmg(src, dest): diff --git a/testing/mozbase/mozinstall/setup.py b/testing/mozbase/mozinstall/setup.py index da5e244d405..9c14f033c7f 100644 --- a/testing/mozbase/mozinstall/setup.py +++ b/testing/mozbase/mozinstall/setup.py @@ -11,7 +11,7 @@ try: except IOError: description = None -PACKAGE_VERSION = '1.7' +PACKAGE_VERSION = '1.6' deps = ['mozinfo >= 0.4', 'mozfile' @@ -33,7 +33,7 @@ setup(name='mozInstall', keywords='mozilla', author='Mozilla Automation and Tools team', author_email='tools@lists.mozilla.org', - url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase', + url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase', license='MPL 2.0', packages=['mozinstall'], include_package_data=True, diff --git a/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.dmg b/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.dmg deleted file mode 100644 index f7f36f6318ace2e4adcbff5d1e2c1129987530f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13441 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 h_d)14^S@#s*WP$58%#mL#jrz0@i*G=;mrU5 diff --git a/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.exe b/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.exe deleted file mode 100644 index c1b0564fde74db63a4cd0c50f4825e058b59e64c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55015 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 diff --git a/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.tar.bz2 b/testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.tar.bz2 deleted file mode 100644 index cb046a0e7ff67feeea570b8c395330d5fce4a2fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2882 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

*(=V!4@`)?UU+<({gl9S^DT9v^4UDMcTcU$kPuw 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 120: - # device hasn't come back online in 2 minutes, something's wrong - raise Exception("Device %s (status: %s) not back online after reboot" % (serial, rstatus)) - time.sleep(5) - rserial, rstatus = self.getDeviceStatus(serial) - self.log.info('device:', serial, 'status:', rstatus) - - def _get_device_status(self, serial=None): - # If we know the device serial number, we look for that, - # otherwise we use the (presumably only) device shown in 'adb devices'. - serial = serial or self.dm._deviceSerial - status = 'unknown' - - proc = subprocess.Popen([self.dm._adbPath, 'devices'], stdout=subprocess.PIPE) - line = proc.stdout.readline() - while line != '': - result = re.match('(.*?)\t(.*)', line) - if result: - thisSerial = result.group(1) - if not serial or thisSerial == serial: - serial = thisSerial - status = result.group(2) - break - line = proc.stdout.readline() - return (serial, status) - - def _wait_for_net(self): - active = False - time_out = 0 - while not active and time_out < 40: - proc = subprocess.Popen([self.dm._adbPath, 'shell', '/system/bin/netcfg']) - proc.stdout.readline() # ignore first line - line = proc.stdout.readline() - while line != "": - if (re.search(r'UP\s+(?:[0-9]{1,3}\.){3}[0-9]{1,3}', line)): - active = True - break - line = proc.stdout.readline() - time_out += 1 - time.sleep(1) - return active - - def _setup_remote_profile(self): - """ - Copy profile and update the remote profiles ini file to point to the new profile - """ - - # copy the profile to the device. - if self.dm.dirExists(self.remote_profile): - self.dm.shellCheckOutput(['rm', '-r', self.remote_profile]) - - try: - self.dm.pushDir(self.profile.profile, self.remote_profile) - except DMError: - self.log.error("Automation Error: Unable to copy profile to device.") - raise - - extension_dir = os.path.join(self.profile.profile, 'extensions', 'staged') - if os.path.isdir(extension_dir): - # Copy the extensions to the B2G bundles dir. - # need to write to read-only dir - subprocess.Popen([self.dm._adbPath, 'remount']).communicate() - for filename in os.listdir(extension_dir): - self.dm.shellCheckOutput(['rm', '-rf', - os.path.join(self.bundles_dir, filename)]) - try: - self.dm.pushDir(extension_dir, self.bundles_dir) - except DMError: - self.log.error("Automation Error: Unable to copy extensions to device.") - raise - - if not self.dm.fileExists(self.remote_profiles_ini): - raise DMError("The profiles.ini file '%s' does not exist on the device" % self.remote_profiles_ini) - - local_profiles_ini = tempfile.NamedTemporaryFile() - self.dm.getFile(self.remote_profiles_ini, local_profiles_ini.name) - - config = ProfileConfigParser() - config.read(local_profiles_ini.name) - for section in config.sections(): - if 'Profile' in section: - config.set(section, 'IsRelative', 0) - config.set(section, 'Path', self.remote_profile) - - new_profiles_ini = tempfile.NamedTemporaryFile() - config.write(open(new_profiles_ini.name, 'w')) - - self.backup_file(self.remote_profiles_ini) - self.dm.pushFile(new_profiles_ini.name, self.remote_profiles_ini) - - # In B2G, user.js is always read from /data/local, not the profile - # directory. Backup the original user.js first so we can restore it. - self.backup_file(self.user_js) - self.dm.pushFile(os.path.join(self.profile.profile, "user.js"), self.user_js) - - def cleanup(self): - super(B2GRunner, self).cleanup() - if getattr(self.marionette, 'instance', False): - self.marionette.instance.close() - del self.marionette - -class ProfileConfigParser(ConfigParser.RawConfigParser): - """Subclass of RawConfigParser that outputs .ini files in the exact - format expected for profiles.ini, which is slightly different - than the default format. - """ - - def optionxform(self, optionstr): - return optionstr - - def write(self, fp): - if self._defaults: - fp.write("[%s]\n" % ConfigParser.DEFAULTSECT) - for (key, value) in self._defaults.items(): - fp.write("%s=%s\n" % (key, str(value).replace('\n', '\n\t'))) - fp.write("\n") - for section in self._sections: - fp.write("[%s]\n" % section) - for (key, value) in self._sections[section].items(): - if key == "__name__": - continue - if (value is not None) or (self._optcre == self.OPTCRE): - key = "=".join((key, str(value).replace('\n', '\n\t'))) - fp.write("%s\n" % (key)) - fp.write("\n") - -remote_runners = {'b2g': 'B2GRunner', - 'fennec': 'FennecRunner'} diff --git a/testing/mozbase/mozrunner/mozrunner/resources/metrotestharness.exe b/testing/mozbase/mozrunner/mozrunner/resources/metrotestharness.exe deleted file mode 100644 index d3bcbfbee9af5f97a6af47287f0f6e22438774bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63488 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 diff --git a/testing/mozbase/mozrunner/mozrunner/runner.py b/testing/mozbase/mozrunner/mozrunner/runner.py index 872895b9786..245fd8e1a35 100755 --- a/testing/mozbase/mozrunner/mozrunner/runner.py +++ b/testing/mozbase/mozrunner/mozrunner/runner.py @@ -3,37 +3,161 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. + +__all__ = ['Runner', 'ThunderbirdRunner', 'FirefoxRunner', 'runners', 'CLI', 'cli', 'package_metadata'] + +import mozinfo +import optparse +import os +import platform import subprocess +import sys +import ConfigParser +from utils import get_metadata_from_egg +from utils import findInPath +from mozprofile import * from mozprocess.processhandler import ProcessHandler -import mozlog -# we can replace this method with 'abc' -# (http://docs.python.org/library/abc.html) when we require Python 2.6+ -def abstractmethod(method): - line = method.func_code.co_firstlineno - filename = method.func_code.co_filename - def not_implemented(*args, **kwargs): - raise NotImplementedError('Abstract method %s at File "%s", line %s ' - 'should be implemented by a concrete class' % - (repr(method), filename, line)) - return not_implemented +if mozinfo.isMac: + from plistlib import readPlist + +package_metadata = get_metadata_from_egg('mozrunner') + +# Map of debugging programs to information about them +# from http://mxr.mozilla.org/mozilla-central/source/build/automationutils.py#59 +debuggers = {'gdb': {'interactive': True, + 'args': ['-q', '--args'],}, + 'valgrind': {'interactive': False, + 'args': ['--leak-check=full']} + } + +def debugger_arguments(debugger, arguments=None, interactive=None): + """ + finds debugger arguments from debugger given and defaults + * debugger : debugger name or path to debugger + * arguments : arguments to the debugger, or None to use defaults + * interactive : whether the debugger should be run in interactive mode, or None to use default + """ + + # find debugger executable if not a file + executable = debugger + if not os.path.exists(executable): + executable = findInPath(debugger) + if executable is None: + raise Exception("Path to '%s' not found" % debugger) + + # if debugger not in dictionary of knowns return defaults + dirname, debugger = os.path.split(debugger) + if debugger not in debuggers: + return ([executable] + (arguments or []), bool(interactive)) + + # otherwise use the dictionary values for arguments unless specified + if arguments is None: + arguments = debuggers[debugger].get('args', []) + if interactive is None: + interactive = debuggers[debugger].get('interactive', False) + return ([executable] + arguments, interactive) class Runner(object): + """Handles all running operations. Finds bins, runs and kills the process.""" - def __init__(self, profile, clean_profile=True, process_class=None, kp_kwargs=None, env=None): - self.clean_profile = clean_profile - self.env = env or {} - self.kp_kwargs = kp_kwargs or {} - self.process_class = process_class or ProcessHandler + profile_class = Profile # profile class to use by default + + @classmethod + def create(cls, binary=None, cmdargs=None, env=None, kp_kwargs=None, profile_args=None, + clean_profile=True, process_class=ProcessHandler): + profile = cls.profile_class(**(profile_args or {})) + return cls(profile, binary=binary, cmdargs=cmdargs, env=env, kp_kwargs=kp_kwargs, + clean_profile=clean_profile, process_class=process_class) + + def __init__(self, profile, binary, cmdargs=None, env=None, + kp_kwargs=None, clean_profile=True, process_class=ProcessHandler): self.process_handler = None + self.process_class = process_class self.profile = profile - self.log = mozlog.getLogger('MozRunner') + self.clean_profile = clean_profile - @abstractmethod - def start(self, *args, **kwargs): + # find the binary + self.binary = binary + if not self.binary: + raise Exception("Binary not specified") + if not os.path.exists(self.binary): + raise OSError("Binary path does not exist: %s" % self.binary) + + # allow Mac binaries to be specified as an app bundle + plist = '%s/Contents/Info.plist' % self.binary + if mozinfo.isMac and os.path.exists(plist): + info = readPlist(plist) + self.binary = os.path.join(self.binary, "Contents/MacOS/", + info['CFBundleExecutable']) + + self.cmdargs = cmdargs or [] + _cmdargs = [i for i in self.cmdargs + if i != '-foreground'] + if len(_cmdargs) != len(self.cmdargs): + # foreground should be last; see + # - https://bugzilla.mozilla.org/show_bug.cgi?id=625614 + # - https://bugzilla.mozilla.org/show_bug.cgi?id=626826 + self.cmdargs = _cmdargs + self.cmdargs.append('-foreground') + + # process environment + if env is None: + self.env = os.environ.copy() + else: + self.env = env.copy() + # allows you to run an instance of Firefox separately from any other instances + self.env['MOZ_NO_REMOTE'] = '1' + # keeps Firefox attached to the terminal window after it starts + self.env['NO_EM_RESTART'] = '1' + + # set the library path if needed on linux + if sys.platform == 'linux2' and self.binary.endswith('-bin'): + dirname = os.path.dirname(self.binary) + if os.environ.get('LD_LIBRARY_PATH', None): + self.env['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRARY_PATH'], dirname) + else: + self.env['LD_LIBRARY_PATH'] = dirname + + # arguments for ProfessHandler.Process + self.kp_kwargs = kp_kwargs or {} + + @property + def command(self): + """Returns the command list to run.""" + return [self.binary, '-profile', self.profile.profile] + + def get_repositoryInfo(self): + """Read repository information from application.ini and platform.ini.""" + + config = ConfigParser.RawConfigParser() + dirname = os.path.dirname(self.binary) + repository = { } + + for file, section in [('application', 'App'), ('platform', 'Build')]: + config.read(os.path.join(dirname, '%s.ini' % file)) + + for key, id in [('SourceRepository', 'repository'), + ('SourceStamp', 'changeset')]: + try: + repository['%s_%s' % (file, id)] = config.get(section, key); + except: + repository['%s_%s' % (file, id)] = None + + return repository + + def is_running(self): + return self.process_handler is not None + + def start(self, debug_args=None, interactive=False, timeout=None, outputTimeout=None): """ - Run the process + Run self.command in the proper environment. + - debug_args: arguments for the debugger + - interactive: uses subprocess.Popen directly + - read_output: sends program output to stdout [default=False] + - timeout: see process_handler.waitForFinish + - outputTimeout: see process_handler.waitForFinish """ # ensure you are stopped @@ -44,7 +168,7 @@ class Runner(object): self.profile.reset() assert self.profile.exists(), "%s : failure to reset profile" % self.__class__.__name__ - cmd = self._wrap_command(self.command) + cmd = self._wrap_command(self.command+self.cmdargs) # attach a debugger, if specified if debug_args: @@ -60,7 +184,7 @@ class Runner(object): def wait(self, timeout=None): """ - Wait for the process to exit. + Wait for the app to exit. If timeout is not None, will return after timeout seconds. Use is_running() to determine whether or not a timeout occured. @@ -74,21 +198,13 @@ class Runner(object): else: self.process_handler.wait(timeout) if self.process_handler.proc.poll() is None: - # wait timed out + # waitForFinish timed out return + self.process_handler = None - def is_running(self): - """ - Returns True if the process is still running, False otherwise - """ - return self.process_handler is not None - - def stop(self): - """ - Kill the process - """ + """Kill the app""" if self.process_handler is None: return self.process_handler.kill() @@ -96,18 +212,177 @@ class Runner(object): def reset(self): """ - Reset the runner to its default state + reset the runner between runs + currently, only resets the profile, but probably should do more """ - if getattr(self, 'profile', False): - self.profile.reset() + self.profile.reset() def cleanup(self): - """ - Cleanup all runner state - """ - if self.is_running(): - self.stop() - if getattr(self, 'profile', False) and self.clean_profile: + self.stop() + if self.clean_profile: self.profile.cleanup() + def _wrap_command(self, cmd): + """ + If running on OS X 10.5 or older, wrap |cmd| so that it will + be executed as an i386 binary, in case it's a 32-bit/64-bit universal + binary. + """ + if mozinfo.isMac and hasattr(platform, 'mac_ver') and \ + platform.mac_ver()[0][:4] < '10.6': + return ["arch", "-arch", "i386"] + cmd + return cmd + __del__ = cleanup + + +class FirefoxRunner(Runner): + """Specialized Runner subclass for running Firefox.""" + + profile_class = FirefoxProfile + + def __init__(self, profile, binary=None, **kwargs): + + # take the binary from BROWSER_PATH environment variable + if (not binary) and 'BROWSER_PATH' in os.environ: + binary = os.environ['BROWSER_PATH'] + + Runner.__init__(self, profile, binary, **kwargs) + +class ThunderbirdRunner(Runner): + """Specialized Runner subclass for running Thunderbird""" + profile_class = ThunderbirdProfile + +runners = {'firefox': FirefoxRunner, + 'thunderbird': ThunderbirdRunner} + +class CLI(MozProfileCLI): + """Command line interface.""" + + module = "mozrunner" + + def __init__(self, args=sys.argv[1:]): + """ + Setup command line parser and parse arguments + - args : command line arguments + """ + + self.metadata = getattr(sys.modules[self.module], + 'package_metadata', + {}) + version = self.metadata.get('Version') + parser_args = {'description': self.metadata.get('Summary')} + if version: + parser_args['version'] = "%prog " + version + self.parser = optparse.OptionParser(**parser_args) + self.add_options(self.parser) + (self.options, self.args) = self.parser.parse_args(args) + + if getattr(self.options, 'info', None): + self.print_metadata() + sys.exit(0) + + # choose appropriate runner and profile classes + try: + self.runner_class = runners[self.options.app] + except KeyError: + self.parser.error('Application "%s" unknown (should be one of "firefox" or "thunderbird")' % self.options.app) + + def add_options(self, parser): + """add options to the parser""" + + # add profile options + MozProfileCLI.add_options(self, parser) + + # add runner options + parser.add_option('-b', "--binary", + dest="binary", help="Binary path.", + metavar=None, default=None) + parser.add_option('--app', dest='app', default='firefox', + help="Application to use [DEFAULT: %default]") + parser.add_option('--app-arg', dest='appArgs', + default=[], action='append', + help="provides an argument to the test application") + parser.add_option('--debugger', dest='debugger', + help="run under a debugger, e.g. gdb or valgrind") + parser.add_option('--debugger-args', dest='debugger_args', + action='append', default=None, + help="arguments to the debugger") + parser.add_option('--interactive', dest='interactive', + action='store_true', + help="run the program interactively") + if self.metadata: + parser.add_option("--info", dest="info", default=False, + action="store_true", + help="Print module information") + + ### methods for introspecting data + + def get_metadata_from_egg(self): + import pkg_resources + ret = {} + dist = pkg_resources.get_distribution(self.module) + if dist.has_metadata("PKG-INFO"): + for line in dist.get_metadata_lines("PKG-INFO"): + key, value = line.split(':', 1) + ret[key] = value + if dist.has_metadata("requires.txt"): + ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt") + return ret + + def print_metadata(self, data=("Name", "Version", "Summary", "Home-page", + "Author", "Author-email", "License", "Platform", "Dependencies")): + for key in data: + if key in self.metadata: + print key + ": " + self.metadata[key] + + ### methods for running + + def command_args(self): + """additional arguments for the mozilla application""" + return self.options.appArgs + + def runner_args(self): + """arguments to instantiate the runner class""" + return dict(cmdargs=self.command_args(), + binary=self.options.binary, + profile_args=self.profile_args()) + + def create_runner(self): + return self.runner_class.create(**self.runner_args()) + + def run(self): + runner = self.create_runner() + self.start(runner) + runner.cleanup() + + def debugger_arguments(self): + """ + returns a 2-tuple of debugger arguments: + (debugger_arguments, interactive) + """ + debug_args = self.options.debugger_args + interactive = self.options.interactive + if self.options.debugger: + debug_args, interactive = debugger_arguments(self.options.debugger) + return debug_args, interactive + + def start(self, runner): + """Starts the runner and waits for Firefox to exit or Keyboard Interrupt. + Shoule be overwritten to provide custom running of the runner instance.""" + + # attach a debugger if specified + debug_args, interactive = self.debugger_arguments() + runner.start(debug_args=debug_args, interactive=interactive) + print 'Starting:', ' '.join(runner.command) + try: + runner.wait() + except KeyboardInterrupt: + runner.stop() + + +def cli(args=sys.argv[1:]): + CLI(args).run() + +if __name__ == '__main__': + cli() diff --git a/testing/mozbase/mozrunner/setup.py b/testing/mozbase/mozrunner/setup.py index 6de698a9239..20a58e585fb 100644 --- a/testing/mozbase/mozrunner/setup.py +++ b/testing/mozbase/mozrunner/setup.py @@ -6,16 +6,13 @@ import sys from setuptools import setup PACKAGE_NAME = "mozrunner" -PACKAGE_VERSION = '5.21' +PACKAGE_VERSION = '5.15' desc = """Reliable start/stop/configuration of Mozilla Applications (Firefox, Thunderbird, etc.)""" -deps = ['mozcrash >= 0.3', - 'mozdevice >= 0.28', - 'mozinfo >= 0.4', - 'mozlog >= 1.3', +deps = ['mozinfo >= 0.4', 'mozprocess >= 0.8', - 'mozprofile >= 0.11', + 'mozprofile >= 0.4', ] # we only support python 2 right now @@ -39,9 +36,6 @@ setup(name=PACKAGE_NAME, url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase', license='MPL 2.0', packages=['mozrunner'], - package_data={'mozrunner': [ - 'resources/metrotestharness.exe' - ]}, zip_safe=False, install_requires = deps, entry_points=""" diff --git a/testing/mozbase/moztest/README.md b/testing/mozbase/moztest/README.md new file mode 100644 index 00000000000..4fed84b57ea --- /dev/null +++ b/testing/mozbase/moztest/README.md @@ -0,0 +1,16 @@ +# Moztest + +Package for handling Mozilla test results. + + +## Usage example + +This shows how you can create an xUnit representation of python unittest results. + + from results import TestResultCollection + from output import XUnitOutput + + collection = TestResultCollection.from_unittest_results(results) + out = XUnitOutput() + with open('out.xml', 'w') as f: + out.serialize(collection, f) diff --git a/testing/mozbase/moztest/setup.py b/testing/mozbase/moztest/setup.py index 28ef8e63c9f..8336dad9456 100644 --- a/testing/mozbase/moztest/setup.py +++ b/testing/mozbase/moztest/setup.py @@ -2,9 +2,18 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. + +import os from setuptools import setup -PACKAGE_VERSION = '0.2' +PACKAGE_VERSION = '0.1' + +# get documentation from the README +try: + here = os.path.dirname(os.path.abspath(__file__)) + description = file(os.path.join(here, 'README.md')).read() +except (OSError, IOError): + description = '' # dependencies deps = ['mozinfo'] @@ -16,12 +25,12 @@ except ImportError: setup(name='moztest', version=PACKAGE_VERSION, description="Package for storing and outputting Mozilla test results", - long_description="see http://mozbase.readthedocs.org/", + long_description=description, classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers keywords='mozilla', author='Mozilla Automation and Tools team', author_email='tools@lists.mozilla.org', - url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase', + url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase', license='MPL', packages=['moztest'], include_package_data=True, diff --git a/testing/mozbase/setup_development.py b/testing/mozbase/setup_development.py index 6d934f04bc7..a6951d87cc2 100755 --- a/testing/mozbase/setup_development.py +++ b/testing/mozbase/setup_development.py @@ -10,9 +10,10 @@ Setup mozbase packages for development. Packages may be specified as command line arguments. If no arguments are given, install all packages. -See https://wiki.mozilla.org/Auto-tools/Projects/Mozbase +See https://wiki.mozilla.org/Auto-tools/Projects/MozBase """ +import pkg_resources import os import subprocess import sys @@ -30,9 +31,7 @@ here = os.path.dirname(os.path.abspath(__file__)) # all python packages mozbase_packages = [i for i in os.listdir(here) if os.path.exists(os.path.join(here, i, 'setup.py'))] -extra_packages = ["sphinx", # documentation: https://wiki.mozilla.org/Auto-tools/Projects/Mozbase#Documentation - "mock", # testing: https://wiki.mozilla.org/Auto-tools/Projects/Mozbase#Tests - ] +extra_packages = ["sphinx"] def cycle_check(order, dependencies): """ensure no cyclic dependencies""" @@ -229,13 +228,6 @@ def main(args=sys.argv[1:]): call([sys.executable, 'setup.py', 'develop', '--no-deps'], cwd=os.path.join(here, reverse_mapping[package])) - # add the directory of sys.executable to path to aid the correct - # `easy_install` getting called - # https://bugzilla.mozilla.org/show_bug.cgi?id=893878 - os.environ['PATH'] = '%s%s%s' % (os.path.dirname(os.path.abspath(sys.executable)), - os.path.pathsep, - os.environ.get('PATH', '').strip(os.path.pathsep)) - # install non-mozbase dependencies # these need to be installed separately and the --no-deps flag # subsequently used due to a bug in setuptools; see diff --git a/testing/mozbase/test-manifest.ini b/testing/mozbase/test-manifest.ini index 6d9fe680acf..30ac9f65246 100644 --- a/testing/mozbase/test-manifest.ini +++ b/testing/mozbase/test-manifest.ini @@ -13,10 +13,6 @@ [include:mozdevice/tests/manifest.ini] [include:mozfile/tests/manifest.ini] [include:mozhttpd/tests/manifest.ini] -[include:mozinfo/tests/manifest.ini] -[include:mozinstall/tests/manifest.ini] -[include:mozlog/tests/manifest.ini] [include:mozprocess/tests/manifest.ini] [include:mozprofile/tests/manifest.ini] [include:moztest/tests/manifest.ini] -[include:moznetwork/tests/manifest.ini]