<?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");
}
}