From ada2bd6752809add6de6d89dd2a9a5a7deb2b333 Mon Sep 17 00:00:00 2001 From: Matt Glazar Date: Tue, 9 Oct 2018 14:31:21 -0700 Subject: [PATCH] Refactor stop_test.py to use pexpect Summary: pexpect prints program output while the program is running, which is very useful when debugging. Make the tests in stop_test.py use this feature. Also, add some assertions to some of the tests. Reviewed By: chadaustin Differential Revision: D10157354 fbshipit-source-id: 210fcde79d5d21c6ee34fad3813e7c56eb298b9b --- eden/integration/health_test.py | 37 +------------- eden/integration/lib/pexpect.py | 63 ++++++++++++++++++++++++ eden/integration/stop_test.py | 85 ++++++++++++++++----------------- 3 files changed, 106 insertions(+), 79 deletions(-) create mode 100644 eden/integration/lib/pexpect.py diff --git a/eden/integration/health_test.py b/eden/integration/health_test.py index ed1d4660eb..581223c9be 100644 --- a/eden/integration/health_test.py +++ b/eden/integration/health_test.py @@ -22,6 +22,7 @@ from eden.cli.daemon import wait_for_shutdown from .lib import edenclient, testcase from .lib.fake_edenfs import fake_eden_daemon from .lib.find_executables import FindExe +from .lib.pexpect import PexpectAssertionMixin class HealthTest(testcase.EdenTestCase): @@ -37,7 +38,7 @@ class HealthTest(testcase.EdenTestCase): self.assertFalse(client.is_healthy()) -class HealthOfFakeEdenFSTest(unittest.TestCase): +class HealthOfFakeEdenFSTest(unittest.TestCase, PexpectAssertionMixin): def setUp(self): super().setUp() @@ -95,37 +96,3 @@ class HealthOfFakeEdenFSTest(unittest.TestCase): status_process.logfile = sys.stderr status_process.expect_exact("eden running normally") self.assert_process_succeeds(status_process) - - def assert_process_succeeds(self, process: pexpect.spawn): - actual_exit_code = wait_for_pexpect_process(process) - self.assertEqual( - actual_exit_code, - 0, - f"Command should return success: {pexpect_process_shell_command(process)}", - ) - - def assert_process_fails(self, process: pexpect.spawn, exit_code: int): - assert exit_code != 0 - actual_exit_code = wait_for_pexpect_process(process) - self.assertEqual( - actual_exit_code, - exit_code, - f"Command should return an error code: " - f"{pexpect_process_shell_command(process)}", - ) - - -def pexpect_process_shell_command(process: pexpect.spawn) -> str: - def str_from_strlike(s: typing.Union[bytes, str]) -> str: - if isinstance(s, str): - return s - else: - return s.decode("utf-8") - - command_parts = [process.command] + [str_from_strlike(arg) for arg in process.args] - return " ".join(map(shlex.quote, command_parts)) - - -def wait_for_pexpect_process(process: pexpect.spawn) -> int: - process.expect_exact(pexpect.EOF) - return process.wait() diff --git a/eden/integration/lib/pexpect.py b/eden/integration/lib/pexpect.py new file mode 100644 index 0000000000..c7adee44ff --- /dev/null +++ b/eden/integration/lib/pexpect.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2016-present, Facebook, Inc. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. + +import abc +import shlex +from typing import Any, Union + +import pexpect + + +class PexpectAssertionMixin(metaclass=abc.ABCMeta): + def assert_process_succeeds(self, process: pexpect.spawn): + actual_exit_code = wait_for_pexpect_process(process) + self.assertEqual( + actual_exit_code, + 0, + f"Command should return success: {pexpect_process_shell_command(process)}", + ) + + def assert_process_fails(self, process: pexpect.spawn, exit_code: int): + assert exit_code != 0 + actual_exit_code = wait_for_pexpect_process(process) + self.assertEqual( + actual_exit_code, + exit_code, + f"Command should return an error code: " + f"{pexpect_process_shell_command(process)}", + ) + + def assert_process_exit_code(self, process: pexpect.spawn, exit_code: int): + actual_exit_code = wait_for_pexpect_process(process) + self.assertEqual( + actual_exit_code, + exit_code, + f"Command should exit with code {exit_code}: " + f"{pexpect_process_shell_command(process)}", + ) + + @abc.abstractmethod + def assertEqual(self, first: Any, second: Any, msg: Any = ...) -> None: + raise NotImplementedError() + + +def pexpect_process_shell_command(process: pexpect.spawn) -> str: + def str_from_strlike(s: Union[bytes, str]) -> str: + if isinstance(s, str): + return s + else: + return s.decode("utf-8") + + command_parts = [process.command] + [str_from_strlike(arg) for arg in process.args] + return " ".join(map(shlex.quote, command_parts)) + + +def wait_for_pexpect_process(process: pexpect.spawn) -> int: + process.expect_exact(pexpect.EOF) + return process.wait() diff --git a/eden/integration/stop_test.py b/eden/integration/stop_test.py index 1c59cde1cf..5399f0f732 100644 --- a/eden/integration/stop_test.py +++ b/eden/integration/stop_test.py @@ -12,15 +12,18 @@ import pathlib import shutil import signal import subprocess +import sys import tempfile import typing import unittest +import pexpect from eden.cli.daemon import did_process_exit from eden.cli.util import poll_until from .lib.fake_edenfs import fake_eden_daemon, spawn_fake_eden_daemon from .lib.find_executables import FindExe +from .lib.pexpect import PexpectAssertionMixin SHUTDOWN_EXIT_CODE_NORMAL = 0 @@ -29,7 +32,7 @@ SHUTDOWN_EXIT_CODE_NOT_RUNNING_ERROR = 2 SHUTDOWN_EXIT_CODE_TERMINATED_VIA_SIGKILL = 3 -class StopTest(unittest.TestCase): +class StopTest(unittest.TestCase, PexpectAssertionMixin): def setUp(self): def cleanup_tmp_dir() -> None: shutil.rmtree(self.tmp_dir, ignore_errors=True) @@ -39,17 +42,14 @@ class StopTest(unittest.TestCase): def test_stop_stops_running_daemon(self): with fake_eden_daemon(pathlib.Path(self.tmp_dir)) as daemon_pid: - stop_result = subprocess.run( - [ - FindExe.EDEN_CLI, - "--config-dir", - self.tmp_dir, - "stop", - "--timeout", - "5", - ] + stop_process = pexpect.spawn( + FindExe.EDEN_CLI, + ["--config-dir", self.tmp_dir, "stop", "--timeout", "5"], + encoding="utf-8", + logfile=sys.stderr, ) - self.assertEqual(SHUTDOWN_EXIT_CODE_NORMAL, stop_result.returncode) + stop_process.expect_exact("edenfs exited cleanly.") + self.assert_process_exit_code(stop_process, SHUTDOWN_EXIT_CODE_NORMAL) self.assertTrue( did_process_exit(daemon_pid), f"Process {daemon_pid} should have died" ) @@ -82,28 +82,25 @@ class StopTest(unittest.TestCase): "1", ] print("Stopping eden: %r" % (stop_cmd,)) - stop_result = subprocess.run( - stop_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE + stop_process = pexpect.spawn( + stop_cmd[0], stop_cmd[1:], encoding="utf-8", logfile=sys.stderr ) - self.assertIn(b"Terminated edenfs with SIGKILL", stop_result.stderr) - self.assertEqual( - SHUTDOWN_EXIT_CODE_TERMINATED_VIA_SIGKILL, stop_result.returncode + stop_process.expect_exact("Terminated edenfs with SIGKILL") + self.assert_process_exit_code( + stop_process, SHUTDOWN_EXIT_CODE_TERMINATED_VIA_SIGKILL ) def test_async_stop_stops_daemon_eventually(self): with fake_eden_daemon(pathlib.Path(self.tmp_dir)) as daemon_pid: - stop_result = subprocess.run( - [ - FindExe.EDEN_CLI, - "--config-dir", - self.tmp_dir, - "stop", - "--timeout", - "0", - ] + stop_process = pexpect.spawn( + FindExe.EDEN_CLI, + ["--config-dir", self.tmp_dir, "stop", "--timeout", "0"], + encoding="utf-8", + logfile=sys.stderr, ) - self.assertEqual( - SHUTDOWN_EXIT_CODE_REQUESTED_SHUTDOWN, stop_result.returncode + stop_process.expect_exact("Sent async shutdown request to edenfs.") + self.assert_process_exit_code( + stop_process, SHUTDOWN_EXIT_CODE_REQUESTED_SHUTDOWN ) def daemon_exited() -> typing.Optional[bool]: @@ -115,28 +112,28 @@ class StopTest(unittest.TestCase): poll_until(daemon_exited, timeout=10) def test_stop_not_running(self): - stop_cmd = [ + stop_process = pexpect.spawn( FindExe.EDEN_CLI, - "--config-dir", - self.tmp_dir, - "stop", - "--timeout", - "1", - ] - stop_result = subprocess.run( - stop_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ["--config-dir", self.tmp_dir, "stop", "--timeout", "1"], + encoding="utf-8", + logfile=sys.stderr, + ) + stop_process.expect_exact("edenfs is not running") + self.assert_process_exit_code( + stop_process, SHUTDOWN_EXIT_CODE_NOT_RUNNING_ERROR ) - self.assertIn(b"edenfs is not running", stop_result.stderr) - self.assertEqual(SHUTDOWN_EXIT_CODE_NOT_RUNNING_ERROR, stop_result.returncode) def test_stopping_killed_daemon_reports_not_running(self): daemon_pid = spawn_fake_eden_daemon(pathlib.Path(self.tmp_dir)) os.kill(daemon_pid, signal.SIGKILL) - stop_result = subprocess.run( - [FindExe.EDEN_CLI, "--config-dir", self.tmp_dir, "stop", "--timeout", "1"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + stop_process = pexpect.spawn( + FindExe.EDEN_CLI, + ["--config-dir", self.tmp_dir, "stop", "--timeout", "1"], + encoding="utf-8", + logfile=sys.stderr, + ) + stop_process.expect_exact("edenfs is not running") + self.assert_process_exit_code( + stop_process, SHUTDOWN_EXIT_CODE_NOT_RUNNING_ERROR ) - self.assertIn(b"edenfs is not running", stop_result.stderr) - self.assertEqual(SHUTDOWN_EXIT_CODE_NOT_RUNNING_ERROR, stop_result.returncode)