var_export($item, true), $schema['enum']))); } if (is_string($types)) { $types = [$types]; } if (is_array($types)) { $typeValid = false; foreach ($types as $type) { $typeValid = match ($type) { 'array' => is_array($value) && array_is_list($value), 'object' => is_array($value) && ! array_is_list($value), 'string' => is_string($value), 'integer' => is_int($value), 'number' => is_int($value) || is_float($value), 'boolean' => is_bool($value), 'null' => $value === null, default => false, }; if ($typeValid) { break; } } if (! $typeValid) { $errors[] = sprintf('%s has the wrong type.', $path); return $errors; } } if (is_array($value) && array_is_list($value)) { if (isset($schema['minItems']) && count($value) < (int) $schema['minItems']) { $errors[] = sprintf('%s must contain at least %d item(s).', $path, (int) $schema['minItems']); } if (isset($schema['items']) && is_array($schema['items'])) { foreach ($value as $index => $item) { $errors = array_merge($errors, validateTrendSchema($item, $schema['items'], $defs, sprintf('%s[%d]', $path, $index))); } } return $errors; } if (is_array($value) && ! array_is_list($value)) { foreach ($schema['required'] ?? [] as $requiredKey) { if (! array_key_exists((string) $requiredKey, $value)) { $errors[] = sprintf('%s is missing required key [%s].', $path, $requiredKey); } } if (($schema['additionalProperties'] ?? true) === false) { $allowedKeys = array_keys($schema['properties'] ?? []); foreach (array_keys($value) as $key) { if (! in_array($key, $allowedKeys, true)) { $errors[] = sprintf('%s contains unsupported key [%s].', $path, $key); } } } foreach ($schema['properties'] ?? [] as $key => $propertySchema) { if (! array_key_exists($key, $value)) { continue; } $errors = array_merge($errors, validateTrendSchema($value[$key], $propertySchema, $defs, $path.'.'.$key)); } return $errors; } if ((is_int($value) || is_float($value)) && isset($schema['minimum']) && $value < $schema['minimum']) { $errors[] = sprintf('%s must be >= %s.', $path, $schema['minimum']); } return $errors; } it('keeps the generated trend-history artifact synchronized with the checked-in JSON schema contract', function (): void { $schemaPath = repo_path('specs/211-runtime-trend-recalibration/contracts/test-runtime-trend-history.schema.json'); $artifactDirectory = TestLaneTrendFixtures::artifactDirectory('trend-schema-contract'); $report = TestLaneTrendFixtures::buildReport( laneId: 'fast-feedback', wallClockSeconds: 184.6, durationsByFile: [ 'tests/Feature/Guards/TestLaneTrendContractSchemaTest.php' => 16.4, 'tests/Feature/Guards/TestLaneArtifactsContractTest.php' => 11.2, ], artifactDirectory: $artifactDirectory, ciContext: [ 'workflowId' => 'pr-fast-feedback', 'triggerClass' => 'pull-request', 'entryPointResolved' => true, 'workflowLaneMatched' => true, ], comparisonProfile: 'shared-test-fixture-slimming', ); /** @var array $schema */ $schema = json_decode((string) file_get_contents($schemaPath), true, 512, JSON_THROW_ON_ERROR); $artifact = $report['trendHistoryArtifact']; $errors = validateTrendSchema($artifact, $schema, $schema['$defs'] ?? []); expect($artifact['schemaVersion'])->toBe('1.0.0') ->and($artifact['laneId'])->toBe('fast-feedback') ->and($artifact['workflowProfile'])->toBe('pr-fast-feedback') ->and($artifact['history'][0]['artifactRefs']['trendHistory'])->toBe($artifactDirectory.'/fast-feedback-latest.trend-history.json') ->and($errors)->toBe([]); });