Problem/Motivation

https://qa.drupal.org/pifr/test/995283

The Template Assertions patch adds this assertion just prior to the return.

assert('$value instanceof \\Drupal\\Core\\Template\\AttributeValueBase', 'invalidReturn');

This is based on and enforces the following existing PHP docs line.

   * @return \Drupal\Core\Template\AttributeValueBase
   *   An AttributeValueBase representation of the attribute's value.

Two exceptions each are raised for these tests.

Drupal\action\Tests\ConfigurationTest
Drupal\search\Tests\SearchConfigSettingsFormTest

The assertion code is not wrong unless the PHP docs code is wrong. Either the code is defective or the documentation is wrong. I don't know which.

Proposed resolution

Three possible, depending on the actual problem.

  1. Fix the underlying code so that an instance of AttributeValueBase is returned as indicated in the PHP Docs
  2. Update the documentation line to match what the function actually is supposed to return and modify the assertion to match the new PHP documentation.
  3. There is a bug in the tests involved that must be updated.

Remaining tasks

One of the above must be followed up on.

User interface changes

None anticipated.

API changes

None anticipated.

Comments

Aki Tendo’s picture

Ok, upon running a local test I discovered that a TranslationWrapper was passed in as the value argument. Let's look at the function since it's fairly short...

  protected function createAttributeValue($name, $value) {
    // If the value is already an AttributeValueBase object, return it
    // straight away.
    if ($value instanceOf AttributeValueBase) {
      return $value;
    }
    // An array value or 'class' attribute name are forced to always be an
    // AttributeArray value for consistency.
    if (is_array($value) || $name == 'class') {
      // Cast the value to an array if the value was passed in as a string.
      // @todo Decide to fix all the broken instances of class as a string
      // in core or cast them.
      $value = new AttributeArray($name, (array) $value);
    }
    elseif (is_bool($value)) {
      $value = new AttributeBoolean($name, $value);
    }
    elseif (!is_object($value)) {
      $value = new AttributeString($name, $value);
    }
    assert('$value instanceof \\Drupal\\Core\\Template\\AttributeValueBase', 'invalidReturn');
    return $value;
  }

If we pass an object to this function then, without the assertion, it will be returned without conversion. Is this the correct behavior.

If yes, the assertion must be changed to allow this.
If no, the code must be changed to forbid it and all calling code debugged.

Or, is the intent that an object's string expression be returned as an AttributeString? If so another branch to the conditional is called for.

That last one is the one I'll try for patching this.

Aki Tendo’s picture

Assigned: Unassigned » Aki Tendo
Status: Active » Needs review
StatusFileSize
new962 bytes

Added a conditional to force an object that has a magic __toString method to cast itself as a string. This satisfies both the two failed tests and the assertion added for the Template Assertions patch. This patch will be added to it's parent so that the parent can pass testing.

Status: Needs review » Needs work

The last submitted patch, 2: Correct_Return-2449741-2.diff, failed testing.

Aki Tendo’s picture

StatusFileSize
new840 bytes

It would help if I'd apply against core.

Aki Tendo’s picture

Status: Needs work » Needs review
dawehner’s picture

I'm curious wehther we can add a test?
Afaik we have a unit test for that already: core/tests/Drupal/Tests/Core/Template/AttributeTest.php

Aki Tendo’s picture

Probably should add a test to the code that is directly affected - then again in a sense the assertion is itself a test of sorts. I'll look into adding a test tomorrow.

Aki Tendo’s picture

Well, here's the problem...

  /**
   * Tests set of values.
   */
  public function testSet() {
    $attribute = new Attribute();
    $attribute['class'] = array('example-class');

    $this->assertTrue(isset($attribute['class']));
    $this->assertEquals(new AttributeArray('class', array('example-class')), $attribute['class']);
  }

That test only checks if arrays get converted. It checks no other types and doesn't check invalids. I'll go ahead an add that logic, but I need to get the assert statement in at this point to check to make sure it gets thrown when invalid objects are supplied. The only thing that can do that is outside code, which indicates a bug in that code, so assert is the correct handle here, not an exception.

Aki Tendo’s picture

StatusFileSize
new5.61 KB

K, multiple new tests added to make sure what I understand the code should be returned is being returned. Also a test to see if the assertion gets thrown.

No interdiff since the first patch was only 3 lines.

Aki Tendo’s picture

StatusFileSize
new5.47 KB
new667 bytes
yesct’s picture

StatusFileSize
new4.42 KB
new5.07 KB
new4.78 KB

[cross posted with #10, (and then copied the summary instead of my comment).
maybe #10 had a more informative tests only patch?]

Thanks for adding tests.

Fixed some coding standard nits:

  1. +++ b/core/tests/Drupal/Tests/Core/Template/AttributeTest.php
    @@ -37,10 +39,58 @@ public function testConstructor() {
    +    // Subtest 1 - setting an array.
    

    We usually say what we are testing without numbering things. (So if we change the order or insert another test case, we do not have to make unrelated changes changing the numbering. ... unless the order actually matters.

  2. +++ b/core/tests/Drupal/Tests/Core/Template/AttributeTest.php
    @@ -37,10 +39,58 @@ public function testConstructor() {
    +    // Subtest 4 - If the name key isn't class the same operation yields a string.
    

    "Lines containing comments (including docblocks) must wrap as close to 80 characters as possible without going over"

    https://www.drupal.org/node/1354#drupal

  3. +++ b/core/tests/Drupal/Tests/Core/Template/AttributeTest.php
    @@ -37,10 +39,58 @@ public function testConstructor() {
    +    /*
    +     * Subtest 7 - setting an object without a __toString method.
    +     *
    +     * More specifically, we trip an assertion statement if this is tried. We
    +     * assert rather than throw an exception because if the code is bug free
    +     * we'll never see an object without a toString passed to the class.
    +     */
    +    $this->setExpectedException('PHPUnit_Framework_Error_Warning');
    

    in line comments are all // even if they are a block of multiple lines.

    "even for multiple lines, repeat the // single-line comment"
    https://www.drupal.org/node/1354#inline

  4. +++ b/core/tests/Drupal/Tests/Mocks/ToString.php
    @@ -0,0 +1,37 @@
    + * Mock object that wraps around string provided at construction time.
    

    tests are sometimes do not follow standards as strictly... but

    one line summaries should start with 3rd person verbs.

    "Use a third person verb to start the summary of a class, interface, or method. For example: "Represents a ..." or "Provides..."."

    https://www.drupal.org/node/1354#classes

  5. +++ b/core/tests/Drupal/Tests/Mocks/ToString.php
    @@ -0,0 +1,37 @@
    +class ToString {
    +  /**
    +   * A string.
    +   */
    +  protected $val = '';
    +  /**
    

    https://www.drupal.org/node/608152#indenting

    some empty lines here...

=====

other than those nits,
I wonder if we can do a tests only patch to show the test additions/changes will catch the problem nicely.
maybe not, though since the fix in core/lib/Drupal/Core/Template/Attribute.php
adds an assert there...

The last submitted patch, 10: 2449741-Tests_Only-10.diff, failed testing.

The last submitted patch, 11: 2449741.10.testsonly.patch, failed testing.

yesct’s picture

Well at least we can see the how the test fails are different including or excluding that assert. :)

yesct’s picture

So do we know enough to clarify the issue summary now?

Aki Tendo’s picture

I'm not sure. The point of the assertion is to enforce the PHPDocs comment - that the function only returns objects of a certain type. It has proven that in at least 4 cases in the existing code that isn't happening. I don't know what is more disruptive - enforcing the PHP docs statement or dropping the assert - but I do know the long term health of the code is best served by enforcing the PHP docs or changing them to match the actual behavior.

Something else that isn't tested for - Numbers of all types. Reading the code flow they will currently drop through and be returned unmodified. Why where they overlooked? Are they even legal inputs? Are they legal only for certain attributes - I know you can't have a class name that is merely a number, nor an ID, and they can't start with a number either. Is it the responsibility of this class to make that check?

Assuming they are legal, do we create a new AttributeValue container for them, or cast them to string and let the AttributeString class work with them?

Aki Tendo’s picture

Status: Needs review » Closed (duplicate)

This morning I started doing a lot more hardening shuffling, so this is no longer a simple issue but does go hand in hand with assertions. Closing this ticket to merge back to the Template Assertions ticket.