[tor-commits] [stem/master] Nice pattern for running async tests

atagar at torproject.org atagar at torproject.org
Thu Jun 8 17:17:55 UTC 2017


commit 902e10498b4b6aff358f810efcee7678561fdc04
Author: Damian Johnson <atagar at torproject.org>
Date:   Mon Jun 5 10:49:50 2017 -0700

    Nice pattern for running async tests
    
    Fiddled with a few options and finally came upon a pattern that's really
    elegant. This allows asynchronous tests to be very similar to test methods.
---
 run_tests.py               |   4 +-
 stem/util/test_tools.py    |  25 ++++++-
 test/integ/installation.py | 159 +++++++++++++++++++++------------------------
 test/integ/process.py      | 108 ++++++++++++++----------------
 4 files changed, 146 insertions(+), 150 deletions(-)

diff --git a/run_tests.py b/run_tests.py
index 2798fd4..961600c 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -235,10 +235,10 @@ def main():
 
   if args.run_integ:
     if not args.specific_test or 'test.integ.installation'.startswith(args.specific_test):
-      test.integ.installation.setup()
+      test.integ.installation.TestInstallation.run_tests()
 
     if not args.specific_test or 'test.integ.process'.startswith(args.specific_test):
-      test.integ.process.setup(args.tor_path)
+      test.integ.process.TestProcess.run_tests(args.tor_path)
 
   if args.run_unit:
     test.output.print_divider('UNIT TESTS', True)
diff --git a/stem/util/test_tools.py b/stem/util/test_tools.py
index 47fc02a..b7208e3 100644
--- a/stem/util/test_tools.py
+++ b/stem/util/test_tools.py
@@ -55,9 +55,26 @@ class SkipTest(Exception):
   'Notes that the test was skipped.'
 
 
-class AsyncTestResult(object):
+class AsyncTest(object):
   """
-  Test results that can be applied later.
+  Test that's run asychronously. These are functions (no self reference)
+  performed like the following...
+
+  ::
+
+    class MyTest(unittest.TestCase):
+      @staticmethod
+      def run_tests():
+        MyTest.test_addition = stem.util.test_tools.AsyncTest(MyTest.test_addition).method
+
+      @staticmethod
+      def test_addition():
+        if 1 + 1 != 2:
+          raise AssertionError('tisk, tisk')
+
+    MyTest.run()
+
+  .. versionadded:: 1.6.0
   """
 
   def __init__(self, test_runner, *test_runner_args):
@@ -72,6 +89,10 @@ class AsyncTestResult(object):
       finally:
         conn.close()
 
+    # method that can be mixed into TestCases
+
+    self.method = lambda test: self.result(test)
+
     self._result_type, self._result_msg = None, None
     self._result_lock = threading.RLock()
     self._results_pipe, child_pipe = multiprocessing.Pipe()
diff --git a/test/integ/installation.py b/test/integ/installation.py
index 2b58e1d..7cf5346 100644
--- a/test/integ/installation.py
+++ b/test/integ/installation.py
@@ -15,91 +15,10 @@ import stem.util.system
 import stem.util.test_tools
 import test
 
-INSTALL_MISMATCH_MSG = "Running 'python setup.py sdist' doesn't match our git contents in the following way. The manifest in our setup.py may need to be updated...\n\n"
-
 BASE_INSTALL_PATH = '/tmp/stem_test'
 DIST_PATH = os.path.join(test.STEM_BASE, 'dist')
 PYTHON_EXE = sys.executable if sys.executable else 'python'
-TEST_INSTALL, TEST_SDIST = None, None
-
-
-def setup():
-  """
-  Performs our tests asynchronously. They take a while due to iops.
-  """
-
-  global TEST_INSTALL, TEST_SDIST
-
-  TEST_INSTALL = stem.util.test_tools.AsyncTestResult(_test_install)
-  TEST_SDIST = stem.util.test_tools.AsyncTestResult(_test_sdist, TEST_INSTALL.pid())
-
-
-def _test_install():
-  try:
-    try:
-      stem.util.system.call('%s setup.py install --prefix %s' % (PYTHON_EXE, BASE_INSTALL_PATH), timeout = 60, cwd = test.STEM_BASE)
-      stem.util.system.call('%s setup.py clean --all' % PYTHON_EXE, timeout = 60, cwd = test.STEM_BASE)  # tidy up the build directory
-      site_packages_paths = glob.glob('%s/lib*/*/site-packages' % BASE_INSTALL_PATH)
-    except Exception as exc:
-      raise AssertionError("Unable to install with 'python setup.py install': %s" % exc)
-
-    if len(site_packages_paths) != 1:
-      raise AssertionError('We should only have a single site-packages directory, but instead had: %s' % site_packages_paths)
-
-    install_path = site_packages_paths[0]
-    version_output = stem.util.system.call([PYTHON_EXE, '-c', "import sys;sys.path.insert(0, '%s');import stem;print(stem.__version__)" % install_path])[0]
-
-    if stem.__version__ != version_output:
-      raise AssertionError('We expected the installed version to be %s but was %s' % (stem.__version__, version_output))
-
-    _assert_has_all_files(install_path)
-  finally:
-    if os.path.exists(BASE_INSTALL_PATH):
-      shutil.rmtree(BASE_INSTALL_PATH)
-
-
-def _test_sdist(dependency_pid):
-  while stem.util.system.is_running(dependency_pid):
-    time.sleep(0.1)  # we need to run these tests serially
-
-  git_dir = os.path.join(test.STEM_BASE, '.git')
-
-  if not stem.util.system.is_available('git'):
-    raise stem.util.test_tools.SkipTest('(git unavailable)')
-  elif not os.path.exists(git_dir):
-    raise stem.util.test_tools.SkipTest('(not a git checkout)')
-
-  if os.path.exists(DIST_PATH):
-    raise AssertionError("%s already exists, maybe you manually ran 'python setup.py sdist'?" % DIST_PATH)
-
-  try:
-    try:
-      stem.util.system.call('%s setup.py sdist' % PYTHON_EXE, timeout = 60, cwd = test.STEM_BASE)
-    except Exception as exc:
-      raise AssertionError("Unable to run 'python setup.py sdist': %s" % exc)
-
-    git_contents = [line.split()[-1] for line in stem.util.system.call('git --git-dir=%s ls-tree --full-tree -r HEAD' % git_dir)]
-
-    # tarball has a prefix 'stem-[verion]' directory so stipping that out
-
-    dist_tar = tarfile.open(os.path.join(DIST_PATH, 'stem-dry-run-%s.tar.gz' % stem.__version__))
-    tar_contents = ['/'.join(info.name.split('/')[1:]) for info in dist_tar.getmembers() if info.isfile()]
-
-    issues = []
-
-    for path in git_contents:
-      if path not in tar_contents and path not in ['.gitignore']:
-        issues.append('  * %s is missing from our release tarball' % path)
-
-    for path in tar_contents:
-      if path not in git_contents and path not in ['MANIFEST.in', 'PKG-INFO']:
-        issues.append("  * %s isn't expected in our release tarball" % path)
-
-    if issues:
-      raise AssertionError(INSTALL_MISMATCH_MSG + '\n'.join(issues))
-  finally:
-    if os.path.exists(DIST_PATH):
-      shutil.rmtree(DIST_PATH)
+INSTALL_MISMATCH_MSG = "Running 'python setup.py sdist' doesn't match our git contents in the following way. The manifest in our setup.py may need to be updated...\n\n"
 
 
 def _assert_has_all_files(path):
@@ -135,19 +54,87 @@ def _assert_has_all_files(path):
 
 
 class TestInstallation(unittest.TestCase):
-  def test_install(self):
+  @staticmethod
+  def run_tests():
+    test_install = stem.util.test_tools.AsyncTest(TestInstallation.test_install)
+    TestInstallation.test_install = test_install.method
+    TestInstallation.test_sdist = stem.util.test_tools.AsyncTest(TestInstallation.test_sdist, test_install.pid()).method
+
+  @staticmethod
+  def test_install():
     """
     Installs with 'python setup.py install' and checks we can use what we
     install.
     """
 
-    TEST_INSTALL.result(self)
+    try:
+      try:
+        stem.util.system.call('%s setup.py install --prefix %s' % (PYTHON_EXE, BASE_INSTALL_PATH), timeout = 60, cwd = test.STEM_BASE)
+        stem.util.system.call('%s setup.py clean --all' % PYTHON_EXE, timeout = 60, cwd = test.STEM_BASE)  # tidy up the build directory
+        site_packages_paths = glob.glob('%s/lib*/*/site-packages' % BASE_INSTALL_PATH)
+      except Exception as exc:
+        raise AssertionError("Unable to install with 'python setup.py install': %s" % exc)
+
+      if len(site_packages_paths) != 1:
+        raise AssertionError('We should only have a single site-packages directory, but instead had: %s' % site_packages_paths)
+
+      install_path = site_packages_paths[0]
+      version_output = stem.util.system.call([PYTHON_EXE, '-c', "import sys;sys.path.insert(0, '%s');import stem;print(stem.__version__)" % install_path])[0]
 
-  def test_sdist(self):
+      if stem.__version__ != version_output:
+        raise AssertionError('We expected the installed version to be %s but was %s' % (stem.__version__, version_output))
+
+      _assert_has_all_files(install_path)
+    finally:
+      if os.path.exists(BASE_INSTALL_PATH):
+        shutil.rmtree(BASE_INSTALL_PATH)
+
+  @staticmethod
+  def test_sdist(dependency_pid):
     """
     Creates a source distribution tarball with 'python setup.py sdist' and
     checks that it matches the content of our git repository. This primarily is
     meant to test that our MANIFEST.in is up to date.
     """
 
-    TEST_SDIST.result(self)
+    while stem.util.system.is_running(dependency_pid):
+      time.sleep(0.1)  # we need to run these tests serially
+
+    git_dir = os.path.join(test.STEM_BASE, '.git')
+
+    if not stem.util.system.is_available('git'):
+      raise stem.util.test_tools.SkipTest('(git unavailable)')
+    elif not os.path.exists(git_dir):
+      raise stem.util.test_tools.SkipTest('(not a git checkout)')
+
+    if os.path.exists(DIST_PATH):
+      raise AssertionError("%s already exists, maybe you manually ran 'python setup.py sdist'?" % DIST_PATH)
+
+    try:
+      try:
+        stem.util.system.call('%s setup.py sdist' % PYTHON_EXE, timeout = 60, cwd = test.STEM_BASE)
+      except Exception as exc:
+        raise AssertionError("Unable to run 'python setup.py sdist': %s" % exc)
+
+      git_contents = [line.split()[-1] for line in stem.util.system.call('git --git-dir=%s ls-tree --full-tree -r HEAD' % git_dir)]
+
+      # tarball has a prefix 'stem-[verion]' directory so stipping that out
+
+      dist_tar = tarfile.open(os.path.join(DIST_PATH, 'stem-dry-run-%s.tar.gz' % stem.__version__))
+      tar_contents = ['/'.join(info.name.split('/')[1:]) for info in dist_tar.getmembers() if info.isfile()]
+
+      issues = []
+
+      for path in git_contents:
+        if path not in tar_contents and path not in ['.gitignore']:
+          issues.append('  * %s is missing from our release tarball' % path)
+
+      for path in tar_contents:
+        if path not in git_contents and path not in ['MANIFEST.in', 'PKG-INFO']:
+          issues.append("  * %s isn't expected in our release tarball" % path)
+
+      if issues:
+        raise AssertionError(INSTALL_MISMATCH_MSG + '\n'.join(issues))
+    finally:
+      if os.path.exists(DIST_PATH):
+        shutil.rmtree(DIST_PATH)
diff --git a/test/integ/process.py b/test/integ/process.py
index 5677be1..3d90c1b 100644
--- a/test/integ/process.py
+++ b/test/integ/process.py
@@ -39,66 +39,12 @@ PublishServerDescriptor 0
 DataDirectory %s
 """
 
-TEST_TAKE_OWNERSHIP_BY_PID = None
-
-
-def setup(tor_cmd):
-  global TEST_TAKE_OWNERSHIP_BY_PID
-
-  TEST_TAKE_OWNERSHIP_BY_PID = stem.util.test_tools.AsyncTestResult(_test_take_ownership_via_pid, tor_cmd)
-
-
-def _test_take_ownership_via_pid(tor_cmd):
-  """
-  Checks that the tor process quits after we do if we set take_ownership. To
-  test this we spawn a process and trick tor into thinking that it is us.
-  """
-
-  if not stem.util.system.is_available('sleep'):
-    raise stem.util.test_tools.SkipTest('(sleep unavailable)')
-  elif test.tor_version() < stem.version.Requirement.TAKEOWNERSHIP:
-    raise stem.util.test_tools.SkipTest('(requires )' % stem.version.Requirement.TAKEOWNERSHIP)
-
-  data_directory = tempfile.mkdtemp()
-
-  try:
-    sleep_process = subprocess.Popen(['sleep', '60'])
-
-    tor_process = stem.process.launch_tor_with_config(
-      tor_cmd = tor_cmd,
-      config = {
-        'SocksPort': '2779',
-        'ControlPort': '2780',
-        'DataDirectory': data_directory,
-        '__OwningControllerProcess': str(sleep_process.pid),
-      },
-      completion_percent = 5,
-    )
-
-    # Kill the sleep command. Tor should quit shortly after.
-
-    sleep_process.kill()
-    sleep_process.communicate()
-
-    # tor polls for the process every fifteen seconds so this may take a
-    # while...
-    #
-    #   https://trac.torproject.org/projects/tor/ticket/21281
-
-    start_time = time.time()
-
-    while time.time() - start_time < 30:
-      if tor_process.poll() == 0:
-        return  # tor exited
-
-      time.sleep(0.01)
-
-    raise AssertionError("tor didn't quit after the process that owned it terminated")
-  finally:
-    shutil.rmtree(data_directory)
-
 
 class TestProcess(unittest.TestCase):
+  @staticmethod
+  def run_tests(tor_cmd):
+    TestProcess.test_take_ownership_via_pid = stem.util.test_tools.AsyncTest(TestProcess.test_take_ownership_via_pid, tor_cmd).method
+
   def setUp(self):
     self.data_directory = tempfile.mkdtemp()
 
@@ -484,13 +430,55 @@ class TestProcess(unittest.TestCase):
     if not (runtime > 0.05 and runtime < 1):
       self.fail('Test should have taken 0.05-1 seconds, took %0.1f instead' % runtime)
 
-  def test_take_ownership_via_pid(self):
+  @staticmethod
+  def test_take_ownership_via_pid(tor_cmd):
     """
     Checks that the tor process quits after we do if we set take_ownership. To
     test this we spawn a process and trick tor into thinking that it is us.
     """
 
-    TEST_TAKE_OWNERSHIP_BY_PID.result(self)
+    if not stem.util.system.is_available('sleep'):
+      raise stem.util.test_tools.SkipTest('(sleep unavailable)')
+    elif test.tor_version() < stem.version.Requirement.TAKEOWNERSHIP:
+      raise stem.util.test_tools.SkipTest('(requires )' % stem.version.Requirement.TAKEOWNERSHIP)
+
+    data_directory = tempfile.mkdtemp()
+
+    try:
+      sleep_process = subprocess.Popen(['sleep', '60'])
+
+      tor_process = stem.process.launch_tor_with_config(
+        tor_cmd = tor_cmd,
+        config = {
+          'SocksPort': '2779',
+          'ControlPort': '2780',
+          'DataDirectory': data_directory,
+          '__OwningControllerProcess': str(sleep_process.pid),
+        },
+        completion_percent = 5,
+      )
+
+      # Kill the sleep command. Tor should quit shortly after.
+
+      sleep_process.kill()
+      sleep_process.communicate()
+
+      # tor polls for the process every fifteen seconds so this may take a
+      # while...
+      #
+      #   https://trac.torproject.org/projects/tor/ticket/21281
+
+      start_time = time.time()
+
+      while time.time() - start_time < 30:
+        if tor_process.poll() == 0:
+          return  # tor exited
+
+        time.sleep(0.01)
+
+      raise AssertionError("tor didn't quit after the process that owned it terminated")
+    finally:
+      shutil.rmtree(data_directory)
 
   @test.require.only_run_once
   @test.require.version(stem.version.Requirement.TAKEOWNERSHIP)





More information about the tor-commits mailing list