Skip to content

[ci]: run the test suite on FreeBSD - #2129

Open
neilpang wants to merge 2 commits into
shirou:masterfrom
neilpang:ci/freebsd
Open

[ci]: run the test suite on FreeBSD#2129
neilpang wants to merge 2 commits into
shirou:masterfrom
neilpang:ci/freebsd

Conversation

@neilpang

@neilpang neilpang commented Aug 2, 2026

Copy link
Copy Markdown

gopsutil has FreeBSD-specific source files in every package -- cpu, disk,
host, load, mem, net, process, sensors -- but lint.yml only
cross-compiles them from a Linux runner. No gopsutil test has ever executed on
a FreeBSD kernel. Makefile says as much for the other BSDs: "tested only for
successful builds. Value testing is not performed."

That gap shipped a release. In #1898, v4.25.7 panicked in
process.ProcessesWithContext on FreeBSD, and you reproduced it on
FreeBSD-13.4 with Test_SendSignal. A job running go test ./... on a real
FreeBSD would have caught it before the tag.

You also asked for exactly this action in that issue:

using vmactions/freebsd-vm is a
great idea! I would really appreciate it if you could open a PR to add it as
a GitHub Action.

What the job needed

Three things a booted FreeBSD system has and the minimal VM image does not.
All three are handled in the workflow; no source change was required.

  • lsof -- process.TestConnections shells out to it (feat: use lsof for net_connections on FreeBSD #1551).
  • /var/run/dmesg.boot -- cpu.TestInfo reads it, and the image does not
    run /etc/rc.d/dmesg.
  • /dev/pts -- process.TestTerminal reads that directory, and devfs
    materialises it only while a pty is open. The job runs over a
    non-interactive ssh session, so it keeps one pty allocated.

Tests run with cgo enabled, which is what the # FIXME line for
CGO_ENABLED: "1", GOOS: freebsd in lint.yml cannot reach by
cross-compilation.

Verification

https://github.com/neilpang/gopsutil/actions/runs/30753529510/job/91511662686

go version go1.25.12 freebsd/amd64

ok  github.com/shirou/gopsutil/v4/cpu
ok  github.com/shirou/gopsutil/v4/disk
ok  github.com/shirou/gopsutil/v4/docker
ok  github.com/shirou/gopsutil/v4/host
ok  github.com/shirou/gopsutil/v4/internal/common
ok  github.com/shirou/gopsutil/v4/internal/common/psutiltest
ok  github.com/shirou/gopsutil/v4/load
ok  github.com/shirou/gopsutil/v4/mem
ok  github.com/shirou/gopsutil/v4/net
ok  github.com/shirou/gopsutil/v4/process
ok  github.com/shirou/gopsutil/v4/sensors

11 packages, zero failures, about 16 minutes. cache-after-prepare: true
caches the prepared image, so later runs skip pkg install entirely.

Not included: OpenBSD and NetBSD

I ran both as well. Neither is green, and neither can be fixed from the
workflow, so I left them out rather than propose a red job:

  • OpenBSD -- process.TestConcurrent panics reproducibly (twice; two
    goroutines the second time) at process_openbsd.go:406, where
    callKernProcSyscall takes &buf[0] after the sizing sysctl returned a
    zero length. The guard added in Fix panic on OpenBSD and FreeBSD systems if KinfoProc size has an unexpected size #1694 lives in getKProc, which runs after
    that point. Separately, cpu.TestTimes fails on any host with 4 or more
    CPUs, since cpu-total comes from kern.cp_time while the per-CPU path
    sums kern.cpustats, and the test's margin of 2.0 only tolerates up to 3.
    I will open an issue with the traces.
  • NetBSD -- process.NewProcess returns not implemented yet, and
    host.TestInfo / mem.TestVirtualMemory fail on fields the port does not
    populate. Those look like real gaps in the port rather than CI problems.

Happy to add either once those are addressed.

The FreeBSD entries in lint.yml only cross-compile, so no gopsutil test
has ever executed on a FreeBSD kernel.

Refs shirou#1898

@shirou shirou left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for taking this on — this is exactly what I hoped for in #1898, and I'd like to merge it. Running the suite on a real FreeBSD kernel with no source changes is a great addition.

Could I ask for one fix and a few smaller adjustments first?

1. The sleep 900 holds the step open for 15 minutes after the tests finish.

script -q /dev/null sleep 900 & is backgrounded but still holds the ssh session's stdout/stderr, and ssh doesn't return until that channel closes. So the step ends when the sleep expires, not when go test does:

first run (cold) second run (cache hit)
step starts 15:04:45 09:03:47
prepare ran (17s) skipped, cache hit
run starts, sleep 900 & 15:06:24 — setup took 99s 09:04:35 — setup took 48s
go test ./... done 15:06:55 — 31s 09:04:57 — 23s
step ends 15:21:24 — 900s later 09:19:35 — 900s later
total 16m39s 15m48s

The second run is the clearest evidence: the cache halved setup time, yet the step as a whole got only 51 seconds shorter. Everything past setup is the fixed wait.

Detaching the holder's fds resolves it, and a trap makes its lifetime explicit:

script -q /dev/null sleep 3600 </dev/null >/dev/null 2>&1 &
pty_pid=$!
trap 'kill "$pty_pid" 2>/dev/null' EXIT
sleep 1
go test ./...

The EXIT trap still runs under set -e, so go test's exit status is preserved. This also removes a hidden flake: as written, 900s is an upper bound too, since the pty has to be open at the moment process.TestTerminal runs. If the suite grows, the holder would die first and TestTerminal would fail in a way that looks like a FreeBSD bug rather than a CI timing issue.

2. Would you mind dropping cache-after-prepare: true? Comparing the two runs, it saves roughly 20 seconds, while the cold run pays an extra 27s restart and a 1.25 GB upload. Two side effects concern me more than the time: that 1.25 GB competes for the repo's 10 GB cache budget with test.yml's setup-go caches (8 OS images × every Go version, evicted LRU), and the cache key hashes the prepare script — so FreeBSD would quietly stay on go1.25.12 indefinitely while test.yml tests every release. If you'd rather keep it, pinning the package explicitly (pkg install -y go125) would at least make the version visible and bust the cache when bumped, in the same spirit as the pinned psutil==7.2.2 there.

3. Could you pin release? It currently resolves to the action's default (15.1 in these runs). Since we SHA-pin the action itself, it seems better not to let the OS underneath move on its own.

4. copyback: false — nothing needs to come back from a test-only job.

With these, a run should land around 1.5–2 minutes.


One follow-up, not for this PR: the pty trick is working around a bug on our side. In getTerminalMap (process/process_posix.go:47-55), when /dev/pts can't be opened and no /dev/ttyp* exists, we discard the /dev/tty* entries already collected and return an error — so Terminal() fails instead of returning "" on any FreeBSD system with no pty open, a jail for instance. Linux always has /dev/pts, which is why it never surfaced. Fixing that would let the script line here go away entirely — would you be interested in opening an issue? (cpu.Info() requiring /var/run/dmesg.boot is similar, though writing the file is a fair way to emulate a booted system.)

A short note that everything runs as root in the VM would also help, since permission-denied paths aren't covered by this job.

Thank you also for #2130 and #2131 — both are well researched, and I'll follow up there. #2131 in particular is timely: #2128 has just added TestTimesTotalMatchesPerCPUSum on Windows, asserting that cpu-total is the field-wise sum of the per-CPU stats, so the averaged KERN_CPTIME breaks the same invariant. As you say, that reads as more than a test tolerance. Once those are resolved I'd be glad to take an OpenBSD job as well.

Detach the pty holder's descriptors so the step ends when go test
does rather than when the sleep expires, and bound its lifetime with
an EXIT trap. Drop cache-after-prepare, pin the release, and set
copyback: false.
@neilpang

Copy link
Copy Markdown
Author

All four are in, and thank you for the timing table -- it made the first
one straightforward to check.

  1. The holder's descriptors are detached and an EXIT trap bounds its
    lifetime, as you wrote. Both files ran off the same pull_request event on
    my fork, so this is same commit, same day, same runner class:

    old, sleep 900 & 16m46s (step 16m38s)
    https://github.com/neilpang/gopsutil/actions/runs/31372189041

    detached fds + trap 1m28s (step 1m23s)
    https://github.com/neilpang/gopsutil/actions/runs/31372189302

Inside the VM the tests themselves take 20 seconds, and the step now ends
when they do rather than 15 minutes later.

The file as it now stands on the branch, run unmodified: 1m52s, 11
packages ok, no failures.
https://github.com/neilpang/gopsutil/actions/runs/31373662084

One thing I wanted to settle before pushing: an EXIT trap can replace the
status that triggered it, and a job that cannot go red would be worse
than the 15 minutes it saves. A second job in that same run installs the
same trap and then runs false -- it fails, and the echo placed after
false never appears:

must_fail   failure
PROBE now failing on purpose
  1. cache-after-prepare is gone.

  2. release is pinned to "15.1", which is what it was already resolving
    to (the action's DEFAULT_RELEASE), so nothing about the image changes --
    it just stops moving on its own.

  3. copyback: false.

I also added a line noting that everything in the step runs as root, so
permission-denied paths are not covered by this job.

On getTerminalMap: yes, I'll open that issue. Discarding the collected
/dev/tty* entries and returning an error, rather than "", when there is
no pty is the real bug, and the script line here only works around it.
I'll write it up with the jail case.

Thank you also for reading #2130 and #2131 so closely. I had not connected
#2131 to #2128's TestTimesTotalMatchesPerCPUSum -- that invariant is a
much stronger argument than test tolerance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants