def test_runtime_dir_uses_tmp_fallback(): # Given XDG_RUNTIME_DIR and TMPDIR are not set # When RuntimeDir is invoked with name 'quicken-test' # Then it should succeed and its path should be # /tmp/quicken-test-{uid} with env(XDG_RUNTIME_DIR=None, TMPDIR=None): runtime_dir = RuntimeDir("quicken-test") uid = os.getuid() assert str(runtime_dir) == f"/tmp/quicken-test-{uid}" os.fchdir(runtime_dir.fileno()) assert str(runtime_dir) == os.getcwd()
def test_runtime_dir_uses_xdg_env_var(): # Given XDG_RUNTIME_DIR is set # When RuntimeDir is invoked with name 'quicken-test' # Then it should succeed and its path should be # $XDG_RUNTIME_DIR/quicken-test. with tempfile.TemporaryDirectory() as p: with env(XDG_RUNTIME_DIR=p): runtime_dir = RuntimeDir("quicken-test") assert str(runtime_dir) == os.path.join(p, "quicken-test") os.fchdir(runtime_dir.fileno()) assert str(runtime_dir) == os.getcwd()
def test_runtime_dir_uses_tmpdir_env_var(): # Given XDG_RUNTIME_DIR is not set # And TMPDIR is set # When RuntimeDir is invoked with name 'quicken-test' # Then it should succeed and its path should be # $TMPDIR/quicken-test-{uid} with tempfile.TemporaryDirectory() as p: with env(XDG_RUNTIME_DIR=None, TMPDIR=p): runtime_dir = RuntimeDir("quicken-test") uid = os.getuid() assert str(runtime_dir) == os.path.join(p, f"quicken-test-{uid}") os.fchdir(runtime_dir.fileno()) assert str(runtime_dir) == os.getcwd()
def test_runtime_dir_succeeds_creating_a_file(): sample_text = "hello" with tempfile.TemporaryDirectory() as p: runtime_dir = RuntimeDir(dir_path=p) file = get_bound_path(runtime_dir, "example") file.write_text(sample_text, encoding="utf-8") text = (Path(p) / "example").read_text(encoding="utf-8") assert sample_text == text
def test_runtime_dir_fails_when_bad_permissions(): # Given a directory that exists with permissions 770. # When a RuntimeDir is constructed from it then with tempfile.TemporaryDirectory() as p: os.chmod(p, 0o770) with pytest.raises(RuntimeError) as excinfo: _runtime_dir = RuntimeDir(dir_path=p) v = str(excinfo.value) assert "must have permissions 700" in v
def test_killed_client_causes_handler_to_exit(): # Given the server is processing a command in a subprocess. # And the client process is killed (receives SIGKILL and exits) # Then the same signal should be sent to the subprocess running the command # And it should exit. @cli_factory() def runner(): def inner(): # Block just to ensure that only an unblockable signal would # be able terminate the process. signal.pthread_sigmask(signal.SIG_BLOCK, forwarded_signals) pid = os.getpid() fd, path = tempfile.mkstemp() os.write(fd, str(pid).encode("utf-8")) os.fsync(fd) os.close(fd) os.rename(path, runner_pid_file) logger.debug("Inner function waiting") while True: signal.pause() return inner # Client runs in child process so we don't kill the test process itself. def client(): logger.debug("Client starting") signal.pthread_sigmask(signal.SIG_BLOCK, forwarded_signals) sys.exit(runner()) with isolated_filesystem() as path: with contained_children(): runner_pid_file = Path("runner_pid").absolute() runtime_dir = RuntimeDir(dir_path=str(path)) p = Process(target=client) logger.debug("Starting process") p.start() logger.debug("Waiting for pid file") assert wait_for_create( get_bound_path(runtime_dir, runner_pid_file.name), timeout=2), f"{runner_pid_file} must have been created" runner_pid = int(runner_pid_file.read_text(encoding="utf-8")) logger.debug("Runner started with pid: %d", runner_pid) client_process = psutil.Process(pid=p.pid) runner_process = psutil.Process(pid=runner_pid) logger.debug("Killing client") client_process.kill() logger.debug("Waiting for client") p.join() logger.debug("Waiting for runner") runner_process.wait()
def test_runtime_dir_path_fails_when_directory_unlinked_and_recreated(): # Given a runtime dir that has been created. # And removed # And recreated manually # When the runtime dir is used to create a new file # Then the operation should fail. sample_text = "hello" with tempfile.TemporaryDirectory() as p: runtime_dir = RuntimeDir(dir_path=p) file = get_bound_path(runtime_dir, "example") Path(p).mkdir() try: with pytest.raises(FileNotFoundError) as excinfo: file.write_text(sample_text, encoding="utf-8") assert "example" in str(excinfo.value) assert Path(p).exists() finally: Path(p).rmdir()
def test_watch_for_create_notices_file_fast(): with isolated_filesystem() as p: # To rule out dependence on being in the cwd. os.chdir("/") runtime_dir = RuntimeDir(dir_path=p) file = get_bound_path(runtime_dir, "example.txt") writer_timestamp: datetime = None def create_file(): nonlocal writer_timestamp time.sleep(0.05) file.write_text("hello", encoding="utf-8") writer_timestamp = datetime.now() t = threading.Thread(target=create_file) t.start() result = wait_for_create(file, timeout=1) t.join() timestamp = datetime.now() assert result, "File must have been created" assert timestamp - writer_timestamp < timedelta(seconds=0.05)
def test_client_receiving_tstp_ttin_stops_itself(): # Given the server is processing a command in a subprocess # When the client receives signal.SIGTSTP or signal.SIGTTIN # Then the same signal should be sent to the subprocess running the command # And the client should be stopped # Ensure that the test runner caller doesn't impact signal handling. signal.pthread_sigmask(signal.SIG_SETMASK, []) test_signals = {signal.SIGTSTP, signal.SIGTTIN} resume_signal = signal.SIGUSR1 @cli_factory() def runner(): def inner(): # Block signals we expect to receive signal.pthread_sigmask(signal.SIG_BLOCK, test_signals | {signal.SIGUSR1}) # Save our pid so it is accessible to the test process, avoiding # any race conditions where the file may be empty. pid = os.getpid() fd, path = tempfile.mkstemp() os.write(fd, str(pid).encode("utf-8")) os.fsync(fd) os.close(fd) os.rename(path, runner_pid_file) for sig in test_signals: logger.debug("Waiting for %s", sig) signal.sigwait({sig}) # Stop self to indicate success to test process. os.kill(pid, signal.SIGSTOP) logger.debug("Waiting for signal to exit") # This is required otherwise we may exit while the test is checking # for our status. signal.sigwait({resume_signal}) logger.debug("All signals received") return inner def client(): # All the work to forward signals is done in the library. sys.exit(runner()) def wait_for(predicate): # Busy wait since we don't have a good way to get signalled on process # status change. while not predicate(): time.sleep(0.1) with isolated_filesystem() as path: with contained_children(): # Get process pids. The Process object already has the client pid, # but we need to wait for the runner pid to be written to the file. runner_pid_file = Path("runner_pid").absolute() runtime_dir = RuntimeDir(dir_path=str(path)) p = Process(target=client) p.start() assert wait_for_create( get_bound_path(runtime_dir, runner_pid_file.name), timeout=2), f"{runner_pid_file} must have been created" runner_pid = int(runner_pid_file.read_text(encoding="utf-8")) # Stop and continue the client process, checking that it was # correctly applied to both the client and runner processes. client_process = psutil.Process(pid=p.pid) runner_process = psutil.Process(pid=runner_pid) for sig in [signal.SIGTSTP, signal.SIGTTIN]: logger.debug("Sending %s", sig) client_process.send_signal(sig) logger.debug("Waiting for client to stop") wait_for( lambda: client_process.status() == psutil.STATUS_STOPPED) logger.debug("Waiting for runner to stop") wait_for( lambda: runner_process.status() == psutil.STATUS_STOPPED) client_process.send_signal(signal.SIGCONT) logger.debug("Waiting for client to resume") wait_for( lambda: client_process.status() != psutil.STATUS_STOPPED) logger.debug("Waiting for runner to resume") wait_for( lambda: runner_process.status() != psutil.STATUS_STOPPED) # Resume runner process so it exits. runner_process.send_signal(resume_signal) logger.debug("Waiting for client to finish") p.join() assert p.exitcode == 0
def test_runtime_dir_fails_when_no_args(): with pytest.raises(ValueError) as excinfo: _runtime_dir = RuntimeDir() v = str(excinfo.value) assert "base_name" in v and "dir_path" in v
def test_wait_for_delete_fails_existing_file(): with isolated_filesystem() as p: runtime_dir = RuntimeDir(dir_path=p) file = get_bound_path(runtime_dir, "example.txt") file.write_text("hello", encoding="utf-8") assert not wait_for_delete(file, timeout=0.1)
def test_wait_for_delete_notices_missing_file(): with isolated_filesystem() as p: runtime_dir = RuntimeDir(dir_path=p) file = get_bound_path(runtime_dir, "example.txt") assert wait_for_delete(file, timeout=0.01)