Skip to content

Build and test on macOS as well as Linux - #24

Merged
Aduneer merged 3 commits into
mainfrom
ci-macos
Aug 20, 2026
Merged

Build and test on macOS as well as Linux#24
Aduneer merged 3 commits into
mainfrom
ci-macos

Conversation

@Aduneer

@Aduneer Aduneer commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Everything FlashTerm does outside the standard library is POSIX rather than Linux — termios, ioctl, dirent, wcwidth — and date.cpp and event.cpp have carried their gmtime_r/localtime_r branches since the log landed. So this was expected to pass.

It did not, three times over, and that is the point of the PR.

What changed

os: [ubuntu-latest, macos-latest] across the existing compiler and mode axes, with one pair excluded: g++ on a macOS runner is a shim for Apple clang rather than GCC, so that combination would run the clang entry a second time under another name. Six jobs rather than eight.

optimised sanitised
ubuntu · g++
ubuntu · clang++
macos · clang++

macos-latest is Apple Silicon, so this also builds for arm64, where char is unsigned by default and every x86 build so far has had it signed.

What it caught

The code compiled clean. Nothing below is a macOS-only defect — each is something that was already true and that only a second platform could show.

1. FLASHTERM_SEED did not fix the order it promised to fix

Six of forty-seven golden cases failed, and every one was a multi-card case.

std::shuffle is specified to produce a uniformly random permutation and nothing about which one, so libstdc++ and libc++ deal the same seeded deck in different orders. A scripted session answers cards in the order it expects to meet them, so transcripts recorded against libstdc++ were simply wrong on libc++ — surfacing as every answer being graded incorrect.

FLASHTERM_SEED is documented as fixing the review order. A promise that holds only on the standard library you happened to build against is not that promise. It is now an explicit Fisher–Yates:

  • std::mt19937 stays. The standard pins its algorithm exactly, down to a required value for the 10000th draw — the generator was never the problem.
  • bounded() draws an index by rejection sampling, since std::uniform_int_distribution is unspecified in the same way.

Nothing was wrong with the randomness and no real session is affected. One transcript changes on Linux, review-multi-card, whose input is remapped rather than re-recorded so the case still exercises what it was written for — the same correct, hint-then-partial, incorrect, correct, correct. Its summary is still Correct: 3 · Partial: 1 · Incorrect: 1. Two lines of the input file change.

2 and 3. Two golden cases could observe the machine

Same class as the bug that made run.sh pin the environment for every case rather than per case.

wc pads its count with leading spaces on a BSD userland, so a binary fixture was described as <binary, 597 bytes> on macOS and <binary, 597 bytes> on Linux. Stripped rather than reformatted, since the value is compared as an integer as well as printed.

review-audio-unplayable pointed FLASHTERM_TTS and FLASHTERM_PLAYER at /bin/false to reach the "No audio available for this card" path. There is no /bin/false on macOS — false lives in /usr/bin — so FlashTerm rejected an override it could not execute, fell back to the built-in candidate list, found macOS's own say, and it succeeded. The case had stopped testing a failed playback at all.

fake-audio.sh now fails on demand via FAKE_AUDIO_FAIL, the same shape as fakebin/piper and FAKE_PIPER_FAIL, and the case asks for that instead of naming a command it hopes exists. Its transcript is byte for byte what it was, so what it checks is unchanged — it merely checks it everywhere now.

Also

  • README's Quick Start now says which platforms are built and tested, which it never did.
  • README's Contributing paragraph said "all four with -Werror", which is now six.
  • CHANGELOG.md gains an Unreleased section. Its historical 0.2.0 and 0.3.0 entries still say four jobs and are deliberately left alone: they record what was true at those releases.

Checks

Green on all six jobs. Locally before each push: make check on gcc and clang, optimised and sanitised (-fsanitize=address,undefined -fno-sanitize-recover=undefined), plus the env -i clean room. 1184 unit checks, 47 golden cases.

Everything outside the standard library is POSIX rather than Linux --
termios, ioctl, dirent, wcwidth -- and date.cpp and event.cpp have had
their gmtime_r and localtime_r branches since the log landed. So this was
expected to pass. Expected is not tested, and "runs on a Mac" is a claim
the README should not make on the strength of reading the source.

The runner is Apple Silicon, so this also builds for arm64, which is the
half worth having beyond the platform itself: char is unsigned there and
signed on x86.

g++ on macOS is a shim for Apple clang rather than GCC, so that pair is
excluded rather than run as a second clang under another name. Six jobs.
The macOS runner added in the previous commit failed on its first run:
every multi-card golden case, and only those.

The cause is std::shuffle, whose output the standard does not specify --
only that the permutation is uniformly random. libstdc++ and libc++
therefore deal the same seeded deck in different orders, and a scripted
session answers cards in the order it expects to meet them, so every
transcript recorded against libstdc++ was wrong on libc++.

FLASHTERM_SEED is documented as fixing the review order. A promise that
holds only on the standard library you happened to build against is not
that promise, so the shuffle is now an explicit Fisher-Yates. mt19937
stays: the standard pins its algorithm exactly, down to a required value
for the 10000th draw, so only the mapping from draws to indices needed
taking back -- hence bounded(), by rejection sampling, since
std::uniform_int_distribution is unspecified in the same way.

Nothing is wrong with the randomness, and no real session changes
meaningfully; the bug was in what the seed promised.

One transcript changes on Linux, review-multi-card. Its input is remapped
rather than simply re-recorded, so that the case still exercises what it
was written for: the same correct, hint-then-partial, incorrect, correct,
correct, landing on the cards the new order actually deals.
Both found by the macOS runner, and both the same class as the bug that
made run.sh pin the environment for every case rather than per case: a
transcript must not be able to notice what it is running on.

wc pads its count with leading spaces on a BSD userland, so a binary
fixture was described as "<binary,      597 bytes>" on macOS and
"<binary, 597 bytes>" on Linux. Stripped rather than reformatted, since
the value is compared as an integer as well as printed.

review-audio-unplayable pointed FLASHTERM_TTS and FLASHTERM_PLAYER at
/bin/false to reach the "No audio available for this card" path. There is
no /bin/false on macOS -- `false` is in /usr/bin there -- so FlashTerm
rejected an override it could not execute, fell back to the built-in
candidate list, and found macOS's own `say`, which succeeded. The case
was not testing a failed playback at all.

fake-audio.sh now fails on demand via FAKE_AUDIO_FAIL, the same shape as
fakebin/piper and FAKE_PIPER_FAIL, and the case asks for that instead of
naming a command it hopes exists. The transcript is byte for byte what it
was, so what the case checks is unchanged -- it merely checks it
everywhere now.
@Aduneer
Aduneer merged commit 9df1baf into main Aug 20, 2026
6 checks passed
@Aduneer
Aduneer deleted the ci-macos branch August 20, 2026 08:36
@Aduneer
Aduneer restored the ci-macos branch August 20, 2026 08:36
@Aduneer
Aduneer deleted the ci-macos branch August 20, 2026 08:37
@Aduneer Aduneer mentioned this pull request Aug 20, 2026
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.

1 participant