diff --git a/src/Contracts/SwaggerDriverContract.php b/src/Contracts/SwaggerDriverContract.php index 0d9aea0..dc74ccb 100644 --- a/src/Contracts/SwaggerDriverContract.php +++ b/src/Contracts/SwaggerDriverContract.php @@ -5,16 +5,26 @@ interface SwaggerDriverContract { /** - * Save temporary data + * Save current temporary data * * @param array $data */ - public function saveTmpData($data); + public function saveTmpData(array $data): void; /** - * Get temporary data + * Get current temporary data */ - public function getTmpData(); + public function getTmpData(): ?array; + + /** + * Save shared (result) temporary data + */ + public function saveSharedTmpData(callable $callback): void; + + /** + * Get shared (result) temporary data + */ + public function getSharedTmpData(): ?array; /** * Save production data diff --git a/src/Drivers/BaseDriver.php b/src/Drivers/BaseDriver.php index 0bef982..c759dc7 100644 --- a/src/Drivers/BaseDriver.php +++ b/src/Drivers/BaseDriver.php @@ -2,28 +2,43 @@ namespace RonasIT\AutoDoc\Drivers; +use Illuminate\Support\Facades\ParallelTesting; use RonasIT\AutoDoc\Contracts\SwaggerDriverContract; +use RuntimeException; abstract class BaseDriver implements SwaggerDriverContract { protected string $tempFilePath; + protected string $sharedTempFilePath; public function __construct() { - $this->tempFilePath = storage_path('temp_documentation.json'); + $this->sharedTempFilePath = storage_path('temp_documentation.json'); + + $this->tempFilePath = ($token = ParallelTesting::token()) + ? storage_path("temp_documentation_{$token}.json") + : $this->sharedTempFilePath; } - public function saveTmpData($data): void + public function saveTmpData(array $data): void { - file_put_contents($this->tempFilePath, json_encode($data)); + $this->saveJsonToFile($this->tempFilePath, $data); } public function getTmpData(): ?array { - if (file_exists($this->tempFilePath)) { - $content = file_get_contents($this->tempFilePath); + return $this->getJsonFromFile($this->tempFilePath); + } - return json_decode($content, true); + public function saveSharedTmpData(callable $callback): void + { + $this->writeFileWithLock($this->sharedTempFilePath, $callback); + } + + public function getSharedTmpData(): ?array + { + if (file_exists($this->sharedTempFilePath)) { + return $this->readFileWithLock($this->sharedTempFilePath); } return null; @@ -35,4 +50,88 @@ protected function clearTmpData(): void unlink($this->tempFilePath); } } + + protected function saveJsonToFile(string $filePath, array $data): void + { + file_put_contents($filePath, json_encode($data)); + } + + protected function getJsonFromFile(string $filePath): ?array + { + if (file_exists($filePath)) { + $content = file_get_contents($filePath); + + return json_decode($content, true); + } + + return null; + } + + protected function readFileWithLock(string $filePath): array + { + $handle = fopen($filePath, 'r'); + + try { + $this->acquireLock($handle, LOCK_SH); + + return $this->readJsonFromStream($handle); + } finally { + flock($handle, LOCK_UN); + fclose($handle); + } + } + + protected function writeFileWithLock(string $filePath, callable $callback): void + { + $handle = fopen($filePath, 'c+'); + + try { + $this->acquireLock($handle, LOCK_EX | LOCK_NB); + + $data = $callback($this->readJsonFromStream($handle)); + + $this->writeJsonToStream($handle, $data); + } finally { + flock($handle, LOCK_UN); + fclose($handle); + } + } + + protected function writeJsonToStream($handle, array $data): void + { + ftruncate($handle, 0); + rewind($handle); + fwrite($handle, json_encode($data)); + fflush($handle); + } + + protected function readJsonFromStream($handle): ?array + { + $content = stream_get_contents($handle); + + return ($content === false) ? null : json_decode($content, true); + } + + /** + * @codeCoverageIgnore + */ + protected function acquireLock( + $handle, + int $operation, + int $maxRetries = 20, + int $minWaitTime = 100, + int $maxWaitTime = 1000, + ): void { + $retryCounter = 0; + + while (!flock($handle, $operation)) { + if ($retryCounter >= $maxRetries) { + throw new RuntimeException('Unable to lock file'); + } + + usleep(rand($minWaitTime, $maxWaitTime)); + + $retryCounter++; + } + } } diff --git a/src/Drivers/LocalDriver.php b/src/Drivers/LocalDriver.php index f8b7f27..58fa996 100755 --- a/src/Drivers/LocalDriver.php +++ b/src/Drivers/LocalDriver.php @@ -22,7 +22,7 @@ public function __construct() public function saveData(): void { - file_put_contents($this->prodFilePath, json_encode($this->getTmpData())); + file_put_contents($this->prodFilePath, json_encode($this->getSharedTmpData())); $this->clearTmpData(); } diff --git a/src/Drivers/RemoteDriver.php b/src/Drivers/RemoteDriver.php index 16b5233..ff7ef24 100755 --- a/src/Drivers/RemoteDriver.php +++ b/src/Drivers/RemoteDriver.php @@ -24,7 +24,7 @@ public function __construct() public function saveData(): void { - $this->makeHttpRequest('post', $this->getUrl(), $this->getTmpData(), [ + $this->makeHttpRequest('post', $this->getUrl(), $this->getSharedTmpData(), [ 'Content-Type: application/json', ]); diff --git a/src/Drivers/StorageDriver.php b/src/Drivers/StorageDriver.php index e08646d..b13c1ed 100755 --- a/src/Drivers/StorageDriver.php +++ b/src/Drivers/StorageDriver.php @@ -26,7 +26,7 @@ public function __construct() public function saveData(): void { - $this->disk->put($this->prodFilePath, json_encode($this->getTmpData())); + $this->disk->put($this->prodFilePath, json_encode($this->getSharedTmpData())); $this->clearTmpData(); } diff --git a/src/Services/SwaggerService.php b/src/Services/SwaggerService.php index 5c8cf87..57984e0 100755 --- a/src/Services/SwaggerService.php +++ b/src/Services/SwaggerService.php @@ -8,6 +8,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Str; use ReflectionClass; +use RonasIT\AutoDoc\Contracts\SwaggerDriverContract; use RonasIT\AutoDoc\Exceptions\DocFileNotExistsException; use RonasIT\AutoDoc\Exceptions\EmptyContactEmailException; use RonasIT\AutoDoc\Exceptions\EmptyDocFileException; @@ -17,7 +18,6 @@ use RonasIT\AutoDoc\Exceptions\SwaggerDriverClassNotFoundException; use RonasIT\AutoDoc\Exceptions\UnsupportedDocumentationViewerException; use RonasIT\AutoDoc\Exceptions\WrongSecurityConfigException; -use RonasIT\AutoDoc\Contracts\SwaggerDriverContract; use RonasIT\AutoDoc\Traits\GetDependenciesTrait; use RonasIT\AutoDoc\Validators\SwaggerSpecValidator; use Symfony\Component\HttpFoundation\Response; @@ -1002,4 +1002,15 @@ protected function mergeOpenAPIDocs(array &$documentation, array $additionalDocu ); } } + + public function mergeTempDocumentation(): void + { + $this->driver->saveSharedTmpData(function ($sharedTmpData) { + $resultDocContent = $sharedTmpData ?? $this->generateEmptyData(); + + $this->mergeOpenAPIDocs($resultDocContent, $this->data); + + return $resultDocContent; + }); + } } diff --git a/src/Support/PHPUnit/EventSubscribers/SwaggerSaveDocumentationSubscriber.php b/src/Support/PHPUnit/EventSubscribers/SwaggerSaveDocumentationSubscriber.php index 6e3f2f3..cad2d71 100644 --- a/src/Support/PHPUnit/EventSubscribers/SwaggerSaveDocumentationSubscriber.php +++ b/src/Support/PHPUnit/EventSubscribers/SwaggerSaveDocumentationSubscriber.php @@ -3,6 +3,8 @@ namespace RonasIT\AutoDoc\Support\PHPUnit\EventSubscribers; use Illuminate\Contracts\Console\Kernel; +use Illuminate\Foundation\Application; +use Illuminate\Support\Facades\ParallelTesting; use PHPUnit\Event\Application\Finished; use PHPUnit\Event\Application\FinishedSubscriber; use RonasIT\AutoDoc\Services\SwaggerService; @@ -13,12 +15,18 @@ public function notify(Finished $event): void { $this->createApplication(); - app(SwaggerService::class)->saveProductionData(); + $swaggerService = app(SwaggerService::class); + + if (ParallelTesting::token()) { + $swaggerService->mergeTempDocumentation(); + } + + $swaggerService->saveProductionData(); } protected function createApplication(): void { - $app = require base_path('bootstrap/app.php'); + $app = require Application::inferBasePath() . '/bootstrap/app.php'; $app->loadEnvironmentFrom('.env.testing'); $app->make(Kernel::class)->bootstrap(); diff --git a/tests/LocalDriverTest.php b/tests/LocalDriverTest.php index d96a7db..8a8acf0 100755 --- a/tests/LocalDriverTest.php +++ b/tests/LocalDriverTest.php @@ -2,6 +2,7 @@ namespace RonasIT\AutoDoc\Tests; +use Illuminate\Support\Facades\ParallelTesting; use RonasIT\AutoDoc\Drivers\LocalDriver; use Illuminate\Contracts\Filesystem\FileNotFoundException; use RonasIT\AutoDoc\Exceptions\MissedProductionFilePathException; @@ -35,6 +36,28 @@ public function testSaveTmpData() $this->assertFileEquals($this->generateFixturePath('tmp_data_non_formatted.json'), self::$tmpDocumentationFilePath); } + public function testSaveTmpDataCheckTokenBasedPath() + { + $token = 'workerID'; + + ParallelTesting::resolveTokenUsing(fn () => $token); + + $tmpDocPath = __DIR__ . "/../storage/temp_documentation_{$token}.json"; + + app(LocalDriver::class)->saveTmpData(self::$tmpData); + + $this->assertFileExists($tmpDocPath); + $this->assertFileEquals($this->generateFixturePath('tmp_data_non_formatted.json'), $tmpDocPath); + } + + public function testSaveSharedTmpData() + { + self::$localDriverClass->saveSharedTmpData(fn () => self::$tmpData); + + $this->assertFileExists(self::$tmpDocumentationFilePath); + $this->assertFileEquals($this->generateFixturePath('tmp_data_non_formatted.json'), self::$tmpDocumentationFilePath); + } + public function testGetTmpData() { file_put_contents(self::$tmpDocumentationFilePath, json_encode(self::$tmpData)); @@ -51,6 +74,22 @@ public function testGetTmpDataNoFile() $this->assertNull($result); } + public function testGetSharedTmpData() + { + file_put_contents(self::$tmpDocumentationFilePath, json_encode(self::$tmpData)); + + $result = self::$localDriverClass->getSharedTmpData(); + + $this->assertEquals(self::$tmpData, $result); + } + + public function testGetSharedTmpDataNoFile() + { + $result = self::$localDriverClass->getSharedTmpData(); + + $this->assertNull($result); + } + public function testCreateClassConfigEmpty() { $this->expectException(MissedProductionFilePathException::class); diff --git a/tests/RemoteDriverTest.php b/tests/RemoteDriverTest.php index 70a8097..a9a7a28 100755 --- a/tests/RemoteDriverTest.php +++ b/tests/RemoteDriverTest.php @@ -33,6 +33,14 @@ public function testSaveTmpData() $this->assertFileEquals($this->generateFixturePath('tmp_data_non_formatted.json'), self::$tmpDocumentationFilePath); } + public function testSaveSharedTmpData() + { + self::$remoteDriverClass->saveSharedTmpData(fn () => self::$tmpData); + + $this->assertFileExists(self::$tmpDocumentationFilePath); + $this->assertFileEquals($this->generateFixturePath('tmp_data_non_formatted.json'), self::$tmpDocumentationFilePath); + } + public function testGetTmpData() { file_put_contents(self::$tmpDocumentationFilePath, json_encode(self::$tmpData)); @@ -49,6 +57,22 @@ public function testGetTmpDataNoFile() $this->assertNull($result); } + public function testGetSharedTmpData() + { + file_put_contents(self::$tmpDocumentationFilePath, json_encode(self::$tmpData)); + + $result = self::$remoteDriverClass->getSharedTmpData(); + + $this->assertEquals(self::$tmpData, $result); + } + + public function testGetSharedTmpDataNoFile() + { + $result = self::$remoteDriverClass->getSharedTmpData(); + + $this->assertNull($result); + } + public function testCreateClassConfigEmpty() { $this->expectException(MissedRemoteDocumentationUrlException::class); diff --git a/tests/StorageDriverTest.php b/tests/StorageDriverTest.php index a30da60..bfda352 100755 --- a/tests/StorageDriverTest.php +++ b/tests/StorageDriverTest.php @@ -41,6 +41,14 @@ public function testSaveTmpData() $this->assertFileEquals($this->generateFixturePath('tmp_data_non_formatted.json'), self::$tmpDocumentationFilePath); } + public function testSaveSharedTmpData() + { + self::$storageDriverClass->saveSharedTmpData(fn () => self::$tmpData); + + $this->assertFileExists(self::$tmpDocumentationFilePath); + $this->assertFileEquals($this->generateFixturePath('tmp_data_non_formatted.json'), self::$tmpDocumentationFilePath); + } + public function testGetTmpData() { file_put_contents(self::$tmpDocumentationFilePath, json_encode(self::$tmpData)); @@ -57,6 +65,22 @@ public function testGetTmpDataNoFile() $this->assertNull($result); } + public function testGetSharedTmpData() + { + file_put_contents(self::$tmpDocumentationFilePath, json_encode(self::$tmpData)); + + $result = self::$storageDriverClass->getSharedTmpData(); + + $this->assertEquals(self::$tmpData, $result); + } + + public function testGetSharedTmpDataNoFile() + { + $result = self::$storageDriverClass->getSharedTmpData(); + + $this->assertNull($result); + } + public function testCreateClassConfigEmpty() { $this->expectException(MissedProductionFilePathException::class); diff --git a/tests/SwaggerServiceTest.php b/tests/SwaggerServiceTest.php index 4fb3872..8987c39 100644 --- a/tests/SwaggerServiceTest.php +++ b/tests/SwaggerServiceTest.php @@ -16,8 +16,8 @@ use RonasIT\AutoDoc\Exceptions\SpecValidation\InvalidSwaggerSpecException; use RonasIT\AutoDoc\Exceptions\SpecValidation\InvalidSwaggerVersionException; use RonasIT\AutoDoc\Exceptions\SpecValidation\MissingExternalRefException; -use RonasIT\AutoDoc\Exceptions\SpecValidation\MissingLocalRefException; use RonasIT\AutoDoc\Exceptions\SpecValidation\MissingFieldException; +use RonasIT\AutoDoc\Exceptions\SpecValidation\MissingLocalRefException; use RonasIT\AutoDoc\Exceptions\SpecValidation\MissingPathParamException; use RonasIT\AutoDoc\Exceptions\SpecValidation\MissingPathPlaceholderException; use RonasIT\AutoDoc\Exceptions\SpecValidation\MissingRefFileException; @@ -811,4 +811,19 @@ public function testAddDataDescriptionForRouteConditionals() app(SwaggerService::class)->addData($request, $response); } + + public function testMergeTempDocumentation() + { + $this->mockDriverGetTmpDataAndGetSharedTmpData( + tmpData: $this->getJsonFixture('tmp_data_search_users_empty_request'), + sharedTmpData: $this->getJsonFixture('tmp_data_post_user_request'), + ); + + $service = app(SwaggerService::class); + + $service->mergeTempDocumentation(); + + $this->assertFileExists(storage_path('temp_documentation.json')); + $this->assertFileEquals($this->generateFixturePath('tmp_data_merged.json'), storage_path('temp_documentation.json')); + } } diff --git a/tests/fixtures/SwaggerServiceTest/tmp_data_merged.json b/tests/fixtures/SwaggerServiceTest/tmp_data_merged.json new file mode 100644 index 0000000..694818a --- /dev/null +++ b/tests/fixtures/SwaggerServiceTest/tmp_data_merged.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","servers":[{"url":"http:\/\/localhost"}],"paths":{"\/users":{"post":{"tags":["users"],"consumes":["application\/x-www-form-urlencoded"],"produces":["application\/json"],"parameters":[],"requestBody":{"required":true,"description":"","content":{"application\/x-www-form-urlencoded":{"schema":{"$ref":"#\/components\/schemas\/usersObject"}}}},"responses":{"200":{"description":"Operation successfully done","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/postUsers200ResponseObject","type":"object"},"example":[{"id":1,"name":"admin","users":[{"id":1,"name":"admin"}]},{"id":2,"name":"client","users":[{"id":2,"name":"first_client"},{"id":3,"name":"second_client"}]}]}}}},"security":[{"jwt":[]}],"description":"","summary":"test","deprecated":false}},"\/api\/users":{"get":{"tags":["api"],"produces":["application\/json"],"parameters":[],"responses":{"200":{"description":"OK","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/getApiusers200ResponseObject","type":"object"},"example":{"current_page":1,"data":[{"id":1,"first_name":"Billy","last_name":"Coleman","email":"billy.coleman@example.com","created_at":null,"updated_at":null,"role_id":1,"date_of_birth":"1986-05-20","phone":"+79535482530","position":"admin","starts_on":"2022-04-16 00:00:00","hr_id":null,"manager_id":null,"lead_id":null,"avatar_id":null,"deleted_at":null,"company_id":1}],"first_page_url":"http:\/\/localhost\/api\/users?page=1","from":1,"last_page":1,"last_page_url":"http:\/\/localhost\/api\/users?page=1","links":[{"url":null,"label":"« Previous","active":false},{"url":"http:\/\/localhost\/api\/users?page=1","label":"1","active":true},{"url":null,"label":"Next »","active":false}],"next_page_url":null,"path":"http:\/\/localhost\/api\/users","per_page":20,"prev_page_url":null,"to":1,"total":1}}}}},"security":[],"description":"","consumes":[]}}},"components":{"schemas":{"usersObject":{"type":"object","properties":{"query":{"type":"string","description":""},"user_id":{"type":"integer","description":"with_to_array_rule_string_name"},"is_email_enabled":{"type":"string","description":"test_rule_without_to_string"}},"example":{"users":[1,2],"query":null},"required":["query"]},"postUsers200ResponseObject":{"type":"array","properties":{"items":{"allOf":[{"type":"array"}]}}},"getApiusers200ResponseObject":{"type":"object","properties":{"current_page":{"type":"integer"},"data":{"type":"array"},"first_page_url":{"type":"string"},"from":{"type":"integer"},"last_page":{"type":"integer"},"last_page_url":{"type":"string"},"links":{"type":"array"},"next_page_url":{"nullable":true},"path":{"type":"string"},"per_page":{"type":"integer"},"prev_page_url":{"nullable":true},"to":{"type":"integer"},"total":{"type":"integer"}}}}},"info":{"description":"This is automatically collected documentation","version":"0.0.0","title":"Name of Your Application","termsOfService":"","contact":{"email":"your@email.com"}},"securityDefinitions":{"jwt":{"type":"apiKey","name":"authorization","in":"header"}}} \ No newline at end of file diff --git a/tests/support/Traits/SwaggerServiceMockTrait.php b/tests/support/Traits/SwaggerServiceMockTrait.php index f6cce41..91e808c 100644 --- a/tests/support/Traits/SwaggerServiceMockTrait.php +++ b/tests/support/Traits/SwaggerServiceMockTrait.php @@ -64,6 +64,23 @@ protected function mockDriverGetTmpData($tmpData, $driverClass = LocalDriver::cl $this->app->instance($driverClass, $driver); } + protected function mockDriverGetTmpDataAndGetSharedTmpData(array $tmpData, array $sharedTmpData, string $driverClass = LocalDriver::class): void + { + $driver = $this->mockClass($driverClass, ['getTmpData', 'readJsonFromStream']); + + $driver + ->expects($this->exactly(1)) + ->method('getTmpData') + ->willReturn($tmpData); + + $driver + ->expects($this->exactly(1)) + ->method('readJsonFromStream') + ->willReturn($sharedTmpData); + + $this->app->instance($driverClass, $driver); + } + protected function mockDriverGetDocumentation($data, $driverClass = LocalDriver::class): void { $driver = $this->mockClass($driverClass, ['getDocumentation']);