|
1 | 1 | """Helpers for filesystem-dependent tests.
|
2 | 2 | """
|
| 3 | +import multiprocessing |
3 | 4 | import os
|
4 | 5 | import socket
|
5 | 6 | import subprocess
|
6 | 7 | import sys
|
| 8 | +import traceback |
7 | 9 | from functools import partial
|
8 | 10 | from itertools import chain
|
9 | 11 |
|
@@ -34,6 +36,101 @@ def make_unreadable_file(path):
|
34 | 36 | subprocess.check_call(args)
|
35 | 37 |
|
36 | 38 |
|
| 39 | +if sys.platform == 'win32': |
| 40 | + def lock_action(f): |
| 41 | + pass |
| 42 | +else: |
| 43 | + def lock_action(f): |
| 44 | + pass |
| 45 | + |
| 46 | + |
| 47 | +def external_file_opener(conn): |
| 48 | + """ |
| 49 | + This external process is run with multiprocessing. |
| 50 | + It waits for a path from the parent, opens it, and then wait for another |
| 51 | + message before closing it. |
| 52 | +
|
| 53 | + :param conn: bi-directional pipe |
| 54 | + :return: nothing |
| 55 | + """ |
| 56 | + f = None |
| 57 | + try: |
| 58 | + # Wait for parent to send path |
| 59 | + msg = conn.recv() |
| 60 | + if msg is True: |
| 61 | + # Do nothing - we have been told to exit without a path or action |
| 62 | + pass |
| 63 | + else: |
| 64 | + path, action = msg |
| 65 | + # Open the file |
| 66 | + try: |
| 67 | + f = open(path, 'r') |
| 68 | + # NOTE: action is for future use and may be unused |
| 69 | + if action == 'lock': |
| 70 | + lock_action(f) |
| 71 | + elif action == 'noread': |
| 72 | + make_unreadable_file(path) |
| 73 | + except (OSError, IOError): |
| 74 | + # IOError is OSError post PEP 3151 |
| 75 | + traceback.print_exc(None, sys.stderr) |
| 76 | + |
| 77 | + # Indicate the file is opened |
| 78 | + conn.send(True) |
| 79 | + # Now path is open and we wait for signal to exit |
| 80 | + conn.recv() |
| 81 | + finally: |
| 82 | + if f: |
| 83 | + f.close() |
| 84 | + conn.close() |
| 85 | + |
| 86 | + |
| 87 | +class FileOpener(object): |
| 88 | + """ |
| 89 | + Test class acts as a context manager which can open a file from a |
| 90 | + subprocess, and hold it open to assure that this does not interfere with |
| 91 | + pip's operations. |
| 92 | +
|
| 93 | + If a path is passed to the FileOpener, it immediately sends a message to |
| 94 | + the other process to open that path. An action of "lock" or "noread" can |
| 95 | + also be sent to the subprocess, resulting in various additional monkey |
| 96 | + wrenches now and in the future. |
| 97 | +
|
| 98 | + Opening the path and taking the action can be deferred however, so that |
| 99 | + the FileOpener may function as a pytest fixture if so desired. |
| 100 | + """ |
| 101 | + def __init__(self, path=None, action=None): |
| 102 | + self.path = None |
| 103 | + self.conn, child_conn = multiprocessing.Pipe() |
| 104 | + self.child = multiprocessing.Process( |
| 105 | + target=external_file_opener, |
| 106 | + args=(child_conn,) |
| 107 | + ) |
| 108 | + self.child.daemon = True |
| 109 | + self.child.start() |
| 110 | + if path: |
| 111 | + self.send(path, action) |
| 112 | + |
| 113 | + def send(self, path, action=None): |
| 114 | + if self.path is not None: |
| 115 | + raise AttributeError('path may only be set once') |
| 116 | + self.path = str(path) |
| 117 | + self.conn.send((str(path), action)) |
| 118 | + return self.conn.recv() |
| 119 | + |
| 120 | + def cleanup(self): |
| 121 | + # send a message to the child to exit |
| 122 | + if self.child: |
| 123 | + self.conn.send(True) |
| 124 | + self.child.join() |
| 125 | + self.child = None |
| 126 | + |
| 127 | + def __enter__(self): |
| 128 | + return self |
| 129 | + |
| 130 | + def __exit__(self, exc_type, exc_val, exc_tb): |
| 131 | + self.cleanup() |
| 132 | + |
| 133 | + |
37 | 134 | def get_filelist(base):
|
38 | 135 | def join(dirpath, dirnames, filenames):
|
39 | 136 | relative_dirpath = os.path.relpath(dirpath, base)
|
|
0 commit comments