Skip to content

[2.6][Translator] Extend, refactor and simplify Translator tests. #14437

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
296 changes: 153 additions & 143 deletions src/Symfony/Component/Translation/Tests/TranslatorCacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@

namespace Symfony\Component\Translation\Tests;

use Symfony\Component\Config\Resource\ResourceInterface;
use Symfony\Component\Translation\Loader\ArrayLoader;
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\MessageSelector;

class TranslatorCacheTest extends \PHPUnit_Framework_TestCase
{
Expand Down Expand Up @@ -51,91 +52,107 @@ protected function deleteTmpDir()
rmdir($this->tmpDir);
}

public function testTransWithoutCaching()
/**
* @dataProvider runForDebugAndProduction
*/
public function testThatACacheIsUsed($debug)
{
$translator = $this->getTranslator($this->getLoader());
$translator->setLocale('fr');
$translator->setFallbackLocales(array('en', 'es', 'pt-PT', 'pt_BR', 'fr.UTF-8', 'sr@latin'));

$this->assertEquals('foo (FR)', $translator->trans('foo'));
$this->assertEquals('bar (EN)', $translator->trans('bar'));
$this->assertEquals('foobar (ES)', $translator->trans('foobar'));
$this->assertEquals('choice 0 (EN)', $translator->transChoice('choice', 0));
$this->assertEquals('no translation', $translator->trans('no translation'));
$this->assertEquals('foobarfoo (PT-PT)', $translator->trans('foobarfoo'));
$this->assertEquals('other choice 1 (PT-BR)', $translator->transChoice('other choice', 1));
$this->assertEquals('foobarbaz (fr.UTF-8)', $translator->trans('foobarbaz'));
$this->assertEquals('foobarbax (sr@latin)', $translator->trans('foobarbax'));
$locale = 'any_locale';
$format = 'some_format';
$msgid = 'test';

// Prime the cache
$translator = new Translator($locale, null, $this->tmpDir, $debug);
$translator->addLoader($format, new ArrayLoader());
$translator->addResource($format, array($msgid => 'OK'), $locale);
$translator->trans($msgid);

// Try again and see we get a valid result whilst no loader can be used
$translator = new Translator($locale, null, $this->tmpDir, $debug);
$translator->addLoader($format, $this->createFailingLoader());
$translator->addResource($format, array($msgid => 'OK'), $locale);
$this->assertEquals('OK', $translator->trans($msgid), '-> caching does not work in '.($debug ? 'debug' : 'production'));
}

public function testTransWithCaching()
public function testCatalogueIsReloadedWhenResourcesAreNoLongerFresh()
{
// prime the cache
$translator = $this->getTranslator($this->getLoader(), $this->tmpDir);
$translator->setLocale('fr');
$translator->setFallbackLocales(array('en', 'es', 'pt-PT', 'pt_BR', 'fr.UTF-8', 'sr@latin'));

$this->assertEquals('foo (FR)', $translator->trans('foo'));
$this->assertEquals('bar (EN)', $translator->trans('bar'));
$this->assertEquals('foobar (ES)', $translator->trans('foobar'));
$this->assertEquals('choice 0 (EN)', $translator->transChoice('choice', 0));
$this->assertEquals('no translation', $translator->trans('no translation'));
$this->assertEquals('foobarfoo (PT-PT)', $translator->trans('foobarfoo'));
$this->assertEquals('other choice 1 (PT-BR)', $translator->transChoice('other choice', 1));
$this->assertEquals('foobarbaz (fr.UTF-8)', $translator->trans('foobarbaz'));
$this->assertEquals('foobarbax (sr@latin)', $translator->trans('foobarbax'));

// do it another time as the cache is primed now
$loader = $this->getMock('Symfony\Component\Translation\Loader\LoaderInterface');
$translator = $this->getTranslator($loader, $this->tmpDir);
$translator->setLocale('fr');
$translator->setFallbackLocales(array('en', 'es', 'pt-PT', 'pt_BR', 'fr.UTF-8', 'sr@latin'));

$this->assertEquals('foo (FR)', $translator->trans('foo'));
$this->assertEquals('bar (EN)', $translator->trans('bar'));
$this->assertEquals('foobar (ES)', $translator->trans('foobar'));
$this->assertEquals('choice 0 (EN)', $translator->transChoice('choice', 0));
$this->assertEquals('no translation', $translator->trans('no translation'));
$this->assertEquals('foobarfoo (PT-PT)', $translator->trans('foobarfoo'));
$this->assertEquals('other choice 1 (PT-BR)', $translator->transChoice('other choice', 1));
$this->assertEquals('foobarbaz (fr.UTF-8)', $translator->trans('foobarbaz'));
$this->assertEquals('foobarbax (sr@latin)', $translator->trans('foobarbax'));
}
/*
* The testThatACacheIsUsed() test showed that we don't need the loader as long as the cache
* is fresh.
*
* Now we add a Resource that is never fresh and make sure that the
* cache is discarded (the loader is called twice).
*
* We need to run this for debug=true only because in production the cache
* will never be revalidated.
*/

public function testTransWithCachingWithInvalidLocale()
{
$loader = $this->getMock('Symfony\Component\Translation\Loader\LoaderInterface');
$translator = $this->getTranslator($loader, $this->tmpDir, 'Symfony\Component\Translation\Tests\TranslatorWithInvalidLocale');
$locale = 'any_locale';
$format = 'some_format';
$msgid = 'test';

$translator->setLocale('invalid locale');
$catalogue = new MessageCatalogue($locale, array());
$catalogue->addResource(new StaleResource()); // better use a helper class than a mock, because it gets serialized in the cache and re-loaded

try {
$translator->trans('foo');
$this->fail();
} catch (\InvalidArgumentException $e) {
$this->assertFalse(file_exists($this->tmpDir.'/catalogue.invalid locale.php'));
}
/** @var LoaderInterface|\PHPUnit_Framework_MockObject_MockObject $loader */
$loader = $this->getMock('Symfony\Component\Translation\Loader\LoaderInterface');
$loader
->expects($this->exactly(2))
->method('load')
->will($this->returnValue($catalogue))
;

// 1st pass
$translator = new Translator($locale, null, $this->tmpDir, true);
$translator->addLoader($format, $loader);
$translator->addResource($format, null, $locale);
$translator->trans($msgid);

// 2nd pass
$translator = new Translator($locale, null, $this->tmpDir, true);
$translator->addLoader($format, $loader);
$translator->addResource($format, null, $locale);
$translator->trans($msgid);
}

public function testLoadCatalogueWithCachingWithInvalidLocale()
/**
* @dataProvider runForDebugAndProduction
*/
public function testDifferentTranslatorsForSameLocaleDoNotOverwriteEachOthersCache($debug)
{
$loader = $this->getMock('Symfony\Component\Translation\Loader\LoaderInterface');
$translator = $this->getTranslator($loader, $this->tmpDir, 'Symfony\Component\Translation\Tests\TranslatorWithInvalidLocale');
/*
* Similar to the previous test. After we used the second translator, make
* sure there's still a useable cache for the first one.
*/

try {
$translator->proxyLoadCatalogue('invalid locale');
$this->fail();
} catch (\InvalidArgumentException $e) {
$this->assertFalse(file_exists($this->tmpDir.'/catalogue.invalid locale.php'));
}
$locale = 'any_locale';
$format = 'some_format';
$msgid = 'test';

// Create a Translator and prime its cache
$translator = new Translator($locale, null, $this->tmpDir, $debug);
$translator->addLoader($format, new ArrayLoader());
$translator->addResource($format, array($msgid => 'OK'), $locale);
$translator->trans($msgid);

// Create another Translator with a different catalogue for the same locale
$translator = new Translator($locale, null, $this->tmpDir, $debug);
$translator->addLoader($format, new ArrayLoader());
$translator->addResource($format, array($msgid => 'FAIL'), $locale);
$translator->trans($msgid);

// Now the first translator must still have a useable cache.
$translator = new Translator($locale, null, $this->tmpDir, $debug);
$translator->addLoader($format, $this->createFailingLoader());
$translator->addResource($format, array($msgid => 'OK'), $locale);
$this->assertEquals('OK', $translator->trans($msgid), '-> the cache was overwritten by another translator instance in '.($debug ? 'debug' : 'production'));
}

public function testDifferentCacheFilesAreUsedForDifferentSetsOfFallbackLocales()
{
/*
* Because the cache file contains a catalogue including all of its fallback
* catalogues (either "inlined" in Symfony 2.7 production or "standalone"),
* we must take the active set of fallback locales into consideration when
* catalogues, we must take the set of fallback locales into consideration when
* loading a catalogue from the cache.
*/
$translator = new Translator('a', null, $this->tmpDir);
Expand All @@ -161,6 +178,54 @@ public function testDifferentCacheFilesAreUsedForDifferentSetsOfFallbackLocales(
$this->assertEquals('bar', $translator->trans('bar'));
}

public function testPrimaryAndFallbackCataloguesContainTheSameMessagesRegardlessOfCaching()
{
/*
* As a safeguard against potential BC breaks, make sure that primary and fallback
* catalogues (reachable via getFallbackCatalogue()) always contain the full set of
* messages provided by the loader. This must also be the case when these catalogues
* are (internally) read from a cache.
*
* Optimizations inside the translator must not change this behaviour.
*/

/*
* Create a translator that loads two catalogues for two different locales.
* The catalogues contain distinct sets of messages.
*/
$translator = new Translator('a', null, $this->tmpDir);
$translator->setFallbackLocales(array('b'));

$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', array('foo' => 'foo (a)'), 'a');
$translator->addResource('array', array('foo' => 'foo (b)'), 'b');
$translator->addResource('array', array('bar' => 'bar (b)'), 'b');

$catalogue = $translator->getCatalogue('a');
$this->assertFalse($catalogue->defines('bar')); // Sure, the "a" catalogue does not contain that message.

$fallback = $catalogue->getFallbackCatalogue();
$this->assertTrue($fallback->defines('foo')); // "foo" is present in "a" and "b"

/*
* Now, repeat the same test.
* Behind the scenes, the cache is used. But that should not matter, right?
*/
$translator = new Translator('a', null, $this->tmpDir);
$translator->setFallbackLocales(array('b'));

$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', array('foo' => 'foo (a)'), 'a');
$translator->addResource('array', array('foo' => 'foo (b)'), 'b');
$translator->addResource('array', array('bar' => 'bar (b)'), 'b');

$catalogue = $translator->getCatalogue('a');
$this->assertFalse($catalogue->defines('bar'));

$fallback = $catalogue->getFallbackCatalogue();
$this->assertTrue($fallback->defines('foo'));
}

public function testRefreshCacheWhenResourcesAreNoLongerFresh()
{
$resource = $this->getMock('Symfony\Component\Config\Resource\ResourceInterface');
Expand Down Expand Up @@ -197,93 +262,38 @@ protected function getCatalogue($locale, $messages, $resources = array())
return $catalogue;
}

protected function getLoader()
public function runForDebugAndProduction()
{
return array(array(true), array(false));
}

/**
* @return LoaderInterface
*/
private function createFailingLoader()
{
$loader = $this->getMock('Symfony\Component\Translation\Loader\LoaderInterface');
$loader
->expects($this->at(0))
->method('load')
->will($this->returnValue($this->getCatalogue('fr', array(
'foo' => 'foo (FR)',
))))
;
$loader
->expects($this->at(1))
->method('load')
->will($this->returnValue($this->getCatalogue('en', array(
'foo' => 'foo (EN)',
'bar' => 'bar (EN)',
'choice' => '{0} choice 0 (EN)|{1} choice 1 (EN)|]1,Inf] choice inf (EN)',
))))
;
$loader
->expects($this->at(2))
->method('load')
->will($this->returnValue($this->getCatalogue('es', array(
'foobar' => 'foobar (ES)',
))))
;
$loader
->expects($this->at(3))
->method('load')
->will($this->returnValue($this->getCatalogue('pt-PT', array(
'foobarfoo' => 'foobarfoo (PT-PT)',
))))
;
$loader
->expects($this->at(4))
->method('load')
->will($this->returnValue($this->getCatalogue('pt_BR', array(
'other choice' => '{0} other choice 0 (PT-BR)|{1} other choice 1 (PT-BR)|]1,Inf] other choice inf (PT-BR)',
))))
;
$loader
->expects($this->at(5))
->method('load')
->will($this->returnValue($this->getCatalogue('fr.UTF-8', array(
'foobarbaz' => 'foobarbaz (fr.UTF-8)',
))))
;
$loader
->expects($this->at(6))
->method('load')
->will($this->returnValue($this->getCatalogue('sr@latin', array(
'foobarbax' => 'foobarbax (sr@latin)',
))))
;
->expects($this->never())
->method('load');

return $loader;
}
}

public function getTranslator($loader, $cacheDir = null, $translatorClass = '\Symfony\Component\Translation\Translator')
class StaleResource implements ResourceInterface
{
public function isFresh($timestamp)
{
$translator = new $translatorClass('fr', new MessageSelector(), $cacheDir);

$translator->addLoader('loader', $loader);
$translator->addResource('loader', 'foo', 'fr');
$translator->addResource('loader', 'foo', 'en');
$translator->addResource('loader', 'foo', 'es');
$translator->addResource('loader', 'foo', 'pt-PT'); // European Portuguese
$translator->addResource('loader', 'foo', 'pt_BR'); // Brazilian Portuguese
$translator->addResource('loader', 'foo', 'fr.UTF-8');
$translator->addResource('loader', 'foo', 'sr@latin'); // Latin Serbian

return $translator;
return false;
}
}

class TranslatorWithInvalidLocale extends Translator
{
/**
* {@inheritdoc}
*/
public function setLocale($locale)
public function getResource()
{
$this->locale = $locale;
}

public function proxyLoadCatalogue($locale)
public function __toString()
{
$this->loadCatalogue($locale);
return '';
}
}
Loading