794 lines
39 KiB
Python
794 lines
39 KiB
Python
|
|
import argparse
|
||
|
|
import contextlib
|
||
|
|
import ctypes
|
||
|
|
import errno
|
||
|
|
import fcntl
|
||
|
|
import hashlib
|
||
|
|
import json
|
||
|
|
import mmap
|
||
|
|
import os
|
||
|
|
import shutil
|
||
|
|
import socket
|
||
|
|
import stat
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
import tempfile
|
||
|
|
import threading
|
||
|
|
import time
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
|
||
|
|
ROOT = Path(__file__).resolve().parents[1]
|
||
|
|
FIXTURES = ROOT / "tests/fixtures/ignored-workspace-v1"
|
||
|
|
NAMESPACE_MARKER = "LAZARET_IGNORED_WORKSPACE_PRIVATE_NAMESPACE"
|
||
|
|
|
||
|
|
|
||
|
|
def command(argv, cwd=None, env=None, check=True):
|
||
|
|
return subprocess.run(
|
||
|
|
[os.fspath(value) for value in argv],
|
||
|
|
cwd=cwd,
|
||
|
|
env=env,
|
||
|
|
check=check,
|
||
|
|
stdout=subprocess.PIPE,
|
||
|
|
stderr=subprocess.PIPE,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def require(condition, message):
|
||
|
|
if not condition:
|
||
|
|
raise AssertionError(message)
|
||
|
|
|
||
|
|
|
||
|
|
def expect_errno(expected, action):
|
||
|
|
try:
|
||
|
|
action()
|
||
|
|
except OSError as error:
|
||
|
|
require(error.errno == expected, f"expected errno {expected}, observed {error.errno}")
|
||
|
|
else:
|
||
|
|
raise AssertionError(f"expected errno {expected}, operation succeeded")
|
||
|
|
|
||
|
|
|
||
|
|
class MountedFixture:
|
||
|
|
def __init__(self, work_root, router, global_excludes=None):
|
||
|
|
self.root = Path(tempfile.mkdtemp(prefix="ignored-workspace-", dir=work_root))
|
||
|
|
self.host = self.root / "host"
|
||
|
|
self.isolated = self.root / "isolated"
|
||
|
|
self.mount = self.root / "mount"
|
||
|
|
for path in (self.host, self.isolated, self.mount):
|
||
|
|
path.mkdir()
|
||
|
|
command(["git", "init", "--quiet"], self.host)
|
||
|
|
command(["git", "config", "user.email", "[email protected]"], self.host)
|
||
|
|
command(["git", "config", "user.name", "Fixture"], self.host)
|
||
|
|
self.router = Path(router)
|
||
|
|
self.global_excludes = global_excludes
|
||
|
|
self.process = None
|
||
|
|
self.closed = False
|
||
|
|
|
||
|
|
def start(self):
|
||
|
|
argv = [self.router, self.host, self.isolated, self.mount]
|
||
|
|
if self.global_excludes is not None:
|
||
|
|
argv.append(self.global_excludes)
|
||
|
|
self.process = subprocess.Popen(argv, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||
|
|
deadline = time.monotonic() + 10
|
||
|
|
while time.monotonic() < deadline:
|
||
|
|
if self.is_mounted():
|
||
|
|
return self
|
||
|
|
if self.process.poll() is not None:
|
||
|
|
stderr = self.process.stderr.read().decode(errors="replace")
|
||
|
|
raise RuntimeError(f"router exited before mount: {stderr}")
|
||
|
|
time.sleep(0.01)
|
||
|
|
raise TimeoutError("router did not mount within ten seconds")
|
||
|
|
|
||
|
|
def stop_router(self):
|
||
|
|
if self.process is not None and self.process.poll() is None:
|
||
|
|
try:
|
||
|
|
self.process.wait(timeout=2)
|
||
|
|
except subprocess.TimeoutExpired:
|
||
|
|
self.process.terminate()
|
||
|
|
self.process.wait(timeout=10)
|
||
|
|
|
||
|
|
def is_mounted(self):
|
||
|
|
target = os.fsencode(self.mount)
|
||
|
|
for line in Path("/proc/self/mountinfo").read_bytes().splitlines():
|
||
|
|
fields = line.split(b" - ", 1)[0].split()
|
||
|
|
if len(fields) >= 5 and mountinfo_path(fields[4]) == target:
|
||
|
|
return True
|
||
|
|
return False
|
||
|
|
|
||
|
|
def force_unmount(self):
|
||
|
|
if not self.is_mounted():
|
||
|
|
return
|
||
|
|
result = command(["fusermount3", "-u", "-z", self.mount], check=False)
|
||
|
|
require(result.returncode == 0 or not self.is_mounted(), result.stderr.decode(errors="replace"))
|
||
|
|
|
||
|
|
def shutdown(self):
|
||
|
|
self.force_unmount()
|
||
|
|
self.stop_router()
|
||
|
|
require(not self.is_mounted(), "owned FUSE mount remains")
|
||
|
|
|
||
|
|
def close(self):
|
||
|
|
if self.closed:
|
||
|
|
return
|
||
|
|
try:
|
||
|
|
self.shutdown()
|
||
|
|
shutil.rmtree(self.root)
|
||
|
|
finally:
|
||
|
|
self.closed = not self.root.exists() and not self.is_mounted()
|
||
|
|
|
||
|
|
def __enter__(self):
|
||
|
|
return self
|
||
|
|
|
||
|
|
def __exit__(self, exc_type, exc, traceback):
|
||
|
|
self.shutdown()
|
||
|
|
|
||
|
|
|
||
|
|
def mountinfo_path(value):
|
||
|
|
return value.replace(b"\\040", b" ").replace(b"\\011", b"\t").replace(b"\\012", b"\n").replace(b"\\134", b"\\")
|
||
|
|
|
||
|
|
|
||
|
|
def write(path, data):
|
||
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
path.write_bytes(data)
|
||
|
|
|
||
|
|
|
||
|
|
def commit(fixture, relative="tracked", data=b"tracked"):
|
||
|
|
write(fixture.host / relative, data)
|
||
|
|
command(["git", "add", "--", relative], fixture.host)
|
||
|
|
command(["git", "commit", "--quiet", "-m", relative], fixture.host)
|
||
|
|
|
||
|
|
|
||
|
|
def ignored_pair(fixture, relative="cache/item", host=b"host", isolated=b"isolated"):
|
||
|
|
write(fixture.host / ".gitignore", (relative.split("/", 1)[0] + "/\n").encode())
|
||
|
|
write(fixture.host / relative, host)
|
||
|
|
write(fixture.isolated / relative, isolated)
|
||
|
|
|
||
|
|
|
||
|
|
def assert_pair_unchanged(fixture, relative, host, isolated):
|
||
|
|
require((fixture.host / relative).read_bytes() == host, "host backing changed")
|
||
|
|
require((fixture.isolated / relative).read_bytes() == isolated, "isolated backing changed")
|
||
|
|
|
||
|
|
|
||
|
|
def classification_case(case_id, fixture):
|
||
|
|
if case_id == "non-utf8-ignore-match":
|
||
|
|
write(fixture.host / ".gitignore", b"*.obj\n")
|
||
|
|
host_path = os.fsencode(fixture.host) + b"/\xff\xfe.obj"
|
||
|
|
with open(host_path, "wb") as output:
|
||
|
|
output.write(b"host-sentinel")
|
||
|
|
path = os.fsencode(fixture.mount) + b"/\xff\xfe.obj"
|
||
|
|
with fixture.start():
|
||
|
|
require(not os.path.exists(path), "non-UTF8 host sentinel was exposed")
|
||
|
|
descriptor = os.open(path, os.O_CREAT | os.O_WRONLY, 0o600)
|
||
|
|
os.write(descriptor, b"isolated")
|
||
|
|
os.close(descriptor)
|
||
|
|
require(os.path.exists(os.fsencode(fixture.isolated) + b"/\xff\xfe.obj"), "non-UTF8 path was not isolated")
|
||
|
|
require(Path(os.fsdecode(host_path)).read_bytes() == b"host-sentinel", "non-UTF8 host sentinel changed")
|
||
|
|
return
|
||
|
|
if case_id == "root-negation-precedence":
|
||
|
|
write(fixture.host / ".gitignore", b"*.log\n!important.log\n")
|
||
|
|
with fixture.start():
|
||
|
|
write(fixture.mount / "ordinary.log", b"isolated")
|
||
|
|
write(fixture.mount / "important.log", b"host")
|
||
|
|
require((fixture.isolated / "ordinary.log").read_bytes() == b"isolated", "root ignore did not route")
|
||
|
|
require((fixture.host / "important.log").read_bytes() == b"host", "negation did not restore host route")
|
||
|
|
return
|
||
|
|
if case_id == "nested-ignore-precedence":
|
||
|
|
write(fixture.host / ".gitignore", b"*.tmp\n!nested/drop.tmp\n")
|
||
|
|
write(fixture.host / "nested/.gitignore", b"drop.tmp\n!keep.tmp\n")
|
||
|
|
with fixture.start():
|
||
|
|
write(fixture.mount / "nested/drop.tmp", b"isolated")
|
||
|
|
write(fixture.mount / "nested/keep.tmp", b"host")
|
||
|
|
require((fixture.isolated / "nested/drop.tmp").is_file(), "nested ignored path not isolated")
|
||
|
|
require((fixture.host / "nested/keep.tmp").is_file(), "nested negation not host-backed")
|
||
|
|
return
|
||
|
|
if case_id == "info-exclude":
|
||
|
|
write(fixture.host / ".git/info/exclude", b"local.secret\n")
|
||
|
|
write(fixture.host / "local.secret", b"host-sentinel")
|
||
|
|
with fixture.start():
|
||
|
|
require(not (fixture.mount / "local.secret").exists(), "info/exclude host sentinel was exposed")
|
||
|
|
return
|
||
|
|
if case_id == "global-excludes":
|
||
|
|
global_file = fixture.root / "global-ignore"
|
||
|
|
write(global_file, b"*.swap\n")
|
||
|
|
fixture.global_excludes = global_file
|
||
|
|
write(fixture.host / "editor.swap", b"host-sentinel")
|
||
|
|
with fixture.start():
|
||
|
|
require(not (fixture.mount / "editor.swap").exists(), "global exclude host sentinel was exposed")
|
||
|
|
return
|
||
|
|
if case_id == "ignore-case-folding":
|
||
|
|
command(["git", "config", "core.ignoreCase", "true"], fixture.host)
|
||
|
|
write(fixture.host / ".gitignore", b"build.log\n")
|
||
|
|
write(fixture.host / "BUILD.LOG", b"host-sentinel")
|
||
|
|
with fixture.start():
|
||
|
|
require(not (fixture.mount / "BUILD.LOG").exists(), "case-folded host sentinel was exposed")
|
||
|
|
return
|
||
|
|
if case_id == "nested-repository-boundary":
|
||
|
|
write(fixture.host / ".gitignore", b"*.cache\n")
|
||
|
|
nested = fixture.host / "vendor/nested"
|
||
|
|
nested.mkdir(parents=True)
|
||
|
|
command(["git", "init", "--quiet"], nested)
|
||
|
|
write(nested / ".gitignore", b"target/\n")
|
||
|
|
write(nested / "target/sentinel", b"host-sentinel")
|
||
|
|
with fixture.start():
|
||
|
|
require(not (fixture.mount / "vendor/nested/target/sentinel").exists(), "nested host sentinel was exposed")
|
||
|
|
write(fixture.mount / "vendor/nested/target/item", b"isolated")
|
||
|
|
write(fixture.mount / "vendor/nested/ordinary.cache", b"host")
|
||
|
|
require((fixture.isolated / "vendor/nested/target/item").is_file(), "nested policy not selected")
|
||
|
|
require((fixture.host / "vendor/nested/ordinary.cache").is_file(), "parent policy crossed repository boundary")
|
||
|
|
return
|
||
|
|
if case_id == "startup-sealed-ignore-mutation":
|
||
|
|
write(fixture.host / ".gitignore", b"different/\n")
|
||
|
|
write(fixture.host / "cache/item", b"host")
|
||
|
|
with fixture.start():
|
||
|
|
require((fixture.mount / "cache/item").read_bytes() == b"host", "initial host route missing")
|
||
|
|
write(fixture.host / ".gitignore", b"cache/\n")
|
||
|
|
require((fixture.mount / "cache/item").read_bytes() == b"host", "live ignore mutation changed the sealed route")
|
||
|
|
require((fixture.host / "cache/item").read_bytes() == b"host", "publication copied or removed host object")
|
||
|
|
return
|
||
|
|
raise AssertionError(f"classification case is not implemented: {case_id}")
|
||
|
|
|
||
|
|
|
||
|
|
def atomic_case(case_id, fixture):
|
||
|
|
if case_id == "cross-route-directory-exchange":
|
||
|
|
write(fixture.host / ".gitignore", b"ignored/\n")
|
||
|
|
(fixture.host / "source").mkdir()
|
||
|
|
write(fixture.host / "source/marker", b"host")
|
||
|
|
(fixture.isolated / "ignored/destination").mkdir(parents=True)
|
||
|
|
write(fixture.isolated / "ignored/destination/marker", b"isolated")
|
||
|
|
else:
|
||
|
|
write(fixture.host / ".gitignore", b"ignored/\n")
|
||
|
|
write(fixture.host / "source", b"source")
|
||
|
|
if case_id in {"cross-route-file-rename", "cross-route-hard-link"}:
|
||
|
|
(fixture.isolated / "ignored").mkdir()
|
||
|
|
with fixture.start():
|
||
|
|
if case_id == "cross-route-hard-link":
|
||
|
|
expect_errno(errno.EXDEV, lambda: os.link(fixture.mount / "source", fixture.mount / "ignored/destination"))
|
||
|
|
elif case_id == "cross-route-directory-exchange":
|
||
|
|
rename_exchange(fixture.mount / "source", fixture.mount / "ignored/destination")
|
||
|
|
else:
|
||
|
|
expect_errno(errno.EXDEV, lambda: os.rename(fixture.mount / "source", fixture.mount / "ignored/destination"))
|
||
|
|
if case_id == "cross-route-directory-exchange":
|
||
|
|
require((fixture.host / "source/marker").read_bytes() == b"host", "source directory mutated after EXDEV")
|
||
|
|
require((fixture.isolated / "ignored/destination/marker").read_bytes() == b"isolated", "destination directory mutated after EXDEV")
|
||
|
|
else:
|
||
|
|
require((fixture.host / "source").read_bytes() == b"source", "source mutated after EXDEV")
|
||
|
|
require(not (fixture.host / "ignored/destination").exists() and not (fixture.isolated / "ignored/destination").exists(), "destination appeared after EXDEV")
|
||
|
|
|
||
|
|
|
||
|
|
def migration_case(case_id, fixture):
|
||
|
|
del case_id
|
||
|
|
write(fixture.host / ".gitignore", b"/target/\n")
|
||
|
|
source = fixture.mount / "targetAbCd12"
|
||
|
|
destination = fixture.mount / "target"
|
||
|
|
payload = b"Signature: 8a477f597d28d172789f06886806bc55\n"
|
||
|
|
with fixture.start():
|
||
|
|
source.mkdir()
|
||
|
|
write(source / "CACHEDIR.TAG", payload)
|
||
|
|
os.rename(source, destination)
|
||
|
|
require((destination / "CACHEDIR.TAG").read_bytes() == payload, "published target differs")
|
||
|
|
expect_errno(errno.EXDEV, lambda: os.rename(destination, fixture.mount / "exported-target"))
|
||
|
|
require(not (fixture.host / "targetAbCd12").exists(), "host source remained after migration")
|
||
|
|
require(not (fixture.host / "target").exists(), "isolated target was exported to host")
|
||
|
|
require((fixture.isolated / "target/CACHEDIR.TAG").read_bytes() == payload, "isolated target missing")
|
||
|
|
intents = fixture.isolated / ".lazaret-ignored-workspace-transactions"
|
||
|
|
require(not list(intents.glob("*.json")), "completed migration retained intent")
|
||
|
|
|
||
|
|
|
||
|
|
def rename_exchange(source, destination):
|
||
|
|
observed = exchange_paths(source, destination)
|
||
|
|
if observed == 0:
|
||
|
|
raise AssertionError("cross-route RENAME_EXCHANGE succeeded")
|
||
|
|
require(observed == errno.EXDEV, f"expected EXDEV from RENAME_EXCHANGE, observed {observed}")
|
||
|
|
|
||
|
|
|
||
|
|
def exchange_paths(source, destination):
|
||
|
|
libc = ctypes.CDLL(None, use_errno=True)
|
||
|
|
result = libc.renameat2(-100, os.fsencode(source), -100, os.fsencode(destination), 2)
|
||
|
|
if result == 0:
|
||
|
|
return 0
|
||
|
|
return ctypes.get_errno()
|
||
|
|
|
||
|
|
|
||
|
|
def copy_file_range(source, destination, length):
|
||
|
|
libc = ctypes.CDLL(None, use_errno=True)
|
||
|
|
syscall = libc.copy_file_range
|
||
|
|
syscall.argtypes = [
|
||
|
|
ctypes.c_int,
|
||
|
|
ctypes.POINTER(ctypes.c_longlong),
|
||
|
|
ctypes.c_int,
|
||
|
|
ctypes.POINTER(ctypes.c_longlong),
|
||
|
|
ctypes.c_size_t,
|
||
|
|
ctypes.c_uint,
|
||
|
|
]
|
||
|
|
syscall.restype = ctypes.c_ssize_t
|
||
|
|
copied = 0
|
||
|
|
while copied < length:
|
||
|
|
result = syscall(source, None, destination, None, length - copied, 0)
|
||
|
|
if result < 0:
|
||
|
|
observed = ctypes.get_errno()
|
||
|
|
raise OSError(observed, os.strerror(observed))
|
||
|
|
if result == 0:
|
||
|
|
break
|
||
|
|
copied += result
|
||
|
|
return copied
|
||
|
|
|
||
|
|
|
||
|
|
def policy_transition_case(case_id, fixture):
|
||
|
|
ignored_pair(fixture)
|
||
|
|
if case_id == "non-ignored-becomes-ignored":
|
||
|
|
write(fixture.host / ".gitignore", b"different/\n")
|
||
|
|
with fixture.start():
|
||
|
|
if case_id == "ignored-becomes-non-ignored":
|
||
|
|
require((fixture.mount / "cache/item").read_bytes() == b"isolated", "initial isolated route missing")
|
||
|
|
else:
|
||
|
|
require((fixture.mount / "cache/item").read_bytes() == b"host", "initial host route missing")
|
||
|
|
write(
|
||
|
|
fixture.host / ".gitignore",
|
||
|
|
b"different/\n" if case_id == "ignored-becomes-non-ignored" else b"cache/\n",
|
||
|
|
)
|
||
|
|
with fixture.start():
|
||
|
|
if case_id == "ignored-becomes-non-ignored":
|
||
|
|
require((fixture.mount / "cache/item").read_bytes() == b"host", "host route not selected after restart")
|
||
|
|
require(not (fixture.isolated / "cache/item").exists(), "released isolated object was retained")
|
||
|
|
else:
|
||
|
|
require((fixture.mount / "cache/item").read_bytes() == b"host", "host clone not selected after restart")
|
||
|
|
require((fixture.isolated / "cache/item").read_bytes() == b"host", "host object was not cloned")
|
||
|
|
if case_id == "ignored-becomes-non-ignored":
|
||
|
|
require((fixture.host / "cache/item").read_bytes() == b"host", "transition changed the host")
|
||
|
|
else:
|
||
|
|
require((fixture.host / "cache/item").read_bytes() == b"host", "clone changed the host")
|
||
|
|
|
||
|
|
|
||
|
|
def index_case(case_id, fixture):
|
||
|
|
ignored_pair(fixture, "deps/lib")
|
||
|
|
with fixture.start():
|
||
|
|
if case_id == "guest-force-add-isolated-only":
|
||
|
|
(fixture.host / "deps/lib").unlink()
|
||
|
|
initial_index = (fixture.host / ".git/index").read_bytes() if (fixture.host / ".git/index").exists() else None
|
||
|
|
result = command(["git", "add", "-f", "--", "deps/lib"], fixture.mount, check=False)
|
||
|
|
require(result.returncode != 0, "isolated-only force-add succeeded")
|
||
|
|
final_index = (fixture.host / ".git/index").read_bytes() if (fixture.host / ".git/index").exists() else None
|
||
|
|
require(final_index == initial_index, "refused force-add published an index")
|
||
|
|
require((fixture.isolated / "deps/lib").read_bytes() == b"isolated", "isolated object was lost")
|
||
|
|
require(not (fixture.host / "deps/lib").exists(), "refused force-add created a host object")
|
||
|
|
elif case_id == "host-established-tracked-cutover":
|
||
|
|
(fixture.host / "deps/lib").unlink()
|
||
|
|
write(fixture.host / "deps/lib", b"independent-host")
|
||
|
|
command(["git", "add", "-f", "--", "deps/lib"], fixture.host)
|
||
|
|
require((fixture.mount / "deps/lib").read_bytes() == b"isolated", "host index mutation changed the sealed route")
|
||
|
|
else:
|
||
|
|
(fixture.host / "deps/lib").unlink()
|
||
|
|
command(["git", "update-index", "--add", "--cacheinfo", "100644", "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", "deps/lib"], fixture.host)
|
||
|
|
require((fixture.mount / "deps/lib").read_bytes() == b"isolated", "host index mutation changed the sealed route")
|
||
|
|
if case_id == "host-established-tracked-cutover":
|
||
|
|
with fixture.start():
|
||
|
|
require((fixture.mount / "deps/lib").read_bytes() == b"independent-host", "restart did not select host object")
|
||
|
|
require(not (fixture.isolated / "deps/lib").exists(), "restart retained released isolated object")
|
||
|
|
elif case_id == "unmediated-inconsistent-index":
|
||
|
|
with fixture.start():
|
||
|
|
require(not (fixture.mount / "deps/lib").exists(), "tracked path without a host object did not become absent")
|
||
|
|
require(not (fixture.isolated / "deps/lib").exists(), "released isolated object was retained")
|
||
|
|
|
||
|
|
|
||
|
|
def open_handle_case(case_id, fixture):
|
||
|
|
ignored_pair(fixture, "cache/pinned", host=b"host-data", isolated=b"isolated-data")
|
||
|
|
with fixture.start():
|
||
|
|
if case_id == "mmap-policy-transition":
|
||
|
|
descriptor = os.open(fixture.mount / "cache/pinned", os.O_RDWR)
|
||
|
|
mapping = mmap.mmap(descriptor, 0, access=mmap.ACCESS_WRITE)
|
||
|
|
write(fixture.host / ".gitignore", b"different/\n")
|
||
|
|
mapping[0:8] = b"mapped!!"
|
||
|
|
mapping.flush()
|
||
|
|
mapping.close()
|
||
|
|
os.close(descriptor)
|
||
|
|
require((fixture.isolated / "cache/pinned").read_bytes().startswith(b"mapped!!"), "mapping rebound")
|
||
|
|
require((fixture.host / "cache/pinned").read_bytes() == b"host-data", "mapping copied back")
|
||
|
|
elif case_id == "directory-stream-policy-snapshot":
|
||
|
|
write(fixture.isolated / "cache/isolated-child", b"isolated")
|
||
|
|
write(fixture.host / "cache/host-child", b"host")
|
||
|
|
iterator = os.scandir(fixture.mount / "cache")
|
||
|
|
write(fixture.host / ".gitignore", b"different/\n")
|
||
|
|
names = {entry.name for entry in iterator}
|
||
|
|
iterator.close()
|
||
|
|
require("isolated-child" in names and "host-child" not in names, "old stream mixed snapshots")
|
||
|
|
new_names = {entry.name for entry in os.scandir(fixture.mount / "cache")}
|
||
|
|
require("isolated-child" in new_names and "host-child" not in new_names, "new stream changed the sealed route")
|
||
|
|
else:
|
||
|
|
descriptor = os.open(fixture.mount / "cache/pinned", os.O_RDWR)
|
||
|
|
fcntl.lockf(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB, 1)
|
||
|
|
write(fixture.host / ".gitignore", b"different/\n")
|
||
|
|
new_descriptor = os.open(fixture.mount / "cache/pinned", os.O_RDWR)
|
||
|
|
require(lock_attempt(fixture.isolated / "cache/pinned") == errno.EAGAIN, "old isolated lock was transferred or lost")
|
||
|
|
require(lock_attempt(fixture.host / "cache/pinned") == 0, "host lock domain was not independent")
|
||
|
|
os.close(new_descriptor)
|
||
|
|
os.close(descriptor)
|
||
|
|
require(lock_attempt(fixture.isolated / "cache/pinned") == 0, "isolated lock remained after handles closed")
|
||
|
|
|
||
|
|
|
||
|
|
def lock_attempt(path):
|
||
|
|
source = """
|
||
|
|
import fcntl, os, sys
|
||
|
|
descriptor = os.open(sys.argv[1], os.O_RDWR)
|
||
|
|
try:
|
||
|
|
fcntl.lockf(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB, 1)
|
||
|
|
except OSError as error:
|
||
|
|
sys.exit(error.errno)
|
||
|
|
fcntl.lockf(descriptor, fcntl.LOCK_UN, 1)
|
||
|
|
"""
|
||
|
|
return subprocess.run([sys.executable, "-c", source, path], check=False).returncode
|
||
|
|
|
||
|
|
|
||
|
|
def operation_case(case_id, fixture):
|
||
|
|
ignored_pair(fixture, "cache/base")
|
||
|
|
if case_id == "timestamps":
|
||
|
|
write(fixture.host / ".gitignore", b"different/\n")
|
||
|
|
with fixture.start():
|
||
|
|
if case_id == "extended-attributes-and-acls":
|
||
|
|
path = fixture.mount / "cache/base"
|
||
|
|
os.setxattr(path, b"user.lazaret", b"value")
|
||
|
|
require(os.getxattr(path, b"user.lazaret") == b"value", "xattr round trip failed")
|
||
|
|
require(os.getxattr(fixture.isolated / "cache/base", b"user.lazaret") == b"value", "xattr reached wrong backing")
|
||
|
|
os.removexattr(path, b"user.lazaret")
|
||
|
|
expect_errno(errno.ENODATA, lambda: os.getxattr(fixture.isolated / "cache/base", b"user.lazaret"))
|
||
|
|
if shutil.which("setfacl"):
|
||
|
|
command(["setfacl", "-m", f"u:{os.getuid()}:rw", path])
|
||
|
|
mounted_acl = command(["getfacl", "--absolute-names", path]).stdout
|
||
|
|
backing_acl = command(["getfacl", "--absolute-names", fixture.isolated / "cache/base"]).stdout
|
||
|
|
require(b"user:" in mounted_acl and b"user:" in backing_acl, "POSIX ACL was not retained on selected backing")
|
||
|
|
elif case_id == "sparse-copy-range":
|
||
|
|
source = fixture.mount / "cache/source"
|
||
|
|
destination = fixture.mount / "cache/destination"
|
||
|
|
with source.open("wb") as output:
|
||
|
|
output.write(b"x" * 8192)
|
||
|
|
output.seek(1024 * 1024)
|
||
|
|
output.write(b"tail")
|
||
|
|
descriptor = os.open(source, os.O_RDONLY)
|
||
|
|
require(os.lseek(descriptor, 0, os.SEEK_DATA) == 0, "SEEK_DATA did not find first extent")
|
||
|
|
require(os.lseek(descriptor, 0, os.SEEK_HOLE) >= 8192, "SEEK_HOLE did not find sparse gap")
|
||
|
|
os.close(descriptor)
|
||
|
|
command(["fallocate", "--punch-hole", "--keep-size", "--offset", "0", "--length", "4096", source])
|
||
|
|
require(source.read_bytes()[:4096] == bytes(4096), "hole punch retained data")
|
||
|
|
with source.open("rb") as input_file, destination.open("wb") as output:
|
||
|
|
require(
|
||
|
|
copy_file_range(input_file.fileno(), output.fileno(), 1024 * 1024 + 4)
|
||
|
|
== 1024 * 1024 + 4,
|
||
|
|
"same-route copy range ended early",
|
||
|
|
)
|
||
|
|
require(input_file.tell() == 1024 * 1024 + 4, "same-route source offset differs")
|
||
|
|
require(output.tell() == 1024 * 1024 + 4, "same-route destination offset differs")
|
||
|
|
require(destination.read_bytes() == source.read_bytes(), "same-route copy differs")
|
||
|
|
require(destination.stat().st_size == source.stat().st_size, "same-route copy size differs")
|
||
|
|
write(fixture.host / "host-copy", b"unchanged")
|
||
|
|
write(fixture.host / "unrelated-host", b"host-sentinel")
|
||
|
|
write(fixture.isolated / "cache/unrelated-isolated", b"isolated-sentinel")
|
||
|
|
source_before = (fixture.isolated / "cache/source").read_bytes()
|
||
|
|
same_route_destination_before = (fixture.isolated / "cache/destination").read_bytes()
|
||
|
|
with (fixture.mount / "host-copy").open("r+b") as output, source.open("rb") as input_file:
|
||
|
|
write(fixture.host / ".gitignore", b"different/\n")
|
||
|
|
os.lseek(input_file.fileno(), 4096, os.SEEK_SET)
|
||
|
|
require(copy_file_range(input_file.fileno(), output.fileno(), 1) == 1, "cross-route copy count differs")
|
||
|
|
require((fixture.host / "host-copy").read_bytes() == b"xnchanged", "cross-route destination range differs")
|
||
|
|
require((fixture.isolated / "cache/source").read_bytes() == source_before, "cross-route copy mutated source")
|
||
|
|
require(
|
||
|
|
(fixture.isolated / "cache/destination").read_bytes() == same_route_destination_before,
|
||
|
|
"cross-route copy mutated same-route destination",
|
||
|
|
)
|
||
|
|
require((fixture.host / "cache/source").exists() is False, "cross-route source handle was reclassified")
|
||
|
|
require((fixture.host / "unrelated-host").read_bytes() == b"host-sentinel", "cross-route copy mutated unrelated host object")
|
||
|
|
require(
|
||
|
|
(fixture.isolated / "cache/unrelated-isolated").read_bytes() == b"isolated-sentinel",
|
||
|
|
"cross-route copy mutated unrelated isolated object",
|
||
|
|
)
|
||
|
|
elif case_id == "symlink-confinement":
|
||
|
|
os.symlink("../../outside", fixture.mount / "cache/link")
|
||
|
|
require(os.readlink(fixture.mount / "cache/link") == "../../outside", "symlink target changed")
|
||
|
|
write(fixture.host / "virtual-target", b"virtual-resolution")
|
||
|
|
os.symlink("../virtual-target", fixture.mount / "cache/virtual-link")
|
||
|
|
require(
|
||
|
|
(fixture.mount / "cache/virtual-link").read_bytes() == b"virtual-resolution",
|
||
|
|
"virtual symlink resolution changed",
|
||
|
|
)
|
||
|
|
command(
|
||
|
|
[
|
||
|
|
"cargo",
|
||
|
|
"nextest",
|
||
|
|
"run",
|
||
|
|
"--offline",
|
||
|
|
"--locked",
|
||
|
|
"-p",
|
||
|
|
"lazaret-ignored-workspace",
|
||
|
|
"--test-threads",
|
||
|
|
"2",
|
||
|
|
"-E",
|
||
|
|
"test(descriptor_traversal_race_never_follows_backing_symlinks)",
|
||
|
|
],
|
||
|
|
ROOT,
|
||
|
|
)
|
||
|
|
elif case_id == "timestamps":
|
||
|
|
path = fixture.mount / "cache/base"
|
||
|
|
stamp = 1_700_000_000_123_456_789
|
||
|
|
os.utime(path, ns=(stamp, stamp), follow_symlinks=False)
|
||
|
|
metadata = os.stat(path, follow_symlinks=False)
|
||
|
|
require(metadata.st_mtime_ns == stamp, "nanosecond timestamp changed")
|
||
|
|
require(os.stat(fixture.host / "cache/base", follow_symlinks=False).st_mtime_ns == stamp, "timestamp was not bidirectional")
|
||
|
|
else:
|
||
|
|
fifo = fixture.mount / "cache/fifo"
|
||
|
|
os.mkfifo(fifo)
|
||
|
|
with socket.socket(socket.AF_UNIX) as endpoint:
|
||
|
|
original_directory = os.open(".", os.O_RDONLY | os.O_DIRECTORY)
|
||
|
|
try:
|
||
|
|
os.chdir(fixture.mount)
|
||
|
|
endpoint.bind("cache/socket")
|
||
|
|
finally:
|
||
|
|
os.fchdir(original_directory)
|
||
|
|
os.close(original_directory)
|
||
|
|
require(stat.S_ISFIFO(os.lstat(fixture.isolated / "cache/fifo").st_mode), "FIFO did not reach isolated backing")
|
||
|
|
require(stat.S_ISSOCK(os.lstat(fixture.isolated / "cache/socket").st_mode), "socket did not reach isolated backing")
|
||
|
|
expect_errno(errno.EPERM, lambda: os.mknod(fixture.mount / "cache/device", stat.S_IFCHR | 0o600, os.makedev(1, 3)))
|
||
|
|
|
||
|
|
|
||
|
|
def git_case(case_id, fixture):
|
||
|
|
commit(fixture)
|
||
|
|
initial_branch = command(["git", "symbolic-ref", "--short", "HEAD"], fixture.host).stdout.strip()
|
||
|
|
ignored_pair(fixture, "cache/sentinel")
|
||
|
|
with fixture.start():
|
||
|
|
if case_id == "git-ordinary-workflow":
|
||
|
|
write(fixture.mount / "ordinary", b"ordinary")
|
||
|
|
command(["git", "status", "--porcelain=v2"], fixture.mount)
|
||
|
|
command(["git", "add", "ordinary"], fixture.mount)
|
||
|
|
command(["git", "commit", "--quiet", "-m", "ordinary"], fixture.mount)
|
||
|
|
write(fixture.mount / "tracked", b"changed")
|
||
|
|
command(["git", "restore", "tracked"], fixture.mount)
|
||
|
|
require((fixture.host / "ordinary").read_bytes() == b"ordinary", "ordinary commit was not bidirectional")
|
||
|
|
require((fixture.host / "tracked").read_bytes() == b"tracked", "restore did not reach host worktree")
|
||
|
|
elif case_id == "git-history-workflow":
|
||
|
|
command(["git", "checkout", "-b", "topic"], fixture.mount)
|
||
|
|
write(fixture.mount / "topic-file", b"topic")
|
||
|
|
command(["git", "add", "topic-file"], fixture.mount)
|
||
|
|
command(["git", "commit", "--quiet", "-m", "topic"], fixture.mount)
|
||
|
|
command(["git", "checkout", initial_branch], fixture.mount)
|
||
|
|
write(fixture.mount / "base-file", b"base")
|
||
|
|
command(["git", "add", "base-file"], fixture.mount)
|
||
|
|
command(["git", "commit", "--quiet", "-m", "base"], fixture.mount)
|
||
|
|
command(["git", "checkout", "topic"], fixture.mount)
|
||
|
|
command(["git", "rebase", initial_branch], fixture.mount)
|
||
|
|
command(["git", "checkout", initial_branch], fixture.mount)
|
||
|
|
command(["git", "merge", "topic"], fixture.mount)
|
||
|
|
write(fixture.mount / "tracked", b"stash")
|
||
|
|
command(["git", "stash"], fixture.mount)
|
||
|
|
command(["git", "stash", "pop"], fixture.mount)
|
||
|
|
command(["git", "restore", "tracked"], fixture.mount)
|
||
|
|
require(command(["git", "rev-parse", "topic"], fixture.mount).stdout == command(["git", "rev-parse", "topic"], fixture.host).stdout, "reference update was not bidirectional")
|
||
|
|
else:
|
||
|
|
raise AssertionError("linked-worktree and Sentry cases use dedicated harnesses")
|
||
|
|
assert_pair_unchanged(fixture, "cache/sentinel", b"host", b"isolated")
|
||
|
|
|
||
|
|
|
||
|
|
def linked_worktree_case(case_id, fixture):
|
||
|
|
del case_id
|
||
|
|
shutil.rmtree(fixture.host)
|
||
|
|
base = fixture.root / "base"
|
||
|
|
base.mkdir()
|
||
|
|
command(["git", "init", "--quiet"], base)
|
||
|
|
command(["git", "config", "user.email", "[email protected]"], base)
|
||
|
|
command(["git", "config", "user.name", "Fixture"], base)
|
||
|
|
write(base / "tracked", b"tracked")
|
||
|
|
command(["git", "add", "tracked"], base)
|
||
|
|
command(["git", "commit", "--quiet", "-m", "base"], base)
|
||
|
|
command(["git", "worktree", "add", "--quiet", fixture.host], base)
|
||
|
|
write(fixture.host / ".gitignore", b"target/\n")
|
||
|
|
write(fixture.host / "target/item", b"host-sentinel")
|
||
|
|
with fixture.start():
|
||
|
|
require(not (fixture.mount / "target/item").exists(), "linked-worktree host sentinel was exposed")
|
||
|
|
write(fixture.mount / "target/item", b"isolated")
|
||
|
|
result = command(["git", "status", "--porcelain=v2", "--untracked-files=all"], fixture.mount)
|
||
|
|
require(b"target/item" not in result.stdout, "linked-worktree ignore policy was not applied")
|
||
|
|
require((fixture.isolated / "target/item").read_bytes() == b"isolated", "linked-worktree isolated state missing")
|
||
|
|
require((fixture.host / "target/item").read_bytes() == b"host-sentinel", "linked-worktree host sentinel changed")
|
||
|
|
|
||
|
|
|
||
|
|
def sentry_case(case_id, fixture, sentry_binary):
|
||
|
|
del case_id
|
||
|
|
commit(fixture)
|
||
|
|
transient = fixture.root / "transient-hooks"
|
||
|
|
transient.mkdir()
|
||
|
|
transient_config = fixture.root / "transient-config"
|
||
|
|
transient_config.touch()
|
||
|
|
write(fixture.host / ".git/hooks/host-sentinel", b"host")
|
||
|
|
host_config = (fixture.host / ".git/config").read_bytes()
|
||
|
|
with fixture.start():
|
||
|
|
process = subprocess.Popen(
|
||
|
|
git_sentry_command(
|
||
|
|
sentry_binary,
|
||
|
|
fixture.mount,
|
||
|
|
fixture.host / ".git",
|
||
|
|
transient,
|
||
|
|
transient_config,
|
||
|
|
),
|
||
|
|
stdin=subprocess.PIPE,
|
||
|
|
stdout=subprocess.PIPE,
|
||
|
|
stderr=subprocess.PIPE,
|
||
|
|
)
|
||
|
|
try:
|
||
|
|
require(process.stdout.readline() == b"ready\n", "Git Sentry did not become ready")
|
||
|
|
expect_errno(errno.EROFS, lambda: write(fixture.mount / ".git/config", b"changed"))
|
||
|
|
require(not (fixture.mount / ".git/hooks/host-sentinel").exists(), "host hook remained visible")
|
||
|
|
write(fixture.mount / ".git/hooks/sandbox-hook", b"transient")
|
||
|
|
command(["git", "status", "--porcelain=v2"], fixture.mount)
|
||
|
|
finally:
|
||
|
|
process.stdin.close()
|
||
|
|
return_code = process.wait(timeout=10)
|
||
|
|
require(return_code == 0, process.stderr.read().decode(errors="replace"))
|
||
|
|
require((fixture.mount / ".git/config").read_bytes() == host_config, "host config was not restored")
|
||
|
|
require((fixture.mount / ".git/hooks/host-sentinel").read_bytes() == b"host", "host hooks were not restored")
|
||
|
|
require((fixture.host / ".git/hooks/host-sentinel").read_bytes() == b"host", "host hook changed")
|
||
|
|
require((transient / "sandbox-hook").read_bytes() == b"transient", "transient hook missing")
|
||
|
|
|
||
|
|
|
||
|
|
def git_sentry_command(binary, workspace, common, hooks, config):
|
||
|
|
return [binary, workspace, common, hooks, config]
|
||
|
|
|
||
|
|
|
||
|
|
def invalidation_or_race_case(case_id, fixture):
|
||
|
|
ignored_pair(fixture)
|
||
|
|
with fixture.start():
|
||
|
|
if case_id == "concurrent-host-sandbox-mutation":
|
||
|
|
barrier = threading.Barrier(2)
|
||
|
|
errors = []
|
||
|
|
def host_replace():
|
||
|
|
try:
|
||
|
|
barrier.wait()
|
||
|
|
write(fixture.host / "ordinary.tmp", b"replacement")
|
||
|
|
os.replace(fixture.host / "ordinary.tmp", fixture.host / "ordinary")
|
||
|
|
except BaseException as error:
|
||
|
|
errors.append(error)
|
||
|
|
write(fixture.host / "ordinary", b"initial")
|
||
|
|
require((fixture.mount / "ordinary").read_bytes() == b"initial", "initial host bytes differ")
|
||
|
|
write(fixture.host / "ordinary", b"changed")
|
||
|
|
require((fixture.mount / "ordinary").read_bytes() == b"changed", "same-size host overwrite was stale")
|
||
|
|
thread = threading.Thread(target=host_replace)
|
||
|
|
thread.start()
|
||
|
|
barrier.wait()
|
||
|
|
with contextlib.suppress(FileNotFoundError):
|
||
|
|
os.replace(fixture.mount / "ordinary", fixture.mount / "ordinary-renamed")
|
||
|
|
thread.join()
|
||
|
|
require(not errors, f"host racer failed: {errors}")
|
||
|
|
names = [entry.name for entry in os.scandir(fixture.mount)]
|
||
|
|
require(len(names) == len(set(names)), "concurrent enumeration duplicated a name")
|
||
|
|
candidates = [name for name in ("ordinary", "ordinary-renamed") if (fixture.mount / name).exists()]
|
||
|
|
require(candidates, "native rename race lost every host-backed outcome")
|
||
|
|
write(fixture.mount / candidates[0], b"sandbox-write")
|
||
|
|
(fixture.mount / candidates[0]).unlink()
|
||
|
|
require((fixture.isolated / "cache/item").read_bytes() == b"isolated", "race crossed backing")
|
||
|
|
else:
|
||
|
|
fixture.process.kill()
|
||
|
|
fixture.process.wait(timeout=10)
|
||
|
|
try:
|
||
|
|
(fixture.mount / "cache/item").read_bytes()
|
||
|
|
except OSError as error:
|
||
|
|
require(error.errno in {errno.ENOTCONN, errno.EIO}, f"unexpected dead-router errno: {error.errno}")
|
||
|
|
else:
|
||
|
|
raise AssertionError("dead router fell through to host backing")
|
||
|
|
fixture.force_unmount()
|
||
|
|
assert_pair_unchanged(fixture, "cache/item", b"host", b"isolated")
|
||
|
|
fixture.start()
|
||
|
|
require((fixture.mount / "cache/item").read_bytes() == b"isolated", "recovered router selected wrong backing")
|
||
|
|
|
||
|
|
|
||
|
|
CASE_GROUPS = {
|
||
|
|
**{name: classification_case for name in [
|
||
|
|
"non-utf8-ignore-match", "root-negation-precedence", "nested-ignore-precedence",
|
||
|
|
"info-exclude", "global-excludes", "ignore-case-folding", "nested-repository-boundary",
|
||
|
|
"startup-sealed-ignore-mutation",
|
||
|
|
]},
|
||
|
|
**{name: atomic_case for name in ["cross-route-file-rename", "cross-route-directory-exchange", "cross-route-hard-link"]},
|
||
|
|
"cargo-target-host-to-isolated-rename": migration_case,
|
||
|
|
**{name: policy_transition_case for name in ["ignored-becomes-non-ignored", "non-ignored-becomes-ignored"]},
|
||
|
|
**{name: index_case for name in ["guest-force-add-isolated-only", "host-established-tracked-cutover", "unmediated-inconsistent-index"]},
|
||
|
|
**{name: open_handle_case for name in ["mmap-policy-transition", "locks-policy-transition", "directory-stream-policy-snapshot"]},
|
||
|
|
**{name: operation_case for name in ["extended-attributes-and-acls", "sparse-copy-range", "symlink-confinement", "timestamps", "special-files"]},
|
||
|
|
**{name: git_case for name in ["git-ordinary-workflow", "git-history-workflow"]},
|
||
|
|
**{name: invalidation_or_race_case for name in ["concurrent-host-sandbox-mutation", "router-loss-fails-closed"]},
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def run_corpus(arguments):
|
||
|
|
inventory = json.loads((FIXTURES / "corpus.json").read_text())["cases"]
|
||
|
|
selected = [case for case in inventory if arguments.case in (None, case["id"])]
|
||
|
|
require(selected, "requested corpus case does not exist")
|
||
|
|
missing = [case["id"] for case in inventory if case["id"] not in CASE_GROUPS and case["id"] not in {"linked-worktree-index", "git-sentry"}]
|
||
|
|
require(not missing, f"corpus adapter is incomplete: {missing}")
|
||
|
|
results = []
|
||
|
|
Path(arguments.work_root).mkdir(parents=True, exist_ok=True)
|
||
|
|
for case in selected:
|
||
|
|
identifier = case["id"]
|
||
|
|
started = time.monotonic_ns()
|
||
|
|
fixture = MountedFixture(arguments.work_root, arguments.router)
|
||
|
|
try:
|
||
|
|
if identifier == "linked-worktree-index":
|
||
|
|
linked_worktree_case(identifier, fixture)
|
||
|
|
elif identifier == "git-sentry":
|
||
|
|
sentry_case(identifier, fixture, arguments.git_sentry)
|
||
|
|
else:
|
||
|
|
CASE_GROUPS[identifier](identifier, fixture)
|
||
|
|
disposition = "PASS"
|
||
|
|
detail = None
|
||
|
|
except BaseException as error:
|
||
|
|
disposition = "FAIL"
|
||
|
|
detail = repr(error)
|
||
|
|
finally:
|
||
|
|
try:
|
||
|
|
fixture.close()
|
||
|
|
except BaseException as cleanup_error:
|
||
|
|
if disposition == "PASS":
|
||
|
|
disposition = "FAIL"
|
||
|
|
detail = f"cleanup failed: {cleanup_error!r}"
|
||
|
|
else:
|
||
|
|
detail = f"{detail}; cleanup failed: {cleanup_error!r}"
|
||
|
|
results.append({"id": identifier, "disposition": disposition, "elapsed_ns": time.monotonic_ns() - started, "detail": detail})
|
||
|
|
if disposition != "PASS" and arguments.fail_fast:
|
||
|
|
break
|
||
|
|
output = {"schema": "lazaret-ignored-workspace-corpus-result-v1", "results": results}
|
||
|
|
Path(arguments.output).parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
Path(arguments.output).write_text(json.dumps(output, indent=2, sort_keys=True) + "\n")
|
||
|
|
require(all(result["disposition"] == "PASS" for result in results), "one or more corpus cases failed")
|
||
|
|
|
||
|
|
|
||
|
|
def unavailable_physical_prerequisite():
|
||
|
|
if sys.platform != "linux":
|
||
|
|
return "Linux is required"
|
||
|
|
for program in ("git", "unshare", "fusermount3"):
|
||
|
|
if shutil.which(program) is None:
|
||
|
|
return f"{program} is unavailable"
|
||
|
|
fuse = Path("/dev/fuse")
|
||
|
|
if not fuse.exists() or not os.access(fuse, os.R_OK | os.W_OK):
|
||
|
|
return "/dev/fuse is unavailable"
|
||
|
|
probe = command(
|
||
|
|
["unshare", "--user", "--map-root-user", "--mount", "--propagation", "private", "--", "true"],
|
||
|
|
check=False,
|
||
|
|
)
|
||
|
|
if probe.returncode != 0:
|
||
|
|
return "private user and mount namespaces are unavailable"
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
parser = argparse.ArgumentParser()
|
||
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||
|
|
corpus = subparsers.add_parser("corpus")
|
||
|
|
corpus.add_argument("--router", type=Path, required=True)
|
||
|
|
corpus.add_argument("--git-sentry", type=Path, required=True)
|
||
|
|
corpus.add_argument("--work-root", type=Path, required=True)
|
||
|
|
corpus.add_argument("--output", type=Path, required=True)
|
||
|
|
corpus.add_argument("--case")
|
||
|
|
corpus.add_argument("--fail-fast", action="store_true")
|
||
|
|
arguments = parser.parse_args()
|
||
|
|
if arguments.command == "corpus":
|
||
|
|
unavailable = unavailable_physical_prerequisite()
|
||
|
|
if unavailable is not None:
|
||
|
|
print(f"SKIP ignored-workspace corpus: {unavailable}", file=sys.stderr)
|
||
|
|
return 77
|
||
|
|
if arguments.case in (None, "git-sentry") and os.environ.get(NAMESPACE_MARKER) != "1":
|
||
|
|
environment = os.environ.copy()
|
||
|
|
environment[NAMESPACE_MARKER] = "1"
|
||
|
|
os.execvpe(
|
||
|
|
"unshare",
|
||
|
|
[
|
||
|
|
"unshare",
|
||
|
|
"--user",
|
||
|
|
"--map-root-user",
|
||
|
|
"--mount",
|
||
|
|
"--propagation",
|
||
|
|
"private",
|
||
|
|
"--",
|
||
|
|
sys.executable,
|
||
|
|
os.fspath(Path(__file__).resolve()),
|
||
|
|
*sys.argv[1:],
|
||
|
|
],
|
||
|
|
environment,
|
||
|
|
)
|
||
|
|
run_corpus(arguments)
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
sys.exit(main())
|