V minulém díle jsme vytvořili PHP část testovacího scriptu, která vygenerovala tabulku s výsledky testů na serveru. Dnes napíšeme JavaScriptovou část, která tytéž testy spustí v prohlížeči a porovná výsledky. To je klíčové pro ověření, že náš CombiScript opravdu vrací stejné hodnoty v obou jazycích.
Funkce tst_ToHTML v JavaScriptu
Začneme funkcí tst_ToHTML. Musíme ji napsat stejně jako v PHP, kde vypadala takto:
- function tst_ToHtml($x)
- {
- return nl2br(htmlspecialchars($x));
- }
JavaScript nemá vestavěnou funkci htmlspecialchars, takže si ji musíme naprogramovat sami. Když se podíváme do PHP manuálu, vidíme přesně, jaké znaky se konvertují. V JS použijeme několikanásobné nahrazení. Pořadí je důležité - ampersand musí být první, jinak bychom převáděli i ampersandy z již převedených entit.
Stejným způsobem nahradíme konce řádků za <br />. Carriage return považujeme za volitelný (proto regex \n\r?).
- function tst_ToHtml(x)
- {
- x = x.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
- x = x.replace(/\r?\n/g, '<br />\n');
- return x;
- }
Funkce Out bez statických proměnných
Dále vytvoříme JS variantu funkce Out. V PHP jsme využívali statickou proměnnou, ale JavaScript statické proměnné nemá. Musíme se tedy poradit jinak.
Řešení je překvapivě elegantní: každá JS funkce je objekt a může mít vlastnosti. Text si tedy schováme přímo do vlastnosti Out._buffer. To je užitečný trik, který se hodí i mimo CombiScript - zapamatujte si ho.
PHP verze pro srovnání:
- // 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';
- }
A nyní JS implementace využívající vlastnosti funkce:
- const TST_OUT_RESET = {};
- const TST_OUT_GET = {};
- function Out(x)
- {
- if (Out._buffer === undefined) Out._buffer = '';
- if (x === TST_OUT_RESET) return Out._buffer = '';
- if (x === TST_OUT_GET) return Out._buffer;
- if (typeof x === "string" || typeof x === "number" || typeof x === "boolean" || x === null)
- {
- Out._buffer += JSON.stringify(x);
- return;
- }
- if (x instanceof Error)
- {
- let s = (x instanceof cs_Error) ? '#Err:' : '?Err:';
- s += x.message;
- const data = x.getData();
- if (Array.isArray(data) && data.length > 0)
- {
- s += ' ' + JSON.stringify(data);
- }
- Out._buffer += s
- return;
- }
- Out._buffer += 'Out-UnknownType';
- }
Spuštění jednotlivých testů
Nakonec vytvoříme funkci tst_RunTest, která spustí jednu anonymní testovací funkci. V PHP vypadá takto:
- function tst_RunTest($test)
- {
- Out(TST_OUT_RESET);
- try
- {
- $test();
- }
- catch (Exception $e)
- {
- Out($e);
- }
- return Out(TST_OUT_GET);
- }
Přepis do JavaScriptu je přímočarý - jen změníme způsob zachytávání výjimek:
- function tst_RunTest($test)
- {
- Out(TST_OUT_RESET);
- try
- {
- $test();
- }
- catch (error)
- {
- Out(error)
- }
- return Out(TST_OUT_GET);
- }
Automatické spuštění všech testů
A zcela na závěr vytvoříme kód, který po načtení stránky automaticky spustí všechny testy v JavaScriptu. Logika je následující:
- Projdeme všechny testovací soubory, funkce a jednotlivé testy
- Pro každý test najdeme odpovídající řádek tabulky (vygenerovaný v PHP)
- Spustíme test a výsledek zapíšeme do buňky s indexem 3
- Porovnáme výsledek PHP (buňka 2) s výsledkem JS (buňka 3)
- Do buňky 4 zapíšeme "OK" nebo "Err" a obarvíme podle očekávání
- window.onload = function()
- {
- for (var $File in $Tests)
- for (var $Func in $Tests[$File])
- for (var $TestId=0; $TestId<$Tests[$File][$Func].length; $TestId++)
- {
- var $TestArr = $Tests[$File][$Func][$TestId];
- var trEl = document.getElementById($File+'-'+$Func+'-'+$TestId);
- if (!trEl) continue;
- trEl.cells[3].innerHTML = tst_ToHtml(tst_RunTest($TestArr[1]));
- trEl.cells[4].innerHTML = (trEl.cells[2].innerHTML===trEl.cells[3].innerHTML) ? 'OK' : 'Err';
- if ($TestArr[0]===1)
- {
- trEl.cells[4].style.color = (trEl.cells[4].innerText==='OK') ? 'green' : 'red';
- }
- }
- }
Kompletní testovací framework
Nyní máme hotovo. Celý testovací script včetně obou částí (PHP i JS) vypadá 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;
- }
- .tst_Source {
- white-space:pre;
- font-family:monospace;
- }
- </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');
- ?>
- </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>';
- 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 .= 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_ToHtml(tst_RunTest($TestArr[1])).'</TD>';
- echo '<TD></TD>';
- echo '<TD></TD>';
- echo '</TR>';
- }
- echo '</TABLE>';
- }
- }
- ?>
- </BODY>
- <SCRIPT>
- "use strict";
- var $Tests = {};
- </SCRIPT>
- <?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";
- function tst_ToHtml(x)
- {
- x = x.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
- x = x.replace(/\r?\n/g, '<br />\n');
- return x;
- }
- const TST_OUT_RESET = {};
- const TST_OUT_GET = {};
- function Out(x)
- {
- if (Out._buffer === undefined) Out._buffer = '';
- if (x === TST_OUT_RESET) return Out._buffer = '';
- if (x === TST_OUT_GET) return Out._buffer;
- if (typeof x === "string" || typeof x === "number" || typeof x === "boolean" || x === null)
- {
- Out._buffer += JSON.stringify(x);
- return;
- }
- if (x instanceof Error)
- {
- let s = (x instanceof cs_Error) ? '#Err:' : '?Err:';
- s += x.message;
- const data = x.getData();
- if (Array.isArray(data) && data.length > 0)
- {
- s += ' ' + JSON.stringify(data);
- }
- Out._buffer += s
- return;
- }
- Out._buffer += 'Out-UnknownType';
- }
- function tst_RunTest($test)
- {
- Out(TST_OUT_RESET);
- try
- {
- $test();
- }
- catch (error)
- {
- Out(error)
- }
- return Out(TST_OUT_GET);
- }
- window.onload = function()
- {
- for (var $File in $Tests)
- for (var $Func in $Tests[$File])
- for (var $TestId=0; $TestId<$Tests[$File][$Func].length; $TestId++)
- {
- var $TestArr = $Tests[$File][$Func][$TestId];
- var trEl = document.getElementById($File+'-'+$Func+'-'+$TestId);
- if (!trEl) continue;
- trEl.cells[3].innerHTML = tst_ToHtml(tst_RunTest($TestArr[1]));
- trEl.cells[4].innerHTML = (trEl.cells[2].innerHTML===trEl.cells[3].innerHTML) ? 'OK' : 'Err';
- if ($TestArr[0]===1)
- {
- trEl.cells[4].style.color = (trEl.cells[4].innerText==='OK') ? 'green' : 'red';
- }
- }
- }
- </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'],
- ];
V živé ukázce výše vidíte tabulku se "zelenými" výsledky - podle toho se PHP a JS v námi definovaných a otestovaných funkcích zatím chovají stejně. To je páteř naší důvěry v CombiScript: pokud testy procházejí zeleně, můžeme nanich klidně stavět složitější funkce - až po návrh celého CADu.
V příštím díle se vrátíme k rozšiřování knihovny nm_Util a podíváme se na další matematické funkce, které budeme potřebovat pro geometrické výpočty v našem CADu.