Commits

Ronald Oussoren  committed 3423d2d

First stab at a number of helper scripts that
will make it easier to test PyObjC with a
number of different python installations.

The scripts don't work yet, and I want to add
another script that enables me to build
on my 10.6 machine and run tests on 10.4
and 10.5 machines.

  • Participants
  • Parent commits f4c902b
  • Branches pyobjc-ancient

Comments (0)

Files changed (4)

File build-support/ReadMe.txt

+This directory contains a number of scripts
+that help in testing PyObjC on a number 
+of platforms.
+
+All scripts are python3 scripts, but are currently
+simple enough that they probably run with python2.6
+as well.
+
+WARNING: The scripts are a work-in-progress and don't 
+work right now.
+
+* build_frameworks.py:
+  This script creates a couple of framework
+  builds of Python for various Python
+  releases:
+
+  - 32-bit (i386, ppc) and intel (i386, x86)
+  - 2.6, 2.7, 3.1 and 3.2
+
+  All builds are with '--with-pydebug' and
+  are installed using an alternate framework
+  name to avoid messing with an already existing
+  install.
+
+  TODO: also install distribute and virtualenv
+
+* run_tests.py
+
+  Uses the frameworks installed by build_frameworks.py
+  to run all PyObjC tests with all supported Python
+  variants.
+
+  TODO:
+  - create virtual env and install PyObjC dependencies
+  - install pyobjc-core, pyobjc-framework-Cocoa and
+    pyobjc-framework-Quartz
+  - run tests for pyobjc-core and all framework wrappers
+  - collect the results and create an HTML page with
+    the summary and details.
+  - send e-mail with the result to an e-mail address
+
+* TODO: run_distributed_tests.py
+
+  This script will do something simular to run_tests,
+  but builds on a 10.6 machine and runs the tests on
+  a number of different machines (10.4, 10.5)

File build-support/build_frameworks.py

+#!/usr/bin/env python3
+"""
+Script that builds a number of python frameworks as
+used by the run_tests.py script
+"""
+import subprocess, getopt, logging, os, sys, shutil
+
+gBaseDir = os.path.dirname(os.path.abspath(__file__))
+
+gURLMap = {
+    '2.6': 'http://svn.python.org/projects/python/branches/release26-maint',
+    '2.7': 'http://svn.python.org/projects/python/trunk',
+
+    '3.1': 'http://svn.python.org/projects/python/branches/release31-maint',
+    '3.2': 'http://svn.python.org/projects/python/branches/py3k',
+}
+
+class ShellError (Exception):
+    pass
+
+def create_checkout(version):
+    lg = logging.getLogger("create_checkout")
+    lg.info("Create checkout for %s", version)
+
+    checkoutdir = os.path.join(gBaseDir, "checkouts", version)
+    if not os.path.exists(checkoutdir):
+        lg.debug("Create directory %r", checkoutdir)
+        os.makedirs(checkoutdir)
+
+    if os.path.exists(os.path.join(checkoutdir, '.svn')):
+        lg.debug("Update checkout")
+        p = subprocess.Popen([
+            'svn', 'up'],
+            cwd=checkoutdir)
+    else:
+        lg.debug("Initial checkout checkout")
+        p = subprocess.Popen([
+            'svn', 'co', gURLMap[version], checkoutdir])
+
+    xit = p.wait()
+    if xit == 0:
+        lg.info("Checkout for %s is now up-to-date", version)
+    else:
+        lg.warn("Checkout for %s failed", version)
+        raise ShellError(xit)
+
+def build_framework(version, archs):
+    lg = logging.getLogger("build_framework")
+    lg.info("Build framework version=%r archs=%r", version, archs)
+
+    builddir = os.path.join(gBaseDir, "checkouts", version, "build")
+    if os.path.exists(builddir):
+        lg.debug("Remove existing build tree")
+        shutil.rmtree(builddir)
+
+    lg.debug("Create build tree %r", builddir)
+    os.mkdir(builddir)
+
+    lg.debug("Running 'configure'")
+    p = subprocess.Popen([
+        "../configure",
+            "--enable-framework",
+            "--with-framework-name=DbgPython-{0}".format(archs),
+            "--enable-universalsdk=/",
+            "--with-universal-archs={0}".format(archs),
+            "--with-pydebug",
+        ], cwd=builddir)
+
+    xit = p.wait()
+    if xit != 0:
+        lg.debug("Configure failed for %s", version)
+        raise ShellError(xit)
+    
+    lg.debug("Running 'make'")
+    p = subprocess.Popen([
+            "make",
+        ], cwd=builddir)
+
+    xit = p.wait()
+    if xit != 0:
+        lg.debug("Make failed for %s", version)
+        raise ShellError(xit)
+
+    lg.debug("Running 'make install'")
+    p = subprocess.Popen([
+            "make",
+            "install",
+        ], cwd=builddir)
+
+    xit = p.wait()
+    if xit != 0:
+        lg.debug("Install failed for %r", version)
+        raise ShellError(xit)
+
+    lg.info("Installation of %r done", version)
+
+def main():
+    logging.basicConfig(level=logging.DEBUG)
+
+    try:
+        for version in ("2.6", "2.7", "3.1", "3.2"):
+            create_checkout(version)
+
+            for archs in ("32-bit", "intel"):
+                build_framework(version, archs)
+    
+    except ShellError:
+        sys.exit(1)
+
+if __name__ == "__main__":
+    main()

File build-support/run_tests.py

+#!/usr/bin/env python3
+"""
+A script that builds and tests the entire pyobjc tree
+in a number of virtual environments:
+
+- python 2.6
+- python 2.7
+- python 3.1
+- python 3.2
+
+- all in 32-bit, 3-way
+
+- with command-line arguments to select specific 
+  combinations
+
+Assumptions:
+- there are python frameworks for the various architectures: DbgPython-VARIANT.framework
+- those frameworks contain distribute and virtualenv
+
+(TODO: create script that builds a fresh copy of these frameworks from svn checkouts)
+"""
+import getopt, sys, os, shutil, logging
+from topsort import topological_sort
+
+gUsage = """\
+run_tests.py [-a archs] [--archs=archs] [-v versions] [--versions,versions]
+
+archs:    32-bit,3-way   (values separated by commas)
+versions: 2.6,2.7,3.1,3.2 (values seperated by commas)
+"""
+
+gBaseDir = os.path.dirname(os.path.abspath(__file__))
+gRootDir = os.path.dirname(gBaseDir)
+
+
+def main():
+    logging.basicConfig(level=logging.DEBUG)
+
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], 'a:v:', ["--archs=", "--versions="])
+    except getopt.error as msg:
+        print(msg)
+        print(gUsage)
+        sys.exit(1)
+
+    if args:
+        print("Additional arguments")
+        print(gUsage)
+        sys.exit(1)
+
+    versions=["2.6", "2.7", "3.1", "3.2"]
+    archs=["32-bit", "3-way"]
+
+    for k, v in opts:
+        if k in ['-a', '--archs']:
+            archs=v.split(',')
+
+        elif k in ['-v', '--versions']:
+            versions=v.split(',')
+
+        else:
+            raise ValueError(k)
+
+    all_results = []
+    for ver in versions:
+        for arch in archs:
+            test_results = run_tests(ver, arch)
+            all_results.append([ver, arch, test_results])
+
+    report_results(all_results)
+
+def report_results(all_results):
+    for version, archs, test_results in all_results:
+        title = "Architectures {1} for Python {0}".format(version, archs)
+        print(title)
+        print("="*len(title))
+        print()
+        
+        for pkg, stdout, stderr in test_results:
+            summary = stdout.splitlines()[-1]
+            print("{0:>20}: {1}".format(pkg, summary))
+
+        print()
+
+
+def detect_frameworks():
+    """
+    Returns a list of framework wrappers in the order they should
+    be build in.
+    """
+    topdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+    frameworks = []
+    partial_order = []
+
+    for subdir in os.listdir(topdir):
+        if not subdir.startswith('pyobjc-framework-'): continue
+
+        setup = os.path.join(topdir, subdir, 'setup.py')
+
+        requires = None
+        with open(setup) as fp:
+            for ln in fp:
+                if requires is None:
+                    if ln.strip().startswith('install_requires'):
+                        requires = []
+                else:
+                    if ln.strip().startswith(']'):
+                        break
+
+                    dep = ln.strip()[1:-1]
+                    if dep.startswith('pyobjc-framework'):
+                        dep = dep.split('>')[0]
+                        requires.append(dep)
+
+        frameworks.append(subdir)
+        for dep in requires:
+            partial_order.append((dep, subdir))
+
+    frameworks = topological_sort(frameworks, partial_order)
+    return frameworks
+
+
+
+def run_tests(version, archs):
+    test_results = []
+
+    lg = logging.getLogger("run_tests")
+
+    lg.info("Run tests for Python %s with archs %s", version, archs)
+
+    subdir = os.path.join(gBaseDir, "virtualenvs", "{0}.{1}".format(version, arch))
+    if os.path.exists(subdir):
+        lg.debug("Remove existing virtualenv")
+        shutil.rmtree(subdir)
+
+    base_python = "/Library/Frameworks/DbgPython-{0}.framework/Versions/{1}/bin/python".format(
+            archs, version)
+    if not os.path.exists(base_python):
+        lg.warning("No python installation for Python %r %r", version, archs)
+        raise RuntimeError(base_python)
+
+
+    lg.debug("Create virtualenv in %s", subdir)
+    p = subprocess.Popen([
+        base_python,
+        "-mvirtualenv",
+        subdir])
+    xit = p.wait()
+    if p != 0:
+        lg.warning("Cannot create virtualenv in %s", subdir)
+        raise RuntimeError(subdir)
+
+    lg.debug("Install dependencies")
+    pass # Nothing to do here
+
+
+    lg.debug("Install base packages")
+    # There are circular dependencies w.r.t. testing the Cocoa and Quartz wrappers,
+    # install pyobjc-core, pyobjc-framework-Cocoa and pyobjc-framework-Quartz
+    # to ensure we avoid those problems.
+    for pkg in ["pyobjc-core-py3k", "pyobjc-framework-Cocoa", "pyobjc-framework-Quartz"]:
+        pkgroot = os.path.join(gRootDir, pkg)
+        pkgbuild = os.path.join(pkgroot, "build")
+        if os.path.exists(pkgbuild):
+            lg.debug("Remove build directory for %s", pkg)
+            shutil.rmtree(pkgbuild)
+        
+        lg.debug("Install %s into %s", pkg, os.path.basename(subdir))
+        p = subprocess.Popen([
+            os.path.join(subdir, "bin", "python"),
+            "setup.py", "install"],
+            cwd=pkgroot)
+
+        xit = p.wait()
+        if xit != 0:
+            lg.warning("Install %s failed", pkg)
+            raise RuntimeError(pkg)
+
+    lg.debug("Start testing cycle")
+    for pkg in ["pyobjc-core-py3k"] + detect_frameworks():
+        pkgroot = os.path.join(gRootDir, pkg)
+        pkgbuild = os.path.join(pkgroot, "build")
+        if os.path.exists(pkgbuild):
+            lg.debug("Remove build directory for %s", pkg)
+            shutil.rmtree(pkgbuild)
+
+        lg.debug("Build %s for %s", pkg.os.path.basename(subdir))
+        p = subprocess.Popen([
+            os.path.join(subdir, "bin", "python"),
+            "setup.py", "build"],
+            cwd=pkgroot)
+
+        xit = p.wait()
+        if xit != 0:
+            lg.warning("Build %s failed", pkg)
+            raise RuntimeError(pkg)
+
+        lg.debug("Test %s for %s", pkg.os.path.basename(subdir))
+        p = subprocess.Popen([
+            os.path.join(subdir, "bin", "python"),
+            "setup.py", "test"],
+            cwd=pkgroot, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        stdout, stderr = p.communicate()
+
+        print "====STDOUT==="
+        print (stdout)
+        print "====STDERR==="
+        print (stderr)
+        print "====ENDEND==="
+
+        test_results.append((pkg, stdout, stderr))
+
+        xit = p.wait()
+        if xit != 0:
+            lg.warning("Test %s failed", pkg)
+            raise RuntimeError(pkg)
+
+        
+        lg.debug("Install %s into %s", pkg, os.path.basename(subdir))
+        p = subprocess.Popen([
+            os.path.join(subdir, "bin", "python"),
+            "setup.py", "install"],
+            cwd=pkgroot)
+
+        xit = p.wait()
+        if xit != 0:
+            lg.warning("Install %s failed", pkg)
+            raise RuntimeError(pkg)
+
+       
+    return test_results
+
+if __name__ == "__main__":
+    try:
+        main()
+    except RuntimeError:
+        sys.exit(1)

File build-support/topsort.py

+"""
+Module defining a topological sort function, see 
+<http://www.bitformation.com/art/python_toposort.html> for more
+information.
+
+Original topological sort code written by Ofer Faigon (www.bitformation.com) and used with permission
+"""
+
+def topological_sort(items, partial_order):
+    """
+    Perform topological sort.
+    items is a list of items to be sorted.
+    partial_order is a list of pairs. If pair (a,b) is in it, it means
+    that item a should appear before item b.
+    Returns a list of the items in one of the possible orders, or None
+    if partial_order contains a loop.
+    """
+
+    def add_node(graph, node):
+        """Add a node to the graph if not already exists."""
+        if node not in graph:
+            graph[node] = [0] # 0 = number of arcs coming into this node.
+
+    def add_arc(graph, fromnode, tonode):
+        """Add an arc to a graph. Can create multiple arcs.
+           The end nodes must already exist."""
+        graph[fromnode].append(tonode)
+        # Update the count of incoming arcs in tonode.
+        graph[tonode][0] += 1
+
+    # step 1 - create a directed graph with an arc a->b for each input
+    # pair (a,b).
+    # The graph is represented by a dictionary. The dictionary contains
+    # a pair item:list for each node in the graph. /item/ is the value
+    # of the node. /list/'s 1st item is the count of incoming arcs, and
+    # the rest are the destinations of the outgoing arcs. For example:
+    #           {'a':[0,'b','c'], 'b':[1], 'c':[1]}
+    # represents the graph:   c <-- a --> b
+    # The graph may contain loops and multiple arcs.
+    # Note that our representation does not contain reference loops to
+    # cause GC problems even when the represented graph contains loops,
+    # because we keep the node names rather than references to the nodes.
+    graph = {}
+    for v in items:
+        add_node(graph, v)
+    for a,b in partial_order:
+        add_arc(graph, a, b)
+
+    # Step 2 - find all roots (nodes with zero incoming arcs).
+    roots = [node for (node,nodeinfo) in graph.items() if nodeinfo[0] == 0]
+
+    # step 3 - repeatedly emit a root and remove it from the graph. Removing
+    # a node may convert some of the node's direct children into roots.
+    # Whenever that happens, we append the new roots to the list of
+    # current roots.
+    sorted = []
+    while len(roots) != 0:
+        # If len(roots) is always 1 when we get here, it means that
+        # the input describes a complete ordering and there is only
+        # one possible output.
+        # When len(roots) > 1, we can choose any root to send to the
+        # output; this freedom represents the multiple complete orderings
+        # that satisfy the input restrictions. We arbitrarily take one of
+        # the roots using pop(). Note that for the algorithm to be efficient,
+        # this operation must be done in O(1) time.
+        root = roots.pop()
+        sorted.append(root)
+        for child in graph[root][1:]:
+            graph[child][0] = graph[child][0] - 1
+            if graph[child][0] == 0:
+                roots.append(child)
+        del graph[root]
+    if len(graph.items()) != 0:
+        # There is a loop in the input.
+        return None
+    return sorted