pact-foundation / pact-php

PHP version of Pact. Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project
Apache License 2.0
273 stars 92 forks source link

Pact-PHP <=9.x.x - phpunit exits with code 137 and pact-files are not written #234

Closed 4d4ch4u32 closed 5 months ago

4d4ch4u32 commented 2 years ago

I'm using pact in combination with phpunit. Sometimes phpunit is exiting with exit code 137. Also, the pact files are not written to the configured folder.

Is that a known error?

4d4ch4u32 commented 1 year ago

Here again with an error 137 in GitHub workflows:

Result of the workflow:

Time: 00:17.983, Memory: 72.50 MB

OK (5 tests, 10 assertions)

Error: Process completed with exit code 137.

The used pact test-case for that tests:

<?php

declare(strict_types=1);

namespace App\Tests\Pact;

use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use ApiPlatform\Symfony\Bundle\Test\Client;
use PhpPact\Consumer\InteractionBuilder;
use PhpPact\Consumer\Model\ConsumerRequest;
use PhpPact\Consumer\Model\ProviderResponse;
use PhpPact\Standalone\MockService\MockServer;
use PhpPact\Standalone\MockService\MockServerConfig;
use function Safe\exec;
use function Safe\fwrite;

/** @psalm-suppress MissingConstructor */
class PactTestCase extends ApiTestCase
{
    public const PROVIDER_NAME    = 'a-service';
    public const MOCK_SERVER_PORT = 9999;
    public const CONSUMER_NAME    = 'another-service';

    private ?Client $currentClient = null;

    private int $mockServerPID;

    private InteractionBuilder $interactionBuilder;

    private string $mockServerUrl;

    public function stopPactMockServer(): void
    {
        // sometimes, this is ending in a exit code 137 of phpunit
        $killCounter = 0;
        fwrite(STDOUT, \sprintf('kill pid %s', $this->mockServerPID));

        do {
            try {
                exec(\sprintf('kill %s || true', $this->mockServerPID));
                \sleep(1);
                $killCounter++;
                fwrite(STDOUT, '.');
            } catch (\Throwable) {
            }
        } while (\file_exists('/proc/' . $this->mockServerPID) && $killCounter < 30);

        if (\file_exists('/proc/' . $this->mockServerPID)) {
            throw new \RuntimeException(\sprintf(
                'Cannot kill mock-server process PID %s',
                $this->mockServerPID,
            ));
        }
    }

    /** @psalm-suppress MethodSignatureMismatch */
    public function getClient(): Client
    {
        if (null === $this->currentClient) {
            $this->currentClient = self::createClient();
        }

        return $this->currentClient;
    }

    protected function getInteractionBuilder(): InteractionBuilder
    {
        return $this->interactionBuilder;
    }

    protected function getPactMockServerUrl(): string
    {
        return $this->mockServerUrl;
    }

    protected function createInteraction(
        string $name,
        string $providerName,
        ConsumerRequest $consumerRequest,
        ProviderResponse $providerResponse,
        string $consumerName,
        int $port,
    ): InteractionBuilder {
        $interactionBuilder =
            new InteractionBuilder($this->getPactMockServerConfiguration($providerName, $consumerName, $port));
        $interactionBuilder
            ->uponReceiving($name)
            ->with($consumerRequest)
            ->willRespondWith($providerResponse);

        $this->interactionBuilder = $interactionBuilder;

        return $interactionBuilder;
    }

    protected function tearDown(): void
    {
        $this->stopPactMockServer();

        parent::tearDown();
    }

    protected function startPactMockServer(string $providerName, string $consumerName, int $port): void
    {
        $this->mockServerPID = $this->getPactMockServer($providerName, $consumerName, $port)->start();
    }

    /** @psalm-suppress UndefinedInterfaceMethod */
    protected function getPactMockServerConfiguration(
        string $providerName,
        string $consumerName,
        int $port,
    ): MockServerConfig {
        $config = new MockServerConfig();
        $config->setHost('localhost');
        $config->setPort($port);
        $config->setConsumer($consumerName);
        $config->setProvider($providerName);
        $config->setCors(true);
        $config->setHealthCheckTimeout(30);
        $config->setHealthCheckRetrySec(1);
        $config->setPactDir(\dirname(__DIR__) . '/../pacts');
        $config->setPactFileWriteMode('merge');
        $config->setLog(\dirname(__DIR__) . '/../var/log/test/pact');
        $config->setLogLevel('DEBUG');

        $this->mockServerUrl = "http://{$config->getHost()}:{$config->getPort()}";

        return $config;
    }

    protected function getPactMockServer(string $providerName, string $consumerName, int $port): MockServer
    {
        return new MockServer($this->getPactMockServerConfiguration($providerName, $consumerName, $port));
    }
}

A test class using the PactTestCase class:

<?php

declare(strict_types=1);

namespace App\Tests\Pact\Tests;

use App\Tests\Pact\DataRetrieverDummy;
use App\Tests\Pact\PactTestCase;
use PhpPact\Consumer\Matcher\Matcher;
use PhpPact\Consumer\Model\ConsumerRequest;
use PhpPact\Consumer\Model\ProviderResponse;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class RetrievePimPreProcessingBrandDataTest extends PactTestCase
{
    /** @phpstan-ignore-next-line  */
    private HttpClientInterface $client;

    protected function setUp(): void
    {
        self::bootKernel();
        $container    = static::getContainer();
        $this->client = $container->get(HttpClientInterface::class);

        $this->startPactMockServer(
            PactTestCase::PROVIDER_NAME,
            PactTestCase::CONSUMER_NAME,
            PactTestCase::MOCK_SERVER_PORT,
        );
    }

    public function testRetrieveData(): void
    {
        $matcher = new Matcher();

        $request = new ConsumerRequest();
        $request
            ->setMethod('GET')
            ->setPath('/the_index/_search');

        $response = new ProviderResponse();
        $response
            ->setStatus(200)
            ->setBody([
                'took'      => $matcher->integer(),
                'timed_out' => $matcher->boolean(),
                '_shards'   => [
                    'total'       => $matcher->integer(),
                    'successfull' => $matcher->integer(),
                    'skipped'     => $matcher->integer(),
                    'failed'      => $matcher->integer(),
                ],
                'hits'      => [
                    'total'     => [
                        'value'    => $matcher->integer(1),
                        'relation' => $matcher->like('eq'),
                    ],
                    'max_score' => $matcher->integer(),
                    'hits'      => $matcher->eachLike([
                        '_index'  => $matcher->like('the_index'),
                        '_type'   => $matcher->like('_doc'),
                        '_score'  => $matcher->integer(),
                        '_source' => [
                            'external_key' => $matcher->like('any-key'),
                            'external_id'  => $matcher->like('522'),
                            'name'         => $matcher->like('Franz'),
                            'ulid'         => $matcher->like('7KVFK80BKTAXAA7EF2Q1QQJEND'),
                        ],
                    ]),
                ],
            ]);

        $this->createInteraction(
            'Retrieve data from ppp (/ppp_none_brands/_search)',
            PactTestCase::PROVIDER_NAME,
            $request,
            $response,
            PactTestCase::CONSUMER_NAME,
            PactTestCase::MOCK_SERVER_PORT,
        );

        $drd = new DataRetrieverDummy();

        /** @var string $path */
        $path   = $request->getPath();
        $result = $drd->retrieveData('http://localhost:9999' . $path);

        $this->getInteractionBuilder()->verify();

        self::assertEquals(1, $result['hits']['total']['value']);
        self::assertEquals(
            'any-key',
            $result['hits']['hits'][0]['_source']['external_key'], 
        );
    }
}

Is this a known error? Can anyone help to solve that issue?

YOU54F commented 5 months ago

Did you ever manage to resolve this @4d4ch4u32

Sorry for the delay in anyone getting back to you. It wouldn't look to me to be a known issue, although I haven't tried to run your code. The project is tested in GitHub actions.

I imagine you may have moved on the from the project now, if so we can consider closing this off, also as you've provided such a nice example, I think it would be worth taking the time to get it working , either with the pact ruby core as you would have been using when this issue was raised, or the pact rust core now available in v10.x (alpha)

4d4ch4u32 commented 5 months ago

After about 1.5 years, this is now very irrelevant...