Skip to content

Forward-port the double-filename fix and test overhaul; per-run test isolation; CI split#42

Merged
petermueller merged 12 commits into
mainfrom
pkm/forward-port-0.3
Jul 3, 2026
Merged

Forward-port the double-filename fix and test overhaul; per-run test isolation; CI split#42
petermueller merged 12 commits into
mainfrom
pkm/forward-port-0.3

Conversation

@petermueller

Copy link
Copy Markdown
Collaborator

Forward-ports the double-filename fix and the rebuilt test suite from the v0.2.x line onto main, plus the first two prioritized v0.3 infrastructure fixes. This is step 0 of the v0.3 plan — see plans/README.md (added here) for the full roadmap and the v0.3 milestone.

Fixes #25. Closes #35. Closes #36.

What's here

The lib fix (#25): put/3 joins the storage dir with file.file_name as-is — Waffle has already resolved the version filename before calling the adapter — while path_for/3 keeps its resolving semantics (via a new fullname/3 wrapper) for delete/3 and the URL builders, which receive the original client filename and must resolve it exactly once. This differs from PR #1, which stopped path_for/3 from resolving at all. A regression test drives store through a probe definition and asserts filename/2 is invoked exactly once per version.

Note for direct callers of CloudStorage.put/3 (not via definition.store): filename resolution is no longer applied inside put — pass the final object name in file.file_name.

The test overhaul: feature-organized integration suite (test/integration/), single-purpose test definitions, shared case template with assertion macros, tag taxonomy (documented in test/test_helper.exs), and a mix test.unit alias for offline runs.

#35 — cleanup safety: all test objects live under a per-run prefix (uploads/<run id>, from WAFFLE_TEST_RUN_ID or generated); the after-suite cleanup deletes only that prefix (never the whole bucket) and is skipped for offline or credential-less runs. mix test.unit now needs no credentials and no network.

#36 — CI split: a secret-free unit job (full version matrix, runs on fork PRs too) and a guarded integration job (one modern pair, real GCS, skipped where secrets don't exist). WAFFLE_BUCKET2 is passed through for #33, and WAFFLE_TEST_RUN_ID is set from the workflow run so concurrent runs share the test buckets safely.

Plans: the plans/ directory (excluded from the hex package) with the 0.3 release plan, offline-test-suite plan, client-rewrite plan, and migration/repair plan.

Verification

  • mix test.unit with zero GCS-related env vars: 56 tests, 0 failures, clean exit.
  • Full suite against real GCS: 56 tests, 0 failures, 1 excluded (the 0.3-gated bucket-from-scope test, see Support bucket selection from scope (bucket/1 definition callback) #33), 4 skipped (documented future behaviors).
  • Regression guard proven to bite: temporarily reverting put/3 fails the regression test; restoring the fix passes it.
  • Both test buckets list zero objects under uploads* after the suite.
  • mix hex.build: plans/ absent from the package.

First-run notes

Waffle resolves the version filename (Versioning.resolve_file_name/3)
and writes it into file.file_name before calling the storage adapter's
put/3, so put must join the storage dir with file.file_name as-is.
Re-resolving here (the pre-PR-#1 behavior) doubled prefix-style
filename/2 results, storing e.g. 1_original_1_original_photo.png (#25).

Unlike PR #1, path_for/3 keeps its resolving semantics (now via the
fullname/3 wrapper): delete/3 and the URL builders receive the
*original* client filename from Waffle, so they must resolve it
exactly once. Only put receives a pre-resolved name.

Note for direct callers of CloudStorage.put/3 (not via
definition.store): filename/2 resolution is no longer applied inside
put; pass the final object name in file.file_name.
A fresh image.png plus variants with a space, a plus sign, and an
invalid (non-image) file, referenced by the incoming integration suite.
Introduce Waffle.GCSCase, an ExUnit.CaseTemplate that centralizes the
integration-test setup, helpers (env_bucket/bucket_url/with_env), and
assertion macros (assert_public/assert_private/assert_header/
assert_public_with_extension/assert_acls_public_reader/
delete_and_assert_gone) that were previously copy-pasted across test files.
It also applies the :integration and :tmp_dir moduletags.

Add feature_definitions.ex: focused, single-purpose Waffle definitions
(GCSTest.*), each exercising exactly one feature, plus GCSTest.FilenameProbe
(@async false + a send/self probe) used by the double-filename regression
test.
gcs_features_test.exs is the canonical public-API integration suite: each
test drives one Waffle feature through definition.store/url/delete against
real GCS, using the single-purpose GCSTest.* definitions and the shared
Waffle.GCSCase. It folds back the coverage the earlier ad-hoc suites had
dropped: object ACL assertions, stored-name equality checks, the pre-store
URL assertion for a definition-level bucket, and per-version extension
assertions.

double_filename_regression_test.exs is an explicit guard for issue #25:
GCSTest.FilenameProbe announces every filename/2 invocation, so the test
asserts (assert_received/refute_received) that the name is resolved exactly
once per version during store and that the stored object name is not
double-prefixed.
cloud_storage_test.exs now covers only the CloudStorage module boundary:
pure-unit path/name construction (storage_dir/3, path_for/3, bucket/1 with a
literal) that needs no network, plus the put/delete/url round-trips tagged
:integration. The duplicated 'from upstream storage tests' block, the
copy-pasted assertion macros, and the leftover IO.inspect calls are gone
(macros now live only in Waffle.GCSCase).

Delete test/support/definitions.ex: the old multi-purpose fixtures were only
used by the now-removed redundant suites. Repoint cleanup.ex's after_suite
teardown at GCSTest.PublicUpload.
Add a tag-taxonomy comment block to test_helper.exs explaining :integration,
:tmp_dir, :bucket_with_file_and_scope, double_filename_regression, skip, and
upstream_mismatch, including how each is toggled with --include/--exclude and
a note that future-behavior skips should become exclude tags for 0.3.

Add a 'mix test.unit' alias (test --exclude integration) with a preferred_env
so the fast, offline unit tests run without GCS credentials, and document the
env vars and example invocations in the README.
Document how to bring the double-filename fix and the test overhaul forward
into main (0.3): the precise put/3 + path_for/3 + fullname/3 lib delta and why
it differs from PR #1, the orthogonal token-fetcher divergence to avoid
dragging along, the cherry-pick order for the four test commits with their
conflict points, and the version-gating flip that makes the
:bucket_with_file_and_scope test run (and fail) once main hits 0.3.

Lives under plans/, which is outside the mix.exs files: whitelist and so is
excluded from the published hex package.
…tooling

Distilled from a full review of the lib and test suite. Index in
plans/README.md; work items tracked on GitHub as milestone v0.3
(issues #27-#37) plus #38 (offline suite), #39 (client rewrite),
and #40 (migration/repair tooling).

forward-port-to-main.md updated in place: the section 4 version-gating
question is decided as option (a) (bucket-from-scope, #33), and
WAFFLE_BUCKET2 is now available as a GHA repository secret.
- forward-port guide: the feature-suite fixture images live outside the
  four test commits and must be pulled across separately.
- Correct the CI finding: main's workflow already triggers on main with
  a modern matrix; issue #36 narrows to fork-PR secrets, WAFFLE_BUCKET2
  passthrough, and the shared-bucket matrix hazard.
- Record the working priority within v0.3: #35, #36, #37 first.
…-friendly

Closes #35.

The after-suite cleanup previously deleted every object in the bucket
and ran unconditionally. Three consequences: pointing WAFFLE_BUCKET at
a bucket containing real data destroyed it; concurrent runs (e.g. a CI
matrix) sharing one bucket wiped each other's in-flight objects; and
credential-less runs (mix test.unit) crashed in after_suite trying to
build an authenticated connection.

Now:

- All test definitions store under a per-run prefix, uploads/<run id>
  (GCSTest.Run). The id comes from WAFFLE_TEST_RUN_ID (CI sets it to
  something job-unique) or is generated per local run. URL assertions
  derive from the same helper.
- Cleanup lists only that prefix (in both configured buckets) and is
  skipped when :integration is excluded or GCP_CREDENTIALS is absent.
- Goth only starts in test_helper when credentials are present, so
  mix test.unit is genuinely offline: no creds, no network.
- util_test restored WAFFLE_BUCKET with put_env(key, nil), which
  crashes when the var was never set - visible for the first time now
  that credential-less runs exist.

Runs that crash before cleanup leave their prefix behind; the README
recommends a short lifecycle TTL on test buckets to sweep those.

Verified: mix test.unit green with no GCS-related env vars at all;
full suite green against real GCS; both buckets list zero objects
under uploads* afterwards.
Closes #36.

The single job ran the full suite - integration tests included - with
secrets on every PR. Fork PRs don't receive secrets, so any outside
contribution failed on auth rather than on its merits.

Now:

- unit: mix test.unit across the version matrix, plus compile
  --warnings-as-errors and the format check. No secrets referenced, so
  it runs (and can pass) everywhere, fork PRs included. MIX_ENV=test is
  explicit on the test step because def cli only exists on
  Elixir >= 1.15.
- integration: the full suite against real GCS on one modern
  Elixir/OTP pair, skipped for fork PRs where secrets are absent.
  Passes WAFFLE_BUCKET2 through (needed by the bucket-from-scope test
  once #33 lands) and sets WAFFLE_TEST_RUN_ID from the workflow run id
  and attempt, so concurrent runs sharing the test buckets stay
  isolated and the after-suite cleanup stays scoped to this run.
- workflow_dispatch trigger for manual runs.
@petermueller petermueller added this to the v0.3 milestone Jul 3, 2026
Three fixes from the first run of the split workflow (PR #42):

- ubuntu-24.04 runner images no longer ship ImageMagick, so the
  integration job installs it before running the transform tests.
- Formatter output is only stable within an Elixir version (1.15 wraps
  lines 1.17 accepts), so the format check now runs once, on the newest
  matrix entry, and the unit matrix gets fail-fast: false so one
  version's failure doesn't cancel the others' results.
- Run mix format properly: the earlier local check piped through tail,
  which hid a real failure. Shorten the regression test's tag string
  while at it.
@petermueller

petermueller commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator Author

@ulissesalmeida I combined all the bits from #26 around the filename fix so it works for all the operations, then added some tests and plan files to go along with the issues/milestones.

I'm probably going to merge in the next couple days unless you wanted to take a look

@ulissesalmeida ulissesalmeida left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thank you for including me

Although I changed jobs a year ago, so it has been years I don't need to use this library 😅 So I think I'm not the best person anymore to review it.

I did a quick scan, I couldn't see anything obviously bad, so follow your ❤️

@petermueller petermueller merged commit 51a6599 into main Jul 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants