You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
QR-Code-generator/solidity/test/QRCodeTest.js

228 lines
10 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* 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 39 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('<svg');
expect(svg).to.include('</svg>');
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"');
});
});