Browse Source

Changes to Lang (fixes #33) with tests

microsub
J. King 7 years ago
parent
commit
6ec13266fa
  1. 3
      autoload.php
  2. 4
      bootstrap.php
  3. 20
      tests/TestConf.php
  4. 60
      tests/TestLang.php
  5. 9
      tests/bootstrap.php
  6. 29
      tests/phpunit.xml
  7. 106
      tests/testLangComplex.php
  8. 56
      vendor/JKingWeb/NewsSync/Lang.php
  9. 2
      vendor/JKingWeb/NewsSync/Lang/Exception.php

3
autoload.php

@ -0,0 +1,3 @@
<?php
require_once __DIR__.DIRECTORY_SEPARATOR."bootstrap.php";
$data = new RuntimeData(new Conf());

4
bootstrap.php

@ -8,6 +8,4 @@ const NS_BASE = __NAMESPACE__."\\";
if(!defined(NS_BASE."INSTALL")) define(NS_BASE."INSTALL", false); if(!defined(NS_BASE."INSTALL")) define(NS_BASE."INSTALL", false);
require_once BASE."vendor".DIRECTORY_SEPARATOR."autoload.php"; require_once BASE."vendor".DIRECTORY_SEPARATOR."autoload.php";
ignore_user_abort(true); ignore_user_abort(true);
$data = new RuntimeData(new Conf());

20
tests/TestConf.php

@ -19,9 +19,9 @@ class TestConf extends \PHPUnit\Framework\TestCase {
'confEmpty' => '', 'confEmpty' => '',
'confUnreadable' => '', 'confUnreadable' => '',
]); ]);
self::$path = self::$vfs->url(); self::$path = self::$vfs->url()."/";
// set up a file without read access // set up a file without read access
chmod(self::$path."/confUnreadable", 0000); chmod(self::$path."confUnreadable", 0000);
} }
static function tearDownAfterClass() { static function tearDownAfterClass() {
@ -48,9 +48,9 @@ class TestConf extends \PHPUnit\Framework\TestCase {
*/ */
function testImportFile() { function testImportFile() {
$conf = new Conf(); $conf = new Conf();
$conf->importFile(self::$path."/confGood"); $conf->importFile(self::$path."confGood");
$this->assertEquals("xx", $conf->lang); $this->assertEquals("xx", $conf->lang);
$conf = new Conf(self::$path."/confGood"); $conf = new Conf(self::$path."confGood");
$this->assertEquals("xx", $conf->lang); $this->assertEquals("xx", $conf->lang);
} }
@ -59,7 +59,7 @@ class TestConf extends \PHPUnit\Framework\TestCase {
*/ */
function testImportFileMissing() { function testImportFileMissing() {
$this->assertException("fileMissing", "Conf"); $this->assertException("fileMissing", "Conf");
$conf = new Conf(self::$path."/confMissing"); $conf = new Conf(self::$path."confMissing");
} }
/** /**
@ -67,7 +67,7 @@ class TestConf extends \PHPUnit\Framework\TestCase {
*/ */
function testImportFileEmpty() { function testImportFileEmpty() {
$this->assertException("fileCorrupt", "Conf"); $this->assertException("fileCorrupt", "Conf");
$conf = new Conf(self::$path."/confEmpty"); $conf = new Conf(self::$path."confEmpty");
} }
/** /**
@ -75,7 +75,7 @@ class TestConf extends \PHPUnit\Framework\TestCase {
*/ */
function testImportFileUnreadable() { function testImportFileUnreadable() {
$this->assertException("fileUnreadable", "Conf"); $this->assertException("fileUnreadable", "Conf");
$conf = new Conf(self::$path."/confUnreadable"); $conf = new Conf(self::$path."confUnreadable");
} }
/** /**
@ -83,7 +83,7 @@ class TestConf extends \PHPUnit\Framework\TestCase {
*/ */
function testImportFileNotAnArray() { function testImportFileNotAnArray() {
$this->assertException("fileCorrupt", "Conf"); $this->assertException("fileCorrupt", "Conf");
$conf = new Conf(self::$path."/confNotArray"); $conf = new Conf(self::$path."confNotArray");
} }
/** /**
@ -92,7 +92,7 @@ class TestConf extends \PHPUnit\Framework\TestCase {
function testImportFileNotPHP() { function testImportFileNotPHP() {
$this->assertException("fileCorrupt", "Conf"); $this->assertException("fileCorrupt", "Conf");
// this should not print the output of the non-PHP file // this should not print the output of the non-PHP file
$conf = new Conf(self::$path."/confNotPHP"); $conf = new Conf(self::$path."confNotPHP");
} }
/** /**
@ -101,6 +101,6 @@ class TestConf extends \PHPUnit\Framework\TestCase {
function testImportFileCorrupt() { function testImportFileCorrupt() {
$this->assertException("fileCorrupt", "Conf"); $this->assertException("fileCorrupt", "Conf");
// this should not print the output of the non-PHP file // this should not print the output of the non-PHP file
$conf = new Conf(self::$path."/confCorrupt"); $conf = new Conf(self::$path."confCorrupt");
} }
} }

60
tests/TestLang.php

@ -10,13 +10,16 @@ class TestLang extends \PHPUnit\Framework\TestCase {
static $vfs; static $vfs;
static $path; static $path;
static $files; static $files;
static $defaultPath;
static function setUpBeforeClass() { static function setUpBeforeClass() {
// this is required to keep from having exceptions in Lang::msg() in turn calling Lang::msg() and looping
Lang\Exception::$test = true; Lang\Exception::$test = true;
// test files
self::$files = [ self::$files = [
'en.php' => '<?php return ["Test.presentText" => "and the Philosopher\'s Stone"];', 'en.php' => '<?php return ["Test.presentText" => "and the Philosopher\'s Stone"];',
'en-ca.php' => '<?php return [];', 'en_ca.php' => '<?php return ["Test.presentText" => "{0} and {1}"];',
'en-us.php' => '<?php return ["Test.presentText" => "and the Sorcerer\'s Stone"];', 'en_us.php' => '<?php return ["Test.presentText" => "and the Sorcerer\'s Stone"];',
'fr.php' => '<?php return ["Test.presentText" => "à l\'école des sorciers"];', 'fr.php' => '<?php return ["Test.presentText" => "à l\'école des sorciers"];',
'ja.php' => '<?php return ["Test.absentText" => "賢者の石"];', 'ja.php' => '<?php return ["Test.absentText" => "賢者の石"];',
'de.php' => '<?php return ["Test.presentText" => "und der Stein der Weisen"];', 'de.php' => '<?php return ["Test.presentText" => "und der Stein der Weisen"];',
@ -24,7 +27,7 @@ class TestLang extends \PHPUnit\Framework\TestCase {
'it.php' => '<?php return 0;', 'it.php' => '<?php return 0;',
'zh.php' => '<?php return 0', 'zh.php' => '<?php return 0',
'ko.php' => 'DEAD BEEF', 'ko.php' => 'DEAD BEEF',
'fr-ca.php' => '', 'fr_ca.php' => '',
// unreadable file // unreadable file
'ru.php' => '', 'ru.php' => '',
]; ];
@ -32,16 +35,65 @@ class TestLang extends \PHPUnit\Framework\TestCase {
self::$path = self::$vfs->url(); self::$path = self::$vfs->url();
// set up a file without read access // set up a file without read access
chmod(self::$path."/ru.php", 0000); chmod(self::$path."/ru.php", 0000);
// make the Lang class use the vfs files
self::$defaultPath = Lang::$path;
Lang::$path = self::$path."/";
} }
static function tearDownAfterClass() { static function tearDownAfterClass() {
Lang\Exception::$test = false; Lang\Exception::$test = false;
Lang::$path = self::$defaultPath;
self::$path = null; self::$path = null;
self::$vfs = null; self::$vfs = null;
self::$files = null; self::$files = null;
Lang::set(Lang::DEFAULT, true);
} }
function testList() { function testList() {
$this->assertEquals(sizeof(self::$files), sizeof(Lang::list("en", "vfs://langtest/"))); $this->assertCount(sizeof(self::$files), Lang::list("en"));
} }
/**
* @depends testList
*/
function testSet() {
$this->assertEquals("en", Lang::set("en"));
$this->assertEquals("en_ca", Lang::set("en_ca"));
$this->assertEquals("de", Lang::set("de_ch"));
$this->assertEquals("en", Lang::set("en_gb_hixie"));
$this->assertEquals("en_ca", Lang::set("en_ca_jking"));
$this->assertEquals("en", Lang::set("es"));
$this->assertEquals("", Lang::set(""));
}
/**
* @depends testSet
*/
function testLoadInternalStrings() {
$this->assertEquals("", Lang::set("", true));
$this->assertCount(sizeof(Lang::REQUIRED), Lang::dump());
}
/**
* @depends testLoadInternalStrings
*/
function testLoadDefaultStrings() {
$this->assertEquals(Lang::DEFAULT, Lang::set(Lang::DEFAULT, true));
$str = Lang::dump();
$this->assertArrayHasKey('Exception.JKingWeb/NewsSync/Exception.uncoded', $str);
$this->assertArrayHasKey('Test.presentText', $str);
}
/**
* @depends testLoadDefaultStrings
*/
function testLoadMultipleFiles() {
Lang::set(Lang::DEFAULT, true);
$this->assertEquals("ja", Lang::set("ja", true));
$str = Lang::dump();
$this->assertArrayHasKey('Exception.JKingWeb/NewsSync/Exception.uncoded', $str);
$this->assertArrayHasKey('Test.presentText', $str);
$this->assertArrayHasKey('Test.absentText', $str);
}
} }

9
tests/bootstrap.php

@ -2,10 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\NewsSync; namespace JKingWeb\NewsSync;
const BASE = __DIR__.DIRECTORY_SEPARATOR."..".DIRECTORY_SEPARATOR; require_once __DIR__.DIRECTORY_SEPARATOR."..".DIRECTORY_SEPARATOR."bootstrap.php";
const NS_BASE = __NAMESPACE__."\\";
require_once BASE."vendor".DIRECTORY_SEPARATOR."autoload.php";
trait TestingHelpers { trait TestingHelpers {
function assertException(string $msg, string $prefix = "", string $type = "Exception") { function assertException(string $msg, string $prefix = "", string $type = "Exception") {
@ -19,6 +16,4 @@ trait TestingHelpers {
$this->expectException($class); $this->expectException($class);
$this->expectExceptionCode($code); $this->expectExceptionCode($code);
} }
} }
ignore_user_abort(true);

29
tests/phpunit.xml

@ -1,19 +1,20 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<phpunit <phpunit
colors="true" colors="true"
bootstrap="bootstrap.php" bootstrap="bootstrap.php"
convertErrorsToExceptions="true" convertErrorsToExceptions="true"
convertNoticesToExceptions="true" convertNoticesToExceptions="true"
convertWarningsToExceptions="true" convertWarningsToExceptions="true"
beStrictAboutTestsThatDoNotTestAnything="true" beStrictAboutTestsThatDoNotTestAnything="true"
beStrictAboutOutputDuringTests="true" beStrictAboutOutputDuringTests="true"
beStrictAboutTestSize="true"> beStrictAboutTestSize="true">
<testsuite name="Base">
<file>TestLang.php</file>
<file>TestConf.php</file>
</testsuite>
<testsuite name="Localization and exceptions">
<file>TestLang.php</file>
<file>TestLangComplex.php</file>
</testsuite>
<testsuite name="Configuration loading and saving">
<file>TestConf.php</file>
</testsuite>
</phpunit> </phpunit>

106
tests/testLangComplex.php

@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace JKingWeb\NewsSync;
use \org\bovigo\vfs\vfsStream;
class TestLangComplex extends \PHPUnit\Framework\TestCase {
use TestingHelpers;
static $vfs;
static $path;
static $files;
static $defaultPath;
static function setUpBeforeClass() {
// this is required to keep from having exceptions in Lang::msg() in turn calling Lang::msg() and looping
Lang\Exception::$test = true;
// test files
self::$files = [
'en.php' => '<?php return ["Test.presentText" => "and the Philosopher\'s Stone"];',
'en_ca.php' => '<?php return ["Test.presentText" => "{0} and {1}"];',
'en_us.php' => '<?php return ["Test.presentText" => "and the Sorcerer\'s Stone"];',
'fr.php' => '<?php return ["Test.presentText" => "à l\'école des sorciers"];',
'ja.php' => '<?php return ["Test.absentText" => "賢者の石"];',
'de.php' => '<?php return ["Test.presentText" => "und der Stein der Weisen"];',
// corrupt files
'it.php' => '<?php return 0;',
'zh.php' => '<?php return 0',
'ko.php' => 'DEAD BEEF',
'fr_ca.php' => '',
// unreadable file
'ru.php' => '',
];
self::$vfs = vfsStream::setup("langtest", 0777, self::$files);
self::$path = self::$vfs->url();
// set up a file without read access
chmod(self::$path."/ru.php", 0000);
// make the Lang class use the vfs files
self::$defaultPath = Lang::$path;
Lang::$path = self::$path."/";
}
static function tearDownAfterClass() {
Lang\Exception::$test = false;
Lang::$path = self::$defaultPath;
self::$path = null;
self::$vfs = null;
self::$files = null;
Lang::set(Lang::DEFAULT, true);
}
function setUp() {
Lang::set(Lang::DEFAULT, true);
}
function testLoadLazy() {
Lang::set("ja");
$this->assertArrayNotHasKey('Test.absentText', Lang::dump());
}
function testLoadCascade() {
Lang::set("ja", true);
$this->assertEquals("de", Lang::set("de", true));
$str = Lang::dump();
$this->assertArrayNotHasKey('Test.absentText', $str);
$this->assertEquals('und der Stein der Weisen', $str['Test.presentText']);
}
/**
* @depends testLoadCascade
*/
function testLoadSubtag() {
$this->assertEquals("en_ca", Lang::set("en_ca", true));
}
/**
* @depends testLoadSubtag
*/
function testMessage() {
Lang::set("de", true);
$this->assertEquals('und der Stein der Weisen', Lang::msg('Test.presentText'));
}
/**
* @depends testMessage
*/
function testMessageNumMSingle() {
Lang::set("en_ca", true);
$this->assertEquals('Default language file "en" missing', Lang::msg('Exception.JKingWeb/NewsSync/Lang/Exception.defaultFileMissing', Lang::DEFAULT));
}
/**
* @depends testMessage
*/
function testMessageNumMulti() {
Lang::set("en_ca", true);
$this->assertEquals('Happy Rotter and the Philosopher\'s Stone', Lang::msg('Test.presentText', ['Happy Rotter', 'the Philosopher\'s Stone']));
}
/**
* @depends testMessage
*/
function testMessageNamed() {
$this->assertEquals('Message string "Test.absentText" missing from all loaded language files (en)', Lang::msg('Exception.JKingWeb/NewsSync/Lang/Exception.stringMissing', ['msgID' => 'Test.absentText', 'fileList' => 'en']));
}
}

56
vendor/JKingWeb/NewsSync/Lang.php

@ -4,7 +4,6 @@ namespace JKingWeb\NewsSync;
use \Webmozart\Glob\Glob; use \Webmozart\Glob\Glob;
class Lang { class Lang {
const PATH = BASE."locale".DIRECTORY_SEPARATOR;
const DEFAULT = "en"; const DEFAULT = "en";
const REQUIRED = [ const REQUIRED = [
'Exception.JKingWeb/NewsSync/Exception.uncoded' => 'The specified exception symbol {0} has no code specified in Exception.php', 'Exception.JKingWeb/NewsSync/Exception.uncoded' => 'The specified exception symbol {0} has no code specified in Exception.php',
@ -16,6 +15,7 @@ class Lang {
'Exception.JKingWeb/NewsSync/Lang/Exception.stringInvalid' => 'Message string "{msgID}" is not a valid ICU message string (language files loaded: {fileList})', 'Exception.JKingWeb/NewsSync/Lang/Exception.stringInvalid' => 'Message string "{msgID}" is not a valid ICU message string (language files loaded: {fileList})',
]; ];
static public $path = BASE."locale".DIRECTORY_SEPARATOR;
static protected $requirementsMet = false; static protected $requirementsMet = false;
static protected $synched = false; static protected $synched = false;
static protected $wanted = self::DEFAULT; static protected $wanted = self::DEFAULT;
@ -25,13 +25,16 @@ class Lang {
protected function __construct() {} protected function __construct() {}
static public function set(string $locale = "", bool $immediate = false): string { static public function set(string $locale, bool $immediate = false): string {
if(!self::$requirementsMet) self::checkRequirements(); if(!self::$requirementsMet) self::checkRequirements();
if($locale=="") $locale = self::DEFAULT;
if($locale==self::$wanted) return $locale; if($locale==self::$wanted) return $locale;
$list = self::listFiles(); if($locale != "") {
if(!in_array(self::DEFAULT, $list)) throw new Lang\Exception("defaultFileMissing", self::DEFAULT); $list = self::listFiles();
self::$wanted = self::match($locale, $list); if(!in_array(self::DEFAULT, $list)) throw new Lang\Exception("defaultFileMissing", self::DEFAULT);
self::$wanted = self::match($locale, $list);
} else {
self::$wanted = "";
}
self::$synched = false; self::$synched = false;
if($immediate) self::load(); if($immediate) self::load();
return self::$wanted; return self::$wanted;
@ -41,12 +44,15 @@ class Lang {
return (self::$locale=="") ? self::DEFAULT : self::$locale; return (self::$locale=="") ? self::DEFAULT : self::$locale;
} }
static public function dump(): array {
return self::$strings;
}
static public function msg(string $msgID, $vars = null): string { static public function msg(string $msgID, $vars = null): string {
// if we're trying to load the system default language and it fails, we have a chicken and egg problem, so we catch the exception and load no language file instead // if we're trying to load the system default language and it fails, we have a chicken and egg problem, so we catch the exception and load no language file instead
if(!self::$synched) try {self::load();} catch(Lang\Exception $e) { if(!self::$synched) try {self::load();} catch(Lang\Exception $e) {
if(self::$wanted==self::DEFAULT) { if(self::$wanted==self::DEFAULT) {
self::set(); self::set("", true);
self::load();
} else { } else {
throw $e; throw $e;
} }
@ -64,9 +70,9 @@ class Lang {
return $msg; return $msg;
} }
static public function list(string $locale = "", string $path = self::PATH): array { static public function list(string $locale = ""): array {
$out = []; $out = [];
$files = self::listFiles($path); $files = self::listFiles();
foreach($files as $tag) { foreach($files as $tag) {
$out[$tag] = \Locale::getDisplayName($tag, ($locale=="") ? $tag : $locale); $out[$tag] = \Locale::getDisplayName($tag, ($locale=="") ? $tag : $locale);
} }
@ -85,10 +91,12 @@ class Lang {
return true; return true;
} }
static protected function listFiles(string $path = self::PATH): array { static protected function listFiles(): array {
$out = Glob::glob($path."*.php"); $out = glob(self::$path."*.php");
if(empty($out)) $out = Glob::glob(self::$path."*.php");
$out = array_map(function($file) { $out = array_map(function($file) {
$file = substr(str_replace(DIRECTORY_SEPARATOR, "/", $file),strrpos($file,"/")+1); $file = str_replace(DIRECTORY_SEPARATOR, "/", $file);
$file = substr($file, strrpos($file, "/")+1);
return strtolower(substr($file,0,strrpos($file,"."))); return strtolower(substr($file,0,strrpos($file,".")));
},$out); },$out);
natsort($out); natsort($out);
@ -96,21 +104,19 @@ class Lang {
} }
static protected function load(): bool { static protected function load(): bool {
self::$synched = true;
if(!self::$requirementsMet) self::checkRequirements(); if(!self::$requirementsMet) self::checkRequirements();
// if we've yet to request a locale, just load the fallback strings and return // if we've requested no locale (""), just load the fallback strings and return
if(self::$wanted=="") { if(self::$wanted=="") {
self::$strings = self::REQUIRED; self::$strings = self::REQUIRED;
self::$locale = self::$wanted; self::$locale = self::$wanted;
self::$synched = true;
return true; return true;
} }
// decompose the requested locale from specific to general, building a list of files to load // decompose the requested locale from specific to general, building a list of files to load
$tags = \Locale::parseLocale(self::$wanted); $tags = \Locale::parseLocale(self::$wanted);
$files = []; $files = [];
$loaded = [];
$strings = [];
while(sizeof($tags) > 0) { while(sizeof($tags) > 0) {
$files[] = \Locale::composeLocale($tags); $files[] = strtolower(\Locale::composeLocale($tags));
$tag = array_pop($tags); $tag = array_pop($tags);
} }
// include the default locale as the base if the most general locale requested is not the default // include the default locale as the base if the most general locale requested is not the default
@ -124,15 +130,21 @@ class Lang {
$files[] = $file; $files[] = $file;
} }
// if we need to load all files, start with the fallback strings // if we need to load all files, start with the fallback strings
if($files==$loaded) $strings[] = self::REQUIRED; $strings = [];
if($files==$loaded) {
$strings[] = self::REQUIRED;
} else {
// otherwise start with the strings we already have if we're going from e.g. "fr" to "fr_ca"
$strings[] = self::$strings;
}
// read files in reverse order // read files in reverse order
$files = array_reverse($files); $files = array_reverse($files);
foreach($files as $file) { foreach($files as $file) {
if(!file_exists(self::PATH."$file.php")) throw new Lang\Exception("fileMissing", $file); if(!file_exists(self::$path."$file.php")) throw new Lang\Exception("fileMissing", $file);
if(!is_readable(self::PATH."$file.php")) throw new Lang\Exception("fileUnreadable", $file); if(!is_readable(self::$path."$file.php")) throw new Lang\Exception("fileUnreadable", $file);
try { try {
ob_start(); ob_start();
$arr = (include self::PATH."$file.php"); $arr = (include self::$path."$file.php");
} catch(\Throwable $e) { } catch(\Throwable $e) {
$arr = null; $arr = null;
} finally { } finally {

2
vendor/JKingWeb/NewsSync/Lang/Exception.php

@ -18,7 +18,7 @@ class Exception extends \JKingWeb\NewsSync\Exception {
$code = self::CODES[$codeID]; $code = self::CODES[$codeID];
$msg = "Exception.".str_replace("\\","/",__CLASS__).".$msgID"; $msg = "Exception.".str_replace("\\","/",__CLASS__).".$msgID";
} }
\Exception::construct($msg, $code, $e); \Exception::__construct($msg, $code, $e);
} }
} }
} }
Loading…
Cancel
Save