diff options
Diffstat (limited to 'addon-sdk/source/python-lib')
156 files changed, 9738 insertions, 0 deletions
diff --git a/addon-sdk/source/python-lib/cuddlefish/__init__.py b/addon-sdk/source/python-lib/cuddlefish/__init__.py new file mode 100644 index 000000000..365d96c5e --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/__init__.py @@ -0,0 +1,959 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import sys +import os +import optparse +import time + +from copy import copy +import simplejson as json +from cuddlefish import packaging +from cuddlefish._version import get_versions + +MOZRUNNER_BIN_NOT_FOUND = 'Mozrunner could not locate your binary' +MOZRUNNER_BIN_NOT_FOUND_HELP = """ +I can't find the application binary in any of its default locations +on your system. Please specify one using the -b/--binary option. +""" + +UPDATE_RDF_FILENAME = "%s.update.rdf" +XPI_FILENAME = "%s.xpi" + +usage = """ +%prog [options] command [command-specific options] + +Supported Commands: + init - create a sample addon in an empty directory + test - run tests + run - run program + xpi - generate an xpi + +Internal Commands: + testcfx - test the cfx tool + testex - test all example code + testpkgs - test all installed packages + testall - test whole environment + +Experimental and internal commands and options are not supported and may be +changed or removed in the future. +""" + +global_options = [ + (("-v", "--verbose",), dict(dest="verbose", + help="enable lots of output", + action="store_true", + default=False)), + ] + +parser_groups = ( + ("Supported Command-Specific Options", [ + (("", "--update-url",), dict(dest="update_url", + help="update URL in install.rdf", + metavar=None, + default=None, + cmds=['xpi'])), + (("", "--update-link",), dict(dest="update_link", + help="generate update.rdf", + metavar=None, + default=None, + cmds=['xpi'])), + (("-p", "--profiledir",), dict(dest="profiledir", + help=("profile directory to pass to " + "app"), + metavar=None, + default=None, + cmds=['test', 'run', 'testex', + 'testpkgs', 'testall'])), + (("-b", "--binary",), dict(dest="binary", + help="path to app binary", + metavar=None, + default=None, + cmds=['test', 'run', 'testex', 'testpkgs', + 'testall'])), + (("", "--binary-args",), dict(dest="cmdargs", + help=("additional arguments passed to the " + "binary"), + metavar=None, + default=None, + cmds=['run', 'test'])), + (("", "--dependencies",), dict(dest="dep_tests", + help="include tests for all deps", + action="store_true", + default=False, + cmds=['test', 'testex', 'testpkgs', + 'testall'])), + (("", "--times",), dict(dest="iterations", + type="int", + help="number of times to run tests", + default=1, + cmds=['test', 'testex', 'testpkgs', + 'testall'])), + (("-f", "--filter",), dict(dest="filter", + help=("only run tests whose filenames " + "match FILENAME and optionally " + "match TESTNAME, both regexps"), + metavar="FILENAME[:TESTNAME]", + default='', + cmds=['test', 'testex', 'testaddons', 'testpkgs', + 'testall'])), + (("-g", "--use-config",), dict(dest="config", + help="use named config from local.json", + metavar=None, + default="default", + cmds=['test', 'run', 'xpi', 'testex', + 'testpkgs', 'testall'])), + (("", "--templatedir",), dict(dest="templatedir", + help="XULRunner app/ext. template", + metavar=None, + default=None, + cmds=['run', 'xpi'])), + (("", "--package-path",), dict(dest="packagepath", action="append", + help="extra directories for package search", + metavar=None, + default=[], + cmds=['run', 'xpi', 'test'])), + (("", "--extra-packages",), dict(dest="extra_packages", + help=("extra packages to include, " + "comma-separated. Default is " + "'addon-sdk'."), + metavar=None, + default="addon-sdk", + cmds=['run', 'xpi', 'test', 'testex', + 'testpkgs', 'testall', + 'testcfx'])), + (("", "--pkgdir",), dict(dest="pkgdir", + help=("package dir containing " + "package.json; default is " + "current directory"), + metavar=None, + default=None, + cmds=['run', 'xpi', 'test'])), + (("", "--static-args",), dict(dest="static_args", + help="extra harness options as JSON", + type="json", + metavar=None, + default="{}", + cmds=['run', 'xpi'])), + (("", "--parseable",), dict(dest="parseable", + help="display test output in a parseable format", + action="store_true", + default=False, + cmds=['run', 'test', 'testex', 'testpkgs', + 'testaddons', 'testall'])), + ] + ), + + ("Experimental Command-Specific Options", [ + (("-a", "--app",), dict(dest="app", + help=("app to run: firefox (default), fennec, " + "fennec-on-device, xulrunner or " + "thunderbird"), + metavar=None, + type="choice", + choices=["firefox", + "fennec-on-device", "thunderbird", + "xulrunner"], + default="firefox", + cmds=['test', 'run', 'testex', 'testpkgs', + 'testall'])), + (("-o", "--overload-modules",), dict(dest="overload_modules", + help=("Overload JS modules integrated into" + " Firefox with the one from your SDK" + " repository"), + action="store_true", + default=False, + cmds=['run', 'test', 'testex', 'testpkgs', + 'testall'])), + (("", "--strip-sdk",), dict(dest="bundle_sdk", + help=("Do not ship SDK modules in the xpi"), + action="store_false", + default=False, + cmds=['run', 'test', 'testex', 'testpkgs', + 'testall', 'xpi'])), + (("", "--force-use-bundled-sdk",), dict(dest="force_use_bundled_sdk", + help=("When --strip-sdk isn't passed, " + "force using sdk modules shipped in " + "the xpi instead of firefox ones"), + action="store_true", + default=False, + cmds=['run', 'test', 'testex', 'testpkgs', + 'testall', 'xpi'])), + (("", "--no-run",), dict(dest="no_run", + help=("Instead of launching the " + "application, just show the command " + "for doing so. Use this to launch " + "the application in a debugger like " + "gdb."), + action="store_true", + default=False, + cmds=['run', 'test'])), + (("", "--no-quit",), dict(dest="no_quit", + help=("Prevent from killing Firefox when" + "running tests"), + action="store_true", + default=False, + cmds=['run', 'test'])), + (("", "--no-strip-xpi",), dict(dest="no_strip_xpi", + help="retain unused modules in XPI", + action="store_true", + default=False, + cmds=['xpi'])), + (("", "--force-mobile",), dict(dest="enable_mobile", + help="Force compatibility with Firefox Mobile", + action="store_true", + default=False, + cmds=['run', 'test', 'xpi', 'testall'])), + (("", "--mobile-app",), dict(dest="mobile_app_name", + help=("Name of your Android application to " + "use. Possible values: 'firefox', " + "'firefox_beta', 'fennec_aurora', " + "'fennec' (for nightly)."), + metavar=None, + default=None, + cmds=['run', 'test', 'testall'])), + (("", "--harness-option",), dict(dest="extra_harness_option_args", + help=("Extra properties added to " + "harness-options.json"), + action="append", + metavar="KEY=VALUE", + default=[], + cmds=['xpi'])), + (("", "--stop-on-error",), dict(dest="stopOnError", + help="Stop running tests after the first failure", + action="store_true", + metavar=None, + default=False, + cmds=['test', 'testex', 'testpkgs'])), + (("", "--check-memory",), dict(dest="check_memory", + help="attempts to detect leaked compartments after a test run", + action="store_true", + default=False, + cmds=['test', 'testpkgs', 'testaddons', + 'testall'])), + (("", "--output-file",), dict(dest="output_file", + help="Where to put the finished .xpi", + default=None, + cmds=['xpi'])), + (("", "--abort-on-missing-module",), dict(dest="abort_on_missing", + help="Abort if required module is missing", + action="store_true", + default=False, + cmds=['test', 'run', 'xpi', 'testpkgs'])), + (("", "--no-connections",), dict(dest="no_connections", + help="disable/enable remote connections (on for cfx run only by default)", + type="choice", + choices=["on", "off", "default"], + default="default", + cmds=['test', 'run', 'testpkgs', + 'testall', 'testaddons', 'testex'])), + ] + ), + + ("Internal Command-Specific Options", [ + (("", "--addons",), dict(dest="addons", + help=("paths of addons to install, " + "comma-separated"), + metavar=None, + default=None, + cmds=['test', 'run', 'testex', 'testpkgs', + 'testall'])), + (("", "--test-runner-pkg",), dict(dest="test_runner_pkg", + help=("name of package " + "containing test runner " + "program (default is " + "test-harness)"), + default="addon-sdk", + cmds=['test', 'testex', 'testpkgs', + 'testall'])), + # --keydir was removed in 1.0b5, but we keep it around in the options + # parser to make life easier for frontends like FlightDeck which + # might still pass it. It can go away once the frontends are updated. + (("", "--keydir",), dict(dest="keydir", + help=("obsolete, ignored"), + metavar=None, + default=None, + cmds=['test', 'run', 'xpi', 'testex', + 'testpkgs', 'testall'])), + (("", "--e10s",), dict(dest="enable_e10s", + help="enable remote windows", + action="store_true", + default=False, + cmds=['test', 'run', 'testex', 'testpkgs', + 'testaddons', 'testcfx', 'testall'])), + (("", "--logfile",), dict(dest="logfile", + help="log console output to file", + metavar=None, + default=None, + cmds=['run', 'test', 'testex', 'testpkgs'])), + # TODO: This should default to true once our memory debugging + # issues are resolved; see bug 592774. + (("", "--profile-memory",), dict(dest="profileMemory", + help=("profile memory usage " + "(default is false)"), + type="int", + action="store", + default=0, + cmds=['test', 'testex', 'testpkgs', + 'testall'])), + ] + ), + ) + +def find_parent_package(cur_dir): + tail = True + while tail: + if os.path.exists(os.path.join(cur_dir, 'package.json')): + return cur_dir + cur_dir, tail = os.path.split(cur_dir) + return None + +def check_json(option, opt, value): + # We return the parsed JSON here; see bug 610816 for background on why. + try: + return json.loads(value) + except ValueError: + raise optparse.OptionValueError("Option %s must be JSON." % opt) + +class CfxOption(optparse.Option): + TYPES = optparse.Option.TYPES + ('json',) + TYPE_CHECKER = copy(optparse.Option.TYPE_CHECKER) + TYPE_CHECKER['json'] = check_json + +def parse_args(arguments, global_options, usage, version, parser_groups, + defaults=None): + parser = optparse.OptionParser(usage=usage.strip(), option_class=CfxOption, + version=version) + + def name_cmp(a, b): + # a[0] = name sequence + # a[0][0] = short name (possibly empty string) + # a[0][1] = long name + names = [] + for seq in (a, b): + names.append(seq[0][0][1:] if seq[0][0] else seq[0][1][2:]) + return cmp(*names) + + global_options.sort(name_cmp) + for names, opts in global_options: + parser.add_option(*names, **opts) + + for group_name, options in parser_groups: + group = optparse.OptionGroup(parser, group_name) + options.sort(name_cmp) + for names, opts in options: + if 'cmds' in opts: + cmds = opts['cmds'] + del opts['cmds'] + cmds.sort() + if not 'help' in opts: + opts['help'] = "" + opts['help'] += " (%s)" % ", ".join(cmds) + group.add_option(*names, **opts) + parser.add_option_group(group) + + if defaults: + parser.set_defaults(**defaults) + + (options, args) = parser.parse_args(args=arguments) + + if not args: + parser.print_help() + parser.exit() + + return (options, args) + +# all tests emit progress messages to stderr, not stdout. (the mozrunner +# console output goes to stderr and is hard to change, and +# unittest.TextTestRunner prefers stderr, so we send everything else there +# too, to keep all the messages in order) + +def test_all(env_root, defaults): + fail = False + + starttime = time.time() + + if not defaults['filter']: + print >>sys.stderr, "Testing cfx..." + sys.stderr.flush() + result = test_cfx(env_root, defaults['verbose']) + if result.failures or result.errors: + fail = True + + if not fail or not defaults.get("stopOnError"): + print >>sys.stderr, "Testing all examples..." + sys.stderr.flush() + + try: + test_all_examples(env_root, defaults) + except SystemExit, e: + fail = (e.code != 0) or fail + + if not fail or not defaults.get("stopOnError"): + print >>sys.stderr, "Testing all unit-test addons..." + sys.stderr.flush() + + try: + test_all_testaddons(env_root, defaults) + except SystemExit, e: + fail = (e.code != 0) or fail + + if not fail or not defaults.get("stopOnError"): + print >>sys.stderr, "Testing all packages..." + sys.stderr.flush() + try: + test_all_packages(env_root, defaults) + except SystemExit, e: + fail = (e.code != 0) or fail + + print >>sys.stderr, "Total time for all tests: %f seconds" % (time.time() - starttime) + + if fail: + print >>sys.stderr, "Some tests were unsuccessful." + sys.exit(1) + print >>sys.stderr, "All tests were successful. Ship it!" + sys.exit(0) + +def test_cfx(env_root, verbose): + import cuddlefish.tests + + # tests write to stderr. flush everything before and after to avoid + # confusion later. + sys.stdout.flush(); sys.stderr.flush() + olddir = os.getcwd() + os.chdir(env_root) + retval = cuddlefish.tests.run(verbose) + os.chdir(olddir) + sys.stdout.flush(); sys.stderr.flush() + return retval + +def test_all_testaddons(env_root, defaults): + addons_dir = os.path.join(env_root, "test", "addons") + addons = [dirname for dirname in os.listdir(addons_dir) + if os.path.isdir(os.path.join(addons_dir, dirname))] + addons.sort() + fail = False + for dirname in addons: + # apply the filter + if (not defaults['filter'].split(":")[0] in dirname): + continue + + print >>sys.stderr, "Testing %s..." % dirname + sys.stderr.flush() + try: + run(arguments=["testrun", + "--pkgdir", + os.path.join(addons_dir, dirname)], + defaults=defaults, + env_root=env_root) + except SystemExit, e: + fail = (e.code != 0) or fail + if fail and defaults.get("stopOnError"): + break + + if fail: + print >>sys.stderr, "Some test addons tests were unsuccessful." + sys.exit(-1) + +def test_all_examples(env_root, defaults): + examples_dir = os.path.join(env_root, "examples") + examples = [dirname for dirname in os.listdir(examples_dir) + if os.path.isdir(os.path.join(examples_dir, dirname))] + examples.sort() + fail = False + for dirname in examples: + if (not defaults['filter'].split(":")[0] in dirname): + continue + + print >>sys.stderr, "Testing %s..." % dirname + sys.stderr.flush() + try: + run(arguments=["test", + "--pkgdir", + os.path.join(examples_dir, dirname)], + defaults=defaults, + env_root=env_root) + except SystemExit, e: + fail = (e.code != 0) or fail + if fail and defaults.get("stopOnError"): + break + + if fail: + print >>sys.stderr, "Some examples tests were unsuccessful." + sys.exit(-1) + +def test_all_packages(env_root, defaults): + packages_dir = os.path.join(env_root, "packages") + if os.path.isdir(packages_dir): + packages = [dirname for dirname in os.listdir(packages_dir) + if os.path.isdir(os.path.join(packages_dir, dirname))] + else: + packages = [] + packages.append(env_root) + packages.sort() + print >>sys.stderr, "Testing all available packages: %s." % (", ".join(packages)) + sys.stderr.flush() + fail = False + for dirname in packages: + print >>sys.stderr, "Testing %s..." % dirname + sys.stderr.flush() + try: + run(arguments=["test", + "--pkgdir", + os.path.join(packages_dir, dirname)], + defaults=defaults, + env_root=env_root) + except SystemExit, e: + fail = (e.code != 0) or fail + if fail and defaults.get('stopOnError'): + break + if fail: + print >>sys.stderr, "Some package tests were unsuccessful." + sys.exit(-1) + +def get_config_args(name, env_root): + local_json = os.path.join(env_root, "local.json") + if not (os.path.exists(local_json) and + os.path.isfile(local_json)): + if name == "default": + return [] + else: + print >>sys.stderr, "File does not exist: %s" % local_json + sys.exit(1) + local_json = packaging.load_json_file(local_json) + if 'configs' not in local_json: + print >>sys.stderr, "'configs' key not found in local.json." + sys.exit(1) + if name not in local_json.configs: + if name == "default": + return [] + else: + print >>sys.stderr, "No config found for '%s'." % name + sys.exit(1) + config = local_json.configs[name] + if type(config) != list: + print >>sys.stderr, "Config for '%s' must be a list of strings." % name + sys.exit(1) + return config + +def initializer(env_root, args, out=sys.stdout, err=sys.stderr): + from templates import PACKAGE_JSON, TEST_MAIN_JS + from preflight import create_jid + path = os.getcwd() + addon = os.path.basename(path) + # if more than two arguments + if len(args) > 2: + print >>err, 'Too many arguments.' + return {"result":1} + if len(args) == 2: + path = os.path.join(path,args[1]) + try: + os.mkdir(path) + print >>out, '*', args[1], 'package directory created' + except OSError: + print >>out, '*', args[1], 'already exists, testing if directory is empty' + # avoid clobbering existing files, but we tolerate things like .git + existing = [fn for fn in os.listdir(path) if not fn.startswith(".")] + if existing: + print >>err, 'This command must be run in an empty directory.' + return {"result":1} + for d in ['lib','data','test']: + os.mkdir(os.path.join(path,d)) + print >>out, '*', d, 'directory created' + jid = create_jid() + print >>out, '* generated jID automatically:', jid + open(os.path.join(path,'package.json'),'w').write(PACKAGE_JSON % {'name':addon.lower(), + 'title':addon, + 'id':jid }) + print >>out, '* package.json written' + open(os.path.join(path,'test','test-main.js'),'w').write(TEST_MAIN_JS) + print >>out, '* test/test-main.js written' + open(os.path.join(path,'lib','main.js'),'w').write('') + print >>out, '* lib/main.js written' + if len(args) == 1: + print >>out, '\nYour sample add-on is now ready.' + print >>out, 'Do "cfx test" to test it and "cfx run" to try it. Have fun!' + else: + print >>out, '\nYour sample add-on is now ready in the \'' + args[1] + '\' directory.' + print >>out, 'Change to that directory, then do "cfx test" to test it, \nand "cfx run" to try it. Have fun!' + return {"result":0, "jid":jid} + +def buildJID(target_cfg): + if "id" in target_cfg: + jid = target_cfg["id"] + else: + import uuid + jid = str(uuid.uuid4()) + if not ("@" in jid or jid.startswith("{")): + jid = jid + "@jetpack" + return jid + +def run(arguments=sys.argv[1:], target_cfg=None, pkg_cfg=None, + defaults=None, env_root=os.environ.get('CUDDLEFISH_ROOT'), + stdout=sys.stdout): + versions = get_versions() + sdk_version = versions["version"] + display_version = "Add-on SDK %s (%s)" % (sdk_version, versions["full"]) + parser_kwargs = dict(arguments=arguments, + global_options=global_options, + parser_groups=parser_groups, + usage=usage, + version=display_version, + defaults=defaults) + + (options, args) = parse_args(**parser_kwargs) + + config_args = get_config_args(options.config, env_root); + + # reparse configs with arguments from local.json + if config_args: + parser_kwargs['arguments'] += config_args + (options, args) = parse_args(**parser_kwargs) + + command = args[0] + + if command == "init": + initializer(env_root, args) + return + if command == "testpkgs": + test_all_packages(env_root, defaults=options.__dict__) + return + elif command == "testaddons": + test_all_testaddons(env_root, defaults=options.__dict__) + return + elif command == "testex": + test_all_examples(env_root, defaults=options.__dict__) + return + elif command == "testall": + test_all(env_root, defaults=options.__dict__) + return + elif command == "testcfx": + if options.filter: + print >>sys.stderr, "The filter option is not valid with the testcfx command" + return + test_cfx(env_root, options.verbose) + return + elif command not in ["xpi", "test", "run", "testrun"]: + print >>sys.stderr, "Unknown command: %s" % command + print >>sys.stderr, "Try using '--help' for assistance." + sys.exit(1) + + target_cfg_json = None + if not target_cfg: + if not options.pkgdir: + options.pkgdir = find_parent_package(os.getcwd()) + if not options.pkgdir: + print >>sys.stderr, ("cannot find 'package.json' in the" + " current directory or any parent.") + sys.exit(1) + else: + options.pkgdir = os.path.abspath(options.pkgdir) + if not os.path.exists(os.path.join(options.pkgdir, 'package.json')): + print >>sys.stderr, ("cannot find 'package.json' in" + " %s." % options.pkgdir) + sys.exit(1) + + target_cfg_json = os.path.join(options.pkgdir, 'package.json') + target_cfg = packaging.get_config_in_dir(options.pkgdir) + + # At this point, we're either building an XPI or running Jetpack code in + # a Mozilla application (which includes running tests). + + use_main = False + inherited_options = ['verbose', 'enable_e10s', 'parseable', 'check_memory', + 'no_quit', 'abort_on_missing'] + enforce_timeouts = False + + if command == "xpi": + use_main = True + elif command == "test": + if 'tests' not in target_cfg: + target_cfg['tests'] = [] + inherited_options.extend(['iterations', 'filter', 'profileMemory', + 'stopOnError']) + enforce_timeouts = True + elif command == "run": + use_main = True + elif command == "testrun": + use_main = True + enforce_timeouts = True + else: + assert 0, "shouldn't get here" + + if use_main and 'main' not in target_cfg: + # If the user supplies a template dir, then the main + # program may be contained in the template. + if not options.templatedir: + print >>sys.stderr, "package.json does not have a 'main' entry." + sys.exit(1) + + if not pkg_cfg: + pkg_cfg = packaging.build_config(env_root, target_cfg, options.packagepath) + + target = target_cfg.name + + # TODO: Consider keeping a cache of dynamic UUIDs, based + # on absolute filesystem pathname, in the root directory + # or something. + if command in ('xpi', 'run', 'testrun'): + from cuddlefish.preflight import preflight_config + if target_cfg_json: + config_was_ok, modified = preflight_config(target_cfg, + target_cfg_json) + if not config_was_ok: + if modified: + # we need to re-read package.json . The safest approach + # is to re-run the "cfx xpi"/"cfx run" command. + print >>sys.stderr, ("package.json modified: please re-run" + " 'cfx %s'" % command) + else: + print >>sys.stderr, ("package.json needs modification:" + " please update it and then re-run" + " 'cfx %s'" % command) + sys.exit(1) + # if we make it this far, we have a JID + else: + assert command == "test" + + jid = buildJID(target_cfg) + + targets = [target] + if command == "test": + targets.append(options.test_runner_pkg) + + extra_packages = [] + if options.extra_packages: + extra_packages = options.extra_packages.split(",") + if extra_packages: + targets.extend(extra_packages) + target_cfg.extra_dependencies = extra_packages + + deps = packaging.get_deps_for_targets(pkg_cfg, targets) + + from cuddlefish.manifest import build_manifest, ModuleNotFoundError, \ + BadChromeMarkerError + # Figure out what loader files should be scanned. This is normally + # computed inside packaging.generate_build_for_target(), by the first + # dependent package that defines a "loader" property in its package.json. + # This property is interpreted as a filename relative to the top of that + # file, and stored as a path in build.loader . generate_build_for_target() + # cannot be called yet (it needs the list of used_deps that + # build_manifest() computes, but build_manifest() needs the list of + # loader files that it computes). We could duplicate or factor out this + # build.loader logic, but that would be messy, so instead we hard-code + # the choice of loader for manifest-generation purposes. In practice, + # this means that alternative loaders probably won't work with + # --strip-xpi. + assert packaging.DEFAULT_LOADER == "addon-sdk" + assert pkg_cfg.packages["addon-sdk"].loader == "lib/sdk/loader/cuddlefish.js" + cuddlefish_js_path = os.path.join(pkg_cfg.packages["addon-sdk"].root_dir, + "lib", "sdk", "loader", "cuddlefish.js") + loader_modules = [("addon-sdk", "lib", "sdk/loader/cuddlefish", cuddlefish_js_path)] + scan_tests = command == "test" + + try: + manifest = build_manifest(target_cfg, pkg_cfg, deps, scan_tests, + None, loader_modules, + abort_on_missing=options.abort_on_missing) + except ModuleNotFoundError, e: + print str(e) + sys.exit(1) + except BadChromeMarkerError, e: + # An error had already been displayed on stderr in manifest code + sys.exit(1) + used_deps = manifest.get_used_packages() + if command == "test": + # The test runner doesn't appear to link against any actual packages, + # because it loads everything at runtime (invisible to the linker). + # If we believe that, we won't set up URI mappings for anything, and + # tests won't be able to run. + used_deps = deps + for xp in extra_packages: + if xp not in used_deps: + used_deps.append(xp) + + build = packaging.generate_build_for_target( + pkg_cfg, target, used_deps, + include_dep_tests=options.dep_tests, + is_running_tests=(command == "test") + ) + + harness_options = { + 'jetpackID': jid, + 'staticArgs': options.static_args, + 'name': target, + } + + harness_options.update(build) + + # When cfx is run from sdk root directory, we will strip sdk modules and + # override them with local modules. + # So that integration tools will continue to work and use local modules + if os.getcwd() == env_root: + options.bundle_sdk = True + options.force_use_bundled_sdk = False + options.overload_modules = True + + if options.pkgdir == env_root: + options.bundle_sdk = True + options.overload_modules = True + + extra_environment = {} + if command == "test": + # This should be contained in the test runner package. + harness_options['main'] = 'sdk/test/runner' + harness_options['mainPath'] = 'sdk/test/runner' + else: + harness_options['main'] = target_cfg.get('main') + harness_options['mainPath'] = manifest.top_path + extra_environment["CFX_COMMAND"] = command + + for option in inherited_options: + harness_options[option] = getattr(options, option) + + harness_options['metadata'] = packaging.get_metadata(pkg_cfg, used_deps) + + harness_options['sdkVersion'] = sdk_version + + packaging.call_plugins(pkg_cfg, used_deps) + + retval = 0 + + if options.templatedir: + app_extension_dir = os.path.abspath(options.templatedir) + elif os.path.exists(os.path.join(options.pkgdir, "app-extension")): + app_extension_dir = os.path.join(options.pkgdir, "app-extension") + else: + mydir = os.path.dirname(os.path.abspath(__file__)) + app_extension_dir = os.path.join(mydir, "../../app-extension") + + # Do not add entries for SDK modules + harness_options['manifest'] = manifest.get_harness_options_manifest(False) + + # Gives an hint to tell if sdk modules are bundled or not + harness_options['is-sdk-bundled'] = options.bundle_sdk or options.no_strip_xpi + + if options.force_use_bundled_sdk: + if not harness_options['is-sdk-bundled']: + print >>sys.stderr, ("--force-use-bundled-sdk " + "can't be used if sdk isn't bundled.") + sys.exit(1) + if options.overload_modules: + print >>sys.stderr, ("--force-use-bundled-sdk and --overload-modules " + "can't be used at the same time.") + sys.exit(1) + # Pass a flag in order to force using sdk modules shipped in the xpi + harness_options['force-use-bundled-sdk'] = True + + from cuddlefish.rdf import gen_manifest, RDFUpdate + + manifest_rdf = gen_manifest(template_root_dir=app_extension_dir, + target_cfg=target_cfg, + jid=jid, + update_url=options.update_url, + bootstrap=True, + enable_mobile=options.enable_mobile) + + if command == "xpi" and options.update_link: + if not options.update_link.startswith("https"): + raise optparse.OptionValueError("--update-link must start with 'https': %s" % options.update_link) + rdf_name = UPDATE_RDF_FILENAME % target_cfg.name + print >>stdout, "Exporting update description to %s." % rdf_name + update = RDFUpdate() + update.add(manifest_rdf, options.update_link) + open(rdf_name, "w").write(str(update)) + + # ask the manifest what files were used, so we can construct an XPI + # without the rest. This will include the loader (and everything it + # uses) because of the "loader_modules" starting points we passed to + # build_manifest earlier + used_files = None + if command == "xpi": + used_files = set(manifest.get_used_files(options.bundle_sdk)) + + if options.no_strip_xpi: + used_files = None # disables the filter, includes all files + + if command == 'xpi': + from cuddlefish.xpi import build_xpi + # Generate extra options + extra_harness_options = {} + for kv in options.extra_harness_option_args: + key,value = kv.split("=", 1) + extra_harness_options[key] = value + # Generate xpi filepath + if options.output_file: + xpi_path = options.output_file + else: + xpi_path = XPI_FILENAME % target_cfg.name + + print >>stdout, "Exporting extension to %s." % xpi_path + build_xpi(template_root_dir=app_extension_dir, + manifest=manifest_rdf, + xpi_path=xpi_path, + harness_options=harness_options, + limit_to=used_files, + extra_harness_options=extra_harness_options, + bundle_sdk=True, + pkgdir=options.pkgdir) + else: + from cuddlefish.runner import run_app + + if options.no_connections == "default": + if command == "run": + no_connections = False + else: + no_connections = True + elif options.no_connections == "on": + no_connections = True + else: + no_connections = False + + if options.profiledir: + options.profiledir = os.path.expanduser(options.profiledir) + options.profiledir = os.path.abspath(options.profiledir) + + if options.addons is not None: + options.addons = options.addons.split(",") + + enable_e10s = options.enable_e10s or target_cfg.get('e10s', False) + + try: + retval = run_app(harness_root_dir=app_extension_dir, + manifest_rdf=manifest_rdf, + harness_options=harness_options, + app_type=options.app, + binary=options.binary, + profiledir=options.profiledir, + verbose=options.verbose, + parseable=options.parseable, + enforce_timeouts=enforce_timeouts, + logfile=options.logfile, + addons=options.addons, + args=options.cmdargs, + extra_environment=extra_environment, + norun=options.no_run, + noquit=options.no_quit, + used_files=used_files, + enable_mobile=options.enable_mobile, + mobile_app_name=options.mobile_app_name, + env_root=env_root, + is_running_tests=(command == "test"), + overload_modules=options.overload_modules, + bundle_sdk=options.bundle_sdk, + pkgdir=options.pkgdir, + enable_e10s=enable_e10s, + no_connections=no_connections) + except ValueError, e: + print "" + print "A given cfx option has an inappropriate value:" + print >>sys.stderr, " " + " \n ".join(str(e).split("\n")) + retval = -1 + except Exception, e: + if str(e).startswith(MOZRUNNER_BIN_NOT_FOUND): + print >>sys.stderr, MOZRUNNER_BIN_NOT_FOUND_HELP.strip() + retval = -1 + else: + raise + sys.exit(retval) diff --git a/addon-sdk/source/python-lib/cuddlefish/_version.py b/addon-sdk/source/python-lib/cuddlefish/_version.py new file mode 100644 index 000000000..6f12874d2 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/_version.py @@ -0,0 +1,174 @@ + +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (build by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by versioneer-0.6 +# (https://github.com/warner/python-versioneer) + +# these strings will be replaced by git during git-archive +git_refnames = "$Format:%d$" +git_full = "$Format:%H$" + + +import subprocess + +def run_command(args, cwd=None, verbose=False): + try: + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd) + except EnvironmentError, e: + if verbose: + print "unable to run %s" % args[0] + print e + return None + stdout = p.communicate()[0].strip() + if p.returncode != 0: + if verbose: + print "unable to run %s (error)" % args[0] + return None + return stdout + + +import sys +import re +import os.path + +def get_expanded_variables(versionfile_source): + """ + the code embedded in _version.py can just fetch the value of these + variables. When used from setup.py, we don't want to import + _version.py, so we do it with a regexp instead. This function is not + used from _version.py. + """ + variables = {} + try: + for line in open(versionfile_source,"r").readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["full"] = mo.group(1) + except EnvironmentError: + pass + return variables + +def versions_from_expanded_variables(variables, tag_prefix): + refnames = variables["refnames"].strip() + if refnames.startswith("$Format"): + return {} # unexpanded, so not in an unpacked git-archive tarball + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + for ref in list(refs): + if not re.search(r'\d', ref): + refs.discard(ref) + # Assume all version tags have a digit. git's %d expansion + # behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us + # distinguish between branches and tags. By ignoring refnames + # without digits, we filter out many common branch names like + # "release" and "stabilization", as well as "HEAD" and "master". + for ref in sorted(refs): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + return { "version": r, + "full": variables["full"].strip() } + # no suitable tags, so we use the full revision id + return { "version": variables["full"].strip(), + "full": variables["full"].strip() } + +def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): + """ + this runs 'git' from the root of the source tree. That either means + someone ran a setup.py command (and this code is in versioneer.py, thus + the containing directory is the root of the source tree), or someone + ran a project-specific entry point (and this code is in _version.py, + thus the containing directory is somewhere deeper in the source tree). + This only gets called if the git-archive 'subst' variables were *not* + expanded, and _version.py hasn't already been rewritten with a short + version string, meaning we're inside a checked out source tree. + """ + try: + here = os.path.abspath(__file__) + except NameError: + # some py2exe/bbfreeze/non-CPython implementations don't do __file__ + return {} # not always correct + + # versionfile_source is the relative path from the top of the source tree + # (where the .git directory might live) to this file. Invert this to find + # the root from __file__. + root = here + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + if not os.path.exists(os.path.join(root, ".git")): + return {} + + GIT = "git" + if sys.platform == "win32": + GIT = "git.cmd" + stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"], + cwd=root) + if stdout is None: + return {} + if not stdout.startswith(tag_prefix): + if verbose: + print "tag '%s' doesn't start with prefix '%s'" % (stdout, tag_prefix) + return {} + tag = stdout[len(tag_prefix):] + stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root) + if stdout is None: + return {} + full = stdout.strip() + if tag.endswith("-dirty"): + full += "-dirty" + return {"version": tag, "full": full} + + +def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False): + try: + here = os.path.abspath(__file__) + # versionfile_source is the relative path from the top of the source + # tree (where the .git directory might live) to _version.py, when + # this is used by the runtime. Invert this to find the root from + # __file__. + root = here + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + except NameError: + # try a couple different things to handle py2exe, bbfreeze, and + # non-CPython implementations which don't do __file__. This code + # either lives in versioneer.py (used by setup.py) or _version.py + # (used by the runtime). In the versioneer.py case, sys.argv[0] will + # be setup.py, in the root of the source tree. In the _version.py + # case, we have no idea what sys.argv[0] is (some + # application-specific runner). + root = os.path.dirname(os.path.abspath(sys.argv[0])) + # Source tarballs conventionally unpack into a directory that includes + # both the project name and a version string. + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): + if verbose: + print "dirname '%s' doesn't start with prefix '%s'" % (dirname, parentdir_prefix) + return None + return {"version": dirname[len(parentdir_prefix):], "full": ""} + +tag_prefix = "" +parentdir_prefix = "addon-sdk-" +versionfile_source = "python-lib/cuddlefish/_version.py" + +def get_versions(): + variables = { "refnames": git_refnames, "full": git_full } + ver = versions_from_expanded_variables(variables, tag_prefix) + if not ver: + ver = versions_from_vcs(tag_prefix, versionfile_source) + if not ver: + ver = versions_from_parentdir(parentdir_prefix, versionfile_source) + if not ver: + ver = {"version": "unknown", "full": ""} + return ver + diff --git a/addon-sdk/source/python-lib/cuddlefish/bunch.py b/addon-sdk/source/python-lib/cuddlefish/bunch.py new file mode 100644 index 000000000..5efa79f16 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/bunch.py @@ -0,0 +1,34 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Taken from Paver's paver.options module. + +class Bunch(dict): + """A dictionary that provides attribute-style access.""" + + def __repr__(self): + keys = self.keys() + keys.sort() + args = ', '.join(['%s=%r' % (key, self[key]) for key in keys]) + return '%s(%s)' % (self.__class__.__name__, args) + + def __getitem__(self, key): + item = dict.__getitem__(self, key) + if callable(item): + return item() + return item + + def __getattr__(self, name): + try: + return self[name] + except KeyError: + raise AttributeError(name) + + __setattr__ = dict.__setitem__ + + def __delattr__(self, name): + try: + del self[name] + except KeyError: + raise AttributeError(name) diff --git a/addon-sdk/source/python-lib/cuddlefish/manifest.py b/addon-sdk/source/python-lib/cuddlefish/manifest.py new file mode 100644 index 000000000..e9913be7b --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/manifest.py @@ -0,0 +1,807 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import os, sys, re, hashlib +import simplejson as json +SEP = os.path.sep +from cuddlefish.util import filter_filenames, filter_dirnames + +# Load new layout mapping hashtable +path = os.path.join(os.environ.get('CUDDLEFISH_ROOT'), "mapping.json") +data = open(path, 'r').read() +NEW_LAYOUT_MAPPING = json.loads(data) + +def js_zipname(packagename, modulename): + return "%s-lib/%s.js" % (packagename, modulename) +def docs_zipname(packagename, modulename): + return "%s-docs/%s.md" % (packagename, modulename) +def datamap_zipname(packagename): + return "%s-data.json" % packagename +def datafile_zipname(packagename, datapath): + return "%s-data/%s" % (packagename, datapath) + +def to_json(o): + return json.dumps(o, indent=1).encode("utf-8")+"\n" + +class ModuleNotFoundError(Exception): + def __init__(self, requirement_type, requirement_name, + used_by, line_number, looked_in): + Exception.__init__(self) + self.requirement_type = requirement_type # "require" or "define" + self.requirement_name = requirement_name # string, what they require()d + self.used_by = used_by # string, full path to module which did require() + self.line_number = line_number # int, 1-indexed line number of first require() + self.looked_in = looked_in # list of full paths to potential .js files + def __str__(self): + what = "%s(%s)" % (self.requirement_type, self.requirement_name) + where = self.used_by + if self.line_number is not None: + where = "%s:%d" % (self.used_by, self.line_number) + searched = "Looked for it in:\n %s\n" % "\n ".join(self.looked_in) + return ("ModuleNotFoundError: unable to satisfy: %s from\n" + " %s:\n" % (what, where)) + searched + +class BadModuleIdentifier(Exception): + pass +class BadSection(Exception): + pass +class UnreachablePrefixError(Exception): + pass + +class ManifestEntry: + def __init__(self): + self.docs_filename = None + self.docs_hash = None + self.requirements = {} + self.datamap = None + + def get_path(self): + name = self.moduleName + + if name.endswith(".js"): + name = name[:-3] + items = [] + # Only add package name for addons, so that system module paths match + # the path from the commonjs root directory and also match the loader + # mappings. + if self.packageName != "addon-sdk": + items.append(self.packageName) + # And for the same reason, do not append `lib/`. + if self.sectionName == "tests": + items.append(self.sectionName) + items.append(name) + + return "/".join(items) + + def get_entry_for_manifest(self): + entry = { "packageName": self.packageName, + "sectionName": self.sectionName, + "moduleName": self.moduleName, + "jsSHA256": self.js_hash, + "docsSHA256": self.docs_hash, + "requirements": {}, + } + for req in self.requirements: + if isinstance(self.requirements[req], ManifestEntry): + them = self.requirements[req] # this is another ManifestEntry + entry["requirements"][req] = them.get_path() + else: + # something magic. The manifest entry indicates that they're + # allowed to require() it + entry["requirements"][req] = self.requirements[req] + assert isinstance(entry["requirements"][req], unicode) or \ + isinstance(entry["requirements"][req], str) + return entry + + def add_js(self, js_filename): + self.js_filename = js_filename + self.js_hash = hash_file(js_filename) + def add_docs(self, docs_filename): + self.docs_filename = docs_filename + self.docs_hash = hash_file(docs_filename) + def add_requirement(self, reqname, reqdata): + self.requirements[reqname] = reqdata + def add_data(self, datamap): + self.datamap = datamap + + def get_js_zipname(self): + return js_zipname(self.packagename, self.modulename) + def get_docs_zipname(self): + if self.docs_hash: + return docs_zipname(self.packagename, self.modulename) + return None + # self.js_filename + # self.docs_filename + + +def hash_file(fn): + return hashlib.sha256(open(fn,"rb").read()).hexdigest() + +def get_datafiles(datadir): + """ + yields pathnames relative to DATADIR, ignoring some files + """ + for dirpath, dirnames, filenames in os.walk(datadir): + filenames = list(filter_filenames(filenames)) + # this tells os.walk to prune the search + dirnames[:] = filter_dirnames(dirnames) + for filename in filenames: + fullname = os.path.join(dirpath, filename) + assert fullname.startswith(datadir+SEP), "%s%s not in %s" % (datadir, SEP, fullname) + yield fullname[len(datadir+SEP):] + + +class DataMap: + # one per package + def __init__(self, pkg): + self.pkg = pkg + self.name = pkg.name + self.files_to_copy = [] + datamap = {} + datadir = os.path.join(pkg.root_dir, "data") + for dataname in get_datafiles(datadir): + absname = os.path.join(datadir, dataname) + zipname = datafile_zipname(pkg.name, dataname) + datamap[dataname] = hash_file(absname) + self.files_to_copy.append( (zipname, absname) ) + self.data_manifest = to_json(datamap) + self.data_manifest_hash = hashlib.sha256(self.data_manifest).hexdigest() + self.data_manifest_zipname = datamap_zipname(pkg.name) + self.data_uri_prefix = "%s/data/" % (self.name) + +class BadChromeMarkerError(Exception): + pass + +class ModuleInfo: + def __init__(self, package, section, name, js, docs): + self.package = package + self.section = section + self.name = name + self.js = js + self.docs = docs + + def __hash__(self): + return hash( (self.package.name, self.section, self.name, + self.js, self.docs) ) + def __eq__(self, them): + if them.__class__ is not self.__class__: + return False + if ((them.package.name, them.section, them.name, them.js, them.docs) != + (self.package.name, self.section, self.name, self.js, self.docs) ): + return False + return True + + def __repr__(self): + return "ModuleInfo [%s %s %s] (%s, %s)" % (self.package.name, + self.section, + self.name, + self.js, self.docs) + +class ManifestBuilder: + def __init__(self, target_cfg, pkg_cfg, deps, extra_modules, + stderr=sys.stderr, abort_on_missing=False): + self.manifest = {} # maps (package,section,module) to ManifestEntry + self.target_cfg = target_cfg # the entry point + self.pkg_cfg = pkg_cfg # all known packages + self.deps = deps # list of package names to search + self.used_packagenames = set() + self.stderr = stderr + self.extra_modules = extra_modules + self.modules = {} # maps ModuleInfo to URI in self.manifest + self.datamaps = {} # maps package name to DataMap instance + self.files = [] # maps manifest index to (absfn,absfn) js/docs pair + self.test_modules = [] # for runtime + self.abort_on_missing = abort_on_missing # cfx eol + + def build(self, scan_tests, test_filter_re): + """ + process the top module, which recurses to process everything it reaches + """ + if "main" in self.target_cfg: + top_mi = self.find_top(self.target_cfg) + top_me = self.process_module(top_mi) + self.top_path = top_me.get_path() + self.datamaps[self.target_cfg.name] = DataMap(self.target_cfg) + if scan_tests: + mi = self._find_module_in_package("addon-sdk", "lib", "sdk/test/runner", []) + self.process_module(mi) + # also scan all test files in all packages that we use. By making + # a copy of self.used_packagenames first, we refrain from + # processing tests in packages that our own tests depend upon. If + # we're running tests for package A, and either modules in A or + # tests in A depend upon modules from package B, we *don't* want + # to run tests for package B. + test_modules = [] + dirnames = self.target_cfg["tests"] + if isinstance(dirnames, basestring): + dirnames = [dirnames] + dirnames = [os.path.join(self.target_cfg.root_dir, d) + for d in dirnames] + for d in dirnames: + for filename in os.listdir(d): + if filename.startswith("test-") and filename.endswith(".js"): + testname = filename[:-3] # require(testname) + if test_filter_re: + if not re.search(test_filter_re, testname): + continue + tmi = ModuleInfo(self.target_cfg, "tests", testname, + os.path.join(d, filename), None) + # scan the test's dependencies + tme = self.process_module(tmi) + test_modules.append( (testname, tme) ) + # also add it as an artificial dependency of unit-test-finder, so + # the runtime dynamic load can work. + test_finder = self.get_manifest_entry("addon-sdk", "lib", + "sdk/deprecated/unit-test-finder") + for (testname,tme) in test_modules: + test_finder.add_requirement(testname, tme) + # finally, tell the runtime about it, so they won't have to + # search for all tests. self.test_modules will be passed + # through the harness-options.json file in the + # .allTestModules property. + # Pass the absolute module path. + self.test_modules.append(tme.get_path()) + + # include files used by the loader + for em in self.extra_modules: + (pkgname, section, modname, js) = em + mi = ModuleInfo(self.pkg_cfg.packages[pkgname], section, modname, + js, None) + self.process_module(mi) + + + def get_module_entries(self): + return frozenset(self.manifest.values()) + def get_data_entries(self): + return frozenset(self.datamaps.values()) + + def get_used_packages(self): + used = set() + for index in self.manifest: + (package, section, module) = index + used.add(package) + return sorted(used) + + def get_used_files(self, bundle_sdk_modules): + """ + returns all .js files that we reference, plus data/ files. You will + need to add the loader, off-manifest files that it needs, and + generated metadata. + """ + for datamap in self.datamaps.values(): + for (zipname, absname) in datamap.files_to_copy: + yield absname + + for me in self.get_module_entries(): + # Only ship SDK files if we are told to do so + if me.packageName != "addon-sdk" or bundle_sdk_modules: + yield me.js_filename + + def get_harness_options_manifest(self, bundle_sdk_modules): + manifest = {} + for me in self.get_module_entries(): + path = me.get_path() + # Do not add manifest entries for system modules. + # Doesn't prevent from shipping modules. + # Shipping modules is decided in `get_used_files`. + if me.packageName != "addon-sdk" or bundle_sdk_modules: + manifest[path] = me.get_entry_for_manifest() + return manifest + + def get_manifest_entry(self, package, section, module): + index = (package, section, module) + if index not in self.manifest: + m = self.manifest[index] = ManifestEntry() + m.packageName = package + m.sectionName = section + m.moduleName = module + self.used_packagenames.add(package) + return self.manifest[index] + + def uri_name_from_path(self, pkg, fn): + # given a filename like .../pkg1/lib/bar/foo.js, and a package + # specification (with a .root_dir like ".../pkg1" and a .lib list of + # paths where .lib[0] is like "lib"), return the appropriate NAME + # that can be put into a URI like resource://JID-pkg1-lib/NAME . This + # will throw an exception if the file is outside of the lib/ + # directory, since that means we can't construct a URI that points to + # it. + # + # This should be a lot easier, and shouldn't fail when the file is in + # the root of the package. Both should become possible when the XPI + # is rearranged and our URI scheme is simplified. + fn = os.path.abspath(fn) + pkglib = pkg.lib[0] + libdir = os.path.abspath(os.path.join(pkg.root_dir, pkglib)) + # AARGH, section and name! we need to reverse-engineer a + # ModuleInfo instance that will produce a URI (in the form + # PREFIX/PKGNAME-SECTION/JS) that will map to the existing file. + # Until we fix URI generation to get rid of "sections", this is + # limited to files in the same .directories.lib as the rest of + # the package uses. So if the package's main files are in lib/, + # but the main.js is in the package root, there is no URI we can + # construct that will point to it, and we must fail. + # + # This will become much easier (and the failure case removed) + # when we get rid of sections and change the URIs to look like + # (PREFIX/PKGNAME/PATH-TO-JS). + + # AARGH 2, allowing .lib to be a list is really getting in the + # way. That needs to go away eventually too. + if not fn.startswith(libdir): + raise UnreachablePrefixError("Sorry, but the 'main' file (%s) in package %s is outside that package's 'lib' directory (%s), so I cannot construct a URI to reach it." + % (fn, pkg.name, pkglib)) + name = fn[len(libdir):].lstrip(SEP)[:-len(".js")] + return name + + + def parse_main(self, root_dir, main, check_lib_dir=None): + # 'main' can be like one of the following: + # a: ./lib/main.js b: ./lib/main c: lib/main + # we require it to be a path to the file, though, and ignore the + # .directories stuff. So just "main" is insufficient if you really + # want something in a "lib/" subdirectory. + if main.endswith(".js"): + main = main[:-len(".js")] + if main.startswith("./"): + main = main[len("./"):] + # package.json must always use "/", but on windows we'll replace that + # with "\" before using it as an actual filename + main = os.sep.join(main.split("/")) + paths = [os.path.join(root_dir, main+".js")] + if check_lib_dir is not None: + paths.append(os.path.join(root_dir, check_lib_dir, main+".js")) + return paths + + def find_top_js(self, target_cfg): + for libdir in target_cfg.lib: + for n in self.parse_main(target_cfg.root_dir, target_cfg.main, + libdir): + if os.path.exists(n): + return n + raise KeyError("unable to find main module '%s.js' in top-level package" % target_cfg.main) + + def find_top(self, target_cfg): + top_js = self.find_top_js(target_cfg) + n = os.path.join(target_cfg.root_dir, "README.md") + if os.path.exists(n): + top_docs = n + else: + top_docs = None + name = self.uri_name_from_path(target_cfg, top_js) + return ModuleInfo(target_cfg, "lib", name, top_js, top_docs) + + def process_module(self, mi): + pkg = mi.package + #print "ENTERING", pkg.name, mi.name + # mi.name must be fully-qualified + assert (not mi.name.startswith("./") and + not mi.name.startswith("../")) + # create and claim the manifest row first + me = self.get_manifest_entry(pkg.name, mi.section, mi.name) + + me.add_js(mi.js) + if mi.docs: + me.add_docs(mi.docs) + + js_lines = open(mi.js,"r").readlines() + requires, problems, locations = scan_module(mi.js,js_lines,self.stderr) + if problems: + # the relevant instructions have already been written to stderr + raise BadChromeMarkerError() + + # We update our requirements on the way out of the depth-first + # traversal of the module graph + + for reqname in sorted(requires.keys()): + # If requirement is chrome or a pseudo-module (starts with @) make + # path a requirement name. + if reqname == "chrome" or reqname.startswith("@"): + me.add_requirement(reqname, reqname) + else: + # when two modules require() the same name, do they get a + # shared instance? This is a deep question. For now say yes. + + # find_req_for() returns an entry to put in our + # 'requirements' dict, and will recursively process + # everything transitively required from here. It will also + # populate the self.modules[] cache. Note that we must + # tolerate cycles in the reference graph. + looked_in = [] # populated by subroutines + them_me = self.find_req_for(mi, reqname, looked_in, locations) + if them_me is None: + if mi.section == "tests": + # tolerate missing modules in tests, because + # test-securable-module.js, and the modules/red.js + # that it imports, both do that intentionally + continue + if reqname.endswith(".jsm"): + # ignore JSM modules + continue + if not self.abort_on_missing: + # print a warning, but tolerate missing modules + # unless cfx --abort-on-missing-module flag was set + print >>self.stderr, "Warning: missing module: %s" % reqname + me.add_requirement(reqname, reqname) + continue + lineno = locations.get(reqname) # None means define() + if lineno is None: + reqtype = "define" + else: + reqtype = "require" + err = ModuleNotFoundError(reqtype, reqname, + mi.js, lineno, looked_in) + raise err + else: + me.add_requirement(reqname, them_me) + + return me + #print "LEAVING", pkg.name, mi.name + + def find_req_for(self, from_module, reqname, looked_in, locations): + # handle a single require(reqname) statement from from_module . + # Return a uri that exists in self.manifest + # Populate looked_in with places we looked. + def BAD(msg): + return BadModuleIdentifier(msg + " in require(%s) from %s" % + (reqname, from_module)) + + if not reqname: + raise BAD("no actual modulename") + + # Allow things in tests/*.js to require both test code and real code. + # But things in lib/*.js can only require real code. + if from_module.section == "tests": + lookfor_sections = ["tests", "lib"] + elif from_module.section == "lib": + lookfor_sections = ["lib"] + else: + raise BadSection(from_module.section) + modulename = from_module.name + + #print " %s require(%s))" % (from_module, reqname) + + if reqname.startswith("./") or reqname.startswith("../"): + # 1: they want something relative to themselves, always from + # their own package + them = modulename.split("/")[:-1] + bits = reqname.split("/") + while bits[0] in (".", ".."): + if not bits: + raise BAD("no actual modulename") + if bits[0] == "..": + if not them: + raise BAD("too many ..") + them.pop() + bits.pop(0) + bits = them+bits + lookfor_pkg = from_module.package.name + lookfor_mod = "/".join(bits) + return self._get_module_from_package(lookfor_pkg, + lookfor_sections, lookfor_mod, + looked_in) + + # non-relative import. Might be a short name (requiring a search + # through "library" packages), or a fully-qualified one. + + if "/" in reqname: + # 2: PKG/MOD: find PKG, look inside for MOD + bits = reqname.split("/") + lookfor_pkg = bits[0] + lookfor_mod = "/".join(bits[1:]) + mi = self._get_module_from_package(lookfor_pkg, + lookfor_sections, lookfor_mod, + looked_in) + if mi: # caution, 0==None + return mi + else: + # 3: try finding PKG, if found, use its main.js entry point + lookfor_pkg = reqname + mi = self._get_entrypoint_from_package(lookfor_pkg, looked_in) + if mi: + return mi + + # 4: search packages for MOD or MODPARENT/MODCHILD. We always search + # their own package first, then the list of packages defined by their + # .dependencies list + from_pkg = from_module.package.name + mi = self._search_packages_for_module(from_pkg, + lookfor_sections, reqname, + looked_in) + if mi: + return mi + + # Only after we look for module in the addon itself, search for a module + # in new layout. + # First normalize require argument in order to easily find a mapping + normalized = reqname + if normalized.endswith(".js"): + normalized = normalized[:-len(".js")] + if normalized.startswith("addon-kit/"): + normalized = normalized[len("addon-kit/"):] + if normalized.startswith("api-utils/"): + normalized = normalized[len("api-utils/"):] + if normalized in NEW_LAYOUT_MAPPING: + # get the new absolute path for this module + original_reqname = reqname + reqname = NEW_LAYOUT_MAPPING[normalized] + from_pkg = from_module.package.name + + # If the addon didn't explicitely told us to ignore deprecated + # require path, warn the developer: + # (target_cfg is the package.json file) + if not "ignore-deprecated-path" in self.target_cfg: + lineno = locations.get(original_reqname) + print >>self.stderr, "Warning: Use of deprecated require path:" + print >>self.stderr, " In %s:%d:" % (from_module.js, lineno) + print >>self.stderr, " require('%s')." % original_reqname + print >>self.stderr, " New path should be:" + print >>self.stderr, " require('%s')" % reqname + + return self._search_packages_for_module(from_pkg, + lookfor_sections, reqname, + looked_in) + else: + # We weren't able to find this module, really. + return None + + def _handle_module(self, mi): + if not mi: + return None + + # we tolerate cycles in the reference graph, which means we need to + # populate the self.modules cache before recursing into + # process_module() . We must also check the cache first, so recursion + # can terminate. + if mi in self.modules: + return self.modules[mi] + + # this creates the entry + new_entry = self.get_manifest_entry(mi.package.name, mi.section, mi.name) + # and populates the cache + self.modules[mi] = new_entry + self.process_module(mi) + return new_entry + + def _get_module_from_package(self, pkgname, sections, modname, looked_in): + if pkgname not in self.pkg_cfg.packages: + return None + mi = self._find_module_in_package(pkgname, sections, modname, + looked_in) + return self._handle_module(mi) + + def _get_entrypoint_from_package(self, pkgname, looked_in): + if pkgname not in self.pkg_cfg.packages: + return None + pkg = self.pkg_cfg.packages[pkgname] + main = pkg.get("main", None) + if not main: + return None + for js in self.parse_main(pkg.root_dir, main): + looked_in.append(js) + if os.path.exists(js): + section = "lib" + name = self.uri_name_from_path(pkg, js) + docs = None + mi = ModuleInfo(pkg, section, name, js, docs) + return self._handle_module(mi) + return None + + def _search_packages_for_module(self, from_pkg, sections, reqname, + looked_in): + searchpath = [] # list of package names + searchpath.append(from_pkg) # search self first + us = self.pkg_cfg.packages[from_pkg] + if 'dependencies' in us: + # only look in dependencies + searchpath.extend(us['dependencies']) + else: + # they didn't declare any dependencies (or they declared an empty + # list, but we'll treat that as not declaring one, because it's + # easier), so look in all deps, sorted alphabetically, so + # addon-kit comes first. Note that self.deps includes all + # packages found by traversing the ".dependencies" lists in each + # package.json, starting from the main addon package, plus + # everything added by --extra-packages + searchpath.extend(sorted(self.deps)) + for pkgname in searchpath: + mi = self._find_module_in_package(pkgname, sections, reqname, + looked_in) + if mi: + return self._handle_module(mi) + return None + + def _find_module_in_package(self, pkgname, sections, name, looked_in): + # require("a/b/c") should look at ...\a\b\c.js on windows + filename = os.sep.join(name.split("/")) + # normalize filename, make sure that we do not add .js if it already has + # it. + if not filename.endswith(".js") and not filename.endswith(".json"): + filename += ".js" + + if filename.endswith(".js"): + basename = filename[:-3] + if filename.endswith(".json"): + basename = filename[:-5] + + pkg = self.pkg_cfg.packages[pkgname] + if isinstance(sections, basestring): + sections = [sections] + for section in sections: + for sdir in pkg.get(section, []): + js = os.path.join(pkg.root_dir, sdir, filename) + looked_in.append(js) + if os.path.exists(js): + docs = None + maybe_docs = os.path.join(pkg.root_dir, "docs", + basename+".md") + if section == "lib" and os.path.exists(maybe_docs): + docs = maybe_docs + return ModuleInfo(pkg, section, name, js, docs) + return None + +def build_manifest(target_cfg, pkg_cfg, deps, scan_tests, + test_filter_re=None, extra_modules=[], abort_on_missing=False): + """ + Perform recursive dependency analysis starting from entry_point, + building up a manifest of modules that need to be included in the XPI. + Each entry will map require() names to the URL of the module that will + be used to satisfy that dependency. The manifest will be used by the + runtime's require() code. + + This returns a ManifestBuilder object, with two public methods. The + first, get_module_entries(), returns a set of ManifestEntry objects, each + of which can be asked for the following: + + * its contribution to the harness-options.json '.manifest' + * the local disk name + * the name in the XPI at which it should be placed + + The second is get_data_entries(), which returns a set of DataEntry + objects, each of which has: + + * local disk name + * name in the XPI + + note: we don't build the XPI here, but our manifest is passed to the + code which does, so it knows what to copy into the XPI. + """ + + mxt = ManifestBuilder(target_cfg, pkg_cfg, deps, extra_modules, + abort_on_missing=abort_on_missing) + mxt.build(scan_tests, test_filter_re) + return mxt + + + +COMMENT_PREFIXES = ["//", "/*", "*", "dump("] + +REQUIRE_RE = r"(?<![\'\"])require\s*\(\s*[\'\"]([^\'\"]+?)[\'\"]\s*\)" + +# detect the define idiom of the form: +# define("module name", ["dep1", "dep2", "dep3"], function() {}) +# by capturing the contents of the list in a group. +DEF_RE = re.compile(r"(require|define)\s*\(\s*([\'\"][^\'\"]+[\'\"]\s*,)?\s*\[([^\]]+)\]") + +# Out of the async dependencies, do not allow quotes in them. +DEF_RE_ALLOWED = re.compile(r"^[\'\"][^\'\"]+[\'\"]$") + +def scan_requirements_with_grep(fn, lines): + requires = {} + first_location = {} + for (lineno0, line) in enumerate(lines): + for clause in line.split(";"): + clause = clause.strip() + iscomment = False + for commentprefix in COMMENT_PREFIXES: + if clause.startswith(commentprefix): + iscomment = True + if iscomment: + continue + mo = re.finditer(REQUIRE_RE, clause) + if mo: + for mod in mo: + modname = mod.group(1) + requires[modname] = {} + if modname not in first_location: + first_location[modname] = lineno0 + 1 + + # define() can happen across multiple lines, so join everyone up. + wholeshebang = "\n".join(lines) + for match in DEF_RE.finditer(wholeshebang): + # this should net us a list of string literals separated by commas + for strbit in match.group(3).split(","): + strbit = strbit.strip() + # There could be a trailing comma netting us just whitespace, so + # filter that out. Make sure that only string values with + # quotes around them are allowed, and no quotes are inside + # the quoted value. + if strbit and DEF_RE_ALLOWED.match(strbit): + modname = strbit[1:-1] + if modname not in ["exports"]: + requires[modname] = {} + # joining all the lines means we lose line numbers, so we + # can't fill first_location[] + + return requires, first_location + +CHROME_ALIASES = [ + (re.compile(r"Components\.classes"), "Cc"), + (re.compile(r"Components\.interfaces"), "Ci"), + (re.compile(r"Components\.utils"), "Cu"), + (re.compile(r"Components\.results"), "Cr"), + (re.compile(r"Components\.manager"), "Cm"), + ] +OTHER_CHROME = re.compile(r"Components\.[a-zA-Z]") + +def scan_for_bad_chrome(fn, lines, stderr): + problems = False + old_chrome = set() # i.e. "Cc" when we see "Components.classes" + old_chrome_lines = [] # list of (lineno, line.strip()) tuples + for lineno,line in enumerate(lines): + # note: this scanner is not obligated to spot all possible forms of + # chrome access. The scanner is detecting voluntary requests for + # chrome. Runtime tools will enforce allowance or denial of access. + line = line.strip() + iscomment = False + for commentprefix in COMMENT_PREFIXES: + if line.startswith(commentprefix): + iscomment = True + break + if iscomment: + continue + old_chrome_in_this_line = set() + for (regexp,alias) in CHROME_ALIASES: + if regexp.search(line): + old_chrome_in_this_line.add(alias) + if not old_chrome_in_this_line: + if OTHER_CHROME.search(line): + old_chrome_in_this_line.add("components") + old_chrome.update(old_chrome_in_this_line) + if old_chrome_in_this_line: + old_chrome_lines.append( (lineno+1, line) ) + + if old_chrome: + print >>stderr, """ +The following lines from file %(fn)s: +%(lines)s +use 'Components' to access chrome authority. To do so, you need to add a +line somewhat like the following: + + const {%(needs)s} = require("chrome"); + +Then you can use any shortcuts to its properties that you import from the +'chrome' module ('Cc', 'Ci', 'Cm', 'Cr', and 'Cu' for the 'classes', +'interfaces', 'manager', 'results', and 'utils' properties, respectively. And +`components` for `Components` object itself). +""" % { "fn": fn, "needs": ",".join(sorted(old_chrome)), + "lines": "\n".join([" %3d: %s" % (lineno,line) + for (lineno, line) in old_chrome_lines]), + } + problems = True + return problems + +def scan_module(fn, lines, stderr=sys.stderr): + filename = os.path.basename(fn) + requires, locations = scan_requirements_with_grep(fn, lines) + if filename == "cuddlefish.js": + # this is the loader: don't scan for chrome + problems = False + else: + problems = scan_for_bad_chrome(fn, lines, stderr) + return requires, problems, locations + + + +if __name__ == '__main__': + for fn in sys.argv[1:]: + requires, problems, locations = scan_module(fn, open(fn).readlines()) + print + print "---", fn + if problems: + print "PROBLEMS" + sys.exit(1) + print "requires: %s" % (",".join(sorted(requires.keys()))) + print "locations: %s" % locations diff --git a/addon-sdk/source/python-lib/cuddlefish/mobile-utils/bootstrap.js b/addon-sdk/source/python-lib/cuddlefish/mobile-utils/bootstrap.js new file mode 100644 index 000000000..a0120b0be --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/mobile-utils/bootstrap.js @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +var Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); + +var { log } = console; + +function startup(data, reason) { + // This code allow to make all stdIO work + try { + Cu.import("resource://gre/modules/ctypes.jsm"); + let libdvm = ctypes.open("libdvm.so"); + let dvmStdioConverterStartup; + // Starting with Android ICS, dalvik uses C++. + // So that the symbol isn't a simple C one + try { + dvmStdioConverterStartup = libdvm.declare("_Z24dvmStdioConverterStartupv", ctypes.default_abi, ctypes.bool); + } + catch(e) { + // Otherwise, before ICS, it was a pure C library + dvmStdioConverterStartup = libdvm.declare("dvmStdioConverterStartup", ctypes.default_abi, ctypes.void_t); + } + dvmStdioConverterStartup(); + log("MU: console redirected to adb logcat."); + } catch(e) { + Cu.reportError("MU: unable to execute jsctype hack: "+e); + } + + try { + let QuitObserver = { + observe: function (aSubject, aTopic, aData) { + Services.obs.removeObserver(QuitObserver, "quit-application"); + dump("MU: APPLICATION-QUIT\n"); + } + }; + Services.obs.addObserver(QuitObserver, "quit-application", false); + log("MU: ready to watch firefox exit."); + } catch(e) { + log("MU: unable to register quit-application observer: " + e); + } +} + +function install() {} +function shutdown() {} diff --git a/addon-sdk/source/python-lib/cuddlefish/mobile-utils/install.rdf b/addon-sdk/source/python-lib/cuddlefish/mobile-utils/install.rdf new file mode 100644 index 000000000..0b81a0e5f --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/mobile-utils/install.rdf @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + + +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:id>mobile-utils@mozilla.com</em:id> + <em:version>1.0</em:version> + <em:type>2</em:type> + <em:bootstrap>true</em:bootstrap> + + <!-- Fennec-XUL --> + <em:targetApplication> + <Description> + <em:id>{a23983c0-fd0e-11dc-95ff-0800200c9a66}</em:id> + <em:minVersion>1</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + + <!-- Fennec-NativeUI --> + <em:targetApplication> + <Description> + <em:id>{aa3c5121-dab2-40e2-81ca-7ea25febc110}</em:id> + <em:minVersion>1</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + + <!-- Front End MetaData --> + <em:name>Mobile Addon-SDK utility addon</em:name> + <em:description>Allow better integration with cfx tool.</em:description> + <em:creator>Mozilla Corporation</em:creator> + + </Description> +</RDF> diff --git a/addon-sdk/source/python-lib/cuddlefish/packaging.py b/addon-sdk/source/python-lib/cuddlefish/packaging.py new file mode 100644 index 000000000..0c5357e8e --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/packaging.py @@ -0,0 +1,463 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import sys +import re +import copy + +import simplejson as json +from cuddlefish.bunch import Bunch + +MANIFEST_NAME = 'package.json' +DEFAULT_LOADER = 'addon-sdk' + +# Is different from root_dir when running tests +env_root = os.environ.get('CUDDLEFISH_ROOT') + +DEFAULT_PROGRAM_MODULE = 'main' + +DEFAULT_ICON = 'icon.png' +DEFAULT_ICON64 = 'icon64.png' + +METADATA_PROPS = ['name', 'description', 'keywords', 'author', 'version', + 'developers', 'translators', 'contributors', 'license', 'homepage', + 'icon', 'icon64', 'main', 'directories', 'permissions', 'preferences'] + +RESOURCE_HOSTNAME_RE = re.compile(r'^[a-z0-9_\-]+$') + +class Error(Exception): + pass + +class MalformedPackageError(Error): + pass + +class MalformedJsonFileError(Error): + pass + +class DuplicatePackageError(Error): + pass + +class PackageNotFoundError(Error): + def __init__(self, missing_package, reason): + self.missing_package = missing_package + self.reason = reason + def __str__(self): + return "%s (%s)" % (self.missing_package, self.reason) + +class BadChromeMarkerError(Error): + pass + +def validate_resource_hostname(name): + """ + Validates the given hostname for a resource: URI. + + For more information, see: + + https://bugzilla.mozilla.org/show_bug.cgi?id=566812#c13 + + Examples: + + >>> validate_resource_hostname('blarg') + + >>> validate_resource_hostname('bl arg') + Traceback (most recent call last): + ... + ValueError: Error: the name of your package contains an invalid character. + Package names can contain only lower-case letters, numbers, underscores, and dashes. + Current package name: bl arg + + >>> validate_resource_hostname('BLARG') + Traceback (most recent call last): + ... + ValueError: Error: the name of your package contains upper-case letters. + Package names can contain only lower-case letters, numbers, underscores, and dashes. + Current package name: BLARG + + >>> validate_resource_hostname('foo@bar') + Traceback (most recent call last): + ... + ValueError: Error: the name of your package contains an invalid character. + Package names can contain only lower-case letters, numbers, underscores, and dashes. + Current package name: foo@bar + """ + + # See https://bugzilla.mozilla.org/show_bug.cgi?id=568131 for details. + if not name.lower() == name: + raise ValueError("""Error: the name of your package contains upper-case letters. +Package names can contain only lower-case letters, numbers, underscores, and dashes. +Current package name: %s""" % name) + + if not RESOURCE_HOSTNAME_RE.match(name): + raise ValueError("""Error: the name of your package contains an invalid character. +Package names can contain only lower-case letters, numbers, underscores, and dashes. +Current package name: %s""" % name) + +def find_packages_with_module(pkg_cfg, name): + # TODO: Make this support more than just top-level modules. + filename = "%s.js" % name + packages = [] + for cfg in pkg_cfg.packages.itervalues(): + if 'lib' in cfg: + matches = [dirname for dirname in resolve_dirs(cfg, cfg.lib) + if os.path.exists(os.path.join(dirname, filename))] + if matches: + packages.append(cfg.name) + return packages + +def resolve_dirs(pkg_cfg, dirnames): + for dirname in dirnames: + yield resolve_dir(pkg_cfg, dirname) + +def resolve_dir(pkg_cfg, dirname): + return os.path.join(pkg_cfg.root_dir, dirname) + +def validate_permissions(perms): + if (perms.get('cross-domain-content') and + not isinstance(perms.get('cross-domain-content'), list)): + raise ValueError("Error: `cross-domain-content` permissions in \ + package.json file must be an array of strings:\n %s" % perms) + +def get_metadata(pkg_cfg, deps): + metadata = Bunch() + for pkg_name in deps: + cfg = pkg_cfg.packages[pkg_name] + metadata[pkg_name] = Bunch() + for prop in METADATA_PROPS: + if cfg.get(prop): + if prop == 'permissions': + validate_permissions(cfg[prop]) + metadata[pkg_name][prop] = cfg[prop] + return metadata + +def set_section_dir(base_json, name, base_path, dirnames, allow_root=False): + resolved = compute_section_dir(base_json, base_path, dirnames, allow_root) + if resolved: + base_json[name] = os.path.abspath(resolved) + +def compute_section_dir(base_json, base_path, dirnames, allow_root): + # PACKAGE_JSON.lib is highest priority + # then PACKAGE_JSON.directories.lib + # then lib/ (if it exists) + # then . (but only if allow_root=True) + for dirname in dirnames: + if base_json.get(dirname): + return os.path.join(base_path, base_json[dirname]) + if "directories" in base_json: + for dirname in dirnames: + if dirname in base_json.directories: + return os.path.join(base_path, base_json.directories[dirname]) + for dirname in dirnames: + if os.path.isdir(os.path.join(base_path, dirname)): + return os.path.join(base_path, dirname) + if allow_root: + return os.path.abspath(base_path) + return None + +def normalize_string_or_array(base_json, key): + if base_json.get(key): + if isinstance(base_json[key], basestring): + base_json[key] = [base_json[key]] + +def load_json_file(path): + data = open(path, 'r').read() + try: + return Bunch(json.loads(data)) + except ValueError, e: + raise MalformedJsonFileError('%s when reading "%s"' % (str(e), + path)) + +def get_config_in_dir(path): + package_json = os.path.join(path, MANIFEST_NAME) + if not (os.path.exists(package_json) and + os.path.isfile(package_json)): + raise MalformedPackageError('%s not found in "%s"' % (MANIFEST_NAME, + path)) + base_json = load_json_file(package_json) + + if 'name' not in base_json: + base_json.name = os.path.basename(path) + + # later processing steps will expect to see the following keys in the + # base_json that we return: + # + # name: name of the package + # lib: list of directories with .js files + # test: list of directories with test-*.js files + # doc: list of directories with documentation .md files + # data: list of directories with bundled arbitrary data files + # packages: ? + + if (not base_json.get('tests') and + os.path.isdir(os.path.join(path, 'test'))): + base_json['tests'] = 'test' + + set_section_dir(base_json, 'lib', path, ['lib'], True) + set_section_dir(base_json, 'tests', path, ['test', 'tests'], False) + set_section_dir(base_json, 'doc', path, ['doc', 'docs']) + set_section_dir(base_json, 'data', path, ['data']) + set_section_dir(base_json, 'packages', path, ['packages']) + set_section_dir(base_json, 'locale', path, ['locale']) + + if (not base_json.get('icon') and + os.path.isfile(os.path.join(path, DEFAULT_ICON))): + base_json['icon'] = DEFAULT_ICON + + if (not base_json.get('icon64') and + os.path.isfile(os.path.join(path, DEFAULT_ICON64))): + base_json['icon64'] = DEFAULT_ICON64 + + for key in ['lib', 'tests', 'dependencies', 'packages']: + # TODO: lib/tests can be an array?? consider interaction with + # compute_section_dir above + normalize_string_or_array(base_json, key) + + if 'main' not in base_json and 'lib' in base_json: + for dirname in base_json['lib']: + program = os.path.join(path, dirname, + '%s.js' % DEFAULT_PROGRAM_MODULE) + if os.path.exists(program): + base_json['main'] = DEFAULT_PROGRAM_MODULE + break + + base_json.root_dir = path + + if "dependencies" in base_json: + deps = base_json["dependencies"] + deps = [x for x in deps if x not in ["addon-kit", "api-utils"]] + deps.append("addon-sdk") + base_json["dependencies"] = deps + + return base_json + +def _is_same_file(a, b): + if hasattr(os.path, 'samefile'): + return os.path.samefile(a, b) + return a == b + +def build_config(root_dir, target_cfg, packagepath=[]): + dirs_to_scan = [env_root] # root is addon-sdk dir, diff from root_dir in tests + + def add_packages_from_config(pkgconfig): + if 'packages' in pkgconfig: + for package_dir in resolve_dirs(pkgconfig, pkgconfig.packages): + dirs_to_scan.append(package_dir) + + add_packages_from_config(target_cfg) + + packages_dir = os.path.join(root_dir, 'packages') + if os.path.exists(packages_dir) and os.path.isdir(packages_dir): + dirs_to_scan.append(packages_dir) + dirs_to_scan.extend(packagepath) + + packages = Bunch({target_cfg.name: target_cfg}) + + while dirs_to_scan: + packages_dir = dirs_to_scan.pop() + if os.path.exists(os.path.join(packages_dir, "package.json")): + package_paths = [packages_dir] + else: + package_paths = [os.path.join(packages_dir, dirname) + for dirname in os.listdir(packages_dir) + if not dirname.startswith('.')] + package_paths = [dirname for dirname in package_paths + if os.path.isdir(dirname)] + + for path in package_paths: + pkgconfig = get_config_in_dir(path) + if pkgconfig.name in packages: + otherpkg = packages[pkgconfig.name] + if not _is_same_file(otherpkg.root_dir, path): + raise DuplicatePackageError(path, otherpkg.root_dir) + else: + packages[pkgconfig.name] = pkgconfig + add_packages_from_config(pkgconfig) + + return Bunch(packages=packages) + +def get_deps_for_targets(pkg_cfg, targets): + visited = [] + deps_left = [[dep, None] for dep in list(targets)] + + while deps_left: + [dep, required_by] = deps_left.pop() + if dep not in visited: + visited.append(dep) + if dep not in pkg_cfg.packages: + required_reason = ("required by '%s'" % (required_by)) \ + if required_by is not None \ + else "specified as target" + raise PackageNotFoundError(dep, required_reason) + dep_cfg = pkg_cfg.packages[dep] + deps_left.extend([[i, dep] for i in dep_cfg.get('dependencies', [])]) + deps_left.extend([[i, dep] for i in dep_cfg.get('extra_dependencies', [])]) + + return visited + +def generate_build_for_target(pkg_cfg, target, deps, + include_tests=True, + include_dep_tests=False, + is_running_tests=False, + default_loader=DEFAULT_LOADER): + + build = Bunch(# Contains section directories for all packages: + packages=Bunch(), + locale=Bunch() + ) + + def add_section_to_build(cfg, section, is_code=False, + is_data=False): + if section in cfg: + dirnames = cfg[section] + if isinstance(dirnames, basestring): + # This is just for internal consistency within this + # function, it has nothing to do w/ a non-canonical + # configuration dict. + dirnames = [dirnames] + for dirname in resolve_dirs(cfg, dirnames): + # ensure that package name is valid + try: + validate_resource_hostname(cfg.name) + except ValueError, err: + print err + sys.exit(1) + # ensure that this package has an entry + if not cfg.name in build.packages: + build.packages[cfg.name] = Bunch() + # detect duplicated sections + if section in build.packages[cfg.name]: + raise KeyError("package's section already defined", + cfg.name, section) + # Register this section (lib, data, tests) + build.packages[cfg.name][section] = dirname + + def add_locale_to_build(cfg): + # Bug 730776: Ignore locales for addon-kit, that are only for unit tests + if not is_running_tests and cfg.name == "addon-sdk": + return + + path = resolve_dir(cfg, cfg['locale']) + files = os.listdir(path) + for filename in files: + fullpath = os.path.join(path, filename) + if os.path.isfile(fullpath) and filename.endswith('.properties'): + language = filename[:-len('.properties')] + + from property_parser import parse_file, MalformedLocaleFileError + try: + content = parse_file(fullpath) + except MalformedLocaleFileError, msg: + print msg[0] + sys.exit(1) + + # Merge current locales into global locale hashtable. + # Locale files only contains one big JSON object + # that act as an hastable of: + # "keys to translate" => "translated keys" + if language in build.locale: + merge = (build.locale[language].items() + + content.items()) + build.locale[language] = Bunch(merge) + else: + build.locale[language] = content + + def add_dep_to_build(dep): + dep_cfg = pkg_cfg.packages[dep] + add_section_to_build(dep_cfg, "lib", is_code=True) + add_section_to_build(dep_cfg, "data", is_data=True) + if include_tests and include_dep_tests: + add_section_to_build(dep_cfg, "tests", is_code=True) + if 'locale' in dep_cfg: + add_locale_to_build(dep_cfg) + if ("loader" in dep_cfg) and ("loader" not in build): + build.loader = "%s/%s" % (dep, + dep_cfg.loader) + + target_cfg = pkg_cfg.packages[target] + + if include_tests and not include_dep_tests: + add_section_to_build(target_cfg, "tests", is_code=True) + + for dep in deps: + add_dep_to_build(dep) + + if 'loader' not in build: + add_dep_to_build(DEFAULT_LOADER) + + if 'icon' in target_cfg: + build['icon'] = os.path.join(target_cfg.root_dir, target_cfg.icon) + del target_cfg['icon'] + + if 'icon64' in target_cfg: + build['icon64'] = os.path.join(target_cfg.root_dir, target_cfg.icon64) + del target_cfg['icon64'] + + if 'id' in target_cfg: + # NOTE: logic duplicated from buildJID() + jid = target_cfg['id'] + if not ('@' in jid or jid.startswith('{')): + jid += '@jetpack' + build['preferencesBranch'] = jid + + if 'preferences-branch' in target_cfg: + build['preferencesBranch'] = target_cfg['preferences-branch'] + + return build + +def _get_files_in_dir(path): + data = {} + files = os.listdir(path) + for filename in files: + fullpath = os.path.join(path, filename) + if os.path.isdir(fullpath): + data[filename] = _get_files_in_dir(fullpath) + else: + try: + info = os.stat(fullpath) + data[filename] = ("file", dict(size=info.st_size)) + except OSError: + pass + return ("directory", data) + +def build_pkg_index(pkg_cfg): + pkg_cfg = copy.deepcopy(pkg_cfg) + for pkg in pkg_cfg.packages: + root_dir = pkg_cfg.packages[pkg].root_dir + files = _get_files_in_dir(root_dir) + pkg_cfg.packages[pkg].files = files + try: + readme = open(root_dir + '/README.md').read() + pkg_cfg.packages[pkg].readme = readme + except IOError: + pass + del pkg_cfg.packages[pkg].root_dir + return pkg_cfg.packages + +def build_pkg_cfg(root): + pkg_cfg = build_config(root, Bunch(name='dummy')) + del pkg_cfg.packages['dummy'] + return pkg_cfg + +def call_plugins(pkg_cfg, deps): + for dep in deps: + dep_cfg = pkg_cfg.packages[dep] + dirnames = dep_cfg.get('python-lib', []) + for dirname in resolve_dirs(dep_cfg, dirnames): + sys.path.append(dirname) + module_names = dep_cfg.get('python-plugins', []) + for module_name in module_names: + module = __import__(module_name) + module.init(root_dir=dep_cfg.root_dir) + +def call_cmdline_tool(env_root, pkg_name): + pkg_cfg = build_config(env_root, Bunch(name='dummy')) + if pkg_name not in pkg_cfg.packages: + print "This tool requires the '%s' package." % pkg_name + sys.exit(1) + cfg = pkg_cfg.packages[pkg_name] + for dirname in resolve_dirs(cfg, cfg['python-lib']): + sys.path.append(dirname) + module_name = cfg.get('python-cmdline-tool') + module = __import__(module_name) + module.run() diff --git a/addon-sdk/source/python-lib/cuddlefish/preflight.py b/addon-sdk/source/python-lib/cuddlefish/preflight.py new file mode 100755 index 000000000..c92a0890d --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/preflight.py @@ -0,0 +1,77 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os, sys +import base64 +import simplejson as json + +def create_jid(): + """Return 'jid1-XYZ', where 'XYZ' is a randomly-generated string. (in the + previous jid0- series, the string securely identified a specific public + key). To get a suitable add-on ID, append '@jetpack' to this string. + """ + # per https://developer.mozilla.org/en/Install_Manifests#id all XPI id + # values must either be in the form of a 128-bit GUID (crazy braces + # and all) or in the form of an email address (crazy @ and all). + # Firefox will refuse to install an add-on with an id that doesn't + # match one of these forms. The actual regexp is at: + # http://mxr.mozilla.org/mozilla-central/source/toolkit/mozapps/extensions/internal/XPIProvider.jsm#130 + # So the JID needs an @-suffix, and the only legal punctuation is + # "-._". So we start with a base64 encoding, and replace the + # punctuation (+/) with letters (AB), losing a few bits of integrity. + + # even better: windows has a maximum path length limitation of 256 + # characters: + # http://msdn.microsoft.com/en-us/library/aa365247%28VS.85%29.aspx + # (unless all paths are prefixed with "\\?\", I kid you not). The + # typical install will put add-on code in a directory like: + # C:\Documents and Settings\<username>\Application Data\Mozilla\Firefox\Profiles\232353483.default\extensions\$JID\... + # (which is 108 chars long without the $JID). + # Then the unpacked XPI contains packaged resources like: + # resources/$JID-api-utils-lib/main.js (35 chars plus the $JID) + # + # We create a random 80 bit string, base64 encode that (with + # AB instead of +/ to be path-safe), then bundle it into + # "jid1-XYZ@jetpack". This gives us 27 characters. The resulting + # main.js will have a path length of 211 characters, leaving us 45 + # characters of margin. + # + # 80 bits is enough to generate one billion JIDs and still maintain lower + # than a one-in-a-million chance of accidental collision. (1e9 JIDs is 30 + # bits, square for the "birthday-paradox" to get 60 bits, add 20 bits for + # the one-in-a-million margin to get 80 bits) + + # if length were no issue, we'd prefer to use this: + h = os.urandom(80/8) + s = base64.b64encode(h, "AB").strip("=") + jid = "jid1-" + s + return jid + +def preflight_config(target_cfg, filename, stderr=sys.stderr): + modified = False + config = json.load(open(filename, 'r')) + + if "id" not in config: + print >>stderr, ("No 'id' in package.json: creating a new ID for you.") + jid = create_jid() + config["id"] = jid + modified = True + + if modified: + i = 0 + backup = filename + ".backup" + while os.path.exists(backup): + if i > 1000: + raise ValueError("I'm having problems finding a good name" + " for the backup file. Please move %s out" + " of the way and try again." + % (filename + ".backup")) + backup = filename + ".backup-%d" % i + i += 1 + os.rename(filename, backup) + new_json = json.dumps(config, indent=4) + open(filename, 'w').write(new_json+"\n") + return False, True + + return True, False diff --git a/addon-sdk/source/python-lib/cuddlefish/prefs.py b/addon-sdk/source/python-lib/cuddlefish/prefs.py new file mode 100644 index 000000000..9192eb72c --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/prefs.py @@ -0,0 +1,239 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DEFAULT_COMMON_PREFS = { + # allow debug output via dump to be printed to the system console + # (setting it here just in case, even though PlainTextConsole also + # sets this preference) + 'browser.dom.window.dump.enabled': True, + # warn about possibly incorrect code + 'javascript.options.showInConsole': True, + + # Allow remote connections to the debugger + 'devtools.debugger.remote-enabled' : True, + + 'extensions.sdk.console.logLevel': 'info', + + 'extensions.checkCompatibility.nightly' : False, + + # Disable extension updates and notifications. + 'extensions.update.enabled' : False, + 'lightweightThemes.update.enabled' : False, + 'extensions.update.notifyUser' : False, + + # From: + # http://hg.mozilla.org/mozilla-central/file/1dd81c324ac7/build/automation.py.in#l372 + # Only load extensions from the application and user profile. + # AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION + 'extensions.enabledScopes' : 5, + # Disable metadata caching for installed add-ons by default + 'extensions.getAddons.cache.enabled' : False, + # Disable intalling any distribution add-ons + 'extensions.installDistroAddons' : False, + # Allow installing extensions dropped into the profile folder + 'extensions.autoDisableScopes' : 10, + + # shut up some warnings on `about:` page + 'app.releaseNotesURL': 'http://localhost/app-dummy/', + 'app.vendorURL': 'http://localhost/app-dummy/', +} + +DEFAULT_NO_CONNECTIONS_PREFS = { + 'toolkit.telemetry.enabled': False, + 'toolkit.telemetry.server': 'https://localhost/telemetry-dummy/', + 'app.update.auto' : False, + 'app.update.url': 'http://localhost/app-dummy/update', + # Make sure GMPInstallManager won't hit the network. + 'media.gmp-gmpopenh264.autoupdate' : False, + 'media.gmp-manager.cert.checkAttributes' : False, + 'media.gmp-manager.cert.requireBuiltIn' : False, + 'media.gmp-manager.url' : 'http://localhost/media-dummy/gmpmanager', + 'media.gmp-manager.url.override': 'http://localhost/dummy-gmp-manager.xml', + 'media.gmp-manager.updateEnabled': False, + 'browser.aboutHomeSnippets.updateUrl': 'https://localhost/snippet-dummy', + 'browser.newtab.url' : 'about:blank', + 'browser.search.update': False, + 'browser.search.suggest.enabled' : False, + 'browser.safebrowsing.phishing.enabled' : False, + 'browser.safebrowsing.provider.google.updateURL': 'http://localhost/safebrowsing-dummy/update', + 'browser.safebrowsing.provider.google.gethashURL': 'http://localhost/safebrowsing-dummy/gethash', + 'browser.safebrowsing.malware.reportURL': 'http://localhost/safebrowsing-dummy/malwarereport', + 'browser.selfsupport.url': 'https://localhost/selfsupport-dummy', + 'browser.safebrowsing.provider.mozilla.gethashURL': 'http://localhost/safebrowsing-dummy/gethash', + 'browser.safebrowsing.provider.mozilla.updateURL': 'http://localhost/safebrowsing-dummy/update', + + # Disable app update + 'app.update.enabled' : False, + 'app.update.staging.enabled': False, + + # Disable about:newtab content fetch and ping + 'browser.newtabpage.directory.source': 'data:application/json,{"jetpack":1}', + 'browser.newtabpage.directory.ping': '', + + # Point update checks to a nonexistent local URL for fast failures. + 'extensions.update.url' : 'http://localhost/extensions-dummy/updateURL', + 'extensions.update.background.url': 'http://localhost/extensions-dummy/updateBackgroundURL', + 'extensions.blocklist.url' : 'http://localhost/extensions-dummy/blocklistURL', + # Make sure opening about:addons won't hit the network. + 'extensions.webservice.discoverURL' : 'http://localhost/extensions-dummy/discoveryURL', + 'extensions.getAddons.maxResults': 0, + + # Disable webapp updates. Yes, it is supposed to be an integer. + 'browser.webapps.checkForUpdates': 0, + + # Location services + 'geo.wifi.uri': 'http://localhost/location-dummy/locationURL', + 'browser.search.geoip.url': 'http://localhost/location-dummy/locationURL', + + # Tell the search service we are running in the US. This also has the + # desired side-effect of preventing our geoip lookup. + 'browser.search.isUS' : True, + 'browser.search.countryCode' : 'US', + + 'geo.wifi.uri' : 'http://localhost/extensions-dummy/geowifiURL', + 'geo.wifi.scan' : False, + + # We don't want to hit the real Firefox Accounts server for tests. We don't + # actually need a functioning FxA server, so just set it to something that + # resolves and accepts requests, even if they all fail. + 'identity.fxaccounts.auth.uri': 'http://localhost/fxa-dummy/' +} + +DEFAULT_FENNEC_PREFS = { + 'browser.console.showInPanel': True, + 'browser.firstrun.show.uidiscovery': False +} + +# When launching a temporary new Firefox profile, use these preferences. +DEFAULT_FIREFOX_PREFS = { + 'browser.startup.homepage' : 'about:blank', + 'startup.homepage_welcome_url' : 'about:blank', + 'devtools.browsertoolbox.panel': 'jsdebugger', + 'devtools.chrome.enabled' : True, + + # From: + # http://hg.mozilla.org/mozilla-central/file/1dd81c324ac7/build/automation.py.in#l388 + # Make url-classifier updates so rare that they won't affect tests. + 'urlclassifier.updateinterval' : 172800, + # Point the url-classifier to a nonexistent local URL for fast failures. + 'browser.safebrowsing.provider.google.gethashURL' : 'http://localhost/safebrowsing-dummy/gethash', + 'browser.safebrowsing.provider.google.updateURL' : 'http://localhost/safebrowsing-dummy/update', + 'browser.safebrowsing.provider.mozilla.gethashURL': 'http://localhost/safebrowsing-dummy/gethash', + 'browser.safebrowsing.provider.mozilla.updateURL': 'http://localhost/safebrowsing-dummy/update', +} + +# When launching a temporary new Thunderbird profile, use these preferences. +# Note that these were taken from: +# http://dxr.mozilla.org/comm-central/source/mail/test/mozmill/runtest.py +DEFAULT_THUNDERBIRD_PREFS = { + # say no to slow script warnings + 'dom.max_chrome_script_run_time': 200, + 'dom.max_script_run_time': 0, + # do not ask about being the default mail client + 'mail.shell.checkDefaultClient': False, + # disable non-gloda indexing daemons + 'mail.winsearch.enable': False, + 'mail.winsearch.firstRunDone': True, + 'mail.spotlight.enable': False, + 'mail.spotlight.firstRunDone': True, + # disable address books for undisclosed reasons + 'ldap_2.servers.osx.position': 0, + 'ldap_2.servers.oe.position': 0, + # disable the first use junk dialog + 'mailnews.ui.junk.firstuse': False, + # other unknown voodoo + # -- dummied up local accounts to stop the account wizard + 'mail.account.account1.server' : "server1", + 'mail.account.account2.identities' : "id1", + 'mail.account.account2.server' : "server2", + 'mail.accountmanager.accounts' : "account1,account2", + 'mail.accountmanager.defaultaccount' : "account2", + 'mail.accountmanager.localfoldersserver' : "server1", + 'mail.identity.id1.fullName' : "Tinderbox", + 'mail.identity.id1.smtpServer' : "smtp1", + 'mail.identity.id1.useremail' : "tinderbox@invalid.com", + 'mail.identity.id1.valid' : True, + 'mail.root.none-rel' : "[ProfD]Mail", + 'mail.root.pop3-rel' : "[ProfD]Mail", + 'mail.server.server1.directory-rel' : "[ProfD]Mail/Local Folders", + 'mail.server.server1.hostname' : "Local Folders", + 'mail.server.server1.name' : "Local Folders", + 'mail.server.server1.type' : "none", + 'mail.server.server1.userName' : "nobody", + 'mail.server.server2.check_new_mail' : False, + 'mail.server.server2.directory-rel' : "[ProfD]Mail/tinderbox", + 'mail.server.server2.download_on_biff' : True, + 'mail.server.server2.hostname' : "tinderbox", + 'mail.server.server2.login_at_startup' : False, + 'mail.server.server2.name' : "tinderbox@invalid.com", + 'mail.server.server2.type' : "pop3", + 'mail.server.server2.userName' : "tinderbox", + 'mail.smtp.defaultserver' : "smtp1", + 'mail.smtpserver.smtp1.hostname' : "tinderbox", + 'mail.smtpserver.smtp1.username' : "tinderbox", + 'mail.smtpservers' : "smtp1", + 'mail.startup.enabledMailCheckOnce' : True, + 'mailnews.start_page_override.mstone' : "ignore", +} + +DEFAULT_TEST_PREFS = { + 'browser.console.showInPanel': True, + 'browser.startup.page': 0, + 'browser.firstrun.show.localepicker': False, + 'browser.firstrun.show.uidiscovery': False, + 'browser.ui.layout.tablet': 0, + 'dom.disable_open_during_load': False, + 'dom.experimental_forms': True, + 'dom.forms.number': True, + 'dom.forms.color': True, + 'dom.max_script_run_time': 0, + 'hangmonitor.timeout': 0, + 'dom.max_chrome_script_run_time': 0, + 'dom.popup_maximum': -1, + 'dom.send_after_paint_to_content': True, + 'dom.successive_dialog_time_limit': 0, + 'browser.shell.checkDefaultBrowser': False, + 'shell.checkDefaultClient': False, + 'browser.warnOnQuit': False, + 'accessibility.typeaheadfind.autostart': False, + 'browser.EULA.override': True, + 'gfx.color_management.force_srgb': True, + 'network.manage-offline-status': False, + # Disable speculative connections so they aren't reported as leaking when they're hanging around. + 'network.http.speculative-parallel-limit': 0, + 'test.mousescroll': True, + # Need to client auth test be w/o any dialogs + 'security.default_personal_cert': 'Select Automatically', + 'network.http.prompt-temp-redirect': False, + 'security.warn_viewing_mixed': False, + 'extensions.defaultProviders.enabled': True, + 'datareporting.policy.dataSubmissionPolicyBypassNotification': True, + 'layout.css.report_errors': True, + 'layout.css.grid.enabled': True, + 'layout.spammy_warnings.enabled': False, + 'dom.mozSettings.enabled': True, + # Make sure the disk cache doesn't get auto disabled + 'network.http.bypass-cachelock-threshold': 200000, + # Always use network provider for geolocation tests + # so we bypass the OSX dialog raised by the corelocation provider + 'geo.provider.testing': True, + # Background thumbnails in particular cause grief, and disabling thumbnails + # in general can't hurt - we re-enable them when tests need them. + 'browser.pagethumbnails.capturing_disabled': True, + # Indicate that the download panel has been shown once so that whichever + # download test runs first doesn't show the popup inconsistently. + 'browser.download.panel.shown': True, + # Assume the about:newtab page's intro panels have been shown to not depend on + # which test runs first and happens to open about:newtab + 'browser.newtabpage.introShown': True, + # Disable useragent updates. + 'general.useragent.updates.enabled': False, + 'media.eme.enabled': True, + 'media.eme.apiVisible': True, + # Don't forceably kill content processes after a timeout + 'dom.ipc.tabs.shutdownTimeoutSecs': 0, + 'general.useragent.locale': "en-US", + 'intl.locale.matchOS': "en-US", + 'dom.indexedDB.experimental': True +} diff --git a/addon-sdk/source/python-lib/cuddlefish/property_parser.py b/addon-sdk/source/python-lib/cuddlefish/property_parser.py new file mode 100644 index 000000000..20068a264 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/property_parser.py @@ -0,0 +1,111 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import re +import codecs + +class MalformedLocaleFileError(Exception): + pass + +def parse_file(path): + return parse(read_file(path), path) + +def read_file(path): + try: + return codecs.open( path, "r", "utf-8" ).readlines() + except UnicodeDecodeError, e: + raise MalformedLocaleFileError( + 'Following locale file is not a valid ' + + 'UTF-8 file: %s\n%s"' % (path, str(e))) + +COMMENT = re.compile(r'\s*#') +EMPTY = re.compile(r'^\s+$') +KEYVALUE = re.compile(r"\s*([^=:]+)(=|:)\s*(.*)") + +def parse(lines, path=None): + lines = iter(lines) + lineNo = 1 + pairs = dict() + for line in lines: + if COMMENT.match(line) or EMPTY.match(line) or len(line) == 0: + continue + m = KEYVALUE.match(line) + if not m: + raise MalformedLocaleFileError( + 'Following locale file is not a valid .properties file: %s\n' + 'Line %d is incorrect:\n%s' % (path, lineNo, line)) + + # All spaces are strip. Spaces at the beginning are stripped + # by the regular expression. We have to strip spaces at the end. + key = m.group(1).rstrip() + val = m.group(3).rstrip() + val = val.encode('raw-unicode-escape').decode('raw-unicode-escape') + + # `key` can be empty when key is only made of spaces + if not key: + raise MalformedLocaleFileError( + 'Following locale file is not a valid .properties file: %s\n' + 'Key is invalid on line %d is incorrect:\n%s' % + (path, lineNo, line)) + + # Multiline value: keep reading lines, while lines end with backslash + # and strip spaces at the beginning of lines except the last line + # that doesn't end up with backslash, we strip all spaces for this one. + if val.endswith("\\"): + val = val[:-1] + try: + # remove spaces before/after and especially the \n at EOL + line = lines.next().strip() + while line.endswith("\\"): + val += line[:-1].lstrip() + line = lines.next() + lineNo += 1 + val += line.strip() + except StopIteration: + raise MalformedLocaleFileError( + 'Following locale file is not a valid .properties file: %s\n' + 'Unexpected EOF in multiline sequence at line %d:\n%s' % + (path, lineNo, line)) + # Save this new pair + pairs[key] = val + lineNo += 1 + + normalize_plural(path, pairs) + return pairs + +# Plural forms in properties files are defined like this: +# key = other form +# key[one] = one form +# key[...] = ... +# Parse them and merge each key into one object containing all forms: +# key: { +# other: "other form", +# one: "one form", +# ...: ... +# } +PLURAL_FORM = re.compile(r'^(.*)\[(zero|one|two|few|many|other)\]$') +def normalize_plural(path, pairs): + for key in list(pairs.keys()): + m = PLURAL_FORM.match(key) + if not m: + continue + main_key = m.group(1) + plural_form = m.group(2) + # Allows not specifying a generic key (i.e a key without [form]) + if not main_key in pairs: + pairs[main_key] = {} + # Ensure that we always have the [other] form + if not main_key + "[other]" in pairs: + raise MalformedLocaleFileError( + 'Following locale file is not a valid UTF-8 file: %s\n' + 'This plural form doesn\'t have a matching `%s[other]` form:\n' + '%s\n' + 'You have to defined following key:\n%s' + % (path, main_key, key, main_key)) + # convert generic form into an object if it is still a string + if isinstance(pairs[main_key], unicode): + pairs[main_key] = {"other": pairs[main_key]} + # then, add this new plural form + pairs[main_key][plural_form] = pairs[key] + del pairs[key] diff --git a/addon-sdk/source/python-lib/cuddlefish/rdf.py b/addon-sdk/source/python-lib/cuddlefish/rdf.py new file mode 100644 index 000000000..2964897c1 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/rdf.py @@ -0,0 +1,214 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import xml.dom.minidom +import StringIO + +RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#" +EM_NS = "http://www.mozilla.org/2004/em-rdf#" + +class RDF(object): + def __str__(self): + # real files have an .encoding attribute and use it when you + # write() unicode into them: they read()/write() unicode and + # put encoded bytes in the backend file. StringIO objects + # read()/write() unicode and put unicode in the backing store, + # so we must encode the output of getvalue() to get a + # bytestring. (cStringIO objects are weirder: they effectively + # have .encoding hardwired to "ascii" and put only bytes in + # the backing store, so we can't use them here). + # + # The encoding= argument to dom.writexml() merely sets the XML header's + # encoding= attribute. It still writes unencoded unicode to the output file, + # so we have to encode it for real afterwards. + # + # Also see: https://bugzilla.mozilla.org/show_bug.cgi?id=567660 + + buf = StringIO.StringIO() + self.dom.writexml(buf, encoding="utf-8") + return buf.getvalue().encode('utf-8') + +class RDFUpdate(RDF): + def __init__(self): + impl = xml.dom.minidom.getDOMImplementation() + self.dom = impl.createDocument(RDF_NS, "RDF", None) + self.dom.documentElement.setAttribute("xmlns", RDF_NS) + self.dom.documentElement.setAttribute("xmlns:em", EM_NS) + + def _make_node(self, name, value, parent): + elem = self.dom.createElement(name) + elem.appendChild(self.dom.createTextNode(value)) + parent.appendChild(elem) + return elem + + def add(self, manifest, update_link): + desc = self.dom.createElement("Description") + desc.setAttribute( + "about", + "urn:mozilla:extension:%s" % manifest.get("em:id") + ) + self.dom.documentElement.appendChild(desc) + + updates = self.dom.createElement("em:updates") + desc.appendChild(updates) + + seq = self.dom.createElement("Seq") + updates.appendChild(seq) + + li = self.dom.createElement("li") + seq.appendChild(li) + + li_desc = self.dom.createElement("Description") + li.appendChild(li_desc) + + self._make_node("em:version", manifest.get("em:version"), + li_desc) + + apps = manifest.dom.documentElement.getElementsByTagName( + "em:targetApplication" + ) + + for app in apps: + target_app = self.dom.createElement("em:targetApplication") + li_desc.appendChild(target_app) + + ta_desc = self.dom.createElement("Description") + target_app.appendChild(ta_desc) + + for name in ["em:id", "em:minVersion", "em:maxVersion"]: + elem = app.getElementsByTagName(name)[0] + self._make_node(name, elem.firstChild.nodeValue, ta_desc) + + self._make_node("em:updateLink", update_link, ta_desc) + +class RDFManifest(RDF): + def __init__(self, path): + self.dom = xml.dom.minidom.parse(path) + + def set(self, property, value): + elements = self.dom.documentElement.getElementsByTagName(property) + if not elements: + raise ValueError("Element with value not found: %s" % property) + if not elements[0].firstChild: + elements[0].appendChild(self.dom.createTextNode(value)) + else: + elements[0].firstChild.nodeValue = value + + def get(self, property, default=None): + elements = self.dom.documentElement.getElementsByTagName(property) + if not elements: + return default + return elements[0].firstChild.nodeValue + + def remove(self, property): + elements = self.dom.documentElement.getElementsByTagName(property) + if not elements: + return True + else: + for i in elements: + i.parentNode.removeChild(i); + + return True; + +def gen_manifest(template_root_dir, target_cfg, jid, + update_url=None, bootstrap=True, enable_mobile=False): + install_rdf = os.path.join(template_root_dir, "install.rdf") + manifest = RDFManifest(install_rdf) + dom = manifest.dom + + manifest.set("em:id", jid) + manifest.set("em:version", + target_cfg.get('version', '1.0')) + manifest.set("em:name", + target_cfg.get('title', target_cfg.get('fullName', target_cfg['name']))) + manifest.set("em:description", + target_cfg.get("description", "")) + manifest.set("em:creator", + target_cfg.get("author", "")) + manifest.set("em:bootstrap", str(bootstrap).lower()) + # XPIs remain packed by default, but package.json can override that. The + # RDF format accepts "true" as True, anything else as False. We expect + # booleans in the .json file, not strings. + manifest.set("em:unpack", "true" if target_cfg.get("unpack") else "false") + + if target_cfg.get('hasEmbeddedWebExtension', False): + elem = dom.createElement("em:hasEmbeddedWebExtension"); + elem.appendChild(dom.createTextNode("true")) + dom.documentElement.getElementsByTagName("Description")[0].appendChild(elem) + + for translator in target_cfg.get("translators", [ ]): + elem = dom.createElement("em:translator"); + elem.appendChild(dom.createTextNode(translator)) + dom.documentElement.getElementsByTagName("Description")[0].appendChild(elem) + + for developer in target_cfg.get("developers", [ ]): + elem = dom.createElement("em:developer"); + elem.appendChild(dom.createTextNode(developer)) + dom.documentElement.getElementsByTagName("Description")[0].appendChild(elem) + + for contributor in target_cfg.get("contributors", [ ]): + elem = dom.createElement("em:contributor"); + elem.appendChild(dom.createTextNode(contributor)) + dom.documentElement.getElementsByTagName("Description")[0].appendChild(elem) + + if update_url: + manifest.set("em:updateURL", update_url) + else: + manifest.remove("em:updateURL") + + if target_cfg.get("preferences"): + manifest.set("em:optionsType", "2") + + # workaround until bug 971249 is fixed + # https://bugzilla.mozilla.org/show_bug.cgi?id=971249 + manifest.set("em:optionsURL", "data:text/xml,<placeholder/>") + + # workaround for workaround, for testing simple-prefs-regression + if (os.path.exists(os.path.join(template_root_dir, "options.xul"))): + manifest.remove("em:optionsURL") + else: + manifest.remove("em:optionsType") + manifest.remove("em:optionsURL") + + if enable_mobile: + target_app = dom.createElement("em:targetApplication") + dom.documentElement.getElementsByTagName("Description")[0].appendChild(target_app) + + ta_desc = dom.createElement("Description") + target_app.appendChild(ta_desc) + + elem = dom.createElement("em:id") + elem.appendChild(dom.createTextNode("{aa3c5121-dab2-40e2-81ca-7ea25febc110}")) + ta_desc.appendChild(elem) + + elem = dom.createElement("em:minVersion") + elem.appendChild(dom.createTextNode("26.0")) + ta_desc.appendChild(elem) + + elem = dom.createElement("em:maxVersion") + elem.appendChild(dom.createTextNode("30.0a1")) + ta_desc.appendChild(elem) + + if target_cfg.get("homepage"): + manifest.set("em:homepageURL", target_cfg.get("homepage")) + else: + manifest.remove("em:homepageURL") + + return manifest + +if __name__ == "__main__": + print "Running smoke test." + root = os.path.join(os.path.dirname(__file__), '../../app-extension') + manifest = gen_manifest(root, {'name': 'test extension'}, + 'fakeid', 'http://foo.com/update.rdf') + update = RDFUpdate() + update.add(manifest, "https://foo.com/foo.xpi") + exercise_str = str(manifest) + str(update) + for tagname in ["em:targetApplication", "em:version", "em:id"]: + if not len(update.dom.getElementsByTagName(tagname)): + raise Exception("tag does not exist: %s" % tagname) + if not update.dom.getElementsByTagName(tagname)[0].firstChild: + raise Exception("tag has no children: %s" % tagname) + print "Success!" diff --git a/addon-sdk/source/python-lib/cuddlefish/runner.py b/addon-sdk/source/python-lib/cuddlefish/runner.py new file mode 100644 index 000000000..7f337cc03 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/runner.py @@ -0,0 +1,767 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import sys +import time +import tempfile +import atexit +import shlex +import subprocess +import re +import shutil + +import mozrunner +from cuddlefish.prefs import DEFAULT_COMMON_PREFS +from cuddlefish.prefs import DEFAULT_FIREFOX_PREFS +from cuddlefish.prefs import DEFAULT_THUNDERBIRD_PREFS +from cuddlefish.prefs import DEFAULT_FENNEC_PREFS +from cuddlefish.prefs import DEFAULT_NO_CONNECTIONS_PREFS +from cuddlefish.prefs import DEFAULT_TEST_PREFS + +# Used to remove noise from ADB output +CLEANUP_ADB = re.compile(r'^(I|E)/(stdout|stderr|GeckoConsole)\s*\(\s*\d+\):\s*(.*)$') +# Used to filter only messages send by `console` module +FILTER_ONLY_CONSOLE_FROM_ADB = re.compile(r'^I/(stdout|stderr)\s*\(\s*\d+\):\s*((info|warning|error|debug): .*)$') + +# Used to detect the currently running test +PARSEABLE_TEST_NAME = re.compile(r'TEST-START \| ([^\n]+)\n') + +# Maximum time we'll wait for tests to finish, in seconds. +# The purpose of this timeout is to recover from infinite loops. It should be +# longer than the amount of time any test run takes, including those on slow +# machines running slow (debug) versions of Firefox. +RUN_TIMEOUT = 5400 #1.5 hours (1.5 * 60 * 60 sec) + +# Maximum time we'll wait for tests to emit output, in seconds. +# The purpose of this timeout is to recover from hangs. It should be longer +# than the amount of time any test takes to report results. +OUTPUT_TIMEOUT = 300 #five minutes (60 * 5 sec) + +def follow_file(filename): + """ + Generator that yields the latest unread content from the given + file, or None if no new content is available. + + For example: + + >>> f = open('temp.txt', 'w') + >>> f.write('hello') + >>> f.flush() + >>> tail = follow_file('temp.txt') + >>> tail.next() + 'hello' + >>> tail.next() is None + True + >>> f.write('there') + >>> f.flush() + >>> tail.next() + 'there' + >>> f.close() + >>> os.remove('temp.txt') + """ + + last_pos = 0 + last_size = 0 + while True: + newstuff = None + if os.path.exists(filename): + size = os.stat(filename).st_size + if size > last_size: + last_size = size + f = open(filename, 'r') + f.seek(last_pos) + newstuff = f.read() + last_pos = f.tell() + f.close() + yield newstuff + +# subprocess.check_output only appeared in python2.7, so this code is taken +# from python source code for compatibility with py2.5/2.6 +class CalledProcessError(Exception): + def __init__(self, returncode, cmd, output=None): + self.returncode = returncode + self.cmd = cmd + self.output = output + def __str__(self): + return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode) + +def check_output(*popenargs, **kwargs): + if 'stdout' in kwargs: + raise ValueError('stdout argument not allowed, it will be overridden.') + process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs) + output, unused_err = process.communicate() + retcode = process.poll() + if retcode: + cmd = kwargs.get("args") + if cmd is None: + cmd = popenargs[0] + raise CalledProcessError(retcode, cmd, output=output) + return output + + +class FennecProfile(mozrunner.Profile): + preferences = {} + names = ['fennec'] + +FENNEC_REMOTE_PATH = '/mnt/sdcard/jetpack-profile' + +class RemoteFennecRunner(mozrunner.Runner): + profile_class = FennecProfile + + names = ['fennec'] + + _INTENT_PREFIX = 'org.mozilla.' + + _adb_path = None + + def __init__(self, binary=None, **kwargs): + # Check that we have a binary set + if not binary: + raise ValueError("You have to define `--binary` option set to the " + "path to your ADB executable.") + # Ensure that binary refer to a valid ADB executable + output = subprocess.Popen([binary], stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate() + output = "".join(output) + if not ("Android Debug Bridge" in output): + raise ValueError("`--binary` option should be the path to your " + "ADB executable.") + self.binary = binary + + mobile_app_name = kwargs['cmdargs'][0] + self.profile = kwargs['profile'] + self._adb_path = binary + + # This pref has to be set to `false` otherwise, we do not receive + # output of adb commands! + subprocess.call([self._adb_path, "shell", + "setprop log.redirect-stdio false"]) + + # Android apps are launched by their "intent" name, + # Automatically detect already installed firefox by using `pm` program + # or use name given as cfx `--mobile-app` argument. + intents = self.getIntentNames() + if not intents: + raise ValueError("Unable to find any Firefox " + "application on your device.") + elif mobile_app_name: + if not mobile_app_name in intents: + raise ValueError("Unable to find Firefox application " + "with intent name '%s'\n" + "Available ones are: %s" % + (mobile_app_name, ", ".join(intents))) + self._intent_name = self._INTENT_PREFIX + mobile_app_name + else: + if "firefox" in intents: + self._intent_name = self._INTENT_PREFIX + "firefox" + elif "firefox_beta" in intents: + self._intent_name = self._INTENT_PREFIX + "firefox_beta" + elif "firefox_nightly" in intents: + self._intent_name = self._INTENT_PREFIX + "firefox_nightly" + else: + self._intent_name = self._INTENT_PREFIX + intents[0] + + print "Launching mobile application with intent name " + self._intent_name + + # First try to kill firefox if it is already running + pid = self.getProcessPID(self._intent_name) + if pid != None: + print "Killing running Firefox instance ..." + subprocess.call([self._adb_path, "shell", + "am force-stop " + self._intent_name]) + time.sleep(7) + # It appears recently that the PID still exists even after + # Fennec closes, so removing this error still allows the tests + # to pass as the new Fennec instance is able to start. + # Leaving error in but commented out for now. + # + #if self.getProcessPID(self._intent_name) != None: + # raise Exception("Unable to automatically kill running Firefox" + + # " instance. Please close it manually before " + + # "executing cfx.") + + print "Pushing the addon to your device" + + # Create a clean empty profile on the sd card + subprocess.call([self._adb_path, "shell", "rm -r " + FENNEC_REMOTE_PATH]) + subprocess.call([self._adb_path, "shell", "mkdir " + FENNEC_REMOTE_PATH]) + + # Push the profile folder created by mozrunner to the device + # (we can't simply use `adb push` as it doesn't copy empty folders) + localDir = self.profile.profile + remoteDir = FENNEC_REMOTE_PATH + for root, dirs, files in os.walk(localDir, followlinks='true'): + relRoot = os.path.relpath(root, localDir) + # Note about os.path usage below: + # Local files may be using Windows `\` separators but + # remote are always `/`, so we need to convert local ones to `/` + for file in files: + localFile = os.path.join(root, file) + remoteFile = remoteDir.replace("/", os.sep) + if relRoot != ".": + remoteFile = os.path.join(remoteFile, relRoot) + remoteFile = os.path.join(remoteFile, file) + remoteFile = "/".join(remoteFile.split(os.sep)) + subprocess.Popen([self._adb_path, "push", localFile, remoteFile], + stderr=subprocess.PIPE).wait() + for dir in dirs: + targetDir = remoteDir.replace("/", os.sep) + if relRoot != ".": + targetDir = os.path.join(targetDir, relRoot) + targetDir = os.path.join(targetDir, dir) + targetDir = "/".join(targetDir.split(os.sep)) + # `-p` option is not supported on all devices! + subprocess.call([self._adb_path, "shell", "mkdir " + targetDir]) + + @property + def command(self): + """Returns the command list to run.""" + return [self._adb_path, + "shell", + "am start " + + "-a android.activity.MAIN " + + "-n " + self._intent_name + "/" + self._intent_name + ".App " + + "--es args \"-profile " + FENNEC_REMOTE_PATH + "\"" + ] + + def start(self): + subprocess.call(self.command) + + def getProcessPID(self, processName): + p = subprocess.Popen([self._adb_path, "shell", "ps"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + line = p.stdout.readline() + while line: + columns = line.split() + pid = columns[1] + name = columns[-1] + line = p.stdout.readline() + if processName in name: + return pid + return None + + def getIntentNames(self): + p = subprocess.Popen([self._adb_path, "shell", "pm list packages"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + names = [] + for line in p.stdout.readlines(): + line = re.sub("(^package:)|\s", "", line) + if self._INTENT_PREFIX in line: + names.append(line.replace(self._INTENT_PREFIX, "")) + return names + + +class XulrunnerAppProfile(mozrunner.Profile): + preferences = {} + names = [] + +class XulrunnerAppRunner(mozrunner.Runner): + """ + Runner for any XULRunner app. Can use a Firefox binary in XULRunner + mode to execute the app, or can use XULRunner itself. Expects the + app's application.ini to be passed in as one of the items in + 'cmdargs' in the constructor. + + This class relies a lot on the particulars of mozrunner.Runner's + implementation, and does some unfortunate acrobatics to get around + some of the class' limitations/assumptions. + """ + + profile_class = XulrunnerAppProfile + + # This is a default, and will be overridden in the instance if + # Firefox is used in XULRunner mode. + names = ['xulrunner'] + + # Default location of XULRunner on OS X. + __DARWIN_PATH = "/Library/Frameworks/XUL.framework/xulrunner-bin" + __LINUX_PATH = "/usr/bin/xulrunner" + + # What our application.ini's path looks like if it's part of + # an "installed" XULRunner app on OS X. + __DARWIN_APP_INI_SUFFIX = '.app/Contents/Resources/application.ini' + + def __init__(self, binary=None, **kwargs): + if sys.platform == 'darwin' and binary and binary.endswith('.app'): + # Assume it's a Firefox app dir. + binary = os.path.join(binary, 'Contents/MacOS/firefox-bin') + + self.__app_ini = None + self.__real_binary = binary + + mozrunner.Runner.__init__(self, **kwargs) + + # See if we're using a genuine xulrunner-bin from the XULRunner SDK, + # or if we're being asked to use Firefox in XULRunner mode. + self.__is_xulrunner_sdk = 'xulrunner' in self.binary + + if sys.platform == 'linux2' and not self.env.get('LD_LIBRARY_PATH'): + self.env['LD_LIBRARY_PATH'] = os.path.dirname(self.binary) + + newargs = [] + for item in self.cmdargs: + if 'application.ini' in item: + self.__app_ini = item + else: + newargs.append(item) + self.cmdargs = newargs + + if not self.__app_ini: + raise ValueError('application.ini not found in cmdargs') + if not os.path.exists(self.__app_ini): + raise ValueError("file does not exist: '%s'" % self.__app_ini) + + if (sys.platform == 'darwin' and + self.binary == self.__DARWIN_PATH and + self.__app_ini.endswith(self.__DARWIN_APP_INI_SUFFIX)): + # If the application.ini is in an app bundle, then + # it could be inside an "installed" XULRunner app. + # If this is the case, use the app's actual + # binary instead of the XUL framework's, so we get + # a proper app icon, etc. + new_binary = '/'.join(self.__app_ini.split('/')[:-2] + + ['MacOS', 'xulrunner']) + if os.path.exists(new_binary): + self.binary = new_binary + + @property + def command(self): + """Returns the command list to run.""" + + if self.__is_xulrunner_sdk: + return [self.binary, self.__app_ini, '-profile', + self.profile.profile] + else: + return [self.binary, '-app', self.__app_ini, '-profile', + self.profile.profile] + + def __find_xulrunner_binary(self): + if sys.platform == 'darwin': + if os.path.exists(self.__DARWIN_PATH): + return self.__DARWIN_PATH + if sys.platform == 'linux2': + if os.path.exists(self.__LINUX_PATH): + return self.__LINUX_PATH + return None + + def find_binary(self): + # This gets called by the superclass constructor. It will + # always get called, even if a binary was passed into the + # constructor, because we want to have full control over + # what the exact setting of self.binary is. + + if not self.__real_binary: + self.__real_binary = self.__find_xulrunner_binary() + if not self.__real_binary: + dummy_profile = {} + runner = mozrunner.FirefoxRunner(profile=dummy_profile) + self.__real_binary = runner.find_binary() + self.names = runner.names + return self.__real_binary + +def set_overloaded_modules(env_root, app_type, addon_id, preferences, overloads): + # win32 file scheme needs 3 slashes + desktop_file_scheme = "file://" + if not env_root.startswith("/"): + desktop_file_scheme = desktop_file_scheme + "/" + + pref_prefix = "extensions.modules." + addon_id + ".path" + + # Set preferences that will map require prefix to a given path + for name, path in overloads.items(): + if len(name) == 0: + prefName = pref_prefix + else: + prefName = pref_prefix + "." + name + if app_type == "fennec-on-device": + # For testing on device, we have to copy overloaded files from fs + # to the device and use device path instead of local fs path. + # Actual copy of files if done after the call to Profile constructor + preferences[prefName] = "file://" + \ + FENNEC_REMOTE_PATH + "/overloads/" + name + else: + preferences[prefName] = desktop_file_scheme + \ + path.replace("\\", "/") + "/" + +def run_app(harness_root_dir, manifest_rdf, harness_options, + app_type, binary=None, profiledir=None, verbose=False, + parseable=False, enforce_timeouts=False, + logfile=None, addons=None, args=None, extra_environment={}, + norun=None, noquit=None, + used_files=None, enable_mobile=False, + mobile_app_name=None, + env_root=None, + is_running_tests=False, + overload_modules=False, + bundle_sdk=True, + pkgdir="", + enable_e10s=False, + no_connections=False): + if binary: + binary = os.path.expanduser(binary) + + if addons is None: + addons = [] + else: + addons = list(addons) + + cmdargs = [] + preferences = dict(DEFAULT_COMMON_PREFS) + + if is_running_tests: + preferences.update(DEFAULT_TEST_PREFS) + + if no_connections: + preferences.update(DEFAULT_NO_CONNECTIONS_PREFS) + + if enable_e10s: + preferences['browser.tabs.remote.autostart'] = True + else: + preferences['browser.tabs.remote.autostart'] = False + preferences['browser.tabs.remote.autostart.1'] = False + preferences['browser.tabs.remote.autostart.2'] = False + + # For now, only allow running on Mobile with --force-mobile argument + if app_type in ["fennec-on-device"] and not enable_mobile: + print """ + WARNING: Firefox Mobile support is still experimental. + If you would like to run an addon on this platform, use --force-mobile flag: + + cfx --force-mobile""" + return 0 + + if app_type == "fennec-on-device": + profile_class = FennecProfile + preferences.update(DEFAULT_FENNEC_PREFS) + runner_class = RemoteFennecRunner + # We pass the intent name through command arguments + cmdargs.append(mobile_app_name) + elif app_type == "xulrunner": + profile_class = XulrunnerAppProfile + runner_class = XulrunnerAppRunner + cmdargs.append(os.path.join(harness_root_dir, 'application.ini')) + elif app_type == "firefox": + profile_class = mozrunner.FirefoxProfile + preferences.update(DEFAULT_FIREFOX_PREFS) + runner_class = mozrunner.FirefoxRunner + elif app_type == "thunderbird": + profile_class = mozrunner.ThunderbirdProfile + preferences.update(DEFAULT_THUNDERBIRD_PREFS) + runner_class = mozrunner.ThunderbirdRunner + else: + raise ValueError("Unknown app: %s" % app_type) + if sys.platform == 'darwin' and app_type != 'xulrunner': + cmdargs.append('-foreground') + + if args: + cmdargs.extend(shlex.split(args)) + + # TODO: handle logs on remote device + if app_type != "fennec-on-device": + # tempfile.gettempdir() was constant, preventing two simultaneous "cfx + # run"/"cfx test" on the same host. On unix it points at /tmp (which is + # world-writeable), enabling a symlink attack (e.g. imagine some bad guy + # does 'ln -s ~/.ssh/id_rsa /tmp/harness_result'). NamedTemporaryFile + # gives us a unique filename that fixes both problems. We leave the + # (0-byte) file in place until the browser-side code starts writing to + # it, otherwise the symlink attack becomes possible again. + fileno,resultfile = tempfile.mkstemp(prefix="harness-result-") + os.close(fileno) + harness_options['resultFile'] = resultfile + + def maybe_remove_logfile(): + if os.path.exists(logfile): + os.remove(logfile) + + logfile_tail = None + + # We always buffer output through a logfile for two reasons: + # 1. On Windows, it's the only way to print console output to stdout/err. + # 2. It enables us to keep track of the last time output was emitted, + # so we can raise an exception if the test runner hangs. + if not logfile: + fileno,logfile = tempfile.mkstemp(prefix="harness-log-") + os.close(fileno) + logfile_tail = follow_file(logfile) + atexit.register(maybe_remove_logfile) + + logfile = os.path.abspath(os.path.expanduser(logfile)) + maybe_remove_logfile() + + env = {} + env.update(os.environ) + if no_connections: + env['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = '1' + env['MOZ_NO_REMOTE'] = '1' + env['XPCOM_DEBUG_BREAK'] = 'stack' + env.update(extra_environment) + if norun: + cmdargs.append("-no-remote") + + # Create the addon XPI so mozrunner will copy it to the profile it creates. + # We delete it below after getting mozrunner to create the profile. + from cuddlefish.xpi import build_xpi + xpi_path = tempfile.mktemp(suffix='cfx-tmp.xpi') + build_xpi(template_root_dir=harness_root_dir, + manifest=manifest_rdf, + xpi_path=xpi_path, + harness_options=harness_options, + limit_to=used_files, + bundle_sdk=bundle_sdk, + pkgdir=pkgdir) + addons.append(xpi_path) + + starttime = last_output_time = time.time() + + # Redirect runner output to a file so we can catch output not generated + # by us. + # In theory, we could do this using simple redirection on all platforms + # other than Windows, but this way we only have a single codepath to + # maintain. + fileno,outfile = tempfile.mkstemp(prefix="harness-stdout-") + os.close(fileno) + outfile_tail = follow_file(outfile) + def maybe_remove_outfile(): + if os.path.exists(outfile): + try: + os.remove(outfile) + except Exception, e: + print "Error Cleaning up: " + str(e) + atexit.register(maybe_remove_outfile) + outf = open(outfile, "w") + popen_kwargs = { 'stdout': outf, 'stderr': outf} + + profile = None + + if app_type == "fennec-on-device": + # Install a special addon when we run firefox on mobile device + # in order to be able to kill it + mydir = os.path.dirname(os.path.abspath(__file__)) + addon_dir = os.path.join(mydir, "mobile-utils") + addons.append(addon_dir) + + # Overload addon-specific commonjs modules path with lib/ folder + overloads = dict() + if overload_modules: + overloads[""] = os.path.join(env_root, "lib") + + # Overload tests/ mapping with test/ folder, only when running test + if is_running_tests: + overloads["tests"] = os.path.join(env_root, "test") + + set_overloaded_modules(env_root, app_type, harness_options["jetpackID"], \ + preferences, overloads) + + # the XPI file is copied into the profile here + profile = profile_class(addons=addons, + profile=profiledir, + preferences=preferences) + + # Delete the temporary xpi file + os.remove(xpi_path) + + # Copy overloaded files registered in set_overloaded_modules + # For testing on device, we have to copy overloaded files from fs + # to the device and use device path instead of local fs path. + # (has to be done after the call to profile_class() which eventualy creates + # profile folder) + if app_type == "fennec-on-device": + profile_path = profile.profile + for name, path in overloads.items(): + shutil.copytree(path, \ + os.path.join(profile_path, "overloads", name)) + + runner = runner_class(profile=profile, + binary=binary, + env=env, + cmdargs=cmdargs, + kp_kwargs=popen_kwargs) + + sys.stdout.flush(); sys.stderr.flush() + + if app_type == "fennec-on-device": + if not enable_mobile: + print >>sys.stderr, """ + WARNING: Firefox Mobile support is still experimental. + If you would like to run an addon on this platform, use --force-mobile flag: + + cfx --force-mobile""" + return 0 + + # In case of mobile device, we need to get stdio from `adb logcat` cmd: + + # First flush logs in order to avoid catching previous ones + subprocess.call([binary, "logcat", "-c"]) + + # Launch adb command + runner.start() + + # We can immediatly remove temporary profile folder + # as it has been uploaded to the device + profile.cleanup() + # We are not going to use the output log file + outf.close() + + # Then we simply display stdout of `adb logcat` + p = subprocess.Popen([binary, "logcat", "stderr:V stdout:V GeckoConsole:V *:S"], stdout=subprocess.PIPE) + while True: + line = p.stdout.readline() + if line == '': + break + # mobile-utils addon contains an application quit event observer + # that will print this string: + if "APPLICATION-QUIT" in line: + break + + if verbose: + # if --verbose is given, we display everything: + # All JS Console messages, stdout and stderr. + m = CLEANUP_ADB.match(line) + if not m: + print line.rstrip() + continue + print m.group(3) + else: + # Otherwise, display addons messages dispatched through + # console.[info, log, debug, warning, error](msg) + m = FILTER_ONLY_CONSOLE_FROM_ADB.match(line) + if m: + print m.group(2) + + print >>sys.stderr, "Program terminated successfully." + return 0 + + + print >>sys.stderr, "Using binary at '%s'." % runner.binary + + # Ensure cfx is being used with Firefox 4.0+. + # TODO: instead of dying when Firefox is < 4, warn when Firefox is outside + # the minVersion/maxVersion boundaries. + version_output = check_output(runner.command + ["-v"]) + # Note: this regex doesn't handle all valid versions in the Toolkit Version + # Format <https://developer.mozilla.org/en/Toolkit_version_format>, just the + # common subset that we expect Mozilla apps to use. + mo = re.search(r"Mozilla (Firefox|Iceweasel|Fennec)\b[^ ]* ((\d+)\.\S*)", + version_output) + if not mo: + # cfx may be used with Thunderbird, SeaMonkey or an exotic Firefox + # version. + print """ + WARNING: cannot determine Firefox version; please ensure you are running + a Mozilla application equivalent to Firefox 4.0 or greater. + """ + elif mo.group(1) == "Fennec": + # For now, only allow running on Mobile with --force-mobile argument + if not enable_mobile: + print """ + WARNING: Firefox Mobile support is still experimental. + If you would like to run an addon on this platform, use --force-mobile flag: + + cfx --force-mobile""" + return + else: + version = mo.group(3) + if int(version) < 4: + print """ + cfx requires Firefox 4 or greater and is unable to find a compatible + binary. Please install a newer version of Firefox or provide the path to + your existing compatible version with the --binary flag: + + cfx --binary=PATH_TO_FIREFOX_BINARY""" + return + + # Set the appropriate extensions.checkCompatibility preference to false, + # so the tests run even if the SDK is not marked as compatible with the + # version of Firefox on which they are running, and we don't have to + # ensure we update the maxVersion before the version of Firefox changes + # every six weeks. + # + # The regex we use here is effectively the same as BRANCH_REGEX from + # /toolkit/mozapps/extensions/content/extensions.js, which toolkit apps + # use to determine whether or not to load an incompatible addon. + # + br = re.search(r"^([^\.]+\.[0-9]+[a-z]*).*", mo.group(2), re.I) + if br: + prefname = 'extensions.checkCompatibility.' + br.group(1) + profile.preferences[prefname] = False + # Calling profile.set_preferences here duplicates the list of prefs + # in prefs.js, since the profile calls self.set_preferences in its + # constructor, but that is ok, because it doesn't change the set of + # preferences that are ultimately registered in Firefox. + profile.set_preferences(profile.preferences) + + print >>sys.stderr, "Using profile at '%s'." % profile.profile + sys.stderr.flush() + + if norun: + print "To launch the application, enter the following command:" + print " ".join(runner.command) + " " + (" ".join(runner.cmdargs)) + return 0 + + runner.start() + + done = False + result = None + test_name = "Jetpack startup" + + def Timeout(message, test_name, parseable): + if parseable: + sys.stderr.write("TEST-UNEXPECTED-FAIL | %s | %s\n" % (test_name, message)) + sys.stderr.flush() + return Exception(message) + + try: + while not done: + time.sleep(0.05) + for tail in (logfile_tail, outfile_tail): + if tail: + new_chars = tail.next() + if new_chars: + last_output_time = time.time() + sys.stderr.write(new_chars) + sys.stderr.flush() + if is_running_tests and parseable: + match = PARSEABLE_TEST_NAME.search(new_chars) + if match: + test_name = match.group(1) + if os.path.exists(resultfile): + result = open(resultfile).read() + if result: + if result in ['OK', 'FAIL']: + done = True + else: + sys.stderr.write("Hrm, resultfile (%s) contained something weird (%d bytes)\n" % (resultfile, len(result))) + sys.stderr.write("'"+result+"'\n") + if enforce_timeouts: + if time.time() - last_output_time > OUTPUT_TIMEOUT: + raise Timeout("Test output exceeded timeout (%ds)." % + OUTPUT_TIMEOUT, test_name, parseable) + if time.time() - starttime > RUN_TIMEOUT: + raise Timeout("Test run exceeded timeout (%ds)." % + RUN_TIMEOUT, test_name, parseable) + except: + if not noquit: + runner.stop() + raise + else: + runner.wait(10) + # double kill - hack for bugs 942111, 1006043.. + try: + runner.stop() + except: + pass + finally: + outf.close() + if profile: + profile.cleanup() + + print >>sys.stderr, "Total time: %f seconds" % (time.time() - starttime) + + if result == 'OK': + print >>sys.stderr, "Program terminated successfully." + return 0 + else: + print >>sys.stderr, "Program terminated unsuccessfully." + return -1 diff --git a/addon-sdk/source/python-lib/cuddlefish/templates.py b/addon-sdk/source/python-lib/cuddlefish/templates.py new file mode 100644 index 000000000..be6110e7a --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/templates.py @@ -0,0 +1,32 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#Template used by test-main.js +TEST_MAIN_JS = '''\ +var main = require("./main"); + +exports["test main"] = function(assert) { + assert.pass("Unit test running!"); +}; + +exports["test main async"] = function(assert, done) { + assert.pass("async Unit test running!"); + done(); +}; + +require("sdk/test").run(exports); +''' + +#Template used by package.json +PACKAGE_JSON = '''\ +{ + "name": "%(name)s", + "title": "%(title)s", + "id": "%(id)s", + "description": "a basic add-on", + "author": "", + "license": "MPL-2.0", + "version": "0.1" +} +''' diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/__init__.py b/addon-sdk/source/python-lib/cuddlefish/tests/__init__.py new file mode 100644 index 000000000..60e7e88b3 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/__init__.py @@ -0,0 +1,52 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import unittest +import doctest +import glob + +env_root = os.environ['CUDDLEFISH_ROOT'] + +def get_tests(): + import cuddlefish + import cuddlefish.tests + + tests = [] + packages = [cuddlefish, cuddlefish.tests] + for package in packages: + path = os.path.abspath(package.__path__[0]) + pynames = glob.glob(os.path.join(path, '*.py')) + for filename in pynames: + basename = os.path.basename(filename) + module_name = os.path.splitext(basename)[0] + full_name = "%s.%s" % (package.__name__, module_name) + module = __import__(full_name, fromlist=[package.__name__]) + + loader = unittest.TestLoader() + suite = loader.loadTestsFromModule(module) + for test in suite: + tests.append(test) + + finder = doctest.DocTestFinder() + doctests = finder.find(module) + for test in doctests: + if len(test.examples) > 0: + tests.append(doctest.DocTestCase(test)) + + return tests + +def run(verbose=False): + if verbose: + verbosity = 2 + else: + verbosity = 1 + + tests = get_tests() + suite = unittest.TestSuite(tests) + runner = unittest.TextTestRunner(verbosity=verbosity) + return runner.run(suite) + +if __name__ == '__main__': + run() diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/explicit-icon/explicit-icon.png b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/explicit-icon/explicit-icon.png new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/explicit-icon/explicit-icon.png diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/explicit-icon/explicit-icon64.png b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/explicit-icon/explicit-icon64.png new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/explicit-icon/explicit-icon64.png diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/explicit-icon/lib/main.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/explicit-icon/lib/main.js new file mode 100644 index 000000000..b7e0a1d5e --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/explicit-icon/lib/main.js @@ -0,0 +1,4 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/explicit-icon/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/explicit-icon/package.json new file mode 100644 index 000000000..8d56d74ba --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/explicit-icon/package.json @@ -0,0 +1,5 @@ +{ + "loader": "lib/main.js", + "icon": "explicit-icon.png", + "icon64": "explicit-icon64.png" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/implicit-icon/icon.png b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/implicit-icon/icon.png new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/implicit-icon/icon.png diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/implicit-icon/icon64.png b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/implicit-icon/icon64.png new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/implicit-icon/icon64.png diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/implicit-icon/lib/main.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/implicit-icon/lib/main.js new file mode 100644 index 000000000..b7e0a1d5e --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/implicit-icon/lib/main.js @@ -0,0 +1,4 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/implicit-icon/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/implicit-icon/package.json new file mode 100644 index 000000000..3f0e2419f --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/implicit-icon/package.json @@ -0,0 +1,3 @@ +{ + "loader": "lib/main.js" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/no-icon/lib/main.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/no-icon/lib/main.js new file mode 100644 index 000000000..b7e0a1d5e --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/no-icon/lib/main.js @@ -0,0 +1,4 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/no-icon/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/no-icon/package.json new file mode 100644 index 000000000..3f0e2419f --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588119-files/packages/no-icon/package.json @@ -0,0 +1,3 @@ +{ + "loader": "lib/main.js" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-588661-files/packages/bar/lib/bar-loader.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588661-files/packages/bar/lib/bar-loader.js new file mode 100644 index 000000000..b7e0a1d5e --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588661-files/packages/bar/lib/bar-loader.js @@ -0,0 +1,4 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-588661-files/packages/bar/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588661-files/packages/bar/package.json new file mode 100644 index 000000000..e83a9d422 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588661-files/packages/bar/package.json @@ -0,0 +1,3 @@ +{ + "loader": "lib/bar-loader.js" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-588661-files/packages/foo/lib/foo-loader.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588661-files/packages/foo/lib/foo-loader.js new file mode 100644 index 000000000..b7e0a1d5e --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588661-files/packages/foo/lib/foo-loader.js @@ -0,0 +1,4 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-588661-files/packages/foo/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588661-files/packages/foo/package.json new file mode 100644 index 000000000..4648df67e --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-588661-files/packages/foo/package.json @@ -0,0 +1,4 @@ +{ + "loader": "lib/foo-loader.js", + "dependencies": ["bar"] +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-611495-files/jspath-one/docs/main.md b/addon-sdk/source/python-lib/cuddlefish/tests/bug-611495-files/jspath-one/docs/main.md new file mode 100644 index 000000000..54518d38d --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-611495-files/jspath-one/docs/main.md @@ -0,0 +1,5 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +minimal docs diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-611495-files/jspath-one/lib/main.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-611495-files/jspath-one/lib/main.js new file mode 100644 index 000000000..aeda0e7fb --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-611495-files/jspath-one/lib/main.js @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.main = function(options, callbacks) { + console.log("minimal"); + callbacks.quit(); +}; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-611495-files/jspath-one/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-611495-files/jspath-one/package.json new file mode 100644 index 000000000..45d409a74 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-611495-files/jspath-one/package.json @@ -0,0 +1,5 @@ +{ + "name": "jspath-one", + "author": "Jon Smith", + "description": "A package w/ a main module; can be built into an extension." +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/commonjs-naming/doc/foo.md b/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/commonjs-naming/doc/foo.md new file mode 100644 index 000000000..c92ddb880 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/commonjs-naming/doc/foo.md @@ -0,0 +1,5 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +I am documentation for foo. diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/commonjs-naming/lib/foo-loader.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/commonjs-naming/lib/foo-loader.js new file mode 100644 index 000000000..2daeb3512 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/commonjs-naming/lib/foo-loader.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/commonjs-naming/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/commonjs-naming/package.json new file mode 100644 index 000000000..ccc61b29d --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/commonjs-naming/package.json @@ -0,0 +1,3 @@ +{ + "loader": "lib/foo-loader.js" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/commonjs-naming/test/test-foo.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/commonjs-naming/test/test-foo.js new file mode 100644 index 000000000..bd0cfa96f --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/commonjs-naming/test/test-foo.js @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.testThing = function(test) { + test.assertEqual(2, 1 + 1); +}; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/original-naming/docs/foo.md b/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/original-naming/docs/foo.md new file mode 100644 index 000000000..c92ddb880 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/original-naming/docs/foo.md @@ -0,0 +1,5 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +I am documentation for foo. diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/original-naming/lib/foo-loader.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/original-naming/lib/foo-loader.js new file mode 100644 index 000000000..2daeb3512 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/original-naming/lib/foo-loader.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/original-naming/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/original-naming/package.json new file mode 100644 index 000000000..ccc61b29d --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/original-naming/package.json @@ -0,0 +1,3 @@ +{ + "loader": "lib/foo-loader.js" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/original-naming/tests/test-foo.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/original-naming/tests/test-foo.js new file mode 100644 index 000000000..bd0cfa96f --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-614712-files/packages/original-naming/tests/test-foo.js @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.testThing = function(test) { + test.assertEqual(2, 1 + 1); +}; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/doc/foo.md b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/doc/foo.md new file mode 100644 index 000000000..c92ddb880 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/doc/foo.md @@ -0,0 +1,5 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +I am documentation for foo. diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/lib/foo.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/lib/foo.js new file mode 100644 index 000000000..2daeb3512 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/lib/foo.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/lib/loader.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/lib/loader.js new file mode 100644 index 000000000..2daeb3512 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/lib/loader.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/package.json new file mode 100644 index 000000000..c2d22aa6f --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/package.json @@ -0,0 +1,3 @@ +{ + "loader": "lib/loader.js" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/test/test-foo.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/test/test-foo.js new file mode 100644 index 000000000..bd0cfa96f --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-lib/test/test-foo.js @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.testThing = function(test) { + test.assertEqual(2, 1 + 1); +}; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-locale/locale/emptyFolder b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-locale/locale/emptyFolder new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-locale/locale/emptyFolder diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-locale/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-locale/package.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-locale/package.json @@ -0,0 +1 @@ +{} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/doc/foo.md b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/doc/foo.md new file mode 100644 index 000000000..c92ddb880 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/doc/foo.md @@ -0,0 +1,5 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +I am documentation for foo. diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/foo.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/foo.js new file mode 100644 index 000000000..2daeb3512 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/foo.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/loader.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/loader.js new file mode 100644 index 000000000..2daeb3512 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/loader.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/package.json new file mode 100644 index 000000000..100249fed --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/package.json @@ -0,0 +1,3 @@ +{ + "loader": "loader.js" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/test/test-foo.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/test/test-foo.js new file mode 100644 index 000000000..bd0cfa96f --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/default-root/test/test-foo.js @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.testThing = function(test) { + test.assertEqual(2, 1 + 1); +}; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/alt-lib/foo.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/alt-lib/foo.js new file mode 100644 index 000000000..2daeb3512 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/alt-lib/foo.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/alt-lib/loader.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/alt-lib/loader.js new file mode 100644 index 000000000..2daeb3512 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/alt-lib/loader.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/doc/foo.md b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/doc/foo.md new file mode 100644 index 000000000..c92ddb880 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/doc/foo.md @@ -0,0 +1,5 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +I am documentation for foo. diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/package.json new file mode 100644 index 000000000..f79250e6e --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/package.json @@ -0,0 +1,4 @@ +{ + "directories": { "lib": "./alt-lib" }, + "loader": "alt-lib/loader.js" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/test/test-foo.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/test/test-foo.js new file mode 100644 index 000000000..bd0cfa96f --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-dir-lib/test/test-foo.js @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.testThing = function(test) { + test.assertEqual(2, 1 + 1); +}; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/alt2-lib/foo.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/alt2-lib/foo.js new file mode 100644 index 000000000..2daeb3512 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/alt2-lib/foo.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/alt2-lib/loader.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/alt2-lib/loader.js new file mode 100644 index 000000000..2daeb3512 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/alt2-lib/loader.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/doc/foo.md b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/doc/foo.md new file mode 100644 index 000000000..c92ddb880 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/doc/foo.md @@ -0,0 +1,5 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +I am documentation for foo. diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/package.json new file mode 100644 index 000000000..b017baa86 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/package.json @@ -0,0 +1,4 @@ +{ + "lib": "./alt2-lib", + "loader": "alt2-lib/loader.js" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/test/test-foo.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/test/test-foo.js new file mode 100644 index 000000000..bd0cfa96f --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-652227-files/packages/explicit-lib/test/test-foo.js @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.testThing = function(test) { + test.assertEqual(2, 1 + 1); +}; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-669274-files/packages/extra-options/docs/main.md b/addon-sdk/source/python-lib/cuddlefish/tests/bug-669274-files/packages/extra-options/docs/main.md new file mode 100644 index 000000000..54518d38d --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-669274-files/packages/extra-options/docs/main.md @@ -0,0 +1,5 @@ +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +minimal docs diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-669274-files/packages/extra-options/lib/main.js b/addon-sdk/source/python-lib/cuddlefish/tests/bug-669274-files/packages/extra-options/lib/main.js new file mode 100644 index 000000000..aeda0e7fb --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-669274-files/packages/extra-options/lib/main.js @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.main = function(options, callbacks) { + console.log("minimal"); + callbacks.quit(); +}; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-669274-files/packages/extra-options/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-669274-files/packages/extra-options/package.json new file mode 100644 index 000000000..de868f776 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-669274-files/packages/extra-options/package.json @@ -0,0 +1,6 @@ +{ + "name": "extra-options", + "author": "Jon Smith", + "description": "A package w/ a main module; can be built into an extension.", + "loader": "lib/main.js" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-715340-files/pkg-1-pack/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-715340-files/pkg-1-pack/package.json new file mode 100644 index 000000000..afa13e9e5 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-715340-files/pkg-1-pack/package.json @@ -0,0 +1,10 @@ +{ + "name": "empty", + "license": "MPL-2.0", + "author": "", + "version": "0.1", + "title": "empty", + "id": "jid1-80fr8b6qeRlQSQ", + "unpack": false, + "description": "test unpack= support" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-715340-files/pkg-2-unpack/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-715340-files/pkg-2-unpack/package.json new file mode 100644 index 000000000..b1f76edcd --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-715340-files/pkg-2-unpack/package.json @@ -0,0 +1,10 @@ +{ + "name": "empty", + "license": "MPL-2.0", + "author": "", + "version": "0.1", + "title": "empty", + "id": "jid1-80fr8b6qeRlQSQ", + "unpack": true, + "description": "test unpack= support" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-715340-files/pkg-3-pack/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-715340-files/pkg-3-pack/package.json new file mode 100644 index 000000000..e3dd9fb72 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-715340-files/pkg-3-pack/package.json @@ -0,0 +1,9 @@ +{ + "name": "empty", + "license": "MPL-2.0", + "author": "", + "version": "0.1", + "title": "empty", + "id": "jid1-80fr8b6qeRlQSQ", + "description": "test unpack= support" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-906359-files/fullName/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-906359-files/fullName/package.json new file mode 100644 index 000000000..2525a1497 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-906359-files/fullName/package.json @@ -0,0 +1,9 @@ +{ + "name": "empty", + "license": "MPL-2.0", + "author": "", + "version": "0.1", + "fullName": "a long fullName", + "id": "jid1-80123", + "description": "test addon name fallback to 'fullName' key" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-906359-files/none/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-906359-files/none/package.json new file mode 100644 index 000000000..b05025c72 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-906359-files/none/package.json @@ -0,0 +1,9 @@ +{ + "name": "a long none", + "license": "MPL-2.0", + "author": "", + "version": "0.1", + + "id": "jid1-80123", + "description": "test addon name falls back all the way to the 'name' key" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/bug-906359-files/title/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/bug-906359-files/title/package.json new file mode 100644 index 000000000..634817b7a --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/bug-906359-files/title/package.json @@ -0,0 +1,9 @@ +{ + "name": "empty", + "license": "MPL-2.0", + "author": "", + "version": "0.1", + "title": "a long title", + "id": "jid1-80123", + "description": "test addon name comes from the 'title' key" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/e10s-adapter-files/packages/foo/lib/bar-e10s-adapter.js b/addon-sdk/source/python-lib/cuddlefish/tests/e10s-adapter-files/packages/foo/lib/bar-e10s-adapter.js new file mode 100644 index 000000000..ad54ae76d --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/e10s-adapter-files/packages/foo/lib/bar-e10s-adapter.js @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +if (this.sendMessage) { +} else { + require('bar'); + + exports.register = function(process) { + }; +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/e10s-adapter-files/packages/foo/lib/bar.js b/addon-sdk/source/python-lib/cuddlefish/tests/e10s-adapter-files/packages/foo/lib/bar.js new file mode 100644 index 000000000..fe9e4fb72 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/e10s-adapter-files/packages/foo/lib/bar.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +require('chrome'); diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/e10s-adapter-files/packages/foo/lib/foo.js b/addon-sdk/source/python-lib/cuddlefish/tests/e10s-adapter-files/packages/foo/lib/foo.js new file mode 100644 index 000000000..55633d167 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/e10s-adapter-files/packages/foo/lib/foo.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +require('bar'); diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/e10s-adapter-files/packages/foo/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/e10s-adapter-files/packages/foo/package.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/e10s-adapter-files/packages/foo/package.json @@ -0,0 +1 @@ +{} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/five/lib/main.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/five/lib/main.js new file mode 100644 index 000000000..e32a30f9c --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/five/lib/main.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.main = "'main' mainly reigns in main(.js)"; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/five/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/five/package.json new file mode 100644 index 000000000..98e4b85aa --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/five/package.json @@ -0,0 +1,3 @@ +{ "name": "five", + "main": "./lib/main" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four-deps/four-a/lib/misc.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four-deps/four-a/lib/misc.js new file mode 100644 index 000000000..7e1ce7ea0 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four-deps/four-a/lib/misc.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.main = 42; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four-deps/four-a/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four-deps/four-a/package.json new file mode 100644 index 000000000..3010fae71 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four-deps/four-a/package.json @@ -0,0 +1,4 @@ +{ "name": "four-a", + "directories": {"lib": "lib"}, + "main": "./topfiles/main.js" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four-deps/four-a/topfiles/main.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four-deps/four-a/topfiles/main.js new file mode 100644 index 000000000..7e1ce7ea0 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four-deps/four-a/topfiles/main.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.main = 42; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four/lib/main.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four/lib/main.js new file mode 100644 index 000000000..b95f7bd53 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four/lib/main.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var a = require("four-a"); diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four/package.json new file mode 100644 index 000000000..53180b9f6 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/four/package.json @@ -0,0 +1,3 @@ +{ "name": "four", + "main": "main" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/one/lib/main.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/one/lib/main.js new file mode 100644 index 000000000..08b9245c5 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/one/lib/main.js @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var panel = require("sdk/panel"); +var two = require("two.js"); +var a = require("./two"); +var b = require("sdk/tabs.js"); +var c = require("./subdir/three"); diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/one/lib/subdir/three.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/one/lib/subdir/three.js new file mode 100644 index 000000000..b594f3c68 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/one/lib/subdir/three.js @@ -0,0 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.foo = 1; +var main = require("../main"); diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/one/lib/two.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/one/lib/two.js new file mode 100644 index 000000000..976521958 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/one/lib/two.js @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +// this ought to find our sibling, not packages/development-mode/lib/main.js +var main = require("main"); +exports.foo = 1; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/one/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/one/package.json new file mode 100644 index 000000000..edd2b17c8 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/one/package.json @@ -0,0 +1,4 @@ +{ "name": "one", + "id": "jid1@jetpack", + "main": "main" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/seven/data/text.data b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/seven/data/text.data new file mode 100644 index 000000000..1269488f7 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/seven/data/text.data @@ -0,0 +1 @@ +data diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/seven/lib/main.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/seven/lib/main.js new file mode 100644 index 000000000..b1d68d3a0 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/seven/lib/main.js @@ -0,0 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var self = require("sdk/self"); // trigger inclusion of data +exports.main = function () { console.log("main"); }; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/seven/lib/unused.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/seven/lib/unused.js new file mode 100644 index 000000000..a7b1c1449 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/seven/lib/unused.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.unused = "just pretend I'm not here"; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/seven/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/seven/package.json new file mode 100644 index 000000000..922c77dfc --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/seven/package.json @@ -0,0 +1,4 @@ +{ + "name": "seven", + "id": "jid7" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/six/lib/unused.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/six/lib/unused.js new file mode 100644 index 000000000..ada31ef8c --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/six/lib/unused.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.unused = "I am."; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/six/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/six/package.json new file mode 100644 index 000000000..906b24927 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/six/package.json @@ -0,0 +1,3 @@ +{ "name": "six", + "main": "./unreachable" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/six/unreachable.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/six/unreachable.js new file mode 100644 index 000000000..e8b229cde --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/six/unreachable.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.main = "I am outside lib/ and cannot be reached, yet"; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/lib/main.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/lib/main.js new file mode 100644 index 000000000..54e4b7efb --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/lib/main.js @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.main = 42; +require("./subdir/subfile"); +require("sdk/self"); // trigger inclusion of our data/ directory + diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/lib/subdir/subfile.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/lib/subdir/subfile.js new file mode 100644 index 000000000..aec24d01b --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/lib/subdir/subfile.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.main = "I should be included in a subdir"; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/lib/unused.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/lib/unused.js new file mode 100644 index 000000000..36c4a4e2b --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/lib/unused.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.main = "unused, linker should not include me in the XPI"; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/locale/fr-FR.properties b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/locale/fr-FR.properties new file mode 100644 index 000000000..980ac46c6 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/locale/fr-FR.properties @@ -0,0 +1,5 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Yes= Oui diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/package.json new file mode 100644 index 000000000..6b796fc33 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-a/package.json @@ -0,0 +1,3 @@ +{ "name": "three-a", + "main": "./lib/main.js" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-b/lib/main.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-b/lib/main.js new file mode 100644 index 000000000..7e1ce7ea0 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-b/lib/main.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.main = 42; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-b/locale/fr-FR.properties b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-b/locale/fr-FR.properties new file mode 100644 index 000000000..c1bf1465f --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-b/locale/fr-FR.properties @@ -0,0 +1,6 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +No= Non +one= un diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-b/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-b/package.json new file mode 100644 index 000000000..c0ff5ee0e --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-b/package.json @@ -0,0 +1,3 @@ +{ "name": "three-b", + "main": "./lib/main" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-c/lib/main.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-c/lib/main.js new file mode 100644 index 000000000..7e1ce7ea0 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-c/lib/main.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.main = 42; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-c/lib/sub/foo.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-c/lib/sub/foo.js new file mode 100644 index 000000000..587849615 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-c/lib/sub/foo.js @@ -0,0 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.foo = "you found me down here"; + diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-c/locale/fr-FR.properties b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-c/locale/fr-FR.properties new file mode 100644 index 000000000..dac3f133b --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-c/locale/fr-FR.properties @@ -0,0 +1,9 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +No= Nein +What?= Quoi? +plural=other +plural[one]=one +uft8_value=é diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-c/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-c/package.json new file mode 100644 index 000000000..169c9146b --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three-deps/three-c/package.json @@ -0,0 +1,3 @@ +{ "name": "three-c", + "main": "lib/main" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/data/msg.txt b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/data/msg.txt new file mode 100644 index 000000000..3b18e512d --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/data/msg.txt @@ -0,0 +1 @@ +hello world diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/data/subdir/submsg.txt b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/data/subdir/submsg.txt new file mode 100644 index 000000000..d2cfe80de --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/data/subdir/submsg.txt @@ -0,0 +1 @@ +hello subdir diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/lib/main.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/lib/main.js new file mode 100644 index 000000000..4f5944374 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/lib/main.js @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var a = require("three-a"); +var b = require("three-b"); +var c = require("three-c"); +var c3 = require("three-c/sub/foo"); diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/package.json new file mode 100644 index 000000000..cbfbc5bb2 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/package.json @@ -0,0 +1,3 @@ +{ "name": "three", + "main": "main" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/tests/nontest.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/tests/nontest.js new file mode 100644 index 000000000..edbc08ef2 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/tests/nontest.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// dummy diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/tests/test-one.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/tests/test-one.js new file mode 100644 index 000000000..edbc08ef2 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/tests/test-one.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// dummy diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/tests/test-two.js b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/tests/test-two.js new file mode 100644 index 000000000..edbc08ef2 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/linker-files/three/tests/test-two.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// dummy diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/doc/main.md b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/doc/main.md new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/doc/main.md diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/lib/ignore_me b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/lib/ignore_me new file mode 100644 index 000000000..014242c92 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/lib/ignore_me @@ -0,0 +1,3 @@ +The docs processor should tolerate (by ignoring) random non-.js files in lib +directories, such as those left around by editors, version-control systems, +or OS metadata like .DS_Store . This file exercises that tolerance. diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/lib/main.js b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/lib/main.js new file mode 100644 index 000000000..026487257 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/lib/main.js @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.main = function(options, callbacks) { + console.log("1 + 1 =", require("bar-module").add(1, 1)); + callbacks.quit(); +}; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/lib/surprise.js/ignore_me_too b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/lib/surprise.js/ignore_me_too new file mode 100644 index 000000000..066f9b55d --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/lib/surprise.js/ignore_me_too @@ -0,0 +1,2 @@ +The docs processor should also ignore directories named *.js, and their +contents. diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/package.json new file mode 100644 index 000000000..afb5698e9 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/aardvark/package.json @@ -0,0 +1,7 @@ +{ + "author": "Jon Smith", + "description": "A package w/ a main module; can be built into an extension.", + "keywords": ["potato"], + "version": "1.0", + "dependencies": ["addon-sdk", "barbeque"] +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/anteater_files/lib/main.js b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/anteater_files/lib/main.js new file mode 100644 index 000000000..026487257 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/anteater_files/lib/main.js @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.main = function(options, callbacks) { + console.log("1 + 1 =", require("bar-module").add(1, 1)); + callbacks.quit(); +}; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/anteater_files/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/anteater_files/package.json new file mode 100644 index 000000000..9684581da --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/anteater_files/package.json @@ -0,0 +1,8 @@ +{ + "name": "anteater", + "author": "Jon Smith", + "description": "A package w/ a main module; can be built into an extension.", + "keywords": ["potato"], + "version": "1.0", + "dependencies": ["addon-sdk", "barbeque"] +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/api-utils/lib/loader.js b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/api-utils/lib/loader.js new file mode 100644 index 000000000..361846d27 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/api-utils/lib/loader.js @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This module will be imported by the XPCOM harness/boostrapper +// via Components.utils.import() and is responsible for creating a +// CommonJS module loader. diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/api-utils/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/api-utils/package.json new file mode 100644 index 000000000..64eb065be --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/api-utils/package.json @@ -0,0 +1,5 @@ +{ + "description": "A foundational package that provides a CommonJS module loader implementation.", + "keywords": ["potato", "jetpack-low-level"], + "loader": "lib/loader.js" +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/barbeque/lib/bar-module.js b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/barbeque/lib/bar-module.js new file mode 100644 index 000000000..ff982ae20 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/barbeque/lib/bar-module.js @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.add = function add(a, b) { + return a + b; +}; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/barbeque/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/barbeque/package.json new file mode 100644 index 000000000..62e3c12d7 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/barbeque/package.json @@ -0,0 +1,4 @@ +{ + "keywords": ["potato", "jetpack-low-level"], + "description": "A package used by 'aardvark' as a library." +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/minimal/lib/main.js b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/minimal/lib/main.js new file mode 100644 index 000000000..aeda0e7fb --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/minimal/lib/main.js @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.main = function(options, callbacks) { + console.log("minimal"); + callbacks.quit(); +}; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/minimal/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/minimal/package.json new file mode 100644 index 000000000..530f3c247 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/minimal/package.json @@ -0,0 +1,4 @@ +{ + "author": "Jon Smith", + "description": "A package w/ a main module; can be built into an extension." +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/third_party/docs/third_party.md b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/third_party/docs/third_party.md new file mode 100644 index 000000000..35152f601 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/third_party/docs/third_party.md @@ -0,0 +1 @@ +hello, I'm a third party.
\ No newline at end of file diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/third_party/lib/third-party.js b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/third_party/lib/third-party.js new file mode 100644 index 000000000..026487257 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/third_party/lib/third-party.js @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +exports.main = function(options, callbacks) { + console.log("1 + 1 =", require("bar-module").add(1, 1)); + callbacks.quit(); +}; diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/third_party/package.json b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/third_party/package.json new file mode 100644 index 000000000..afb5698e9 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/packages/third_party/package.json @@ -0,0 +1,7 @@ +{ + "author": "Jon Smith", + "description": "A package w/ a main module; can be built into an extension.", + "keywords": ["potato"], + "version": "1.0", + "dependencies": ["addon-sdk", "barbeque"] +} diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/static-files/xpi-template/components/harness.js b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/xpi-template/components/harness.js new file mode 100644 index 000000000..a20bf3fe6 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/static-files/xpi-template/components/harness.js @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file contains XPCOM code that bootstraps an SDK-based add-on +// by loading its harness-options.json, registering all its resource +// directories, executing its loader, and then executing its program's +// main() function. diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/test_init.py b/addon-sdk/source/python-lib/cuddlefish/tests/test_init.py new file mode 100644 index 000000000..5ececfea6 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_init.py @@ -0,0 +1,211 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os, unittest, shutil +import zipfile +from StringIO import StringIO +from cuddlefish import initializer +from cuddlefish.templates import TEST_MAIN_JS, PACKAGE_JSON + +tests_path = os.path.abspath(os.path.dirname(__file__)) + +class TestInit(unittest.TestCase): + + def run_init_in_subdir(self, dirname, f, *args, **kwargs): + top = os.path.abspath(os.getcwd()) + basedir = os.path.abspath(os.path.join(".test_tmp",self.id(),dirname)) + if os.path.isdir(basedir): + assert basedir.startswith(top) + shutil.rmtree(basedir) + os.makedirs(basedir) + try: + os.chdir(basedir) + return f(basedir, *args, **kwargs) + finally: + os.chdir(top) + + def do_test_init(self,basedir): + # Let's init the addon, no error admitted + f = open(".ignoreme","w") + f.write("stuff") + f.close() + + out, err = StringIO(), StringIO() + init_run = initializer(None, ["init"], out, err) + out, err = out.getvalue(), err.getvalue() + self.assertEqual(init_run["result"], 0) + self.assertTrue("* lib directory created" in out) + self.assertTrue("* data directory created" in out) + self.assertTrue("Have fun!" in out) + self.assertEqual(err,"") + self.assertTrue(len(os.listdir(basedir))>0) + main_js = os.path.join(basedir,"lib","main.js") + package_json = os.path.join(basedir,"package.json") + test_main_js = os.path.join(basedir,"test","test-main.js") + self.assertTrue(os.path.exists(main_js)) + self.assertTrue(os.path.exists(package_json)) + self.assertTrue(os.path.exists(test_main_js)) + self.assertEqual(open(main_js,"r").read(),"") + self.assertEqual(open(package_json,"r").read() % {"id":"tmp_addon_id" }, + PACKAGE_JSON % {"name":"tmp_addon_sample", + "title": "tmp_addon_SAMPLE", + "id":init_run["jid"] }) + self.assertEqual(open(test_main_js,"r").read(),TEST_MAIN_JS) + + # Let's check that the addon is initialized + out, err = StringIO(), StringIO() + init_run = initializer(None, ["init"], out, err) + out, err = out.getvalue(), err.getvalue() + self.failIfEqual(init_run["result"],0) + self.assertTrue("This command must be run in an empty directory." in err) + + def test_initializer(self): + self.run_init_in_subdir("tmp_addon_SAMPLE",self.do_test_init) + + def do_test_args(self, basedir): + # check that running it with spurious arguments will fail + out,err = StringIO(), StringIO() + init_run = initializer(None, ["init", "specified-dirname", "extra-arg"], out, err) + out, err = out.getvalue(), err.getvalue() + self.failIfEqual(init_run["result"], 0) + self.assertTrue("Too many arguments" in err) + + def test_args(self): + self.run_init_in_subdir("tmp_addon_sample", self.do_test_args) + + def _test_existing_files(self, basedir): + f = open("pay_attention_to_me","w") + f.write("stuff") + f.close() + out,err = StringIO(), StringIO() + rc = initializer(None, ["init"], out, err) + out, err = out.getvalue(), err.getvalue() + self.assertEqual(rc["result"], 1) + self.failUnless("This command must be run in an empty directory" in err, + err) + self.failIf(os.path.exists("lib")) + + def test_existing_files(self): + self.run_init_in_subdir("existing_files", self._test_existing_files) + + def test_init_subdir(self): + parent = os.path.abspath(os.path.join(".test_tmp", self.id())) + basedir = os.path.join(parent, "init-basedir") + if os.path.exists(parent): + shutil.rmtree(parent) + os.makedirs(parent) + + # if the basedir exists and is not empty, init should refuse + os.makedirs(basedir) + f = open(os.path.join(basedir, "boo"), "w") + f.write("stuff") + f.close() + out, err = StringIO(), StringIO() + rc = initializer(None, ["init", basedir], out, err) + out, err = out.getvalue(), err.getvalue() + self.assertEqual(rc["result"], 1) + self.assertTrue("testing if directory is empty" in out, out) + self.assertTrue("This command must be run in an empty directory." in err, + err) + + # a .dotfile should be tolerated + os.rename(os.path.join(basedir, "boo"), os.path.join(basedir, ".phew")) + out, err = StringIO(), StringIO() + rc = initializer(None, ["init", basedir], out, err) + out, err = out.getvalue(), err.getvalue() + self.assertEqual(rc["result"], 0) + self.assertTrue("* data directory created" in out, out) + self.assertTrue("Have fun!" in out) + self.assertEqual(err,"") + self.assertTrue(os.listdir(basedir)) + main_js = os.path.join(basedir,"lib","main.js") + package_json = os.path.join(basedir,"package.json") + self.assertTrue(os.path.exists(main_js)) + self.assertTrue(os.path.exists(package_json)) + shutil.rmtree(basedir) + + # init should create directories that don't exist already + out, err = StringIO(), StringIO() + rc = initializer(None, ["init", basedir], out, err) + out, err = out.getvalue(), err.getvalue() + self.assertEqual(rc["result"], 0) + self.assertTrue("* data directory created" in out) + self.assertTrue("Have fun!" in out) + self.assertEqual(err,"") + self.assertTrue(os.listdir(basedir)) + main_js = os.path.join(basedir,"lib","main.js") + package_json = os.path.join(basedir,"package.json") + self.assertTrue(os.path.exists(main_js)) + self.assertTrue(os.path.exists(package_json)) + + +class TestCfxQuits(unittest.TestCase): + + def run_cfx(self, addon_path, command): + old_cwd = os.getcwd() + os.chdir(addon_path) + import sys + old_stdout = sys.stdout + old_stderr = sys.stderr + sys.stdout = out = StringIO() + sys.stderr = err = StringIO() + rc = 0 + try: + import cuddlefish + args = list(command) + # Pass arguments given to cfx so that cfx can find firefox path + # if --binary option is given: + args.extend(sys.argv[1:]) + cuddlefish.run(arguments=args) + except SystemExit, e: + if "code" in e: + rc = e.code + elif "args" in e and len(e.args)>0: + rc = e.args[0] + else: + rc = 0 + finally: + sys.stdout = old_stdout + sys.stderr = old_stderr + os.chdir(old_cwd) + out.flush() + err.flush() + return rc, out.getvalue(), err.getvalue() + + # this method doesn't exists in python 2.5, + # implements our own + def assertIn(self, member, container): + """Just like self.assertTrue(a in b), but with a nicer default message.""" + if member not in container: + standardMsg = '"%s" not found in "%s"' % (member, + container) + self.fail(standardMsg) + + def test_cfx_init(self): + # Create an empty test directory + addon_path = os.path.abspath(os.path.join(".test_tmp", "test-cfx-init")) + if os.path.isdir(addon_path): + shutil.rmtree(addon_path) + os.makedirs(addon_path) + + # Fake a call to cfx init + old_cwd = os.getcwd() + os.chdir(addon_path) + out, err = StringIO(), StringIO() + rc = initializer(None, ["init"], out, err) + os.chdir(old_cwd) + out, err = out.getvalue(), err.getvalue() + self.assertEqual(rc["result"], 0) + self.assertTrue("Have fun!" in out) + self.assertEqual(err,"") + + # run cfx test + rc, out, err = self.run_cfx(addon_path, ["test"]) + self.assertEqual(rc, 0) + self.assertIn("6 of 6 tests passed.", err) + self.assertIn("Program terminated successfully.", err) + + +if __name__ == "__main__": + unittest.main() diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/test_licenses.py b/addon-sdk/source/python-lib/cuddlefish/tests/test_licenses.py new file mode 100644 index 000000000..574812ada --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_licenses.py @@ -0,0 +1,100 @@ + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import unittest +import os.path + +parent = os.path.dirname +test_dir = parent(os.path.abspath(__file__)) +sdk_root = parent(parent(parent(test_dir))) + +def from_sdk_top(fn): + return os.path.abspath(os.path.join(sdk_root, fn)) + +MPL2_URL = "http://mozilla.org/MPL/2.0/" + +# These files all come with their own license headers +skip = [ + "python-lib/cuddlefish/_version.py", # generated, public domain + "doc/static-files/js/jquery.js", # MIT/GPL dual + "examples/annotator/data/jquery-1.4.2.min.js", # MIT/GPL dual + "examples/reddit-panel/data/jquery-1.4.4.min.js", # MIT/GPL dual + "examples/library-detector/data/library-detector.js", # MIT + "python-lib/mozrunner/killableprocess.py", # MIT? BSDish? + "python-lib/mozrunner/winprocess.py", # MIT + "packages/api-utils/tests/test-querystring.js", # MIT + "packages/api-utils/lib/promise.js", # MIT + "packages/api-utils/tests/test-promise.js", # MIT + "examples/actor-repl/README.md", # It's damn readme file + "examples/actor-repl/data/codemirror-compressed.js", # MIT + "examples/actor-repl/data/codemirror.css", # MIT + ] +absskip = [from_sdk_top(os.path.join(*fn.split("/"))) for fn in skip] + +class Licenses(unittest.TestCase): + def test(self): + # Examine most SDK files to check if they've got an MPL2 license + # header. We exclude some files that are known to include different + # licenses. + self.missing = [] + self.scan_file(from_sdk_top(os.path.join("python-lib", "jetpack_sdk_env.py"))) + self.scan(os.path.join("python-lib", "cuddlefish"), [".js", ".py"], + skipdirs=["sdk-docs"], # test_generate.py makes this + ) + self.scan(os.path.join("python-lib", "mozrunner"), [".py"]) + self.scan("lib", [".js", ".jsm", ".css"], + skipdirs=[ + "diffpatcher", # MIT + "method", # MIT + "child_process", # MPL 1.1/GPL 2.0/LGPL 2.1 + "fs" # MIT + ]) + self.scan("test", [".js", ".jsm", ".css", ".html"], + skipdirs=[ + "buffers", # MIT + "querystring", # MIT + "path" # MIT + ]) + self.scan("modules", [".js", ".jsm"]) + self.scan("examples", [".js", ".css", ".html", ".md"]) + self.scan("bin", [".bat", ".ps1", ".js"]) + for fn in [os.path.join("bin", "activate"), + os.path.join("bin", "cfx"), + os.path.join("bin", "integration-scripts", "buildbot-run-cfx-helper"), + os.path.join("bin", "integration-scripts", "integration-check"), + ]: + self.scan_file(from_sdk_top(fn)) + + if self.missing: + print + print "The following files are missing an MPL2 header:" + for fn in sorted(self.missing): + print " "+fn + self.fail("%d files are missing an MPL2 header" % len(self.missing)) + + def scan(self, start, extensions=[], skipdirs=[]): + # scan a whole subdirectory + start = from_sdk_top(start) + for root, dirs, files in os.walk(start): + for d in skipdirs: + if d in dirs: + dirs.remove(d) + for fn in files: + ext = os.path.splitext(fn)[1] + if extensions and ext not in extensions: + continue + absfn = os.path.join(root, fn) + if absfn in absskip: + continue + self.scan_file(absfn) + + def scan_file(self, fn): + # scan a single file + if not MPL2_URL in open(fn, "r").read(): + relfile = fn[len(sdk_root)+1:] + self.missing.append(relfile) + +if __name__ == '__main__': + unittest.main() diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/test_linker.py b/addon-sdk/source/python-lib/cuddlefish/tests/test_linker.py new file mode 100755 index 000000000..7a6fd12e4 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_linker.py @@ -0,0 +1,247 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os.path +import shutil +import zipfile +from StringIO import StringIO +import simplejson as json +import unittest +import cuddlefish +from cuddlefish import packaging, manifest + +def up(path, generations=1): + for i in range(generations): + path = os.path.dirname(path) + return path + +ROOT = up(os.path.abspath(__file__), 4) +def get_linker_files_dir(name): + return os.path.join(up(os.path.abspath(__file__)), "linker-files", name) + +class Basic(unittest.TestCase): + def get_pkg(self, name): + d = get_linker_files_dir(name) + return packaging.get_config_in_dir(d) + + def test_deps(self): + target_cfg = self.get_pkg("one") + pkg_cfg = packaging.build_config(ROOT, target_cfg) + deps = packaging.get_deps_for_targets(pkg_cfg, ["one"]) + self.failUnlessEqual(deps, ["one"]) + deps = packaging.get_deps_for_targets(pkg_cfg, + [target_cfg.name, "addon-sdk"]) + self.failUnlessEqual(deps, ["addon-sdk", "one"]) + + def test_manifest(self): + target_cfg = self.get_pkg("one") + pkg_cfg = packaging.build_config(ROOT, target_cfg) + deps = packaging.get_deps_for_targets(pkg_cfg, + [target_cfg.name, "addon-sdk"]) + self.failUnlessEqual(deps, ["addon-sdk", "one"]) + # target_cfg.dependencies is not provided, so we'll search through + # all known packages (everything in 'deps'). + m = manifest.build_manifest(target_cfg, pkg_cfg, deps, scan_tests=False) + m = m.get_harness_options_manifest(False) + + def assertReqIs(modname, reqname, path): + reqs = m["one/%s" % modname]["requirements"] + self.failUnlessEqual(reqs[reqname], path) + + assertReqIs("main", "sdk/panel", "sdk/panel") + assertReqIs("main", "two.js", "one/two") + assertReqIs("main", "./two", "one/two") + assertReqIs("main", "sdk/tabs.js", "sdk/tabs") + assertReqIs("main", "./subdir/three", "one/subdir/three") + assertReqIs("two", "main", "one/main") + assertReqIs("subdir/three", "../main", "one/main") + + target_cfg.dependencies = [] + + try: + # this should now work, as we ignore missing modules by default + m = manifest.build_manifest(target_cfg, pkg_cfg, deps, scan_tests=False) + m = m.get_harness_options_manifest(False) + + assertReqIs("main", "sdk/panel", "sdk/panel") + # note that with "addon-sdk" dependency present, + # "sdk/tabs.js" mapped to "sdk/tabs", but without, + # we just get the default (identity) mapping + assertReqIs("main", "sdk/tabs.js", "sdk/tabs.js") + except Exception, e: + self.fail("Must not throw from build_manifest() if modules are missing") + + # now, because .dependencies *is* provided, we won't search 'deps', + # and stop_on_missing is True, we'll get a link error + self.assertRaises(manifest.ModuleNotFoundError, + manifest.build_manifest, + target_cfg, pkg_cfg, deps, scan_tests=False, + abort_on_missing=True) + + def test_main_in_deps(self): + target_cfg = self.get_pkg("three") + package_path = [get_linker_files_dir("three-deps")] + pkg_cfg = packaging.build_config(ROOT, target_cfg, + packagepath=package_path) + deps = packaging.get_deps_for_targets(pkg_cfg, + [target_cfg.name, "addon-sdk"]) + self.failUnlessEqual(deps, ["addon-sdk", "three"]) + m = manifest.build_manifest(target_cfg, pkg_cfg, deps, scan_tests=False) + m = m.get_harness_options_manifest(False) + def assertReqIs(modname, reqname, path): + reqs = m["three/%s" % modname]["requirements"] + self.failUnlessEqual(reqs[reqname], path) + assertReqIs("main", "three-a", "three-a/main") + assertReqIs("main", "three-b", "three-b/main") + assertReqIs("main", "three-c", "three-c/main") + + def test_relative_main_in_top(self): + target_cfg = self.get_pkg("five") + package_path = [] + pkg_cfg = packaging.build_config(ROOT, target_cfg, + packagepath=package_path) + deps = packaging.get_deps_for_targets(pkg_cfg, + [target_cfg.name, "addon-sdk"]) + self.failUnlessEqual(deps, ["addon-sdk", "five"]) + # all we care about is that this next call doesn't raise an exception + m = manifest.build_manifest(target_cfg, pkg_cfg, deps, scan_tests=False) + m = m.get_harness_options_manifest(False) + reqs = m["five/main"]["requirements"] + self.failUnlessEqual(reqs, {}); + + def test_unreachable_relative_main_in_top(self): + target_cfg = self.get_pkg("six") + package_path = [] + pkg_cfg = packaging.build_config(ROOT, target_cfg, + packagepath=package_path) + deps = packaging.get_deps_for_targets(pkg_cfg, + [target_cfg.name, "addon-sdk"]) + self.failUnlessEqual(deps, ["addon-sdk", "six"]) + self.assertRaises(manifest.UnreachablePrefixError, + manifest.build_manifest, + target_cfg, pkg_cfg, deps, scan_tests=False) + + def test_unreachable_in_deps(self): + target_cfg = self.get_pkg("four") + package_path = [get_linker_files_dir("four-deps")] + pkg_cfg = packaging.build_config(ROOT, target_cfg, + packagepath=package_path) + deps = packaging.get_deps_for_targets(pkg_cfg, + [target_cfg.name, "addon-sdk"]) + self.failUnlessEqual(deps, ["addon-sdk", "four"]) + self.assertRaises(manifest.UnreachablePrefixError, + manifest.build_manifest, + target_cfg, pkg_cfg, deps, scan_tests=False) + +class Contents(unittest.TestCase): + + def run_in_subdir(self, dirname, f, *args, **kwargs): + top = os.path.abspath(os.getcwd()) + basedir = os.path.abspath(os.path.join(".test_tmp",self.id(),dirname)) + if os.path.isdir(basedir): + assert basedir.startswith(top) + shutil.rmtree(basedir) + os.makedirs(basedir) + try: + os.chdir(basedir) + return f(basedir, *args, **kwargs) + finally: + os.chdir(top) + + def assertIn(self, what, inside_what): + self.failUnless(what in inside_what, inside_what) + + def test_jetpackID(self): + # this uses "id": "jid7", to which a @jetpack should be appended + seven = get_linker_files_dir("seven") + def _test(basedir): + stdout = StringIO() + shutil.copytree(seven, "seven") + os.chdir("seven") + try: + # regrettably, run() always finishes with sys.exit() + cuddlefish.run(["xpi", "--no-strip-xpi"], + stdout=stdout) + except SystemExit, e: + self.failUnlessEqual(e.args[0], 0) + zf = zipfile.ZipFile("seven.xpi", "r") + hopts = json.loads(zf.read("harness-options.json")) + self.failUnlessEqual(hopts["jetpackID"], "jid7@jetpack") + self.run_in_subdir("x", _test) + + def test_jetpackID_suffix(self): + # this uses "id": "jid1@jetpack", so no suffix should be appended + one = get_linker_files_dir("one") + def _test(basedir): + stdout = StringIO() + shutil.copytree(one, "one") + os.chdir("one") + try: + # regrettably, run() always finishes with sys.exit() + cuddlefish.run(["xpi", "--no-strip-xpi"], + stdout=stdout) + except SystemExit, e: + self.failUnlessEqual(e.args[0], 0) + zf = zipfile.ZipFile("one.xpi", "r") + hopts = json.loads(zf.read("harness-options.json")) + self.failUnlessEqual(hopts["jetpackID"], "jid1@jetpack") + self.run_in_subdir("x", _test) + + def test_strip_default(self): + seven = get_linker_files_dir("seven") + # now run 'cfx xpi' in that directory, except put the generated .xpi + # elsewhere + def _test(basedir): + stdout = StringIO() + shutil.copytree(seven, "seven") + os.chdir("seven") + try: + # regrettably, run() always finishes with sys.exit() + cuddlefish.run(["xpi"], # --strip-xpi is now the default + stdout=stdout) + except SystemExit, e: + self.failUnlessEqual(e.args[0], 0) + zf = zipfile.ZipFile("seven.xpi", "r") + names = zf.namelist() + # problem found in bug 664840 was that an addon + # without an explicit tests/ directory would copy all files from + # the package into a bogus JID-PKGNAME-tests/ directory, so check + # for that + testfiles = [fn for fn in names if "seven/tests" in fn] + self.failUnlessEqual([], testfiles) + # another problem was that data files were being stripped from + # the XPI. Note that data/ is only supposed to be included if a + # module that actually gets used does a require("self") . + self.assertIn("resources/seven/data/text.data", + names) + self.failIf("seven/lib/unused.js" + in names, names) + self.run_in_subdir("x", _test) + + def test_no_strip(self): + seven = get_linker_files_dir("seven") + def _test(basedir): + stdout = StringIO() + shutil.copytree(seven, "seven") + os.chdir("seven") + try: + # regrettably, run() always finishes with sys.exit() + cuddlefish.run(["xpi", "--no-strip-xpi"], + stdout=stdout) + except SystemExit, e: + self.failUnlessEqual(e.args[0], 0) + zf = zipfile.ZipFile("seven.xpi", "r") + names = zf.namelist() + self.assertIn("resources/addon-sdk/lib/sdk/loader/cuddlefish.js", names) + testfiles = [fn for fn in names if "seven/tests" in fn] + self.failUnlessEqual([], testfiles) + self.assertIn("resources/seven/data/text.data", + names) + self.failUnless("resources/seven/lib/unused.js" + in names, names) + self.run_in_subdir("x", _test) + + +if __name__ == '__main__': + unittest.main() diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/test_manifest.py b/addon-sdk/source/python-lib/cuddlefish/tests/test_manifest.py new file mode 100644 index 000000000..1bced1234 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_manifest.py @@ -0,0 +1,257 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import unittest +from StringIO import StringIO +from cuddlefish.manifest import scan_module + +class Extra: + def failUnlessKeysAre(self, d, keys): + self.failUnlessEqual(sorted(d.keys()), sorted(keys)) + +class Require(unittest.TestCase, Extra): + def scan(self, text): + lines = StringIO(text).readlines() + requires, problems, locations = scan_module("fake.js", lines) + self.failUnlessEqual(problems, False) + return requires + + def scan_locations(self, text): + lines = StringIO(text).readlines() + requires, problems, locations = scan_module("fake.js", lines) + self.failUnlessEqual(problems, False) + return requires, locations + + def test_modules(self): + mod = """var foo = require('one');""" + requires = self.scan(mod) + self.failUnlessKeysAre(requires, ["one"]) + + mod = """var foo = require(\"one\");""" + requires = self.scan(mod) + self.failUnlessKeysAre(requires, ["one"]) + + mod = """var foo=require( 'one' ) ; """ + requires = self.scan(mod) + self.failUnlessKeysAre(requires, ["one"]) + + mod = """var foo = require('o'+'ne'); // tricky, denied""" + requires = self.scan(mod) + self.failUnlessKeysAre(requires, []) + + mod = """require('one').immediately.do().stuff();""" + requires, locations = self.scan_locations(mod) + self.failUnlessKeysAre(requires, ["one"]) + self.failUnlessEqual(locations, {"one": 1}) + + # these forms are commented out, and thus ignored + + mod = """// var foo = require('one');""" + requires = self.scan(mod) + self.failUnlessKeysAre(requires, []) + + mod = """/* var foo = require('one');""" + requires = self.scan(mod) + self.failUnlessKeysAre(requires, []) + + mod = """ * var foo = require('one');""" + requires = self.scan(mod) + self.failUnlessKeysAre(requires, []) + + mod = """ ' var foo = require('one');""" + requires = self.scan(mod) + self.failUnlessKeysAre(requires, ["one"]) + + mod = """ \" var foo = require('one');""" + requires = self.scan(mod) + self.failUnlessKeysAre(requires, ["one"]) + + # multiple requires + + mod = """const foo = require('one'); + const foo = require('two');""" + requires, locations = self.scan_locations(mod) + self.failUnlessKeysAre(requires, ["one", "two"]) + self.failUnlessEqual(locations["one"], 1) + self.failUnlessEqual(locations["two"], 2) + + mod = """const foo = require('repeated'); + const bar = require('repeated'); + const baz = require('repeated');""" + requires, locations = self.scan_locations(mod) + self.failUnlessKeysAre(requires, ["repeated"]) + self.failUnlessEqual(locations["repeated"], 1) # first occurrence + + mod = """const foo = require('one'); const foo = require('two');""" + requires = self.scan(mod) + self.failUnlessKeysAre(requires, ["one", "two"]) + + # define calls + + mod = """define('one', ['two', 'numbers/three'], function(t, th) {});""" + requires = self.scan(mod) + self.failUnlessKeysAre(requires, ["two", "numbers/three"]) + + mod = """define( + ['odd', + "numbers/four"], function() {});""" + requires = self.scan(mod) + self.failUnlessKeysAre(requires, ["odd", "numbers/four"]) + + mod = """define(function(require, exports, module) { + var a = require("some/module/a"), + b = require('b/v1'); + exports.a = a; + //This is a fakeout: require('bad'); + /* And another var bad = require('bad2'); */ + require('foo').goFoo(); + });""" + requires = self.scan(mod) + self.failUnlessKeysAre(requires, ["some/module/a", "b/v1", "foo"]) + + mod = """define ( + "foo", + ["bar"], function (bar) { + var me = require("me"); + } + )""" + requires = self.scan(mod) + self.failUnlessKeysAre(requires, ["bar", "me"]) + + mod = """define(['se' + 'ven', 'eight', nine], function () {});""" + requires = self.scan(mod) + self.failUnlessKeysAre(requires, ["eight"]) + + # async require calls + + mod = """require(['one'], function(one) {var o = require("one");});""" + requires = self.scan(mod) + self.failUnlessKeysAre(requires, ["one"]) + + mod = """require([ 'one' ], function(one) {var t = require("two");});""" + requires = self.scan(mod) + self.failUnlessKeysAre(requires, ["one", "two"]) + + mod = """require ( ['two', 'numbers/three'], function(t, th) {});""" + requires = self.scan(mod) + self.failUnlessKeysAre(requires, ["two", "numbers/three"]) + + mod = """require ( + ["bar", "fa" + 'ke' ], function (bar) { + var me = require("me"); + // require("bad").doBad(); + } + )""" + requires = self.scan(mod) + self.failUnlessKeysAre(requires, ["bar", "me"]) + +def scan2(text, fn="fake.js"): + stderr = StringIO() + lines = StringIO(text).readlines() + requires, problems, locations = scan_module(fn, lines, stderr) + stderr.seek(0) + return requires, problems, stderr.readlines() + +class Chrome(unittest.TestCase, Extra): + + def test_ignore_loader(self): + # we specifically ignore the loader itself + mod = """let {Cc,Ci} = require('chrome');""" + requires, problems, err = scan2(mod, "blah/cuddlefish.js") + self.failUnlessKeysAre(requires, ["chrome"]) + self.failUnlessEqual(problems, False) + self.failUnlessEqual(err, []) + + def test_chrome(self): + mod = """let {Cc,Ci} = require('chrome');""" + requires, problems, err = scan2(mod) + self.failUnlessKeysAre(requires, ["chrome"]) + self.failUnlessEqual(problems, False) + self.failUnlessEqual(err, []) + + mod = """var foo = require('foo'); + let {Cc,Ci} = require('chrome');""" + requires, problems, err = scan2(mod) + self.failUnlessKeysAre(requires, ["foo", "chrome"]) + self.failUnlessEqual(problems, False) + self.failUnlessEqual(err, []) + + mod = """let c = require('chrome');""" + requires, problems, err = scan2(mod) + self.failUnlessKeysAre(requires, ["chrome"]) + self.failUnlessEqual(problems, False) + self.failUnlessEqual(err, []) + + mod = """var foo = require('foo'); + let c = require('chrome');""" + requires, problems, err = scan2(mod) + self.failUnlessKeysAre(requires, ["foo", "chrome"]) + self.failUnlessEqual(problems, False) + self.failUnlessEqual(err, []) + + def test_not_chrome(self): + # from bug 596595 + mod = r'soughtLines: new RegExp("^\\s*(\\[[0-9 .]*\\])?\\s*\\(\\((EE|WW)\\)|.* [Cc]hipsets?: \\)|\\s*Backtrace")' + requires, problems, err = scan2(mod) + self.failUnlessKeysAre(requires, []) + self.failUnlessEqual((problems,err), (False, [])) + + def test_not_chrome2(self): + # from bug 655788 + mod = r"var foo = 'some stuff Cr';" + requires, problems, err = scan2(mod) + self.failUnlessKeysAre(requires, []) + self.failUnlessEqual((problems,err), (False, [])) + +class BadChrome(unittest.TestCase, Extra): + def test_bad_alias(self): + # using Components.* gets you an error, with a message that teaches + # you the correct approach. + mod = """let Cc = Components.classes; + let Cu = Components.utils; + """ + requires, problems, err = scan2(mod) + self.failUnlessKeysAre(requires, []) + self.failUnlessEqual(problems, True) + self.failUnlessEqual(err[1], "The following lines from file fake.js:\n") + self.failUnlessEqual(err[2], " 1: let Cc = Components.classes;\n") + self.failUnlessEqual(err[3], " 2: let Cu = Components.utils;\n") + self.failUnlessEqual(err[4], "use 'Components' to access chrome authority. To do so, you need to add a\n") + self.failUnlessEqual(err[5], "line somewhat like the following:\n") + self.failUnlessEqual(err[7], ' const {Cc,Cu} = require("chrome");\n') + self.failUnlessEqual(err[9], "Then you can use any shortcuts to its properties that you import from the\n") + + def test_bad_misc(self): + # If it looks like you're using something that doesn't have an alias, + # the warning also suggests a better way. + mod = """if (Components.isSuccessCode(foo)) + """ + requires, problems, err = scan2(mod) + self.failUnlessKeysAre(requires, []) + self.failUnlessEqual(problems, True) + self.failUnlessEqual(err[1], "The following lines from file fake.js:\n") + self.failUnlessEqual(err[2], " 1: if (Components.isSuccessCode(foo))\n") + self.failUnlessEqual(err[3], "use 'Components' to access chrome authority. To do so, you need to add a\n") + self.failUnlessEqual(err[4], "line somewhat like the following:\n") + self.failUnlessEqual(err[6], ' const {components} = require("chrome");\n') + self.failUnlessEqual(err[8], "Then you can use any shortcuts to its properties that you import from the\n") + + def test_chrome_components(self): + # Bug 636145/774636: We no longer tolerate usages of "Components", + # even when adding `require("chrome")` to your module. + mod = """require("chrome"); + var ios = Components.classes['@mozilla.org/network/io-service;1'];""" + requires, problems, err = scan2(mod) + self.failUnlessKeysAre(requires, ["chrome"]) + self.failUnlessEqual(problems, True) + self.failUnlessEqual(err[1], "The following lines from file fake.js:\n") + self.failUnlessEqual(err[2], " 2: var ios = Components.classes['@mozilla.org/network/io-service;1'];\n") + self.failUnlessEqual(err[3], "use 'Components' to access chrome authority. To do so, you need to add a\n") + self.failUnlessEqual(err[4], "line somewhat like the following:\n") + self.failUnlessEqual(err[6], ' const {Cc} = require("chrome");\n') + self.failUnlessEqual(err[8], "Then you can use any shortcuts to its properties that you import from the\n") + +if __name__ == '__main__': + unittest.main() diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/test_packaging.py b/addon-sdk/source/python-lib/cuddlefish/tests/test_packaging.py new file mode 100644 index 000000000..6944dc394 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_packaging.py @@ -0,0 +1,117 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import unittest + +from cuddlefish import packaging +from cuddlefish.bunch import Bunch + +tests_path = os.path.abspath(os.path.dirname(__file__)) +static_files_path = os.path.join(tests_path, 'static-files') + +def get_configs(pkg_name, dirname='static-files'): + root_path = os.path.join(tests_path, dirname) + pkg_path = os.path.join(root_path, 'packages', pkg_name) + if not (os.path.exists(pkg_path) and os.path.isdir(pkg_path)): + raise Exception('path does not exist: %s' % pkg_path) + target_cfg = packaging.get_config_in_dir(pkg_path) + pkg_cfg = packaging.build_config(root_path, target_cfg) + deps = packaging.get_deps_for_targets(pkg_cfg, [pkg_name]) + build = packaging.generate_build_for_target( + pkg_cfg=pkg_cfg, + target=pkg_name, + deps=deps, + is_running_tests=True, + ) + return Bunch(target_cfg=target_cfg, pkg_cfg=pkg_cfg, build=build) + +class PackagingTests(unittest.TestCase): + def test_bug_588661(self): + configs = get_configs('foo', 'bug-588661-files') + self.assertEqual(configs.build.loader, + 'foo/lib/foo-loader.js') + + def test_bug_614712(self): + configs = get_configs('commonjs-naming', 'bug-614712-files') + packages = configs.pkg_cfg.packages + base = os.path.join(tests_path, 'bug-614712-files', 'packages') + self.assertEqual(packages['original-naming'].tests, + [os.path.join(base, 'original-naming', 'tests')]) + self.assertEqual(packages['commonjs-naming'].tests, + [os.path.join(base, 'commonjs-naming', 'test')]) + + def test_basic(self): + configs = get_configs('aardvark') + packages = configs.pkg_cfg.packages + + self.assertTrue('addon-sdk' in packages) + self.assertTrue('aardvark' in packages) + self.assertTrue('addon-sdk' in packages.aardvark.dependencies) + self.assertEqual(packages['addon-sdk'].loader, 'lib/sdk/loader/cuddlefish.js') + self.assertTrue(packages.aardvark.main == 'main') + self.assertTrue(packages.aardvark.version == "1.0") + +class PackagePath(unittest.TestCase): + def test_packagepath(self): + root_path = os.path.join(tests_path, 'static-files') + pkg_path = os.path.join(root_path, 'packages', 'minimal') + target_cfg = packaging.get_config_in_dir(pkg_path) + pkg_cfg = packaging.build_config(root_path, target_cfg) + base_packages = set(pkg_cfg.packages.keys()) + ppath = [os.path.join(tests_path, 'bug-611495-files')] + pkg_cfg2 = packaging.build_config(root_path, target_cfg, packagepath=ppath) + all_packages = set(pkg_cfg2.packages.keys()) + self.assertEqual(sorted(["jspath-one"]), + sorted(all_packages - base_packages)) + +class Directories(unittest.TestCase): + # for bug 652227 + packages_path = os.path.join(tests_path, "bug-652227-files", "packages") + def get_config(self, pkg_name): + pkg_path = os.path.join(tests_path, "bug-652227-files", "packages", + pkg_name) + return packaging.get_config_in_dir(pkg_path) + + def test_explicit_lib(self): + # package.json provides .lib + p = self.get_config('explicit-lib') + self.assertEqual(os.path.abspath(p.lib[0]), + os.path.abspath(os.path.join(self.packages_path, + "explicit-lib", + "alt2-lib"))) + + def test_directories_lib(self): + # package.json provides .directories.lib + p = self.get_config('explicit-dir-lib') + self.assertEqual(os.path.abspath(p.lib[0]), + os.path.abspath(os.path.join(self.packages_path, + "explicit-dir-lib", + "alt-lib"))) + + def test_lib(self): + # package.json is empty, but lib/ exists + p = self.get_config("default-lib") + self.assertEqual(os.path.abspath(p.lib[0]), + os.path.abspath(os.path.join(self.packages_path, + "default-lib", + "lib"))) + + def test_root(self): + # package.json is empty, no lib/, so files are in root + p = self.get_config('default-root') + self.assertEqual(os.path.abspath(p.lib[0]), + os.path.abspath(os.path.join(self.packages_path, + "default-root"))) + + def test_locale(self): + # package.json is empty, but locale/ exists and should be used + p = self.get_config("default-locale") + self.assertEqual(os.path.abspath(p.locale), + os.path.abspath(os.path.join(self.packages_path, + "default-locale", + "locale"))) + +if __name__ == "__main__": + unittest.main() diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/test_preflight.py b/addon-sdk/source/python-lib/cuddlefish/tests/test_preflight.py new file mode 100644 index 000000000..571b7911b --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_preflight.py @@ -0,0 +1,147 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import os, shutil +import simplejson as json +import unittest +import hashlib +import base64 +from cuddlefish import preflight +from StringIO import StringIO + +class Util(unittest.TestCase): + def get_basedir(self): + return os.path.join(".test_tmp", self.id()) + def make_basedir(self): + basedir = self.get_basedir() + if os.path.isdir(basedir): + here = os.path.abspath(os.getcwd()) + assert os.path.abspath(basedir).startswith(here) # safety + shutil.rmtree(basedir) + os.makedirs(basedir) + return basedir + + def test_base62(self): + for i in range(1000): + h = hashlib.sha1(str(i)).digest() + s1 = base64.b64encode(h, "AB").strip("=") + s2 = base64.b64encode(h).strip("=").replace("+","A").replace("/","B") + self.failUnlessEqual(s1, s2) + + def write(self, config): + basedir = self.get_basedir() + fn = os.path.join(basedir, "package.json") + open(fn,"w").write(config) + def read(self): + basedir = self.get_basedir() + fn = os.path.join(basedir, "package.json") + return open(fn,"r").read() + + def get_cfg(self): + cfg = json.loads(self.read()) + if "name" not in cfg: + # the cfx parser always provides a name, even if package.json + # doesn't contain one + cfg["name"] = "pretend name" + return cfg + + def parse(self, keydata): + fields = {} + fieldnames = [] + for line in keydata.split("\n"): + if line.strip(): + k,v = line.split(":", 1) + k = k.strip() ; v = v.strip() + fields[k] = v + fieldnames.append(k) + return fields, fieldnames + + def test_preflight(self): + basedir = self.make_basedir() + fn = os.path.join(basedir, "package.json") + + # empty config is not ok: need id (name is automatically supplied) + config_orig = "{}" + self.write(config_orig) + out = StringIO() + cfg = self.get_cfg() + config_was_ok, modified = preflight.preflight_config(cfg, fn, + stderr=out) + self.failUnlessEqual(config_was_ok, False) + self.failUnlessEqual(modified, True) + backup_fn = os.path.join(basedir, "package.json.backup") + config_backup = open(backup_fn,"r").read() + self.failUnlessEqual(config_backup, config_orig) + config = json.loads(self.read()) + self.failIf("name" in config) + self.failUnless("id" in config) + self.failUnless(config["id"].startswith("jid1-"), config["id"]) + self.failUnlessEqual(out.getvalue().strip(), + "No 'id' in package.json: creating a new ID for you.") + os.unlink(backup_fn) + + # just a name? we add the id + config_orig = '{"name": "my-awesome-package"}' + self.write(config_orig) + out = StringIO() + cfg = self.get_cfg() + config_was_ok, modified = preflight.preflight_config(cfg, fn, + stderr=out) + self.failUnlessEqual(config_was_ok, False) + self.failUnlessEqual(modified, True) + backup_fn = os.path.join(basedir, "package.json.backup") + config_backup = open(backup_fn,"r").read() + self.failUnlessEqual(config_backup, config_orig) + config = json.loads(self.read()) + self.failUnlessEqual(config["name"], "my-awesome-package") + self.failUnless("id" in config) + self.failUnless(config["id"].startswith("jid1-"), config["id"]) + jid = str(config["id"]) + self.failUnlessEqual(out.getvalue().strip(), + "No 'id' in package.json: creating a new ID for you.") + os.unlink(backup_fn) + + # name and valid id? great! ship it! + config2 = '{"name": "my-awesome-package", "id": "%s"}' % jid + self.write(config2) + out = StringIO() + cfg = self.get_cfg() + config_was_ok, modified = preflight.preflight_config(cfg, fn, + stderr=out) + self.failUnlessEqual(config_was_ok, True) + self.failUnlessEqual(modified, False) + config2a = self.read() + self.failUnlessEqual(config2a, config2) + self.failUnlessEqual(out.getvalue().strip(), "") + + # name and anonymous ID? without asking to see its papers, ship it + config3 = '{"name": "my-old-skool-package", "id": "anonid0-deadbeef"}' + self.write(config3) + out = StringIO() + cfg = self.get_cfg() + config_was_ok, modified = preflight.preflight_config(cfg, fn, + stderr=out) + self.failUnlessEqual(config_was_ok, True) + self.failUnlessEqual(modified, False) + config3a = self.read() + self.failUnlessEqual(config3a, config3) + self.failUnlessEqual(out.getvalue().strip(), "") + + # name and old-style ID? with nostalgic trepidation, ship it + config4 = '{"name": "my-old-skool-package", "id": "foo@bar.baz"}' + self.write(config4) + out = StringIO() + cfg = self.get_cfg() + config_was_ok, modified = preflight.preflight_config(cfg, fn, + stderr=out) + self.failUnlessEqual(config_was_ok, True) + self.failUnlessEqual(modified, False) + config4a = self.read() + self.failUnlessEqual(config4a, config4) + self.failUnlessEqual(out.getvalue().strip(), "") + + +if __name__ == '__main__': + unittest.main() diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/test_property_parser.py b/addon-sdk/source/python-lib/cuddlefish/tests/test_property_parser.py new file mode 100644 index 000000000..4988f8ef5 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_property_parser.py @@ -0,0 +1,93 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import unittest + +from cuddlefish.property_parser import parse, MalformedLocaleFileError + +class TestParser(unittest.TestCase): + + def test_parse(self): + lines = [ + # Comments are striped only if `#` is the first non-space character + "sharp=#can be in value", + "# comment", + "#key=value", + " # comment2", + + "keyWithNoValue=", + "valueWithSpaces= ", + "valueWithMultilineSpaces= \\", + " \\", + " ", + + # All spaces before/after are striped + " key = value ", + "key2=value2", + # Keys can contain '%' + "%s key=%s value", + + # Accept empty lines + "", + " ", + + # Multiline string must use backslash at end of lines + "multi=line\\", "value", + # With multiline string, left spaces are stripped ... + "some= spaces\\", " are\\ ", " stripped ", + # ... but not right spaces, except the last line! + "but=not \\", "all of \\", " them ", + + # Explicit [other] plural definition + "explicitPlural[one] = one", + "explicitPlural[other] = other", + + # Implicit [other] plural definition + "implicitPlural[one] = one", + "implicitPlural = other", # This key is the [other] one + ] + # Ensure that all lines end with a `\n` + # And that strings are unicode ones (parser code relies on it) + lines = [unicode(l + "\n") for l in lines] + pairs = parse(lines) + expected = { + "sharp": "#can be in value", + + "key": "value", + "key2": "value2", + "%s key": "%s value", + + "keyWithNoValue": "", + "valueWithSpaces": "", + "valueWithMultilineSpaces": "", + + "multi": "linevalue", + "some": "spacesarestripped", + "but": "not all of them", + + "implicitPlural": { + "one": "one", + "other": "other" + }, + "explicitPlural": { + "one": "one", + "other": "other" + }, + } + self.assertEqual(pairs, expected) + + def test_exceptions(self): + self.failUnlessRaises(MalformedLocaleFileError, parse, + ["invalid line with no key value"]) + self.failUnlessRaises(MalformedLocaleFileError, parse, + ["plural[one]=plural with no [other] value"]) + self.failUnlessRaises(MalformedLocaleFileError, parse, + ["multiline with no last empty line=\\"]) + self.failUnlessRaises(MalformedLocaleFileError, parse, + ["=no key"]) + self.failUnlessRaises(MalformedLocaleFileError, parse, + [" =only spaces in key"]) + +if __name__ == "__main__": + unittest.main() diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/test_rdf.py b/addon-sdk/source/python-lib/cuddlefish/tests/test_rdf.py new file mode 100644 index 000000000..5d9254e4e --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_rdf.py @@ -0,0 +1,54 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import unittest +import xml.dom.minidom +import os.path + +from cuddlefish import rdf, packaging + +parent = os.path.dirname +test_dir = parent(os.path.abspath(__file__)) +template_dir = os.path.join(parent(test_dir), "../../app-extension") + +class RDFTests(unittest.TestCase): + def testBug567660(self): + obj = rdf.RDF() + data = u'\u2026'.encode('utf-8') + x = '<?xml version="1.0" encoding="utf-8"?><blah>%s</blah>' % data + obj.dom = xml.dom.minidom.parseString(x) + self.assertEqual(obj.dom.documentElement.firstChild.nodeValue, + u'\u2026') + self.assertEqual(str(obj).replace("\n",""), x.replace("\n","")) + + def failUnlessIn(self, substring, s, msg=""): + if substring not in s: + self.fail("(%s) substring '%s' not in string '%s'" + % (msg, substring, s)) + + def testUnpack(self): + basedir = os.path.join(test_dir, "bug-715340-files") + for n in ["pkg-1-pack", "pkg-2-unpack", "pkg-3-pack"]: + cfg = packaging.get_config_in_dir(os.path.join(basedir, n)) + m = rdf.gen_manifest(template_dir, cfg, jid="JID") + if n.endswith("-pack"): + # these ones should remain packed + self.failUnlessEqual(m.get("em:unpack"), "false") + self.failUnlessIn("<em:unpack>false</em:unpack>", str(m), n) + else: + # and these should be unpacked + self.failUnlessEqual(m.get("em:unpack"), "true") + self.failUnlessIn("<em:unpack>true</em:unpack>", str(m), n) + + def testTitle(self): + basedir = os.path.join(test_dir, 'bug-906359-files') + for n in ['title', 'fullName', 'none']: + cfg = packaging.get_config_in_dir(os.path.join(basedir, n)) + m = rdf.gen_manifest(template_dir, cfg, jid='JID') + self.failUnlessEqual(m.get('em:name'), 'a long ' + n) + self.failUnlessIn('<em:name>a long ' + n + '</em:name>', str(m), n) + + +if __name__ == '__main__': + unittest.main() diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/test_runner.py b/addon-sdk/source/python-lib/cuddlefish/tests/test_runner.py new file mode 100644 index 000000000..26583abd1 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_runner.py @@ -0,0 +1,27 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +def xulrunner_app_runner_doctests(): + """ + >>> import sys + >>> from cuddlefish import runner + >>> runner.XulrunnerAppRunner(binary='foo') + Traceback (most recent call last): + ... + Exception: Binary path does not exist foo + + >>> runner.XulrunnerAppRunner(binary=sys.executable) + Traceback (most recent call last): + ... + ValueError: application.ini not found in cmdargs + + >>> runner.XulrunnerAppRunner(binary=sys.executable, + ... cmdargs=['application.ini']) + Traceback (most recent call last): + ... + ValueError: file does not exist: 'application.ini' + """ + + pass diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/test_util.py b/addon-sdk/source/python-lib/cuddlefish/tests/test_util.py new file mode 100644 index 000000000..aa636a48c --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_util.py @@ -0,0 +1,22 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +import unittest +from cuddlefish.manifest import filter_filenames, filter_dirnames + +class Filter(unittest.TestCase): + def test_filter_filenames(self): + names = ["foo", "bar.js", "image.png", + ".hidden", "foo~", ".foo.swp", "bar.js.swp"] + self.failUnlessEqual(sorted(filter_filenames(names)), + sorted(["foo", "bar.js", "image.png"])) + + def test_filter_dirnames(self): + names = ["subdir", "data", ".git", ".hg", ".svn", "defaults"] + self.failUnlessEqual(sorted(filter_dirnames(names)), + sorted(["subdir", "data", "defaults"])) + +if __name__ == '__main__': + unittest.main() diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/test_version.py b/addon-sdk/source/python-lib/cuddlefish/tests/test_version.py new file mode 100644 index 000000000..814c57c89 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_version.py @@ -0,0 +1,28 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import unittest +import shutil + +from cuddlefish._version import get_versions + +class Version(unittest.TestCase): + def get_basedir(self): + return os.path.join(".test_tmp", self.id()) + def make_basedir(self): + basedir = self.get_basedir() + if os.path.isdir(basedir): + here = os.path.abspath(os.getcwd()) + assert os.path.abspath(basedir).startswith(here) # safety + shutil.rmtree(basedir) + os.makedirs(basedir) + return basedir + + def test_current_version(self): + # the SDK should be able to determine its own version. We don't care + # what it is, merely that it can be computed. + version = get_versions()["version"] + self.failUnless(isinstance(version, str), (version, type(version))) + self.failUnless(len(version) > 0, version) diff --git a/addon-sdk/source/python-lib/cuddlefish/tests/test_xpi.py b/addon-sdk/source/python-lib/cuddlefish/tests/test_xpi.py new file mode 100644 index 000000000..bbefee3b2 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/tests/test_xpi.py @@ -0,0 +1,310 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import unittest +import zipfile +import pprint +import shutil + +import simplejson as json +from cuddlefish import xpi, packaging, manifest, buildJID +from cuddlefish.tests import test_packaging +from test_linker import up + +import xml.etree.ElementTree as ElementTree + +xpi_template_path = os.path.join(test_packaging.static_files_path, + 'xpi-template') + +fake_manifest = '<RDF><!-- Extension metadata is here. --></RDF>' + + +class Bug588119Tests(unittest.TestCase): + def makexpi(self, pkg_name): + self.xpiname = "%s.xpi" % pkg_name + create_xpi(self.xpiname, pkg_name, 'bug-588119-files') + self.xpi = zipfile.ZipFile(self.xpiname, 'r') + options = self.xpi.read('harness-options.json') + self.xpi_harness_options = json.loads(options) + + def setUp(self): + self.xpiname = None + self.xpi = None + + def tearDown(self): + if self.xpi: + self.xpi.close() + if self.xpiname and os.path.exists(self.xpiname): + os.remove(self.xpiname) + + def testPackageWithImplicitIcon(self): + self.makexpi('implicit-icon') + assert 'icon.png' in self.xpi.namelist() + + def testPackageWithImplicitIcon64(self): + self.makexpi('implicit-icon') + assert 'icon64.png' in self.xpi.namelist() + + def testPackageWithExplicitIcon(self): + self.makexpi('explicit-icon') + assert 'icon.png' in self.xpi.namelist() + + def testPackageWithExplicitIcon64(self): + self.makexpi('explicit-icon') + assert 'icon64.png' in self.xpi.namelist() + + def testPackageWithNoIcon(self): + self.makexpi('no-icon') + assert 'icon.png' not in self.xpi.namelist() + + def testIconPathNotInHarnessOptions(self): + self.makexpi('implicit-icon') + assert 'icon' not in self.xpi_harness_options + + def testIcon64PathNotInHarnessOptions(self): + self.makexpi('implicit-icon') + assert 'icon64' not in self.xpi_harness_options + +class ExtraHarnessOptions(unittest.TestCase): + def setUp(self): + self.xpiname = None + self.xpi = None + + def tearDown(self): + if self.xpi: + self.xpi.close() + if self.xpiname and os.path.exists(self.xpiname): + os.remove(self.xpiname) + + def testOptions(self): + pkg_name = "extra-options" + self.xpiname = "%s.xpi" % pkg_name + create_xpi(self.xpiname, pkg_name, "bug-669274-files", + extra_harness_options={"builderVersion": "futuristic"}) + self.xpi = zipfile.ZipFile(self.xpiname, 'r') + options = self.xpi.read('harness-options.json') + hopts = json.loads(options) + self.failUnless("builderVersion" in hopts) + self.failUnlessEqual(hopts["builderVersion"], "futuristic") + + def testBadOptionName(self): + pkg_name = "extra-options" + self.xpiname = "%s.xpi" % pkg_name + self.failUnlessRaises(xpi.HarnessOptionAlreadyDefinedError, + create_xpi, + self.xpiname, pkg_name, "bug-669274-files", + extra_harness_options={"main": "already in use"}) + +class SmallXPI(unittest.TestCase): + def setUp(self): + self.root = up(os.path.abspath(__file__), 4) + def get_linker_files_dir(self, name): + return os.path.join(up(os.path.abspath(__file__)), "linker-files", name) + def get_pkg(self, name): + d = self.get_linker_files_dir(name) + return packaging.get_config_in_dir(d) + + def get_basedir(self): + return os.path.join(".test_tmp", self.id()) + def make_basedir(self): + basedir = self.get_basedir() + if os.path.isdir(basedir): + here = os.path.abspath(os.getcwd()) + assert os.path.abspath(basedir).startswith(here) # safety + shutil.rmtree(basedir) + os.makedirs(basedir) + return basedir + + def test_contents(self): + target_cfg = self.get_pkg("three") + package_path = [self.get_linker_files_dir("three-deps")] + pkg_cfg = packaging.build_config(self.root, target_cfg, + packagepath=package_path) + deps = packaging.get_deps_for_targets(pkg_cfg, + [target_cfg.name, "addon-sdk"]) + addon_sdk_dir = pkg_cfg.packages["addon-sdk"].lib[0] + m = manifest.build_manifest(target_cfg, pkg_cfg, deps, scan_tests=False) + used_files = list(m.get_used_files(True)) + here = up(os.path.abspath(__file__)) + def absify(*parts): + fn = os.path.join(here, "linker-files", *parts) + return os.path.abspath(fn) + expected = [absify(*parts) for parts in + [("three", "lib", "main.js"), + ("three-deps", "three-a", "lib", "main.js"), + ("three-deps", "three-a", "lib", "subdir", "subfile.js"), + ("three", "data", "msg.txt"), + ("three", "data", "subdir", "submsg.txt"), + ("three-deps", "three-b", "lib", "main.js"), + ("three-deps", "three-c", "lib", "main.js"), + ("three-deps", "three-c", "lib", "sub", "foo.js") + ]] + + add_addon_sdk= lambda path: os.path.join(addon_sdk_dir, path) + expected.extend([add_addon_sdk(module) for module in [ + os.path.join("sdk", "self.js"), + os.path.join("sdk", "core", "promise.js"), + os.path.join("sdk", "net", "url.js"), + os.path.join("sdk", "util", "object.js"), + os.path.join("sdk", "util", "array.js"), + os.path.join("sdk", "preferences", "service.js") + ]]) + + missing = set(expected) - set(used_files) + extra = set(used_files) - set(expected) + self.failUnlessEqual(list(missing), []) + self.failUnlessEqual(list(extra), []) + used_deps = m.get_used_packages() + + build = packaging.generate_build_for_target(pkg_cfg, target_cfg.name, + used_deps, + include_tests=False) + options = {'main': target_cfg.main} + options.update(build) + basedir = self.make_basedir() + xpi_name = os.path.join(basedir, "contents.xpi") + xpi.build_xpi(template_root_dir=xpi_template_path, + manifest=fake_manifest, + xpi_path=xpi_name, + harness_options=options, + limit_to=used_files) + x = zipfile.ZipFile(xpi_name, "r") + names = x.namelist() + expected = ["components/", + "components/harness.js", + # the real template also has 'bootstrap.js', but the fake + # one in tests/static-files/xpi-template doesn't + "harness-options.json", + "install.rdf", + "resources/", + "resources/addon-sdk/", + "resources/addon-sdk/lib/", + "resources/addon-sdk/lib/sdk/", + "resources/addon-sdk/lib/sdk/self.js", + "resources/addon-sdk/lib/sdk/core/", + "resources/addon-sdk/lib/sdk/util/", + "resources/addon-sdk/lib/sdk/net/", + "resources/addon-sdk/lib/sdk/core/promise.js", + "resources/addon-sdk/lib/sdk/util/object.js", + "resources/addon-sdk/lib/sdk/util/array.js", + "resources/addon-sdk/lib/sdk/net/url.js", + "resources/addon-sdk/lib/sdk/preferences/", + "resources/addon-sdk/lib/sdk/preferences/service.js", + "resources/three/", + "resources/three/lib/", + "resources/three/lib/main.js", + "resources/three/data/", + "resources/three/data/msg.txt", + "resources/three/data/subdir/", + "resources/three/data/subdir/submsg.txt", + "resources/three-a/", + "resources/three-a/lib/", + "resources/three-a/lib/main.js", + "resources/three-a/lib/subdir/", + "resources/three-a/lib/subdir/subfile.js", + "resources/three-b/", + "resources/three-b/lib/", + "resources/three-b/lib/main.js", + "resources/three-c/", + "resources/three-c/lib/", + "resources/three-c/lib/main.js", + "resources/three-c/lib/sub/", + "resources/three-c/lib/sub/foo.js", + # notably absent: three-a/lib/unused.js + "locale/", + "locale/fr-FR.json", + "locales.json", + ] + # showing deltas makes failures easier to investigate + missing = set(expected) - set(names) + extra = set(names) - set(expected) + self.failUnlessEqual((list(missing), list(extra)), ([], [])) + self.failUnlessEqual(sorted(names), sorted(expected)) + + # check locale files + localedata = json.loads(x.read("locales.json")) + self.failUnlessEqual(sorted(localedata["locales"]), sorted(["fr-FR"])) + content = x.read("locale/fr-FR.json") + locales = json.loads(content) + # Locale files are merged into one. + # Conflicts are silently resolved by taking last package translation, + # so that we get "No" translation from three-c instead of three-b one. + self.failUnlessEqual(locales, json.loads(u''' + { + "No": "Nein", + "one": "un", + "What?": "Quoi?", + "Yes": "Oui", + "plural": { + "other": "other", + "one": "one" + }, + "uft8_value": "\u00e9" + }''')) + + +def document_dir(name): + if name in ['packages', 'xpi-template']: + dirname = os.path.join(test_packaging.static_files_path, name) + document_dir_files(dirname) + elif name == 'xpi-output': + create_xpi('test-xpi.xpi') + document_zip_file('test-xpi.xpi') + os.remove('test-xpi.xpi') + else: + raise Exception('unknown dir: %s' % name) + +def normpath(path): + """ + Make a platform-specific relative path use '/' as a separator. + """ + + return path.replace(os.path.sep, '/') + +def document_zip_file(path): + zip = zipfile.ZipFile(path, 'r') + for name in sorted(zip.namelist()): + contents = zip.read(name) + lines = contents.splitlines() + if len(lines) == 1 and name.endswith('.json') and len(lines[0]) > 75: + # Ideally we would json-decode this, but it results + # in an annoying 'u' before every string literal, + # since json decoding makes all strings unicode. + contents = eval(contents) + contents = pprint.pformat(contents) + lines = contents.splitlines() + contents = "\n ".join(lines) + print "%s:\n %s" % (normpath(name), contents) + zip.close() + +def document_dir_files(path): + filename_contents_tuples = [] + for dirpath, dirnames, filenames in os.walk(path): + relpath = dirpath[len(path)+1:] + for filename in filenames: + abspath = os.path.join(dirpath, filename) + contents = open(abspath, 'r').read() + contents = "\n ".join(contents.splitlines()) + relfilename = os.path.join(relpath, filename) + filename_contents_tuples.append((normpath(relfilename), contents)) + filename_contents_tuples.sort() + for filename, contents in filename_contents_tuples: + print "%s:" % filename + print " %s" % contents + +def create_xpi(xpiname, pkg_name='aardvark', dirname='static-files', + extra_harness_options={}): + configs = test_packaging.get_configs(pkg_name, dirname) + options = {'main': configs.target_cfg.main, + 'jetpackID': buildJID(configs.target_cfg), } + options.update(configs.build) + xpi.build_xpi(template_root_dir=xpi_template_path, + manifest=fake_manifest, + xpi_path=xpiname, + harness_options=options, + extra_harness_options=extra_harness_options) + +if __name__ == '__main__': + unittest.main() diff --git a/addon-sdk/source/python-lib/cuddlefish/util.py b/addon-sdk/source/python-lib/cuddlefish/util.py new file mode 100644 index 000000000..513495a69 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/util.py @@ -0,0 +1,23 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +IGNORED_FILE_PREFIXES = ["."] +IGNORED_FILE_SUFFIXES = ["~", ".swp"] +IGNORED_DIRS = [".git", ".svn", ".hg"] + +def filter_filenames(filenames, ignored_files=[".hgignore"]): + for filename in filenames: + if filename in ignored_files: + continue + if any([filename.startswith(suffix) + for suffix in IGNORED_FILE_PREFIXES]): + continue + if any([filename.endswith(suffix) + for suffix in IGNORED_FILE_SUFFIXES]): + continue + yield filename + +def filter_dirnames(dirnames): + return [dirname for dirname in dirnames if dirname not in IGNORED_DIRS] diff --git a/addon-sdk/source/python-lib/cuddlefish/version_comparator.py b/addon-sdk/source/python-lib/cuddlefish/version_comparator.py new file mode 100644 index 000000000..fee1341fd --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/version_comparator.py @@ -0,0 +1,206 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +''' + This is a really crummy, slow Python implementation of the Mozilla + platform's nsIVersionComparator interface: + + https://developer.mozilla.org/En/NsIVersionComparator + + For more information, also see: + + http://dxr.mozilla.org/mozilla-central/source/xpcom/glue/nsVersionComparator.cpp +''' + +import re +import sys + +class VersionPart(object): + ''' + Examples: + + >>> VersionPart('1') + (1, None, 0, None) + + >>> VersionPart('1pre') + (1, 'pre', 0, None) + + >>> VersionPart('1pre10') + (1, 'pre', 10, None) + + >>> VersionPart('1pre10a') + (1, 'pre', 10, 'a') + + >>> VersionPart('1+') + (2, 'pre', 0, None) + + >>> VersionPart('*').numA == sys.maxint + True + + >>> VersionPart('1') < VersionPart('2') + True + + >>> VersionPart('2') > VersionPart('1') + True + + >>> VersionPart('1') == VersionPart('1') + True + + >>> VersionPart('1pre') > VersionPart('1') + False + + >>> VersionPart('1') < VersionPart('1pre') + False + + >>> VersionPart('1pre1') < VersionPart('1pre2') + True + + >>> VersionPart('1pre10b') > VersionPart('1pre10a') + True + + >>> VersionPart('1pre10b') == VersionPart('1pre10b') + True + + >>> VersionPart('1pre10a') < VersionPart('1pre10b') + True + + >>> VersionPart('1') > VersionPart('') + True + ''' + + _int_part = re.compile('[+-]?(\d*)(.*)') + _num_chars = '0123456789+-' + + def __init__(self, part): + self.numA = 0 + self.strB = None + self.numC = 0 + self.extraD = None + + if not part: + return + + if part == '*': + self.numA = sys.maxint + else: + match = self._int_part.match(part) + self.numA = int(match.group(1)) + self.strB = match.group(2) or None + if self.strB == '+': + self.strB = 'pre' + self.numA += 1 + elif self.strB: + i = 0 + num_found = -1 + for char in self.strB: + if char in self._num_chars: + num_found = i + break + i += 1 + if num_found != -1: + match = self._int_part.match(self.strB[num_found:]) + self.numC = int(match.group(1)) + self.extraD = match.group(2) or None + self.strB = self.strB[:num_found] + + def _strcmp(self, str1, str2): + # Any string is *before* no string. + if str1 is None: + if str2 is None: + return 0 + else: + return 1 + + if str2 is None: + return -1 + + return cmp(str1, str2) + + def __cmp__(self, other): + r = cmp(self.numA, other.numA) + if r: + return r + + r = self._strcmp(self.strB, other.strB) + if r: + return r + + r = cmp(self.numC, other.numC) + if r: + return r + + return self._strcmp(self.extraD, other.extraD) + + def __repr__(self): + return repr((self.numA, self.strB, self.numC, self.extraD)) + +def compare(a, b): + ''' + Examples: + + >>> compare('1', '2') + -1 + + >>> compare('1', '1') + 0 + + >>> compare('2', '1') + 1 + + >>> compare('1.0pre1', '1.0pre2') + -1 + + >>> compare('1.0pre2', '1.0') + -1 + + >>> compare('1.0', '1.0.0') + 0 + + >>> compare('1.0.0', '1.0.0.0') + 0 + + >>> compare('1.0.0.0', '1.1pre') + -1 + + >>> compare('1.1pre', '1.1pre0') + 0 + + >>> compare('1.1pre0', '1.0+') + 0 + + >>> compare('1.0+', '1.1pre1a') + -1 + + >>> compare('1.1pre1a', '1.1pre1') + -1 + + >>> compare('1.1pre1', '1.1pre10a') + -1 + + >>> compare('1.1pre10a', '1.1pre10') + -1 + + >>> compare('1.1pre10a', '1.*') + -1 + ''' + + a_parts = a.split('.') + b_parts = b.split('.') + + if len(a_parts) < len(b_parts): + a_parts.extend([''] * (len(b_parts) - len(a_parts))) + else: + b_parts.extend([''] * (len(a_parts) - len(b_parts))) + + for a_part, b_part in zip(a_parts, b_parts): + r = cmp(VersionPart(a_part), VersionPart(b_part)) + if r: + return r + + return 0 + +if __name__ == '__main__': + import doctest + + doctest.testmod(verbose=True) diff --git a/addon-sdk/source/python-lib/cuddlefish/xpi.py b/addon-sdk/source/python-lib/cuddlefish/xpi.py new file mode 100644 index 000000000..4ac497e89 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/xpi.py @@ -0,0 +1,169 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import zipfile +import simplejson as json +from cuddlefish.util import filter_filenames, filter_dirnames + +class HarnessOptionAlreadyDefinedError(Exception): + """You cannot use --harness-option on keys that already exist in + harness-options.json""" + +ZIPSEP = "/" # always use "/" in zipfiles + +def make_zipfile_path(localroot, localpath): + return ZIPSEP.join(localpath[len(localroot)+1:].split(os.sep)) + +def mkzipdir(zf, path): + dirinfo = zipfile.ZipInfo(path) + dirinfo.external_attr = int("040755", 8) << 16L + zf.writestr(dirinfo, "") + +def build_xpi(template_root_dir, manifest, xpi_path, + harness_options, limit_to=None, extra_harness_options={}, + bundle_sdk=True, pkgdir=""): + IGNORED_FILES = [".hgignore", ".DS_Store", + "application.ini", xpi_path] + IGNORED_TOP_LVL_FILES = ["install.rdf"] + + files_to_copy = {} # maps zipfile path to local-disk abspath + dirs_to_create = set() # zipfile paths, no trailing slash + + zf = zipfile.ZipFile(xpi_path, "w", zipfile.ZIP_DEFLATED) + + zf.writestr('install.rdf', str(manifest)) + + # Handle add-on icon + if 'icon' in harness_options: + zf.write(os.path.join(str(harness_options['icon'])), 'icon.png') + del harness_options['icon'] + + if 'icon64' in harness_options: + zf.write(os.path.join(str(harness_options['icon64'])), 'icon64.png') + del harness_options['icon64'] + + # chrome.manifest + if os.path.isfile(os.path.join(pkgdir, 'chrome.manifest')): + files_to_copy['chrome.manifest'] = os.path.join(pkgdir, 'chrome.manifest') + + def add_special_dir(folder): + if os.path.exists(os.path.join(pkgdir, folder)): + dirs_to_create.add(folder) + # cp -r folder + abs_dirname = os.path.join(pkgdir, folder) + for dirpath, dirnames, filenames in os.walk(abs_dirname): + goodfiles = list(filter_filenames(filenames, IGNORED_FILES)) + dirnames[:] = filter_dirnames(dirnames) + for dirname in dirnames: + arcpath = make_zipfile_path(template_root_dir, + os.path.join(dirpath, dirname)) + dirs_to_create.add(arcpath) + for filename in goodfiles: + abspath = os.path.join(dirpath, filename) + arcpath = ZIPSEP.join( + [folder, + make_zipfile_path(abs_dirname, os.path.join(dirpath, filename)), + ]) + files_to_copy[str(arcpath)] = str(abspath) + + + # chrome folder (would contain content, skin, and locale folders typically) + add_special_dir('chrome') + # optionally include a `webextension/` dir from the add-on dir. + add_special_dir('webextension') + + for dirpath, dirnames, filenames in os.walk(template_root_dir): + if template_root_dir == dirpath: + filenames = list(filter_filenames(filenames, IGNORED_TOP_LVL_FILES)) + filenames = list(filter_filenames(filenames, IGNORED_FILES)) + dirnames[:] = filter_dirnames(dirnames) + for dirname in dirnames: + arcpath = make_zipfile_path(template_root_dir, + os.path.join(dirpath, dirname)) + dirs_to_create.add(arcpath) + for filename in filenames: + abspath = os.path.join(dirpath, filename) + arcpath = make_zipfile_path(template_root_dir, abspath) + files_to_copy[arcpath] = abspath + + # `packages` attribute contains a dictionnary of dictionnary + # of all packages sections directories + for packageName in harness_options['packages']: + base_arcpath = ZIPSEP.join(['resources', packageName]) + # Eventually strip sdk files. + if not bundle_sdk and packageName == 'addon-sdk': + continue + # Always write the top directory, even if it contains no files, since + # the harness will try to access it. + dirs_to_create.add(base_arcpath) + for sectionName in harness_options['packages'][packageName]: + abs_dirname = harness_options['packages'][packageName][sectionName] + base_arcpath = ZIPSEP.join(['resources', packageName, sectionName]) + # Always write the top directory, even if it contains no files, since + # the harness will try to access it. + dirs_to_create.add(base_arcpath) + # cp -r stuff from abs_dirname/ into ZIP/resources/RESOURCEBASE/ + for dirpath, dirnames, filenames in os.walk(abs_dirname): + goodfiles = list(filter_filenames(filenames, IGNORED_FILES)) + dirnames[:] = filter_dirnames(dirnames) + for filename in goodfiles: + abspath = os.path.join(dirpath, filename) + if limit_to is not None and abspath not in limit_to: + continue # strip unused files + arcpath = ZIPSEP.join( + ['resources', + packageName, + sectionName, + make_zipfile_path(abs_dirname, + os.path.join(dirpath, filename)), + ]) + files_to_copy[str(arcpath)] = str(abspath) + del harness_options['packages'] + + locales_json_data = {"locales": []} + mkzipdir(zf, "locale/") + for language in sorted(harness_options['locale']): + locales_json_data["locales"].append(language) + locale = harness_options['locale'][language] + # Be carefull about strings, we need to always ensure working with UTF-8 + jsonStr = json.dumps(locale, indent=1, sort_keys=True, ensure_ascii=False) + info = zipfile.ZipInfo('locale/' + language + '.json') + info.external_attr = 0644 << 16L + zf.writestr(info, jsonStr.encode( "utf-8" )) + del harness_options['locale'] + + jsonStr = json.dumps(locales_json_data, ensure_ascii=True) +"\n" + info = zipfile.ZipInfo('locales.json') + info.external_attr = 0644 << 16L + zf.writestr(info, jsonStr.encode("utf-8")) + + # now figure out which directories we need: all retained files parents + for arcpath in files_to_copy: + bits = arcpath.split("/") + for i in range(1,len(bits)): + parentpath = ZIPSEP.join(bits[0:i]) + dirs_to_create.add(parentpath) + + # Create zipfile in alphabetical order, with each directory before its + # files + for name in sorted(dirs_to_create.union(set(files_to_copy))): + if name in dirs_to_create: + mkzipdir(zf, name+"/") + if name in files_to_copy: + zf.write(files_to_copy[name], name) + + # Add extra harness options + harness_options = harness_options.copy() + for key,value in extra_harness_options.items(): + if key in harness_options: + msg = "Can't use --harness-option for existing key '%s'" % key + raise HarnessOptionAlreadyDefinedError(msg) + harness_options[key] = value + + # Write harness-options.json + zf.writestr('harness-options.json', json.dumps(harness_options, indent=1, + sort_keys=True)) + + zf.close() diff --git a/addon-sdk/source/python-lib/jetpack_sdk_env.py b/addon-sdk/source/python-lib/jetpack_sdk_env.py new file mode 100644 index 000000000..fb8238bde --- /dev/null +++ b/addon-sdk/source/python-lib/jetpack_sdk_env.py @@ -0,0 +1,66 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import sys +import os + +def welcome(): + """ + Perform a bunch of sanity tests to make sure the Add-on SDK + environment is sane, and then display a welcome message. + """ + + try: + if sys.version_info[0] > 2: + print ("Error: You appear to be using Python %d, but " + "the Add-on SDK only supports the Python 2.x line." % + (sys.version_info[0])) + return + + import mozrunner + + if 'CUDDLEFISH_ROOT' not in os.environ: + print ("Error: CUDDLEFISH_ROOT environment variable does " + "not exist! It should point to the root of the " + "Add-on SDK repository.") + return + + env_root = os.environ['CUDDLEFISH_ROOT'] + + bin_dir = os.path.join(env_root, 'bin') + python_lib_dir = os.path.join(env_root, 'python-lib') + path = os.environ['PATH'].split(os.path.pathsep) + + if bin_dir not in path: + print ("Warning: the Add-on SDK binary directory %s " + "does not appear to be in your PATH. You may " + "not be able to run 'cfx' or other SDK tools." % + bin_dir) + + if python_lib_dir not in sys.path: + print ("Warning: the Add-on SDK python-lib directory %s " + "does not appear to be in your sys.path, which " + "is odd because I'm running from it." % python_lib_dir) + + if not mozrunner.__path__[0].startswith(env_root): + print ("Warning: your mozrunner package is installed at %s, " + "which does not seem to be located inside the Jetpack " + "SDK. This may cause problems, and you may want to " + "uninstall the other version. See bug 556562 for " + "more information." % mozrunner.__path__[0]) + except Exception: + # Apparently we can't get the actual exception object in the + # 'except' clause in a way that's syntax-compatible for both + # Python 2.x and 3.x, so we'll have to use the traceback module. + + import traceback + _, e, _ = sys.exc_info() + print ("Verification of Add-on SDK environment failed (%s)." % e) + print ("Your SDK may not work properly.") + return + + print ("Welcome to the Add-on SDK. For the docs, visit https://developer.mozilla.org/en-US/Add-ons/SDK") + +if __name__ == '__main__': + welcome() diff --git a/addon-sdk/source/python-lib/mozrunner/__init__.py b/addon-sdk/source/python-lib/mozrunner/__init__.py new file mode 100644 index 000000000..87c2c320f --- /dev/null +++ b/addon-sdk/source/python-lib/mozrunner/__init__.py @@ -0,0 +1,694 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import sys +import copy +import tempfile +import signal +import commands +import zipfile +import optparse +import killableprocess +import subprocess +import platform +import shutil +from StringIO import StringIO +from xml.dom import minidom + +from distutils import dir_util +from time import sleep + +# conditional (version-dependent) imports +try: + import simplejson +except ImportError: + import json as simplejson + +import logging +logger = logging.getLogger(__name__) + +# Use dir_util for copy/rm operations because shutil is all kinds of broken +copytree = dir_util.copy_tree +rmtree = dir_util.remove_tree + +def findInPath(fileName, path=os.environ['PATH']): + dirs = path.split(os.pathsep) + for dir in dirs: + if os.path.isfile(os.path.join(dir, fileName)): + return os.path.join(dir, fileName) + if os.name == 'nt' or sys.platform == 'cygwin': + if os.path.isfile(os.path.join(dir, fileName + ".exe")): + return os.path.join(dir, fileName + ".exe") + return None + +stdout = sys.stdout +stderr = sys.stderr +stdin = sys.stdin + +def run_command(cmd, env=None, **kwargs): + """Run the given command in killable process.""" + killable_kwargs = {'stdout':stdout ,'stderr':stderr, 'stdin':stdin} + killable_kwargs.update(kwargs) + + if sys.platform != "win32": + return killableprocess.Popen(cmd, preexec_fn=lambda : os.setpgid(0, 0), + env=env, **killable_kwargs) + else: + return killableprocess.Popen(cmd, env=env, **killable_kwargs) + +def getoutput(l): + tmp = tempfile.mktemp() + x = open(tmp, 'w') + subprocess.call(l, stdout=x, stderr=x) + x.close(); x = open(tmp, 'r') + r = x.read() ; x.close() + os.remove(tmp) + return r + +def get_pids(name, minimun_pid=0): + """Get all the pids matching name, exclude any pids below minimum_pid.""" + if os.name == 'nt' or sys.platform == 'cygwin': + import wpk + + pids = wpk.get_pids(name) + + else: + data = getoutput(['ps', 'ax']).splitlines() + pids = [int(line.split()[0]) for line in data if line.find(name) is not -1] + + matching_pids = [m for m in pids if m > minimun_pid] + return matching_pids + +def makedirs(name): + + head, tail = os.path.split(name) + if not tail: + head, tail = os.path.split(head) + if head and tail and not os.path.exists(head): + try: + makedirs(head) + except OSError, e: + pass + if tail == os.curdir: # xxx/newdir/. exists if xxx/newdir exists + return + try: + os.mkdir(name) + except: + pass + +# addon_details() copied from mozprofile +def addon_details(install_rdf_fh): + """ + returns a dictionary of details about the addon + - addon_path : path to the addon directory + Returns: + {'id': u'rainbow@colors.org', # id of the addon + 'version': u'1.4', # version of the addon + 'name': u'Rainbow', # name of the addon + 'unpack': # whether to unpack the addon + """ + + details = { + 'id': None, + 'unpack': False, + 'name': None, + 'version': None + } + + def get_namespace_id(doc, url): + attributes = doc.documentElement.attributes + namespace = "" + for i in range(attributes.length): + if attributes.item(i).value == url: + if ":" in attributes.item(i).name: + # If the namespace is not the default one remove 'xlmns:' + namespace = attributes.item(i).name.split(':')[1] + ":" + break + return namespace + + def get_text(element): + """Retrieve the text value of a given node""" + rc = [] + for node in element.childNodes: + if node.nodeType == node.TEXT_NODE: + rc.append(node.data) + return ''.join(rc).strip() + + doc = minidom.parse(install_rdf_fh) + + # Get the namespaces abbreviations + em = get_namespace_id(doc, "http://www.mozilla.org/2004/em-rdf#") + rdf = get_namespace_id(doc, "http://www.w3.org/1999/02/22-rdf-syntax-ns#") + + description = doc.getElementsByTagName(rdf + "Description").item(0) + for node in description.childNodes: + # Remove the namespace prefix from the tag for comparison + entry = node.nodeName.replace(em, "") + if entry in details.keys(): + details.update({ entry: get_text(node) }) + + # turn unpack into a true/false value + if isinstance(details['unpack'], basestring): + details['unpack'] = details['unpack'].lower() == 'true' + + return details + +class Profile(object): + """Handles all operations regarding profile. Created new profiles, installs extensions, + sets preferences and handles cleanup.""" + + def __init__(self, binary=None, profile=None, addons=None, + preferences=None): + + self.binary = binary + + self.create_new = not(bool(profile)) + if profile: + self.profile = profile + else: + self.profile = self.create_new_profile(self.binary) + + self.addons_installed = [] + self.addons = addons or [] + + ### set preferences from class preferences + preferences = preferences or {} + if hasattr(self.__class__, 'preferences'): + self.preferences = self.__class__.preferences.copy() + else: + self.preferences = {} + self.preferences.update(preferences) + + for addon in self.addons: + self.install_addon(addon) + + self.set_preferences(self.preferences) + + def create_new_profile(self, binary): + """Create a new clean profile in tmp which is a simple empty folder""" + profile = tempfile.mkdtemp(suffix='.mozrunner') + return profile + + def unpack_addon(self, xpi_zipfile, addon_path): + for name in xpi_zipfile.namelist(): + if name.endswith('/'): + makedirs(os.path.join(addon_path, name)) + else: + if not os.path.isdir(os.path.dirname(os.path.join(addon_path, name))): + makedirs(os.path.dirname(os.path.join(addon_path, name))) + data = xpi_zipfile.read(name) + f = open(os.path.join(addon_path, name), 'wb') + f.write(data) ; f.close() + zi = xpi_zipfile.getinfo(name) + os.chmod(os.path.join(addon_path,name), (zi.external_attr>>16)) + + def install_addon(self, path): + """Installs the given addon or directory of addons in the profile.""" + + extensions_path = os.path.join(self.profile, 'extensions') + if not os.path.exists(extensions_path): + os.makedirs(extensions_path) + + addons = [path] + if not path.endswith('.xpi') and not os.path.exists(os.path.join(path, 'install.rdf')): + addons = [os.path.join(path, x) for x in os.listdir(path)] + + for addon in addons: + if addon.endswith('.xpi'): + xpi_zipfile = zipfile.ZipFile(addon, "r") + details = addon_details(StringIO(xpi_zipfile.read('install.rdf'))) + addon_path = os.path.join(extensions_path, details["id"]) + if details.get("unpack", True): + self.unpack_addon(xpi_zipfile, addon_path) + self.addons_installed.append(addon_path) + else: + shutil.copy(addon, addon_path + '.xpi') + else: + # it's already unpacked, but we need to extract the id so we + # can copy it + details = addon_details(open(os.path.join(addon, "install.rdf"), "rb")) + addon_path = os.path.join(extensions_path, details["id"]) + shutil.copytree(addon, addon_path, symlinks=True) + + def set_preferences(self, preferences): + """Adds preferences dict to profile preferences""" + prefs_file = os.path.join(self.profile, 'user.js') + # Ensure that the file exists first otherwise create an empty file + if os.path.isfile(prefs_file): + f = open(prefs_file, 'a+') + else: + f = open(prefs_file, 'w') + + f.write('\n#MozRunner Prefs Start\n') + + pref_lines = ['user_pref(%s, %s);' % + (simplejson.dumps(k), simplejson.dumps(v) ) for k, v in + preferences.items()] + for line in pref_lines: + f.write(line+'\n') + f.write('#MozRunner Prefs End\n') + f.flush() ; f.close() + + def pop_preferences(self): + """ + pop the last set of preferences added + returns True if popped + """ + + # our magic markers + delimeters = ('#MozRunner Prefs Start', '#MozRunner Prefs End') + + lines = file(os.path.join(self.profile, 'user.js')).read().splitlines() + def last_index(_list, value): + """ + returns the last index of an item; + this should actually be part of python code but it isn't + """ + for index in reversed(range(len(_list))): + if _list[index] == value: + return index + s = last_index(lines, delimeters[0]) + e = last_index(lines, delimeters[1]) + + # ensure both markers are found + if s is None: + assert e is None, '%s found without %s' % (delimeters[1], delimeters[0]) + return False # no preferences found + elif e is None: + assert e is None, '%s found without %s' % (delimeters[0], delimeters[1]) + + # ensure the markers are in the proper order + assert e > s, '%s found at %s, while %s found at %s' (delimeter[1], e, delimeter[0], s) + + # write the prefs + cleaned_prefs = '\n'.join(lines[:s] + lines[e+1:]) + f = file(os.path.join(self.profile, 'user.js'), 'w') + f.write(cleaned_prefs) + f.close() + return True + + def clean_preferences(self): + """Removed preferences added by mozrunner.""" + while True: + if not self.pop_preferences(): + break + + def clean_addons(self): + """Cleans up addons in the profile.""" + for addon in self.addons_installed: + if os.path.isdir(addon): + rmtree(addon) + + def cleanup(self): + """Cleanup operations on the profile.""" + def oncleanup_error(function, path, excinfo): + #TODO: How should we handle this? + print "Error Cleaning up: " + str(excinfo[1]) + if self.create_new: + shutil.rmtree(self.profile, False, oncleanup_error) + else: + self.clean_preferences() + self.clean_addons() + +class FirefoxProfile(Profile): + """Specialized Profile subclass for Firefox""" + preferences = {# Don't automatically update the application + 'app.update.enabled' : False, + # Don't restore the last open set of tabs if the browser has crashed + 'browser.sessionstore.resume_from_crash': False, + # Don't check for the default web browser + 'browser.shell.checkDefaultBrowser' : False, + # Don't warn on exit when multiple tabs are open + 'browser.tabs.warnOnClose' : False, + # Don't warn when exiting the browser + 'browser.warnOnQuit': False, + # Only install add-ons from the profile and the app folder + 'extensions.enabledScopes' : 5, + # Don't automatically update add-ons + 'extensions.update.enabled' : False, + # Don't open a dialog to show available add-on updates + 'extensions.update.notifyUser' : False, + } + + # The possible names of application bundles on Mac OS X, in order of + # preference from most to least preferred. + # Note: Nightly is obsolete, as it has been renamed to FirefoxNightly, + # but it will still be present if users update an older nightly build + # via the app update service. + bundle_names = ['Firefox', 'FirefoxNightly', 'Nightly'] + + # The possible names of binaries, in order of preference from most to least + # preferred. + @property + def names(self): + if sys.platform == 'darwin': + return ['firefox', 'nightly', 'shiretoko'] + if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')): + return ['firefox', 'mozilla-firefox', 'iceweasel'] + if os.name == 'nt' or sys.platform == 'cygwin': + return ['firefox'] + +class ThunderbirdProfile(Profile): + preferences = {'extensions.update.enabled' : False, + 'extensions.update.notifyUser' : False, + 'browser.shell.checkDefaultBrowser' : False, + 'browser.tabs.warnOnClose' : False, + 'browser.warnOnQuit': False, + 'browser.sessionstore.resume_from_crash': False, + } + + # The possible names of application bundles on Mac OS X, in order of + # preference from most to least preferred. + bundle_names = ["Thunderbird", "Shredder"] + + # The possible names of binaries, in order of preference from most to least + # preferred. + names = ["thunderbird", "shredder"] + + +class Runner(object): + """Handles all running operations. Finds bins, runs and kills the process.""" + + def __init__(self, binary=None, profile=None, cmdargs=[], env=None, + kp_kwargs={}): + if binary is None: + self.binary = self.find_binary() + elif sys.platform == 'darwin' and binary.find('Contents/MacOS/') == -1: + self.binary = os.path.join(binary, 'Contents/MacOS/%s-bin' % self.names[0]) + else: + self.binary = binary + + if not os.path.exists(self.binary): + raise Exception("Binary path does not exist "+self.binary) + + if sys.platform == 'linux2' and self.binary.endswith('-bin'): + dirname = os.path.dirname(self.binary) + if os.environ.get('LD_LIBRARY_PATH', None): + os.environ['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRARY_PATH'], dirname) + else: + os.environ['LD_LIBRARY_PATH'] = dirname + + # Disable the crash reporter by default + os.environ['MOZ_CRASHREPORTER_NO_REPORT'] = '1' + + self.profile = profile + + self.cmdargs = cmdargs + if env is None: + self.env = copy.copy(os.environ) + self.env.update({'MOZ_NO_REMOTE':"1",}) + else: + self.env = env + self.kp_kwargs = kp_kwargs or {} + + def find_binary(self): + """Finds the binary for self.names if one was not provided.""" + binary = None + if sys.platform in ('linux2', 'sunos5', 'solaris') \ + or sys.platform.startswith('freebsd'): + for name in reversed(self.names): + binary = findInPath(name) + elif os.name == 'nt' or sys.platform == 'cygwin': + + # find the default executable from the windows registry + try: + import _winreg + except ImportError: + pass + else: + sam_flags = [0] + # KEY_WOW64_32KEY etc only appeared in 2.6+, but that's OK as + # only 2.6+ has functioning 64bit builds. + if hasattr(_winreg, "KEY_WOW64_32KEY"): + if "64 bit" in sys.version: + # a 64bit Python should also look in the 32bit registry + sam_flags.append(_winreg.KEY_WOW64_32KEY) + else: + # possibly a 32bit Python on 64bit Windows, so look in + # the 64bit registry incase there is a 64bit app. + sam_flags.append(_winreg.KEY_WOW64_64KEY) + for sam_flag in sam_flags: + try: + # assumes self.app_name is defined, as it should be for + # implementors + keyname = r"Software\Mozilla\Mozilla %s" % self.app_name + sam = _winreg.KEY_READ | sam_flag + app_key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, keyname, 0, sam) + version, _type = _winreg.QueryValueEx(app_key, "CurrentVersion") + version_key = _winreg.OpenKey(app_key, version + r"\Main") + path, _ = _winreg.QueryValueEx(version_key, "PathToExe") + return path + except _winreg.error: + pass + + # search for the binary in the path + for name in reversed(self.names): + binary = findInPath(name) + if sys.platform == 'cygwin': + program_files = os.environ['PROGRAMFILES'] + else: + program_files = os.environ['ProgramFiles'] + + if binary is None: + for bin in [(program_files, 'Mozilla Firefox', 'firefox.exe'), + (os.environ.get("ProgramFiles(x86)"),'Mozilla Firefox', 'firefox.exe'), + (program_files, 'Nightly', 'firefox.exe'), + (os.environ.get("ProgramFiles(x86)"),'Nightly', 'firefox.exe'), + (program_files, 'Aurora', 'firefox.exe'), + (os.environ.get("ProgramFiles(x86)"),'Aurora', 'firefox.exe') + ]: + path = os.path.join(*bin) + if os.path.isfile(path): + binary = path + break + elif sys.platform == 'darwin': + for bundle_name in self.bundle_names: + # Look for the application bundle in the user's home directory + # or the system-wide /Applications directory. If we don't find + # it in one of those locations, we move on to the next possible + # bundle name. + appdir = os.path.join("~/Applications/%s.app" % bundle_name) + if not os.path.isdir(appdir): + appdir = "/Applications/%s.app" % bundle_name + if not os.path.isdir(appdir): + continue + + # Look for a binary with any of the possible binary names + # inside the application bundle. + for binname in self.names: + binpath = os.path.join(appdir, + "Contents/MacOS/%s-bin" % binname) + if (os.path.isfile(binpath)): + binary = binpath + break + + if binary: + break + + if binary is None: + raise Exception('Mozrunner could not locate your binary, you will need to set it.') + return binary + + @property + def command(self): + """Returns the command list to run.""" + cmd = [self.binary, '-profile', self.profile.profile] + # On i386 OS X machines, i386+x86_64 universal binaries need to be told + # to run as i386 binaries. If we're not running a i386+x86_64 universal + # binary, then this command modification is harmless. + if sys.platform == 'darwin': + if hasattr(platform, 'architecture') and platform.architecture()[0] == '32bit': + cmd = ['arch', '-i386'] + cmd + return cmd + + def get_repositoryInfo(self): + """Read repository information from application.ini and platform.ini.""" + import ConfigParser + + config = ConfigParser.RawConfigParser() + dirname = os.path.dirname(self.binary) + repository = { } + + for entry in [['application', 'App'], ['platform', 'Build']]: + (file, section) = entry + config.read(os.path.join(dirname, '%s.ini' % file)) + + for entry in [['SourceRepository', 'repository'], ['SourceStamp', 'changeset']]: + (key, id) = entry + + try: + repository['%s_%s' % (file, id)] = config.get(section, key); + except: + repository['%s_%s' % (file, id)] = None + + return repository + + def start(self): + """Run self.command in the proper environment.""" + if self.profile is None: + self.profile = self.profile_class() + self.process_handler = run_command(self.command+self.cmdargs, self.env, **self.kp_kwargs) + + def wait(self, timeout=None): + """Wait for the browser to exit.""" + self.process_handler.wait(timeout=timeout) + + if sys.platform != 'win32': + for name in self.names: + for pid in get_pids(name, self.process_handler.pid): + self.process_handler.pid = pid + self.process_handler.wait(timeout=timeout) + + def kill(self, kill_signal=signal.SIGTERM): + """Kill the browser""" + if sys.platform != 'win32': + self.process_handler.kill() + for name in self.names: + for pid in get_pids(name, self.process_handler.pid): + self.process_handler.pid = pid + self.process_handler.kill() + else: + try: + self.process_handler.kill(group=True) + # On windows, it sometimes behooves one to wait for dust to settle + # after killing processes. Let's try that. + # TODO: Bug 640047 is invesitgating the correct way to handle this case + self.process_handler.wait(timeout=10) + except Exception, e: + logger.error('Cannot kill process, '+type(e).__name__+' '+e.message) + + def stop(self): + self.kill() + +class FirefoxRunner(Runner): + """Specialized Runner subclass for running Firefox.""" + + app_name = 'Firefox' + profile_class = FirefoxProfile + + # The possible names of application bundles on Mac OS X, in order of + # preference from most to least preferred. + # Note: Nightly is obsolete, as it has been renamed to FirefoxNightly, + # but it will still be present if users update an older nightly build + # only via the app update service. + bundle_names = ['Firefox', 'FirefoxNightly', 'Nightly'] + + @property + def names(self): + if sys.platform == 'darwin': + return ['firefox', 'nightly', 'shiretoko'] + if sys.platform in ('linux2', 'sunos5', 'solaris') \ + or sys.platform.startswith('freebsd'): + return ['firefox', 'mozilla-firefox', 'iceweasel'] + if os.name == 'nt' or sys.platform == 'cygwin': + return ['firefox'] + +class ThunderbirdRunner(Runner): + """Specialized Runner subclass for running Thunderbird""" + + app_name = 'Thunderbird' + profile_class = ThunderbirdProfile + + # The possible names of application bundles on Mac OS X, in order of + # preference from most to least preferred. + bundle_names = ["Thunderbird", "Shredder"] + + # The possible names of binaries, in order of preference from most to least + # preferred. + names = ["thunderbird", "shredder"] + +class CLI(object): + """Command line interface.""" + + runner_class = FirefoxRunner + profile_class = FirefoxProfile + module = "mozrunner" + + parser_options = {("-b", "--binary",): dict(dest="binary", help="Binary path.", + metavar=None, default=None), + ('-p', "--profile",): dict(dest="profile", help="Profile path.", + metavar=None, default=None), + ('-a', "--addons",): dict(dest="addons", + help="Addons paths to install.", + metavar=None, default=None), + ("--info",): dict(dest="info", default=False, + action="store_true", + help="Print module information") + } + + def __init__(self): + """ Setup command line parser and parse arguments """ + self.metadata = self.get_metadata_from_egg() + self.parser = optparse.OptionParser(version="%prog " + self.metadata["Version"]) + for names, opts in self.parser_options.items(): + self.parser.add_option(*names, **opts) + (self.options, self.args) = self.parser.parse_args() + + if self.options.info: + self.print_metadata() + sys.exit(0) + + # XXX should use action='append' instead of rolling our own + try: + self.addons = self.options.addons.split(',') + except: + self.addons = [] + + def get_metadata_from_egg(self): + import pkg_resources + ret = {} + dist = pkg_resources.get_distribution(self.module) + if dist.has_metadata("PKG-INFO"): + for line in dist.get_metadata_lines("PKG-INFO"): + key, value = line.split(':', 1) + ret[key] = value + if dist.has_metadata("requires.txt"): + ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt") + return ret + + def print_metadata(self, data=("Name", "Version", "Summary", "Home-page", + "Author", "Author-email", "License", "Platform", "Dependencies")): + for key in data: + if key in self.metadata: + print key + ": " + self.metadata[key] + + def create_runner(self): + """ Get the runner object """ + runner = self.get_runner(binary=self.options.binary) + profile = self.get_profile(binary=runner.binary, + profile=self.options.profile, + addons=self.addons) + runner.profile = profile + return runner + + def get_runner(self, binary=None, profile=None): + """Returns the runner instance for the given command line binary argument + the profile instance returned from self.get_profile().""" + return self.runner_class(binary, profile) + + def get_profile(self, binary=None, profile=None, addons=None, preferences=None): + """Returns the profile instance for the given command line arguments.""" + addons = addons or [] + preferences = preferences or {} + return self.profile_class(binary, profile, addons, preferences) + + def run(self): + runner = self.create_runner() + self.start(runner) + runner.profile.cleanup() + + def start(self, runner): + """Starts the runner and waits for Firefox to exitor Keyboard Interrupt. + Shoule be overwritten to provide custom running of the runner instance.""" + runner.start() + print 'Started:', ' '.join(runner.command) + try: + runner.wait() + except KeyboardInterrupt: + runner.stop() + + +def cli(): + CLI().run() diff --git a/addon-sdk/source/python-lib/mozrunner/killableprocess.py b/addon-sdk/source/python-lib/mozrunner/killableprocess.py new file mode 100644 index 000000000..daf52f0c9 --- /dev/null +++ b/addon-sdk/source/python-lib/mozrunner/killableprocess.py @@ -0,0 +1,329 @@ +# killableprocess - subprocesses which can be reliably killed +# +# Parts of this module are copied from the subprocess.py file contained +# in the Python distribution. +# +# Copyright (c) 2003-2004 by Peter Astrand <astrand@lysator.liu.se> +# +# Additions and modifications written by Benjamin Smedberg +# <benjamin@smedbergs.us> are Copyright (c) 2006 by the Mozilla Foundation +# <http://www.mozilla.org/> +# +# More Modifications +# Copyright (c) 2006-2007 by Mike Taylor <bear@code-bear.com> +# Copyright (c) 2007-2008 by Mikeal Rogers <mikeal@mozilla.com> +# +# By obtaining, using, and/or copying this software and/or its +# associated documentation, you agree that you have read, understood, +# and will comply with the following terms and conditions: +# +# Permission to use, copy, modify, and distribute this software and +# its associated documentation for any purpose and without fee is +# hereby granted, provided that the above copyright notice appears in +# all copies, and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of the +# author not be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior +# permission. +# +# THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""killableprocess - Subprocesses which can be reliably killed + +This module is a subclass of the builtin "subprocess" module. It allows +processes that launch subprocesses to be reliably killed on Windows (via the Popen.kill() method. + +It also adds a timeout argument to Wait() for a limited period of time before +forcefully killing the process. + +Note: On Windows, this module requires Windows 2000 or higher (no support for +Windows 95, 98, or NT 4.0). It also requires ctypes, which is bundled with +Python 2.5+ or available from http://python.net/crew/theller/ctypes/ +""" + +import subprocess +import sys +import os +import time +import datetime +import types +import exceptions + +try: + from subprocess import CalledProcessError +except ImportError: + # Python 2.4 doesn't implement CalledProcessError + class CalledProcessError(Exception): + """This exception is raised when a process run by check_call() returns + a non-zero exit status. The exit status will be stored in the + returncode attribute.""" + def __init__(self, returncode, cmd): + self.returncode = returncode + self.cmd = cmd + def __str__(self): + return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode) + +mswindows = (sys.platform == "win32") + +if mswindows: + import winprocess +else: + import signal + +# This is normally defined in win32con, but we don't want +# to incur the huge tree of dependencies (pywin32 and friends) +# just to get one constant. So here's our hack +STILL_ACTIVE = 259 + +def call(*args, **kwargs): + waitargs = {} + if "timeout" in kwargs: + waitargs["timeout"] = kwargs.pop("timeout") + + return Popen(*args, **kwargs).wait(**waitargs) + +def check_call(*args, **kwargs): + """Call a program with an optional timeout. If the program has a non-zero + exit status, raises a CalledProcessError.""" + + retcode = call(*args, **kwargs) + if retcode: + cmd = kwargs.get("args") + if cmd is None: + cmd = args[0] + raise CalledProcessError(retcode, cmd) + +if not mswindows: + def DoNothing(*args): + pass + +class Popen(subprocess.Popen): + kill_called = False + if mswindows: + def _execute_child(self, *args_tuple): + # workaround for bug 958609 + if sys.hexversion < 0x02070600: # prior to 2.7.6 + (args, executable, preexec_fn, close_fds, + cwd, env, universal_newlines, startupinfo, + creationflags, shell, + p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite) = args_tuple + to_close = set() + else: # 2.7.6 and later + (args, executable, preexec_fn, close_fds, + cwd, env, universal_newlines, startupinfo, + creationflags, shell, to_close, + p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite) = args_tuple + + if not isinstance(args, types.StringTypes): + args = subprocess.list2cmdline(args) + + # Always or in the create new process group + creationflags |= winprocess.CREATE_NEW_PROCESS_GROUP + + if startupinfo is None: + startupinfo = winprocess.STARTUPINFO() + + if None not in (p2cread, c2pwrite, errwrite): + startupinfo.dwFlags |= winprocess.STARTF_USESTDHANDLES + + startupinfo.hStdInput = int(p2cread) + startupinfo.hStdOutput = int(c2pwrite) + startupinfo.hStdError = int(errwrite) + if shell: + startupinfo.dwFlags |= winprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = winprocess.SW_HIDE + comspec = os.environ.get("COMSPEC", "cmd.exe") + args = comspec + " /c " + args + + # determine if we can create create a job + canCreateJob = winprocess.CanCreateJobObject() + + # set process creation flags + creationflags |= winprocess.CREATE_SUSPENDED + creationflags |= winprocess.CREATE_UNICODE_ENVIRONMENT + if canCreateJob: + # Uncomment this line below to discover very useful things about your environment + #print "++++ killableprocess: releng twistd patch not applied, we can create job objects" + creationflags |= winprocess.CREATE_BREAKAWAY_FROM_JOB + + # create the process + hp, ht, pid, tid = winprocess.CreateProcess( + executable, args, + None, None, # No special security + 1, # Must inherit handles! + creationflags, + winprocess.EnvironmentBlock(env), + cwd, startupinfo) + self._child_created = True + self._handle = hp + self._thread = ht + self.pid = pid + self.tid = tid + + if canCreateJob: + # We create a new job for this process, so that we can kill + # the process and any sub-processes + self._job = winprocess.CreateJobObject() + winprocess.AssignProcessToJobObject(self._job, int(hp)) + else: + self._job = None + + winprocess.ResumeThread(int(ht)) + ht.Close() + + if p2cread is not None: + p2cread.Close() + if c2pwrite is not None: + c2pwrite.Close() + if errwrite is not None: + errwrite.Close() + time.sleep(.1) + + def kill(self, group=True): + """Kill the process. If group=True, all sub-processes will also be killed.""" + self.kill_called = True + + if mswindows: + if group and self._job: + winprocess.TerminateJobObject(self._job, 127) + else: + winprocess.TerminateProcess(self._handle, 127) + self.returncode = 127 + else: + if group: + try: + os.killpg(self.pid, signal.SIGKILL) + except: pass + else: + os.kill(self.pid, signal.SIGKILL) + self.returncode = -9 + + def wait(self, timeout=None, group=True): + """Wait for the process to terminate. Returns returncode attribute. + If timeout seconds are reached and the process has not terminated, + it will be forcefully killed. If timeout is -1, wait will not + time out.""" + if timeout is not None: + # timeout is now in milliseconds + timeout = timeout * 1000 + + starttime = datetime.datetime.now() + + if mswindows: + if timeout is None: + timeout = -1 + rc = winprocess.WaitForSingleObject(self._handle, timeout) + + if (rc == winprocess.WAIT_OBJECT_0 or + rc == winprocess.WAIT_ABANDONED or + rc == winprocess.WAIT_FAILED): + # Object has either signaled, or the API call has failed. In + # both cases we want to give the OS the benefit of the doubt + # and supply a little time before we start shooting processes + # with an M-16. + + # Returns 1 if running, 0 if not, -1 if timed out + def check(): + now = datetime.datetime.now() + diff = now - starttime + if (diff.seconds * 1000000 + diff.microseconds) < (timeout * 1000): # (1000*1000) + if self._job: + if (winprocess.QueryInformationJobObject(self._job, 8)['BasicInfo']['ActiveProcesses'] > 0): + # Job Object is still containing active processes + return 1 + else: + # No job, we use GetExitCodeProcess, which will tell us if the process is still active + self.returncode = winprocess.GetExitCodeProcess(self._handle) + if (self.returncode == STILL_ACTIVE): + # Process still active, continue waiting + return 1 + # Process not active, return 0 + return 0 + else: + # Timed out, return -1 + return -1 + + notdone = check() + while notdone == 1: + time.sleep(.5) + notdone = check() + + if notdone == -1: + # Then check timed out, we have a hung process, attempt + # last ditch kill with explosives + self.kill(group) + + else: + # In this case waitforsingleobject timed out. We have to + # take the process behind the woodshed and shoot it. + self.kill(group) + + else: + if sys.platform in ('linux2', 'sunos5', 'solaris') \ + or sys.platform.startswith('freebsd'): + def group_wait(timeout): + try: + os.waitpid(self.pid, 0) + except OSError, e: + pass # If wait has already been called on this pid, bad things happen + return self.returncode + elif sys.platform == 'darwin': + def group_wait(timeout): + try: + count = 0 + if timeout is None and self.kill_called: + timeout = 10 # Have to set some kind of timeout or else this could go on forever + if timeout is None: + while 1: + os.killpg(self.pid, signal.SIG_DFL) + while ((count * 2) <= timeout): + os.killpg(self.pid, signal.SIG_DFL) + # count is increased by 500ms for every 0.5s of sleep + time.sleep(.5); count += 500 + except exceptions.OSError: + return self.returncode + + if timeout is None: + if group is True: + return group_wait(timeout) + else: + subprocess.Popen.wait(self) + return self.returncode + + returncode = False + + now = datetime.datetime.now() + diff = now - starttime + while (diff.seconds * 1000 * 1000 + diff.microseconds) < (timeout * 1000) and ( returncode is False ): + if group is True: + return group_wait(timeout) + else: + if subprocess.poll() is not None: + returncode = self.returncode + time.sleep(.5) + now = datetime.datetime.now() + diff = now - starttime + return self.returncode + + return self.returncode + # We get random maxint errors from subprocesses __del__ + __del__ = lambda self: None + +def setpgid_preexec_fn(): + os.setpgid(0, 0) + +def runCommand(cmd, **kwargs): + if sys.platform != "win32": + return Popen(cmd, preexec_fn=setpgid_preexec_fn, **kwargs) + else: + return Popen(cmd, **kwargs) diff --git a/addon-sdk/source/python-lib/mozrunner/qijo.py b/addon-sdk/source/python-lib/mozrunner/qijo.py new file mode 100644 index 000000000..058055731 --- /dev/null +++ b/addon-sdk/source/python-lib/mozrunner/qijo.py @@ -0,0 +1,166 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from ctypes import c_void_p, POINTER, sizeof, Structure, windll, WinError, WINFUNCTYPE, addressof, c_size_t, c_ulong +from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LARGE_INTEGER + +LPVOID = c_void_p +LPDWORD = POINTER(DWORD) +SIZE_T = c_size_t +ULONG_PTR = POINTER(c_ulong) + +# A ULONGLONG is a 64-bit unsigned integer. +# Thus there are 8 bytes in a ULONGLONG. +# XXX why not import c_ulonglong ? +ULONGLONG = BYTE * 8 + +class IO_COUNTERS(Structure): + # The IO_COUNTERS struct is 6 ULONGLONGs. + # TODO: Replace with non-dummy fields. + _fields_ = [('dummy', ULONGLONG * 6)] + +class JOBOBJECT_BASIC_ACCOUNTING_INFORMATION(Structure): + _fields_ = [('TotalUserTime', LARGE_INTEGER), + ('TotalKernelTime', LARGE_INTEGER), + ('ThisPeriodTotalUserTime', LARGE_INTEGER), + ('ThisPeriodTotalKernelTime', LARGE_INTEGER), + ('TotalPageFaultCount', DWORD), + ('TotalProcesses', DWORD), + ('ActiveProcesses', DWORD), + ('TotalTerminatedProcesses', DWORD)] + +class JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION(Structure): + _fields_ = [('BasicInfo', JOBOBJECT_BASIC_ACCOUNTING_INFORMATION), + ('IoInfo', IO_COUNTERS)] + +# see http://msdn.microsoft.com/en-us/library/ms684147%28VS.85%29.aspx +class JOBOBJECT_BASIC_LIMIT_INFORMATION(Structure): + _fields_ = [('PerProcessUserTimeLimit', LARGE_INTEGER), + ('PerJobUserTimeLimit', LARGE_INTEGER), + ('LimitFlags', DWORD), + ('MinimumWorkingSetSize', SIZE_T), + ('MaximumWorkingSetSize', SIZE_T), + ('ActiveProcessLimit', DWORD), + ('Affinity', ULONG_PTR), + ('PriorityClass', DWORD), + ('SchedulingClass', DWORD) + ] + +# see http://msdn.microsoft.com/en-us/library/ms684156%28VS.85%29.aspx +class JOBOBJECT_EXTENDED_LIMIT_INFORMATION(Structure): + _fields_ = [('BasicLimitInformation', JOBOBJECT_BASIC_LIMIT_INFORMATION), + ('IoInfo', IO_COUNTERS), + ('ProcessMemoryLimit', SIZE_T), + ('JobMemoryLimit', SIZE_T), + ('PeakProcessMemoryUsed', SIZE_T), + ('PeakJobMemoryUsed', SIZE_T)] + +# XXX Magical numbers like 8 should be documented +JobObjectBasicAndIoAccountingInformation = 8 + +# ...like magical number 9 comes from +# http://community.flexerasoftware.com/archive/index.php?t-181670.html +# I wish I had a more canonical source +JobObjectExtendedLimitInformation = 9 + +class JobObjectInfo(object): + mapping = { 'JobObjectBasicAndIoAccountingInformation': 8, + 'JobObjectExtendedLimitInformation': 9 + } + structures = { 8: JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION, + 9: JOBOBJECT_EXTENDED_LIMIT_INFORMATION + } + def __init__(self, _class): + if isinstance(_class, basestring): + assert _class in self.mapping, 'Class should be one of %s; you gave %s' % (self.mapping, _class) + _class = self.mapping[_class] + assert _class in self.structures, 'Class should be one of %s; you gave %s' % (self.structures, _class) + self.code = _class + self.info = self.structures[_class]() + + +QueryInformationJobObjectProto = WINFUNCTYPE( + BOOL, # Return type + HANDLE, # hJob + DWORD, # JobObjectInfoClass + LPVOID, # lpJobObjectInfo + DWORD, # cbJobObjectInfoLength + LPDWORD # lpReturnLength + ) + +QueryInformationJobObjectFlags = ( + (1, 'hJob'), + (1, 'JobObjectInfoClass'), + (1, 'lpJobObjectInfo'), + (1, 'cbJobObjectInfoLength'), + (1, 'lpReturnLength', None) + ) + +_QueryInformationJobObject = QueryInformationJobObjectProto( + ('QueryInformationJobObject', windll.kernel32), + QueryInformationJobObjectFlags + ) + +class SubscriptableReadOnlyStruct(object): + def __init__(self, struct): + self._struct = struct + + def _delegate(self, name): + result = getattr(self._struct, name) + if isinstance(result, Structure): + return SubscriptableReadOnlyStruct(result) + return result + + def __getitem__(self, name): + match = [fname for fname, ftype in self._struct._fields_ + if fname == name] + if match: + return self._delegate(name) + raise KeyError(name) + + def __getattr__(self, name): + return self._delegate(name) + +def QueryInformationJobObject(hJob, JobObjectInfoClass): + jobinfo = JobObjectInfo(JobObjectInfoClass) + result = _QueryInformationJobObject( + hJob=hJob, + JobObjectInfoClass=jobinfo.code, + lpJobObjectInfo=addressof(jobinfo.info), + cbJobObjectInfoLength=sizeof(jobinfo.info) + ) + if not result: + raise WinError() + return SubscriptableReadOnlyStruct(jobinfo.info) + +def test_qijo(): + from killableprocess import Popen + + popen = Popen('c:\\windows\\notepad.exe') + + try: + result = QueryInformationJobObject(0, 8) + raise AssertionError('throw should occur') + except WindowsError, e: + pass + + try: + result = QueryInformationJobObject(0, 1) + raise AssertionError('throw should occur') + except NotImplementedError, e: + pass + + result = QueryInformationJobObject(popen._job, 8) + if result['BasicInfo']['ActiveProcesses'] != 1: + raise AssertionError('expected ActiveProcesses to be 1') + popen.kill() + + result = QueryInformationJobObject(popen._job, 8) + if result.BasicInfo.ActiveProcesses != 0: + raise AssertionError('expected ActiveProcesses to be 0') + +if __name__ == '__main__': + print "testing." + test_qijo() + print "success!" diff --git a/addon-sdk/source/python-lib/mozrunner/winprocess.py b/addon-sdk/source/python-lib/mozrunner/winprocess.py new file mode 100644 index 000000000..16666b0eb --- /dev/null +++ b/addon-sdk/source/python-lib/mozrunner/winprocess.py @@ -0,0 +1,379 @@ +# A module to expose various thread/process/job related structures and +# methods from kernel32 +# +# The MIT License +# +# Copyright (c) 2003-2004 by Peter Astrand <astrand@lysator.liu.se> +# +# Additions and modifications written by Benjamin Smedberg +# <benjamin@smedbergs.us> are Copyright (c) 2006 by the Mozilla Foundation +# <http://www.mozilla.org/> +# +# More Modifications +# Copyright (c) 2006-2007 by Mike Taylor <bear@code-bear.com> +# Copyright (c) 2007-2008 by Mikeal Rogers <mikeal@mozilla.com> +# +# By obtaining, using, and/or copying this software and/or its +# associated documentation, you agree that you have read, understood, +# and will comply with the following terms and conditions: +# +# Permission to use, copy, modify, and distribute this software and +# its associated documentation for any purpose and without fee is +# hereby granted, provided that the above copyright notice appears in +# all copies, and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of the +# author not be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior +# permission. +# +# THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +from ctypes import c_void_p, POINTER, sizeof, Structure, windll, WinError, WINFUNCTYPE +from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LPCWSTR, LPWSTR, UINT, WORD, \ + c_buffer, c_ulong, byref +from qijo import QueryInformationJobObject + +LPVOID = c_void_p +LPBYTE = POINTER(BYTE) +LPDWORD = POINTER(DWORD) +LPBOOL = POINTER(BOOL) + +def ErrCheckBool(result, func, args): + """errcheck function for Windows functions that return a BOOL True + on success""" + if not result: + raise WinError() + return args + + +# AutoHANDLE + +class AutoHANDLE(HANDLE): + """Subclass of HANDLE which will call CloseHandle() on deletion.""" + + CloseHandleProto = WINFUNCTYPE(BOOL, HANDLE) + CloseHandle = CloseHandleProto(("CloseHandle", windll.kernel32)) + CloseHandle.errcheck = ErrCheckBool + + def Close(self): + if self.value and self.value != HANDLE(-1).value: + self.CloseHandle(self) + self.value = 0 + + def __del__(self): + self.Close() + + def __int__(self): + return self.value + +def ErrCheckHandle(result, func, args): + """errcheck function for Windows functions that return a HANDLE.""" + if not result: + raise WinError() + return AutoHANDLE(result) + +# PROCESS_INFORMATION structure + +class PROCESS_INFORMATION(Structure): + _fields_ = [("hProcess", HANDLE), + ("hThread", HANDLE), + ("dwProcessID", DWORD), + ("dwThreadID", DWORD)] + + def __init__(self): + Structure.__init__(self) + + self.cb = sizeof(self) + +LPPROCESS_INFORMATION = POINTER(PROCESS_INFORMATION) + +# STARTUPINFO structure + +class STARTUPINFO(Structure): + _fields_ = [("cb", DWORD), + ("lpReserved", LPWSTR), + ("lpDesktop", LPWSTR), + ("lpTitle", LPWSTR), + ("dwX", DWORD), + ("dwY", DWORD), + ("dwXSize", DWORD), + ("dwYSize", DWORD), + ("dwXCountChars", DWORD), + ("dwYCountChars", DWORD), + ("dwFillAttribute", DWORD), + ("dwFlags", DWORD), + ("wShowWindow", WORD), + ("cbReserved2", WORD), + ("lpReserved2", LPBYTE), + ("hStdInput", HANDLE), + ("hStdOutput", HANDLE), + ("hStdError", HANDLE) + ] +LPSTARTUPINFO = POINTER(STARTUPINFO) + +SW_HIDE = 0 + +STARTF_USESHOWWINDOW = 0x01 +STARTF_USESIZE = 0x02 +STARTF_USEPOSITION = 0x04 +STARTF_USECOUNTCHARS = 0x08 +STARTF_USEFILLATTRIBUTE = 0x10 +STARTF_RUNFULLSCREEN = 0x20 +STARTF_FORCEONFEEDBACK = 0x40 +STARTF_FORCEOFFFEEDBACK = 0x80 +STARTF_USESTDHANDLES = 0x100 + +# EnvironmentBlock + +class EnvironmentBlock: + """An object which can be passed as the lpEnv parameter of CreateProcess. + It is initialized with a dictionary.""" + + def __init__(self, dict): + if not dict: + self._as_parameter_ = None + else: + values = ["%s=%s" % (key, value) + for (key, value) in dict.iteritems()] + values.append("") + self._as_parameter_ = LPCWSTR("\0".join(values)) + +# CreateProcess() + +CreateProcessProto = WINFUNCTYPE(BOOL, # Return type + LPCWSTR, # lpApplicationName + LPWSTR, # lpCommandLine + LPVOID, # lpProcessAttributes + LPVOID, # lpThreadAttributes + BOOL, # bInheritHandles + DWORD, # dwCreationFlags + LPVOID, # lpEnvironment + LPCWSTR, # lpCurrentDirectory + LPSTARTUPINFO, # lpStartupInfo + LPPROCESS_INFORMATION # lpProcessInformation + ) + +CreateProcessFlags = ((1, "lpApplicationName", None), + (1, "lpCommandLine"), + (1, "lpProcessAttributes", None), + (1, "lpThreadAttributes", None), + (1, "bInheritHandles", True), + (1, "dwCreationFlags", 0), + (1, "lpEnvironment", None), + (1, "lpCurrentDirectory", None), + (1, "lpStartupInfo"), + (2, "lpProcessInformation")) + +def ErrCheckCreateProcess(result, func, args): + ErrCheckBool(result, func, args) + # return a tuple (hProcess, hThread, dwProcessID, dwThreadID) + pi = args[9] + return AutoHANDLE(pi.hProcess), AutoHANDLE(pi.hThread), pi.dwProcessID, pi.dwThreadID + +CreateProcess = CreateProcessProto(("CreateProcessW", windll.kernel32), + CreateProcessFlags) +CreateProcess.errcheck = ErrCheckCreateProcess + +# flags for CreateProcess +CREATE_BREAKAWAY_FROM_JOB = 0x01000000 +CREATE_DEFAULT_ERROR_MODE = 0x04000000 +CREATE_NEW_CONSOLE = 0x00000010 +CREATE_NEW_PROCESS_GROUP = 0x00000200 +CREATE_NO_WINDOW = 0x08000000 +CREATE_SUSPENDED = 0x00000004 +CREATE_UNICODE_ENVIRONMENT = 0x00000400 + +# flags for job limit information +# see http://msdn.microsoft.com/en-us/library/ms684147%28VS.85%29.aspx +JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800 +JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK = 0x00001000 + +# XXX these flags should be documented +DEBUG_ONLY_THIS_PROCESS = 0x00000002 +DEBUG_PROCESS = 0x00000001 +DETACHED_PROCESS = 0x00000008 + +# CreateJobObject() + +CreateJobObjectProto = WINFUNCTYPE(HANDLE, # Return type + LPVOID, # lpJobAttributes + LPCWSTR # lpName + ) + +CreateJobObjectFlags = ((1, "lpJobAttributes", None), + (1, "lpName", None)) + +CreateJobObject = CreateJobObjectProto(("CreateJobObjectW", windll.kernel32), + CreateJobObjectFlags) +CreateJobObject.errcheck = ErrCheckHandle + +# AssignProcessToJobObject() + +AssignProcessToJobObjectProto = WINFUNCTYPE(BOOL, # Return type + HANDLE, # hJob + HANDLE # hProcess + ) +AssignProcessToJobObjectFlags = ((1, "hJob"), + (1, "hProcess")) +AssignProcessToJobObject = AssignProcessToJobObjectProto( + ("AssignProcessToJobObject", windll.kernel32), + AssignProcessToJobObjectFlags) +AssignProcessToJobObject.errcheck = ErrCheckBool + +# GetCurrentProcess() +# because os.getPid() is way too easy +GetCurrentProcessProto = WINFUNCTYPE(HANDLE # Return type + ) +GetCurrentProcessFlags = () +GetCurrentProcess = GetCurrentProcessProto( + ("GetCurrentProcess", windll.kernel32), + GetCurrentProcessFlags) +GetCurrentProcess.errcheck = ErrCheckHandle + +# IsProcessInJob() +try: + IsProcessInJobProto = WINFUNCTYPE(BOOL, # Return type + HANDLE, # Process Handle + HANDLE, # Job Handle + LPBOOL # Result + ) + IsProcessInJobFlags = ((1, "ProcessHandle"), + (1, "JobHandle", HANDLE(0)), + (2, "Result")) + IsProcessInJob = IsProcessInJobProto( + ("IsProcessInJob", windll.kernel32), + IsProcessInJobFlags) + IsProcessInJob.errcheck = ErrCheckBool +except AttributeError: + # windows 2k doesn't have this API + def IsProcessInJob(process): + return False + + +# ResumeThread() + +def ErrCheckResumeThread(result, func, args): + if result == -1: + raise WinError() + + return args + +ResumeThreadProto = WINFUNCTYPE(DWORD, # Return type + HANDLE # hThread + ) +ResumeThreadFlags = ((1, "hThread"),) +ResumeThread = ResumeThreadProto(("ResumeThread", windll.kernel32), + ResumeThreadFlags) +ResumeThread.errcheck = ErrCheckResumeThread + +# TerminateProcess() + +TerminateProcessProto = WINFUNCTYPE(BOOL, # Return type + HANDLE, # hProcess + UINT # uExitCode + ) +TerminateProcessFlags = ((1, "hProcess"), + (1, "uExitCode", 127)) +TerminateProcess = TerminateProcessProto( + ("TerminateProcess", windll.kernel32), + TerminateProcessFlags) +TerminateProcess.errcheck = ErrCheckBool + +# TerminateJobObject() + +TerminateJobObjectProto = WINFUNCTYPE(BOOL, # Return type + HANDLE, # hJob + UINT # uExitCode + ) +TerminateJobObjectFlags = ((1, "hJob"), + (1, "uExitCode", 127)) +TerminateJobObject = TerminateJobObjectProto( + ("TerminateJobObject", windll.kernel32), + TerminateJobObjectFlags) +TerminateJobObject.errcheck = ErrCheckBool + +# WaitForSingleObject() + +WaitForSingleObjectProto = WINFUNCTYPE(DWORD, # Return type + HANDLE, # hHandle + DWORD, # dwMilliseconds + ) +WaitForSingleObjectFlags = ((1, "hHandle"), + (1, "dwMilliseconds", -1)) +WaitForSingleObject = WaitForSingleObjectProto( + ("WaitForSingleObject", windll.kernel32), + WaitForSingleObjectFlags) + +INFINITE = -1 +WAIT_TIMEOUT = 0x0102 +WAIT_OBJECT_0 = 0x0 +WAIT_ABANDONED = 0x0080 +WAIT_FAILED = 0xFFFFFFFF + +# GetExitCodeProcess() + +GetExitCodeProcessProto = WINFUNCTYPE(BOOL, # Return type + HANDLE, # hProcess + LPDWORD, # lpExitCode + ) +GetExitCodeProcessFlags = ((1, "hProcess"), + (2, "lpExitCode")) +GetExitCodeProcess = GetExitCodeProcessProto( + ("GetExitCodeProcess", windll.kernel32), + GetExitCodeProcessFlags) +GetExitCodeProcess.errcheck = ErrCheckBool + +def CanCreateJobObject(): + # Running firefox in a job (from cfx) hangs on sites using flash plugin + # so job creation is turned off for now. (see Bug 768651). + return False + +### testing functions + +def parent(): + print 'Starting parent' + currentProc = GetCurrentProcess() + if IsProcessInJob(currentProc): + print >> sys.stderr, "You should not be in a job object to test" + sys.exit(1) + assert CanCreateJobObject() + print 'File: %s' % __file__ + command = [sys.executable, __file__, '-child'] + print 'Running command: %s' % command + process = Popen(command) + process.kill() + code = process.returncode + print 'Child code: %s' % code + assert code == 127 + +def child(): + print 'Starting child' + currentProc = GetCurrentProcess() + injob = IsProcessInJob(currentProc) + print "Is in a job?: %s" % injob + can_create = CanCreateJobObject() + print 'Can create job?: %s' % can_create + process = Popen('c:\\windows\\notepad.exe') + assert process._job + jobinfo = QueryInformationJobObject(process._job, 'JobObjectExtendedLimitInformation') + print 'Job info: %s' % jobinfo + limitflags = jobinfo['BasicLimitInformation']['LimitFlags'] + print 'LimitFlags: %s' % limitflags + process.kill() + +if __name__ == '__main__': + import sys + from killableprocess import Popen + nargs = len(sys.argv[1:]) + if nargs: + if nargs != 1 or sys.argv[1] != '-child': + raise AssertionError('Wrong flags; run like `python /path/to/winprocess.py`') + child() + else: + parent() diff --git a/addon-sdk/source/python-lib/mozrunner/wpk.py b/addon-sdk/source/python-lib/mozrunner/wpk.py new file mode 100644 index 000000000..6c92f5d4e --- /dev/null +++ b/addon-sdk/source/python-lib/mozrunner/wpk.py @@ -0,0 +1,80 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from ctypes import sizeof, windll, addressof, c_wchar, create_unicode_buffer +from ctypes.wintypes import DWORD, HANDLE + +PROCESS_TERMINATE = 0x0001 +PROCESS_QUERY_INFORMATION = 0x0400 +PROCESS_VM_READ = 0x0010 + +def get_pids(process_name): + BIG_ARRAY = DWORD * 4096 + processes = BIG_ARRAY() + needed = DWORD() + + pids = [] + result = windll.psapi.EnumProcesses(processes, + sizeof(processes), + addressof(needed)) + if not result: + return pids + + num_results = needed.value / sizeof(DWORD) + + for i in range(num_results): + pid = processes[i] + process = windll.kernel32.OpenProcess(PROCESS_QUERY_INFORMATION | + PROCESS_VM_READ, + 0, pid) + if process: + module = HANDLE() + result = windll.psapi.EnumProcessModules(process, + addressof(module), + sizeof(module), + addressof(needed)) + if result: + name = create_unicode_buffer(1024) + result = windll.psapi.GetModuleBaseNameW(process, module, + name, len(name)) + # TODO: This might not be the best way to + # match a process name; maybe use a regexp instead. + if name.value.startswith(process_name): + pids.append(pid) + windll.kernel32.CloseHandle(module) + windll.kernel32.CloseHandle(process) + + return pids + +def kill_pid(pid): + process = windll.kernel32.OpenProcess(PROCESS_TERMINATE, 0, pid) + if process: + windll.kernel32.TerminateProcess(process, 0) + windll.kernel32.CloseHandle(process) + +if __name__ == '__main__': + import subprocess + import time + + # This test just opens a new notepad instance and kills it. + + name = 'notepad' + + old_pids = set(get_pids(name)) + subprocess.Popen([name]) + time.sleep(0.25) + new_pids = set(get_pids(name)).difference(old_pids) + + if len(new_pids) != 1: + raise Exception('%s was not opened or get_pids() is ' + 'malfunctioning' % name) + + kill_pid(tuple(new_pids)[0]) + + newest_pids = set(get_pids(name)).difference(old_pids) + + if len(newest_pids) != 0: + raise Exception('kill_pid() is malfunctioning') + + print "Test passed." diff --git a/addon-sdk/source/python-lib/plural-rules-generator.py b/addon-sdk/source/python-lib/plural-rules-generator.py new file mode 100644 index 000000000..02cdee135 --- /dev/null +++ b/addon-sdk/source/python-lib/plural-rules-generator.py @@ -0,0 +1,185 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Program used to generate /packages/api-utils/lib/l10n/plural-rules.js +# Fetch unicode.org data in order to build functions specific to each language +# that will return for a given integer, its plural form name. +# Plural form names are: zero, one, two, few, many, other. +# +# More information here: +# http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html +# http://cldr.unicode.org/index/cldr-spec/plural-rules + +# Usage: +# $ python plural-rules-generator.py > ../packages/api-utils/lib/l10n/plural-rules.js + +import urllib2 +import xml.dom.minidom +import json +import re + +PRINT_CONDITIONS_IN_COMMENTS = False + +UNICODE_ORG_XML_URL = "http://unicode.org/repos/cldr/trunk/common/supplemental/plurals.xml" + +CONDITION_RE = r'n( mod \d+)? (is|in|within|(not in))( not)? ([^\s]+)' + + +def parseCondition(g): + """ + For a given regexp.MatchObject `g` for `CONDITION_RE`, + returns the equivalent JS piece of code + i.e. maps pseudo conditional language from unicode.org XML to JS code + """ + lvalue = "n" + if g.group(1): + lvalue = "(n %% %d)" % int(g.group(1).replace("mod ", "")) + + operator = g.group(2) + if g.group(4): + operator += " not" + + rvalue = g.group(5) + + if operator == "is": + return "%s == %s" % (lvalue, rvalue) + if operator == "is not": + return "%s != %s" % (lvalue, rvalue) + + # "in", "within" or "not in" case: + notPrefix = "" + if operator == "not in": + notPrefix = "!" + + # `rvalue` is a comma seperated list of either: + # - numbers: 42 + # - ranges: 42..72 + sections = rvalue.split(',') + + if ".." not in rvalue: + # If we don't have range, but only a list of integer, + # we can simplify the generated code by using `isIn` + # n in 1,3,6,42 + return "%sisIn(%s, [%s])" % (notPrefix, lvalue, ", ".join(sections)) + + # n in 1..42 + # n in 1..3,42 + subCondition = [] + integers = [] + for sub in sections: + if ".." in sub: + left, right = sub.split("..") + subCondition.append("isBetween(%s, %d, %d)" % ( + lvalue, + int(left), + int(right) + )) + else: + integers.append(int(sub)) + if len(integers) > 1: + subCondition.append("isIn(%s, [%s])" % (lvalue, ", ".join(integers))) + elif len(integers) == 1: + subCondition.append("(%s == %s)" % (lvalue, integers[0])) + return "%s(%s)" % (notPrefix, " || ".join(subCondition)) + +def computeRules(): + """ + Fetch plural rules data directly from unicode.org website: + """ + url = UNICODE_ORG_XML_URL + f = urllib2.urlopen(url) + doc = xml.dom.minidom.parse(f) + + # Read XML document and extract locale to rules mapping + localesMapping = {} + algorithms = {} + for index,pluralRules in enumerate(doc.getElementsByTagName("pluralRules")): + if not index in algorithms: + algorithms[index] = {} + for locale in pluralRules.getAttribute("locales").split(): + localesMapping[locale] = index + for rule in pluralRules.childNodes: + if rule.nodeType != rule.ELEMENT_NODE or rule.tagName != "pluralRule": + continue + pluralForm = rule.getAttribute("count") + algorithm = rule.firstChild.nodeValue + algorithms[index][pluralForm] = algorithm + + # Go through all rules and compute a Javascript code for each of them + rules = {} + for index,rule in algorithms.iteritems(): + lines = [] + for pluralForm in rule: + condition = rule[pluralForm] + originalCondition = str(condition) + + # Convert pseudo language to JS code + condition = rule[pluralForm].lower() + condition = re.sub(CONDITION_RE, parseCondition, condition) + condition = re.sub(r'or', "||", condition) + condition = re.sub(r'and', "&&", condition) + + # Prints original condition in unicode.org pseudo language + if PRINT_CONDITIONS_IN_COMMENTS: + lines.append( '// %s' % originalCondition ) + + lines.append( 'if (%s)' % condition ) + lines.append( ' return "%s";' % pluralForm ) + + rules[index] = "\n ".join(lines) + return localesMapping, rules + + +localesMapping, rules = computeRules() + +rulesLines = [] +for index in rules: + lines = rules[index] + rulesLines.append('"%d": function (n) {' % index) + rulesLines.append(' %s' % lines) + rulesLines.append(' return "other"') + rulesLines.append('},') + +print """/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file is automatically generated with /python-lib/plural-rules-generator.py +// Fetching data from: %s + +// Mapping of short locale name == to == > rule index in following list +const LOCALES_TO_RULES = %s; + +// Utility functions for plural rules methods +function isIn(n, list) { + return list.indexOf(n) !== -1; +} +function isBetween(n, start, end) { + return start <= n && n <= end; +} + +// List of all plural rules methods, that maps an integer to the plural form name to use +const RULES = { + %s +}; + +/** + * Return a function that gives the plural form name for a given integer + * for the specified `locale` + * let fun = getRulesForLocale('en'); + * fun(1) -> 'one' + * fun(0) -> 'other' + * fun(1000) -> 'other' + */ +exports.getRulesForLocale = function getRulesForLocale(locale) { + let index = LOCALES_TO_RULES[locale]; + if (!(index in RULES)) { + console.warn('Plural form unknown for locale "' + locale + '"'); + return function () { return "other"; }; + } + return RULES[index]; +} +""" % (UNICODE_ORG_XML_URL, + json.dumps(localesMapping, sort_keys=True, indent=2), + "\n ".join(rulesLines)) diff --git a/addon-sdk/source/python-lib/simplejson/LICENSE.txt b/addon-sdk/source/python-lib/simplejson/LICENSE.txt new file mode 100644 index 000000000..ad95f29c1 --- /dev/null +++ b/addon-sdk/source/python-lib/simplejson/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2006 Bob Ippolito + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/addon-sdk/source/python-lib/simplejson/__init__.py b/addon-sdk/source/python-lib/simplejson/__init__.py new file mode 100644 index 000000000..adcce7efb --- /dev/null +++ b/addon-sdk/source/python-lib/simplejson/__init__.py @@ -0,0 +1,376 @@ +r""" +A simple, fast, extensible JSON encoder and decoder + +JSON (JavaScript Object Notation) <http://json.org> is a subset of +JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data +interchange format. + +simplejson exposes an API familiar to uses of the standard library +marshal and pickle modules. + +Encoding basic Python object hierarchies:: + + >>> import simplejson + >>> simplejson.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) + '["foo", {"bar": ["baz", null, 1.0, 2]}]' + >>> print simplejson.dumps("\"foo\bar") + "\"foo\bar" + >>> print simplejson.dumps(u'\u1234') + "\u1234" + >>> print simplejson.dumps('\\') + "\\" + >>> print simplejson.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True) + {"a": 0, "b": 0, "c": 0} + >>> from StringIO import StringIO + >>> io = StringIO() + >>> simplejson.dump(['streaming API'], io) + >>> io.getvalue() + '["streaming API"]' + +Compact encoding:: + + >>> import simplejson + >>> simplejson.dumps([1,2,3,{'4': 5, '6': 7}], separators=(',',':')) + '[1,2,3,{"4":5,"6":7}]' + +Pretty printing:: + + >>> import simplejson + >>> print simplejson.dumps({'4': 5, '6': 7}, sort_keys=True, indent=4) + { + "4": 5, + "6": 7 + } + +Decoding JSON:: + + >>> import simplejson + >>> simplejson.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') + [u'foo', {u'bar': [u'baz', None, 1.0, 2]}] + >>> simplejson.loads('"\\"foo\\bar"') + u'"foo\x08ar' + >>> from StringIO import StringIO + >>> io = StringIO('["streaming API"]') + >>> simplejson.load(io) + [u'streaming API'] + +Specializing JSON object decoding:: + + >>> import simplejson + >>> def as_complex(dct): + ... if '__complex__' in dct: + ... return complex(dct['real'], dct['imag']) + ... return dct + ... + >>> simplejson.loads('{"__complex__": true, "real": 1, "imag": 2}', + ... object_hook=as_complex) + (1+2j) + >>> import decimal + >>> simplejson.loads('1.1', parse_float=decimal.Decimal) + Decimal("1.1") + +Extending JSONEncoder:: + + >>> import simplejson + >>> class ComplexEncoder(simplejson.JSONEncoder): + ... def default(self, obj): + ... if isinstance(obj, complex): + ... return [obj.real, obj.imag] + ... return simplejson.JSONEncoder.default(self, obj) + ... + >>> dumps(2 + 1j, cls=ComplexEncoder) + '[2.0, 1.0]' + >>> ComplexEncoder().encode(2 + 1j) + '[2.0, 1.0]' + >>> list(ComplexEncoder().iterencode(2 + 1j)) + ['[', '2.0', ', ', '1.0', ']'] + + +Using simplejson from the shell to validate and +pretty-print:: + + $ echo '{"json":"obj"}' | python -msimplejson.tool + { + "json": "obj" + } + $ echo '{ 1.2:3.4}' | python -msimplejson.tool + Expecting property name: line 1 column 2 (char 2) + +Note that the JSON produced by this module's default settings +is a subset of YAML, so it may be used as a serializer for that as well. +""" +__version__ = '1.9.2' +__all__ = [ + 'dump', 'dumps', 'load', 'loads', + 'JSONDecoder', 'JSONEncoder', +] + +if __name__ == '__main__': + import warnings + warnings.warn('python -msimplejson is deprecated, use python -msiplejson.tool', DeprecationWarning) + from simplejson.decoder import JSONDecoder + from simplejson.encoder import JSONEncoder +else: + from decoder import JSONDecoder + from encoder import JSONEncoder + +_default_encoder = JSONEncoder( + skipkeys=False, + ensure_ascii=True, + check_circular=True, + allow_nan=True, + indent=None, + separators=None, + encoding='utf-8', + default=None, +) + +def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, + allow_nan=True, cls=None, indent=None, separators=None, + encoding='utf-8', default=None, **kw): + """ + Serialize ``obj`` as a JSON formatted stream to ``fp`` (a + ``.write()``-supporting file-like object). + + If ``skipkeys`` is ``True`` then ``dict`` keys that are not basic types + (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) + will be skipped instead of raising a ``TypeError``. + + If ``ensure_ascii`` is ``False``, then the some chunks written to ``fp`` + may be ``unicode`` instances, subject to normal Python ``str`` to + ``unicode`` coercion rules. Unless ``fp.write()`` explicitly + understands ``unicode`` (as in ``codecs.getwriter()``) this is likely + to cause an error. + + If ``check_circular`` is ``False``, then the circular reference check + for container types will be skipped and a circular reference will + result in an ``OverflowError`` (or worse). + + If ``allow_nan`` is ``False``, then it will be a ``ValueError`` to + serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) + in strict compliance of the JSON specification, instead of using the + JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + + If ``indent`` is a non-negative integer, then JSON array elements and object + members will be pretty-printed with that indent level. An indent level + of 0 will only insert newlines. ``None`` is the most compact representation. + + If ``separators`` is an ``(item_separator, dict_separator)`` tuple + then it will be used instead of the default ``(', ', ': ')`` separators. + ``(',', ':')`` is the most compact JSON representation. + + ``encoding`` is the character encoding for str instances, default is UTF-8. + + ``default(obj)`` is a function that should return a serializable version + of obj or raise TypeError. The default simply raises TypeError. + + To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the + ``.default()`` method to serialize additional types), specify it with + the ``cls`` kwarg. + """ + # cached encoder + if (skipkeys is False and ensure_ascii is True and + check_circular is True and allow_nan is True and + cls is None and indent is None and separators is None and + encoding == 'utf-8' and default is None and not kw): + iterable = _default_encoder.iterencode(obj) + else: + if cls is None: + cls = JSONEncoder + iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, + check_circular=check_circular, allow_nan=allow_nan, indent=indent, + separators=separators, encoding=encoding, + default=default, **kw).iterencode(obj) + # could accelerate with writelines in some versions of Python, at + # a debuggability cost + for chunk in iterable: + fp.write(chunk) + + +def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, + allow_nan=True, cls=None, indent=None, separators=None, + encoding='utf-8', default=None, **kw): + """ + Serialize ``obj`` to a JSON formatted ``str``. + + If ``skipkeys`` is ``True`` then ``dict`` keys that are not basic types + (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) + will be skipped instead of raising a ``TypeError``. + + If ``ensure_ascii`` is ``False``, then the return value will be a + ``unicode`` instance subject to normal Python ``str`` to ``unicode`` + coercion rules instead of being escaped to an ASCII ``str``. + + If ``check_circular`` is ``False``, then the circular reference check + for container types will be skipped and a circular reference will + result in an ``OverflowError`` (or worse). + + If ``allow_nan`` is ``False``, then it will be a ``ValueError`` to + serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in + strict compliance of the JSON specification, instead of using the + JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + + If ``indent`` is a non-negative integer, then JSON array elements and + object members will be pretty-printed with that indent level. An indent + level of 0 will only insert newlines. ``None`` is the most compact + representation. + + If ``separators`` is an ``(item_separator, dict_separator)`` tuple + then it will be used instead of the default ``(', ', ': ')`` separators. + ``(',', ':')`` is the most compact JSON representation. + + ``encoding`` is the character encoding for str instances, default is UTF-8. + + ``default(obj)`` is a function that should return a serializable version + of obj or raise TypeError. The default simply raises TypeError. + + To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the + ``.default()`` method to serialize additional types), specify it with + the ``cls`` kwarg. + """ + # cached encoder + if (skipkeys is False and ensure_ascii is True and + check_circular is True and allow_nan is True and + cls is None and indent is None and separators is None and + encoding == 'utf-8' and default is None and not kw): + return _default_encoder.encode(obj) + if cls is None: + cls = JSONEncoder + return cls( + skipkeys=skipkeys, ensure_ascii=ensure_ascii, + check_circular=check_circular, allow_nan=allow_nan, indent=indent, + separators=separators, encoding=encoding, default=default, + **kw).encode(obj) + + +_default_decoder = JSONDecoder(encoding=None, object_hook=None) + + +def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, **kw): + """ + Deserialize ``fp`` (a ``.read()``-supporting file-like object containing + a JSON document) to a Python object. + + If the contents of ``fp`` is encoded with an ASCII based encoding other + than utf-8 (e.g. latin-1), then an appropriate ``encoding`` name must + be specified. Encodings that are not ASCII based (such as UCS-2) are + not allowed, and should be wrapped with + ``codecs.getreader(fp)(encoding)``, or simply decoded to a ``unicode`` + object and passed to ``loads()`` + + ``object_hook`` is an optional function that will be called with the + result of any object literal decode (a ``dict``). The return value of + ``object_hook`` will be used instead of the ``dict``. This feature + can be used to implement custom decoders (e.g. JSON-RPC class hinting). + + To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` + kwarg. + """ + return loads(fp.read(), + encoding=encoding, cls=cls, object_hook=object_hook, + parse_float=parse_float, parse_int=parse_int, + parse_constant=parse_constant, **kw) + + +def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, **kw): + """ + Deserialize ``s`` (a ``str`` or ``unicode`` instance containing a JSON + document) to a Python object. + + If ``s`` is a ``str`` instance and is encoded with an ASCII based encoding + other than utf-8 (e.g. latin-1) then an appropriate ``encoding`` name + must be specified. Encodings that are not ASCII based (such as UCS-2) + are not allowed and should be decoded to ``unicode`` first. + + ``object_hook`` is an optional function that will be called with the + result of any object literal decode (a ``dict``). The return value of + ``object_hook`` will be used instead of the ``dict``. This feature + can be used to implement custom decoders (e.g. JSON-RPC class hinting). + + ``parse_float``, if specified, will be called with the string + of every JSON float to be decoded. By default this is equivalent to + float(num_str). This can be used to use another datatype or parser + for JSON floats (e.g. decimal.Decimal). + + ``parse_int``, if specified, will be called with the string + of every JSON int to be decoded. By default this is equivalent to + int(num_str). This can be used to use another datatype or parser + for JSON integers (e.g. float). + + ``parse_constant``, if specified, will be called with one of the + following strings: -Infinity, Infinity, NaN, null, true, false. + This can be used to raise an exception if invalid JSON numbers + are encountered. + + To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` + kwarg. + """ + if (cls is None and encoding is None and object_hook is None and + parse_int is None and parse_float is None and + parse_constant is None and not kw): + return _default_decoder.decode(s) + if cls is None: + cls = JSONDecoder + if object_hook is not None: + kw['object_hook'] = object_hook + if parse_float is not None: + kw['parse_float'] = parse_float + if parse_int is not None: + kw['parse_int'] = parse_int + if parse_constant is not None: + kw['parse_constant'] = parse_constant + return cls(encoding=encoding, **kw).decode(s) + + +# +# Compatibility cruft from other libraries +# + + +def decode(s): + """ + demjson, python-cjson API compatibility hook. Use loads(s) instead. + """ + import warnings + warnings.warn("simplejson.loads(s) should be used instead of decode(s)", + DeprecationWarning) + return loads(s) + + +def encode(obj): + """ + demjson, python-cjson compatibility hook. Use dumps(s) instead. + """ + import warnings + warnings.warn("simplejson.dumps(s) should be used instead of encode(s)", + DeprecationWarning) + return dumps(obj) + + +def read(s): + """ + jsonlib, JsonUtils, python-json, json-py API compatibility hook. + Use loads(s) instead. + """ + import warnings + warnings.warn("simplejson.loads(s) should be used instead of read(s)", + DeprecationWarning) + return loads(s) + + +def write(obj): + """ + jsonlib, JsonUtils, python-json, json-py API compatibility hook. + Use dumps(s) instead. + """ + import warnings + warnings.warn("simplejson.dumps(s) should be used instead of write(s)", + DeprecationWarning) + return dumps(obj) + + +if __name__ == '__main__': + import simplejson.tool + simplejson.tool.main() diff --git a/addon-sdk/source/python-lib/simplejson/decoder.py b/addon-sdk/source/python-lib/simplejson/decoder.py new file mode 100644 index 000000000..baf10e990 --- /dev/null +++ b/addon-sdk/source/python-lib/simplejson/decoder.py @@ -0,0 +1,343 @@ +""" +Implementation of JSONDecoder +""" +import re +import sys + +from simplejson.scanner import Scanner, pattern +try: + from simplejson._speedups import scanstring as c_scanstring +except ImportError: + pass + +FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL + +def _floatconstants(): + import struct + import sys + _BYTES = '7FF80000000000007FF0000000000000'.decode('hex') + if sys.byteorder != 'big': + _BYTES = _BYTES[:8][::-1] + _BYTES[8:][::-1] + nan, inf = struct.unpack('dd', _BYTES) + return nan, inf, -inf + +NaN, PosInf, NegInf = _floatconstants() + + +def linecol(doc, pos): + lineno = doc.count('\n', 0, pos) + 1 + if lineno == 1: + colno = pos + else: + colno = pos - doc.rindex('\n', 0, pos) + return lineno, colno + + +def errmsg(msg, doc, pos, end=None): + lineno, colno = linecol(doc, pos) + if end is None: + return '%s: line %d column %d (char %d)' % (msg, lineno, colno, pos) + endlineno, endcolno = linecol(doc, end) + return '%s: line %d column %d - line %d column %d (char %d - %d)' % ( + msg, lineno, colno, endlineno, endcolno, pos, end) + + +_CONSTANTS = { + '-Infinity': NegInf, + 'Infinity': PosInf, + 'NaN': NaN, + 'true': True, + 'false': False, + 'null': None, +} + +def JSONConstant(match, context, c=_CONSTANTS): + s = match.group(0) + fn = getattr(context, 'parse_constant', None) + if fn is None: + rval = c[s] + else: + rval = fn(s) + return rval, None +pattern('(-?Infinity|NaN|true|false|null)')(JSONConstant) + + +def JSONNumber(match, context): + match = JSONNumber.regex.match(match.string, *match.span()) + integer, frac, exp = match.groups() + if frac or exp: + fn = getattr(context, 'parse_float', None) or float + res = fn(integer + (frac or '') + (exp or '')) + else: + fn = getattr(context, 'parse_int', None) or int + res = fn(integer) + return res, None +pattern(r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?')(JSONNumber) + + +STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS) +BACKSLASH = { + '"': u'"', '\\': u'\\', '/': u'/', + 'b': u'\b', 'f': u'\f', 'n': u'\n', 'r': u'\r', 't': u'\t', +} + +DEFAULT_ENCODING = "utf-8" + +def py_scanstring(s, end, encoding=None, strict=True, _b=BACKSLASH, _m=STRINGCHUNK.match): + if encoding is None: + encoding = DEFAULT_ENCODING + chunks = [] + _append = chunks.append + begin = end - 1 + while 1: + chunk = _m(s, end) + if chunk is None: + raise ValueError( + errmsg("Unterminated string starting at", s, begin)) + end = chunk.end() + content, terminator = chunk.groups() + if content: + if not isinstance(content, unicode): + content = unicode(content, encoding) + _append(content) + if terminator == '"': + break + elif terminator != '\\': + if strict: + raise ValueError(errmsg("Invalid control character %r at", s, end)) + else: + _append(terminator) + continue + try: + esc = s[end] + except IndexError: + raise ValueError( + errmsg("Unterminated string starting at", s, begin)) + if esc != 'u': + try: + m = _b[esc] + except KeyError: + raise ValueError( + errmsg("Invalid \\escape: %r" % (esc,), s, end)) + end += 1 + else: + esc = s[end + 1:end + 5] + next_end = end + 5 + msg = "Invalid \\uXXXX escape" + try: + if len(esc) != 4: + raise ValueError + uni = int(esc, 16) + if 0xd800 <= uni <= 0xdbff and sys.maxunicode > 65535: + msg = "Invalid \\uXXXX\\uXXXX surrogate pair" + if not s[end + 5:end + 7] == '\\u': + raise ValueError + esc2 = s[end + 7:end + 11] + if len(esc2) != 4: + raise ValueError + uni2 = int(esc2, 16) + uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00)) + next_end += 6 + m = unichr(uni) + except ValueError: + raise ValueError(errmsg(msg, s, end)) + end = next_end + _append(m) + return u''.join(chunks), end + + +# Use speedup +try: + scanstring = c_scanstring +except NameError: + scanstring = py_scanstring + +def JSONString(match, context): + encoding = getattr(context, 'encoding', None) + strict = getattr(context, 'strict', True) + return scanstring(match.string, match.end(), encoding, strict) +pattern(r'"')(JSONString) + + +WHITESPACE = re.compile(r'\s*', FLAGS) + +def JSONObject(match, context, _w=WHITESPACE.match): + pairs = {} + s = match.string + end = _w(s, match.end()).end() + nextchar = s[end:end + 1] + # Trivial empty object + if nextchar == '}': + return pairs, end + 1 + if nextchar != '"': + raise ValueError(errmsg("Expecting property name", s, end)) + end += 1 + encoding = getattr(context, 'encoding', None) + strict = getattr(context, 'strict', True) + iterscan = JSONScanner.iterscan + while True: + key, end = scanstring(s, end, encoding, strict) + end = _w(s, end).end() + if s[end:end + 1] != ':': + raise ValueError(errmsg("Expecting : delimiter", s, end)) + end = _w(s, end + 1).end() + try: + value, end = iterscan(s, idx=end, context=context).next() + except StopIteration: + raise ValueError(errmsg("Expecting object", s, end)) + pairs[key] = value + end = _w(s, end).end() + nextchar = s[end:end + 1] + end += 1 + if nextchar == '}': + break + if nextchar != ',': + raise ValueError(errmsg("Expecting , delimiter", s, end - 1)) + end = _w(s, end).end() + nextchar = s[end:end + 1] + end += 1 + if nextchar != '"': + raise ValueError(errmsg("Expecting property name", s, end - 1)) + object_hook = getattr(context, 'object_hook', None) + if object_hook is not None: + pairs = object_hook(pairs) + return pairs, end +pattern(r'{')(JSONObject) + + +def JSONArray(match, context, _w=WHITESPACE.match): + values = [] + s = match.string + end = _w(s, match.end()).end() + # Look-ahead for trivial empty array + nextchar = s[end:end + 1] + if nextchar == ']': + return values, end + 1 + iterscan = JSONScanner.iterscan + while True: + try: + value, end = iterscan(s, idx=end, context=context).next() + except StopIteration: + raise ValueError(errmsg("Expecting object", s, end)) + values.append(value) + end = _w(s, end).end() + nextchar = s[end:end + 1] + end += 1 + if nextchar == ']': + break + if nextchar != ',': + raise ValueError(errmsg("Expecting , delimiter", s, end)) + end = _w(s, end).end() + return values, end +pattern(r'\[')(JSONArray) + + +ANYTHING = [ + JSONObject, + JSONArray, + JSONString, + JSONConstant, + JSONNumber, +] + +JSONScanner = Scanner(ANYTHING) + + +class JSONDecoder(object): + """ + Simple JSON <http://json.org> decoder + + Performs the following translations in decoding by default: + + +---------------+-------------------+ + | JSON | Python | + +===============+===================+ + | object | dict | + +---------------+-------------------+ + | array | list | + +---------------+-------------------+ + | string | unicode | + +---------------+-------------------+ + | number (int) | int, long | + +---------------+-------------------+ + | number (real) | float | + +---------------+-------------------+ + | true | True | + +---------------+-------------------+ + | false | False | + +---------------+-------------------+ + | null | None | + +---------------+-------------------+ + + It also understands ``NaN``, ``Infinity``, and ``-Infinity`` as + their corresponding ``float`` values, which is outside the JSON spec. + """ + + _scanner = Scanner(ANYTHING) + __all__ = ['__init__', 'decode', 'raw_decode'] + + def __init__(self, encoding=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, strict=True): + """ + ``encoding`` determines the encoding used to interpret any ``str`` + objects decoded by this instance (utf-8 by default). It has no + effect when decoding ``unicode`` objects. + + Note that currently only encodings that are a superset of ASCII work, + strings of other encodings should be passed in as ``unicode``. + + ``object_hook``, if specified, will be called with the result + of every JSON object decoded and its return value will be used in + place of the given ``dict``. This can be used to provide custom + deserializations (e.g. to support JSON-RPC class hinting). + + ``parse_float``, if specified, will be called with the string + of every JSON float to be decoded. By default this is equivalent to + float(num_str). This can be used to use another datatype or parser + for JSON floats (e.g. decimal.Decimal). + + ``parse_int``, if specified, will be called with the string + of every JSON int to be decoded. By default this is equivalent to + int(num_str). This can be used to use another datatype or parser + for JSON integers (e.g. float). + + ``parse_constant``, if specified, will be called with one of the + following strings: -Infinity, Infinity, NaN, null, true, false. + This can be used to raise an exception if invalid JSON numbers + are encountered. + """ + self.encoding = encoding + self.object_hook = object_hook + self.parse_float = parse_float + self.parse_int = parse_int + self.parse_constant = parse_constant + self.strict = strict + + def decode(self, s, _w=WHITESPACE.match): + """ + Return the Python representation of ``s`` (a ``str`` or ``unicode`` + instance containing a JSON document) + """ + obj, end = self.raw_decode(s, idx=_w(s, 0).end()) + end = _w(s, end).end() + if end != len(s): + raise ValueError(errmsg("Extra data", s, end, len(s))) + return obj + + def raw_decode(self, s, **kw): + """ + Decode a JSON document from ``s`` (a ``str`` or ``unicode`` beginning + with a JSON document) and return a 2-tuple of the Python + representation and the index in ``s`` where the document ended. + + This can be used to decode a JSON document from a string that may + have extraneous data at the end. + """ + kw.setdefault('context', self) + try: + obj, end = self._scanner.iterscan(s, **kw).next() + except StopIteration: + raise ValueError("No JSON object could be decoded") + return obj, end + +__all__ = ['JSONDecoder'] diff --git a/addon-sdk/source/python-lib/simplejson/encoder.py b/addon-sdk/source/python-lib/simplejson/encoder.py new file mode 100644 index 000000000..befc5c1f5 --- /dev/null +++ b/addon-sdk/source/python-lib/simplejson/encoder.py @@ -0,0 +1,395 @@ +""" +Implementation of JSONEncoder +""" +import re + +try: + from simplejson._speedups import encode_basestring_ascii as c_encode_basestring_ascii +except ImportError: + pass + +ESCAPE = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t]') +ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])') +HAS_UTF8 = re.compile(r'[\x80-\xff]') +ESCAPE_DCT = { + '\\': '\\\\', + '"': '\\"', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', +} +for i in range(0x20): + ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,)) + +# Assume this produces an infinity on all machines (probably not guaranteed) +INFINITY = float('1e66666') +FLOAT_REPR = repr + +def floatstr(o, allow_nan=True): + """ + Check for specials. Note that this type of test is processor- and/or + platform-specific, so do tests which don't depend on the internals. + """ + if o != o: + text = 'NaN' + elif o == INFINITY: + text = 'Infinity' + elif o == -INFINITY: + text = '-Infinity' + else: + return FLOAT_REPR(o) + + if not allow_nan: + raise ValueError("Out of range float values are not JSON compliant: %r" + % (o,)) + + return text + + +def encode_basestring(s): + """ + Return a JSON representation of a Python string + """ + def replace(match): + return ESCAPE_DCT[match.group(0)] + return '"' + ESCAPE.sub(replace, s) + '"' + + +def py_encode_basestring_ascii(s): + if isinstance(s, str) and HAS_UTF8.search(s) is not None: + s = s.decode('utf-8') + def replace(match): + s = match.group(0) + try: + return ESCAPE_DCT[s] + except KeyError: + n = ord(s) + if n < 0x10000: + return '\\u%04x' % (n,) + else: + # surrogate pair + n -= 0x10000 + s1 = 0xd800 | ((n >> 10) & 0x3ff) + s2 = 0xdc00 | (n & 0x3ff) + return '\\u%04x\\u%04x' % (s1, s2) + return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"' + + +try: + encode_basestring_ascii = c_encode_basestring_ascii +except NameError: + encode_basestring_ascii = py_encode_basestring_ascii + + +class JSONEncoder(object): + """ + Extensible JSON <http://json.org> encoder for Python data structures. + + Supports the following objects and types by default: + + +-------------------+---------------+ + | Python | JSON | + +===================+===============+ + | dict | object | + +-------------------+---------------+ + | list, tuple | array | + +-------------------+---------------+ + | str, unicode | string | + +-------------------+---------------+ + | int, long, float | number | + +-------------------+---------------+ + | True | true | + +-------------------+---------------+ + | False | false | + +-------------------+---------------+ + | None | null | + +-------------------+---------------+ + + To extend this to recognize other objects, subclass and implement a + ``.default()`` method with another method that returns a serializable + object for ``o`` if possible, otherwise it should call the superclass + implementation (to raise ``TypeError``). + """ + __all__ = ['__init__', 'default', 'encode', 'iterencode'] + item_separator = ', ' + key_separator = ': ' + def __init__(self, skipkeys=False, ensure_ascii=True, + check_circular=True, allow_nan=True, sort_keys=False, + indent=None, separators=None, encoding='utf-8', default=None): + """ + Constructor for JSONEncoder, with sensible defaults. + + If skipkeys is False, then it is a TypeError to attempt + encoding of keys that are not str, int, long, float or None. If + skipkeys is True, such items are simply skipped. + + If ensure_ascii is True, the output is guaranteed to be str + objects with all incoming unicode characters escaped. If + ensure_ascii is false, the output will be unicode object. + + If check_circular is True, then lists, dicts, and custom encoded + objects will be checked for circular references during encoding to + prevent an infinite recursion (which would cause an OverflowError). + Otherwise, no such check takes place. + + If allow_nan is True, then NaN, Infinity, and -Infinity will be + encoded as such. This behavior is not JSON specification compliant, + but is consistent with most JavaScript based encoders and decoders. + Otherwise, it will be a ValueError to encode such floats. + + If sort_keys is True, then the output of dictionaries will be + sorted by key; this is useful for regression tests to ensure + that JSON serializations can be compared on a day-to-day basis. + + If indent is a non-negative integer, then JSON array + elements and object members will be pretty-printed with that + indent level. An indent level of 0 will only insert newlines. + None is the most compact representation. + + If specified, separators should be a (item_separator, key_separator) + tuple. The default is (', ', ': '). To get the most compact JSON + representation you should specify (',', ':') to eliminate whitespace. + + If specified, default is a function that gets called for objects + that can't otherwise be serialized. It should return a JSON encodable + version of the object or raise a ``TypeError``. + + If encoding is not None, then all input strings will be + transformed into unicode using that encoding prior to JSON-encoding. + The default is UTF-8. + """ + + self.skipkeys = skipkeys + self.ensure_ascii = ensure_ascii + self.check_circular = check_circular + self.allow_nan = allow_nan + self.sort_keys = sort_keys + self.indent = indent + self.current_indent_level = 0 + if separators is not None: + self.item_separator, self.key_separator = separators + if default is not None: + self.default = default + self.encoding = encoding + + def _newline_indent(self): + """ + Indent lines by level + """ + return '\n' + (' ' * (self.indent * self.current_indent_level)) + + def _iterencode_list(self, lst, markers=None): + """ + Encoding lists, yielding by level + """ + if not lst: + yield '[]' + return + if markers is not None: + markerid = id(lst) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = lst + yield '[' + if self.indent is not None: + self.current_indent_level += 1 + newline_indent = self._newline_indent() + separator = self.item_separator + newline_indent + yield newline_indent + else: + newline_indent = None + separator = self.item_separator + first = True + for value in lst: + if first: + first = False + else: + yield separator + for chunk in self._iterencode(value, markers): + yield chunk + if newline_indent is not None: + self.current_indent_level -= 1 + yield self._newline_indent() + yield ']' + if markers is not None: + del markers[markerid] + + def _iterencode_dict(self, dct, markers=None): + """ + Encoding dictionaries, yielding by level + """ + if not dct: + yield '{}' + return + if markers is not None: + markerid = id(dct) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = dct + yield '{' + key_separator = self.key_separator + if self.indent is not None: + self.current_indent_level += 1 + newline_indent = self._newline_indent() + item_separator = self.item_separator + newline_indent + yield newline_indent + else: + newline_indent = None + item_separator = self.item_separator + first = True + if self.ensure_ascii: + encoder = encode_basestring_ascii + else: + encoder = encode_basestring + allow_nan = self.allow_nan + if self.sort_keys: + keys = dct.keys() + keys.sort() + items = [(k, dct[k]) for k in keys] + else: + items = dct.iteritems() + _encoding = self.encoding + _do_decode = (_encoding is not None + and not (_encoding == 'utf-8')) + for key, value in items: + if isinstance(key, str): + if _do_decode: + key = key.decode(_encoding) + elif isinstance(key, basestring): + pass + # JavaScript is weakly typed for these, so it makes sense to + # also allow them. Many encoders seem to do something like this. + elif isinstance(key, float): + key = floatstr(key, allow_nan) + elif isinstance(key, (int, long)): + key = str(key) + elif key is True: + key = 'true' + elif key is False: + key = 'false' + elif key is None: + key = 'null' + elif self.skipkeys: + continue + else: + raise TypeError("key %r is not a string" % (key,)) + if first: + first = False + else: + yield item_separator + yield encoder(key) + yield key_separator + for chunk in self._iterencode(value, markers): + yield chunk + if newline_indent is not None: + self.current_indent_level -= 1 + yield self._newline_indent() + yield '}' + if markers is not None: + del markers[markerid] + + def _iterencode(self, o, markers=None): + if isinstance(o, basestring): + if self.ensure_ascii: + encoder = encode_basestring_ascii + else: + encoder = encode_basestring + _encoding = self.encoding + if (_encoding is not None and isinstance(o, str) + and not (_encoding == 'utf-8')): + o = o.decode(_encoding) + yield encoder(o) + elif o is None: + yield 'null' + elif o is True: + yield 'true' + elif o is False: + yield 'false' + elif isinstance(o, (int, long)): + yield str(o) + elif isinstance(o, float): + yield floatstr(o, self.allow_nan) + elif isinstance(o, (list, tuple)): + for chunk in self._iterencode_list(o, markers): + yield chunk + elif isinstance(o, dict): + for chunk in self._iterencode_dict(o, markers): + yield chunk + else: + if markers is not None: + markerid = id(o) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = o + for chunk in self._iterencode_default(o, markers): + yield chunk + if markers is not None: + del markers[markerid] + + def _iterencode_default(self, o, markers=None): + newobj = self.default(o) + return self._iterencode(newobj, markers) + + def default(self, o): + """ + Implement this method in a subclass such that it returns + a serializable object for ``o``, or calls the base implementation + (to raise a ``TypeError``). + + For example, to support arbitrary iterators, you could + implement default like this:: + + def default(self, o): + try: + iterable = iter(o) + except TypeError: + pass + else: + return list(iterable) + return JSONEncoder.default(self, o) + """ + raise TypeError("%r is not JSON serializable" % (o,)) + + def encode(self, o): + """ + Return a JSON string representation of a Python data structure. + + >>> JSONEncoder().encode({"foo": ["bar", "baz"]}) + '{"foo": ["bar", "baz"]}' + """ + # This is for extremely simple cases and benchmarks. + if isinstance(o, basestring): + if isinstance(o, str): + _encoding = self.encoding + if (_encoding is not None + and not (_encoding == 'utf-8')): + o = o.decode(_encoding) + if self.ensure_ascii: + return encode_basestring_ascii(o) + else: + return encode_basestring(o) + # This doesn't pass the iterator directly to ''.join() because the + # exceptions aren't as detailed. The list call should be roughly + # equivalent to the PySequence_Fast that ''.join() would do. + chunks = list(self.iterencode(o)) + return ''.join(chunks) + + def iterencode(self, o): + """ + Encode the given object and yield each string + representation as available. + + For example:: + + for chunk in JSONEncoder().iterencode(bigobject): + mysocket.write(chunk) + """ + if self.check_circular: + markers = {} + else: + markers = None + return self._iterencode(o, markers) + +__all__ = ['JSONEncoder'] diff --git a/addon-sdk/source/python-lib/simplejson/scanner.py b/addon-sdk/source/python-lib/simplejson/scanner.py new file mode 100644 index 000000000..2a18390d0 --- /dev/null +++ b/addon-sdk/source/python-lib/simplejson/scanner.py @@ -0,0 +1,67 @@ +""" +Iterator based sre token scanner +""" +import re +from re import VERBOSE, MULTILINE, DOTALL +import sre_parse +import sre_compile +import sre_constants +from sre_constants import BRANCH, SUBPATTERN + +__all__ = ['Scanner', 'pattern'] + +FLAGS = (VERBOSE | MULTILINE | DOTALL) + +class Scanner(object): + def __init__(self, lexicon, flags=FLAGS): + self.actions = [None] + # Combine phrases into a compound pattern + s = sre_parse.Pattern() + s.flags = flags + p = [] + for idx, token in enumerate(lexicon): + phrase = token.pattern + try: + subpattern = sre_parse.SubPattern(s, + [(SUBPATTERN, (idx + 1, sre_parse.parse(phrase, flags)))]) + except sre_constants.error: + raise + p.append(subpattern) + self.actions.append(token) + + s.groups = len(p) + 1 # NOTE(guido): Added to make SRE validation work + p = sre_parse.SubPattern(s, [(BRANCH, (None, p))]) + self.scanner = sre_compile.compile(p) + + def iterscan(self, string, idx=0, context=None): + """ + Yield match, end_idx for each match + """ + match = self.scanner.scanner(string, idx).match + actions = self.actions + lastend = idx + end = len(string) + while True: + m = match() + if m is None: + break + matchbegin, matchend = m.span() + if lastend == matchend: + break + action = actions[m.lastindex] + if action is not None: + rval, next_pos = action(m, context) + if next_pos is not None and next_pos != matchend: + # "fast forward" the scanner + matchend = next_pos + match = self.scanner.scanner(string, matchend).match + yield rval, matchend + lastend = matchend + + +def pattern(pattern, flags=FLAGS): + def decorator(fn): + fn.pattern = pattern + fn.regex = re.compile(pattern, flags) + return fn + return decorator
\ No newline at end of file diff --git a/addon-sdk/source/python-lib/simplejson/tool.py b/addon-sdk/source/python-lib/simplejson/tool.py new file mode 100644 index 000000000..caa1818f8 --- /dev/null +++ b/addon-sdk/source/python-lib/simplejson/tool.py @@ -0,0 +1,44 @@ +r""" +Using simplejson from the shell to validate and +pretty-print:: + + $ echo '{"json":"obj"}' | python -msimplejson + { + "json": "obj" + } + $ echo '{ 1.2:3.4}' | python -msimplejson + Expecting property name: line 1 column 2 (char 2) + +Note that the JSON produced by this module's default settings +is a subset of YAML, so it may be used as a serializer for that as well. +""" +import simplejson + +# +# Pretty printer: +# curl http://mochikit.com/examples/ajax_tables/domains.json | python -msimplejson.tool +# + +def main(): + import sys + if len(sys.argv) == 1: + infile = sys.stdin + outfile = sys.stdout + elif len(sys.argv) == 2: + infile = open(sys.argv[1], 'rb') + outfile = sys.stdout + elif len(sys.argv) == 3: + infile = open(sys.argv[1], 'rb') + outfile = open(sys.argv[2], 'wb') + else: + raise SystemExit("%s [infile [outfile]]" % (sys.argv[0],)) + try: + obj = simplejson.load(infile) + except ValueError, e: + raise SystemExit(e) + simplejson.dump(obj, outfile, sort_keys=True, indent=4) + outfile.write('\n') + + +if __name__ == '__main__': + main() |