Skip to content

Commit

Permalink
Merge branch '5' into 6.0
Browse files Browse the repository at this point in the history
  • Loading branch information
emteknetnz committed Jan 29, 2025
2 parents 917ffbb + 31e5e4f commit 5ca45f4
Show file tree
Hide file tree
Showing 5 changed files with 267 additions and 43 deletions.
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@
},
"conflict": {
"egulias/email-validator": "<3.2.1",
"oscarotero/html-parser": "<0.1.7"
"silverstripe/behat-extension": "<5.5",
"oscarotero/html-parser": "<0.1.7",
"symfony/process": "<5.3.7"
},
"provide": {
"psr/container-implementation": "1.0.0"
Expand Down
67 changes: 56 additions & 11 deletions src/Dev/SapphireTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
use SilverStripe\Security\Member;
use SilverStripe\Security\Permission;
use SilverStripe\Security\Security;
use SilverStripe\SupportedModules\MetaData;
use SilverStripe\View\SSViewer;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Mailer\MailerInterface;
Expand All @@ -49,6 +50,7 @@
use SilverStripe\Dev\Exceptions\ExpectedNoticeException;
use SilverStripe\Dev\Exceptions\ExpectedWarningException;
use SilverStripe\ORM\DB;
use SilverStripe\Core\Path;

/**
* Test case class for the Silverstripe framework.
Expand All @@ -70,6 +72,13 @@ abstract class SapphireTest extends TestCase implements TestOnly
*/
protected static $fixture_file = null;

/**
* Whether to set the i18n locale to en_US for supported modules before running the test.
* This should only be set to false if calling setI18nLocale() causes the test to
* throw an exception as part of calling setI18nLocale()
*/
protected bool $doSetSupportedModuleLocaleToUS = true;

/**
* @var Boolean If set to TRUE, this will force a test database to be generated
* in {@link setUp()}. Note that this flag is overruled by the presence of a
Expand Down Expand Up @@ -288,21 +297,13 @@ protected function setUp(): void
// Call state helpers
static::$state->setUp($this);

// i18n needs to be set to the defaults or tests fail
if (class_exists(i18n::class)) {
i18n::set_locale(i18n::config()->uninherited('default_locale'));
}
$this->setI18nLocale();

// Set default timezone consistently to avoid NZ-specific dependencies
date_default_timezone_set('UTC');

if (class_exists(Member::class)) {
Member::set_password_validator(null);
}

if (class_exists(Cookie::class)) {
Cookie::config()->set('report_errors', false);
}
Member::set_password_validator(null);
Cookie::config()->set('report_errors', false);

if (class_exists(RootURLController::class)) {
RootURLController::reset();
Expand Down Expand Up @@ -1301,4 +1302,48 @@ protected function mockSleep(int $seconds): DBDatetime

return $now;
}

/**
* Sets the locale which unit-tests should be run in
*/
private function setI18nLocale(): void
{
if (!$this->doSetSupportedModuleLocaleToUS) {
$this->setLocaleToDefault();
return;
}
$path = $this->getCurrentRelativePath();
$packagistName = '';
if (preg_match('#(^|/)vendor/([^/]+/[^/]+)/.+#', $path, $matches)) {
// Running unit tests of a module in the vendor folder
$packagistName = $matches[2];
} else {
// Running unit tests of a module or project in the root folder
$file = Path::join(BASE_PATH, 'composer.json');
if (file_exists($file)) {
$json = json_decode(file_get_contents($file), true);
$packagistName = $json['name'] ?? '';
}
}
$metaData = MetaData::getMetaDataByPackagistName($packagistName);
$isSupportedModule = !empty($metaData);
if ($isSupportedModule) {
// Anything that is in silverstripe/supported-module has unit tests in en_US
// Update the default_locale config in case in case any config at the project level or any
// installed optional module has set it to a non-en_US locale
i18n::config()->set('default_locale', 'en_US');
i18n::set_locale('en_US');
} else {
$this->setLocaleToDefault();
}
}

/**
* Set the locale to the default_locale, which may have been set at project level to a
* non-en_US locale and the project unit tests expect that locale to be set
*/
private function setLocaleToDefault(): void
{
i18n::set_locale(i18n::config()->get('default_locale'));
}
}
132 changes: 101 additions & 31 deletions src/i18n/TextCollection/i18nTextCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ protected function getConflicts($entitiesByModule)
// bubble-compare each group of modules
for ($i = 0; $i < count($modules ?? []) - 1; $i++) {
$left = array_keys($entitiesByModule[$modules[$i]] ?? []);
for ($j = $i+1; $j < count($modules ?? []); $j++) {
for ($j = $i + 1; $j < count($modules ?? []); $j++) {
$right = array_keys($entitiesByModule[$modules[$j]] ?? []);
$conflicts = array_intersect($left ?? [], $right);
$allConflicts = array_merge($allConflicts, $conflicts);
Expand Down Expand Up @@ -617,15 +617,63 @@ public function collectFromCode($content, $fileName, Module $module)
$potentialClassName = null;
$currentUse = null;
$currentUseAlias = null;
$inVar = null; // Tracks string variables
$inVarConcat = false; // Track if we do things $x .= 'my str'
$inVarText = ''; // Tracks the content of the current string variable
$stringVariables = []; // Store all string variables by name
foreach ($tokens as $token) {
// Shuffle last token to $lastToken
$previousToken = $thisToken;
$thisToken = $token;

// Track string variables
// Store when reaching end of statement if we have content
if ($token === ";" && $inVar) {
if (strlen($inVarText) > 0) {
if ($inVarConcat && isset($stringVariables[$inVar])) {
$stringVariables[$inVar] .= $inVarText;
} else {
$stringVariables[$inVar] = $inVarText;
}
}
$inVar = null;
$inVarConcat = false;
}
// End track string variables

// Not all tokens are returned as an array.
// If a token is not variable, but instead it is one particular constant string, it is returned as a string instead.
// You don't get a line number.
// This is the case for braces( "{", "}"), parentheses ("(", ")"), brackets ("[", "]"), comma (","), semi-colon (";")...
if (is_array($token)) {
list($id, $text, $lineNo) = $token;
// minus 2 is used so the the line we get corresponds with what number token_get_all() returned
$line = $lines[$lineNo - 2] ?? '';

// Ignore whitespace
if ($id === T_WHITESPACE) {
continue;
}

// Track string variables
if (!$inTransFn) {
if ($id === T_CONCAT_EQUAL && $inVar) {
$inVarConcat = true;
}
if ($id === T_VARIABLE) {
$inVar = $text;
$inVarText = '';
continue;
}
if ($id === T_CONSTANT_ENCAPSED_STRING && $inVar !== null && $text !== null) {
// We need to call process strings because $text is like 'my' or 'string' or "my" or "string"
// This can be called multiple time, eg: $str = 'my' . 'string';
$inVarText .= $this->processString($text);
continue;
}
}
// End track string variables

// Collect use statements so we can get fully qualified class names
// Note that T_USE will match both use statements and anonymous functions with the "use" keyword
// e.g. $func = function () use ($var) { ... };
Expand Down Expand Up @@ -732,6 +780,24 @@ public function collectFromCode($content, $fileName, Module $module)
continue;
}

// Allow _t(Entity.Key, 'default text', $var) and expand _t(Entity.Key, $var)
if ($id == T_VARIABLE && !empty($currentEntity)) {
// We have a default text, eg: _t(Entity.Key, 'default text', $var)
if (count($currentEntity) == 2) {
continue;
}

// The variable is the second argument or present in the parameter
// Try to find it _t(Entity.Key, $var)
$stringValue = $stringVariables[$text] ?? null;

// It has a default translation, continue
if ($stringValue !== null) {
$currentEntity[] = $stringValue;
continue;
}
}

// If inside this translation, some elements might be unreachable
if (in_array($id, [T_VARIABLE, T_STATIC]) ||
($id === T_STRING && in_array($text, ['static', 'parent']))
Expand All @@ -753,26 +819,7 @@ public function collectFromCode($content, $fileName, Module $module)

// Check text
if ($id == T_CONSTANT_ENCAPSED_STRING) {
// Fixed quoting escapes, and remove leading/trailing quotes
if (preg_match('/^\'(?<text>.*)\'$/s', $text ?? '', $matches)) {
$text = preg_replace_callback(
'/\\\\([\\\\\'])/s', // only \ and '
function ($input) {
return stripcslashes($input[0] ?? '');
},
$matches['text'] ?? ''
);
} elseif (preg_match('/^\"(?<text>.*)\"$/s', $text ?? '', $matches)) {
$text = preg_replace_callback(
'/\\\\([nrtvf\\\\$"]|[0-7]{1,3}|\x[0-9A-Fa-f]{1,2})/s', // rich replacement
function ($input) {
return stripcslashes($input[0] ?? '');
},
$matches['text'] ?? ''
);
} else {
throw new LogicException("Invalid string escape: " . $text);
}
$text = $this->processString($text);
} elseif ($id === T_CLASS_C || $id === T_TRAIT_C) {
// Evaluate __CLASS__ . '.KEY' and i18nTextCollector::class concatenation
$text = implode('\\', $currentClass);
Expand Down Expand Up @@ -837,16 +884,9 @@ function ($input) {
// Ensure key is valid before saving
if (!empty($currentEntity[0])) {
$key = $currentEntity[0];
$default = '';
$comment = '';
if (!empty($currentEntity[1])) {
$default = $currentEntity[1];
if (!empty($currentEntity[2])) {
$comment = $currentEntity[2];
}
}
// Save in appropriate format
if ($default) {
$default = $currentEntity[1] ?? '';
$comment = $currentEntity[2] ?? '';
if (strlen($default) > 0) {
$plurals = i18n::parse_plurals($default);
// Use array form if either plural or metadata is provided
if ($plurals) {
Expand Down Expand Up @@ -880,6 +920,36 @@ function ($input) {
return $entities;
}

/**
* Fixed quoting escapes, and remove leading/trailing quotes
* @throws LogicException if there is no single or double quotes
* @param string $text
* @return string
*/
private function processString(string $text): string
{
if (preg_match('/^\'(?<text>.*)\'$/s', $text ?? '', $matches)) {
$text = preg_replace_callback(
'/\\\\([\\\\\'])/s', // only \ and '
function ($input) {
return stripcslashes($input[0] ?? '');
},
$matches['text'] ?? ''
);
} elseif (preg_match('/^\"(?<text>.*)\"$/s', $text ?? '', $matches)) {
$text = preg_replace_callback(
'/\\\\([nrtvf\\\\$"]|[0-7]{1,3}|\x[0-9A-Fa-f]{1,2})/s', // rich replacement
function ($input) {
return stripcslashes($input[0] ?? '');
},
$matches['text'] ?? ''
);
} else {
throw new LogicException("Invalid string escape: " . $text);
}
return $text;
}

/**
* Extracts translatables from .ss templates (Self referencing)
*
Expand Down
54 changes: 54 additions & 0 deletions tests/php/Dev/SapphireTestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace SilverStripe\Dev\Tests;

use ReflectionMethod;
use PHPUnit\Framework\ExpectationFailedException;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Model\List\ArrayList;
Expand All @@ -11,6 +12,7 @@
use PHPUnit\Framework\Attributes\DataProviderExternal;
use SilverStripe\Dev\Exceptions\ExpectedNoticeException;
use SilverStripe\Dev\Exceptions\ExpectedWarningException;
use SilverStripe\i18n\i18n;

/**
* @sometag This is a test annotation used in the testGetAnnotations test
Expand Down Expand Up @@ -286,4 +288,56 @@ public static function provideEnableErrorHandler(): array
],
];
}
public static function provideSetI18nLocale(): array
{
return [
'supported-true' => [
'doSet' => true,
'supported' => true,
'expected' => 'en_US',
],
'supported-false' => [
'doSet' => false,
'supported' => true,
'expected' => 'ab_CD',
],
'unsupported-true' => [
'doSet' => true,
'supported' => false,
'expected' => 'ab_CD',
],
'unsupported-false' => [
'doSet' => false,
'supported' => false,
'expected' => 'ab_CD',
],
];
}

#[DataProvider('provideSetI18nLocale')]
public function testSetI18nLocale(bool $doSet, bool $supported, string $expected): void
{
i18n::set_locale('ab_CD');
i18n::config()->set('default_locale', 'ab_CD');
$method = new ReflectionMethod(SapphireTest::class, 'setI18nLocale');
$method->setAccessible(true);
$obj = new class($doSet, $supported) extends SapphireTest
{
private string $supported;
public function __construct(bool $doSet, bool $supported)
{
$this->doSetSupportedModuleLocaleToUS = $doSet;
$this->supported = $supported;
}
protected function getCurrentRelativePath()
{
return $this->supported
? '/vendor/silverstripe/framework/tests/php/FooBarTest.php'
: '/vendor/something/different/tests/php/FooBarTest.php';
}
};
$method->invoke($obj);
$this->assertEquals($expected, i18n::get_locale());
$this->assertEquals($expected, i18n::config()->get('default_locale'));
}
}
Loading

0 comments on commit 5ca45f4

Please sign in to comment.