From afbce1bfd4a8cc72f7a77bf7199fec684b63724f Mon Sep 17 00:00:00 2001 From: Naohiro CHIKAMATSU Date: Fri, 8 May 2026 09:06:08 +0900 Subject: [PATCH] test(facade): pin empty-input round-trip uniformly across all codecs (#70) --- CHANGELOG.md | 13 ++ test/empty_input_round_trip_test.gleam | 206 +++++++++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 test/empty_input_round_trip_test.gleam diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a9b025..a6e96d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Tests +- New `test/empty_input_round_trip_test.gleam` pins the empty-input + round-trip contract uniformly across all 32 facade pairs in + `yabase/facade`. For every total `encode_*` function, the test + asserts `decode(encode(<<>>)) == Ok(<<>>)`. For the two `Result`- + returning encoders (Z85, RFC 1924 Base85) the same round-trip is + pinned via `let assert Ok(encoded) = encode(<<>>)`. The RFC 4648 + family additionally pins `decode("") == Ok(<<>>)` directly. A future + refactor that flips the rule for one codec now surfaces here as a + single-test diff. metamon is not a dev-dep so the property is + exercised exhaustively over the facade rather than via + `forall_round_trip`. (#70) + ## [0.15.0] - 2026-05-07 ### Added diff --git a/test/empty_input_round_trip_test.gleam b/test/empty_input_round_trip_test.gleam new file mode 100644 index 0000000..3b20ba4 --- /dev/null +++ b/test/empty_input_round_trip_test.gleam @@ -0,0 +1,206 @@ +//// Empty-input round-trip pinned across every facade codec (#70). +//// +//// The contract for total `encode_*` functions: `encode(<<>>)` is some +//// canonical string `s` such that `decode(s) == Ok(<<>>)`. For codecs +//// whose `encode` itself returns `Result` (Z85, RFC 1924 Base85), the +//// contract is `encode(<<>>) == Ok(s)` and the same round-trip +//// property holds on `s`. +//// +//// The empty case is the smallest, simplest test case — a codec that +//// fails on it almost certainly has a more general bug. Pinning every +//// facade pair in one file means a future refactor that changes the +//// rule for one codec surfaces here as a single test diff. Companion +//// to yabase#63 (return type unification), #64 (sub-byte truncation), +//// and #65 (facade contract). + +import gleeunit/should +import yabase/facade + +// --- Total encode (returns String) --- + +pub fn empty_round_trip_base2_test() { + facade.decode_base2(facade.encode_base2(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base8_test() { + facade.decode_base8(facade.encode_base8(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base10_test() { + facade.decode_base10(facade.encode_base10(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base16_test() { + facade.decode_base16(facade.encode_base16(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base16_lowercase_test() { + facade.decode_base16(facade.encode_base16_lowercase(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base32_test() { + facade.decode_base32(facade.encode_base32(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base32_strict_test() { + facade.decode_base32_strict(facade.encode_base32(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base32_hex_test() { + facade.decode_base32_hex(facade.encode_base32_hex(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base32_crockford_test() { + facade.decode_base32_crockford(facade.encode_base32_crockford(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base32_crockford_check_test() { + facade.decode_base32_crockford_check( + facade.encode_base32_crockford_check(<<>>), + ) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base32_clockwork_test() { + facade.decode_base32_clockwork(facade.encode_base32_clockwork(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_zbase32_test() { + facade.decode_zbase32(facade.encode_zbase32(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base36_test() { + facade.decode_base36(facade.encode_base36(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base45_test() { + facade.decode_base45(facade.encode_base45(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base58_test() { + facade.decode_base58(facade.encode_base58(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base58_flickr_test() { + facade.decode_base58_flickr(facade.encode_base58_flickr(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base62_test() { + facade.decode_base62(facade.encode_base62(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base64_test() { + facade.decode_base64(facade.encode_base64(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base64_strict_test() { + facade.decode_base64_strict(facade.encode_base64(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base64_urlsafe_test() { + facade.decode_base64_urlsafe(facade.encode_base64_urlsafe(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base64_urlsafe_nopadding_test() { + facade.decode_base64_urlsafe_nopadding( + facade.encode_base64_urlsafe_nopadding(<<>>), + ) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base64_nopadding_test() { + facade.decode_base64_nopadding(facade.encode_base64_nopadding(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base64_dq_test() { + facade.decode_base64_dq(facade.encode_base64_dq(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_base91_test() { + facade.decode_base91(facade.encode_base91(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_ascii85_test() { + facade.decode_ascii85(facade.encode_ascii85(<<>>)) + |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_adobe_ascii85_test() { + facade.decode_adobe_ascii85(facade.encode_adobe_ascii85(<<>>)) + |> should.equal(Ok(<<>>)) +} + +// --- encode_* returns Result (Z85, RFC 1924 Base85) --- +// +// Z85 and RFC 1924 Base85 require the input length to be a multiple of +// 4 bytes. `<<>>` has length 0 which trivially satisfies that, so the +// encode call must succeed and the round-trip must hold. + +pub fn empty_round_trip_z85_test() { + let assert Ok(encoded) = facade.encode_z85(<<>>) + facade.decode_z85(encoded) |> should.equal(Ok(<<>>)) +} + +pub fn empty_round_trip_rfc1924_base85_test() { + let assert Ok(encoded) = facade.encode_rfc1924_base85(<<>>) + facade.decode_rfc1924_base85(encoded) |> should.equal(Ok(<<>>)) +} + +// --- decode_* on the canonical empty string `""` --- +// +// The RFC 4648 family (Base16, Base32, Base32hex, Base64, Base64url and +// their variants) defines `encode(<<>>) == ""` directly. Pin that the +// decoder accepts `""` for these — non-RFC-4648 codecs may have their +// own empty-string conventions (e.g. Bech32 has no notion of an empty +// HRP), so we don't try to make `decode("")` a uniform contract beyond +// the RFC 4648 family. + +pub fn decode_empty_string_base16_test() { + facade.decode_base16("") |> should.equal(Ok(<<>>)) +} + +pub fn decode_empty_string_base32_test() { + facade.decode_base32("") |> should.equal(Ok(<<>>)) +} + +pub fn decode_empty_string_base32_hex_test() { + facade.decode_base32_hex("") |> should.equal(Ok(<<>>)) +} + +pub fn decode_empty_string_base64_test() { + facade.decode_base64("") |> should.equal(Ok(<<>>)) +} + +pub fn decode_empty_string_base64_urlsafe_test() { + facade.decode_base64_urlsafe("") |> should.equal(Ok(<<>>)) +} + +pub fn decode_empty_string_base64_nopadding_test() { + facade.decode_base64_nopadding("") |> should.equal(Ok(<<>>)) +} + +pub fn decode_empty_string_base64_urlsafe_nopadding_test() { + facade.decode_base64_urlsafe_nopadding("") |> should.equal(Ok(<<>>)) +}