Index: tests/pybin/tools.py
===================================================================
--- tests/pybin/tools.py	(revision 05c34c383fed3f7742634eb2d19a0c5c482a4c8d)
+++ tests/pybin/tools.py	(revision 35a408b7afe5994cf2251ff7078ad4636f315997)
@@ -179,4 +179,15 @@
 			os.chdir(cwd)
 
+def killgroup():
+	try:
+		os.killpg(os.getpgrp(), signal.SIGINT)
+	except KeyboardInterrupt:
+		pass # expected
+	except Exception as exc:
+		print("Unexpected exception", file=sys.stderr)
+		print(exc, file=sys.stderr)
+		sys.stderr.flush()
+		sys.exit(2)
+
 ################################################################################
 #               file handling
@@ -301,2 +312,8 @@
         self.end = time.time()
         self.duration = self.end - self.start
+
+def timed(src, timeout):
+	expire = time.time() + timeout
+	i = iter(src)
+	while True:
+		yield i.next(max(expire - time.time(), 0))
Index: tests/test.py
===================================================================
--- tests/test.py	(revision 05c34c383fed3f7742634eb2d19a0c5c482a4c8d)
+++ tests/test.py	(revision 35a408b7afe5994cf2251ff7078ad4636f315997)
@@ -205,5 +205,10 @@
 		return retcode == TestResult.SUCCESS, text
 	except KeyboardInterrupt:
-		False, ""
+		return False, ""
+	except:
+		print("Unexpected error in worker thread", file=sys.stderr)
+		sys.stderr.flush()
+		return False, ""
+
 
 # run the given list of tests with the given parameters
@@ -221,9 +226,11 @@
 		num = len(tests)
 		fancy = sys.stdout.isatty()
-		for i, (succ, txt) in enumerate(pool.imap(
+		results = pool.imap_unordered(
 			run_test_worker,
 			tests,
 			chunksize = 1
-		)) :
+		)
+
+		for i, (succ, txt) in enumerate(timed(results, timeout = settings.timeout.total), 1) :
 			if not succ :
 				failed = True
@@ -236,11 +243,15 @@
 
 	except KeyboardInterrupt:
+		print("Tests interrupted by user", file=sys.stderr)
 		pool.terminate()
-		print("Tests interrupted by user", file=sys.stderr)
+		pool.join()
 		failed = True
 	except multiprocessing.TimeoutError:
+		print("ERROR: Test suite timed out", file=sys.stderr)
 		pool.terminate()
-		print("ERROR: Test suite timed out", file=sys.stderr)
+		pool.join()
 		failed = True
+		killgroup() # needed to cleanly kill all children
+
 
 	# clean the workspace
