Part of #2557113: Make t() return a TranslationWrapper object to remove reliance on a static, unpredictable safe list

It is currently hard to do work with objects implementing SafeStringInterface in tests as we have to do string casting in the calling tests themselves as opposed to already allowing for this in the TestBase helper methods.

We use these objects more and more, and people shouldn't have to cast when doing simple equality checks with another string.

In working on #2557113: Make t() return a TranslationWrapper object to remove reliance on a static, unpredictable safe list most of he work went into fixing test fails rather than actual code that broke - without a patch like the work would be quite a bit more involved, so this will definitely be a help to contrib as well in having less refactoring to do in dealing with the BC break.

Comments

stefan.r created an issue. See original summary.

stefan.r’s picture

Component: other » simpletest.module
Status: Active » Needs review
StatusFileSize
new9.81 KB
stefan.r’s picture

comments from @dawehner in the parent issue:

+++ b/core/modules/simpletest/src/AssertContentTrait.php
@@ -178,6 +179,10 @@ protected function getUrl() {
+ // @todo fix this
+ if ($value instanceof SafeStringInterface) {
+ $value = (string) $value;
+ }

IMHO for xpath we can always cast to string, right? anything else would not pack anyway, given how xpath works
+++ b/core/modules/simpletest/src/TestBase.php
@@ -654,6 +655,18 @@ protected function assertNotNull($value, $message = '', $group = 'Other') {
+ if (is_array($first)) {
+ array_walk_recursive($first, [$this, 'translationWrapperToString']);
+ }
+ if (is_array($second)) {
+ array_walk_recursive($second, [$this, 'translationWrapperToString']);
+ }

@@ -683,6 +696,15 @@ protected function assertNotEqual($first, $second, $message = '', $group = 'Othe
+ * @todo remove this
+ */
+ public function translationWrapperToString(&$value) {
+ if ($value instanceof SafeStringInterface) {
+ $value = (string) $value;
+ }
+ }
+

What about using strval for iteration?

stefan.r’s picture

Issue summary: View changes
stefan.r’s picture

Priority: Normal » Major
Issue summary: View changes

Upgrading to major as these were split out of a Major issue

stefan.r’s picture

Seems we already do something similar in AssertContentTrait::assertThemeOutput() as well:

    // The string cast is necessary because theme functions return
    // SafeStringInterface objects. This means we can assert that $expected
    // matches the theme output without having to worry about 0 == ''.
    $output = (string) $renderer->executeInRenderContext(new RenderContext(), function() use ($callback, $variables) {
      return \Drupal::theme()->render($callback, $variables);
    });
stefan.r’s picture

StatusFileSize
new13.78 KB

Sadly this can't really be tested, so a few manual reviews would be good.

As to #3, I had rather it keep failing on anything other than SafeStringInterface... And strval the same concern -- I'd rather not blindly cast everything to string.

alexpott’s picture

#2560715: Prefer toString to __toString in renderVar for ToStringInterface is very related to this... perhaps we want to push on that one first.

stefan.r’s picture

Status: Needs review » Postponed

Agreed...

joelpittet’s picture

Status: Postponed » Needs review

That is now closed won't fix, so opening this up again.

stefan.r’s picture

Issue summary: View changes

Jut gave this another look and not much to add here.

We already use SafeStringInterface in various places interchangeably with strings and in the renderer we do quite a bit of string casting as well so it only makes sense for us to cater to this in tests. SafeMarkup::format() may soon only output safe strings anymore for instance.

This is all a bit magical and it will make the equality checks and assertions more lenient but I don't really see where this could currently pose any serious problem. Maybe in the future we (or contrib) will have some wonky objects implementing SafeStringInterface, but it should only be used for value objects anyway, and TranslationWrappers seem like the only possible place where casting is a bit more involved than printing the value. And that is where we actuallly /want/ the string casting as when we make t() return a TranslationWrapper, the string casting in tests will make the BC break less severe as it will make t() behave the way it used to historically.

In #2557113: Make t() return a TranslationWrapper object to remove reliance on a static, unpredictable safe list most of the work went into fixing test fails rather than actual code that broke - without a patch like tere the work would be quite a bit more involved, so this will definitely be a help to contrib as well in having less refactoring to do in dealing with the BC break.

stefan.r’s picture

Issue summary: View changes
lauriii’s picture

Do we need casting on assertEqual? Anyway for me the patch looks sane. Lets just add some documentation at least for the assertIdentical that its parameters will be automatically casted from SafeStringInterface to string because it might be unexpected.

stefan.r’s picture

StatusFileSize
new14.44 KB
new1.82 KB

Added comments to clarify - I think we want to keep the string cast in assertEquals() just to be sure, rather than relying on PHP silently casting compared TranslationWrappers to string.

lauriii’s picture

  1. +++ b/core/modules/simpletest/src/TestBase.php
    @@ -658,6 +658,8 @@
    +    // We cast objects implementing SafeStringInterface to string ourself so as to
    +    // not rely on PHP casting them to string depending on what we're comparing with.
    

    80 chars

  2. +++ b/core/modules/simpletest/src/TestBase.php
    @@ -744,6 +749,10 @@
    +    // keep the backward compatibility break small once issue #2557113 lands.
    

    Maybe remove the reference on that issue or at least make it something that works also after the issue has landed and if the issue doesn't ever get commited.

stefan.r’s picture

StatusFileSize
new14.39 KB
new991 bytes
lauriii’s picture

Status: Needs review » Reviewed & tested by the community
StatusFileSize
new14.4 KB
new984 bytes

Fixed the 80 chars problem. Otherwise RTBC for me.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/modules/simpletest/src/TestBase.php
    @@ -683,8 +691,46 @@ protected function assertNotEqual($first, $second, $message = '', $group = 'Othe
    +      array_walk_recursive($first, [$this, 'safeStringToString']);
    ...
    +      array_walk_recursive($second, [$this, 'safeStringToString']);
    ...
    +  /**
    +   * Callback that converts all safe strings into strings.
    +   */
    +  protected function safeStringToString(&$value) {
    +    if ($value instanceof SafeStringInterface) {
    +      $value = (string) $value;
    +    }
    +  }
    

    Let's make this an anonymous function that way we don't have wonder why we're not using it elsewhere. In fact you could make a helper method for an array because there are other array_walk_recursive().

  2. +++ b/core/modules/simpletest/src/WebTestBase.php
    @@ -233,7 +234,7 @@ function __construct($test_id = NULL) {
    +   * @param stirng||\Drupal\Component\Utility\SafeStringInterface $title
    

    string and single |

  3. +++ b/core/modules/simpletest/src/AssertContentTrait.php
    @@ -178,6 +179,10 @@ protected function getUrl() {
    +      if ($value instanceof SafeStringInterface) {
    +        $value = (string) $value;
    +      }
    

    I think that just the string cast is fine... is the interface checking everywhere really necessary? How about something like this...

    // Cast SafeStringInterface objects to string.
    $value = (string) $value;
    
stefan.r’s picture

StatusFileSize
new7.24 KB
new13.6 KB

I'd rather err on the side of being more specific in these cases rather than blindly casting - we don't want false positives/negatives or hide any errors when passing in NULL/FALSE or such.

Anyway let's see if this passes tests anyway, these seem like they should be fine.

stefan.r’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 20: 2560729-20.patch, failed testing.

stefan.r’s picture

Status: Needs work » Needs review
StatusFileSize
new13.61 KB
new565 bytes

Status: Needs review » Needs work

The last submitted patch, 23: 2560729-23.patch, failed testing.

stefan.r’s picture

Status: Needs work » Needs review
StatusFileSize
new7.06 KB
new13.73 KB

Converted the ones that were possible -- turned one into an isset (as it could also be NULL) and two into a more lenient "is_object" as they could also be NULL or an array.

stefan.r’s picture

StatusFileSize
new12.77 KB
new1.16 KB

Removing the cast from the identical check, we'll have to run $this->castSafeStrings() in the individual tests themselves before identical checks that include output from t()

alexpott’s picture

@stefan.r or just change to assertEquals()

lauriii’s picture

Status: Needs review » Reviewed & tested by the community
+++ b/core/modules/simpletest/src/TestBase.php
@@ -683,6 +691,40 @@ protected function assertNotEqual($first, $second, $message = '', $group = 'Othe
+   * @return list

s/list/mixed but can be fixed on commit. Otherwise everything looks good.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

This is a bit ugly but it is test code so I'm not too bothered. Committed 1bd7a29 and pushed to 8.0.x. Thanks!

  • alexpott committed 1bd7a29 on 8.0.x
    Issue #2560729 by stefan.r, lauriii: Cast objects implementing...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.