Skip to content

Stop the test suite touching the developer's machine on macOS, and fix three more macOS-only defects - #1623

Merged
geodro merged 6 commits into
mainfrom
fix/macos-test-isolation
Aug 31, 2026
Merged

Stop the test suite touching the developer's machine on macOS, and fix three more macOS-only defects#1623
geodro merged 6 commits into
mainfrom
fix/macos-test-isolation

Conversation

@geodro

@geodro geodro commented Aug 31, 2026

Copy link
Copy Markdown
Member

Records the manager a platform installs at init apart from whatever sits in UnitLifecycle, so the under-test refusal covers the platform case and not only the unset one. A stub a test assigns directly is still told apart from the real thing and still drives the lifecycle, which is what keeps the tests that do exercise it working. A full suite run now leaves the launchd unit list byte-identical, where before it left nginx pointing at a deleted temp plist.

Removing a container unit goes through StopUnit before dropping the plist, so launchd is told the job is going rather than left holding a file that is gone. Routing it through that funnel rather than a bootout written out at the removal site means it lands on the one path that already refuses a real service manager under test, and Linux keeps the behaviour it had.

A guard now sits under the launchctl helper too, for the same reason the write guard sits under writePlist: isolating HOME moves the plist but not the domain, and launchd offers no per-test domain, so a mutating verb reached from a test has nowhere safe to land and says so. Reads are left alone because several paths depend on them and none of them change anything. It earned its place immediately by catching three further leak paths the first fix did not cover.

The installer checks that need a process with no controlling terminal fall back to perl's POSIX::setsid where setsid is missing, and the pty helper orders its arguments the way the local script(1) wants, with stdin held open long enough for the BSD one to see the prompt. That suite goes from 60 to 63 of 63 on macOS.

lerd domain add and lerd domain remove now take the name with or without the TLD through one helper both call, which is what makes them agree with the TUI about what a domain argument means.

Closes #1622

geodro added 4 commits August 31, 2026 13:49
The guard that stops a test driving the real service manager asked whether podman.UnitLifecycle was nil, on the reasoning that a test which wants the lifecycle installs a stub there. On macOS the launchd manager is assigned from an init() in internal/services, so the var is never nil and the guard has never once fired: every test that reached StartUnit, StopUnit or RestartUnit drove the launchd of whoever ran the suite.

What that cost is not theoretical. A single go test ./... booted out the developer's own com.lerd.lerd-nginx job, enabled it again and re-bootstrapped it against a plist inside the test's temp directory, which the test then removed on cleanup. The job was left registered to a path that no longer exists, so the next restart of nginx had nothing to load, and a com.lerd.lerd-php81-fpm job for a version nobody installed was left behind the same way.

Isolating HOME, which the tests already do, moves the plist file but not the launchd domain it is bootstrapped into, and no per-test domain exists to move it to. So the manager a platform installs at init is now recorded apart from whatever is in UnitLifecycle, and the refusal covers both the unset case and the platform one. A stub a test assigns directly is still told apart from the real thing and still drives the lifecycle, which is what keeps the tests that do exercise it working.
Three checks in the installer suite could not pass on a Mac. Two of them reach for setsid to get a process with no controlling terminal, which is util-linux and has no macOS equivalent, so both died at command not found rather than testing anything. The third allocates a pty through script(1), which exists on both, but spelled its arguments the util-linux way, and the BSD one rejects -c outright.

Both now go through a helper that picks the form the platform accepts. Perl's POSIX::setsid stands in where setsid is missing, since perl is in the base install on macOS and on every distro lerd targets, and the pty helper puts the file and the command in whichever order the local script(1) wants. The BSD side also needs stdin held open a moment longer, or it reads EOF before the prompt it is meant to answer has been printed.

The suite now runs 63 for 63 on macOS instead of 60, which matters because these particular checks cover the path that decides whether lerd install can ask a question at all, and that path is one a Mac contributor is as likely to change as anyone.
…its plist

RemoveContainerUnit removed the plist and stopped there, and the comment above RemoveQuadlet already claimed this was what kept an orphan agent out of the LaunchAgents directory. The file went, but launchd was never told, so the job stayed registered against a path that no longer existed and went on answering launchctl list forever. That is where a com.lerd.lerd-kafka job for a service disabled long ago was still coming from.

The stop goes through StopUnit rather than a bootout written out here, so it lands on the one funnel that already knows how to refuse a real service manager when a test is driving it, and Linux keeps the behaviour it had.

A guard now sits under the launchctl helper as well, for the same reason the write guard sits under writePlist. Isolating HOME moves the plist but not the domain it is bootstrapped into, and launchd offers no per-test domain to move it to, so a mutating verb reached from a test has nowhere safe to land and says so instead of landing on the developer's own. Reads are left alone because several paths depend on them and none of them change anything.
domain add appended the TLD to whatever it was given, so a name that already ended in one came back doubled: shop.acme.test was stored as shop.acme.test.test, which nginx served under a name no resolver would ever be asked for. The help says to pass the name without the TLD, but the domain is the thing people have in mind and the thing they type, and remove had the same gap, so a mistyped add could not be undone by repeating it.

The TUI has trimmed a trailing TLD off this input since it gained the inline domain editor. The CLI now does the same, through one helper both subcommands call, which is what makes the two agree about what a domain argument means.
@geodro
geodro requested a review from a team as a code owner August 31, 2026 11:36
geodro added 2 commits August 31, 2026 15:39
util-linux script runs the command it is given through $SHELL, not through sh, so the tty check ran its payload under whatever the developer's login shell is. install.sh only skips main when BASH_SOURCE says it was sourced, and no other shell sets it, so under zsh or fish or dash the guard read as "not sourced" and sourcing the installer to get at ask ran the installer instead. The check then failed on a GOT_YES that was never printed, and the suite spent that time running install against the throwaway HOME bats had set.

The form this replaced wrapped the payload in bash -c, which is what kept it away from $SHELL, and the BSD branch still does. Pinning SHELL for the call is the same guarantee without a second layer of quoting.
@geodro
geodro merged commit 62b499f into main Aug 31, 2026
3 checks passed
@geodro
geodro deleted the fix/macos-test-isolation branch August 31, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant