V minulém článku jsme definovali strukturu testů uložených v globální proměnné $Tests. Dnes se zaměříme na skript, který tyto testy spustí a zajistí konzistenci mezi PHP a JS – kritický požadavek pro naši kreativní platformu.
Struktura každého testu je jednoduchá:
- očekávání shody mezi PHP a JS (1 = ano, 0 = ne),
- anonymní funkce s voláním testovaného kódu,
- vysvětlení testu pro člověka.
- $Tests['nm_Util']['nm_IsNumber'][0] = [1, function()
- {
- Out(nm_IsNumber(5));
- },
- '5 je číslo'];
První výzva: zachytávání výstupů. K porovnání výsledků potřebujeme funkci Out(), která umí:
- Akumulovat výstupy (i při opakovaném volání v cyklech),
- Udržovat vnitřní stav,
- Resetovat stav mezi jednotlivými testy.
Místo globálních proměnných či objektů jsem se rozhodl pro statickou proměnnou uvnitř funkce – čisté řešení bez znečištění globálního prostoru:
- // Definice jednoznačných instancí
- define('TST_OUT_RESET', new stdClass());
- define('TST_OUT_GET', new stdClass());
- function Out($x)
- {
- static $buffer = '';
- if ($x === TST_OUT_RESET) return $buffer = '';
- if ($x === TST_OUT_GET) return $buffer;
- if (is_string($x) || is_int($x) || is_float($x) || is_bool($x) || is_null($x))
- {
- $buffer .= tst_ToHtml(json_encode($x, JSON_UNESCAPED_UNICODE));
- return;
- }
- if ($x instanceof Exception)
- {
- $s = ($x instanceof cs_Error) ? '#Err:' : '?Err:';
- $s .= $x->getMessage();
- if ($x instanceof cs_Error)
- {
- $data = $x->getData();
- if ($data && is_array($data) && count($data))
- {
- $s .= ' ' . json_encode($data, JSON_UNESCAPED_UNICODE);
- }
- }
- $buffer .= $s;
- return;
- }
- $buffer .= 'Out-UnknownType';
- }
Pro řízení funkce Out() jsem se rozhodl použít instance objektů protože nějaký "speciální string" by určitě mohl kolidovat s výsledkem testu.
Prozatím ukládáme data jako JSON – jednoduchý způsob, jak odlišit např. 5 (číslo) od "5" (řetězec). Očekáváme, že serializace v PHP a JS bude kompatibilní (později otestujeme hranice tohoto předpokladu :-).
Dále si definujeme základní strukturu stránky a v ní si definujeme, jaké knihovny budeme vkládat (jak pro PHP tak pro JS). Abychom tyto knihovny zadávalli jen 1x tak si je dáme do pole. A z tohoto pole je budeme cyklem vkládat jak do PHP (pomocí include), tak do JS (jako hodnotu atributu scr tagu SCRIPT). Pro systénové knihovny si určíme podadresář cs_src pro testovací cs_tests.
Základ našeho testovací scriptu bude tedy vypadat takto:
- <!DOCTYPE html>
- <HTML lang="cs">
- <HEAD>
- <META charset="UTF-8"/>
- <META name="viewport" content="width=device-width, initial-scale=1.0">
- <LINK rel="shortcut icon" href="./favicon.ico"/>
- <TITLE>CombiScript Tests</TITLE>
- <STYLE>
- BODY {
- background-color:#EEE;
- font-family:sans-serif;
- }
- .Tests {
- border-collapse: collapse;
- width:100%;
- }
- .Tests TD, .Tests TH {
- border:1px solid black;
- position:relative;
- }
- </STYLE>
- </HEAD>
- <BODY>
- <?php
- $Libs = ['cs_Util', 'nm_Util'];
- $TestLibs = ['nm_Util_Test'];
- foreach ($Libs AS $Nr => $Lib)
- include('./cs_src/'.$Lib.'.php.js');
- foreach ($TestLibs AS $Nr => $Lib)
- include('./cs_tests/'.$Lib.'.php.js');
Pro každý test generujeme HTML tabulku s pěti sloupci:
- Popis testu,
- Zdrojový kód,
- Výsledek v PHP,
- Výsledek v JS (později),
- Shoda/rozdíl (barevně odlišená).
Každý řádek má unikátní ID pro snadné ovládání z JavaScriptu:
- foreach ($Tests AS $File=>$FileArr)
- {
- foreach ($FileArr AS $Func=>$FuncArr)
- {
- echo '<H3>'.$Func.'</H3>';
- echo '<TABLE class=Tests><TR><TH>Popis</TH><TH>Zdroj</TH><TH>PHP</TH><TH>JS</TH><TH>Vysledek</TH></TR>';
- foreach ($FuncArr AS $TestId=>$TestArr)
- {
- echo '<TR id="'.$File.'-'.$Func.'-'.$TestId.'">';
- echo '<TD>'.tst_ToHtml($TestArr[2]).'</TD>';
- echo '<TD style="white-space:pre; font-family:monospace">'.htmlspecialchars(tst_GetClosureCode($TestArr[1])).'</TD>';
- echo '<TD>'.tst_RunTest($TestArr[1]).'</TD>';
- echo '<TD></TD>';
- echo '<TD></TD>';
- echo '</TR>';
- }
- echo '</TABLE>';
- }
- }
tst_ToHtml pro převedení textu na HTML:
- function tst_ToHtml($x)
- {
- return nl2br(htmlspecialchars($x));
- }
Obtížnější je funkce tst_SourceAnonymFunc, která zjistí zdrojový kód předané anonymní funkce. PHP bohužel nemá žádnou funkci, která by vracela zdrojový kód anonymní funkce v proměnné a proto si ji musíme napsat sami.
Takže jak na to půjdeme? Pomocí ReflectionFunction si zjistíme soubor a řádky kde funkce začíná a končí a vyparsujeme ji. Parsování bychom mohli udělat přes tokenizaci, ale zvolíme jednodušší metodu pomocí regulárního výrazu, s tím, že definujeme, že první výskyt function ( na řadku kde je deklarace funkce musí být tato funkce a funkce končí posledním výskytem }
- function tst_GetClosureCode($func)
- {
- static $cache = [];
- $ref = new ReflectionFunction($func);
- if (!$ref->isUserDefined()) return null;
- $file = $ref->getFileName();
- if (!isset($cache[$file]))
- $cache[$file] = file($file);
- $start = $ref->getStartLine() - 1;
- $end = $ref->getEndLine();
- $ret = implode('', array_slice($cache[$file], $start, $end - $start)); //\n uz je soucasti kazdy line ziskane pomoci funkce file()
- if (preg_match('/function\s*\(/i', $ret, $matches, PREG_OFFSET_CAPTURE)) $Begin = $matches[0][1]; //Pocitam s tim ze je to prvni "function (" na radku kde zacina deklarace funkce
- $End = strrpos($ret, '}'); //Pocitam s tim ze je to posledni "}" na radku kde konci deklarace funkce
- $ret = substr($ret, $Begin, $End-$Begin+1);
- return $ret;
- }
No a nakonec máme funkci tst_RunTest, u teré si musíme uvědomit, že běžící kód může vygenerovat i výjimku a my ji chceme převést na string.
- function tst_RunTest($test)
- {
- Out(TST_OUT_RESET);
- try
- {
- $test();
- }
- catch (Exception $e)
- {
- Out($e);
- }
- return Out(TST_OUT_GET);
- }
No a pokud všechny scripty spojíme získáme takovýto výstup:
- <!DOCTYPE html>
- <HTML lang="cs">
- <HEAD>
- <META charset="UTF-8"/>
- <META name="viewport" content="width=device-width, initial-scale=1.0">
- <LINK rel="shortcut icon" href="./favicon.ico"/>
- <TITLE>CombiScript Tests</TITLE>
- <STYLE>
- BODY {
- background-color:#EEE;
- font-family:sans-serif;
- }
- .Tests {
- border-collapse: collapse;
- width:100%;
- }
- .Tests TD, .Tests TH {
- border:1px solid black;
- position:relative;
- }
- </STYLE>
- </HEAD>
- <BODY>
- <?php
- $Libs = ['cs_Util', 'nm_Util'];
- $TestLibs = ['nm_Util_Test'];
- foreach ($Libs AS $Nr => $Lib)
- include('./cs_src/'.$Lib.'.php.js');
- foreach ($TestLibs AS $Nr => $Lib)
- include('./cs_tests/'.$Lib.'.php.js');
- function tst_ToHtml($x)
- {
- return nl2br(htmlspecialchars($x));
- }
- // Definice jednoznačných instancí
- define('TST_OUT_RESET', new stdClass());
- define('TST_OUT_GET', new stdClass());
- function Out($x)
- {
- static $buffer = '';
- if ($x === TST_OUT_RESET) return $buffer = '';
- if ($x === TST_OUT_GET) return $buffer;
- if (is_string($x) || is_int($x) || is_float($x) || is_bool($x) || is_null($x))
- {
- $buffer .= tst_ToHtml(json_encode($x, JSON_UNESCAPED_UNICODE));
- return;
- }
- if ($x instanceof Exception)
- {
- $s = ($x instanceof cs_Error) ? '#Err:' : '?Err:';
- $s .= $x->getMessage();
- if ($x instanceof cs_Error)
- {
- $data = $x->getData();
- if ($data && is_array($data) && count($data))
- {
- $s .= ' ' . json_encode($data, JSON_UNESCAPED_UNICODE);
- }
- }
- $buffer .= $s;
- return;
- }
- $buffer .= 'Out-UnknownType';
- }
- function tst_RunTest($test)
- {
- Out(TST_OUT_RESET);
- try
- {
- $test();
- }
- catch (Exception $e)
- {
- Out($e);
- }
- return Out(TST_OUT_GET);
- }
- function tst_GetClosureCode($func)
- {
- static $cache = [];
- $ref = new ReflectionFunction($func);
- if (!$ref->isUserDefined()) return null;
- $file = $ref->getFileName();
- if (!isset($cache[$file]))
- $cache[$file] = file($file);
- $start = $ref->getStartLine() - 1;
- $end = $ref->getEndLine();
- $ret = implode('', array_slice($cache[$file], $start, $end - $start)); //\n uz je soucasti kazdy line ziskane pomoci funkce file()
- if (preg_match('/function\s*\(/i', $ret, $matches, PREG_OFFSET_CAPTURE)) $Begin = $matches[0][1]; //Pocitam s tim ze je to prvni "function (" na radku kde zacina deklarace funkce
- $End = strrpos($ret, '}'); //Pocitam s tim ze je to posledni "}" na radku kde konci deklarace funkce
- $ret = substr($ret, $Begin, $End-$Begin+1);
- return $ret;
- }
- foreach ($Tests AS $File=>$FileArr)
- {
- foreach ($FileArr AS $Func=>$FuncArr)
- {
- echo '<H3>'.$Func.'</H3>';
- echo '<TABLE class=Tests><TR><TH>Popis</TH><TH>Zdroj</TH><TH>PHP</TH><TH>JS</TH><TH>Vysledek</TH></TR>';
- foreach ($FuncArr AS $TestId=>$TestArr)
- {
- echo '<TR id="'.$File.'-'.$Func.'-'.$TestId.'">';
- echo '<TD>'.tst_ToHtml($TestArr[2]).'</TD>';
- echo '<TD style="white-space:pre; font-family:monospace">'.htmlspecialchars(tst_GetClosureCode($TestArr[1])).'</TD>';
- echo '<TD>'.tst_RunTest($TestArr[1]).'</TD>';
- echo '<TD></TD>';
- echo '<TD></TD>';
- echo '</TR>';
- }
- echo '</TABLE>';
- }
- }
- ?>
- </BODY>
- <?php
- foreach ($Libs AS $Nr => $Lib)
- echo '<SCRIPT src="./cs_src/'.$Lib.'.php.js?'.date('YmdHis').'"></SCRIPT>';
- foreach ($TestLibs AS $Nr => $Lib)
- echo '<SCRIPT src="./cs_tests/'.$Lib.'.php.js?'.date('YmdHis').'"></SCRIPT>';
- ?>
- <SCRIPT>
- "use strict";
- </SCRIPT>
- </HTML>
- <!-- --><?php /*
- "use strict";
- //*/
- function nm_IsNumber($num)
- {
- 0&&0<!--$_;/*
- return typeof $num === 'number'
- /*/
- return is_int($num) OR is_float($num);
- //*/
- }
- function nm_Abs($num)
- {
- if (!nm_IsNumber($num)) throw new cs_Error('nm_Abs-$num-NotNumber', [cs_Type($num)]);
- 0&&0<!--$_;/*
- return Math.abs($num);
- /*/
- return abs($num);
- //*/
- }
- function nm_Sign($num)
- {
- if (!nm_IsNumber($num)) throw new cs_Error('nm_Sign-$num-NotNumber', [cs_Type($num)]);
- 0&&0<!--$_;/*
- return Math.sign($num);
- /*/
- return ($num > 0) - ($num < 0);
- //*/
- }
- function nm_IsAsInt($num)
- {
- 0&&0<!--$_;/*
- return Number.isInteger($num);
- /*/
- if (is_int($num)) return true;
- return (is_float($num) && floor($num) === $num);
- //*/
- }
- function nm_Pi()
- {
- 0&&0<!--$_;/*
- return Math.PI;
- /*/
- return Pi();
- //*/
- }
- function nm_Rad2Deg($rad)
- {
- if (!nm_IsNumber($rad)) throw new cs_Error('nm_Rad2Deg-$rad-NotNumber', [cs_Type($rad)]);
- 0&&0<!--$_;/*
- return $rad * (180/Math.PI);
- /*/
- return rad2deg($rad);
- //*/
- }
- function nm_Deg2Rad($deg)
- {
- if (!nm_IsNumber($deg)) throw new cs_Error('nm_Deg2Rad-$deg-NotNumber', [cs_Type($deg)]);
- 0&&0<!--$_;/*
- return $deg/180*Math.PI;
- /*/
- return deg2rad($deg);
- //*/
- }
- function nm_Sin($rad)
- {
- if (!nm_IsNumber($rad)) throw new cs_Error('nm_Sin-$rad-NotNumber', [cs_Type($rad)]);
- 0&&0<!--$_;/*
- return Math.sin($rad);
- /*/
- return sin($rad);
- //*/
- }
- function nm_Cos($rad)
- {
- if (!nm_IsNumber($rad)) throw new cs_Error('nm_Cos-$rad-NotNumber', [cs_Type($rad)]);
- 0&&0<!--$_;/*
- return Math.cos($rad);
- /*/
- return cos($rad);
- //*/
- }
- function nm_Tan($rad)
- {
- if (!nm_IsNumber($rad)) throw new cs_Error('nm_Tan-$rad-NotNumber', [cs_Type($rad)]);
- 0&&0<!--$_;/*
- return Math.tan($rad);
- /*/
- return tan($rad);
- //*/
- }
- function nm_ASin($num)
- {
- if (!nm_IsNumber($num)) throw new cs_Error('nm_ASin-$num-NotNumber', [cs_Type($num)]);
- if ($num>1 || $num<-1) throw new cs_Error('nm_ASin-$num-OutOfRange');
- 0&&0<!--$_;/*
- return Math.asin($num);
- /*/
- return asin($num);
- //*/
- }
- function nm_ACos($num)
- {
- if (!nm_IsNumber($num)) throw new cs_Error('nm_ACos-$num-NotNumber', [cs_Type($num)]);
- if ($num>1 || $num<-1) throw new cs_Error('nm_ACos-$num-OutOfRange');
- 0&&0<!--$_;/*
- return Math.acos($num);
- /*/
- return acos($num);
- //*/
- }
- function nm_ATan($num)
- {
- if (!nm_IsNumber($num)) throw new cs_Error('nm_ATan-$num-NotNumber', [cs_Type($num)]);
- 0&&0<!--$_;/*
- return Math.atan($num);
- /*/
- return atan($num);
- //*/
- }
- function nm_ATan2($y, $x)
- {
- if (!nm_IsNumber($y)) throw new cs_Error('nm_ATan2-$y-NotNumber', [cs_Type($y)]);
- if (!nm_IsNumber($x)) throw new cs_Error('nm_ATan2-$x-NotNumber', [cs_Type($x)]);
- 0&&0<!--$_;/*
- return Math.atan2($y, $x);
- /*/
- return atan2($y, $x);
- //*/
- }
- <!-- --><?php /*
- "use strict";
- class cs_Error extends Error
- {
- constructor($message, $data=[])
- {
- super($message);
- if (Error.captureStackTrace) Error.captureStackTrace(this, cs_Error);
- this.data = $data;
- }
- getData()
- {
- return this.data;
- }
- }
- /*/
- class cs_Error extends Exception
- {
- private $_data = '';
- public function __construct($message, $data=[])
- {
- $this->_data = $data;
- parent::__construct($message);
- }
- public function getData()
- {
- return $this->_data;
- }
- }
- //*/
- function cs_Type($var)
- {
- if ($var === null) return 'Null';
- if (nm_IsNumber($var)) return 'Number';
- //if (bl_IsBoolean($var)) return 'Boolean'; //TODO next
- //if (st_IsString($var)) return 'String';
- //if (ar_IsArray($var)) return 'Array';
- return 'Unknown';
- }
- <!-- --><?php
- "use strict";
- $Tests['nm_Util'] = [];
- $Tests['nm_Util']['nm_IsNumber'] = [
- [1, function()
- {
- Out(nm_IsNumber(5));
- },
- 'Ověřuje, že kladné celé číslo je číslo'],
- [1, function()
- {
- Out(nm_IsNumber(-5));
- },
- 'Ověřuje, že záporné celé číslo je číslo'],
- [1, function()
- {
- Out(nm_IsNumber(0));
- },
- 'Ověřuje, že nula je číslo'],
- [1, function()
- {
- Out(nm_IsNumber(2.5));
- },
- 'Ověřuje, že kladné desetinné číslo je číslo'],
- [1, function()
- {
- Out(nm_IsNumber(-2.5));
- },
- 'Ověřuje, že záporné desetinné číslo je číslo'],
- [1, function()
- {
- Out(nm_IsNumber(.5));
- },
- 'Ověřuje, že desetinné číslo s vynechanou úvodní nulou je číslo'],
- [1, function()
- {
- Out(nm_IsNumber(-.5));
- },
- 'Ověřuje, že záporné desetinné číslo s vynechanou úvodní nulou je číslo'],
- [1, function()
- {
- Out(nm_IsNumber(3.21E4));
- },
- 'Ověřuje, že číslo v exponenciálním tvaru je číslo'],
- [1, function()
- {
- Out(nm_IsNumber(-3.21E4));
- },
- 'Ověřuje, že záporné číslo v exponenciálním tvaru je číslo'],
- [1, function()
- {
- Out(nm_IsNumber(3.21E-4));
- },
- 'Ověřuje, že číslo s negativním exponentem je číslo'],
- [1, function()
- {
- Out(nm_IsNumber(-3.21E-4));
- },
- 'Ověřuje, že záporné číslo s negativním exponentem je číslo'],
- [1, function()
- {
- Out(nm_IsNumber(-3.21e-4));
- },
- 'Ověřuje, že se akceptuje malé "e" v exponenciálním tvaru'],
- [1, function()
- {
- Out(nm_IsNumber("123"));
- },
- 'Ověřuje, že string obsahující číslo není číslo'],
- [1, function()
- {
- Out(nm_IsNumber([1]));
- },
- 'Ověřuje, že pole není číslo']
- ];
- $Tests['nm_Util']['nm_IsAsInt'] = [
- [1, function()
- {
- Out(nm_IsAsInt(5));
- },
- 'Ověřuje, že kladné celé číslo je celé číslo'],
- [1, function()
- {
- Out(nm_IsAsInt(-5));
- },
- 'Ověřuje, že záporné celé číslo je celé číslo'],
- [1, function()
- {
- Out(nm_IsAsInt(5.0));
- },
- 'Ověřuje, že celočíselný float je celé číslo'],
- [1, function()
- {
- Out(nm_IsAsInt(-5.0));
- },
- 'Ověřuje, že záporný celočíselný float je celé číslo'],
- [1, function()
- {
- Out(nm_IsAsInt(0));
- },
- 'Ověřuje, že nula je celé číslo'],
- [1, function()
- {
- Out(nm_IsAsInt(-0));
- },
- 'Ověřuje, že minus nula je celé číslo'],
- [1, function()
- {
- Out(nm_IsAsInt(2.5));
- },
- 'Ověřuje, že kladné desetinné číslo není celé číslo'],
- [1, function()
- {
- Out(nm_IsAsInt(-2.5));
- },
- 'Ověřuje, že záporné desetinné číslo není celé číslo'],
- [1, function()
- {
- Out(nm_IsAsInt("123"));
- },
- 'Ověřuje, že string obsahující číslo není celé číslo'],
- [1, function()
- {
- Out(nm_IsAsInt(9007199254740991.5));
- },
- 'Ověřuje, že velké číslo s desetinnou částí se stále jeví jako integer (rozložení na číselné ose je už tak hrubé)'],
- [1, function()
- {
- Out(nm_IsAsInt(4.9999999999999995));
- },
- 'Toto ještě není 5'],
- [1, function()
- {
- Out(nm_IsAsInt(4.9999999999999996));
- },
- 'Toto už je 5']
- ];
- $Tests['nm_Util']['nm_Abs'] = [
- [1, function()
- {
- Out(nm_Abs(2.5));
- },
- 'Ověřuje, že absolutní hodnota kladného čísla je to samé číslo'],
- [1, function()
- {
- Out(nm_Abs(-2.5));
- },
- 'Ověřuje, že absolutní hodnota záporného čísla změní znaménko na kladné'],
- [1, function()
- {
- Out(nm_Abs(0));
- },
- 'Ověřuje, že absolutní hodnota nuly je nula'],
- [1, function()
- {
- Out(nm_Abs("123"));
- },
- 'Ověřuje, že pro nečíselný parametr se vyhodí chyba'],
- ];
- $Tests['nm_Util']['nm_Sign'] = [
- [1, function()
- {
- Out(nm_Sign(2.5));
- },
- 'Ověřuje, že signum kladného čísla je +1'],
- [1, function()
- {
- Out(nm_Sign(-2.5));
- },
- 'Ověřuje, že signum záporného čísla je -1'],
- [1, function()
- {
- Out(nm_Sign(0));
- },
- 'Ověřuje, že signum nuly je 0'],
- [1, function()
- {
- Out(nm_Sign(-0));
- },
- 'Ověřuje, že signum záporné nuly je také 0'],
- [1, function()
- {
- Out(nm_Sign("0"));
- },
- 'Ověřuje, že pro nečíselný parametr se vyhodí chyba'],
- ];
A příště si testovací script dokončíme doplněním jeho JS části