THRIFT-3516 Add feature test for THeader TBinaryProtocol

This closes #767
diff --git a/test/crossrunner/__init__.py b/test/crossrunner/__init__.py
index 584cc07..49025ed 100644
--- a/test/crossrunner/__init__.py
+++ b/test/crossrunner/__init__.py
@@ -18,7 +18,6 @@
 #
 
 from .test import test_name
-from .collect import collect_tests
+from .collect import collect_cross_tests, collect_feature_tests
 from .run import TestDispatcher
 from .report import generate_known_failures, load_known_failures
-from .prepare import prepare
diff --git a/test/crossrunner/collect.py b/test/crossrunner/collect.py
index c6e33e9..455189c 100644
--- a/test/crossrunner/collect.py
+++ b/test/crossrunner/collect.py
@@ -18,6 +18,7 @@
 #
 
 import platform
+import re
 from itertools import product
 
 from .util import merge_dict
@@ -51,7 +52,7 @@
 DEFAULT_TIMEOUT = 5
 
 
-def collect_testlibs(config, server_match, client_match):
+def _collect_testlibs(config, server_match, client_match=[None]):
   """Collects server/client configurations from library configurations"""
   def expand_libs(config):
     for lib in config:
@@ -73,7 +74,12 @@
   return servers, clients
 
 
-def do_collect_tests(servers, clients):
+def collect_features(config, match):
+  res = list(map(re.compile, match))
+  return list(filter(lambda c: any(map(lambda r: r.search(c['name']), res)), config))
+
+
+def _do_collect_tests(servers, clients):
   def intersection(key, o1, o2):
     """intersection of two collections.
     collections are replaced with sets the first time"""
@@ -137,6 +143,12 @@
           }
 
 
-def collect_tests(tests_dict, server_match, client_match):
-  sv, cl = collect_testlibs(tests_dict, server_match, client_match)
-  return list(do_collect_tests(sv, cl))
+def collect_cross_tests(tests_dict, server_match, client_match):
+  sv, cl = _collect_testlibs(tests_dict, server_match, client_match)
+  return list(_do_collect_tests(sv, cl))
+
+
+def collect_feature_tests(tests_dict, features_dict, server_match, feature_match):
+  sv, _ = _collect_testlibs(tests_dict, server_match)
+  ft = collect_features(features_dict, feature_match)
+  return list(_do_collect_tests(sv, ft))
diff --git a/test/crossrunner/prepare.py b/test/crossrunner/prepare.py
deleted file mode 100644
index c6784af..0000000
--- a/test/crossrunner/prepare.py
+++ /dev/null
@@ -1,55 +0,0 @@
-#
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-#
-
-import os
-import subprocess
-
-from .collect import collect_testlibs
-
-
-def prepare(config_dict, testdir, server_match, client_match):
-  libs, libs2 = collect_testlibs(config_dict, server_match, client_match)
-  libs.extend(libs2)
-
-  def prepares():
-    for lib in libs:
-      pre = lib.get('prepare')
-      if pre:
-        yield pre, lib['workdir']
-
-  def files():
-    for lib in libs:
-      workdir = os.path.join(testdir, lib['workdir'])
-      for c in lib['command']:
-        if not c.startswith('-'):
-          p = os.path.join(workdir, c)
-          if not os.path.exists(p):
-            yield os.path.split(p)
-
-  def make(p):
-    d, f = p
-    with open(os.devnull, 'w') as devnull:
-      return subprocess.Popen(['make', f], cwd=d, stderr=devnull)
-
-  for pre, d in prepares():
-    subprocess.Popen(pre, cwd=d).wait()
-
-  for p in list(map(make, set(files()))):
-    p.wait()
-  return True
diff --git a/test/crossrunner/report.py b/test/crossrunner/report.py
index 6d843d9..7840724 100644
--- a/test/crossrunner/report.py
+++ b/test/crossrunner/report.py
@@ -45,7 +45,7 @@
       if not r[success_index]:
         yield TestEntry.get_name(*r)
   try:
-    with logfile_open(os.path.join(testdir, RESULT_JSON), 'r') as fp:
+    with logfile_open(path_join(testdir, RESULT_JSON), 'r') as fp:
       results = json.load(fp)
   except IOError:
     sys.stderr.write('Unable to load last result. Did you run tests ?\n')
@@ -68,7 +68,7 @@
 
 def load_known_failures(testdir):
   try:
-    with logfile_open(os.path.join(testdir, FAIL_JSON % platform.system()), 'r') as fp:
+    with logfile_open(path_join(testdir, FAIL_JSON % platform.system()), 'r') as fp:
       return json.load(fp)
   except IOError:
     return []
@@ -85,7 +85,7 @@
 
   @classmethod
   def test_logfile(cls, test_name, prog_kind, dir=None):
-    relpath = os.path.join('log', '%s_%s.log' % (test_name, prog_kind))
+    relpath = path_join('log', '%s_%s.log' % (test_name, prog_kind))
     return relpath if not dir else os.path.realpath(path_join(dir, relpath))
 
   def _start(self):
@@ -207,8 +207,8 @@
   def __init__(self, testdir, concurrent=True):
     super(SummaryReporter, self).__init__()
     self.testdir = testdir
-    self.logdir = os.path.join(testdir, LOG_DIR)
-    self.out_path = os.path.join(testdir, RESULT_JSON)
+    self.logdir = path_join(testdir, LOG_DIR)
+    self.out_path = path_join(testdir, RESULT_JSON)
     self.concurrent = concurrent
     self.out = sys.stdout
     self._platform = platform.system()
diff --git a/test/crossrunner/run.py b/test/crossrunner/run.py
index acba335..abbd70b 100644
--- a/test/crossrunner/run.py
+++ b/test/crossrunner/run.py
@@ -110,13 +110,13 @@
     return self.proc.returncode if self.proc else None
 
 
-def exec_context(port, testdir, test, prog):
-  report = ExecReporter(testdir, test, prog)
+def exec_context(port, logdir, test, prog):
+  report = ExecReporter(logdir, test, prog)
   prog.build_command(port)
   return ExecutionContext(prog.command, prog.workdir, prog.env, report)
 
 
-def run_test(testdir, test_dict, async=True, max_retry=3):
+def run_test(testdir, logdir, test_dict, async=True, max_retry=3):
   try:
     logger = multiprocessing.get_logger()
     retry_count = 0
@@ -128,8 +128,8 @@
       logger.debug('Start')
       with PortAllocator.alloc_port_scoped(ports, test.socket) as port:
         logger.debug('Start with port %d' % port)
-        sv = exec_context(port, testdir, test, test.server)
-        cl = exec_context(port, testdir, test, test.client)
+        sv = exec_context(port, logdir, test, test.server)
+        cl = exec_context(port, logdir, test, test.client)
 
         logger.debug('Starting server')
         with sv.start():
@@ -256,9 +256,10 @@
 
 
 class TestDispatcher(object):
-  def __init__(self, testdir, concurrency):
+  def __init__(self, testdir, logdir, concurrency):
     self._log = multiprocessing.get_logger()
     self.testdir = testdir
+    self.logdir = logdir
     # seems needed for python 2.x to handle keyboard interrupt
     self._stop = multiprocessing.Event()
     self._async = concurrency > 1
@@ -273,7 +274,7 @@
       self._m.register('ports', PortAllocator)
       self._m.start()
       self._pool = multiprocessing.Pool(concurrency, self._pool_init, (self._m.address,))
-    self._report = SummaryReporter(testdir, concurrency > 1)
+    self._report = SummaryReporter(logdir, concurrency > 1)
     self._log.debug(
         'TestDispatcher started with %d concurrent jobs' % concurrency)
 
@@ -287,12 +288,13 @@
     ports = m.ports()
 
   def _dispatch_sync(self, test, cont):
-    r = run_test(self.testdir, test, False)
+    r = run_test(self.testdir, self.logdir, test, False)
     cont(r)
     return NonAsyncResult(r)
 
   def _dispatch_async(self, test, cont):
-    return self._pool.apply_async(func=run_test, args=(self.testdir, test,), callback=cont)
+    self._log.debug('_dispatch_async')
+    return self._pool.apply_async(func=run_test, args=(self.testdir, self.logdir, test,), callback=cont)
 
   def dispatch(self, test):
     index = self._report.add_test(test)