mdwsetup.py: Add a command for running tests.
[runlisp] / mdwsetup.py
index dfa217e..55120fb 100644 (file)
@@ -34,6 +34,13 @@ import distutils.core as DC
 import distutils.log as DL
 
 ###--------------------------------------------------------------------------
 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):
 ### Random utilities.
 
 def uniquify(seq):
@@ -73,15 +80,16 @@ def progoutput(command):
   The COMMAND must produce exactly one line of output, and must exit with
   status zero.
   """
   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()
   rc = kid.wait()
-  if rc != 0:
-    raise SubprocessFailure, (command, rc)
+  if rc != 0: raise SubprocessFailure(command, rc)
   return out.rstrip('\n')
 
 ###--------------------------------------------------------------------------
   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.
   """
   library-directory names are in LIBDIRS; and the library names themselves
   are in LIBS.
   """
-  spec = '%s >= %s' % (pkg, version)
+
   def weird(what, word):
   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.
 
 ###--------------------------------------------------------------------------
 ### 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)
     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(me.sources[0], rc << 8)
+    if rc != 0: raise SubprocessFailure(me.sources[0], rc << 8)
     OS.rename(temp, me.target)
 
 ## Backward compatibility.
     OS.rename(temp, me.target)
 
 ## Backward compatibility.
@@ -224,7 +234,7 @@ class CommandClass (type):
     else: CMDS[name] = c
     return c
 
     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.
 
   """
   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
   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'.
 
   """
   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
 
   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'.
 
   """
   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)
 
     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'.
   """
   """
   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 = []
   ## 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)
     me.cleanfiles = []
     me._auto_version_p = False
     DC.Distribution.__init__(me, attrs)