mdwsetup.py: Add a command for running tests.
[runlisp] / mdwsetup.py
index a6ca3f4..55120fb 100644 (file)
@@ -34,6 +34,13 @@ import distutils.core as DC
 import distutils.log as DL
 
 ###--------------------------------------------------------------------------
+### Compatibility hacks.
+
+def with_metaclass(meta, *supers):
+  return meta("#<anonymous base %s>" % meta.__name__,
+              supers or (object,), dict())
+
+###--------------------------------------------------------------------------
 ### Random utilities.
 
 def uniquify(seq):
@@ -59,10 +66,10 @@ class SubprocessFailure (Exception):
     me.file = file
     me.rc = rc
   def __str__(me):
-    if WIFEXITED(me.rc):
-      return '%s failed (rc = %d)' % (me.file, WEXITSTATUS(me.rc))
-    elif WIFSIGNALED(me.rc):
-      return '%s died (signal %d)' % (me.file, WTERMSIG(me.rc))
+    if OS.WIFEXITED(me.rc):
+      return '%s failed (rc = %d)' % (me.file, OS.WEXITSTATUS(me.rc))
+    elif OS.WIFSIGNALED(me.rc):
+      return '%s died (signal %d)' % (me.file, OS.WTERMSIG(me.rc))
     else:
       return '%s died inexplicably' % (me.file)
 
@@ -73,15 +80,16 @@ def progoutput(command):
   The COMMAND must produce exactly one line of output, and must exit with
   status zero.
   """
-  kid = SUB.Popen(command, stdout = SUB.PIPE)
-  out = kid.stdout.readline()
-  junk = kid.stdout.read()
-  if junk != '':
-    raise ValueError, \
-          "Child process `%s' produced unspected output %r" % (command, junk)
+  kid = SUB.Popen(command, stdout = SUB.PIPE, universal_newlines = True)
+  try:
+    out = kid.stdout.readline()
+    junk = kid.stdout.read(1)
+  finally:
+    kid.stdout.close()
+  if junk != '': raise ValueError \
+    ("Child process `%s' produced unspected output %r" % (command, junk))
   rc = kid.wait()
-  if rc != 0:
-    raise SubprocessFailure, (command, rc)
+  if rc != 0: raise SubprocessFailure(command, rc)
   return out.rstrip('\n')
 
 ###--------------------------------------------------------------------------
@@ -99,22 +107,24 @@ def pkg_config(pkg, version):
   library-directory names are in LIBDIRS; and the library names themselves
   are in LIBS.
   """
-  spec = '%s >= %s' % (pkg, version)
+
   def weird(what, word):
-    raise ValueError, \
-          "Unexpected `%s' item `%s' from package `%s'" % (what, word, pkg)
-  for word in progoutput(['pkg-config', '--cflags', spec]).split():
-    if word.startswith('-I'):
-      INCLUDEDIRS.append(word[2:])
-    else:
-      weird('--cflags', word)
-  for word in progoutput(['pkg-config', '--libs', spec]).split():
-    if word.startswith('-L'):
-      LIBDIRS.append(word[2:])
-    elif word.startswith('-l'):
-      LIBS.append(word[2:])
-    else:
-      weird('--libs', word)
+    raise ValueError \
+      ("Unexpected `%s' item `%s' from package `%s'" % (what, word, pkg))
+
+  spec = '%s >= %s' % (pkg, version)
+
+  try: cflags = OS.environ["%s_CFLAGS" % pkg]
+  except KeyError: cflags = progoutput(['pkg-config', '--cflags', spec])
+  for word in cflags.split():
+    if word.startswith('-I'): INCLUDEDIRS.append(word[2:])
+    else: weird('CFLAGS', word)
+  try: libs = OS.environ["%s_LIBS" % pkg]
+  except KeyError: libs = progoutput(['pkg-config', '--libs', spec])
+  for word in libs.split():
+    if word.startswith('-L'): LIBDIRS.append(word[2:])
+    elif word.startswith('-l'): LIBS.append(word[2:])
+    else: weird('LIBS', word)
 
 ###--------------------------------------------------------------------------
 ### Substituting variables in files.
@@ -181,7 +191,7 @@ class Generate (BaseGenFile):
     temp = me.target + '.new'
     with open(temp, 'w') as ft:
       rc = SUB.call([SYS.executable, me.sources[0]], stdout = ft)
-    if rc != 0: raise SubprocessFailure, (source, rc)
+    if rc != 0: raise SubprocessFailure(me.sources[0], rc << 8)
     OS.rename(temp, me.target)
 
 ## Backward compatibility.
@@ -224,7 +234,7 @@ class CommandClass (type):
     else: CMDS[name] = c
     return c
 
-class Command (DC.Command, object):
+class Command (with_metaclass(CommandClass, DC.Command, object)):
   """
   Base class for `mdwsetup' command classes.
 
@@ -247,9 +257,9 @@ class distdir (Command):
   description = "print the distribution directory name to stdout"
   def run(me):
     d = me.distribution
-    print '%s-%s' % (d.get_name(), d.get_version())
+    print('%s-%s' % (d.get_name(), d.get_version()))
 
-class build_gen(Command):
+class build_gen (Command):
   """
   Generate files, according to the `genfiles'.
 
@@ -278,7 +288,47 @@ class build (_build, Command):
   sub_commands = [('build_gen', lambda me: me.distribution.genfiles)]
   sub_commands += _build.sub_commands
 
-class clean_gen(Command):
+class test (Command):
+  """
+  Run unit tests, according to the `unittests'.
+
+  The `unittests' keyword argument to `setup' lists module names (or other
+  things acceptable to the `loadTestsFromNames' test-loader method) to be
+  run.  The build library directory is prepended to the load path before
+  running the tests to ensure that the newly built modules are tested.  If
+  `unittest_dir' is set, then this is appended to the load path so that test
+  modules can be found there.
+  """
+  NAME = "test"
+  description = "run the included test suite"
+
+  user_options = \
+    [('build-lib=', 'b', "directory containing compiled moules"),
+     ('tests=', 't', "tests to run"),
+     ('verbose-test', 'V', "run tests verbosely")]
+
+  def initialize_options(me):
+    me.build_lib = None
+    me.verbose_test = False
+    me.tests = None
+  def finalize_options(me):
+    me.set_undefined_options('build', ('build_lib', 'build_lib'))
+  def run(me):
+    import unittest as U
+    d = me.distribution
+    SYS.path = [me.build_lib] + SYS.path
+    if d.unittest_dir is not None: SYS.path.append(d.unittest_dir)
+    if me.tests is not None: tests = me.tests.split(",")
+    else: tests = d.unittests
+    suite = U.defaultTestLoader.loadTestsFromNames(tests)
+    runner = U.TextTestRunner(verbosity = me.verbose_test and 2 or 1)
+    if me.dry_run: return
+    result = runner.run(suite)
+    if result.errors or result.failures or \
+       getattr(result, "unexpectedSuccesses", 0):
+      SYS.exit(2)
+
+class clean_gen (Command):
   """
   Remove the generated files, as listed in `genfiles'.
 
@@ -290,7 +340,7 @@ class clean_gen(Command):
     d = me.distribution
     for g in d.genfiles: g.clean(dry_run_p = me.dry_run)
 
-class clean_others(Command):
+class clean_others (Command):
   """
   Remove the files listed in the `cleanfiles' argument to `setup'.
   """
@@ -341,6 +391,8 @@ class Dist (DC.Distribution):
   ## our enhanced commands.
   def __init__(me, attrs = None):
     me.genfiles = []
+    me.unittest_dir = None
+    me.unittests = []
     me.cleanfiles = []
     me._auto_version_p = False
     DC.Distribution.__init__(me, attrs)