Skip to content

Commit

Permalink
(hopefully) Increase readability of tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Bernhard Schmitt committed Aug 18, 2023
1 parent b16bde1 commit d614486
Show file tree
Hide file tree
Showing 126 changed files with 1,191 additions and 872 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ Feature: Run integrity violation detection regarding hierarchy relations and nod
As a user of the CR I want to know whether there are nodes or hierarchy relations with invalid hashes or parents / children

Background:
Given I use the following content dimensions:
Given using the following content dimensions:
| Identifier | Values | Generalizations |
| language | de, gsw, fr | gsw->de |
And the following NodeTypes to define content repository "default":
And using the following node types:
"""
'Neos.ContentRepository:Root': []
'Neos.ContentRepository.Testing:Document': []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ Feature: Run integrity violation detection regarding parent relations
As a user of the CR I want to know whether there are nodes that have multiple parents per subgraph

Background:
Given I use the following content dimensions:
Given using the following content dimensions:
| Identifier | Values | Generalizations |
| language | de, gsw, fr | gsw->de |
And the following NodeTypes to define content repository "default":
And using the following node types:
"""
'Neos.ContentRepository:Root': []
'Neos.ContentRepository.Testing:Document': []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ Feature: Run integrity violation detection regarding reference relations
As a user of the CR I want to know whether there are disconnected reference relations

Background:
Given I use the following content dimensions:
Given using the following content dimensions:
| Identifier | Values | Generalizations |
| language | de, gsw, fr | gsw->de |
And the following NodeTypes to define content repository "default":
And using the following node types:
"""
'Neos.ContentRepository:Root': []
'Neos.ContentRepository.Testing:Document':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ Feature: Run integrity violation detection regarding restriction relations
As a user of the CR I want to know whether there are nodes with restriction relations missing from their ancestors

Background:
Given I use the following content dimensions:
Given using the following content dimensions:
| Identifier | Values | Generalizations |
| language | de, gsw, fr | gsw->de |
And the following NodeTypes to define content repository "default":
And using the following node types:
"""
'Neos.ContentRepository:Root': []
'Neos.ContentRepository.Testing:Document': []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ Feature: Run integrity violation detection regarding restriction relations
As a user of the CR I want to know whether there are nodes with restriction relations missing from their ancestors

Background:
Given I use the following content dimensions:
Given using the following content dimensions:
| Identifier | Values | Generalizations |
| language | de, gsw, fr | gsw->de |
And the following NodeTypes to define content repository "default":
And using the following node types:
"""
'Neos.ContentRepository:Root': []
'Neos.ContentRepository.Testing:Document': []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ Feature: Run integrity violation detection regarding sibling sorting
As a user of the CR I want to know whether there are siblings with ambiguous sorting

Background:
Given I use the following content dimensions:
Given using the following content dimensions:
| Identifier | Values | Generalizations |
| language | de, gsw, fr | gsw->de |
And the following NodeTypes to define content repository "default":
And using the following node types:
"""
'Neos.ContentRepository:Root': []
'Neos.ContentRepository.Testing:Document': []
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
<?php

/*
* This file is part of the Neos.ContentRepository.TestSuite package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

declare(strict_types=1);

namespace Neos\ContentRepository\BehavioralTests\TestSuite\Behavior;

use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
use Doctrine\DBAL\Connection;
use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\Dimension\ContentDimensionSourceInterface;
use Neos\ContentRepository\Core\Factory\ContentRepositoryId;
use Neos\ContentRepository\Core\NodeType\NodeTypeManager;
use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Helpers\GherkinPyStringNodeBasedNodeTypeManagerFactory;
use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\Helpers\GherkinTableNodeBasedContentDimensionSource;

/**
* Subject provider for behavioral tests
*/
trait CRBehavioralTestsSubjectProvider
{
protected ?ContentDimensionSourceInterface $contentDimensionsToUse = null;

protected ?NodeTypeManager $nodeTypesToUse = null;

/**
* @var array<string,ContentRepository>
*/
protected array $contentRepositories = [];

/**
* A runtime cache of all content repositories already set up, represented by their ID
* @var array<ContentRepositoryId>
*/
protected array $alreadySetUpContentRepositories = [];

protected ?ContentRepository $currentContentRepository = null;

/**
* @throws \DomainException if the requested content repository instance does not exist
*/
protected function getContentRepository(ContentRepositoryId $contentRepositoryId): ContentRepository
{
if (!array_key_exists($contentRepositoryId->value, $this->contentRepositories)) {
throw new \DomainException('undeclared content repository ' . $contentRepositoryId->value);
}

return $this->contentRepositories[$contentRepositoryId->value];
}

/**
* @Given /^using no content dimensions$/
*/
public function usingNoContentDimensions(): void
{
$this->contentDimensionsToUse = GherkinTableNodeBasedContentDimensionSource::createEmpty();
}

/**
* @Given /^using the following content dimensions:$/
*/
public function usingTheFollowingContentDimensions(TableNode $contentDimensions): void
{
$this->contentDimensionsToUse = GherkinTableNodeBasedContentDimensionSource::fromGherkinTableNode($contentDimensions);
}

/**
* @Given /^using the following node types:$/
*/
public function usingTheFollowingNodeTypes(PyStringNode $serializedNodeTypesConfiguration): void
{
$this->nodeTypesToUse = GherkinPyStringNodeBasedNodeTypeManagerFactory::create($serializedNodeTypesConfiguration);
}

/**
* @Given /^using the following node types with fallback to "([^"]*)":$/
*/
public function usingTheFollowingNodeTypesWithFallback(string $fallbackNodeTypeName, PyStringNode $serializedNodeTypesConfiguration): void
{
$this->nodeTypesToUse = GherkinPyStringNodeBasedNodeTypeManagerFactory::create(
$serializedNodeTypesConfiguration,
$fallbackNodeTypeName
);
}

/**
* @Given /^using identifier "([^"]*)", I define a content repository$/
*/
public function usingIdentifierIDefineAContentRepository(string $contentRepositoryId): void
{
if (array_key_exists($contentRepositoryId, $this->contentRepositories)) {
throw new \DomainException('already defined content repository ' . $contentRepositoryId);
} else {
$this->contentRepositories[$contentRepositoryId] = $this->setUpContentRepository(
ContentRepositoryId::fromString($contentRepositoryId),
$this->contentDimensionsToUse,
$this->nodeTypesToUse
);
$this->contentDimensionsToUse = null;
$this->nodeTypesToUse = null;
}
}

/**
* @Given /^I change the content dimensions in content repository "([^"]*)" to:$/
*/
public function iChangeTheContentDimensionsInContentRepositoryTo(string $contentRepositoryId, TableNode $contentDimensions): void
{
if (!array_key_exists($contentRepositoryId, $this->contentRepositories)) {
throw new \DomainException('undeclared content repository ' . $contentRepositoryId);
} else {
$contentRepository = $this->contentRepositories[$contentRepositoryId];
$this->contentRepositories[$contentRepositoryId] = $this->createContentRepository(
ContentRepositoryId::fromString($contentRepositoryId),
GherkinTableNodeBasedContentDimensionSource::fromGherkinTableNode($contentDimensions),
$contentRepository->getNodeTypeManager()
);
if ($this->currentContentRepository->id->value === $contentRepositoryId) {
$this->currentContentRepository = $this->contentRepositories[$contentRepositoryId];
}
}
}

/**
* @Given /^I change the node types in content repository "([^"]*)" to:$/
*/
public function iChangeTheNodeTypesInContentRepositoryTo(
string $contentRepositoryId,
PyStringNode $serializedNodeTypesConfiguration
): void {
if (!array_key_exists($contentRepositoryId, $this->contentRepositories)) {
throw new \DomainException('undeclared content repository ' . $contentRepositoryId);
} else {
$contentRepository = $this->contentRepositories[$contentRepositoryId];
$this->contentRepositories[$contentRepositoryId] = $this->createContentRepository(
ContentRepositoryId::fromString($contentRepositoryId),
$contentRepository->getContentDimensionSource(),
GherkinPyStringNodeBasedNodeTypeManagerFactory::create($serializedNodeTypesConfiguration)
);
if ($this->currentContentRepository->id->value === $contentRepositoryId) {
$this->currentContentRepository = $this->contentRepositories[$contentRepositoryId];
}
}
}

/**
* @Given /^I change the node types in content repository "([^"]*)" with fallback "([^"]*)" to:$/
*/
public function iChangeTheNodeTypesInContentRepositoryWithFallbackTo(
string $contentRepositoryId,
string $fallbackNodeTypeName,
PyStringNode $serializedNodeTypesConfiguration
): void {
if (!array_key_exists($contentRepositoryId, $this->contentRepositories)) {
throw new \DomainException('undeclared content repository ' . $contentRepositoryId);
} else {
$contentRepository = $this->contentRepositories[$contentRepositoryId];
$this->contentRepositories[$contentRepositoryId] = $this->createContentRepository(
ContentRepositoryId::fromString($contentRepositoryId),
$contentRepository->getContentDimensionSource(),
GherkinPyStringNodeBasedNodeTypeManagerFactory::create(
$serializedNodeTypesConfiguration,
$fallbackNodeTypeName
)
);
if ($this->currentContentRepository->id->value === $contentRepositoryId) {
$this->currentContentRepository = $this->contentRepositories[$contentRepositoryId];
}
}
}

protected function setUpContentRepository(
ContentRepositoryId $contentRepositoryId,
ContentDimensionSourceInterface $contentDimensionSource,
NodeTypeManager $nodeTypeManager
): ContentRepository {
/**
* Reset events and projections
* ============================
*
* PITFALL: for a long time, the code below was a two-liner (it is not anymore, for reasons explained here):
* - reset projections (truncate table contents)
* - truncate events table.
*
* This code has SERIOUS Race Condition and Bug Potential.
* tl;dr: It is CRUCIAL that *FIRST* the event store is emptied, and *then* the projection state is reset;
* so the OPPOSITE order as described above.
*
* If doing it in the way described initially, the following can happen (time flows from top to bottom):
*
* ```
* Main Behat Process Dangling Projection catch up worker
* ================== ===================================
*
* (hasn't started working yet, simply sleeping)
*
* 1) Projection State reset
* "oh, I have some work to do to catch up EVERYTHING"
* "query the events table"
*
* 2) Event Table Reset
* (events table is already loaded into memory) -> replay WIP
*
* (new commands/events start happening,
* in the new testcase)
* ==> ERRORS because the projection now contains the result of both
* old AND new events (of the two different testcases) <==
* ```
*
* This was an actual bug which bit us and made our tests unstable :D :D
*
* How did we find this? By the virtue of our Race Tracker (Docs: see {@see RaceTrackerCatchUpHook}), which
* checks for events being applied multiple times to a projection.
* ... and additionally by using {@see logToRaceConditionTracker()} to find the interleavings between the
* Catch Up process and the testcase reset.
*/

$eventTableName = sprintf('cr_%s_events', $contentRepositoryId->value);
$this->getDatabaseConnection()->executeStatement('TRUNCATE ' . $eventTableName);

$contentRepository = $this->createContentRepository(
$contentRepositoryId,
$contentDimensionSource,
$nodeTypeManager
);

if (!in_array($contentRepository->id, $this->alreadySetUpContentRepositories)) {
$contentRepository->setUp();
}
$contentRepository->resetProjectionStates();

return $contentRepository;
}

abstract protected function getDatabaseConnection(): Connection;

abstract protected function createContentRepository(
ContentRepositoryId $contentRepositoryId,
ContentDimensionSourceInterface $contentDimensionSource,
NodeTypeManager $nodeTypeManager
): ContentRepository;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ Feature: Create a root node aggregate
and Nody McNodeface, a new root node aggregate to be added.

Background: The stage is set
Given I use no content dimensions
And the following NodeTypes to define content repository "default":
"""
Given using no content dimensions
And using the following node types:
"""yaml
'Neos.ContentRepository:Root': []
'Neos.ContentRepository.Testing:AbstractRoot':
abstract: true
'Neos.ContentRepository.Testing:NonRoot': []
"""
And using identifier "default", I define a content repository
And I am in content repository "default"
And I am user identified by "initiating-user-identifier"
And the command CreateRootWorkspace is executed with payload:
| Key | Value |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ Feature: Create a root node aggregate
and Nody McNodeface, a new root node aggregate to be added.

Background:
Given I use no content dimensions
And the following NodeTypes to define content repository "default":
"""
Given using no content dimensions
And using the following node types:
"""yaml
'Neos.ContentRepository:Root': []
'Neos.ContentRepository:AnotherRoot':
superTypes:
'Neos.ContentRepository:Root': true
'Neos.ContentRepository.Testing:NonRoot': []
"""
And using identifier "default", I define a content repository
And I am in content repository "default"
And I am user identified by "initiating-user-identifier"
And the command CreateRootWorkspace is executed with payload:
| Key | Value |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ Feature: Create a root node aggregate
and Nody McNodeface, a new root node aggregate to be added.

Background:
Given I use the following content dimensions:
Given using the following content dimensions:
| Identifier | Values | Generalizations |
| language | mul, de, en, gsw | gsw->de->mul, en->mul |
And the following NodeTypes to define content repository "default":
"""
And using the following node types:
"""yaml
'Neos.ContentRepository:Root': []
'Neos.ContentRepository:AnotherRoot':
superTypes:
'Neos.ContentRepository:Root': true
"""
And using identifier "default", I define a content repository
And I am in content repository "default"
And I am user identified by "initiating-user-identifier"
And the command CreateRootWorkspace is executed with payload:
| Key | Value |
Expand Down
Loading

0 comments on commit d614486

Please sign in to comment.