From 0abe88f99bcc4e15a002c0781c423ee61838b555 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Sun, 22 Mar 2020 19:43:48 +0100 Subject: [PATCH 1/3] Use asyncio in run_tests.py Only tested for Linux and tested locally. This branch should evolve more. --- sbin/run_tests.py | 156 ++++++++++++++++----------------------- unittesting/__init__.py | 4 +- unittesting/mixin.py | 16 ++-- unittesting/scheduler.py | 71 ++++++++---------- ut.py | 2 + 5 files changed, 106 insertions(+), 143 deletions(-) diff --git a/sbin/run_tests.py b/sbin/run_tests.py index df07a525..e49b8161 100644 --- a/sbin/run_tests.py +++ b/sbin/run_tests.py @@ -6,7 +6,6 @@ 2. python path/to/run_tests.py PACKAGE """ -from __future__ import print_function import json import optparse import os @@ -14,7 +13,10 @@ import shutil import subprocess import sys +import asyncio import time +from typing import Any, Dict, Optional + # todo: allow different sublime versions @@ -23,8 +25,6 @@ SCHEDULE_FILE_PATH = os.path.realpath(os.path.join(UT_OUTPUT_DIR_PATH, 'schedule.json')) UT_DIR_PATH = os.path.realpath(os.path.join(PACKAGES_DIR_PATH, 'UnitTesting')) UT_SBIN_PATH = os.path.realpath(os.path.join(PACKAGES_DIR_PATH, 'UnitTesting', 'sbin')) -SCHEDULE_RUNNER_SOURCE = os.path.join(UT_SBIN_PATH, "run_scheduler.py") -SCHEDULE_RUNNER_TARGET = os.path.join(UT_DIR_PATH, "zzz_run_scheduler.py") RX_RESULT = re.compile(r'^(?POK|FAILED|ERROR)', re.MULTILINE) RX_DONE = re.compile(r'^UnitTesting: Done\.$', re.MULTILINE) @@ -46,7 +46,7 @@ def copy_file_if_not_exists(source, target): shutil.copyfile(source, target) -def create_schedule(package, output_file, default_schedule): +def create_schedule(package, default_schedule): schedule = [] try: @@ -66,76 +66,56 @@ def create_schedule(package, output_file, default_schedule): f.write(json.dumps(schedule, ensure_ascii=False, indent=True)) -def wait_for_output(path, schedule, timeout=10): - start_time = time.time() - needs_newline = False - - def check_has_timed_out(): - return time.time() - start_time > timeout - - def check_is_output_available(): - try: - return os.stat(path).st_size != 0 - except Exception: - pass - - while not check_is_output_available(): - print(".", end="") - sys.stdout.flush() - needs_newline = True - - if check_has_timed_out(): - print() - delete_file_if_exists(schedule) - raise ValueError('timeout') - - time.sleep(1) - else: - if needs_newline: - print() +def blocking_shell_cmd(cmd: str) -> int: + p = subprocess.Popen( + cmd, shell=True, stdout=subprocess.DEVNULL, stdin=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + return p.wait() def start_sublime_text(): - subprocess.Popen("subl &", shell=True) + return blocking_shell_cmd("subl &") def kill_sublime_text(): - subprocess.Popen("pkill [Ss]ubl || true", shell=True) - subprocess.Popen("pkill plugin_host || true", shell=True) + blocking_shell_cmd("pkill [Ss]ubl || true") + blocking_shell_cmd("pkill plugin_host || true") -def read_output(path): - # todo: use notification instead of polling - success = None - - def check_is_success(result): - try: - return RX_RESULT.search(result).group('result') == 'OK' - except AttributeError: - return success - - def check_is_done(result): - return RX_DONE.search(result) is not None - - with open(path, 'r') as f: - while True: - offset = f.tell() - result = f.read() - - print(result, end="") +def run_sublime_application_command( + cmd: str, + args: Optional[Dict[str, Any]] = None +) -> int: + if args is not None: + command = f"{cmd} '{json.dumps(args)}'" + else: + command = cmd + return blocking_shell_cmd(f"subl --command {command}") - # Keep checking while we don't have a definite result. - success = check_is_success(result) - if check_is_done(result): - assert success is not None, 'final test result must not be None' - break - elif not result: - f.seek(offset) +success: Optional[bool] = None - time.sleep(0.2) - return success +async def read_output( + reader: asyncio.StreamReader, + _: asyncio.StreamWriter +) -> None: + try: + while not reader.at_eof(): + line = str(await reader.readline(), encoding='UTF-8').rstrip() + print(line) + global success + match = RX_RESULT.search(line) + if match: + success = match.group('result') == 'OK' + else: + match = RX_DONE.search(line) + if match: + assert success is not None + asyncio.get_running_loop().stop() + except Exception as ex: + print("ERROR:", ex, file=sys.stderr) + asyncio.get_running_loop().stop() def restore_coverage_file(path, package): @@ -151,38 +131,30 @@ def restore_coverage_file(path, package): def main(default_schedule_info): package_under_test = default_schedule_info['package'] output_dir = os.path.join(UT_OUTPUT_DIR_PATH, package_under_test) - output_file = os.path.join(output_dir, "result") + ping_file = os.path.join(UT_OUTPUT_DIR_PATH, "ready") coverage_file = os.path.join(output_dir, "coverage") - - default_schedule_info['output'] = output_file - - for i in range(3): - create_dir_if_not_exists(output_dir) - delete_file_if_exists(output_file) - delete_file_if_exists(coverage_file) - create_schedule(package_under_test, output_file, default_schedule_info) - delete_file_if_exists(SCHEDULE_RUNNER_TARGET) - copy_file_if_not_exists(SCHEDULE_RUNNER_SOURCE, SCHEDULE_RUNNER_TARGET) - start_sublime_text() - try: - print("Wait for tests output...", end="") - wait_for_output(output_file, SCHEDULE_RUNNER_TARGET) - break - except ValueError: - if i == 2: - print("Timeout: Could not obtain tests output.") - print("Maybe Sublime Text is not responding or the tests output " - "is being written to the wrong file.") - delete_file_if_exists(SCHEDULE_RUNNER_TARGET) - sys.exit(1) - kill_sublime_text() - time.sleep(2) - - print("Start to read output...") - if not read_output(output_file): - sys.exit(1) + port = 34151 + default_schedule_info['tcp_port'] = port + create_dir_if_not_exists(output_dir) + create_schedule(package_under_test, default_schedule_info) + delete_file_if_exists(coverage_file) + delete_file_if_exists(ping_file) + start_sublime_text() + coro = asyncio.start_server(read_output, host='localhost', port=port) + loop = asyncio.get_event_loop() + server = loop.run_until_complete(coro) + while not os.path.exists(ping_file): + time.sleep(0.1) + run_sublime_application_command("unit_testing_ping") + delete_file_if_exists(ping_file) + run_sublime_application_command("unit_testing_run_scheduler") + loop.run_forever() + server.close() + loop.run_until_complete(server.wait_closed()) + loop.close() + kill_sublime_text() restore_coverage_file(coverage_file, package_under_test) - delete_file_if_exists(SCHEDULE_RUNNER_TARGET) + exit(0 if success else 1) if __name__ == '__main__': diff --git a/unittesting/__init__.py b/unittesting/__init__.py index 79ae21b0..b2022dc0 100644 --- a/unittesting/__init__.py +++ b/unittesting/__init__.py @@ -1,6 +1,6 @@ from .core import DeferrableTestCase, AWAIT_WORKER, expectedFailure from .scheduler import UnitTestingRunSchedulerCommand -from .scheduler import run_scheduler +from .scheduler import UnitTestingPingCommand from .package import UnitTestingCommand from .coverage import UnitTestingCoverageCommand from .current import UnitTestingCurrentFileCommand @@ -14,7 +14,7 @@ __all__ = [ "DeferrableTestCase", "UnitTestingRunSchedulerCommand", - "run_scheduler", + "UnitTestingPingCommand", "UnitTestingCommand", "UnitTestingCoverageCommand", "UnitTestingCurrentFileCommand", diff --git a/unittesting/mixin.py b/unittesting/mixin.py index 4af69020..375d02a6 100644 --- a/unittesting/mixin.py +++ b/unittesting/mixin.py @@ -2,6 +2,7 @@ import os import sys import re +import socket from glob import glob from .utils import reload_package @@ -103,20 +104,17 @@ def default_output(self, package): return outfile def load_stream(self, package, settings): - output = settings["output"] - if not output or output == "": + tcp_port = settings.get("tcp_port") + if tcp_port is None: + print("tcp_port is None :(") output_panel = OutputPanel( 'UnitTesting', file_regex=r'File "([^"]*)", line (\d+)') output_panel.show() stream = output_panel else: - if not os.path.isabs(output): - if sublime.platform() == "windows": - output = output.replace("/", "\\") - output = os.path.join(sublime.packages_path(), package, output) - if os.path.exists(output): - os.remove(output) - stream = open(output, "w") + print("tcp_port is not None :)") + conn = socket.create_connection(('localhost', tcp_port)) + stream = conn.makefile('w') return stream diff --git a/unittesting/scheduler.py b/unittesting/scheduler.py index 07e72e6d..d5d6de8a 100644 --- a/unittesting/scheduler.py +++ b/unittesting/scheduler.py @@ -1,48 +1,47 @@ import os -import threading -import time import sublime import sublime_plugin from .utils import JsonFile +_is_loaded = False + + +def set_loaded(b): + global _is_loaded + print("setting _is_loaded to", b) + _is_loaded = b + + +def is_loaded(): + global _is_loaded + return _is_loaded + + class Unit: def __init__(self, s): - self.package = s['package'] - - self.output = s.get('output', None) self.syntax_test = s.get('syntax_test', False) self.syntax_compatibility = s.get('syntax_compatibility', False) self.color_scheme_test = s.get('color_scheme_test', False) self.coverage = s.get('coverage', False) + self.kwargs = {"package": s['package']} + tcp_port = s.get('tcp_port') + if tcp_port is not None: + self.kwargs["tcp_port"] = tcp_port def run(self): if self.syntax_test: - sublime.run_command("unit_testing_syntax", { - "package": self.package, - "output": self.output - }) + command = "unit_testing_syntax" elif self.syntax_compatibility: - sublime.run_command("unit_testing_syntax_compatibility", { - "package": self.package, - "output": self.output - }) + command = "unit_testing_syntax_compatibility" elif self.color_scheme_test: - sublime.run_command("unit_testing_color_scheme", { - "package": self.package, - "output": self.output - }) + command = "unit_testing_color_scheme" elif self.coverage: - sublime.run_command("unit_testing_coverage", { - "package": self.package, - "output": self.output - }) + command = "unit_testing_coverage" else: - sublime.run_command("unit_testing", { - "package": self.package, - "output": self.output - }) + command = "unit_testing" + sublime.run_command(command, self.kwargs) class Scheduler: @@ -71,23 +70,15 @@ def clean_schedule(self): class UnitTestingRunSchedulerCommand(sublime_plugin.ApplicationCommand): - ready = False def run(self): - UnitTestingRunSchedulerCommand.ready = True - scheduler = Scheduler() - sublime.set_timeout(scheduler.run, 2000) - + Scheduler().run() -def try_running_scheduler(): - while not UnitTestingRunSchedulerCommand.ready: - sublime.set_timeout( - lambda: sublime.run_command("unit_testing_run_scheduler"), 1) - time.sleep(1) +class UnitTestingPingCommand(sublime_plugin.ApplicationCommand): - -def run_scheduler(): - UnitTestingRunSchedulerCommand.ready = False - th = threading.Thread(target=try_running_scheduler) - th.start() + def run(self): + ready_file = os.path.join( + sublime.packages_path(), "User", "UnitTesting", "ready") + with open(ready_file, 'w') as fp: + print("ready", file=fp) diff --git a/ut.py b/ut.py index fc111d31..6eeb8e37 100644 --- a/ut.py +++ b/ut.py @@ -26,6 +26,7 @@ from unittesting import UnitTestingRunSchedulerCommand # noqa: F401 +from unittesting import UnitTestingPingCommand # noqa: F401 from unittesting import UnitTestingCommand # noqa: F401 from unittesting import UnitTestingCoverageCommand # noqa: F401 from unittesting import UnitTestingCurrentFileCommand # noqa: F401 @@ -38,6 +39,7 @@ __all__ = [ "UnitTestingRunSchedulerCommand", + "UnitTestingPingCommand", "UnitTestingCommand", "UnitTestingCoverageCommand", "UnitTestingCurrentFileCommand", From dd9f8ce3ac62b87b09dc94cf3c55a3fb1124daa6 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Sun, 22 Mar 2020 19:46:42 +0100 Subject: [PATCH 2/3] cleanup --- unittesting/mixin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/unittesting/mixin.py b/unittesting/mixin.py index 375d02a6..645ef322 100644 --- a/unittesting/mixin.py +++ b/unittesting/mixin.py @@ -106,13 +106,11 @@ def default_output(self, package): def load_stream(self, package, settings): tcp_port = settings.get("tcp_port") if tcp_port is None: - print("tcp_port is None :(") output_panel = OutputPanel( 'UnitTesting', file_regex=r'File "([^"]*)", line (\d+)') output_panel.show() stream = output_panel else: - print("tcp_port is not None :)") conn = socket.create_connection(('localhost', tcp_port)) stream = conn.makefile('w') From 436080b7e0abe7d76160592114793faaad6ff675 Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Sun, 22 Mar 2020 19:50:03 +0100 Subject: [PATCH 3/3] Remove _is_loaded, no longer needed --- unittesting/scheduler.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/unittesting/scheduler.py b/unittesting/scheduler.py index d5d6de8a..b68a3240 100644 --- a/unittesting/scheduler.py +++ b/unittesting/scheduler.py @@ -4,20 +4,6 @@ from .utils import JsonFile -_is_loaded = False - - -def set_loaded(b): - global _is_loaded - print("setting _is_loaded to", b) - _is_loaded = b - - -def is_loaded(): - global _is_loaded - return _is_loaded - - class Unit: def __init__(self, s):