Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions .github/workflows/macos-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -302,17 +302,27 @@ jobs:
- name: Run unit tests
run: |
set -o pipefail
# To a file on its own line, not through a process substitution. `< <(...)` discards the
# producer's exit status, so the script refusing an entry that would skip nothing printed
# its diagnostic, exited 1, and the step carried on to run the whole suite unskipped and
# report success. The refusal the comment above promises never once failed a job.
SKIP_FILE="${RUNNER_TEMP:-.}/quarantine-skip-args.txt"
scripts/ci/quarantine-args.sh .github/macos-test-quarantine.txt TableProTests "$XCTESTRUN" "$TEST_DESTINATION" > "$SKIP_FILE"
# A read loop, not mapfile: the macOS runner's /bin/bash is 3.2, which has neither
# mapfile nor readarray, and a `run:` block gets that bash.
SKIP_ARGS=()
while IFS= read -r arg; do
# A blank line would become a literal empty argument, and xcodebuild reads that as a
# build action and refuses the whole run. An empty array expands to nothing and is
# fine; one empty string is not.
[ -n "$arg" ] || continue
SKIP_ARGS+=("$arg")
done < <(scripts/ci/quarantine-args.sh .github/macos-test-quarantine.txt TableProTests "$XCTESTRUN" "$TEST_DESTINATION")
done < "$SKIP_FILE"
xcodebuild test-without-building \
-xctestrun "$XCTESTRUN" \
-destination "$TEST_DESTINATION" \
-only-testing:TableProTests \
"${SKIP_ARGS[@]}" \
${SKIP_ARGS[@]+"${SKIP_ARGS[@]}"} \
-parallel-testing-enabled NO \
-resultBundlePath TestResults.xcresult \
| xcbeautify --renderer github-actions
Expand Down Expand Up @@ -380,17 +390,22 @@ jobs:
SHARD_COUNT: ${{ matrix.of }}
run: |
set -o pipefail
# To a file on its own line, so a failure of the lister fails the step. A process
# substitution discards its exit status, which reads here as "the shard selected nothing".
ONLY_FILE="${RUNNER_TEMP:-.}/shard-only-args.txt"
scripts/ci/list-tests.sh \
--xctestrun "$XCTESTRUN" \
--target TableProUITests \
--quarantine .github/macos-ui-test-quarantine.txt \
--shard "$SHARD/$SHARD_COUNT" > "$ONLY_FILE"
# A read loop, not mapfile: the macOS runner's /bin/bash is 3.2, which has neither.
ONLY_ARGS=()
while IFS= read -r arg; do
# The count check below cannot see an empty argument: one blank line makes the count 1
# and xcodebuild then reads that empty argument as a build action.
[ -n "$arg" ] || continue
ONLY_ARGS+=("$arg")
done < <(
scripts/ci/list-tests.sh \
--xctestrun "$XCTESTRUN" \
--target TableProUITests \
--quarantine .github/macos-ui-test-quarantine.txt \
--shard "$SHARD/$SHARD_COUNT"
)
done < "$ONLY_FILE"
[ "${#ONLY_ARGS[@]}" -gt 0 ] || { echo "::error::shard $SHARD selected no cases"; exit 1; }
echo "shard $SHARD of $SHARD_COUNT runs ${#ONLY_ARGS[@]} cases"
xcodebuild test-without-building \
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/repo-hygiene.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ jobs:
- name: Validate the registry update script
run: python3 .github/scripts/test_update_registry.py

# The macOS suite cannot catch this one: the script runs before any test does, and when it
# gets this wrong no test runs at all. An empty quarantine list made it print a blank line,
# which the caller turned into an empty xcodebuild argument, and main went red for three
# commits with `Unknown build action ''`.
- name: Validate the test quarantine script
run: python3 scripts/ci/test_quarantine_args.py

# A check that guarded a real invariant and ran nowhere. Pure grep over Swift sources, so it
# belongs on the free Linux runner. The MongoDB filter-shape check is the other one that was
# orphaned, but it compiles a C probe against Libs/libbson, so it lives in the macOS build
Expand Down
7 changes: 6 additions & 1 deletion TablePro/Views/Main/EditorTabStripSurfaces.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ internal enum EditorTabStripPalette {
internal static var selectedFill: Color { Color(nsColor: .controlColor) }
/// Half the weight of a separator. `separatorColor` was twice the measured edge and read as a
/// drawn outline rather than the lit rim the system puts there.
internal static var trackEdge: Color { Color(nsColor: .quinaryLabelColor) }
///
/// `quinaryLabel`, not `quinaryLabelColor`. The two spellings swap places between toolchains:
/// the Xcode 27 beta deprecates this one in favour of `quinaryLabelColor`, and Xcode 26.4, the
/// one CI builds with, rejects `quinaryLabelColor` outright as renamed to this. Take the
/// spelling CI accepts and ignore the beta's deprecation hint.
internal static var trackEdge: Color { Color(nsColor: .quinaryLabel) }
internal static var hoverFill: Color { Color(nsColor: .tertiarySystemFill) }
internal static var separator: Color { Color(nsColor: .separatorColor) }
}
Expand Down
9 changes: 8 additions & 1 deletion scripts/ci/quarantine_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,14 @@ def main():

if not ok:
sys.exit(1)
print("\n".join(args))

# Nothing at all when there is nothing to skip, because `print("")` is a blank line and the
# caller reads this stdout one line per argument. An empty list therefore reached xcodebuild as
# a literal empty argument and it stopped with `Unknown build action ''` before running a
# single test. An empty quarantine file is the goal, not an edge case: main went red the
# commit the last entry was removed and stayed red.
if args:
print("\n".join(args))


if __name__ == "__main__":
Expand Down
106 changes: 106 additions & 0 deletions scripts/ci/test_quarantine_args.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Tests for quarantine_args.py, whose stdout is read one line per xcodebuild argument.

Run: python3 scripts/ci/test_quarantine_args.py

The first test is the one that matters. `print("\\n".join([]))` is a blank line, the caller turns
every line into an argument, and xcodebuild reads an empty argument as a build action: `Unknown
build action ''`, exit 65, before a single test runs. An empty quarantine file is the goal the file
itself states, so main went red the commit the last entry was removed and stayed red for three
commits.
"""
import importlib.util
import io
import json
import os
import sys
import tempfile
from contextlib import redirect_stdout

_spec = importlib.util.spec_from_file_location(
"quarantine_args",
os.path.join(os.path.dirname(__file__), "quarantine_args.py"),
)
quarantine_args = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(quarantine_args)

ENUMERATED = ["TableProTests/SomeSuite/someCase()", "TableProTests/OtherSuite/otherCase()"]


def _run(quarantine_text, identifiers=None):
"""Returns (stdout, exit code or None), the way the shell caller sees it."""
with tempfile.TemporaryDirectory() as work:
quarantine = os.path.join(work, "quarantine.txt")
with open(quarantine, "w", encoding="utf-8") as handle:
handle.write(quarantine_text)

enumeration = os.path.join(work, "tests.json")
listed = ENUMERATED if identifiers is None else identifiers
with open(enumeration, "w", encoding="utf-8") as handle:
json.dump({"values": [{"enabledTests": [{"identifier": i} for i in listed]}]}, handle)

os.environ["QUARANTINE"] = quarantine
os.environ["TARGET"] = "TableProTests"
argv = sys.argv
sys.argv = ["quarantine_args.py", enumeration]
captured = io.StringIO()
status = None
try:
with redirect_stdout(captured):
quarantine_args.main()
except SystemExit as error:
status = error.code
finally:
sys.argv = argv
return captured.getvalue(), status


def test_an_empty_list_prints_nothing_at_all():
"""Not even a newline. One blank line is one empty argument, and xcodebuild refuses the run."""
out, status = _run("# every entry has been burned down\n#\n")
assert out == "", repr(out)
assert status is None, status


def test_a_file_of_only_whitespace_prints_nothing():
out, status = _run("\n \n\t\n")
assert out == "", repr(out)
assert status is None, status


def test_no_line_is_ever_empty():
"""The caller reads one argument per line, so a blank one is unrepresentable by construction."""
out, _ = _run("SomeSuite/someCase()\nOtherSuite/otherCase()\n")
assert out.splitlines() == [
"-skip-testing:TableProTests/SomeSuite/someCase()",
"-skip-testing:TableProTests/OtherSuite/otherCase()",
], out
assert all(line.strip() for line in out.splitlines()), repr(out)


def test_a_whole_suite_is_accepted_without_a_case():
out, _ = _run("SomeSuite\n")
assert out.splitlines() == ["-skip-testing:TableProTests/SomeSuite"], out


def test_an_entry_that_would_skip_nothing_still_fails_the_job():
"""The guard the script exists for, unchanged by the empty-list fix."""
out, status = _run("NoSuchSuite/nope()\n")
assert status == 1, status
assert out == "", repr(out)


def test_a_swift_testing_case_without_its_parentheses_still_fails():
out, status = _run("SomeSuite/someCase\n")
assert status == 1, status
assert out == "", repr(out)


if __name__ == "__main__":
test_an_empty_list_prints_nothing_at_all()
test_a_file_of_only_whitespace_prints_nothing()
test_no_line_is_ever_empty()
test_a_whole_suite_is_accepted_without_a_case()
test_an_entry_that_would_skip_nothing_still_fails_the_job()
test_a_swift_testing_case_without_its_parentheses_still_fails()
print("All quarantine-args tests passed.")
Loading