| 122 | |
| 123 | |
| 124 | class LsofFdLeakChecker: |
| 125 | def get_open_files(self) -> List[Tuple[str, str]]: |
| 126 | out = subprocess.run( |
| 127 | ("lsof", "-Ffn0", "-p", str(os.getpid())), |
| 128 | stdout=subprocess.PIPE, |
| 129 | stderr=subprocess.DEVNULL, |
| 130 | check=True, |
| 131 | universal_newlines=True, |
| 132 | ).stdout |
| 133 | |
| 134 | def isopen(line: str) -> bool: |
| 135 | return line.startswith("f") and ( |
| 136 | "deleted" not in line |
| 137 | and "mem" not in line |
| 138 | and "txt" not in line |
| 139 | and "cwd" not in line |
| 140 | ) |
| 141 | |
| 142 | open_files = [] |
| 143 | |
| 144 | for line in out.split("\n"): |
| 145 | if isopen(line): |
| 146 | fields = line.split("\0") |
| 147 | fd = fields[0][1:] |
| 148 | filename = fields[1][1:] |
| 149 | if filename in IGNORE_PAM: |
| 150 | continue |
| 151 | if filename.startswith("/"): |
| 152 | open_files.append((fd, filename)) |
| 153 | |
| 154 | return open_files |
| 155 | |
| 156 | def matching_platform(self) -> bool: |
| 157 | try: |
| 158 | subprocess.run(("lsof", "-v"), check=True) |
| 159 | except (OSError, subprocess.CalledProcessError): |
| 160 | return False |
| 161 | else: |
| 162 | return True |
| 163 | |
| 164 | @hookimpl(hookwrapper=True, tryfirst=True) |
| 165 | def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: |
| 166 | lines1 = self.get_open_files() |
| 167 | yield |
| 168 | if hasattr(sys, "pypy_version_info"): |
| 169 | gc.collect() |
| 170 | lines2 = self.get_open_files() |
| 171 | |
| 172 | new_fds = {t[0] for t in lines2} - {t[0] for t in lines1} |
| 173 | leaked_files = [t for t in lines2 if t[0] in new_fds] |
| 174 | if leaked_files: |
| 175 | error = [ |
| 176 | "***** %s FD leakage detected" % len(leaked_files), |
| 177 | *(str(f) for f in leaked_files), |
| 178 | "*** Before:", |
| 179 | *(str(f) for f in lines1), |
| 180 | "*** After:", |
| 181 | *(str(f) for f in lines2), |
no outgoing calls