/ tests / QrCodeTest.php
<?php
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 */
namespace seekquarry\yioop\tests;

use seekquarry\yioop\library\QrCode;
use seekquarry\yioop\library\UnitTest;

/**
 * Checks the square symbol Yioop now draws itself rather than asking the
 * qrencode command for. The arithmetic behind it cannot be eyeballed, so
 * these check it three ways: against the values the standard publishes,
 * against a property that must hold whatever the text is, and against the
 * shapes a reader looks for to find its bearings.
 */
class QrCodeTest extends UnitTest
{
    /**
     * Nothing is set up before a case: the encoder is arithmetic over the
     * text it is given and keeps nothing between calls.
     */
    public function setUp()
    {
    }
    /**
     * Nothing is left behind after a case.
     */
    public function tearDown()
    {
    }
    /**
     * The polynomial used to make ten correction bytes is published in the
     * standard, so it can be compared outright.
     */
    public function correctionPolynomialMatchesTheStandardTestCase()
    {
        $this->assertEqual(QrCode::correctionPolynomial(10),
            [0, 251, 67, 46, 61, 118, 70, 64, 94, 32, 45],
            "the polynomial for ten correction bytes is the published one");
        $this->assertEqual(count(QrCode::correctionPolynomial(26)), 27,
            "one for twenty-six has twenty-seven terms");
    }
    /**
     * A block with its correction bytes on the end must divide by the same
     * polynomial with nothing left over. That has to hold whatever the
     * text is, so it checks the arithmetic without another encoder to
     * compare against.
     */
    public function correctionDividesWithNothingLeftOverTestCase()
    {
        foreach ([10, 16, 18, 22, 24, 26] as $wanted) {
            $block = QrCode::messageBytes("a piece of text", 3);
            $correction = QrCode::correctionFor($block, $wanted);
            $again = QrCode::correctionFor(
                array_merge($block, $correction), $wanted);
            $clean = true;
            foreach ($again as $value) {
                if ($value != 0) {
                    $clean = false;
                }
            }
            $this->assertTrue($clean,
                "$wanted correction bytes divide with nothing left over");
        }
    }
    /**
     * The message begins with a mark saying it is written as plain bytes,
     * then how many there are, then those bytes, and is filled out to the
     * length its size expects with two values used turn about.
     */
    public function messageCarriesTheTextAndItsLengthTestCase()
    {
        $message = QrCode::messageBytes("Hi", 1);
        $this->assertEqual(count($message), 16,
            "size one holds sixteen bytes at this level");
        $this->assertEqual($message[0], 0x40 | 0,
            "the mark says plain bytes, and the length begins");
        $this->assertEqual($message[1], (2 << 4) | (ord("H") >> 4),
            "the length and the first letter share a byte");
        $this->assertEqual($message[14], 0xEC,
            "the filling is the first of the two values");
        $this->assertEqual($message[15], 0x11,
            "and then the second");
    }
    /**
     * The symbol grows with the text, and text too long for the largest
     * size made here is refused rather than drawn wrongly.
     */
    public function symbolGrowsWithTheTextTestCase()
    {
        $this->assertEqual(count(QrCode::grid("short")), 21,
            "a few letters fit the smallest symbol, twenty-one across");
        $this->assertEqual(count(QrCode::grid(str_repeat("a", 100))), 41,
            "a hundred letters need one twenty-one across");
        $this->assertTrue(QrCode::grid(str_repeat("a", 500)) === false,
            "more than the largest size holds is refused");
    }
    /**
     * Every symbol carries the same shapes in the same places: a square
     * with a clear ring inside it at three corners, a dotted line joining
     * two of them, and one square that is always dark.
     */
    public function everySymbolCarriesItsBearingsTestCase()
    {
        foreach (["short", "https://www.seekquarry.com/",
            str_repeat("a", 100)] as $text) {
            $grid = QrCode::grid($text);
            $width = count($grid);
            $this->assertTrue($grid[0][0] && $grid[0][6] && $grid[6][6],
                "the corner square's outside is dark for '$text'");
            $this->assertTrue(!$grid[1][1] && $grid[2][2],
                "with a clear ring and a dark middle for '$text'");
            $dotted = true;
            for ($along = 8; $along < $width - 8; $along++) {
                if ($grid[6][$along] !== ($along % 2 == 0)) {
                    $dotted = false;
                }
            }
            $this->assertTrue($dotted,
                "the dotted line alternates for '$text'");
            $this->assertTrue($grid[$width - 8][8],
                "the always-dark square is dark for '$text'");
        }
    }
    /**
     * The picture drawn from a symbol says how wide it is, leaves clear
     * space around it, and holds a dark shape for every dark square.
     */
    public function pictureDrawsEverySquareTestCase()
    {
        $grid = QrCode::grid("short");
        $dark = 0;
        foreach ($grid as $row) {
            foreach ($row as $square) {
                if ($square) {
                    $dark++;
                }
            }
        }
        $picture = QrCode::svg("short");
        $this->assertTrue(strpos($picture, 'viewBox="0 0 29 29"') !== false,
            "twenty-one across with four squares clear either side");
        $this->assertEqual(substr_count($picture, "h1v1h-1z"), $dark,
            "one shape drawn for every dark square");
    }
}
X