Files
volumen/internal/qrcode/qrcode_test.go
T

145 lines
4.1 KiB
Go
Raw Normal View History

2026-09-18 12:03:35 +02:00
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
package qrcode
import (
"os/exec"
"strings"
"testing"
)
// reference renders the same symbol with the system qrencode and
// returns its module matrix, or skips the test when the tool is absent.
func reference(t *testing.T, text string) [][]bool {
t.Helper()
binary, err := exec.LookPath("qrencode")
if err != nil {
t.Skip("qrencode is not installed; the differential test cannot run")
}
out, err := exec.Command(binary, "-l", "M", "-m", "0", "--type=ASCII", text).Output()
if err != nil {
t.Fatalf("qrencode: %v", err)
}
var rows [][]bool
for line := range strings.SplitSeq(string(out), "\n") {
line = strings.TrimSuffix(line, "\r")
if line == "" {
continue
}
// A row may be entirely light modules, which trims to spaces:
// only a truly empty line separates rows, so nothing is dropped.
if len(line)%2 != 0 {
t.Fatalf("odd ASCII line %q", line)
}
row := make([]bool, len(line)/2)
for i := range row {
row[i] = line[i*2] == '#'
}
rows = append(rows, row)
}
return rows
}
// referenceMask decodes the mask the reference chose from its own
// format information, using the layout this package writes.
func referenceMask(t *testing.T, ref [][]bool) int {
t.Helper()
var word uint
cells := func(y, x int) uint {
if ref[y][x] {
return 1
}
return 0
}
for i := range 6 {
word = word<<1 | cells(8, i)
}
word = word<<1 | cells(8, 7)
word = word<<1 | cells(8, 8)
word = word<<1 | cells(7, 8)
for i := range 6 {
word = word<<1 | cells(5-i, 8)
}
// The stored word carries the XOR mask; the data sits on top once
// it is undone.
return int((word^0x5412)>>10) & 7
}
func diffMatrices(t *testing.T, text string) {
t.Helper()
// The reference's own mask, forced: every other bit of the symbol
// must then match, which proves the encoding, the error correction,
// the placement and the format information. The mask each encoder
// picks for itself is a choice among equally valid symbols.
theirs := reference(t, text)
mask := referenceMask(t, theirs)
ours, err := encodeMasked([]byte(text), mask)
if err != nil {
t.Fatalf("encode %q: %v", text, err)
}
if len(theirs) != len(ours) {
t.Fatalf("%q: size %d, reference %d", text, len(ours), len(theirs))
}
for y := range ours {
for x := range ours[y] {
if ours[y][x] != theirs[y][x] {
t.Fatalf("%q: module differs at (%d,%d)", text, x, y)
}
}
}
}
// TestAgainstQrencode is the proof of the encoder: for the full range
// of payloads, every module must match the reference implementation
// once its own mask is forced. The payloads stay clear of the
// reference's segmentation optimiser: letters and underscores carry no
// alphanumeric runs worth switching for, so it speaks plain byte mode
// the way this package always does.
func TestAgainstQrencode(t *testing.T) {
payloads := []string{
"hello world",
"otpauth_totp_volumen_petr_secret_and_issuer_example_uri",
"https_petrbalvin_org_neco_delsi_cesta_query_hodnota_druha_ano",
strings.Repeat("a", 14), // v1 edge
strings.Repeat("a", 15), // v2
strings.Repeat("b", 84), // v5 edge
strings.Repeat("b", 85), // v6
strings.Repeat("c", 180), // v9 edge
strings.Repeat("c", 181), // v10 (16-bit count)
strings.Repeat("d", 450), // v16 edge
}
for i := range 10 {
payloads = append(payloads, strings.Repeat("x", 1+i*45))
}
for _, p := range payloads {
diffMatrices(t, p)
}
}
func TestSVGShape(t *testing.T) {
svg, err := SVG("https://example.com/")
if err != nil {
t.Fatalf("SVG: %v", err)
}
for _, want := range []string{
`viewBox="0 0 33 33"`, // v1 + quiet zone
`<rect width="100%" height="100%" fill="#fff"/>`,
`fill="#000"`,
`</svg>`,
} {
if !strings.Contains(svg, want) {
t.Fatalf("missing %q in %s", want, svg[:min(len(svg), 200)])
}
}
}
func TestTooLongIsRefused(t *testing.T) {
if _, err := SVG(strings.Repeat("z", 450)); err != nil {
t.Fatalf("version 16 capacity refused: %v", err)
}
if _, err := SVG(strings.Repeat("z", 451)); err == nil {
t.Fatal("input beyond version 16 accepted")
}
}