Skip to content
Closed
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
577 changes: 577 additions & 0 deletions .github/testshards/internal-ui.tsv

Large diffs are not rendered by default.

89 changes: 73 additions & 16 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ permissions:
contents: read

jobs:
build-and-test:
runs-on: ubuntu-latest
validation:
name: Validation (attempt ${{ github.run_attempt }})
runs-on: ubuntu-24.04
steps:
- name: Check out code
uses: actions/checkout@v7
Expand All @@ -41,6 +42,9 @@ jobs:
sudo apt-get update
sudo apt-get install -y gcc libgl1-mesa-dev xorg-dev libwayland-dev libxkbcommon-dev

- name: Validate UI shard manifest
run: make check-test-shards-direct

- name: Vet
run: go vet ./...

Expand All @@ -64,19 +68,72 @@ jobs:
# the same reason.
run: GOOS=windows GOARCH=amd64 go vet ./internal/...

- name: Test
# The runner's default locale is "C", which is not a well-formed
# BCP-47 tag, so Fyne logs a three-line parse fault before any test
# runs. Naming a real one keeps that noise out of the test output.
#
# -timeout 30m: go test defaults to 10m per package. internal/ui on
# ubuntu-latest already took ~9m39s before folder-sibling tests; the
# next commit then panicked at 10m0s while a later test was starting.
linux-race:
name: Linux race (${{ matrix.partition }}, attempt ${{ github.run_attempt }})
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
partition: [non-ui, ui-1, ui-2, ui-3]
steps:
- name: Check out code
uses: actions/checkout@v7

- name: Set up Go
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache-dependency-path: go.sum

- name: Install Linux GUI build dependencies
run: |
sudo apt-get update
sudo apt-get install -y gcc libgl1-mesa-dev xorg-dev libwayland-dev libxkbcommon-dev

- name: Run ${{ matrix.partition }} race contract (attempt ${{ github.run_attempt }})
id: race
env:
TEST_CAPTURE: ${{ runner.temp }}/go-test-linux-race-${{ matrix.partition }}.json
run: |
case "${{ matrix.partition }}" in
non-ui)
make --no-print-directory test-race-non-ui-direct
;;
ui-1|ui-2|ui-3)
make --no-print-directory test-race-ui-direct TEST_SHARD="${{ matrix.partition }}"
;;
*)
echo "Unexpected Linux race partition: ${{ matrix.partition }}" >&2
exit 2
;;
esac

- name: Summarize ${{ matrix.partition }} race result (attempt ${{ github.run_attempt }})
if: always()
env:
LANG: en_US.UTF-8
run: go test -timeout 30m -race ./...
PARTITION: ${{ matrix.partition }}
RACE_OUTCOME: ${{ steps.race.outcome }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
{
echo "## Linux race \`$PARTITION\` (attempt \`$RUN_ATTEMPT\`)"
echo
echo "- Workflow attempt: \`$RUN_ATTEMPT\`"
echo "- Test step outcome: \`$RACE_OUTCOME\`"
echo "- Raw-event artifact: \`linux-race-$PARTITION-$GITHUB_RUN_ID-attempt-$GITHUB_RUN_ATTEMPT\`"
} >> "$GITHUB_STEP_SUMMARY"

- name: Upload ${{ matrix.partition }} raw events (attempt ${{ github.run_attempt }})
if: always()
uses: actions/upload-artifact@v7
with:
name: linux-race-${{ matrix.partition }}-${{ github.run_id }}-attempt-${{ github.run_attempt }}
path: ${{ runner.temp }}/go-test-linux-race-${{ matrix.partition }}.json
if-no-files-found: error
retention-days: 14

windows-test:
name: Windows tests (attempt ${{ github.run_attempt }})
runs-on: windows-latest
steps:
- name: Check out code
Expand All @@ -93,8 +150,8 @@ jobs:
# runner doesn't have by default. Not ./...: internal/ui isn't
# excluded for lack of a display driver - it uses the same headless
# fyne.io/fyne/v2/test driver as internal/ui/autoupdate below. It's
# excluded because it already costs ~9m39s on ubuntu-latest (see the
# Test step above) and could carry golden-master/font differences on
# Windows; these two packages are the file-replacement update logic,
# not the concurrent UI.
# excluded because it already costs ~9m39s in a monolithic Ubuntu run
# and could carry golden-master/font differences on Windows; these two
# packages are the file-replacement update logic, not the concurrent
# UI.
run: go test ./internal/update/... ./internal/ui/autoupdate/...
17 changes: 17 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@ Entry point only. `main.go` calls `openwith.Install` (first statement, see
`ui.Run`. `main_darwin_test.go` asserts the graft landed — this is the only
test binary that links the Cocoa driver.

### `scripts/testshards`

Repository tooling for measured Linux race-test sharding. It parses complete
`go test -json` streams, emits deterministic package/top-level-test summaries,
plans a reviewable manifest from median durations with stable LPT tie-breaking,
checks that manifest against the build-selected Linux test inventory, derives
the fail-closed non-UI package partition, and emits exact shard filters. Its
capture path streams compact partition/package/test diagnostics while retaining
the raw events outside the repository; the generated assignment lives at
`.github/testshards/internal-ui.tsv`.

| File | Responsibility |
|------|----------------|
| `main.go` | `summarize`, `plan`, `check`, `regex`, `partition`, and `capture` command paths: complete event-stream validation, deterministic assignment, live build-selected inventory and parallel-call checks, exact anchored filters, exact-package subtraction, concise diagnostics, and raw preservation. |
| `main_test.go` | Command-boundary fixtures for event streams and capture, deterministic planning, every manifest rejection, build-selected runnable forms, parallel-call refusal, exact filter generation, package partitioning, and Make contract expansion. |

### `internal/ui`

The application. Unexported `appState` is the file-set model (scan/drop
Expand Down Expand Up @@ -405,6 +421,7 @@ see `AGENTS.md`.
- "How is the last session saved/restored?" → `internal/session` + `session.go` `restoreSession`.
- "How do in-app updates work?" → `internal/update` + `internal/ui/autoupdate` (serialized automatic/manual checks, staging, apply intent, What's-New cache, apply-failure cache) + `internal/ui/autoupdate.go` (`maybeStartUpdateCheck` / `CheckForUpdatesNow` / `PerformUpdate` / `maybeShowWhatsNew` / `maybeShowUpdateFailure`) + `settingswin` (manual dialogs) + `help/whatsnew.go` (the window). Automatic checks are off by default (`preferences.CheckForUpdates`) and stage silently. Apply remains OnStopped: normal shutdown installs without relaunch; explicit Perform update adds a post-apply relaunch. On Windows that relaunch starts the new executable with `PICFETCH_UPDATE_AWAIT_PID` set to the installing process's PID; `update.CleanupPredecessor` (`internal/update/await.go`), called from `main.go` before `app.NewWithID`, waits on that PID before preferences are touched, unsets the variable, and sweeps leftovers from pre-2026-08-30 updates. If `update.Apply` fails, `ClassifyApplyError` (`internal/update/applyerr.go`) records the reason via `autoupdate.SaveApplyFailure`, and `maybeShowUpdateFailure` explains it on the next launch with a button to the releases page — releases are unsigned, so Controlled Folder Access can still deny the write even to `picfetch.exe` itself. GitHub TUF bootstrap expiry: `tufroot.go`.
- "How are GitHub release notes written?" → `todos.md` `## Done` + `scripts/releasenotes` + `make release` + `.github/workflows/release.yml` `body_path`.
- "How are Linux race-test shards measured and assigned?" → `scripts/testshards` + `.github/testshards/internal-ui.tsv` + the measured CI sharding plan.
- "How is a WinGet publish gated after Release?" → `.github/workflows/winget.yml` + `scripts/wingettag` (vX.Y.Z allowlist; `workflow_run` must be `release.yml` on a published tag).
- "How does a macOS Open With reach the viewer?" → `internal/openwith` (queue + Objective-C graft) + `main.go` `openwith.Install` + `internal/ui/openwith.go` + `run.go` `SetOnStarted`.
- "How does the packaged macOS app declare file/folder associations (Open With)?" → `internal/imaging/loader.go` `SupportedExtensions` + `scripts/plistdoctypes` + `Makefile` `package-mac`.
Expand Down
76 changes: 72 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ TEST_TIMEOUT := 30m
TEST_IMAGE := ubuntu:24.04
TEST_CONTAINER_LABEL := io.github.frathe.picfetch.test=true
TEST_RACE :=
TEST_RACE_FLAGS := -race -count=1 -timeout $(TEST_TIMEOUT)
TEST_LOCALE := en_US.UTF-8
TEST_SHARD_MANIFEST := .github/testshards/internal-ui.tsv
TEST_SHARD_PACKAGE := ./internal/ui
TEST_PARTITION :=
TEST_CAPTURE ?= /tmp/picfetch-test-$(TEST_PARTITION).json

.PHONY: all build build-linux-all run fmt fmt-check vet test update-test-image enter-test-container test-native test-race verify golden tidy clean package-mac package-windows package-windows-debug package-linux package-linux-debug build-all install-tools install-linux-tools security security-govulncheck security-github bump-version release check-tuf-root sync-tuf-root sync-qodana-test-exclusions check-qodana-test-exclusions help
.PHONY: all build build-linux-all run fmt fmt-check vet test update-test-image enter-test-container test-native test-race test-race-direct test-race-non-ui-direct test-race-ui-direct verify golden tidy clean package-mac package-windows package-windows-debug package-linux package-linux-debug build-all install-tools install-linux-tools security security-govulncheck security-github bump-version release check-tuf-root sync-tuf-root sync-qodana-test-exclusions check-qodana-test-exclusions check-test-shards check-test-shards-direct help

all: build

Expand Down Expand Up @@ -116,6 +122,53 @@ check-qodana-test-exclusions: ## Fail if qodana.yaml does not exclude every *_te
vet: ## Run go vet
go vet ./...

check-test-shards: ## Validate the UI shard manifest against the live Linux/amd64 test inventory
docker run --rm --platform linux/amd64 \
--label "$(TEST_CONTAINER_LABEL)" \
-v "$(CURDIR):/work" -w /work \
-v picfetch-go-build-linux-amd64:/root/.cache/go-build \
-v picfetch-go-mod-linux-amd64:/root/go/pkg/mod \
$(TEST_IMAGE) bash -c '\
set -e; \
apt-get update -qq; \
apt-get install -y -qq make gcc libgl1-mesa-dev xorg-dev libwayland-dev libxkbcommon-dev golang-go ca-certificates >/dev/null; \
make --no-print-directory check-test-shards-direct \
'

# Internal entry point for a prepared Linux/amd64 runner. Use the public Docker
# target above for canonical validation from any host.
check-test-shards-direct:
go run ./scripts/testshards check -package "$(TEST_SHARD_PACKAGE)" -manifest "$(TEST_SHARD_MANIFEST)"

# Internal entry points for a prepared Linux/amd64 runner. The public test-race
# target enters Docker once and runs this sequence there; hosted CI can call the
# partition targets directly without nesting Docker.
test-race-direct:
@$(MAKE) --no-print-directory check-test-shards-direct
@$(MAKE) --no-print-directory test-race-non-ui-direct
@$(MAKE) --no-print-directory test-race-ui-direct TEST_SHARD=ui-1
@$(MAKE) --no-print-directory test-race-ui-direct TEST_SHARD=ui-2
@$(MAKE) --no-print-directory test-race-ui-direct TEST_SHARD=ui-3

test-race-non-ui-direct: override TEST_PARTITION := non-ui
test-race-non-ui-direct:
bash -c '\
set -eu -o pipefail; \
packages="$$(go run ./scripts/testshards partition -package "$(TEST_SHARD_PACKAGE)")"; \
LANG="$(TEST_LOCALE)" go test $(TEST_RACE_FLAGS) -json $$packages | \
go run ./scripts/testshards capture -out "$(TEST_CAPTURE)" -partition "$(TEST_PARTITION)" \
'

test-race-ui-direct: override TEST_PARTITION = $(TEST_SHARD)
test-race-ui-direct:
bash -c '\
set -eu -o pipefail; \
case "$(TEST_SHARD)" in ui-1|ui-2|ui-3) ;; *) echo "TEST_SHARD must be one of ui-1, ui-2, or ui-3" >&2; exit 2;; esac; \
filter="$$(go run ./scripts/testshards regex -manifest "$(TEST_SHARD_MANIFEST)" -shard "$(TEST_SHARD)")"; \
LANG="$(TEST_LOCALE)" go test $(TEST_RACE_FLAGS) -json -run "$$filter" $(TEST_SHARD_PACKAGE) | \
go run ./scripts/testshards capture -out "$(TEST_CAPTURE)" -partition "$(TEST_PARTITION)" \
'

update-test-image: ## Pull the latest Linux/amd64 Ubuntu image used by Docker tests
docker pull --platform linux/amd64 "$(TEST_IMAGE)"

Expand All @@ -129,7 +182,7 @@ test: ## Run tests in Linux/amd64 Docker, matching CI and golden rendering (need
$(TEST_IMAGE) bash -c '\
set -e; \
apt-get update -qq; \
apt-get install -y -qq gcc libgl1-mesa-dev xorg-dev libwayland-dev libxkbcommon-dev golang-go ca-certificates locales procps htop >/dev/null; \
apt-get install -y -qq make gcc libgl1-mesa-dev xorg-dev libwayland-dev libxkbcommon-dev golang-go ca-certificates locales procps htop >/dev/null; \
locale-gen en_US.UTF-8 >/dev/null; \
export LANG=en_US.UTF-8; \
status=0; \
Expand Down Expand Up @@ -161,8 +214,23 @@ enter-test-container: ## Open Bash in the running test container (htop/top avail
test-native: ## Run tests directly on the current OS/architecture
go test -timeout $(TEST_TIMEOUT) ./...

test-race: TEST_RACE := -race
test-race: test
test-race: ## Run the guarded race partitions sequentially in one Linux/amd64 Docker container
docker run --rm --platform linux/amd64 \
--label "$(TEST_CONTAINER_LABEL)" \
-v "$(CURDIR):/work" -w /work \
-v picfetch-go-build-linux-amd64:/root/.cache/go-build \
-v picfetch-go-mod-linux-amd64:/root/go/pkg/mod \
-e HOST_UID=$$(id -u) -e HOST_GID=$$(id -g) \
$(TEST_IMAGE) bash -c '\
set -e; \
apt-get update -qq; \
apt-get install -y -qq make gcc libgl1-mesa-dev xorg-dev libwayland-dev libxkbcommon-dev golang-go ca-certificates locales procps htop >/dev/null; \
locale-gen $(TEST_LOCALE) >/dev/null; \
status=0; \
make --no-print-directory test-race-direct || status=$$?; \
if [ -d internal/ui/testdata/failed ]; then chown -R "$$HOST_UID:$$HOST_GID" internal/ui/testdata/failed; fi; \
exit $$status \
'

verify: fmt-check check-tuf-root check-qodana-test-exclusions ## Run the same checks CI does (format, TUF root, Qodana exclusions, vet, build, race tests)
go vet ./...
Expand Down
Loading
Loading