/
cloud-castle
/
memcached
Обзор
Документация
Войти
/
cloud-castle
/
memcached
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
9
CI/CD
Аналитика
main
benchmarks/generate.php
272 строки
11 KB
Алексей Зорин
docs: бейджи качества генерируются из отчётов инструментов
28 июл 2026, 07:34
28 июл 2026, 07:34
4d3b79c
Код
Авторство
О чём код?
<?php declare(strict_types=1); /** * Генерация сравнительных таблиц из результатов замеров. * * Читает var/comparison/results.json и подставляет таблицы в размеченные * блоки README и wiki на всех языках. Ни одна цифра не пишется руками: * таблица либо сгенерирована из замера, либо её нет. * * @internal инструмент разработки */ $root = \dirname(__DIR__); $resultsFile = $root . '/var/comparison/results.json'; if (!is_file($resultsFile)) { fwrite(STDERR, "Нет результатов замеров: запустите composer docs:compare\n"); exit(1); } /** @var array<string, mixed> $data */ $data = json_decode((string) file_get_contents($resultsFile), true, 512, JSON_THROW_ON_ERROR); /** @var array<string, string> $labels */ $labels = $data['labels']; /** @var list<string> $baselines */ $baselines = $data['baselines']; $own = (string) $data['own']; /** * Экранирует содержимое ячейки таблицы Markdown. */ $cell = static fn (string $value): string => str_replace('|', '\\|', $value); /** * Определяет победителя среди участников, исключая базовые уровни. * * @param array<string, int|float> $values значения метрики * @param string $best 'min' — меньше лучше, 'max' — больше лучше * * @return list<string> имена победителей */ $winners = static function (array $values, string $best) use ($baselines): array { $candidates = array_diff_key($values, array_flip($baselines)); if ($candidates === []) { return []; } $target = $best === 'min' ? min($candidates) : max($candidates); return array_keys($candidates, $target, true); }; /** * Собирает таблицу метрики с колонкой победителя. * * @param array<string, int|float> $values значения метрики * @param string $unit единица измерения * @param string $best 'min' или 'max' * @param string $metricName заголовок колонки значения */ $metricTable = static function (array $values, string $unit, string $best, string $metricName) use ($labels, $baselines, $winners, $cell): string { $champions = $winners($values, $best); $sorted = $values; $best === 'min' ? asort($sorted) : arsort($sorted); $rows = ['| Пакет | ' . $metricName . ' | 🏆 Победитель |', '|---|---:|:---:|']; foreach ($sorted as $name => $value) { $label = $labels[$name] ?? $name; $mark = \in_array($name, $champions, true) ? '🏆' : ''; $suffix = \in_array($name, $baselines, true) ? ' _(базовый уровень)_' : ''; $rows[] = sprintf( '| %s%s | %s %s | %s |', $cell($label), $suffix, is_float($value) ? number_format($value, 1, '.', ' ') : number_format((float) $value, 0, '.', ' '), $unit, $mark, ); } return implode("\n", $rows); }; /** * Собирает матрицу возможностей с колонкой победителя. * * @param array{capabilities: list<string>, matrix: array<string, list<bool>>} $section раздел матрицы * @param string $title заголовок первой колонки */ $matrixTable = static function (array $section, string $title) use ($labels, $baselines, $cell): string { $names = array_keys($section['matrix']); $header = '| ' . $title . ' | ' . implode(' | ', array_map( static fn (string $name): string => $cell($labels[$name] ?? $name), $names, )) . ' |'; $rows = [$header, '|---|' . str_repeat(':---:|', \count($names))]; foreach ($section['capabilities'] as $index => $capability) { $cells = array_map( static fn (string $name): string => ($section['matrix'][$name][$index] ?? false) ? '✅' : '—', $names, ); $rows[] = '| ' . $cell($capability) . ' | ' . implode(' | ', $cells) . ' |'; } $totals = []; foreach ($names as $name) { $totals[$name] = \count(array_filter($section['matrix'][$name])); } $competitors = array_diff_key($totals, array_flip($baselines)); $top = $competitors === [] ? 0 : max($competitors); $rows[] = '| **Итого возможностей** | ' . implode(' | ', array_map( static fn (string $name): string => '**' . $totals[$name] . '**', $names, )) . ' |'; $rows[] = '| **🏆 Победитель** | ' . implode(' | ', array_map( static fn (string $name): string => !\in_array($name, $baselines, true) && $totals[$name] === $top ? '🏆' : '', $names, )) . ' |'; return implode("\n", $rows); }; /** * Собирает таблицу качества кода с победителем по каждой метрике. * * @param array{metrics: array<string, array{label: string, best: string}>, results: array<string, array<string, int|string>>, php_constraints: array<string, string>} $quality */ $qualityTable = static function (array $quality) use ($labels, $baselines, $winners, $cell): string { $names = array_keys($quality['results']); $rows = [ '| Метрика | ' . implode(' | ', array_map(static fn (string $n): string => $cell($labels[$n] ?? $n), $names)) . ' | 🏆 Победитель |', '|---|' . str_repeat('---:|', \count($names)) . ':---:|', ]; foreach ($quality['metrics'] as $key => $meta) { $values = []; foreach ($names as $name) { $values[$name] = (int) $quality['results'][$name][$key]; } $champions = $winners($values, $meta['best']); $rows[] = '| ' . $cell($meta['label']) . ' | ' . implode(' | ', array_map(static fn (string $n): string => (string) $values[$n], $names)) . ' | ' . implode(', ', array_map(static fn (string $n): string => $cell($labels[$n] ?? $n), $champions)) . ' 🏆 |'; } $rows[] = '| Минимальная версия PHP | ' . implode(' | ', array_map( static fn (string $n): string => $cell((string) ($quality['php_constraints'][$n] ?? '—')), $names, )) . ' | — |'; return implode("\n", $rows); }; /** * Собирает бейджи качества из отчётов инструментов. * * Числа берутся из последних прогонов, а не пишутся руками: зашитый * в разметку бейдж устаревает после первого же добавленного теста * и начинает врать пользователю. * * @return string разметка бейджей либо пустая строка, если отчётов нет */ $qualityBadges = static function () use ($root): string { $badges = [ '[](phpstan.neon)', '[](psalm.xml)', '[](phpmd.xml)', '[](phpcs.xml.dist)', '[](deptrac.yaml)', ]; $clover = $root . '/var/coverage/clover.xml'; if (is_file($clover)) { $xml = simplexml_load_string((string) file_get_contents($clover)); if ($xml !== false) { $metrics = $xml->xpath('//project/metrics'); if (isset($metrics[0])) { $statements = (int) $metrics[0]['statements']; $covered = (int) $metrics[0]['coveredstatements']; $percent = $statements === 0 ? 0 : (int) round(100 * $covered / $statements); $badges[] = sprintf( '[](tests)', $percent, $percent >= 90 ? 'brightgreen' : ($percent >= 75 ? 'green' : 'yellow'), ); } } } $summary = $root . '/var/infection/summary.log'; if (is_file($summary)) { $text = (string) file_get_contents($summary); preg_match('/Total:\s*(\d+)/', $text, $total); preg_match('/Killed:\s*(\d+)/', $text, $killed); if (isset($total[1], $killed[1]) && (int) $total[1] > 0) { $msi = (int) round(100 * (int) $killed[1] / (int) $total[1]); $color = $msi >= 90 ? 'brightgreen' : ($msi >= 70 ? 'yellow' : 'orange'); $badges[] = sprintf( '[](infection.json.dist)', $msi, $color, ); } } $badges[] = '[]' . '(https://securityscorecards.dev)'; return implode("\n", $badges); }; $blocks = [ 'BADGES' => $qualityBadges(), 'FEATURES' => "### Функциональность\n\n" . $matrixTable($data['features'], 'Возможность'), 'SECURITY' => "### Безопасность\n\n" . $matrixTable($data['security'], 'Механизм защиты'), 'PERFORMANCE' => "### Производительность\n\n_" . $data['performance']['operation'] . "._\n\n" . $metricTable($data['performance']['results'], $data['performance']['unit'], 'min', 'Время'), 'MEMORY' => "### Потребление памяти\n\n_" . $data['memory']['operation'] . "._\n\n" . $metricTable($data['memory']['results'], $data['memory']['unit'], 'min', 'Память'), 'LEAKS' => "### Утечки памяти\n\n_" . $data['leaks']['operation'] . "._\n\n" . $metricTable($data['leaks']['results'], $data['leaks']['unit'], 'min', 'Рост'), 'QUALITY' => "### Качество кода\n\n" . $qualityTable($data['quality']), 'FOOTNOTES' => implode("\n\n", array_map(static fn (string $note): string => '> ' . $note, $data['footnotes'])) . "\n\n> Замер выполнен " . substr((string) $data['generated_at'], 0, 10) . ' на PHP ' . $data['php_version'] . '.', ]; $targets = array_merge( [$root . '/README.md'], glob($root . '/docs/*/README.md') ?: [], glob($root . '/wiki/docs/*/Comparison.md') ?: [], ); $updated = 0; foreach ($targets as $file) { if (!is_file($file)) { continue; } $content = (string) file_get_contents($file); $before = $content; foreach ($blocks as $name => $body) { $pattern = sprintf('/<!-- %s:START -->.*?<!-- %s:END -->/s', $name, $name); $replacement = sprintf("<!-- %s:START -->\n%s\n<!-- %s:END -->", $name, $body, $name); $content = preg_replace($pattern, addcslashes($replacement, '\\$'), $content) ?? $content; } if ($content !== $before) { file_put_contents($file, $content); $updated++; } } printf("Таблицы обновлены в %d файлах.\n", $updated);