author | Andrew Halberstadt <ahalberstadt@mozilla.com> |
Tue, 06 Dec 2011 09:26:24 -0500 | |
changeset 82071 | 65c05ff60e47d68eebe82705b389c07ece2bdfcd |
parent 82070 | 9a59028a35108dc809341ed9ecf9c71ba6e1538b |
child 82072 | 3204b70435fe8a83ff33cbaef12e33ab3ebe3b2b |
push id | 21582 |
push user | bmo@edmorley.co.uk |
push date | Wed, 07 Dec 2011 09:30:09 +0000 |
treeherder | mozilla-central@489f2d51b011 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | jmaher |
bugs | 706844 |
milestone | 11.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/testing/mozbase/Makefile.in +++ b/testing/mozbase/Makefile.in @@ -61,13 +61,19 @@ MOZBASE_PACKAGES = \ mozrunner \ $(NULL) MOZBASE_EXTRAS = \ setup_development.py \ README \ $(NULL) +_DEST_DIR = $(DEPTH)/_tests/mozbase +libs:: $(MOZBASE_PACKAGES) + $(PYTHON) $(topsrcdir)/config/nsinstall.py $^ $(_DEST_DIR) +libs:: $(MOZBASE_EXTRAS) + $(PYTHON) $(topsrcdir)/config/nsinstall.py $^ $(_DEST_DIR) + stage-package: PKG_STAGE = $(DIST)/test-package-stage stage-package: $(NSINSTALL) -D $(PKG_STAGE)/mozbase @(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(MOZBASE_PACKAGES)) | (cd $(PKG_STAGE)/mozbase && tar -xf -) @(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(MOZBASE_EXTRAS)) | (cd $(PKG_STAGE)/mozbase && tar -xf -)
new file mode 100644 --- /dev/null +++ b/testing/mozbase/manifestdestiny/README.md @@ -0,0 +1,325 @@ +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: + + [{'path': + '/home/jhammel/mozmill/src/ManifestDestiny/manifestdestiny/tests/testToolbar/testBackForwardButtons.js', + 'name': 'testToolbar/testBackForwardButtons.js', 'here': + '/home/jhammel/mozmill/src/ManifestDestiny/manifestdestiny/tests', + 'manifest': '/home/jhammel/mozmill/src/ManifestDestiny/manifestdestiny/tests',}] + +The keys displayed here (path, 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? + +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 +* name: short name of the test; this is the (usually) relative path + specified in the section name +* 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 ... + + +# 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 <directory> <...> [create-options] + +To output a manifest of tests: + + manifestparser [options] write manifest <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. + + +# 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
deleted file mode 100644 --- a/testing/mozbase/manifestdestiny/README.txt +++ /dev/null @@ -1,345 +0,0 @@ -ManifestDestiny -=============== - -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:: - - [{'path': - '/home/jhammel/mozmill/src/ManifestDestiny/manifestdestiny/tests/testToolbar/testBackForwardButtons.js', - 'name': 'testToolbar/testBackForwardButtons.js', 'here': - '/home/jhammel/mozmill/src/ManifestDestiny/manifestdestiny/tests', - 'manifest': '/home/jhammel/mozmill/src/ManifestDestiny/manifestdestiny/tests',}] - -The keys displayed here (path, 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? ------------------------- - -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 -* name: short name of the test; this is the (usually) relative path - specified in the section name -* 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 http://hg.mozilla.org/automation/ManifestDestiny -and -http://hg.mozilla.org/automation/ManifestDestiny/file/tip/manifestparser.py -in particular. - - -Using Manifests ---------------- - -A test harness will normally call ``TestManifest.active_tests`` ( -http://hg.mozilla.org/automation/ManifestDestiny/file/c0399fbfa830/manifestparser.py#l506 ):: - - 506 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.${TAG}`` or -``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 ... - - -Tests ------ - -ManifestDestiny includes a suite of tests: - -http://hg.mozilla.org/automation/ManifestDestiny/file/tip/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 <directory> <...> [create-options] - -To output a manifest of tests:: - - manifestparser [options] write manifest <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. - - -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
deleted file mode 100644 --- a/testing/mozbase/manifestdestiny/manifestparser.py +++ /dev/null @@ -1,1114 +0,0 @@ -#!/usr/bin/env python -# ***** BEGIN LICENSE BLOCK ***** -# Version: MPL 1.1/GPL 2.0/LGPL 2.1 -# -# The contents of this file are subject to the Mozilla Public License Version -# 1.1 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# http://www.mozilla.org/MPL/ -# -# Software distributed under the License is distributed on an "AS IS" basis, -# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License -# for the specific language governing rights and limitations under the -# License. -# -# The Original Code is manifestdestiny. -# -# The Initial Developer of the Original Code is -# The Mozilla Foundation. -# Portions created by the Initial Developer are Copyright (C) 2010 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Jeff Hammel <jhammel@mozilla.com> (Original author) -# -# Alternatively, the contents of this file may be used under the terms of -# either of the GNU General Public License Version 2 or later (the "GPL"), -# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), -# in which case the provisions of the GPL or the LGPL are applicable instead -# of those above. If you wish to allow use of your version of this file only -# under the terms of either the GPL or the LGPL, and not to allow others to -# use your version of this file under the terms of the MPL, indicate your -# decision by deleting the provisions above and replace them with the notice -# and other provisions required by the GPL or the LGPL. If you do not delete -# the provisions above, a recipient may use your version of this file under -# the terms of any one of the MPL, the GPL or the LGPL. -# -# ***** END LICENSE BLOCK ***** - -""" -Mozilla universal manifest parser -""" - -# this file lives at -# http://hg.mozilla.org/automation/ManifestDestiny/raw-file/tip/manifestparser.py - -__all__ = ['read_ini', # .ini reader - 'ManifestParser', 'TestManifest', 'convert', # manifest handling - 'parse', 'ParseError', 'ExpressionParser'] # conditional expression parser - -import os -import re -import shutil -import sys -from fnmatch import fnmatch -from optparse import OptionParser - -version = '0.5.4' # package version -try: - from setuptools import setup -except: - setup = None - -# 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 start - return os.path.join(*rel_list) - -# expr.py -# from: -# http://k0s.org/mozilla/hg/expressionparser -# http://hg.mozilla.org/users/tmielczarek_mozilla.com/expressionparser - -# Implements a top-down parser/evaluator for simple boolean expressions. -# ideas taken from http://effbot.org/zone/simple-top-down-parsing.htm -# -# Rough grammar: -# expr := literal -# | '(' expr ')' -# | expr '&&' expr -# | expr '||' expr -# | expr '==' expr -# | expr '!=' expr -# literal := BOOL -# | INT -# | STRING -# | IDENT -# BOOL := true|false -# INT := [0-9]+ -# STRING := "[^"]*" -# IDENT := [A-Za-z_]\w* - -# Identifiers take their values from a mapping dictionary passed as the second -# argument. - -# Glossary (see above URL for details): -# - nud: null denotation -# - led: left detonation -# - lbp: left binding power -# - rbp: right binding power - -class ident_token(object): - def __init__(self, value): - self.value = value - def nud(self, parser): - # identifiers take their value from the value mappings passed - # to the parser - return parser.value(self.value) - -class literal_token(object): - def __init__(self, value): - self.value = value - def nud(self, parser): - return self.value - -class eq_op_token(object): - "==" - def led(self, parser, left): - return left == parser.expression(self.lbp) - -class neq_op_token(object): - "!=" - def led(self, parser, left): - return left != parser.expression(self.lbp) - -class not_op_token(object): - "!" - def nud(self, parser): - return not parser.expression() - -class and_op_token(object): - "&&" - def led(self, parser, left): - right = parser.expression(self.lbp) - return left and right - -class or_op_token(object): - "||" - def led(self, parser, left): - right = parser.expression(self.lbp) - return left or right - -class lparen_token(object): - "(" - def nud(self, parser): - expr = parser.expression() - parser.advance(rparen_token) - return expr - -class rparen_token(object): - ")" - -class end_token(object): - """always ends parsing""" - -### derived literal tokens - -class bool_token(literal_token): - def __init__(self, value): - value = {'true':True, 'false':False}[value] - literal_token.__init__(self, value) - -class int_token(literal_token): - def __init__(self, value): - literal_token.__init__(self, int(value)) - -class string_token(literal_token): - def __init__(self, value): - literal_token.__init__(self, value[1:-1]) - -precedence = [(end_token, rparen_token), - (or_op_token,), - (and_op_token,), - (eq_op_token, neq_op_token), - (lparen_token,), - ] -for index, rank in enumerate(precedence): - for token in rank: - token.lbp = index # lbp = lowest left binding power - -class ParseError(Exception): - """errror parsing conditional expression""" - -class ExpressionParser(object): - def __init__(self, text, valuemapping, strict=False): - """ - Initialize the parser with input |text|, and |valuemapping| as - a dict mapping identifier names to values. - """ - self.text = text - self.valuemapping = valuemapping - self.strict = strict - - def _tokenize(self): - """ - Lex the input text into tokens and yield them in sequence. - """ - # scanner callbacks - def bool_(scanner, t): return bool_token(t) - def identifier(scanner, t): return ident_token(t) - def integer(scanner, t): return int_token(t) - def eq(scanner, t): return eq_op_token() - def neq(scanner, t): return neq_op_token() - def or_(scanner, t): return or_op_token() - def and_(scanner, t): return and_op_token() - def lparen(scanner, t): return lparen_token() - def rparen(scanner, t): return rparen_token() - def string_(scanner, t): return string_token(t) - def not_(scanner, t): return not_op_token() - - scanner = re.Scanner([ - (r"true|false", bool_), - (r"[a-zA-Z_]\w*", identifier), - (r"[0-9]+", integer), - (r'("[^"]*")|(\'[^\']*\')', string_), - (r"==", eq), - (r"!=", neq), - (r"\|\|", or_), - (r"!", not_), - (r"&&", and_), - (r"\(", lparen), - (r"\)", rparen), - (r"\s+", None), # skip whitespace - ]) - tokens, remainder = scanner.scan(self.text) - for t in tokens: - yield t - yield end_token() - - def value(self, ident): - """ - Look up the value of |ident| in the value mapping passed in the - constructor. - """ - if self.strict: - return self.valuemapping[ident] - else: - return self.valuemapping.get(ident, None) - - def advance(self, expected): - """ - Assert that the next token is an instance of |expected|, and advance - to the next token. - """ - if not isinstance(self.token, expected): - raise Exception, "Unexpected token!" - self.token = self.iter.next() - - def expression(self, rbp=0): - """ - Parse and return the value of an expression until a token with - right binding power greater than rbp is encountered. - """ - t = self.token - self.token = self.iter.next() - left = t.nud(self) - while rbp < self.token.lbp: - t = self.token - self.token = self.iter.next() - left = t.led(self, left) - return left - - def parse(self): - """ - Parse and return the value of the expression in the text - passed to the constructor. Raises a ParseError if the expression - could not be parsed. - """ - try: - self.iter = self._tokenize() - self.token = self.iter.next() - return self.expression() - except: - raise ParseError("could not parse: %s; variables: %s" % (self.text, self.valuemapping)) - - __call__ = parse - -def parse(text, **values): - """ - Parse and evaluate a boolean expression in |text|. Use |values| to look - up the value of identifiers referenced in the expression. Returns the final - value of the expression. A ParseError will be raised if parsing fails. - """ - return ExpressionParser(text, values).parse() - -def normalize_path(path): - """normalize a relative path""" - if sys.platform.startswith('win'): - return path.replace('/', os.path.sep) - return path - -def denormalize_path(path): - """denormalize a relative path""" - if sys.platform.startswith('win'): - return path.replace(os.path.sep, '/') - return path - - -def read_ini(fp, variables=None, default='DEFAULT', - comments=';#', separators=('=', ':'), - strict=True): - """ - read an .ini file and return a list of [(section, values)] - - fp : file pointer or path to read - - variables : default set of variables - - default : name of the section for the default section - - comments : characters that if they start a line denote a comment - - separators : strings that denote key, value separation in order - - strict : whether to be strict about parsing - """ - - if variables is None: - variables = {} - - if isinstance(fp, basestring): - fp = file(fp) - - sections = [] - key = value = None - section_names = set([]) - - # read the lines - for line in fp.readlines(): - - stripped = line.strip() - - # ignore blank lines - if not stripped: - # reset key and value to avoid continuation lines - key = value = None - continue - - # ignore comment lines - if stripped[0] in comments: - continue - - # check for a new section - if len(stripped) > 2 and stripped[0] == '[' and stripped[-1] == ']': - section = stripped[1:-1].strip() - key = value = None - - # deal with DEFAULT section - if section.lower() == default.lower(): - if strict: - assert default not in section_names - section_names.add(default) - current_section = variables - continue - - if strict: - # make sure this section doesn't already exist - assert section not in section_names - - section_names.add(section) - current_section = {} - sections.append((section, current_section)) - continue - - # if there aren't any sections yet, something bad happen - if not section_names: - raise Exception('No sections found') - - # (key, value) pair - for separator in separators: - if separator in stripped: - key, value = stripped.split(separator, 1) - key = key.strip() - value = value.strip() - - if strict: - # make sure this key isn't already in the section or empty - assert key - if current_section is not variables: - assert key not in current_section - - current_section[key] = value - break - else: - # continuation line ? - if line[0].isspace() and key: - value = '%s%s%s' % (value, os.linesep, stripped) - current_section[key] = value - else: - # something bad happen! - raise Exception("Not sure what you're trying to do") - - # interpret the variables - def interpret_variables(global_dict, local_dict): - variables = global_dict.copy() - variables.update(local_dict) - return variables - - sections = [(i, interpret_variables(variables, j)) for i, j in sections] - return sections - - -### objects for parsing manifests - -class ManifestParser(object): - """read .ini manifests""" - - ### methods for reading manifests - - def __init__(self, manifests=(), defaults=None, strict=True): - self._defaults = defaults or {} - self.tests = [] - self.strict = strict - self.rootdir = None - self.relativeRoot = None - if manifests: - self.read(*manifests) - - def getRelativeRoot(self, root): - return root - - def read(self, *filenames, **defaults): - - # ensure all files exist - missing = [ filename for filename in filenames - if not os.path.exists(filename) ] - if missing: - raise IOError('Missing files: %s' % ', '.join(missing)) - - # process each file - for filename in filenames: - - # set the per file defaults - defaults = defaults.copy() or self._defaults.copy() - here = os.path.dirname(os.path.abspath(filename)) - defaults['here'] = here - - if self.rootdir is None: - # set the root directory - # == the directory of the first manifest given - self.rootdir = here - - # read the configuration - sections = read_ini(fp=filename, variables=defaults, strict=self.strict) - - # get the tests - for section, data in sections: - - # a file to include - # TODO: keep track of included file structure: - # self.manifests = {'manifest.ini': 'relative/path.ini'} - if section.startswith('include:'): - include_file = section.split('include:', 1)[-1] - include_file = normalize_path(include_file) - if not os.path.isabs(include_file): - include_file = os.path.join(self.getRelativeRoot(here), include_file) - if not os.path.exists(include_file): - if self.strict: - raise IOError("File '%s' does not exist" % include_file) - else: - continue - include_defaults = data.copy() - self.read(include_file, **include_defaults) - continue - - # otherwise an item - test = data - test['name'] = section - test['manifest'] = os.path.abspath(filename) - - # determine the path - path = test.get('path', section) - if '://' not in path: # don't futz with URLs - path = normalize_path(path) - if not os.path.isabs(path): - path = os.path.join(here, path) - test['path'] = path - - # append the item - self.tests.append(test) - - ### methods for querying manifests - - def query(self, *checks, **kw): - """ - general query function for tests - - checks : callable conditions to test if the test fulfills the query - """ - tests = kw.get('tests', None) - if tests is None: - tests = self.tests - retval = [] - for test in tests: - for check in checks: - if not check(test): - break - else: - retval.append(test) - return retval - - def get(self, _key=None, inverse=False, tags=None, tests=None, **kwargs): - # TODO: pass a dict instead of kwargs since you might hav - # e.g. 'inverse' as a key in the dict - - # TODO: tags should just be part of kwargs with None values - # (None == any is kinda weird, but probably still better) - - # fix up tags - if tags: - tags = set(tags) - else: - tags = set() - - # make some check functions - if inverse: - has_tags = lambda test: not tags.intersection(test.keys()) - def dict_query(test): - for key, value in kwargs.items(): - if test.get(key) == value: - return False - return True - else: - has_tags = lambda test: tags.issubset(test.keys()) - def dict_query(test): - for key, value in kwargs.items(): - if test.get(key) != value: - return False - return True - - # query the tests - tests = self.query(has_tags, dict_query, tests=tests) - - # if a key is given, return only a list of that key - # useful for keys like 'name' or 'path' - if _key: - return [test[_key] for test in tests] - - # return the tests - return tests - - def missing(self, tests=None): - """return list of tests that do not exist on the filesystem""" - if tests is None: - tests = self.tests - return [test for test in tests - if not os.path.exists(test['path'])] - - def manifests(self, tests=None): - """ - return manifests in order in which they appear in the tests - """ - if tests is None: - tests = self.tests - manifests = [] - for test in tests: - manifest = test.get('manifest') - if not manifest: - continue - if manifest not in manifests: - manifests.append(manifest) - return manifests - - ### methods for outputting from manifests - - def write(self, fp=sys.stdout, rootdir=None, - global_tags=None, global_kwargs=None, - local_tags=None, local_kwargs=None): - """ - write a manifest given a query - global and local options will be munged to do the query - globals will be written to the top of the file - locals (if given) will be written per test - """ - - # root directory - if rootdir is None: - rootdir = self.rootdir - - # sanitize input - global_tags = global_tags or set() - local_tags = local_tags or set() - global_kwargs = global_kwargs or {} - local_kwargs = local_kwargs or {} - - # create the query - tags = set([]) - tags.update(global_tags) - tags.update(local_tags) - kwargs = {} - kwargs.update(global_kwargs) - kwargs.update(local_kwargs) - - # get matching tests - tests = self.get(tags=tags, **kwargs) - - # print the .ini manifest - if global_tags or global_kwargs: - print >> fp, '[DEFAULT]' - for tag in global_tags: - print >> fp, '%s =' % tag - for key, value in global_kwargs.items(): - print >> fp, '%s = %s' % (key, value) - print >> fp - - for test in tests: - test = test.copy() # don't overwrite - - path = test['name'] - if not os.path.isabs(path): - path = test['path'] - if self.rootdir: - path = relpath(test['path'], self.rootdir) - path = denormalize_path(path) - print >> fp, '[%s]' % path - - # reserved keywords: - reserved = ['path', 'name', 'here', 'manifest'] - for key in sorted(test.keys()): - if key in reserved: - continue - if key in global_kwargs: - continue - if key in global_tags and not test[key]: - continue - print >> fp, '%s = %s' % (key, test[key]) - print >> fp - - def copy(self, directory, rootdir=None, *tags, **kwargs): - """ - copy the manifests and associated tests - - directory : directory to copy to - - rootdir : root directory to copy to (if not given from manifests) - - tags : keywords the tests must have - - kwargs : key, values the tests must match - """ - # XXX note that copy does *not* filter the tests out of the - # resulting manifest; it just stupidly copies them over. - # ideally, it would reread the manifests and filter out the - # tests that don't match *tags and **kwargs - - # destination - if not os.path.exists(directory): - os.path.makedirs(directory) - else: - # sanity check - assert os.path.isdir(directory) - - # tests to copy - tests = self.get(tags=tags, **kwargs) - if not tests: - return # nothing to do! - - # root directory - if rootdir is None: - rootdir = self.rootdir - - # copy the manifests + tests - manifests = [relpath(manifest, rootdir) for manifest in self.manifests()] - for manifest in manifests: - destination = os.path.join(directory, manifest) - dirname = os.path.dirname(destination) - if not os.path.exists(dirname): - os.makedirs(dirname) - else: - # sanity check - assert os.path.isdir(dirname) - shutil.copy(os.path.join(rootdir, manifest), destination) - for test in tests: - if os.path.isabs(test['name']): - continue - source = test['path'] - if not os.path.exists(source): - print >> sys.stderr, "Missing test: '%s' does not exist!" % source - continue - # TODO: should err on strict - destination = os.path.join(directory, relpath(test['path'], rootdir)) - shutil.copy(source, destination) - # TODO: ensure that all of the tests are below the from_dir - - def update(self, from_dir, rootdir=None, *tags, **kwargs): - """ - update the tests as listed in a manifest from a directory - - from_dir : directory where the tests live - - rootdir : root directory to copy to (if not given from manifests) - - tags : keys the tests must have - - kwargs : key, values the tests must match - """ - - # get the tests - tests = self.get(tags=tags, **kwargs) - - # get the root directory - if not rootdir: - rootdir = self.rootdir - - # copy them! - for test in tests: - if not os.path.isabs(test['name']): - _relpath = relpath(test['path'], rootdir) - source = os.path.join(from_dir, _relpath) - if not os.path.exists(source): - # TODO err on strict - print >> sys.stderr, "Missing test: '%s'; skipping" % test['name'] - continue - destination = os.path.join(rootdir, _relpath) - shutil.copy(source, destination) - - -class TestManifest(ManifestParser): - """ - apply logic to manifests; this is your integration layer :) - specific harnesses may subclass from this if they need more logic - """ - - def filter(self, values, tests): - """ - filter on a specific list tag, e.g.: - run-if.os = win linux - skip-if.os = mac - """ - - # tags: - run_tag = 'run-if' - skip_tag = 'skip-if' - fail_tag = 'fail-if' - - # loop over test - for test in tests: - reason = None # reason to disable - - # tagged-values to run - if run_tag in test: - condition = test[run_tag] - if not parse(condition, **values): - reason = '%s: %s' % (run_tag, condition) - - # tagged-values to skip - if skip_tag in test: - condition = test[skip_tag] - if parse(condition, **values): - reason = '%s: %s' % (skip_tag, condition) - - # mark test as disabled if there's a reason - if reason: - test.setdefault('disabled', reason) - - # mark test as a fail if so indicated - if fail_tag in test: - condition = test[fail_tag] - if parse(condition, **values): - test['expected'] = 'fail' - - def active_tests(self, exists=True, disabled=True, **values): - """ - - exists : return only existing tests - - disabled : whether to return disabled tests - - tags : keys and values to filter on (e.g. `os = linux mac`) - """ - - tests = [i.copy() for i in self.tests] # shallow copy - - # mark all tests as passing unless indicated otherwise - for test in tests: - test['expected'] = test.get('expected', 'pass') - - # ignore tests that do not exist - if exists: - tests = [test for test in tests if os.path.exists(test['path'])] - - # filter by tags - self.filter(values, tests) - - # ignore disabled tests if specified - if not disabled: - tests = [test for test in tests - if not 'disabled' in test] - - # return active tests - return tests - - def test_paths(self): - return [test['path'] for test in self.active_tests()] - - -### utility function(s); probably belongs elsewhere - -def convert(directories, pattern=None, ignore=(), write=None): - """ - convert directories to a simple manifest - """ - - retval = [] - include = [] - for directory in directories: - for dirpath, dirnames, filenames in os.walk(directory): - - # filter out directory names - dirnames = [ i for i in dirnames if i not in ignore ] - dirnames.sort() - - # reference only the subdirectory - _dirpath = dirpath - dirpath = dirpath.split(directory, 1)[-1].strip(os.path.sep) - - if dirpath.split(os.path.sep)[0] in ignore: - continue - - # filter by glob - if pattern: - filenames = [filename for filename in filenames - if fnmatch(filename, pattern)] - - filenames.sort() - - # write a manifest for each directory - if write and (dirnames or filenames): - manifest = file(os.path.join(_dirpath, write), 'w') - for dirname in dirnames: - print >> manifest, '[include:%s]' % os.path.join(dirname, write) - for filename in filenames: - print >> manifest, '[%s]' % filename - manifest.close() - - # add to the list - retval.extend([denormalize_path(os.path.join(dirpath, filename)) - for filename in filenames]) - - if write: - return # the manifests have already been written! - - retval.sort() - retval = ['[%s]' % filename for filename in retval] - return '\n'.join(retval) - -### command line attributes - -class ParserError(Exception): - """error for exceptions while parsing the command line""" - -def parse_args(_args): - """ - parse and return: - --keys=value (or --key value) - -tags - args - """ - - # return values - _dict = {} - tags = [] - args = [] - - # parse the arguments - key = None - for arg in _args: - if arg.startswith('---'): - raise ParserError("arguments should start with '-' or '--' only") - elif arg.startswith('--'): - if key: - raise ParserError("Key %s still open" % key) - key = arg[2:] - if '=' in key: - key, value = key.split('=', 1) - _dict[key] = value - key = None - continue - elif arg.startswith('-'): - if key: - raise ParserError("Key %s still open" % key) - tags.append(arg[1:]) - continue - else: - if key: - _dict[key] = arg - continue - args.append(arg) - - # return values - return (_dict, tags, args) - - -### classes for subcommands - -class CLICommand(object): - usage = '%prog [options] command' - def __init__(self, parser): - self._parser = parser # master parser - def parser(self): - return OptionParser(usage=self.usage, description=self.__doc__, - add_help_option=False) - -class Copy(CLICommand): - usage = '%prog [options] copy manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ...' - def __call__(self, options, args): - # parse the arguments - try: - kwargs, tags, args = parse_args(args) - except ParserError, e: - self._parser.error(e.message) - - # make sure we have some manifests, otherwise it will - # be quite boring - if not len(args) == 2: - HelpCLI(self._parser)(options, ['copy']) - return - - # read the manifests - # TODO: should probably ensure these exist here - manifests = ManifestParser() - manifests.read(args[0]) - - # print the resultant query - manifests.copy(args[1], None, *tags, **kwargs) - - -class CreateCLI(CLICommand): - """ - create a manifest from a list of directories - """ - usage = '%prog [options] create directory <directory> <...>' - - def parser(self): - parser = CLICommand.parser(self) - parser.add_option('-p', '--pattern', dest='pattern', - help="glob pattern for files") - parser.add_option('-i', '--ignore', dest='ignore', - default=[], action='append', - help='directories to ignore') - parser.add_option('-w', '--in-place', dest='in_place', - help='Write .ini files in place; filename to write to') - return parser - - def __call__(self, _options, args): - parser = self.parser() - options, args = parser.parse_args(args) - - # need some directories - if not len(args): - parser.print_usage() - return - - # add the directories to the manifest - for arg in args: - assert os.path.exists(arg) - assert os.path.isdir(arg) - manifest = convert(args, pattern=options.pattern, ignore=options.ignore, - write=options.in_place) - if manifest: - print manifest - - -class WriteCLI(CLICommand): - """ - write a manifest based on a query - """ - usage = '%prog [options] write manifest <manifest> -tag1 -tag2 --key1=value1 --key2=value2 ...' - def __call__(self, options, args): - - # parse the arguments - try: - kwargs, tags, args = parse_args(args) - except ParserError, e: - self._parser.error(e.message) - - # make sure we have some manifests, otherwise it will - # be quite boring - if not args: - HelpCLI(self._parser)(options, ['write']) - return - - # read the manifests - # TODO: should probably ensure these exist here - manifests = ManifestParser() - manifests.read(*args) - - # print the resultant query - manifests.write(global_tags=tags, global_kwargs=kwargs) - - -class HelpCLI(CLICommand): - """ - get help on a command - """ - usage = '%prog [options] help [command]' - - def __call__(self, options, args): - if len(args) == 1 and args[0] in commands: - commands[args[0]](self._parser).parser().print_help() - else: - self._parser.print_help() - print '\nCommands:' - for command in sorted(commands): - print ' %s : %s' % (command, commands[command].__doc__.strip()) - -class SetupCLI(CLICommand): - """ - setup using setuptools - """ - # use setup.py from the repo when you want to distribute to python! - # otherwise setuptools will complain that it can't find setup.py - # and result in a useless package - - usage = '%prog [options] setup [setuptools options]' - - def __call__(self, options, args): - sys.argv = [sys.argv[0]] + args - assert setup is not None, "You must have setuptools installed to use SetupCLI" - here = os.path.dirname(os.path.abspath(__file__)) - try: - filename = os.path.join(here, 'README.txt') - description = file(filename).read() - except: - description = '' - os.chdir(here) - - setup(name='ManifestDestiny', - version=version, - 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='Jeff Hammel', - author_email='jhammel@mozilla.com', - url='https://wiki.mozilla.org/Auto-tools/Projects/ManifestDestiny', - license='MPL', - zip_safe=False, - py_modules=['manifestparser'], - install_requires=[ - # -*- Extra requirements: -*- - ], - entry_points=""" - [console_scripts] - manifestparser = manifestparser:main - """, - ) - - -class UpdateCLI(CLICommand): - """ - update the tests as listed in a manifest from a directory - """ - usage = '%prog [options] update manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ...' - - def __call__(self, options, args): - # parse the arguments - try: - kwargs, tags, args = parse_args(args) - except ParserError, e: - self._parser.error(e.message) - - # make sure we have some manifests, otherwise it will - # be quite boring - if not len(args) == 2: - HelpCLI(self._parser)(options, ['update']) - return - - # read the manifests - # TODO: should probably ensure these exist here - manifests = ManifestParser() - manifests.read(args[0]) - - # print the resultant query - manifests.update(args[1], None, *tags, **kwargs) - - -# command -> class mapping -commands = { 'create': CreateCLI, - 'help': HelpCLI, - 'update': UpdateCLI, - 'write': WriteCLI } -if setup is not None: - commands['setup'] = SetupCLI - -def main(args=sys.argv[1:]): - """console_script entry point""" - - # set up an option parser - usage = '%prog [options] [command] ...' - description = __doc__ - parser = OptionParser(usage=usage, description=description) - parser.add_option('-s', '--strict', dest='strict', - action='store_true', default=False, - help='adhere strictly to errors') - parser.disable_interspersed_args() - - options, args = parser.parse_args(args) - - if not args: - HelpCLI(parser)(options, args) - parser.exit() - - # get the command - command = args[0] - if command not in commands: - parser.error("Command must be one of %s (you gave '%s')" % (', '.join(sorted(commands.keys())), command)) - - handler = commands[command](parser) - handler(options, args[1:]) - -if __name__ == '__main__': - main()
new file mode 100644 --- /dev/null +++ b/testing/mozbase/manifestdestiny/manifestparser/__init__.py @@ -0,0 +1,38 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is manifestdestiny. +# +# The Initial Developer of the Original Code is +# The Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Jeff Hammel <jhammel@mozilla.com> (Original author) +# +# Alternatively, the contents of this file may be used under the terms of +# either of the GNU General Public License Version 2 or later (the "GPL"), +# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +from manifestparser import *
new file mode 100755 --- /dev/null +++ b/testing/mozbase/manifestdestiny/manifestparser/manifestparser.py @@ -0,0 +1,1069 @@ +#!/usr/bin/env python +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is manifestdestiny. +# +# The Initial Developer of the Original Code is +# The Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Jeff Hammel <jhammel@mozilla.com> (Original author) +# +# Alternatively, the contents of this file may be used under the terms of +# either of the GNU General Public License Version 2 or later (the "GPL"), +# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +""" +Mozilla universal manifest parser +""" + +# this file lives at +# http://hg.mozilla.org/automation/ManifestDestiny/raw-file/tip/manifestparser.py + +__all__ = ['read_ini', # .ini reader + 'ManifestParser', 'TestManifest', 'convert', # manifest handling + 'parse', 'ParseError', 'ExpressionParser'] # conditional expression parser + +import os +import re +import shutil +import sys +from fnmatch import fnmatch +from optparse import OptionParser + +version = '0.5.4' # package version +try: + from setuptools import setup +except: + setup = None + +# 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 start + return os.path.join(*rel_list) + +# expr.py +# from: +# http://k0s.org/mozilla/hg/expressionparser +# http://hg.mozilla.org/users/tmielczarek_mozilla.com/expressionparser + +# Implements a top-down parser/evaluator for simple boolean expressions. +# ideas taken from http://effbot.org/zone/simple-top-down-parsing.htm +# +# Rough grammar: +# expr := literal +# | '(' expr ')' +# | expr '&&' expr +# | expr '||' expr +# | expr '==' expr +# | expr '!=' expr +# literal := BOOL +# | INT +# | STRING +# | IDENT +# BOOL := true|false +# INT := [0-9]+ +# STRING := "[^"]*" +# IDENT := [A-Za-z_]\w* + +# Identifiers take their values from a mapping dictionary passed as the second +# argument. + +# Glossary (see above URL for details): +# - nud: null denotation +# - led: left detonation +# - lbp: left binding power +# - rbp: right binding power + +class ident_token(object): + def __init__(self, value): + self.value = value + def nud(self, parser): + # identifiers take their value from the value mappings passed + # to the parser + return parser.value(self.value) + +class literal_token(object): + def __init__(self, value): + self.value = value + def nud(self, parser): + return self.value + +class eq_op_token(object): + "==" + def led(self, parser, left): + return left == parser.expression(self.lbp) + +class neq_op_token(object): + "!=" + def led(self, parser, left): + return left != parser.expression(self.lbp) + +class not_op_token(object): + "!" + def nud(self, parser): + return not parser.expression() + +class and_op_token(object): + "&&" + def led(self, parser, left): + right = parser.expression(self.lbp) + return left and right + +class or_op_token(object): + "||" + def led(self, parser, left): + right = parser.expression(self.lbp) + return left or right + +class lparen_token(object): + "(" + def nud(self, parser): + expr = parser.expression() + parser.advance(rparen_token) + return expr + +class rparen_token(object): + ")" + +class end_token(object): + """always ends parsing""" + +### derived literal tokens + +class bool_token(literal_token): + def __init__(self, value): + value = {'true':True, 'false':False}[value] + literal_token.__init__(self, value) + +class int_token(literal_token): + def __init__(self, value): + literal_token.__init__(self, int(value)) + +class string_token(literal_token): + def __init__(self, value): + literal_token.__init__(self, value[1:-1]) + +precedence = [(end_token, rparen_token), + (or_op_token,), + (and_op_token,), + (eq_op_token, neq_op_token), + (lparen_token,), + ] +for index, rank in enumerate(precedence): + for token in rank: + token.lbp = index # lbp = lowest left binding power + +class ParseError(Exception): + """errror parsing conditional expression""" + +class ExpressionParser(object): + def __init__(self, text, valuemapping, strict=False): + """ + Initialize the parser with input |text|, and |valuemapping| as + a dict mapping identifier names to values. + """ + self.text = text + self.valuemapping = valuemapping + self.strict = strict + + def _tokenize(self): + """ + Lex the input text into tokens and yield them in sequence. + """ + # scanner callbacks + def bool_(scanner, t): return bool_token(t) + def identifier(scanner, t): return ident_token(t) + def integer(scanner, t): return int_token(t) + def eq(scanner, t): return eq_op_token() + def neq(scanner, t): return neq_op_token() + def or_(scanner, t): return or_op_token() + def and_(scanner, t): return and_op_token() + def lparen(scanner, t): return lparen_token() + def rparen(scanner, t): return rparen_token() + def string_(scanner, t): return string_token(t) + def not_(scanner, t): return not_op_token() + + scanner = re.Scanner([ + (r"true|false", bool_), + (r"[a-zA-Z_]\w*", identifier), + (r"[0-9]+", integer), + (r'("[^"]*")|(\'[^\']*\')', string_), + (r"==", eq), + (r"!=", neq), + (r"\|\|", or_), + (r"!", not_), + (r"&&", and_), + (r"\(", lparen), + (r"\)", rparen), + (r"\s+", None), # skip whitespace + ]) + tokens, remainder = scanner.scan(self.text) + for t in tokens: + yield t + yield end_token() + + def value(self, ident): + """ + Look up the value of |ident| in the value mapping passed in the + constructor. + """ + if self.strict: + return self.valuemapping[ident] + else: + return self.valuemapping.get(ident, None) + + def advance(self, expected): + """ + Assert that the next token is an instance of |expected|, and advance + to the next token. + """ + if not isinstance(self.token, expected): + raise Exception, "Unexpected token!" + self.token = self.iter.next() + + def expression(self, rbp=0): + """ + Parse and return the value of an expression until a token with + right binding power greater than rbp is encountered. + """ + t = self.token + self.token = self.iter.next() + left = t.nud(self) + while rbp < self.token.lbp: + t = self.token + self.token = self.iter.next() + left = t.led(self, left) + return left + + def parse(self): + """ + Parse and return the value of the expression in the text + passed to the constructor. Raises a ParseError if the expression + could not be parsed. + """ + try: + self.iter = self._tokenize() + self.token = self.iter.next() + return self.expression() + except: + raise ParseError("could not parse: %s; variables: %s" % (self.text, self.valuemapping)) + + __call__ = parse + +def parse(text, **values): + """ + Parse and evaluate a boolean expression in |text|. Use |values| to look + up the value of identifiers referenced in the expression. Returns the final + value of the expression. A ParseError will be raised if parsing fails. + """ + return ExpressionParser(text, values).parse() + +def normalize_path(path): + """normalize a relative path""" + if sys.platform.startswith('win'): + return path.replace('/', os.path.sep) + return path + +def denormalize_path(path): + """denormalize a relative path""" + if sys.platform.startswith('win'): + return path.replace(os.path.sep, '/') + return path + + +def read_ini(fp, variables=None, default='DEFAULT', + comments=';#', separators=('=', ':'), + strict=True): + """ + read an .ini file and return a list of [(section, values)] + - fp : file pointer or path to read + - variables : default set of variables + - default : name of the section for the default section + - comments : characters that if they start a line denote a comment + - separators : strings that denote key, value separation in order + - strict : whether to be strict about parsing + """ + + if variables is None: + variables = {} + + if isinstance(fp, basestring): + fp = file(fp) + + sections = [] + key = value = None + section_names = set([]) + + # read the lines + for line in fp.readlines(): + + stripped = line.strip() + + # ignore blank lines + if not stripped: + # reset key and value to avoid continuation lines + key = value = None + continue + + # ignore comment lines + if stripped[0] in comments: + continue + + # check for a new section + if len(stripped) > 2 and stripped[0] == '[' and stripped[-1] == ']': + section = stripped[1:-1].strip() + key = value = None + + # deal with DEFAULT section + if section.lower() == default.lower(): + if strict: + assert default not in section_names + section_names.add(default) + current_section = variables + continue + + if strict: + # make sure this section doesn't already exist + assert section not in section_names, "Section '%s' already found in '%s'" % (section, section_names) + + section_names.add(section) + current_section = {} + sections.append((section, current_section)) + continue + + # if there aren't any sections yet, something bad happen + if not section_names: + raise Exception('No sections found') + + # (key, value) pair + for separator in separators: + if separator in stripped: + key, value = stripped.split(separator, 1) + key = key.strip() + value = value.strip() + + if strict: + # make sure this key isn't already in the section or empty + assert key + if current_section is not variables: + assert key not in current_section + + current_section[key] = value + break + else: + # continuation line ? + if line[0].isspace() and key: + value = '%s%s%s' % (value, os.linesep, stripped) + current_section[key] = value + else: + # something bad happen! + raise Exception("Not sure what you're trying to do") + + # interpret the variables + def interpret_variables(global_dict, local_dict): + variables = global_dict.copy() + variables.update(local_dict) + return variables + + sections = [(i, interpret_variables(variables, j)) for i, j in sections] + return sections + + +### objects for parsing manifests + +class ManifestParser(object): + """read .ini manifests""" + + ### methods for reading manifests + + def __init__(self, manifests=(), defaults=None, strict=True): + self._defaults = defaults or {} + self.tests = [] + self.strict = strict + self.rootdir = None + self.relativeRoot = None + if manifests: + self.read(*manifests) + + def getRelativeRoot(self, root): + return root + + def read(self, *filenames, **defaults): + + # ensure all files exist + missing = [ filename for filename in filenames + if not os.path.exists(filename) ] + if missing: + raise IOError('Missing files: %s' % ', '.join(missing)) + + # process each file + for filename in filenames: + + # set the per file defaults + defaults = defaults.copy() or self._defaults.copy() + here = os.path.dirname(os.path.abspath(filename)) + defaults['here'] = here + + if self.rootdir is None: + # set the root directory + # == the directory of the first manifest given + self.rootdir = here + + # read the configuration + sections = read_ini(fp=filename, variables=defaults, strict=self.strict) + + # get the tests + for section, data in sections: + + # a file to include + # TODO: keep track of included file structure: + # self.manifests = {'manifest.ini': 'relative/path.ini'} + if section.startswith('include:'): + include_file = section.split('include:', 1)[-1] + include_file = normalize_path(include_file) + if not os.path.isabs(include_file): + include_file = os.path.join(self.getRelativeRoot(here), include_file) + if not os.path.exists(include_file): + if self.strict: + raise IOError("File '%s' does not exist" % include_file) + else: + continue + include_defaults = data.copy() + self.read(include_file, **include_defaults) + continue + + # otherwise an item + test = data + test['name'] = section + test['manifest'] = os.path.abspath(filename) + + # determine the path + path = test.get('path', section) + if '://' not in path: # don't futz with URLs + path = normalize_path(path) + if not os.path.isabs(path): + path = os.path.join(here, path) + test['path'] = path + + # append the item + self.tests.append(test) + + ### methods for querying manifests + + def query(self, *checks, **kw): + """ + general query function for tests + - checks : callable conditions to test if the test fulfills the query + """ + tests = kw.get('tests', None) + if tests is None: + tests = self.tests + retval = [] + for test in tests: + for check in checks: + if not check(test): + break + else: + retval.append(test) + return retval + + def get(self, _key=None, inverse=False, tags=None, tests=None, **kwargs): + # TODO: pass a dict instead of kwargs since you might hav + # e.g. 'inverse' as a key in the dict + + # TODO: tags should just be part of kwargs with None values + # (None == any is kinda weird, but probably still better) + + # fix up tags + if tags: + tags = set(tags) + else: + tags = set() + + # make some check functions + if inverse: + has_tags = lambda test: not tags.intersection(test.keys()) + def dict_query(test): + for key, value in kwargs.items(): + if test.get(key) == value: + return False + return True + else: + has_tags = lambda test: tags.issubset(test.keys()) + def dict_query(test): + for key, value in kwargs.items(): + if test.get(key) != value: + return False + return True + + # query the tests + tests = self.query(has_tags, dict_query, tests=tests) + + # if a key is given, return only a list of that key + # useful for keys like 'name' or 'path' + if _key: + return [test[_key] for test in tests] + + # return the tests + return tests + + def missing(self, tests=None): + """return list of tests that do not exist on the filesystem""" + if tests is None: + tests = self.tests + return [test for test in tests + if not os.path.exists(test['path'])] + + def manifests(self, tests=None): + """ + return manifests in order in which they appear in the tests + """ + if tests is None: + tests = self.tests + manifests = [] + for test in tests: + manifest = test.get('manifest') + if not manifest: + continue + if manifest not in manifests: + manifests.append(manifest) + return manifests + + ### methods for outputting from manifests + + def write(self, fp=sys.stdout, rootdir=None, + global_tags=None, global_kwargs=None, + local_tags=None, local_kwargs=None): + """ + write a manifest given a query + global and local options will be munged to do the query + globals will be written to the top of the file + locals (if given) will be written per test + """ + + # root directory + if rootdir is None: + rootdir = self.rootdir + + # sanitize input + global_tags = global_tags or set() + local_tags = local_tags or set() + global_kwargs = global_kwargs or {} + local_kwargs = local_kwargs or {} + + # create the query + tags = set([]) + tags.update(global_tags) + tags.update(local_tags) + kwargs = {} + kwargs.update(global_kwargs) + kwargs.update(local_kwargs) + + # get matching tests + tests = self.get(tags=tags, **kwargs) + + # print the .ini manifest + if global_tags or global_kwargs: + print >> fp, '[DEFAULT]' + for tag in global_tags: + print >> fp, '%s =' % tag + for key, value in global_kwargs.items(): + print >> fp, '%s = %s' % (key, value) + print >> fp + + for test in tests: + test = test.copy() # don't overwrite + + path = test['name'] + if not os.path.isabs(path): + path = test['path'] + if self.rootdir: + path = relpath(test['path'], self.rootdir) + path = denormalize_path(path) + print >> fp, '[%s]' % path + + # reserved keywords: + reserved = ['path', 'name', 'here', 'manifest'] + for key in sorted(test.keys()): + if key in reserved: + continue + if key in global_kwargs: + continue + if key in global_tags and not test[key]: + continue + print >> fp, '%s = %s' % (key, test[key]) + print >> fp + + def copy(self, directory, rootdir=None, *tags, **kwargs): + """ + copy the manifests and associated tests + - directory : directory to copy to + - rootdir : root directory to copy to (if not given from manifests) + - tags : keywords the tests must have + - kwargs : key, values the tests must match + """ + # XXX note that copy does *not* filter the tests out of the + # resulting manifest; it just stupidly copies them over. + # ideally, it would reread the manifests and filter out the + # tests that don't match *tags and **kwargs + + # destination + if not os.path.exists(directory): + os.path.makedirs(directory) + else: + # sanity check + assert os.path.isdir(directory) + + # tests to copy + tests = self.get(tags=tags, **kwargs) + if not tests: + return # nothing to do! + + # root directory + if rootdir is None: + rootdir = self.rootdir + + # copy the manifests + tests + manifests = [relpath(manifest, rootdir) for manifest in self.manifests()] + for manifest in manifests: + destination = os.path.join(directory, manifest) + dirname = os.path.dirname(destination) + if not os.path.exists(dirname): + os.makedirs(dirname) + else: + # sanity check + assert os.path.isdir(dirname) + shutil.copy(os.path.join(rootdir, manifest), destination) + for test in tests: + if os.path.isabs(test['name']): + continue + source = test['path'] + if not os.path.exists(source): + print >> sys.stderr, "Missing test: '%s' does not exist!" % source + continue + # TODO: should err on strict + destination = os.path.join(directory, relpath(test['path'], rootdir)) + shutil.copy(source, destination) + # TODO: ensure that all of the tests are below the from_dir + + def update(self, from_dir, rootdir=None, *tags, **kwargs): + """ + update the tests as listed in a manifest from a directory + - from_dir : directory where the tests live + - rootdir : root directory to copy to (if not given from manifests) + - tags : keys the tests must have + - kwargs : key, values the tests must match + """ + + # get the tests + tests = self.get(tags=tags, **kwargs) + + # get the root directory + if not rootdir: + rootdir = self.rootdir + + # copy them! + for test in tests: + if not os.path.isabs(test['name']): + _relpath = relpath(test['path'], rootdir) + source = os.path.join(from_dir, _relpath) + if not os.path.exists(source): + # TODO err on strict + print >> sys.stderr, "Missing test: '%s'; skipping" % test['name'] + continue + destination = os.path.join(rootdir, _relpath) + shutil.copy(source, destination) + + +class TestManifest(ManifestParser): + """ + apply logic to manifests; this is your integration layer :) + specific harnesses may subclass from this if they need more logic + """ + + def filter(self, values, tests): + """ + filter on a specific list tag, e.g.: + run-if.os = win linux + skip-if.os = mac + """ + + # tags: + run_tag = 'run-if' + skip_tag = 'skip-if' + fail_tag = 'fail-if' + + # loop over test + for test in tests: + reason = None # reason to disable + + # tagged-values to run + if run_tag in test: + condition = test[run_tag] + if not parse(condition, **values): + reason = '%s: %s' % (run_tag, condition) + + # tagged-values to skip + if skip_tag in test: + condition = test[skip_tag] + if parse(condition, **values): + reason = '%s: %s' % (skip_tag, condition) + + # mark test as disabled if there's a reason + if reason: + test.setdefault('disabled', reason) + + # mark test as a fail if so indicated + if fail_tag in test: + condition = test[fail_tag] + if parse(condition, **values): + test['expected'] = 'fail' + + def active_tests(self, exists=True, disabled=True, **values): + """ + - exists : return only existing tests + - disabled : whether to return disabled tests + - tags : keys and values to filter on (e.g. `os = linux mac`) + """ + + tests = [i.copy() for i in self.tests] # shallow copy + + # mark all tests as passing unless indicated otherwise + for test in tests: + test['expected'] = test.get('expected', 'pass') + + # ignore tests that do not exist + if exists: + tests = [test for test in tests if os.path.exists(test['path'])] + + # filter by tags + self.filter(values, tests) + + # ignore disabled tests if specified + if not disabled: + tests = [test for test in tests + if not 'disabled' in test] + + # return active tests + return tests + + def test_paths(self): + return [test['path'] for test in self.active_tests()] + + +### utility function(s); probably belongs elsewhere + +def convert(directories, pattern=None, ignore=(), write=None): + """ + convert directories to a simple manifest + """ + + retval = [] + include = [] + for directory in directories: + for dirpath, dirnames, filenames in os.walk(directory): + + # filter out directory names + dirnames = [ i for i in dirnames if i not in ignore ] + dirnames.sort() + + # reference only the subdirectory + _dirpath = dirpath + dirpath = dirpath.split(directory, 1)[-1].strip(os.path.sep) + + if dirpath.split(os.path.sep)[0] in ignore: + continue + + # filter by glob + if pattern: + filenames = [filename for filename in filenames + if fnmatch(filename, pattern)] + + filenames.sort() + + # write a manifest for each directory + if write and (dirnames or filenames): + manifest = file(os.path.join(_dirpath, write), 'w') + for dirname in dirnames: + print >> manifest, '[include:%s]' % os.path.join(dirname, write) + for filename in filenames: + print >> manifest, '[%s]' % filename + manifest.close() + + # add to the list + retval.extend([denormalize_path(os.path.join(dirpath, filename)) + for filename in filenames]) + + if write: + return # the manifests have already been written! + + retval.sort() + retval = ['[%s]' % filename for filename in retval] + return '\n'.join(retval) + +### command line attributes + +class ParserError(Exception): + """error for exceptions while parsing the command line""" + +def parse_args(_args): + """ + parse and return: + --keys=value (or --key value) + -tags + args + """ + + # return values + _dict = {} + tags = [] + args = [] + + # parse the arguments + key = None + for arg in _args: + if arg.startswith('---'): + raise ParserError("arguments should start with '-' or '--' only") + elif arg.startswith('--'): + if key: + raise ParserError("Key %s still open" % key) + key = arg[2:] + if '=' in key: + key, value = key.split('=', 1) + _dict[key] = value + key = None + continue + elif arg.startswith('-'): + if key: + raise ParserError("Key %s still open" % key) + tags.append(arg[1:]) + continue + else: + if key: + _dict[key] = arg + continue + args.append(arg) + + # return values + return (_dict, tags, args) + + +### classes for subcommands + +class CLICommand(object): + usage = '%prog [options] command' + def __init__(self, parser): + self._parser = parser # master parser + def parser(self): + return OptionParser(usage=self.usage, description=self.__doc__, + add_help_option=False) + +class Copy(CLICommand): + usage = '%prog [options] copy manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ...' + def __call__(self, options, args): + # parse the arguments + try: + kwargs, tags, args = parse_args(args) + except ParserError, e: + self._parser.error(e.message) + + # make sure we have some manifests, otherwise it will + # be quite boring + if not len(args) == 2: + HelpCLI(self._parser)(options, ['copy']) + return + + # read the manifests + # TODO: should probably ensure these exist here + manifests = ManifestParser() + manifests.read(args[0]) + + # print the resultant query + manifests.copy(args[1], None, *tags, **kwargs) + + +class CreateCLI(CLICommand): + """ + create a manifest from a list of directories + """ + usage = '%prog [options] create directory <directory> <...>' + + def parser(self): + parser = CLICommand.parser(self) + parser.add_option('-p', '--pattern', dest='pattern', + help="glob pattern for files") + parser.add_option('-i', '--ignore', dest='ignore', + default=[], action='append', + help='directories to ignore') + parser.add_option('-w', '--in-place', dest='in_place', + help='Write .ini files in place; filename to write to') + return parser + + def __call__(self, _options, args): + parser = self.parser() + options, args = parser.parse_args(args) + + # need some directories + if not len(args): + parser.print_usage() + return + + # add the directories to the manifest + for arg in args: + assert os.path.exists(arg) + assert os.path.isdir(arg) + manifest = convert(args, pattern=options.pattern, ignore=options.ignore, + write=options.in_place) + if manifest: + print manifest + + +class WriteCLI(CLICommand): + """ + write a manifest based on a query + """ + usage = '%prog [options] write manifest <manifest> -tag1 -tag2 --key1=value1 --key2=value2 ...' + def __call__(self, options, args): + + # parse the arguments + try: + kwargs, tags, args = parse_args(args) + except ParserError, e: + self._parser.error(e.message) + + # make sure we have some manifests, otherwise it will + # be quite boring + if not args: + HelpCLI(self._parser)(options, ['write']) + return + + # read the manifests + # TODO: should probably ensure these exist here + manifests = ManifestParser() + manifests.read(*args) + + # print the resultant query + manifests.write(global_tags=tags, global_kwargs=kwargs) + + +class HelpCLI(CLICommand): + """ + get help on a command + """ + usage = '%prog [options] help [command]' + + def __call__(self, options, args): + if len(args) == 1 and args[0] in commands: + commands[args[0]](self._parser).parser().print_help() + else: + self._parser.print_help() + print '\nCommands:' + for command in sorted(commands): + print ' %s : %s' % (command, commands[command].__doc__.strip()) + +class UpdateCLI(CLICommand): + """ + update the tests as listed in a manifest from a directory + """ + usage = '%prog [options] update manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ...' + + def __call__(self, options, args): + # parse the arguments + try: + kwargs, tags, args = parse_args(args) + except ParserError, e: + self._parser.error(e.message) + + # make sure we have some manifests, otherwise it will + # be quite boring + if not len(args) == 2: + HelpCLI(self._parser)(options, ['update']) + return + + # read the manifests + # TODO: should probably ensure these exist here + manifests = ManifestParser() + manifests.read(args[0]) + + # print the resultant query + manifests.update(args[1], None, *tags, **kwargs) + + +# command -> class mapping +commands = { 'create': CreateCLI, + 'help': HelpCLI, + 'update': UpdateCLI, + 'write': WriteCLI } + +def main(args=sys.argv[1:]): + """console_script entry point""" + + # set up an option parser + usage = '%prog [options] [command] ...' + description = __doc__ + parser = OptionParser(usage=usage, description=description) + parser.add_option('-s', '--strict', dest='strict', + action='store_true', default=False, + help='adhere strictly to errors') + parser.disable_interspersed_args() + + options, args = parser.parse_args(args) + + if not args: + HelpCLI(parser)(options, args) + parser.exit() + + # get the command + command = args[0] + if command not in commands: + parser.error("Command must be one of %s (you gave '%s')" % (', '.join(sorted(commands.keys())), command)) + + handler = commands[command](parser) + handler(options, args[1:]) + +if __name__ == '__main__': + main()
--- a/testing/mozbase/manifestdestiny/setup.py +++ b/testing/mozbase/manifestdestiny/setup.py @@ -36,11 +36,42 @@ # # ***** END LICENSE BLOCK ***** # The real details are in manifestparser.py; this is just a front-end # BUT use this file when you want to distribute to python! # otherwise setuptools will complain that it can't find setup.py # and result in a useless package +from setuptools import setup, find_packages import sys -from manifestparser import SetupCLI -SetupCLI(None)(None, sys.argv[1:]) +import os + +here = os.path.dirname(os.path.abspath(__file__)) +try: + filename = os.path.join(here, 'README.txt') + description = file(filename).read() +except: + description = '' + +PACKAGE_NAME = "ManifestDestiny" +PACKAGE_VERSION = "0.5.4" + +setup(name=PACKAGE_NAME, + version=PACKAGE_VERSION, + 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='Jeff Hammel', + author_email='jhammel@mozilla.com', + url='https://github.com/mozilla/mozbase/tree/master/manifestdestiny', + license='MPL', + zip_safe=False, + packages=find_packages(exclude=['legacy']), + install_requires=[ + # -*- Extra requirements: -*- + ], + entry_points=""" + [console_scripts] + manifestparser = manifestparser:main + """, + )
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozdevice/README.md @@ -0,0 +1,5 @@ +[mozdevice](https://github.com/mozilla/mozbase/tree/master/mozdevice) provides +an interface to interact with a remote device such as an Android phone connected +to a workstation. Currently there are two implementations of the interface: one +uses a TCP-based protocol to communicate with a server running on the device, +another uses Android's adb utility.
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozdevice/mozdevice/__init__.py @@ -0,0 +1,39 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is Mozbase. +# +# The Initial Developer of the Original Code is +# The Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2011 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Will Lachance <wlachance@mozilla.com> +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +from devicemanagerADB import DeviceManagerADB +from devicemanagerSUT import DeviceManagerSUT
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozdevice/mozdevice/devicemanager.py @@ -0,0 +1,538 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is Test Automation Framework. +# +# The Initial Developer of the Original Code is Joel Maher. +# +# Portions created by the Initial Developer are Copyright (C) 2009 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Joel Maher <joel.maher@gmail.com> (Original Developer) +# Clint Talbert <cmtalbert@gmail.com> +# Mark Cote <mcote@mozilla.com> +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +import time +import hashlib +import socket +import os +import re + +class FileError(Exception): + " Signifies an error which occurs while doing a file operation." + + def __init__(self, msg = ''): + self.msg = msg + + def __str__(self): + return self.msg + +class DMError(Exception): + "generic devicemanager exception." + + def __init__(self, msg= ''): + self.msg = msg + + def __str__(self): + return self.msg + + +class DeviceManager: + # external function + # returns: + # success: True + # failure: False + def pushFile(self, localname, destname): + assert 0 == 1 + return False + + # external function + # returns: + # success: directory name + # failure: None + def mkDir(self, name): + assert 0 == 1 + return None + + # make directory structure on the device + # external function + # returns: + # success: directory structure that we created + # failure: None + def mkDirs(self, filename): + assert 0 == 1 + return None + + # push localDir from host to remoteDir on the device + # external function + # returns: + # success: remoteDir + # failure: None + def pushDir(self, localDir, remoteDir): + assert 0 == 1 + return None + + # external function + # returns: + # success: True + # failure: False + def dirExists(self, dirname): + assert 0 == 1 + return False + + # Because we always have / style paths we make this a lot easier with some + # assumptions + # external function + # returns: + # success: True + # failure: False + def fileExists(self, filepath): + assert 0 == 1 + return False + + # list files on the device, requires cd to directory first + # external function + # returns: + # success: array of filenames, ['file1', 'file2', ...] + # failure: [] + def listFiles(self, rootdir): + assert 0 == 1 + return [] + + # external function + # returns: + # success: output of telnet, i.e. "removing file: /mnt/sdcard/tests/test.txt" + # failure: None + def removeFile(self, filename): + assert 0 == 1 + return False + + # does a recursive delete of directory on the device: rm -Rf remoteDir + # external function + # returns: + # success: output of telnet, i.e. "removing file: /mnt/sdcard/tests/test.txt" + # failure: None + def removeDir(self, remoteDir): + assert 0 == 1 + return None + + # external function + # returns: + # success: array of process tuples + # failure: [] + def getProcessList(self): + assert 0 == 1 + return [] + + # external function + # returns: + # success: pid + # failure: None + def fireProcess(self, appname, failIfRunning=False): + assert 0 == 1 + return None + + # external function + # returns: + # success: output filename + # failure: None + def launchProcess(self, cmd, outputFile = "process.txt", cwd = '', env = '', failIfRunning=False): + assert 0 == 1 + return None + + # loops until 'process' has exited or 'timeout' seconds is reached + # loop sleeps for 'interval' seconds between iterations + # external function + # returns: + # success: [file contents, None] + # failure: [None, None] + def communicate(self, process, timeout = 600, interval = 5): + timed_out = True + if (timeout > 0): + total_time = 0 + while total_time < timeout: + time.sleep(interval) + if self.processExist(process) == None: + timed_out = False + break + total_time += interval + + if (timed_out == True): + return [None, None] + + return [self.getFile(process, "temp.txt"), None] + + # iterates process list and returns pid if exists, otherwise None + # external function + # returns: + # success: pid + # failure: None + def processExist(self, appname): + pid = None + + #filter out extra spaces + parts = filter(lambda x: x != '', appname.split(' ')) + appname = ' '.join(parts) + + #filter out the quoted env string if it exists + #ex: '"name=value;name2=value2;etc=..." process args' -> 'process args' + parts = appname.split('"') + if (len(parts) > 2): + appname = ' '.join(parts[2:]).strip() + + pieces = appname.split(' ') + parts = pieces[0].split('/') + app = parts[-1] + procre = re.compile('.*' + app + '.*') + + procList = self.getProcessList() + if (procList == []): + return None + + for proc in procList: + if (procre.match(proc[1])): + pid = proc[0] + break + return pid + + # external function + # returns: + # success: output from testagent + # failure: None + def killProcess(self, appname): + assert 0 == 1 + return None + + # external function + # returns: + # success: filecontents + # failure: None + def catFile(self, remoteFile): + assert 0 == 1 + return None + + # external function + # returns: + # success: output of pullfile, string + # failure: None + def pullFile(self, remoteFile): + assert 0 == 1 + return None + + # copy file from device (remoteFile) to host (localFile) + # external function + # returns: + # success: output of pullfile, string + # failure: None + def getFile(self, remoteFile, localFile = ''): + assert 0 == 1 + return None + + # copy directory structure from device (remoteDir) to host (localDir) + # external function + # checkDir exists so that we don't create local directories if the + # remote directory doesn't exist but also so that we don't call isDir + # twice when recursing. + # returns: + # success: list of files, string + # failure: None + def getDirectory(self, remoteDir, localDir, checkDir=True): + assert 0 == 1 + return None + + # external function + # returns: + # success: True + # failure: False + # Throws a FileError exception when null (invalid dir/filename) + def isDir(self, remotePath): + assert 0 == 1 + return False + + # true/false check if the two files have the same md5 sum + # external function + # returns: + # success: True + # failure: False + def validateFile(self, remoteFile, localFile): + assert 0 == 1 + return False + + # return the md5 sum of a remote file + # internal function + # returns: + # success: MD5 hash for given filename + # failure: None + def getRemoteHash(self, filename): + assert 0 == 1 + return None + + # return the md5 sum of a file on the host + # internal function + # returns: + # success: MD5 hash for given filename + # failure: None + def getLocalHash(self, filename): + file = open(filename, 'rb') + if (file == None): + return None + + try: + mdsum = hashlib.md5() + except: + return None + + while 1: + data = file.read(1024) + if not data: + break + mdsum.update(data) + + file.close() + hexval = mdsum.hexdigest() + if (self.debug >= 3): print "local hash returned: '" + hexval + "'" + return hexval + # Gets the device root for the testing area on the device + # For all devices we will use / type slashes and depend on the device-agent + # to sort those out. The agent will return us the device location where we + # should store things, we will then create our /tests structure relative to + # that returned path. + # Structure on the device is as follows: + # /tests + # /<fennec>|<firefox> --> approot + # /profile + # /xpcshell + # /reftest + # /mochitest + # + # external function + # returns: + # success: path for device root + # failure: None + def getDeviceRoot(self): + assert 0 == 1 + return None + + # Either we will have /tests/fennec or /tests/firefox but we will never have + # both. Return the one that exists + # TODO: ensure we can support org.mozilla.firefox + # external function + # returns: + # success: path for app root + # failure: None + def getAppRoot(self): + devroot = self.getDeviceRoot() + if (devroot == None): + return None + + if (self.dirExists(devroot + '/fennec')): + return devroot + '/fennec' + elif (self.dirExists(devroot + '/firefox')): + return devroot + '/firefox' + elif (self.dirExsts('/data/data/org.mozilla.fennec')): + return 'org.mozilla.fennec' + elif (self.dirExists('/data/data/org.mozilla.firefox')): + return 'org.mozilla.firefox' + elif (self.dirExists('/data/data/org.mozilla.fennec_aurora')): + return 'org.mozilla.fennec_aurora' + elif (self.dirExists('/data/data/org.mozilla.firefox_beta')): + return 'org.mozilla.firefox_beta' + + # Failure (either not installed or not a recognized platform) + return None + + # Gets the directory location on the device for a specific test type + # Type is one of: xpcshell|reftest|mochitest + # external function + # returns: + # success: path for test root + # failure: None + def getTestRoot(self, type): + devroot = self.getDeviceRoot() + if (devroot == None): + return None + + if (re.search('xpcshell', type, re.I)): + self.testRoot = devroot + '/xpcshell' + elif (re.search('?(i)reftest', type)): + self.testRoot = devroot + '/reftest' + elif (re.search('?(i)mochitest', type)): + self.testRoot = devroot + '/mochitest' + return self.testRoot + + # Sends a specific process ID a signal code and action. + # For Example: SIGINT and SIGDFL to process x + def signal(self, processID, signalType, signalAction): + # currently not implemented in device agent - todo + pass + + # Get a return code from process ending -- needs support on device-agent + def getReturnCode(self, processID): + # TODO: make this real + return 0 + + # external function + # returns: + # success: output of unzip command + # failure: None + def unpackFile(self, filename): + return None + + # external function + # returns: + # success: status from test agent + # failure: None + def reboot(self, ipAddr=None, port=30000): + assert 0 == 1 + return None + + # validate localDir from host to remoteDir on the device + # external function + # returns: + # success: True + # failure: False + def validateDir(self, localDir, remoteDir): + if (self.debug >= 2): print "validating directory: " + localDir + " to " + remoteDir + for root, dirs, files in os.walk(localDir): + parts = root.split(localDir) + for file in files: + remoteRoot = remoteDir + '/' + parts[1] + remoteRoot = remoteRoot.replace('/', '/') + if (parts[1] == ""): remoteRoot = remoteDir + remoteName = remoteRoot + '/' + file + if (self.validateFile(remoteName, os.path.join(root, file)) <> True): + return False + return True + + # Returns information about the device: + # Directive indicates the information you want to get, your choices are: + # os - name of the os + # id - unique id of the device + # uptime - uptime of the device + # systime - system time of the device + # screen - screen resolution + # memory - memory stats + # process - list of running processes (same as ps) + # disk - total, free, available bytes on disk + # power - power status (charge, battery temp) + # all - all of them - or call it with no parameters to get all the information + # returns: + # success: dict of info strings by directive name + # failure: {} + def getInfo(self, directive=None): + assert 0 == 1 + return {} + + # external function + # returns: + # success: output from agent for inst command + # failure: None + def installApp(self, appBundlePath, destPath=None): + assert 0 == 1 + return None + + # external function + # returns: + # success: True + # failure: None + def uninstallAppAndReboot(self, appName, installPath=None): + assert 0 == 1 + return None + + # external function + # returns: + # success: text status from command or callback server + # failure: None + def updateApp(self, appBundlePath, processName=None, destPath=None, ipAddr=None, port=30000): + assert 0 == 1 + return None + + # external function + # returns: + # success: time in ms + # failure: None + def getCurrentTime(self): + assert 0 == 1 + return None + + +class NetworkTools: + def __init__(self): + pass + + # Utilities to get the local ip address + def getInterfaceIp(self, ifname): + if os.name != "nt": + import fcntl + import struct + 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]) + else: + return None + + def getLanIp(self): + ip = socket.gethostbyname(socket.gethostname()) + if ip.startswith("127.") and os.name != "nt": + interfaces = ["eth0","eth1","eth2","wlan0","wlan1","wifi0","ath0","ath1","ppp0"] + for ifname in interfaces: + try: + ip = self.getInterfaceIp(ifname) + break; + except IOError: + pass + return ip + + # Gets an open port starting with the seed by incrementing by 1 each time + def findOpenPort(self, ip, seed): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + connected = False + if isinstance(seed, basestring): + seed = int(seed) + maxportnum = seed + 5000 # We will try at most 5000 ports to find an open one + while not connected: + try: + s.bind((ip, seed)) + connected = True + s.close() + break + except: + if seed > maxportnum: + print "Could not find open port after checking 5000 ports" + raise + seed += 1 + except: + print "Socket error trying to find open port" + + return seed +
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py @@ -0,0 +1,580 @@ +import subprocess +from devicemanager import DeviceManager, DMError +import re +import os +import sys + +class DeviceManagerADB(DeviceManager): + + def __init__(self, host = None, port = 20701, retrylimit = 5, packageName = None): + self.host = host + self.port = port + self.retrylimit = retrylimit + self.retries = 0 + self._sock = None + self.useRunAs = False + self.packageName = None + if packageName == None: + if os.getenv('USER'): + packageName = 'org.mozilla.fennec_' + os.getenv('USER') + else: + packageName = 'org.mozilla.fennec_' + self.Init(packageName) + + def Init(self, packageName): + # Initialization code that may fail: Catch exceptions here to allow + # successful initialization even if, for example, adb is not installed. + try: + self.verifyADB() + self.verifyRunAs(packageName) + except: + self.useRunAs = False + self.packageName = None + try: + # a test to see if we have root privs + files = self.listFiles("/data/data") + if (len(files) == 1): + if (files[0].find("Permission denied") != -1): + print "NOT running as root" + raise Exception("not running as root") + except: + try: + self.checkCmd(["root"]) + except: + print "restarting as root failed" + + # external function + # returns: + # success: True + # failure: False + def pushFile(self, localname, destname): + try: + if (os.name == "nt"): + destname = destname.replace('\\', '/') + if (self.useRunAs): + remoteTmpFile = self.tmpDir + "/" + os.path.basename(localname) + self.checkCmd(["push", os.path.realpath(localname), remoteTmpFile]) + self.checkCmdAs(["shell", "cp", remoteTmpFile, destname]) + self.checkCmd(["shell", "rm", remoteTmpFile]) + else: + self.checkCmd(["push", os.path.realpath(localname), destname]) + if (self.isDir(destname)): + destname = destname + "/" + os.path.basename(localname) + self.chmodDir(destname) + return True + except: + return False + + # external function + # returns: + # success: directory name + # failure: None + def mkDir(self, name): + try: + self.checkCmdAs(["shell", "mkdir", name]) + self.chmodDir(name) + return name + except: + return None + + # make directory structure on the device + # external function + # returns: + # success: directory structure that we created + # failure: None + def mkDirs(self, filename): + parts = filename.split('/') + name = "" + for part in parts: + if (part == parts[-1]): break + if (part != ""): + name += '/' + part + if (not self.dirExists(name)): + if (self.mkDir(name) == None): + print "failed making directory: " + str(name) + return None + return name + + # push localDir from host to remoteDir on the device + # external function + # returns: + # success: remoteDir + # failure: None + def pushDir(self, localDir, remoteDir): + # adb "push" accepts a directory as an argument, but if the directory + # contains symbolic links, the links are pushed, rather than the linked + # files; we push file-by-file to get around this limitation + try: + if (not self.dirExists(remoteDir)): + self.mkDirs(remoteDir+"/x") + for root, dirs, files in os.walk(localDir, followlinks='true'): + relRoot = os.path.relpath(root, localDir) + for file in files: + localFile = os.path.join(root, file) + remoteFile = remoteDir + "/" + if (relRoot!="."): + remoteFile = remoteFile + relRoot + "/" + remoteFile = remoteFile + file + self.pushFile(localFile, remoteFile) + for dir in dirs: + targetDir = remoteDir + "/" + if (relRoot!="."): + targetDir = targetDir + relRoot + "/" + targetDir = targetDir + dir + if (not self.dirExists(targetDir)): + self.mkDir(targetDir) + self.checkCmdAs(["shell", "chmod", "777", remoteDir]) + return True + except: + print "pushing " + localDir + " to " + remoteDir + " failed" + return False + + # external function + # returns: + # success: True + # failure: False + def dirExists(self, dirname): + return self.isDir(dirname) + + # Because we always have / style paths we make this a lot easier with some + # assumptions + # external function + # returns: + # success: True + # failure: False + def fileExists(self, filepath): + p = self.runCmd(["shell", "ls", "-a", filepath]) + data = p.stdout.readlines() + if (len(data) == 1): + if (data[0].rstrip() == filepath): + return True + return False + + def removeFile(self, filename): + return self.runCmd(["shell", "rm", filename]).stdout.read() + + # does a recursive delete of directory on the device: rm -Rf remoteDir + # external function + # returns: + # success: output of telnet, i.e. "removing file: /mnt/sdcard/tests/test.txt" + # failure: None + def removeSingleDir(self, remoteDir): + return self.runCmd(["shell", "rmdir", remoteDir]).stdout.read() + + # does a recursive delete of directory on the device: rm -Rf remoteDir + # external function + # returns: + # success: output of telnet, i.e. "removing file: /mnt/sdcard/tests/test.txt" + # failure: None + def removeDir(self, remoteDir): + out = "" + if (self.isDir(remoteDir)): + files = self.listFiles(remoteDir.strip()) + for f in files: + if (self.isDir(remoteDir.strip() + "/" + f.strip())): + out += self.removeDir(remoteDir.strip() + "/" + f.strip()) + else: + out += self.removeFile(remoteDir.strip() + "/" + f.strip()) + out += self.removeSingleDir(remoteDir.strip()) + else: + out += self.removeFile(remoteDir.strip()) + return out + + def isDir(self, remotePath): + p = self.runCmd(["shell", "ls", "-a", remotePath]) + data = p.stdout.readlines() + if (len(data) == 0): + return True + if (len(data) == 1): + if (data[0].rstrip() == remotePath): + return False + if (data[0].find("No such file or directory") != -1): + return False + if (data[0].find("Not a directory") != -1): + return False + return True + + def listFiles(self, rootdir): + p = self.runCmd(["shell", "ls", "-a", rootdir]) + data = p.stdout.readlines() + if (len(data) == 1): + if (data[0] == rootdir): + return [] + if (data[0].find("No such file or directory") != -1): + return [] + if (data[0].find("Not a directory") != -1): + return [] + return data + + # external function + # returns: + # success: array of process tuples + # failure: [] + def getProcessList(self): + p = self.runCmd(["shell", "ps"]) + # first line is the headers + p.stdout.readline() + proc = p.stdout.readline() + ret = [] + while (proc): + els = proc.split() + ret.append(list([els[1], els[len(els) - 1], els[0]])) + proc = p.stdout.readline() + return ret + + # external function + # returns: + # success: pid + # failure: None + def fireProcess(self, appname, failIfRunning=False): + #strip out env vars + parts = appname.split('"'); + if (len(parts) > 2): + parts = parts[2:] + return self.launchProcess(parts, failIfRunning) + + # external function + # returns: + # success: output filename + # failure: None + def launchProcess(self, cmd, outputFile = "process.txt", cwd = '', env = '', failIfRunning=False): + acmd = ["shell", "am","start"] + cmd = ' '.join(cmd).strip() + i = cmd.find(" ") + acmd.append("-n") + acmd.append(cmd[0:i] + "/.App") + acmd.append("--es") + acmd.append("args") + acmd.append(cmd[i:]) + print acmd + self.checkCmd(acmd) + return outputFile; + + # external function + # returns: + # success: output from testagent + # failure: None + def killProcess(self, appname): + procs = self.getProcessList() + for (pid, name, user) in procs: + if name == appname: + p = self.runCmdAs(["shell", "kill", pid]) + return p.stdout.read() + return None + + # external function + # returns: + # success: filecontents + # failure: None + def catFile(self, remoteFile): + #p = self.runCmd(["shell", "cat", remoteFile]) + #return p.stdout.read() + return self.getFile(remoteFile) + + # external function + # returns: + # success: output of pullfile, string + # failure: None + def pullFile(self, remoteFile): + #return self.catFile(remoteFile) + return self.getFile(remoteFile) + + # copy file from device (remoteFile) to host (localFile) + # external function + # returns: + # success: output of pullfile, string + # failure: None + def getFile(self, remoteFile, localFile = 'tmpfile_dm_adb'): + # TODO: add debug flags and allow for printing stdout + # self.runCmd(["pull", remoteFile, localFile]) + try: + self.runCmd(["pull", remoteFile, localFile]).stdout.read() + f = open(localFile) + ret = f.read() + f.close() + return ret; + except: + return None + + # copy directory structure from device (remoteDir) to host (localDir) + # external function + # checkDir exists so that we don't create local directories if the + # remote directory doesn't exist but also so that we don't call isDir + # twice when recursing. + # returns: + # success: list of files, string + # failure: None + def getDirectory(self, remoteDir, localDir, checkDir=True): + ret = [] + p = self.runCmd(["pull", remoteDir, localDir]) + p.stderr.readline() + line = p.stderr.readline() + while (line): + els = line.split() + f = els[len(els) - 1] + i = f.find(localDir) + if (i != -1): + if (localDir[len(localDir) - 1] != '/'): + i = i + 1 + f = f[i + len(localDir):] + i = f.find("/") + if (i > 0): + f = f[0:i] + ret.append(f) + line = p.stderr.readline() + #the last line is a summary + if (len(ret) > 0): + ret.pop() + return ret + + + + # true/false check if the two files have the same md5 sum + # external function + # returns: + # success: True + # failure: False + def validateFile(self, remoteFile, localFile): + return self.getRemoteHash(remoteFile) == self.getLocalHash(localFile) + + # return the md5 sum of a remote file + # internal function + # returns: + # success: MD5 hash for given filename + # failure: None + def getRemoteHash(self, filename): + data = p = self.runCmd(["shell", "ls", "-l", filename]).stdout.read() + return data.split()[3] + + def getLocalHash(self, filename): + data = p = subprocess.Popen(["ls", "-l", filename], stdout=subprocess.PIPE).stdout.read() + return data.split()[4] + + # Gets the device root for the testing area on the device + # For all devices we will use / type slashes and depend on the device-agent + # to sort those out. The agent will return us the device location where we + # should store things, we will then create our /tests structure relative to + # that returned path. + # Structure on the device is as follows: + # /tests + # /<fennec>|<firefox> --> approot + # /profile + # /xpcshell + # /reftest + # /mochitest + # + # external function + # returns: + # success: path for device root + # failure: None + def getDeviceRoot(self): + # /mnt/sdcard/tests is preferred to /data/local/tests, but this can be + # over-ridden by creating /data/local/tests + testRoot = "/data/local/tests" + if (self.dirExists(testRoot)): + return testRoot + root = "/mnt/sdcard" + if (not self.dirExists(root)): + root = "/data/local" + testRoot = root + "/tests" + if (not self.dirExists(testRoot)): + self.mkDir(testRoot) + return testRoot + + # Either we will have /tests/fennec or /tests/firefox but we will never have + # both. Return the one that exists + # TODO: ensure we can support org.mozilla.firefox + # external function + # returns: + # success: path for app root + # failure: None + def getAppRoot(self): + devroot = self.getDeviceRoot() + if (devroot == None): + return None + + if (self.dirExists(devroot + '/fennec')): + return devroot + '/fennec' + elif (self.dirExists(devroot + '/firefox')): + return devroot + '/firefox' + elif (self.packageName and self.dirExists('/data/data/' + self.packageName)): + return '/data/data/' + self.packageName + + # Failure (either not installed or not a recognized platform) + print "devicemanagerADB: getAppRoot failed" + return None + + # Gets the directory location on the device for a specific test type + # Type is one of: xpcshell|reftest|mochitest + # external function + # returns: + # success: path for test root + # failure: None + def getTestRoot(self, type): + devroot = self.getDeviceRoot() + if (devroot == None): + return None + + if (re.search('xpcshell', type, re.I)): + self.testRoot = devroot + '/xpcshell' + elif (re.search('?(i)reftest', type)): + self.testRoot = devroot + '/reftest' + elif (re.search('?(i)mochitest', type)): + self.testRoot = devroot + '/mochitest' + return self.testRoot + + + # external function + # returns: + # success: status from test agent + # failure: None + def reboot(self, wait = False): + ret = self.runCmd(["reboot"]).stdout.read() + if (not wait): + return "Success" + countdown = 40 + while (countdown > 0): + countdown + try: + self.checkCmd(["wait-for-device", "shell", "ls", "/sbin"]) + return ret + except: + try: + self.checkCmd(["root"]) + except: + time.sleep(1) + print "couldn't get root" + return "Success" + + # external function + # returns: + # success: text status from command or callback server + # failure: None + def updateApp(self, appBundlePath, processName=None, destPath=None, ipAddr=None, port=30000): + return self.runCmd(["install", "-r", appBundlePath]).stdout.read() + + # external function + # returns: + # success: time in ms + # failure: None + def getCurrentTime(self): + timestr = self.runCmd(["shell", "date", "+%s"]).stdout.read().strip() + if (not timestr or not timestr.isdigit()): + return None + return str(int(timestr)*1000) + + # Returns information about the device: + # Directive indicates the information you want to get, your choices are: + # os - name of the os + # id - unique id of the device + # uptime - uptime of the device + # systime - system time of the device + # screen - screen resolution + # memory - memory stats + # process - list of running processes (same as ps) + # disk - total, free, available bytes on disk + # power - power status (charge, battery temp) + # all - all of them - or call it with no parameters to get all the information + # returns: + # success: dict of info strings by directive name + # failure: {} + def getInfo(self, directive="all"): + ret = {} + if (directive == "id" or directive == "all"): + ret["id"] = self.runCmd(["get-serialno"]).stdout.read() + if (directive == "os" or directive == "all"): + ret["os"] = self.runCmd(["shell", "getprop", "ro.build.display.id"]).stdout.read() + if (directive == "uptime" or directive == "all"): + utime = self.runCmd(["shell", "uptime"]).stdout.read() + if (not utime): + raise DMError("error getting uptime") + utime = utime[9:] + hours = utime[0:utime.find(":")] + utime = utime[utime[1:].find(":") + 2:] + minutes = utime[0:utime.find(":")] + utime = utime[utime[1:].find(":") + 2:] + seconds = utime[0:utime.find(",")] + ret["uptime"] = ["0 days " + hours + " hours " + minutes + " minutes " + seconds + " seconds"] + if (directive == "process" or directive == "all"): + ret["process"] = self.runCmd(["shell", "ps"]).stdout.read() + if (directive == "systime" or directive == "all"): + ret["systime"] = self.runCmd(["shell", "date"]).stdout.read() + print ret + return ret + + def runCmd(self, args): + args.insert(0, "adb") + return subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + def runCmdAs(self, args): + if self.useRunAs: + args.insert(1, "run-as") + args.insert(2, self.packageName) + return self.runCmd(args) + + def checkCmd(self, args): + args.insert(0, "adb") + return subprocess.check_call(args) + + def checkCmdAs(self, args): + if (self.useRunAs): + args.insert(1, "run-as") + args.insert(2, self.packageName) + return self.checkCmd(args) + + def chmodDir(self, remoteDir): + if (self.isDir(remoteDir)): + files = self.listFiles(remoteDir.strip()) + for f in files: + if (self.isDir(remoteDir.strip() + "/" + f.strip())): + self.chmodDir(remoteDir.strip() + "/" + f.strip()) + else: + self.checkCmdAs(["shell", "chmod", "777", remoteDir.strip()]) + print "chmod " + remoteDir.strip() + self.checkCmdAs(["shell", "chmod", "777", remoteDir]) + print "chmod " + remoteDir + else: + self.checkCmdAs(["shell", "chmod", "777", remoteDir.strip()]) + print "chmod " + remoteDir.strip() + + def verifyADB(self): + # Check to see if adb itself can be executed. + try: + self.runCmd(["version"]) + except Exception as (ex): + print "unable to execute ADB: ensure Android SDK is installed and adb is in your $PATH" + + def isCpAvailable(self): + # Some Android systems may not have a cp command installed, + # or it may not be executable by the user. + data = self.runCmd(["shell", "cp"]).stdout.read() + if (re.search('Usage', data)): + return True + else: + print "unable to execute 'cp' on device; consider installing busybox from Android Market" + return False + + def verifyRunAs(self, packageName): + # If a valid package name is available, and certain other + # conditions are met, devicemanagerADB can execute file operations + # via the "run-as" command, so that pushed files and directories + # are created by the uid associated with the package, more closely + # echoing conditions encountered by Fennec at run time. + # Check to see if run-as can be used here, by verifying a + # file copy via run-as. + self.useRunAs = False + devroot = self.getDeviceRoot() + if (packageName and self.isCpAvailable() and devroot): + self.tmpDir = devroot + "/tmp" + if (not self.dirExists(self.tmpDir)): + self.mkDir(self.tmpDir) + self.checkCmd(["shell", "run-as", packageName, "mkdir", devroot + "/sanity"]) + self.checkCmd(["push", os.path.abspath(sys.argv[0]), self.tmpDir + "/tmpfile"]) + self.checkCmd(["shell", "run-as", packageName, "cp", self.tmpDir + "/tmpfile", devroot + "/sanity"]) + if (self.fileExists(devroot + "/sanity/tmpfile")): + print "will execute commands via run-as " + packageName + self.packageName = packageName + self.useRunAs = True + self.checkCmd(["shell", "rm", devroot + "/tmp/tmpfile"]) + self.checkCmd(["shell", "run-as", packageName, "rm", "-r", devroot + "/sanity"]) +
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py @@ -0,0 +1,1239 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is Test Automation Framework. +# +# The Initial Developer of the Original Code is Joel Maher. +# +# Portions created by the Initial Developer are Copyright (C) 2009 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Joel Maher <joel.maher@gmail.com> (Original Developer) +# Clint Talbert <cmtalbert@gmail.com> +# Mark Cote <mcote@mozilla.com> +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +import socket +import SocketServer +import time, datetime +import os +import re +import hashlib +import subprocess +from threading import Thread +import traceback +import sys +from devicemanager import DeviceManager, DMError, FileError, NetworkTools + +class DeviceManagerSUT(DeviceManager): + host = '' + port = 0 + debug = 2 + retries = 0 + tempRoot = os.getcwd() + base_prompt = '$>' + base_prompt_re = '\$\>' + prompt_sep = '\x00' + prompt_regex = '.*(' + base_prompt_re + prompt_sep + ')' + agentErrorRE = re.compile('^##AGENT-WARNING##.*') + + # TODO: member variable to indicate error conditions. + # This should be set to a standard error from the errno module. + # So, for example, when an error occurs because of a missing file/directory, + # before returning, the function would do something like 'self.error = errno.ENOENT'. + # The error would be set where appropriate--so sendCMD() could set socket errors, + # pushFile() and other file-related commands could set filesystem errors, etc. + + def __init__(self, host, port = 20701, retrylimit = 5): + self.host = host + self.port = port + self.retrylimit = retrylimit + self.retries = 0 + self._sock = None + self.getDeviceRoot() + + def cmdNeedsResponse(self, cmd): + """ Not all commands need a response from the agent: + * if the cmd matches the pushRE then it is the first half of push + and therefore we want to wait until the second half before looking + for a response + * rebt obviously doesn't get a response + * uninstall performs a reboot to ensure starting in a clean state and + so also doesn't look for a response + """ + noResponseCmds = [re.compile('^push .*$'), + re.compile('^rebt'), + re.compile('^uninst .*$'), + re.compile('^pull .*$')] + + for c in noResponseCmds: + if (c.match(cmd)): + return False + + # If the command is not in our list, then it gets a response + return True + + def shouldCmdCloseSocket(self, cmd): + """ Some commands need to close the socket after they are sent: + * push + * rebt + * uninst + * quit + """ + + socketClosingCmds = [re.compile('^push .*$'), + re.compile('^quit.*'), + re.compile('^rebt.*'), + re.compile('^uninst .*$')] + + for c in socketClosingCmds: + if (c.match(cmd)): + return True + + return False + + # convenience function to enable checks for agent errors + def verifySendCMD(self, cmdline, newline = True): + return self.sendCMD(cmdline, newline, False) + + + # + # create a wrapper for sendCMD that loops up to self.retrylimit iterations. + # this allows us to move the retry logic outside of the _doCMD() to make it + # easier for debugging in the future. + # note that since cmdline is a list of commands, they will all be retried if + # one fails. this is necessary in particular for pushFile(), where we don't want + # to accidentally send extra data if a failure occurs during data transmission. + # + def sendCMD(self, cmdline, newline = True, ignoreAgentErrors = True): + done = False + while (not done): + retVal = self._doCMD(cmdline, newline) + if (retVal is None): + self.retries += 1 + else: + self.retries = 0 + if ignoreAgentErrors == False: + if (self.agentErrorRE.match(retVal)): + raise DMError("error on the agent executing '%s'" % cmdline) + return retVal + + if (self.retries >= self.retrylimit): + done = True + + raise DMError("unable to connect to %s after %s attempts" % (self.host, self.retrylimit)) + + def _doCMD(self, cmdline, newline = True): + promptre = re.compile(self.prompt_regex + '$') + data = "" + shouldCloseSocket = False + recvGuard = 1000 + + if (self._sock == None): + try: + if (self.debug >= 1): + print "reconnecting socket" + self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + except: + self._sock = None + if (self.debug >= 2): + print "unable to create socket" + return None + + try: + self._sock.connect((self.host, int(self.port))) + self._sock.recv(1024) + except: + self._sock.close() + self._sock = None + if (self.debug >= 2): + print "unable to connect socket" + return None + + for cmd in cmdline: + if newline: cmd += '\r\n' + + try: + numbytes = self._sock.send(cmd) + if (numbytes != len(cmd)): + print "ERROR: our cmd was " + str(len(cmd)) + " bytes and we only sent " + str(numbytes) + return None + if (self.debug >= 4): print "send cmd: " + str(cmd) + except: + self._sock.close() + self._sock = None + return None + + # Check if the command should close the socket + shouldCloseSocket = self.shouldCmdCloseSocket(cmd) + + # Handle responses from commands + if (self.cmdNeedsResponse(cmd)): + found = False + loopguard = 0 + + while (found == False and (loopguard < recvGuard)): + temp = '' + if (self.debug >= 4): print "recv'ing..." + + # Get our response + try: + temp = self._sock.recv(1024) + if (self.debug >= 4): print "response: " + str(temp) + except: + self._sock.close() + self._sock = None + return None + + # If something goes wrong in the agent it will send back a string that + # starts with '##AGENT-ERROR##' + if (self.agentErrorRE.match(temp)): + data = temp + break + + lines = temp.split('\n') + + for line in lines: + if (promptre.match(line)): + found = True + data += temp + + # If we violently lose the connection to the device, this loop tends to spin, + # this guard prevents that + if (temp == ''): + loopguard += 1 + + if (shouldCloseSocket == True): + try: + self._sock.close() + self._sock = None + except: + self._sock = None + return None + + return data + + # internal function + # take a data blob and strip instances of the prompt '$>\x00' + def stripPrompt(self, data): + promptre = re.compile(self.prompt_regex + '.*') + retVal = [] + lines = data.split('\n') + for line in lines: + try: + while (promptre.match(line)): + pieces = line.split(self.prompt_sep) + index = pieces.index('$>') + pieces.pop(index) + line = self.prompt_sep.join(pieces) + except(ValueError): + pass + retVal.append(line) + + return '\n'.join(retVal) + + + # external function + # returns: + # success: True + # failure: False + def pushFile(self, localname, destname): + if (os.name == "nt"): + destname = destname.replace('\\', '/') + + if (self.debug >= 3): print "in push file with: " + localname + ", and: " + destname + if (self.dirExists(destname)): + if (not destname.endswith('/')): + destname = destname + '/' + destname = destname + os.path.basename(localname) + if (self.validateFile(destname, localname) == True): + if (self.debug >= 3): print "files are validated" + return True + + if self.mkDirs(destname) == None: + print "unable to make dirs: " + destname + return False + + if (self.debug >= 3): print "sending: push " + destname + + filesize = os.path.getsize(localname) + f = open(localname, 'rb') + data = f.read() + f.close() + + try: + retVal = self.verifySendCMD(['push ' + destname + ' ' + str(filesize) + '\r\n', data], newline = False) + except(DMError): + retVal = False + + if (self.debug >= 3): print "push returned: " + str(retVal) + + validated = False + if (retVal): + retline = self.stripPrompt(retVal).strip() + if (retline == None): + # Then we failed to get back a hash from agent, try manual validation + validated = self.validateFile(destname, localname) + else: + # Then we obtained a hash from push + localHash = self.getLocalHash(localname) + if (str(localHash) == str(retline)): + validated = True + else: + # We got nothing back from sendCMD, try manual validation + validated = self.validateFile(destname, localname) + + if (validated): + if (self.debug >= 3): print "Push File Validated!" + return True + else: + if (self.debug >= 2): print "Push File Failed to Validate!" + return False + + # external function + # returns: + # success: directory name + # failure: None + def mkDir(self, name): + if (self.dirExists(name)): + return name + else: + try: + retVal = self.verifySendCMD(['mkdr ' + name]) + except(DMError): + retVal = None + return retVal + + # make directory structure on the device + # external function + # returns: + # success: directory structure that we created + # failure: None + def mkDirs(self, filename): + parts = filename.split('/') + name = "" + for part in parts: + if (part == parts[-1]): break + if (part != ""): + name += '/' + part + if (self.mkDir(name) == None): + print "failed making directory: " + str(name) + return None + return name + + # push localDir from host to remoteDir on the device + # external function + # returns: + # success: remoteDir + # failure: None + def pushDir(self, localDir, remoteDir): + if (self.debug >= 2): print "pushing directory: %s to %s" % (localDir, remoteDir) + for root, dirs, files in os.walk(localDir): + parts = root.split(localDir) + for file in files: + remoteRoot = remoteDir + '/' + parts[1] + if (remoteRoot.endswith('/')): + remoteName = remoteRoot + file + else: + remoteName = remoteRoot + '/' + file + if (parts[1] == ""): remoteRoot = remoteDir + if (self.pushFile(os.path.join(root, file), remoteName) == False): + # retry once + self.removeFile(remoteName) + if (self.pushFile(os.path.join(root, file), remoteName) == False): + return None + return remoteDir + + # external function + # returns: + # success: True + # failure: False + def dirExists(self, dirname): + match = ".*" + dirname + "$" + dirre = re.compile(match) + try: + data = self.verifySendCMD(['cd ' + dirname, 'cwd']) + except(DMError): + return False + + retVal = self.stripPrompt(data) + data = retVal.split('\n') + found = False + for d in data: + if (dirre.match(d)): + found = True + + return found + + # Because we always have / style paths we make this a lot easier with some + # assumptions + # external function + # returns: + # success: True + # failure: False + def fileExists(self, filepath): + s = filepath.split('/') + containingpath = '/'.join(s[:-1]) + listfiles = self.listFiles(containingpath) + for f in listfiles: + if (f == s[-1]): + return True + return False + + # list files on the device, requires cd to directory first + # external function + # returns: + # success: array of filenames, ['file1', 'file2', ...] + # failure: [] + def listFiles(self, rootdir): + rootdir = rootdir.rstrip('/') + if (self.dirExists(rootdir) == False): + return [] + try: + data = self.verifySendCMD(['cd ' + rootdir, 'ls']) + except(DMError): + return [] + + retVal = self.stripPrompt(data) + files = filter(lambda x: x, retVal.split('\n')) + if len(files) == 1 and files[0] == '<empty>': + # special case on the agent: empty directories return just the string "<empty>" + return [] + return files + + # external function + # returns: + # success: output of telnet, i.e. "removing file: /mnt/sdcard/tests/test.txt" + # failure: None + def removeFile(self, filename): + if (self.debug>= 2): print "removing file: " + filename + try: + retVal = self.verifySendCMD(['rm ' + filename]) + except(DMError): + return None + + return retVal + + # does a recursive delete of directory on the device: rm -Rf remoteDir + # external function + # returns: + # success: output of telnet, i.e. "removing file: /mnt/sdcard/tests/test.txt" + # failure: None + def removeDir(self, remoteDir): + try: + retVal = self.verifySendCMD(['rmdr ' + remoteDir]) + except(DMError): + return None + + return retVal + + # external function + # returns: + # success: array of process tuples + # failure: [] + def getProcessList(self): + try: + data = self.verifySendCMD(['ps']) + except DMError: + return [] + + retVal = self.stripPrompt(data) + lines = retVal.split('\n') + files = [] + for line in lines: + if (line.strip() != ''): + pidproc = line.strip().split() + if (len(pidproc) == 2): + files += [[pidproc[0], pidproc[1]]] + elif (len(pidproc) == 3): + #android returns <userID> <procID> <procName> + files += [[pidproc[1], pidproc[2], pidproc[0]]] + return files + + # external function + # returns: + # success: pid + # failure: None + def fireProcess(self, appname, failIfRunning=False): + if (not appname): + if (self.debug >= 1): print "WARNING: fireProcess called with no command to run" + return None + + if (self.debug >= 2): print "FIRE PROC: '" + appname + "'" + + if (self.processExist(appname) != None): + print "WARNING: process %s appears to be running already\n" % appname + if (failIfRunning): + return None + + try: + data = self.verifySendCMD(['exec ' + appname]) + except(DMError): + return None + + # wait up to 30 seconds for process to start up + timeslept = 0 + while (timeslept <= 30): + process = self.processExist(appname) + if (process is not None): + break + time.sleep(3) + timeslept += 3 + + if (self.debug >= 4): print "got pid: %s for process: %s" % (process, appname) + return process + + # external function + # returns: + # success: output filename + # failure: None + def launchProcess(self, cmd, outputFile = "process.txt", cwd = '', env = '', failIfRunning=False): + if not cmd: + if (self.debug >= 1): print "WARNING: launchProcess called without command to run" + return None + + cmdline = subprocess.list2cmdline(cmd) + if (outputFile == "process.txt" or outputFile == None): + outputFile = self.getDeviceRoot(); + if outputFile is None: + return None + outputFile += "/process.txt" + cmdline += " > " + outputFile + + # Prepend our env to the command + cmdline = '%s %s' % (self.formatEnvString(env), cmdline) + + if self.fireProcess(cmdline, failIfRunning) is None: + return None + return outputFile + + # iterates process list and returns pid if exists, otherwise None + # external function + # returns: + # success: pid + # failure: None + def processExist(self, appname): + pid = None + + #filter out extra spaces + parts = filter(lambda x: x != '', appname.split(' ')) + appname = ' '.join(parts) + + #filter out the quoted env string if it exists + #ex: '"name=value;name2=value2;etc=..." process args' -> 'process args' + parts = appname.split('"') + if (len(parts) > 2): + appname = ' '.join(parts[2:]).strip() + + pieces = appname.split(' ') + parts = pieces[0].split('/') + app = parts[-1] + procre = re.compile('.*' + app + '.*') + + procList = self.getProcessList() + if (procList == []): + return None + + for proc in procList: + if (procre.match(proc[1])): + pid = proc[0] + break + return pid + + # external function + # returns: + # success: output from testagent + # failure: None + def killProcess(self, appname): + try: + data = self.verifySendCMD(['kill ' + appname]) + except(DMError): + return None + + return data + + # external function + # returns: + # success: tmpdir, string + # failure: None + def getTempDir(self): + try: + data = self.verifySendCMD(['tmpd']) + except(DMError): + return None + + return self.stripPrompt(data).strip('\n') + + # external function + # returns: + # success: filecontents + # failure: None + def catFile(self, remoteFile): + try: + data = self.verifySendCMD(['cat ' + remoteFile]) + except(DMError): + return None + + return self.stripPrompt(data) + + # external function + # returns: + # success: output of pullfile, string + # failure: None + def pullFile(self, remoteFile): + """Returns contents of remoteFile using the "pull" command. + The "pull" command is different from other commands in that DeviceManager + has to read a certain number of bytes instead of just reading to the + next prompt. This is more robust than the "cat" command, which will be + confused if the prompt string exists within the file being catted. + However it means we can't use the response-handling logic in sendCMD(). + """ + + def err(error_msg): + err_str = 'error returned from pull: %s' % error_msg + print err_str + self._sock = None + raise FileError(err_str) + + # FIXME: We could possibly move these socket-reading functions up to + # the class level if we wanted to refactor sendCMD(). For now they are + # only used to pull files. + + def uread(to_recv, error_msg): + """ unbuffered read """ + try: + data = self._sock.recv(to_recv) + if not data: + err(error_msg) + return None + return data + except: + err(error_msg) + return None + + def read_until_char(c, buffer, error_msg): + """ read until 'c' is found; buffer rest """ + while not '\n' in buffer: + data = uread(1024, error_msg) + if data == None: + err(error_msg) + return ('', '', '') + buffer += data + return buffer.partition(c) + + def read_exact(total_to_recv, buffer, error_msg): + """ read exact number of 'total_to_recv' bytes """ + while len(buffer) < total_to_recv: + to_recv = min(total_to_recv - len(buffer), 1024) + data = uread(to_recv, error_msg) + if data == None: + return None + buffer += data + return buffer + + prompt = self.base_prompt + self.prompt_sep + buffer = '' + + # expected return value: + # <filename>,<filesize>\n<filedata> + # or, if error, + # <filename>,-1\n<error message> + try: + data = self.verifySendCMD(['pull ' + remoteFile]) + except(DMError): + return None + + # read metadata; buffer the rest + metadata, sep, buffer = read_until_char('\n', buffer, 'could not find metadata') + if not metadata: + return None + if self.debug >= 3: + print 'metadata: %s' % metadata + + filename, sep, filesizestr = metadata.partition(',') + if sep == '': + err('could not find file size in returned metadata') + return None + try: + filesize = int(filesizestr) + except ValueError: + err('invalid file size in returned metadata') + return None + + if filesize == -1: + # read error message + error_str, sep, buffer = read_until_char('\n', buffer, 'could not find error message') + if not error_str: + return None + # prompt should follow + read_exact(len(prompt), buffer, 'could not find prompt') + print "DeviceManager: error pulling file '%s': %s" % (remoteFile, error_str) + return None + + # read file data + total_to_recv = filesize + len(prompt) + buffer = read_exact(total_to_recv, buffer, 'could not get all file data') + if buffer == None: + return None + if buffer[-len(prompt):] != prompt: + err('no prompt found after file data--DeviceManager may be out of sync with agent') + return buffer + return buffer[:-len(prompt)] + + # copy file from device (remoteFile) to host (localFile) + # external function + # returns: + # success: output of pullfile, string + # failure: None + def getFile(self, remoteFile, localFile = ''): + if localFile == '': + localFile = os.path.join(self.tempRoot, "temp.txt") + + try: + retVal = self.pullFile(remoteFile) + except: + return None + + if (retVal is None): + return None + + fhandle = open(localFile, 'wb') + fhandle.write(retVal) + fhandle.close() + if not self.validateFile(remoteFile, localFile): + print 'failed to validate file when downloading %s!' % remoteFile + return None + return retVal + + # copy directory structure from device (remoteDir) to host (localDir) + # external function + # checkDir exists so that we don't create local directories if the + # remote directory doesn't exist but also so that we don't call isDir + # twice when recursing. + # returns: + # success: list of files, string + # failure: None + def getDirectory(self, remoteDir, localDir, checkDir=True): + if (self.debug >= 2): print "getting files in '" + remoteDir + "'" + if checkDir: + try: + is_dir = self.isDir(remoteDir) + except FileError: + return None + if not is_dir: + return None + + filelist = self.listFiles(remoteDir) + if (self.debug >= 3): print filelist + if not os.path.exists(localDir): + os.makedirs(localDir) + + for f in filelist: + if f == '.' or f == '..': + continue + remotePath = remoteDir + '/' + f + localPath = os.path.join(localDir, f) + try: + is_dir = self.isDir(remotePath) + except FileError: + print 'isdir failed on file "%s"; continuing anyway...' % remotePath + continue + if is_dir: + if (self.getDirectory(remotePath, localPath, False) == None): + print 'failed to get directory "%s"' % remotePath + return None + else: + # It's sometimes acceptable to have getFile() return None, such as + # when the agent encounters broken symlinks. + # FIXME: This should be improved so we know when a file transfer really + # failed. + if self.getFile(remotePath, localPath) == None: + print 'failed to get file "%s"; continuing anyway...' % remotePath + return filelist + + # external function + # returns: + # success: True + # failure: False + # Throws a FileError exception when null (invalid dir/filename) + def isDir(self, remotePath): + try: + data = self.verifySendCMD(['isdir ' + remotePath]) + except(DMError): + # normally there should be no error here; a nonexistent file/directory will + # return the string "<filename>: No such file or directory". + # However, I've seen AGENT-WARNING returned before. + return False + retVal = self.stripPrompt(data).strip() + if not retVal: + raise FileError('isdir returned null') + return retVal == 'TRUE' + + # true/false check if the two files have the same md5 sum + # external function + # returns: + # success: True + # failure: False + def validateFile(self, remoteFile, localFile): + remoteHash = self.getRemoteHash(remoteFile) + localHash = self.getLocalHash(localFile) + + if (remoteHash == None): + return False + + if (remoteHash == localHash): + return True + + return False + + # return the md5 sum of a remote file + # internal function + # returns: + # success: MD5 hash for given filename + # failure: None + def getRemoteHash(self, filename): + try: + data = self.verifySendCMD(['hash ' + filename]) + except(DMError): + return None + + retVal = self.stripPrompt(data) + if (retVal != None): + retVal = retVal.strip('\n') + if (self.debug >= 3): print "remote hash returned: '" + retVal + "'" + return retVal + + # Gets the device root for the testing area on the device + # For all devices we will use / type slashes and depend on the device-agent + # to sort those out. The agent will return us the device location where we + # should store things, we will then create our /tests structure relative to + # that returned path. + # Structure on the device is as follows: + # /tests + # /<fennec>|<firefox> --> approot + # /profile + # /xpcshell + # /reftest + # /mochitest + # + # external function + # returns: + # success: path for device root + # failure: None + def getDeviceRoot(self): + try: + data = self.verifySendCMD(['testroot']) + except: + return None + + deviceRoot = self.stripPrompt(data).strip('\n') + '/tests' + + if (not self.dirExists(deviceRoot)): + if (self.mkDir(deviceRoot) == None): + return None + + return deviceRoot + + # external function + # returns: + # success: output of unzip command + # failure: None + def unpackFile(self, filename): + devroot = self.getDeviceRoot() + if (devroot == None): + return None + + dir = '' + parts = filename.split('/') + if (len(parts) > 1): + if self.fileExists(filename): + dir = '/'.join(parts[:-1]) + elif self.fileExists('/' + filename): + dir = '/' + filename + elif self.fileExists(devroot + '/' + filename): + dir = devroot + '/' + filename + else: + return None + + try: + data = self.verifySendCMD(['cd ' + dir, 'unzp ' + filename]) + except(DMError): + return None + + return data + + # external function + # returns: + # success: status from test agent + # failure: None + def reboot(self, ipAddr=None, port=30000): + cmd = 'rebt' + + if (self.debug > 3): print "INFO: sending rebt command" + callbacksvrstatus = None + + if (ipAddr is not None): + #create update.info file: + try: + destname = '/data/data/com.mozilla.SUTAgentAndroid/files/update.info' + data = "%s,%s\rrebooting\r" % (ipAddr, port) + self.verifySendCMD(['push ' + destname + ' ' + str(len(data)) + '\r\n', data], newline = False) + except(DMError): + return None + + ip, port = self.getCallbackIpAndPort(ipAddr, port) + cmd += " %s %s" % (ip, port) + # Set up our callback server + callbacksvr = callbackServer(ip, port, self.debug) + + try: + status = self.verifySendCMD([cmd]) + except(DMError): + return None + + if (ipAddr is not None): + status = callbacksvr.disconnect() + + if (self.debug > 3): print "INFO: rebt- got status back: " + str(status) + return status + + # Returns information about the device: + # Directive indicates the information you want to get, your choices are: + # os - name of the os + # id - unique id of the device + # uptime - uptime of the device + # systime - system time of the device + # screen - screen resolution + # memory - memory stats + # process - list of running processes (same as ps) + # disk - total, free, available bytes on disk + # power - power status (charge, battery temp) + # all - all of them - or call it with no parameters to get all the information + # returns: + # success: dict of info strings by directive name + # failure: {} + def getInfo(self, directive=None): + data = None + result = {} + collapseSpaces = re.compile(' +') + + directives = ['os', 'id','uptime','systime','screen','memory','process', + 'disk','power'] + if (directive in directives): + directives = [directive] + + for d in directives: + data = self.verifySendCMD(['info ' + d]) + if (data is None): + continue + data = self.stripPrompt(data) + data = collapseSpaces.sub(' ', data) + result[d] = data.split('\n') + + # Get rid of any 0 length members of the arrays + for k, v in result.iteritems(): + result[k] = filter(lambda x: x != '', result[k]) + + # Format the process output + if 'process' in result: + proclist = [] + for l in result['process']: + if l: + proclist.append(l.split('\t')) + result['process'] = proclist + + if (self.debug >= 3): print "results: " + str(result) + return result + + """ + Installs the application onto the device + Application bundle - path to the application bundle on the device + Destination - destination directory of where application should be + installed to (optional) + Returns None for success, or output if known failure + """ + # external function + # returns: + # success: output from agent for inst command + # failure: None + def installApp(self, appBundlePath, destPath=None): + cmd = 'inst ' + appBundlePath + if destPath: + cmd += ' ' + destPath + try: + data = self.verifySendCMD([cmd]) + except(DMError): + return None + + f = re.compile('Failure') + for line in data.split(): + if (f.match(line)): + return data + return None + + """ + Uninstalls the named application from device and causes a reboot. + Takes an optional argument of installation path - the path to where the application + was installed. + Returns True, but it doesn't mean anything other than the command was sent, + the reboot happens and we don't know if this succeeds or not. + """ + # external function + # returns: + # success: True + # failure: None + def uninstallAppAndReboot(self, appName, installPath=None): + cmd = 'uninst ' + appName + if installPath: + cmd += ' ' + installPath + try: + data = self.verifySendCMD([cmd]) + except(DMError): + return None + + if (self.debug > 3): print "uninstallAppAndReboot: " + str(data) + return True + + """ + Updates the application on the device. + Application bundle - path to the application bundle on the device + Process name of application - used to end the process if the applicaiton is + currently running + Destination - Destination directory to where the application should be + installed (optional) + ipAddr - IP address to await a callback ping to let us know that the device has updated + properly - defaults to current IP. + port - port to await a callback ping to let us know that the device has updated properly + defaults to 30000, and counts up from there if it finds a conflict + Returns True if succeeds, False if not + """ + # external function + # returns: + # success: text status from command or callback server + # failure: None + def updateApp(self, appBundlePath, processName=None, destPath=None, ipAddr=None, port=30000): + status = None + cmd = 'updt ' + if (processName == None): + # Then we pass '' for processName + cmd += "'' " + appBundlePath + else: + cmd += processName + ' ' + appBundlePath + + if (destPath): + cmd += " " + destPath + + if (ipAddr is not None): + ip, port = self.getCallbackIpAndPort(ipAddr, port) + cmd += " %s %s" % (ip, port) + # Set up our callback server + callbacksvr = callbackServer(ip, port, self.debug) + + if (self.debug >= 3): print "INFO: updateApp using command: " + str(cmd) + + try: + status = self.verifySendCMD([cmd]) + except(DMError): + return None + + if ipAddr is not None: + status = callbacksvr.disconnect() + + if (self.debug >= 3): print "INFO: updateApp: got status back: " + str(status) + + return status + + """ + return the current time on the device + """ + # external function + # returns: + # success: time in ms + # failure: None + def getCurrentTime(self): + try: + data = self.verifySendCMD(['clok']) + except(DMError): + return None + + return self.stripPrompt(data).strip('\n') + + """ + Connect the ipaddress and port for a callback ping. Defaults to current IP address + And ports starting at 30000. + NOTE: the detection for current IP address only works on Linux! + """ + # external function + # returns: + # success: output of unzip command + # failure: None + def unpackFile(self, filename): + devroot = self.getDeviceRoot() + if (devroot == None): + return None + + dir = '' + parts = filename.split('/') + if (len(parts) > 1): + if self.fileExists(filename): + dir = '/'.join(parts[:-1]) + elif self.fileExists('/' + filename): + dir = '/' + filename + elif self.fileExists(devroot + '/' + filename): + dir = devroot + '/' + filename + else: + return None + + try: + data = self.verifySendCMD(['cd ' + dir, 'unzp ' + filename]) + except(DMError): + return None + + return data + + def getCallbackIpAndPort(self, aIp, aPort): + ip = aIp + nettools = NetworkTools() + if (ip == None): + ip = nettools.getLanIp() + if (aPort != None): + port = nettools.findOpenPort(ip, aPort) + else: + port = nettools.findOpenPort(ip, 30000) + return ip, port + + """ + Returns a properly formatted env string for the agent. + Input - env, which is either None, '', or a dict + Output - a quoted string of the form: '"envvar1=val1,envvar2=val2..."' + If env is None or '' return '' (empty quoted string) + """ + def formatEnvString(self, env): + if (env == None or env == ''): + return '' + + retVal = '"%s"' % ','.join(map(lambda x: '%s=%s' % (x[0], x[1]), env.iteritems())) + if (retVal == '""'): + return '' + + return retVal + + """ + adjust the screen resolution on the device, REBOOT REQUIRED + NOTE: this only works on a tegra ATM + success: True + failure: False + + supported resolutions: 640x480, 800x600, 1024x768, 1152x864, 1200x1024, 1440x900, 1680x1050, 1920x1080 + """ + def adjustResolution(self, width=1680, height=1050, type='hdmi'): + if self.getInfo('os')['os'][0].split()[0] != 'harmony-eng': + if (self.debug >= 2): print "WARNING: unable to adjust screen resolution on non Tegra device" + return False + + results = self.getInfo('screen') + parts = results['screen'][0].split(':') + if (self.debug >= 3): print "INFO: we have a current resolution of %s, %s" % (parts[1].split()[0], parts[2].split()[0]) + + #verify screen type is valid, and set it to the proper value (https://bugzilla.mozilla.org/show_bug.cgi?id=632895#c4) + screentype = -1 + if (type == 'hdmi'): + screentype = 5 + elif (type == 'vga' or type == 'crt'): + screentype = 3 + else: + return False + + #verify we have numbers + if not (isinstance(width, int) and isinstance(height, int)): + return False + + if (width < 100 or width > 9999): + return False + + if (height < 100 or height > 9999): + return False + + if (self.debug >= 3): print "INFO: adjusting screen resolution to %s, %s and rebooting" % (width, height) + try: + self.verifySendCMD(["exec setprop persist.tegra.dpy%s.mode.width %s" % (screentype, width)]) + self.verifySendCMD(["exec setprop persist.tegra.dpy%s.mode.height %s" % (screentype, height)]) + except(DMError): + return False + + return True + +gCallbackData = '' + +class myServer(SocketServer.TCPServer): + allow_reuse_address = True + +class callbackServer(): + def __init__(self, ip, port, debuglevel): + global gCallbackData + if (debuglevel >= 1): print "DEBUG: gCallbackData is: %s on port: %s" % (gCallbackData, port) + gCallbackData = '' + self.ip = ip + self.port = port + self.connected = False + self.debug = debuglevel + if (self.debug >= 3): print "Creating server with " + str(ip) + ":" + str(port) + self.server = myServer((ip, port), self.myhandler) + self.server_thread = Thread(target=self.server.serve_forever) + self.server_thread.setDaemon(True) + self.server_thread.start() + + def disconnect(self, step = 60, timeout = 600): + t = 0 + if (self.debug >= 3): print "Calling disconnect on callback server" + while t < timeout: + if (gCallbackData): + # Got the data back + if (self.debug >= 3): print "Got data back from agent: " + str(gCallbackData) + break + else: + if (self.debug >= 0): print '.', + time.sleep(step) + t += step + + try: + if (self.debug >= 3): print "Shutting down server now" + self.server.shutdown() + except: + if (self.debug >= 1): print "Unable to shutdown callback server - check for a connection on port: " + str(self.port) + + #sleep 1 additional step to ensure not only we are online, but all our services are online + time.sleep(step) + return gCallbackData + + class myhandler(SocketServer.BaseRequestHandler): + def handle(self): + global gCallbackData + gCallbackData = self.request.recv(1024) + #print "Callback Handler got data: " + str(gCallbackData) + self.request.send("OK") +
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozdevice/setup.py @@ -0,0 +1,67 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozdevice. +# +# The Initial Developer of the Original Code is +# The Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2011 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Will Lachance <wlachance@mozilla.com> +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +import os +from setuptools import setup, find_packages + +version = '0.1' + +# 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 = '' + +setup(name='mozdevice', + version=version, + description="Mozilla-authored device management", + 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.com', + url='http://github.com/mozilla/mozbase', + license='MPL', + packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), + include_package_data=True, + zip_safe=False, + install_requires=[], + entry_points=""" + # -*- Entry points: -*- + """, + )
deleted file mode 100644 --- a/testing/mozbase/mozhttpd/mozhttpd.py +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env python - -# ***** BEGIN LICENSE BLOCK ***** -# Version: MPL 1.1/GPL 2.0/LGPL 2.1 -# -# The contents of this file are subject to the Mozilla Public License Version -# 1.1 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# http://www.mozilla.org/MPL/ -# -# Software distributed under the License is distributed on an "AS IS" basis, -# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License -# for the specific language governing rights and limitations under the -# License. -# -# The Original Code is mozilla.org code. -# -# The Initial Developer of the Original Code is -# the Mozilla Foundation. -# Portions created by the Initial Developer are Copyright (C) 2011 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Joel Maher <joel.maher@gmail.com> -# -# Alternatively, the contents of this file may be used under the terms of -# either the GNU General Public License Version 2 or later (the "GPL"), or -# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), -# in which case the provisions of the GPL or the LGPL are applicable instead -# of those above. If you wish to allow use of your version of this file only -# under the terms of either the GPL or the LGPL, and not to allow others to -# use your version of this file under the terms of the MPL, indicate your -# decision by deleting the provisions above and replace them with the notice -# and other provisions required by the GPL or the LGPL. If you do not delete -# the provisions above, a recipient may use your version of this file under -# the terms of any one of the MPL, the GPL or the LGPL. -# -# ***** END LICENSE BLOCK ***** - -import BaseHTTPServer -import SimpleHTTPServer -import threading -import sys -import os -import urllib -import re -from SocketServer import ThreadingMixIn - -class EasyServer(ThreadingMixIn, BaseHTTPServer.HTTPServer): - allow_reuse_address = True - -class MozRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): - docroot = os.getcwd() - - def parse_request(self): - retval = SimpleHTTPServer.SimpleHTTPRequestHandler.parse_request(self) - if '?' in self.path: - # ignore query string, otherwise SimpleHTTPRequestHandler - # will treat it as PATH_INFO for `translate_path` - self.path = self.path.split('?', 1)[0] - return retval - - def translate_path(self, path): - path = path.strip('/').split() - if path == ['']: - path = [] - path.insert(0, self.docroot) - return os.path.join(*path) - - # I found on my local network that calls to this were timing out - # I believe all of these calls are from log_message - def address_string(self): - return "a.b.c.d" - - # This produces a LOT of noise - def log_message(self, format, *args): - pass - -class MozHttpd(object): - - def __init__(self, host="127.0.0.1", port=8888, docroot=os.getcwd()): - self.host = host - self.port = int(port) - self.docroot = docroot - self.httpd = None - - def start(self, block=False): - """ - 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() - """ - - class MozRequestHandlerInstance(MozRequestHandler): - docroot = self.docroot - - self.httpd = EasyServer((self.host, self.port), MozRequestHandlerInstance) - if block: - self.httpd.serve_forever() - else: - self.server = threading.Thread(target=self.httpd.serve_forever) - self.server.setDaemon(True) # don't hang on exit - self.server.start() - - def testServer(self): - fileList = os.listdir(self.docroot) - filehandle = urllib.urlopen('http://%s:%s/?foo=bar&fleem=&foo=fleem' % (self.host, self.port)) - data = filehandle.readlines() - filehandle.close() - - retval = True - - for line in data: - found = False - # '@' denotes a symlink and we need to ignore it. - webline = re.sub('\<[a-zA-Z0-9\-\_\.\=\"\'\/\\\%\!\@\#\$\^\&\*\(\) ]*\>', '', line.strip('\n')).strip('/').strip().strip('@') - if webline != "": - if webline == "Directory listing for": - found = True - else: - for fileName in fileList: - if fileName == webline: - found = True - - if not found: - retval = False - print >> sys.stderr, "NOT FOUND: " + webline.strip() - return retval - - def stop(self): - if self.httpd: - self.httpd.shutdown() - self.httpd = None - - __del__ = stop - - -def main(args=sys.argv[1:]): - - # parse command line options - from optparse import OptionParser - parser = OptionParser() - parser.add_option('-p', '--port', dest='port', - type="int", default=8888, - help="port to run the server on [DEFAULT: %default]") - parser.add_option('-H', '--host', dest='host', - default='127.0.0.1', - help="host [DEFAULT: %default]") - parser.add_option('-d', '--docroot', dest='docroot', - default=os.getcwd(), - help="directory to serve files from [DEFAULT: %default]") - parser.add_option('--test', dest='test', - action='store_true', default=False, - help='run the tests and exit') - options, args = parser.parse_args(args) - if args: - parser.print_help() - parser.exit() - - # create the server - kwargs = options.__dict__.copy() - test = kwargs.pop('test') - server = MozHttpd(**kwargs) - - if test: - server.start() - server.testServer() - else: - server.start(block=True) - -if __name__ == '__main__': - main()
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozhttpd/mozhttpd/__init__.py @@ -0,0 +1,39 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# the Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2011 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# William Lachance <wlachance@mozilla.com> +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +from mozhttpd import MozHttpd, MozRequestHandler +import iface
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozhttpd/mozhttpd/iface.py @@ -0,0 +1,62 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# the Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2011 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Joel Maher <joel.maher@gmail.com> +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +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(): + ip = socket.gethostbyname(socket.gethostname()) + 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
new file mode 100755 --- /dev/null +++ b/testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python + +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# the Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2011 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Joel Maher <joel.maher@gmail.com> +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +import BaseHTTPServer +import SimpleHTTPServer +import threading +import sys +import os +import urllib +import re +from SocketServer import ThreadingMixIn + +class EasyServer(ThreadingMixIn, BaseHTTPServer.HTTPServer): + allow_reuse_address = True + +class MozRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): + docroot = os.getcwd() + + def parse_request(self): + retval = SimpleHTTPServer.SimpleHTTPRequestHandler.parse_request(self) + if '?' in self.path: + # ignore query string, otherwise SimpleHTTPRequestHandler + # will treat it as PATH_INFO for `translate_path` + self.path = self.path.split('?', 1)[0] + return retval + + def translate_path(self, path): + path = path.strip('/').split() + if path == ['']: + path = [] + path.insert(0, self.docroot) + return os.path.join(*path) + + # I found on my local network that calls to this were timing out + # I believe all of these calls are from log_message + def address_string(self): + return "a.b.c.d" + + # This produces a LOT of noise + def log_message(self, format, *args): + pass + +class MozHttpd(object): + + def __init__(self, host="127.0.0.1", port=8888, docroot=os.getcwd(), handler_class=MozRequestHandler): + self.host = host + self.port = int(port) + self.docroot = docroot + self.httpd = None + + class MozRequestHandlerInstance(handler_class): + docroot = self.docroot + + self.handler_class = MozRequestHandlerInstance + + def start(self, block=False): + """ + 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: + self.httpd.serve_forever() + else: + self.server = threading.Thread(target=self.httpd.serve_forever) + self.server.setDaemon(True) # don't hang on exit + self.server.start() + + def testServer(self): + fileList = os.listdir(self.docroot) + filehandle = urllib.urlopen('http://%s:%s/?foo=bar&fleem=&foo=fleem' % (self.host, self.port)) + data = filehandle.readlines() + filehandle.close() + + retval = True + + for line in data: + found = False + # '@' denotes a symlink and we need to ignore it. + webline = re.sub('\<[a-zA-Z0-9\-\_\.\=\"\'\/\\\%\!\@\#\$\^\&\*\(\) ]*\>', '', line.strip('\n')).strip('/').strip().strip('@') + if webline != "": + if webline == "Directory listing for": + found = True + else: + for fileName in fileList: + if fileName == webline: + found = True + + if not found: + retval = False + print >> sys.stderr, "NOT FOUND: " + webline.strip() + return retval + + def stop(self): + if self.httpd: + self.httpd.shutdown() + self.httpd = None + + __del__ = stop + + +def main(args=sys.argv[1:]): + + # parse command line options + from optparse import OptionParser + parser = OptionParser() + parser.add_option('-p', '--port', dest='port', + type="int", default=8888, + help="port to run the server on [DEFAULT: %default]") + parser.add_option('-H', '--host', dest='host', + default='127.0.0.1', + help="host [DEFAULT: %default]") + parser.add_option('-d', '--docroot', dest='docroot', + default=os.getcwd(), + help="directory to serve files from [DEFAULT: %default]") + parser.add_option('--test', dest='test', + action='store_true', default=False, + help='run the tests and exit') + options, args = parser.parse_args(args) + if args: + parser.print_help() + parser.exit() + + # create the server + kwargs = options.__dict__.copy() + test = kwargs.pop('test') + server = MozHttpd(**kwargs) + + if test: + server.start() + server.testServer() + else: + server.start(block=True) + +if __name__ == '__main__': + main()
--- a/testing/mozbase/mozhttpd/setup.py +++ b/testing/mozbase/mozhttpd/setup.py @@ -31,17 +31,17 @@ # decision by deleting the provisions above and replace them with the notice # and other provisions required by the GPL or the LGPL. If you do not delete # the provisions above, a recipient may use your version of this file under # the terms of any one of the MPL, the GPL or the LGPL. # # ***** END LICENSE BLOCK ***** import os -from setuptools import setup +from setuptools import setup, find_packages try: here = os.path.dirname(os.path.abspath(__file__)) description = file(os.path.join(here, 'README.md')).read() except IOError: description = None version = '0.1' @@ -54,17 +54,17 @@ setup(name='mozhttpd', long_description=description, classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers keywords='mozilla', author='Joel Maher', author_email='tools@lists.mozilla.org', url='https://github.com/mozilla/mozbase/tree/master/mozhttpd', license='MPL', py_modules=['mozhttpd'], - packages=[], + packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), include_package_data=True, zip_safe=False, install_requires=deps, entry_points=""" # -*- Entry points: -*- [console_scripts] mozhttpd = mozhttpd:main """,
deleted file mode 100644 --- a/testing/mozbase/mozinfo/mozinfo.py +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/env python - -# ***** BEGIN LICENSE BLOCK ***** -# Version: MPL 1.1/GPL 2.0/LGPL 2.1 -# -# The contents of this file are subject to the Mozilla Public License Version -# 1.1 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# http://www.mozilla.org/MPL/ -# -# Software distributed under the License is distributed on an "AS IS" basis, -# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License -# for the specific language governing rights and limitations under the -# License. -# -# The Original Code is mozinfo. -# -# The Initial Developer of the Original Code is -# The Mozilla Foundation. -# Portions created by the Initial Developer are Copyright (C) 2010 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Jeff Hammel <jhammel@mozilla.com> -# Clint Talbert <ctalbert@mozilla.com> -# -# Alternatively, the contents of this file may be used under the terms of -# either the GNU General Public License Version 2 or later (the "GPL"), or -# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), -# in which case the provisions of the GPL or the LGPL are applicable instead -# of those above. If you wish to allow use of your version of this file only -# under the terms of either the GPL or the LGPL, and not to allow others to -# use your version of this file under the terms of the MPL, indicate your -# decision by deleting the provisions above and replace them with the notice -# and other provisions required by the GPL or the LGPL. If you do not delete -# the provisions above, a recipient may use your version of this file under -# the terms of any one of the MPL, the GPL or the LGPL. -# -# ***** END LICENSE BLOCK ***** - -""" -file for interface to transform introspected system information to a format -pallatable to Mozilla - -Information: -- os : what operating system ['win', 'mac', 'linux', ...] -- bits : 32 or 64 -- processor : processor architecture ['x86', 'x86_64', 'ppc', ...] -- version : operating system version string - -For windows, the service pack information is also included -""" - -# TODO: it might be a good idea of adding a system name (e.g. 'Ubuntu' for -# linux) to the information; I certainly wouldn't want anyone parsing this -# information and having behaviour depend on it - -import os -import platform -import re -import sys - -# keep a copy of the os module since updating globals overrides this -_os = os - -class unknown(object): - """marker class for unknown information""" - def __nonzero__(self): - return False - def __str__(self): - return 'UNKNOWN' -unknown = unknown() # singleton - -# get system information -info = {'os': unknown, - 'processor': unknown, - 'version': unknown, - 'bits': unknown } -(system, node, release, version, machine, processor) = platform.uname() -(bits, linkage) = platform.architecture() - -# get os information and related data -if system in ["Microsoft", "Windows"]: - info['os'] = 'win' - # There is a Python bug on Windows to determine platform values - # http://bugs.python.org/issue7860 - if "PROCESSOR_ARCHITEW6432" in os.environ: - processor = os.environ.get("PROCESSOR_ARCHITEW6432", processor) - else: - processor = os.environ.get('PROCESSOR_ARCHITECTURE', processor) - system = os.environ.get("OS", system).replace('_', ' ') - service_pack = os.sys.getwindowsversion()[4] - info['service_pack'] = service_pack -elif system == "Linux": - (distro, version, codename) = platform.dist() - version = "%s %s" % (distro, version) - if not processor: - processor = machine - info['os'] = 'linux' -elif system == "Darwin": - (release, versioninfo, machine) = platform.mac_ver() - version = "OS X %s" % release - info['os'] = 'mac' -elif sys.platform in ('solaris', 'sunos5'): - info['os'] = 'unix' - version = sys.platform -info['version'] = version # os version - -# processor type and bits -if processor in ["i386", "i686"]: - if bits == "32bit": - processor = "x86" - elif bits == "64bit": - processor = "x86_64" -elif processor == "AMD64": - bits = "64bit" - processor = "x86_64" -elif processor == "Power Macintosh": - processor = "ppc" -bits = re.search('(\d+)bit', bits).group(1) -info.update({'processor': processor, - 'bits': int(bits), - }) - -# standard value of choices, for easy inspection -choices = {'os': ['linux', 'win', 'mac', 'unix'], - 'bits': [32, 64], - 'processor': ['x86', 'x86_64', 'ppc']} - - -def sanitize(info): - """Do some sanitization of input values, primarily - to handle universal Mac builds.""" - if "processor" in info and info["processor"] == "universal-x86-x86_64": - # If we're running on OS X 10.6 or newer, assume 64-bit - if release[:4] >= "10.6": # Note this is a string comparison - info["processor"] = "x86_64" - info["bits"] = 64 - else: - info["processor"] = "x86" - info["bits"] = 32 - -# method for updating information -def update(new_info): - """update the info""" - info.update(new_info) - sanitize(info) - globals().update(info) - - # convenience data for os access - for os_name in choices['os']: - globals()['is' + os_name.title()] = info['os'] == os_name - # unix is special - if isLinux: - globals()['isUnix'] = True - -update({}) - -# exports -__all__ = info.keys() -__all__ += ['is' + os_name.title() for os_name in choices['os']] -__all__ += ['info', 'unknown', 'main', 'choices', 'update'] - - -def main(args=None): - - # parse the command line - from optparse import OptionParser - parser = OptionParser(description=__doc__) - for key in choices: - parser.add_option('--%s' % key, dest=key, - action='store_true', default=False, - help="display choices for %s" % key) - options, args = parser.parse_args() - - # args are JSON blobs to override info - if args: - try: - from json import loads - except ImportError: - try: - from simplejson import loads - except ImportError: - def loads(string): - """*really* simple json; will not work with unicode""" - return eval(string, {'true': True, 'false': False, 'null': None}) - for arg in args: - if _os.path.exists(arg): - string = file(arg).read() - else: - string = arg - update(loads(string)) - - # print out choices if requested - flag = False - for key, value in options.__dict__.items(): - if value is True: - print '%s choices: %s' % (key, ' '.join([str(choice) - for choice in choices[key]])) - flag = True - if flag: return - - # otherwise, print out all info - for key, value in info.items(): - print '%s: %s' % (key, value) - -if __name__ == '__main__': - main()
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozinfo/mozinfo/__init__.py @@ -0,0 +1,39 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozinfo. +# +# The Initial Developer of the Original Code is +# The Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Jeff Hammel <jhammel@mozilla.com> +# Clint Talbert <ctalbert@mozilla.com> +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +from mozinfo import *
new file mode 100755 --- /dev/null +++ b/testing/mozbase/mozinfo/mozinfo/mozinfo.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python + +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozinfo. +# +# The Initial Developer of the Original Code is +# The Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Jeff Hammel <jhammel@mozilla.com> +# Clint Talbert <ctalbert@mozilla.com> +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +""" +file for interface to transform introspected system information to a format +pallatable to Mozilla + +Information: +- os : what operating system ['win', 'mac', 'linux', ...] +- bits : 32 or 64 +- processor : processor architecture ['x86', 'x86_64', 'ppc', ...] +- version : operating system version string + +For windows, the service pack information is also included +""" + +# TODO: it might be a good idea of adding a system name (e.g. 'Ubuntu' for +# linux) to the information; I certainly wouldn't want anyone parsing this +# information and having behaviour depend on it + +import os +import platform +import re +import sys + +# keep a copy of the os module since updating globals overrides this +_os = os + +class unknown(object): + """marker class for unknown information""" + def __nonzero__(self): + return False + def __str__(self): + return 'UNKNOWN' +unknown = unknown() # singleton + +# get system information +info = {'os': unknown, + 'processor': unknown, + 'version': unknown, + 'bits': unknown } +(system, node, release, version, machine, processor) = platform.uname() +(bits, linkage) = platform.architecture() + +# get os information and related data +if system in ["Microsoft", "Windows"]: + info['os'] = 'win' + # There is a Python bug on Windows to determine platform values + # http://bugs.python.org/issue7860 + if "PROCESSOR_ARCHITEW6432" in os.environ: + processor = os.environ.get("PROCESSOR_ARCHITEW6432", processor) + else: + processor = os.environ.get('PROCESSOR_ARCHITECTURE', processor) + system = os.environ.get("OS", system).replace('_', ' ') + service_pack = os.sys.getwindowsversion()[4] + info['service_pack'] = service_pack +elif system == "Linux": + (distro, version, codename) = platform.dist() + version = "%s %s" % (distro, version) + if not processor: + processor = machine + info['os'] = 'linux' +elif system == "Darwin": + (release, versioninfo, machine) = platform.mac_ver() + version = "OS X %s" % release + info['os'] = 'mac' +elif sys.platform in ('solaris', 'sunos5'): + info['os'] = 'unix' + version = sys.platform +info['version'] = version # os version + +# processor type and bits +if processor in ["i386", "i686"]: + if bits == "32bit": + processor = "x86" + elif bits == "64bit": + processor = "x86_64" +elif processor == "AMD64": + bits = "64bit" + processor = "x86_64" +elif processor == "Power Macintosh": + processor = "ppc" +bits = re.search('(\d+)bit', bits).group(1) +info.update({'processor': processor, + 'bits': int(bits), + }) + +# standard value of choices, for easy inspection +choices = {'os': ['linux', 'win', 'mac', 'unix'], + 'bits': [32, 64], + 'processor': ['x86', 'x86_64', 'ppc']} + + +def sanitize(info): + """Do some sanitization of input values, primarily + to handle universal Mac builds.""" + if "processor" in info and info["processor"] == "universal-x86-x86_64": + # If we're running on OS X 10.6 or newer, assume 64-bit + if release[:4] >= "10.6": # Note this is a string comparison + info["processor"] = "x86_64" + info["bits"] = 64 + else: + info["processor"] = "x86" + info["bits"] = 32 + +# method for updating information +def update(new_info): + """update the info""" + info.update(new_info) + sanitize(info) + globals().update(info) + + # convenience data for os access + for os_name in choices['os']: + globals()['is' + os_name.title()] = info['os'] == os_name + # unix is special + if isLinux: + globals()['isUnix'] = True + +update({}) + +# exports +__all__ = info.keys() +__all__ += ['is' + os_name.title() for os_name in choices['os']] +__all__ += ['info', 'unknown', 'main', 'choices', 'update'] + + +def main(args=None): + + # parse the command line + from optparse import OptionParser + parser = OptionParser(description=__doc__) + for key in choices: + parser.add_option('--%s' % key, dest=key, + action='store_true', default=False, + help="display choices for %s" % key) + options, args = parser.parse_args() + + # args are JSON blobs to override info + if args: + try: + from json import loads + except ImportError: + try: + from simplejson import loads + except ImportError: + def loads(string): + """*really* simple json; will not work with unicode""" + return eval(string, {'true': True, 'false': False, 'null': None}) + for arg in args: + if _os.path.exists(arg): + string = file(arg).read() + else: + string = arg + update(loads(string)) + + # print out choices if requested + flag = False + for key, value in options.__dict__.items(): + if value is True: + print '%s choices: %s' % (key, ' '.join([str(choice) + for choice in choices[key]])) + flag = True + if flag: return + + # otherwise, print out all info + for key, value in info.items(): + print '%s: %s' % (key, value) + +if __name__ == '__main__': + main()
--- a/testing/mozbase/mozinfo/setup.py +++ b/testing/mozbase/mozinfo/setup.py @@ -32,17 +32,17 @@ # and other provisions required by the GPL or the LGPL. If you do not delete # the provisions above, a recipient may use your version of this file under # the terms of any one of the MPL, the GPL or the LGPL. # # ***** END LICENSE BLOCK ***** import os -from setuptools import setup +from setuptools import setup, find_packages version = '0.3.3' # 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): @@ -60,18 +60,17 @@ setup(name='mozinfo', description="file for interface to transform introspected system information to a format pallatable to Mozilla", long_description=description, classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers keywords='mozilla', author='Jeff Hammel', author_email='jhammel@mozilla.com', url='https://wiki.mozilla.org/Auto-tools', license='MPL', - py_modules=['mozinfo'], - packages=[], + packages=find_packages(exclude=['legacy']), include_package_data=True, zip_safe=False, install_requires=deps, entry_points=""" # -*- Entry points: -*- [console_scripts] mozinfo = mozinfo:main """,
deleted file mode 100644 --- a/testing/mozbase/mozinstall/mozinstall.py +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/env python -# ***** BEGIN LICENSE BLOCK ***** -# Version: MPL 1.1/GPL 2.0/LGPL 2.1 -# -# The contents of this file are subject to the Mozilla Public License Version -# 1.1 (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# http://www.mozilla.org/MPL/ -# -# Software distributed under the License is distributed on an "AS IS" basis, -# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License -# for the specific language governing rights and limitations under the -# License. -# -# The Original Code is mozinstall. -# -# The Initial Developer of the Original Code is -# The Mozilla Foundation. -# Portions created by the Initial Developer are Copyright (C) 2011 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Clint Talbert <ctalbert@mozilla.com> -# Andrew Halberstadt <halbersa@gmail.com> -# -# Alternatively, the contents of this file may be used under the terms of -# either the GNU General Public License Version 2 or later (the "GPL"), or -# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), -# in which case the provisions of the GPL or the LGPL are applicable instead -# of those above. If you wish to allow use of your version of this file only -# under the terms of either the GPL or the LGPL, and not to allow others to -# use your version of this file under the terms of the MPL, indicate your -# decision by deleting the provisions above and replace them with the notice -# and other provisions required by the GPL or the LGPL. If you do not delete -# the provisions above, a recipient may use your version of this file under -# the terms of any one of the MPL, the GPL or the LGPL. -# -# ***** END LICENSE BLOCK ***** - -from optparse import OptionParser -import mozinfo -import subprocess -import zipfile -import tarfile -import sys -import os - -_default_apps = ["firefox", - "thunderbird", - "fennec"] - -def install(src, dest=None, apps=_default_apps): - """ - Installs a zip, exe, tar.gz, tar.bz2 or dmg file - src - the path to the install file - dest - the path to install to [default is os.path.dirname(src)] - returns - the full path to the binary in the installed folder - or None if the binary cannot be found - """ - src = os.path.realpath(src) - assert(os.path.isfile(src)) - if not dest: - dest = os.path.dirname(src) - - trbk = None - try: - install_dir = None - if zipfile.is_zipfile(src) or tarfile.is_tarfile(src): - install_dir = _extract(src, dest)[0] - elif mozinfo.isMac and src.lower().endswith(".dmg"): - install_dir = _install_dmg(src, dest) - elif mozinfo.isWin and os.access(src, os.X_OK): - install_dir = _install_exe(src, dest) - else: - raise InvalidSource(src + " is not a recognized file type " + - "(zip, exe, tar.gz, tar.bz2 or dmg)") - except InvalidSource, e: - raise - except Exception, e: - cls, exc, trbk = sys.exc_info() - install_error = InstallError("Failed to install %s" % src) - raise install_error.__class__, install_error, trbk - finally: - # trbk won't get GC'ed due to circular reference - # http://docs.python.org/library/sys.html#sys.exc_info - del trbk - - if install_dir: - return get_binary(install_dir, apps=apps) - -def get_binary(path, apps=_default_apps): - """ - Finds the binary in the specified path - path - the path within which to search for the binary - returns - the full path to the binary in the folder - or None if the binary cannot be found - """ - if mozinfo.isWin: - apps = [app + ".exe" for app in apps] - for root, dirs, files in os.walk(path): - for filename in files: - # os.access evaluates to False for some reason, so not using it - if filename in apps: - return os.path.realpath(os.path.join(root, filename)) - -def _extract(path, extdir=None, delete=False): - """ - Takes in a tar or zip file and extracts it to extdir - If extdir is not specified, extracts to os.path.dirname(path) - If delete is set to True, deletes the bundle at path - Returns the list of top level files that were extracted - """ - if zipfile.is_zipfile(path): - bundle = zipfile.ZipFile(path) - namelist = bundle.namelist() - elif tarfile.is_tarfile(path): - bundle = tarfile.open(path) - namelist = bundle.getnames() - else: - return - if extdir is None: - extdir = os.path.dirname(path) - elif not os.path.exists(extdir): - os.makedirs(extdir) - bundle.extractall(path=extdir) - bundle.close() - if delete: - os.remove(path) - # namelist returns paths with forward slashes even in windows - top_level_files = [os.path.join(extdir, name) for name in namelist - if len(name.rstrip('/').split('/')) == 1] - # namelist doesn't include folders in windows, append these to the list - if mozinfo.isWin: - for name in namelist: - root = name[:name.find('/')] - if root not in top_level_files: - top_level_files.append(root) - return top_level_files - -def _install_dmg(src, dest): - proc = subprocess.Popen("hdiutil attach " + src, - shell=True, - stdout=subprocess.PIPE) - try: - for data in proc.communicate()[0].split(): - if data.find("/Volumes/") != -1: - appDir = data - break - for appFile in os.listdir(appDir): - if appFile.endswith(".app"): - appName = appFile - break - subprocess.call("cp -r " + os.path.join(appDir, appName) + " " + dest, - shell=True) - finally: - subprocess.call("hdiutil detach " + appDir + " -quiet", - shell=True) - return os.path.join(dest, appName) - -def _install_exe(src, dest): - # possibly gets around UAC in vista (still need to run as administrator) - os.environ['__compat_layer'] = "RunAsInvoker" - cmd = [src, "/S", "/D=" + os.path.realpath(dest)] - subprocess.call(cmd) - return dest - -def cli(argv=sys.argv[1:]): - parser = OptionParser() - parser.add_option("-s", "--source", - dest="src", - help="Path to installation file. " - "Accepts: zip, exe, tar.bz2, tar.gz, and dmg") - parser.add_option("-d", "--destination", - dest="dest", - default=None, - help="[optional] Directory to install application into") - parser.add_option("--app", dest="app", - action="append", - default=_default_apps, - help="[optional] Application being installed. " - "Should be lowercase, e.g: " - "firefox, fennec, thunderbird, etc.") - - (options, args) = parser.parse_args(argv) - if not options.src or not os.path.exists(options.src): - print "Error: must specify valid source" - return 2 - - # Run it - if os.path.isdir(options.src): - binary = get_binary(options.src, apps=options.app) - else: - binary = install(options.src, dest=options.dest, apps=options.app) - print binary - -class InvalidSource(Exception): - """ - Thrown when the specified source is not a recognized - file type (zip, exe, tar.gz, tar.bz2 or dmg) - """ - -class InstallError(Exception): - """ - Thrown when the installation fails. Includes traceback - if available. - """ - -if __name__ == "__main__": - sys.exit(cli())
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozinstall/mozinstall/__init__.py @@ -0,0 +1,38 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozinstall. +# +# The Initial Developer of the Original Code is +# The Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2011 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Clint Talbert <ctalbert@mozilla.com> +# Andrew Halberstadt <halbersa@gmail.com> +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** +from mozinstall import *
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozinstall/mozinstall/mozinstall.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozinstall. +# +# The Initial Developer of the Original Code is +# The Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2011 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Clint Talbert <ctalbert@mozilla.com> +# Andrew Halberstadt <halbersa@gmail.com> +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +from optparse import OptionParser +import mozinfo +import subprocess +import zipfile +import tarfile +import sys +import os + +_default_apps = ["firefox", + "thunderbird", + "fennec"] + +def install(src, dest=None, apps=_default_apps): + """ + Installs a zip, exe, tar.gz, tar.bz2 or dmg file + src - the path to the install file + dest - the path to install to [default is os.path.dirname(src)] + returns - the full path to the binary in the installed folder + or None if the binary cannot be found + """ + src = os.path.realpath(src) + assert(os.path.isfile(src)) + if not dest: + dest = os.path.dirname(src) + + trbk = None + try: + install_dir = None + if zipfile.is_zipfile(src) or tarfile.is_tarfile(src): + install_dir = _extract(src, dest)[0] + elif mozinfo.isMac and src.lower().endswith(".dmg"): + install_dir = _install_dmg(src, dest) + elif mozinfo.isWin and os.access(src, os.X_OK): + install_dir = _install_exe(src, dest) + else: + raise InvalidSource(src + " is not a recognized file type " + + "(zip, exe, tar.gz, tar.bz2 or dmg)") + except InvalidSource, e: + raise + except Exception, e: + cls, exc, trbk = sys.exc_info() + install_error = InstallError("Failed to install %s" % src) + raise install_error.__class__, install_error, trbk + finally: + # trbk won't get GC'ed due to circular reference + # http://docs.python.org/library/sys.html#sys.exc_info + del trbk + + if install_dir: + return get_binary(install_dir, apps=apps) + +def get_binary(path, apps=_default_apps): + """ + Finds the binary in the specified path + path - the path within which to search for the binary + returns - the full path to the binary in the folder + or None if the binary cannot be found + """ + if mozinfo.isWin: + apps = [app + ".exe" for app in apps] + for root, dirs, files in os.walk(path): + for filename in files: + # os.access evaluates to False for some reason, so not using it + if filename in apps: + return os.path.realpath(os.path.join(root, filename)) + +def _extract(path, extdir=None, delete=False): + """ + Takes in a tar or zip file and extracts it to extdir + If extdir is not specified, extracts to os.path.dirname(path) + If delete is set to True, deletes the bundle at path + Returns the list of top level files that were extracted + """ + if zipfile.is_zipfile(path): + bundle = zipfile.ZipFile(path) + namelist = bundle.namelist() + elif tarfile.is_tarfile(path): + bundle = tarfile.open(path) + namelist = bundle.getnames() + else: + return + if extdir is None: + extdir = os.path.dirname(path) + elif not os.path.exists(extdir): + os.makedirs(extdir) + bundle.extractall(path=extdir) + bundle.close() + if delete: + os.remove(path) + # namelist returns paths with forward slashes even in windows + top_level_files = [os.path.join(extdir, name) for name in namelist + if len(name.rstrip('/').split('/')) == 1] + # namelist doesn't include folders in windows, append these to the list + if mozinfo.isWin: + for name in namelist: + root = name[:name.find('/')] + if root not in top_level_files: + top_level_files.append(root) + return top_level_files + +def _install_dmg(src, dest): + proc = subprocess.Popen("hdiutil attach " + src, + shell=True, + stdout=subprocess.PIPE) + try: + for data in proc.communicate()[0].split(): + if data.find("/Volumes/") != -1: + appDir = data + break + for appFile in os.listdir(appDir): + if appFile.endswith(".app"): + appName = appFile + break + subprocess.call("cp -r " + os.path.join(appDir, appName) + " " + dest, + shell=True) + finally: + subprocess.call("hdiutil detach " + appDir + " -quiet", + shell=True) + return os.path.join(dest, appName) + +def _install_exe(src, dest): + # possibly gets around UAC in vista (still need to run as administrator) + os.environ['__compat_layer'] = "RunAsInvoker" + cmd = [src, "/S", "/D=" + os.path.realpath(dest)] + subprocess.call(cmd) + return dest + +def cli(argv=sys.argv[1:]): + parser = OptionParser() + parser.add_option("-s", "--source", + dest="src", + help="Path to installation file. " + "Accepts: zip, exe, tar.bz2, tar.gz, and dmg") + parser.add_option("-d", "--destination", + dest="dest", + default=None, + help="[optional] Directory to install application into") + parser.add_option("--app", dest="app", + action="append", + default=_default_apps, + help="[optional] Application being installed. " + "Should be lowercase, e.g: " + "firefox, fennec, thunderbird, etc.") + + (options, args) = parser.parse_args(argv) + if not options.src or not os.path.exists(options.src): + print "Error: must specify valid source" + return 2 + + # Run it + if os.path.isdir(options.src): + binary = get_binary(options.src, apps=options.app) + else: + binary = install(options.src, dest=options.dest, apps=options.app) + print binary + +class InvalidSource(Exception): + """ + Thrown when the specified source is not a recognized + file type (zip, exe, tar.gz, tar.bz2 or dmg) + """ + +class InstallError(Exception): + """ + Thrown when the installation fails. Includes traceback + if available. + """ + +if __name__ == "__main__": + sys.exit(cli())
--- a/testing/mozbase/mozinstall/setup.py +++ b/testing/mozbase/mozinstall/setup.py @@ -32,17 +32,17 @@ # decision by deleting the provisions above and replace them with the notice # and other provisions required by the GPL or the LGPL. If you do not delete # the provisions above, a recipient may use your version of this file under # the terms of any one of the MPL, the GPL or the LGPL. # # ***** END LICENSE BLOCK ***** import os -from setuptools import setup +from setuptools import setup, find_packages try: here = os.path.dirname(os.path.abspath(__file__)) description = file(os.path.join(here, 'README.md')).read() except IOError: description = None version = '0.3' @@ -61,18 +61,17 @@ setup(name='mozInstall', 'Programming Language :: Python', 'Topic :: Software Development :: Libraries :: Python Modules', ], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers keywords='mozilla', author='mdas', author_email='mdas@mozilla.com', url='https://github.com/mozilla/mozbase', license='MPL', - py_modules=['mozinstall'], - packages=[], + packages=find_packages(exclude=['legacy']), include_package_data=True, zip_safe=False, install_requires=deps, entry_points=""" # -*- Entry points: -*- [console_scripts] mozinstall = mozinstall:cli """,
--- a/testing/mozbase/mozprofile/setup.py +++ b/testing/mozbase/mozprofile/setup.py @@ -37,22 +37,22 @@ # the terms of any one of the MPL, the GPL or the LGPL. # # ***** END LICENSE BLOCK ***** import os import sys from setuptools import setup, find_packages -version = '0.1b2' +version = '0.1' # we only support python 2 right now assert sys.version_info[0] == 2 -deps = ["ManifestDestiny == 0.5.4"] +deps = ["ManifestDestiny >= 0.5.4"] # version-dependent dependencies try: import json except ImportError: deps.append('simplejson') # take description from README here = os.path.dirname(os.path.abspath(__file__))
--- a/testing/mozbase/mozrunner/mozrunner/utils.py +++ b/testing/mozbase/mozrunner/mozrunner/utils.py @@ -51,17 +51,20 @@ import os import sys ### python package method metadata by introspection try: import pkg_resources def get_metadata_from_egg(module): ret = {} - dist = pkg_resources.get_distribution(module) + try: + dist = pkg_resources.get_distribution(module) + except pkg_resources.DistributionNotFound: + return {} if dist.has_metadata("PKG-INFO"): key = None for line in dist.get_metadata("PKG-INFO").splitlines(): # see http://www.python.org/dev/peps/pep-0314/ if key == 'Description': # descriptions can be long if not line or line[0].isspace(): value += '\n' + line
--- a/testing/mozbase/mozrunner/setup.py +++ b/testing/mozbase/mozrunner/setup.py @@ -38,27 +38,30 @@ # # ***** END LICENSE BLOCK ***** import os import sys from setuptools import setup, find_packages PACKAGE_NAME = "mozrunner" -PACKAGE_VERSION = "4.0" +PACKAGE_VERSION = "4.1" desc = """Reliable start/stop/configuration of Mozilla Applications (Firefox, Thunderbird, etc.)""" # 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 = ['mozprocess', 'mozprofile', 'mozinfo'] +deps = ['mozinfo', + 'mozprocess', + 'mozprofile >= 0.1', + ] # we only support python 2 right now assert sys.version_info[0] == 2 setup(name=PACKAGE_NAME, version=PACKAGE_VERSION, description=desc, long_description=description,
--- a/testing/peptest/Makefile.in +++ b/testing/peptest/Makefile.in @@ -47,22 +47,31 @@ MODULE = testing_peptest include $(topsrcdir)/config/rules.mk PEPTEST_HARNESS = \ peptest \ $(NULL) PEPTEST_EXTRAS = \ setup.py \ + runtests.py \ MANIFEST.in \ README.md \ $(NULL) PEPTEST_TESTS = \ tests \ $(NULL) +_DEST_DIR = $(DEPTH)/_tests/peptest +libs:: $(PEPTEST_HARNESS) + $(PYTHON) $(topsrcdir)/config/nsinstall.py $^ $(_DEST_DIR) +libs:: $(PEPTEST_EXTRAS) + $(PYTHON) $(topsrcdir)/config/nsinstall.py $^ $(_DEST_DIR) +libs:: $(PEPTEST_TESTS) + $(PYTHON) $(topsrcdir)/config/nsinstall.py $^ $(_DEST_DIR) + stage-package: PKG_STAGE = $(DIST)/test-package-stage stage-package: $(NSINSTALL) -D $(PKG_STAGE)/peptest @(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(PEPTEST_HARNESS)) | (cd $(PKG_STAGE)/peptest && tar -xf -) @(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(PEPTEST_EXTRAS)) | (cd $(PKG_STAGE)/peptest && tar -xf -) @(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(PEPTEST_TESTS)) | (cd $(PKG_STAGE)/peptest && tar -xf -)
new file mode 100644 --- /dev/null +++ b/testing/peptest/runtests.py @@ -0,0 +1,61 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is peptest. +# +# The Initial Developer of the Original Code is +# The Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2011 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Andrew Halberstadt <halbersa@gmail.com> +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** +""" +Adds peptest's dependencies to sys.path then runs the tests +""" +import os +import sys + +deps = ['manifestdestiny', + 'mozinfo', + 'mozhttpd', + 'mozlog', + 'mozprofile', + 'mozprocess', + 'mozrunner', + ] + +here = os.path.dirname(__file__) +mozbase = os.path.realpath(os.path.join(here, '..', 'mozbase')) + +for dep in deps: + module = os.path.join(mozbase, dep) + if module not in sys.path: + sys.path.insert(0, module) + +from peptest import runpeptests +runpeptests.main(sys.argv[1:])
--- a/testing/testsuite-targets.mk +++ b/testing/testsuite-targets.mk @@ -36,18 +36,20 @@ # # ***** END LICENSE BLOCK ***** # Shortcut for mochitest* and xpcshell-tests targets, # replaces 'EXTRA_TEST_ARGS=--test-path=...'. ifdef TEST_PATH TEST_PATH_ARG := --test-path=$(TEST_PATH) +PEPTEST_PATH_ARG := --test-path=$(TEST_PATH) else TEST_PATH_ARG := +PEPTEST_PATH_ARG := --test-path=_tests/peptest/tests/firefox/firefox_all.ini endif # include automation-build.mk to get the path to the binary TARGET_DEPTH = $(DEPTH) include $(topsrcdir)/build/binary-location.mk SYMBOLS_PATH := --symbols-path=$(DIST)/crashreporter-symbols @@ -223,16 +225,26 @@ REMOTE_XPCSHELL = \ xpcshell-tests-remote: DM_TRANS?=adb xpcshell-tests-remote: @if [ "${TEST_DEVICE}" != "" -o "$(DM_TRANS)" = "adb" ]; \ then $(call REMOTE_XPCSHELL); $(CHECK_TEST_ERROR); \ else \ echo "please prepare your host with environment variables for TEST_DEVICE"; \ fi +# Runs peptest, for usage see: https://developer.mozilla.org/en/Peptest#Running_Tests +RUN_PEPTEST = \ + rm -f ./$@.log && \ + $(PYTHON) _tests/peptest/runtests.py --binary=$(browser_path) $(PEPTEST_PATH_ARG) \ + --log-file=./$@.log $(SYMBOLS_PATH) $(EXTRA_TEST_ARGS) + +peptest: + $(RUN_PEPTEST) + $(CHECK_TEST_ERROR) + # Package up the tests and test harnesses include $(topsrcdir)/toolkit/mozapps/installer/package-name.mk ifndef UNIVERSAL_BINARY PKG_STAGE = $(DIST)/test-package-stage package-tests: stage-mochitest stage-reftest stage-xpcshell stage-jstests stage-jetpack stage-firebug stage-peptest stage-mozbase else # This staging area has been built for us by universal/flight.mk @@ -286,9 +298,10 @@ stage-peptest: make-stage-dir stage-mozbase: make-stage-dir $(MAKE) -C $(DEPTH)/testing/mozbase stage-package .PHONY: \ mochitest mochitest-plain mochitest-chrome mochitest-a11y mochitest-ipcplugins \ reftest crashtest \ xpcshell-tests \ jstestbrowser \ + peptest \ package-tests make-stage-dir stage-mochitest stage-reftest stage-xpcshell stage-jstests stage-android stage-jetpack stage-firebug stage-peptest stage-mozbase