/* * QR Code generator test suite (Solidity) * * Deploys QRCodeDemo to a local Hardhat network and validates the output of * every demo function against known-good values produced by the C reference * implementation in this repository. * * Run with: npx hardhat test * or: npm test * * Gas note: On-chain QR Code generation for small inputs costs 3–9 million gas. * Auto-mask selection evaluates all 8 mask patterns and can exceed Hardhat's * default per-transaction gas cap (16 777 216 gas, `FUSAKA_TRANSACTION_GAS_LIMIT` * in hardhat/src/internal/constants.ts). The fixed-mask demo functions * (doBasicDemoFixedMask, doNumericDemoFixedMask, etc.) are provided specifically * to allow byte-exact comparison against the C reference output without triggering * the 8-pass auto-mask penalty scoring. * * The expected byte strings were obtained by compiling and running the C reference * implementation (c/qrcodegen.c) with identical parameters. */ import { expect } from "chai"; import hre from "hardhat"; // --------------------------------------------------------------------------- // Utility: read a single module from raw QR Code bytes (JS-side helper) // --------------------------------------------------------------------------- function getModule(buf, x, y) { const size = buf[0]; if (x < 0 || x >= size || y < 0 || y >= size) return false; const index = y * size + x; const byteIdx = (index >> 3) + 1; // +1 because buf[0] is the size const bitIdx = index & 7; return ((buf[byteIdx] >> bitIdx) & 1) !== 0; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function hexToBytes(hex) { const h = hex.startsWith("0x") ? hex.slice(2) : hex; return Buffer.from(h, "hex"); } // --------------------------------------------------------------------------- // Test suite // --------------------------------------------------------------------------- describe("QRCode library", function () { let demo; before(async function () { const Demo = await hre.ethers.getContractFactory("QRCodeDemo"); demo = await Demo.deploy(); await demo.waitForDeployment(); }); // ----------------------------------------------------------------------- // 1. Byte-for-byte comparison against C reference output // (all use explicit fixed masks to stay within the ~16 M gas cap) // ----------------------------------------------------------------------- it("'Hello, world!' ECC_LOW MASK_2 matches C reference", async function () { // C: qrcodegen_encodeText("Hello, world!", …, Ecc_LOW, 1, 40, Mask_2, true) // MASK_AUTO also selects mask 2 for this input (verified with C reference). const expected = "157fd03f48097675ddaea0db3575839ee05ff50728007dce0733e622dd39535eb3170d01c2c79f3d0baa755d7bb9ab70747d93209af3b79400"; const qr = await demo.doBasicDemoFixedMask(); expect(hexToBytes(qr).toString("hex")).to.equal(expected); }); it("50-digit π string ECC_MEDIUM MASK_3 matches C reference", async function () { // MASK_AUTO also selects mask 3 for this input (verified with C reference). const expected = "197f41fc83ea0a7611d7eddaa9db254f3748a2e05f557f805c00edcaa4617a999dd9898804a9aaad4452e225bb57d5dcc4bb4fcc2fbf006a23fd7d540aba8ad6a5fbbb6ba668d7eead20c1617fd14e01"; const qr = await demo.doNumericDemoFixedMask(); expect(hexToBytes(qr).toString("hex")).to.equal(expected); }); it("version-5 HIGH MASK_2 no-boost matches C reference", async function () { const expected = "257f6508cf3f68a7ef0b76d55b29dd2e744aa6db45deac748376fdbee05f5555f507a8bfda005ce991fd9c56ef6a79bb0b047f44571c56f6bd2fdcd60729a7e6d607fb4ef64f6d0de054fc70fa2cadfe0049104b434d6803d3564d9e3aa5a6151aa557908cc828520456b6cbd5f0e92f5e4ab1c06ae47eee8f7667cc18ba8e62973892567445bf00e64f32f61fc408d60de234ab085d4516f1b52bb7cf93765dfcded820496061fe874add2601"; const qr = await demo.doVersionConstraintDemo(); expect(hexToBytes(qr).toString("hex")).to.equal(expected); }); it("'https://www.nayuki.io/' ECC_HIGH MASK_3 matches C reference", async function () { const expected = "1d7f90d53fc8bc0b76c951dd2ee7afdb55aa7483a0b0e05f55f507987000cc016961224f2cbe770339c40188488dcbeb3b9591ad9c95ee96c5f9dbcdfbb45bc469d014f35c0b1057f52b9e4cdf015630f65ffc560b229c085d60f7b76b01e175c556c220cab8f5a7c48b00"; const qr = await demo.doFixedMaskDemo(); expect(hexToBytes(qr).toString("hex")).to.equal(expected); }); it("binary 'Hello, world!' ECC_MEDIUM MASK_0 matches C reference", async function () { const expected = "157fd63f680a7669dd2eacdb557583ace05ff507e000550869272153c1fe024274661100b2db1f4c0f62045dbda88bb77565d42086f4d78801"; const qr = await demo.doBinaryDemoFixedMask(); expect(hexToBytes(qr).toString("hex")).to.equal(expected); }); it("mixed alphanumeric+numeric segment ECC_LOW MASK_0 matches C reference", async function () { const expected = "1d7fc4cd3f68280b76e104dd2ea0acdb75d87583809de05f55f507b0a00055e00849c1cc499d5da07ccf6115830593916751f86e70bc0e548c7038efad85dd322d311707051055dd499ec7645f012e2cca1fc9d50fc29c285d15f4ad8bb19d75955ede202034fe37173401"; const qr = await demo.doSegmentDemoFixedMask(); expect(hexToBytes(qr).toString("hex")).to.equal(expected); }); it("ECI(26)+UTF-8 bytes ECC_MEDIUM MASK_0 matches C reference", async function () { const expected = "157fc93f88097609ddaea3db657583ace05ff507c000742df291a763e1e7504154374201c6df1fe30eaaf15d53a48bfb748dc720ccfa07ea01"; const qr = await demo.doEciDemoFixedMask(); expect(hexToBytes(qr).toString("hex")).to.equal(expected); }); // ----------------------------------------------------------------------- // 2. Size / structural invariants for fixed-mask functions // ----------------------------------------------------------------------- it("doBasicDemoFixedMask: size is 21 modules (version 1)", async function () { const qr = await demo.doBasicDemoFixedMask(); const size = hexToBytes(qr)[0]; expect(size).to.equal(21); }); it("doNumericDemoFixedMask: size is 25 modules (version 2)", async function () { const qr = await demo.doNumericDemoFixedMask(); const size = hexToBytes(qr)[0]; expect(size).to.equal(25); }); it("doVersionConstraintDemo: size is 37 modules (version 5)", async function () { const qr = await demo.doVersionConstraintDemo(); const size = hexToBytes(qr)[0]; expect(size).to.equal(37); }); it("doFixedMaskDemo: size is 29 modules (version 3)", async function () { const qr = await demo.doFixedMaskDemo(); const size = hexToBytes(qr)[0]; expect(size).to.equal(29); }); it("doSegmentDemoFixedMask: size is 29 modules (version 3)", async function () { const qr = await demo.doSegmentDemoFixedMask(); const size = hexToBytes(qr)[0]; expect(size).to.equal(29); }); it("doEciDemoFixedMask: size is 21 modules (version 1)", async function () { const qr = await demo.doEciDemoFixedMask(); const size = hexToBytes(qr)[0]; expect(size).to.equal(21); }); // ----------------------------------------------------------------------- // 3. Buffer-length invariant: length == 1 + ceil(size² / 8) // ----------------------------------------------------------------------- const lengthCheckCases = [ ["doBasicDemoFixedMask", (d) => d.doBasicDemoFixedMask()], ["doNumericDemoFixedMask", (d) => d.doNumericDemoFixedMask()], ["doVersionConstraintDemo", (d) => d.doVersionConstraintDemo()], ["doFixedMaskDemo", (d) => d.doFixedMaskDemo()], ["doSegmentDemoFixedMask", (d) => d.doSegmentDemoFixedMask()], ["doEciDemoFixedMask", (d) => d.doEciDemoFixedMask()], ]; for (const [name, fn] of lengthCheckCases) { it(`${name}: buffer length equals 1 + ceil(size² / 8)`, async function () { const qr = await fn(demo); const buf = hexToBytes(qr); const size = buf[0]; const expected = 1 + Math.ceil(size * size / 8); expect(buf.length).to.equal(expected); }); } // ----------------------------------------------------------------------- // 4. Finder-pattern module checks (version 1, top-left corner) // ----------------------------------------------------------------------- it("finder pattern: outer corners dark, separator ring light, centre dark", async function () { const qr = await demo.doBasicDemoFixedMask(); const buf = hexToBytes(qr); // Top-left finder outer corners must be dark expect(getModule(buf, 0, 0)).to.be.true; expect(getModule(buf, 6, 0)).to.be.true; expect(getModule(buf, 0, 6)).to.be.true; expect(getModule(buf, 6, 6)).to.be.true; // Inner dark 3×3 centre expect(getModule(buf, 3, 3)).to.be.true; // Separator ring (Chebyshev distance 2 from centre (3,3)) must be light expect(getModule(buf, 1, 1)).to.be.false; expect(getModule(buf, 5, 1)).to.be.false; expect(getModule(buf, 1, 5)).to.be.false; expect(getModule(buf, 5, 5)).to.be.false; }); // ----------------------------------------------------------------------- // 5. Determinism: calling the same function twice gives the same output // ----------------------------------------------------------------------- it("encodeText is deterministic", async function () { const qr1 = await demo.doBasicDemoFixedMask(); const qr2 = await demo.doBasicDemoFixedMask(); expect(qr1).to.equal(qr2); }); // ----------------------------------------------------------------------- // 6. SVG output // ----------------------------------------------------------------------- it("toSvgString returns well-formed SVG", async function () { const qr = await demo.doBasicDemoFixedMask(); const svg = await demo.toSvgString(qr, 4); expect(svg).to.include(''); expect(svg).to.include('fill="#000000"'); expect(svg).to.include('fill="#FFFFFF"'); // viewBox = size + 2*border = 21 + 8 = 29 expect(svg).to.include('viewBox="0 0 29 29"'); }); });