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);
require_once BASE."vendor".DIRECTORY_SEPARATOR."autoload.php";
ignore_user_abort(true);
$data = new RuntimeData(new Conf());
ignore_user_abort(true);

20
tests/TestConf.php

@ -19,9 +19,9 @@ class TestConf extends \PHPUnit\Framework\TestCase {
'confEmpty' => '',
'confUnreadable' => '',
]);
self::$path = self::$vfs->url();
self::$path = self::$vfs->url()."/";
// set up a file without read access
chmod(self::$path."/confUnreadable", 0000);
chmod(self::$path."confUnreadable", 0000);
}
static function tearDownAfterClass() {
@ -48,9 +48,9 @@ class TestConf extends \PHPUnit\Framework\TestCase {
*/
function testImportFile() {
$conf = new Conf();
$conf->importFile(self::$path."/confGood");
$conf->importFile(self::$path."confGood");
$this->assertEquals("xx", $conf->lang);
$conf = new Conf(self::$path."/confGood");
$conf = new Conf(self::$path."confGood");
$this->assertEquals("xx", $conf->lang);
}
@ -59,7 +59,7 @@ class TestConf extends \PHPUnit\Framework\TestCase {
*/
function testImportFileMissing() {
$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() {
$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() {
$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() {
$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() {
$this->assertException("fileCorrupt", "Conf");
// 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() {
$this->assertException("fileCorrupt", "Conf");
// 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 $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 [];',
'en-us.php' => '<?php return ["Test.presentText" => "and the Sorcerer\'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"];',
@ -24,7 +27,7 @@ class TestLang extends \PHPUnit\Framework\TestCase {
'it.php' => '<?php return 0;',
'zh.php' => '<?php return 0',
'ko.php' => 'DEAD BEEF',
'fr-ca.php' => '',
'fr_ca.php' => '',
// unreadable file
'ru.php' => '',
];
@ -32,16 +35,65 @@ class TestLang extends \PHPUnit\Framework\TestCase {
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 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);
namespace JKingWeb\NewsSync;
const BASE = __DIR__.DIRECTORY_SEPARATOR."..".DIRECTORY_SEPARATOR;
const NS_BASE = __NAMESPACE__."\\";
require_once BASE."vendor".DIRECTORY_SEPARATOR."autoload.php";
require_once __DIR__.DIRECTORY_SEPARATOR."..".DIRECTORY_SEPARATOR."bootstrap.php";
trait TestingHelpers {
function assertException(string $msg, string $prefix = "", string $type = "Exception") {
@ -19,6 +16,4 @@ trait TestingHelpers {
$this->expectException($class);
$this->expectExceptionCode($code);
}
}
ignore_user_abort(true);
}

29
tests/phpunit.xml

@ -1,19 +1,20 @@
<?xml version="1.0"?>
<phpunit
colors="true"
bootstrap="bootstrap.php"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
beStrictAboutTestsThatDoNotTestAnything="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTestSize="true">
<testsuite name="Base">
<file>TestLang.php</file>
<file>TestConf.php</file>
</testsuite>
colors="true"
bootstrap="bootstrap.php"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
beStrictAboutTestsThatDoNotTestAnything="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTestSize="true">
<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>

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;
class Lang {
const PATH = BASE."locale".DIRECTORY_SEPARATOR;
const DEFAULT = "en";
const REQUIRED = [
'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})',
];
static public $path = BASE."locale".DIRECTORY_SEPARATOR;
static protected $requirementsMet = false;
static protected $synched = false;
static protected $wanted = self::DEFAULT;
@ -25,13 +25,16 @@ class Lang {
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($locale=="") $locale = self::DEFAULT;
if($locale==self::$wanted) return $locale;
$list = self::listFiles();
if(!in_array(self::DEFAULT, $list)) throw new Lang\Exception("defaultFileMissing", self::DEFAULT);
self::$wanted = self::match($locale, $list);
if($locale != "") {
$list = self::listFiles();
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;
if($immediate) self::load();
return self::$wanted;
@ -41,12 +44,15 @@ class Lang {
return (self::$locale=="") ? self::DEFAULT : self::$locale;
}
static public function dump(): array {
return self::$strings;
}
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(!self::$synched) try {self::load();} catch(Lang\Exception $e) {
if(self::$wanted==self::DEFAULT) {
self::set();
self::load();
self::set("", true);
} else {
throw $e;
}
@ -64,9 +70,9 @@ class Lang {
return $msg;
}
static public function list(string $locale = "", string $path = self::PATH): array {
static public function list(string $locale = ""): array {
$out = [];
$files = self::listFiles($path);
$files = self::listFiles();
foreach($files as $tag) {
$out[$tag] = \Locale::getDisplayName($tag, ($locale=="") ? $tag : $locale);
}
@ -85,10 +91,12 @@ class Lang {
return true;
}
static protected function listFiles(string $path = self::PATH): array {
$out = Glob::glob($path."*.php");
static protected function listFiles(): array {
$out = glob(self::$path."*.php");
if(empty($out)) $out = Glob::glob(self::$path."*.php");
$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,".")));
},$out);
natsort($out);
@ -96,21 +104,19 @@ class Lang {
}
static protected function load(): bool {
self::$synched = true;
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=="") {
self::$strings = self::REQUIRED;
self::$locale = self::$wanted;
self::$synched = true;
return true;
}
// decompose the requested locale from specific to general, building a list of files to load
$tags = \Locale::parseLocale(self::$wanted);
$files = [];
$loaded = [];
$strings = [];
while(sizeof($tags) > 0) {
$files[] = \Locale::composeLocale($tags);
$files[] = strtolower(\Locale::composeLocale($tags));
$tag = array_pop($tags);
}
// 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;
}
// 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
$files = array_reverse($files);
foreach($files as $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(!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);
try {
ob_start();
$arr = (include self::PATH."$file.php");
$arr = (include self::$path."$file.php");
} catch(\Throwable $e) {
$arr = null;
} finally {

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

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