From 29e2644b75f0de2847cefa8318d5d61418d3e272 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Fri, 31 May 2024 11:13:48 -0400 Subject: [PATCH 1/4] Add commands to compare awards, scoreboard and results --- webapp/src/Command/AbstractCompareCommand.php | 129 +++++++++++++++ webapp/src/Command/CompareAwardsCommand.php | 82 ++++++++++ webapp/src/Command/CompareResultsCommand.php | 119 ++++++++++++++ .../src/Command/CompareScoreboardCommand.php | 153 ++++++++++++++++++ webapp/src/DataTransferObject/Award.php | 2 +- webapp/src/DataTransferObject/Result.php | 16 ++ .../DataTransferObject/Scoreboard/Problem.php | 2 +- 7 files changed, 501 insertions(+), 2 deletions(-) create mode 100644 webapp/src/Command/AbstractCompareCommand.php create mode 100644 webapp/src/Command/CompareAwardsCommand.php create mode 100644 webapp/src/Command/CompareResultsCommand.php create mode 100644 webapp/src/Command/CompareScoreboardCommand.php create mode 100644 webapp/src/DataTransferObject/Result.php diff --git a/webapp/src/Command/AbstractCompareCommand.php b/webapp/src/Command/AbstractCompareCommand.php new file mode 100644 index 0000000000..e4654e6b84 --- /dev/null +++ b/webapp/src/Command/AbstractCompareCommand.php @@ -0,0 +1,129 @@ +addArgument('file1', InputArgument::REQUIRED, 'First file to compare') + ->addArgument('file2', InputArgument::REQUIRED, 'Second file to compare'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $style = new SymfonyStyle($input, $output); + $messages = []; + $success = true; + if (!file_exists($input->getArgument('file1'))) { + $this->addMessage($messages, $success, 'error', sprintf('File "%s" does not exist', $input->getArgument('file1'))); + } + if (!file_exists($input->getArgument('file2'))) { + $this->addMessage($messages, $success, 'error', sprintf('File "%s" does not exist', $input->getArgument('file2'))); + } + if (!$success) { + return $this->displayMessages($style, $messages); + } + + $this->compare($messages, $success, $input->getArgument('file1'), $input->getArgument('file2')); + + return $this->displayMessages($style, $messages) ?? Command::SUCCESS; + } + + /** + * @param array $messages + */ + abstract protected function compare( + array &$messages, + bool &$success, + string $file1, + string $file2, + ): void; + + /** + * @param array $messages + */ + protected function addMessage( + array &$messages, + bool &$success, + string $type, + string $message, + ?string $source = null, + ?string $target = null, + ): void { + $messages[] = [ + 'type' => $type, + 'message' => $message, + 'source' => $source, + 'target' => $target, + ]; + if ($type === 'error') { + $success = false; + } + } + + /** + * @param array $messages + */ + protected function displayMessages(SymfonyStyle $style, array $messages): ?int + { + if (empty($messages)) { + $style->success('Files match fully'); + return null; + } + + $headers = ['Level', 'Message', 'Source', 'Target']; + $rows = []; + $counts = []; + foreach ($messages as $message) { + if (!isset($counts[$message['type']])) { + $counts[$message['type']] = 0; + } + $counts[$message['type']]++; + $rows[] = [ + $this->formatMessage($message['type'], $message['type']), + $this->formatMessage($message['type'], $message['message']), + $this->formatMessage($message['type'], $message['source'] ?? ''), + $this->formatMessage($message['type'], $message['target'] ?? ''), + ]; + } + $style->table($headers, $rows); + + $style->newLine(); + foreach ($counts as $type => $count) { + $style->writeln($this->formatMessage($type, sprintf('Found %d %s(s)', $count, $type))); + } + + if (isset($counts['error'])) { + $style->error('Files have potential critical differences'); + return Command::FAILURE; + } + + $style->success('Files have differences but probably non critical'); + + return null; + } + + protected function formatMessage(string $level, string $message): string + { + $colors = [ + 'error' => 'red', + 'warning' => 'yellow', + 'info' => 'green', + ]; + return sprintf('%s', $colors[$level], $message); + } +} diff --git a/webapp/src/Command/CompareAwardsCommand.php b/webapp/src/Command/CompareAwardsCommand.php new file mode 100644 index 0000000000..6f23712947 --- /dev/null +++ b/webapp/src/Command/CompareAwardsCommand.php @@ -0,0 +1,82 @@ +serializer->deserialize(file_get_contents($file1), Award::class . '[]', 'json'); + } catch (ExceptionInterface $e) { + $this->addMessage($messages, $success, 'error', sprintf('Error deserializing file "%s": %s', $file1, $e->getMessage())); + } + try { + /** @var Award[] $awards2 */ + $awards2 = $this->serializer->deserialize(file_get_contents($file2), Award::class . '[]', 'json'); + } catch (ExceptionInterface $e) { + $this->addMessage($messages, $success, 'error', sprintf('Error deserializing file "%s": %s', $file2, $e->getMessage())); + } + + if (!$success || !isset($awards1) || !isset($awards2)) { + return; + } + + /** @var array $awards1Indexed */ + $awards1Indexed = []; + foreach ($awards1 as $award) { + $awards1Indexed[$award->id] = $award; + } + + /** @var array $awards2Indexed */ + $awards2Indexed = []; + foreach ($awards2 as $award) { + $awards2Indexed[$award->id] = $award; + } + + foreach ($awards1Indexed as $awardId => $award) { + if (!isset($awards2Indexed[$awardId])) { + if (!$award->teamIds) { + $this->addMessage($messages, $success, 'info', sprintf('Award "%s" not found in second file, but has no team ID\'s in first file', $awardId)); + } else { + $this->addMessage($messages, $success, 'error', sprintf('Award "%s" not found in second file', $awardId)); + } + } else { + $award2 = $awards2Indexed[$awardId]; + if ($award->citation !== $award2->citation) { + $this->addMessage($messages, $success, 'warning', sprintf('Award "%s" has different citation', $awardId), $award->citation, $award2->citation); + } + $award1TeamIds = $award->teamIds; + sort($award1TeamIds); + $award2TeamIds = $award2->teamIds; + sort($award2TeamIds); + if ($award1TeamIds !== $award2TeamIds) { + $this->addMessage($messages, $success, 'error', sprintf('Award "%s" has different team ID\'s', $awardId), implode(', ', $award->teamIds), implode(', ', $award2->teamIds)); + } + } + } + + foreach ($awards2Indexed as $awardId => $award) { + if (!isset($awards1Indexed[$awardId])) { + if (!$award->teamIds) { + $this->addMessage($messages, $success, 'info', sprintf('Award "%s" not found in first file, but has no team ID\'s in second file', $awardId)); + } else { + $this->addMessage($messages, $success, 'error', sprintf('Award "%s" not found in first file', $awardId)); + } + } + } + } +} diff --git a/webapp/src/Command/CompareResultsCommand.php b/webapp/src/Command/CompareResultsCommand.php new file mode 100644 index 0000000000..0b29a3b2a0 --- /dev/null +++ b/webapp/src/Command/CompareResultsCommand.php @@ -0,0 +1,119 @@ +addMessage($messages, $success, 'error', sprintf("File \"%s\" does not start with \"results\t1\"", $file1)); + } + $results2Contents = file_get_contents($file2); + if (!str_starts_with($results2Contents, "results\t1")) { + $this->addMessage($messages, $success, 'error', sprintf("File \"%s\" does not start with \"results\t1\"", $file2)); + } + + if (!$success) { + return; + } + + $results1Contents = substr($results1Contents, strpos($results1Contents, "\n") + 1); + $results2Contents = substr($results2Contents, strpos($results2Contents, "\n") + 1); + + // Prefix both files with a fake header, so we can deserialize them + $results1Contents = "team_id\trank\taward\tnum_solved\ttotal_time\tlast_time\tgroup_winner\n" . $results1Contents; + $results2Contents = "team_id\trank\taward\tnum_solved\ttotal_time\tlast_time\tgroup_winner\n" . $results2Contents; + + try { + /** @var Result[] $results1 */ + $results1 = $this->serializer->deserialize($results1Contents, Result::class . '[]', 'csv', [ + CsvEncoder::DELIMITER_KEY => "\t", + ]); + } catch (ExceptionInterface $e) { + $this->addMessage($messages, $success, 'error', sprintf('Error deserializing file "%s": %s', $file1, $e->getMessage())); + } + + try { + /** @var Result[] $results2 */ + $results2 = $this->serializer->deserialize($results2Contents, Result::class . '[]', 'csv', [ + CsvEncoder::DELIMITER_KEY => "\t", + ]); + } catch (ExceptionInterface $e) { + $this->addMessage($messages, $success, 'error', sprintf('Error deserializing file "%s": %s', $file2, $e->getMessage())); + } + + if (!$success || !isset($results1) || !isset($results2)) { + return; + } + + // Sort results for both files: first by num_solved, then by total_time + usort($results1, fn( + Result $a, + Result $b + ) => $a->numSolved === $b->numSolved ? $a->totalTime <=> $b->totalTime : $b->numSolved <=> $a->numSolved); + usort($results2, fn( + Result $a, + Result $b + ) => $a->numSolved === $b->numSolved ? $a->totalTime <=> $b->totalTime : $b->numSolved <=> $a->numSolved); + + /** @var array $results1Indexed */ + $results1Indexed = []; + foreach ($results1 as $result) { + $results1Indexed[$result->teamId] = $result; + } + + /** @var array $results2Indexed */ + $results2Indexed = []; + foreach ($results2 as $result) { + $results2Indexed[$result->teamId] = $result; + } + + foreach ($results1 as $result) { + if (!isset($results2Indexed[$result->teamId])) { + $this->addMessage($messages, $success, 'error', sprintf('Team "%s" not found in second file', $result->teamId)); + } else { + $result2 = $results2Indexed[$result->teamId]; + if ($result->rank !== $result2->rank) { + $this->addMessage($messages, $success, 'error', sprintf('Team %s has different rank', $result->teamId), (string)$result->rank, (string)$result2->rank); + } + if ($result->award !== $result2->award) { + $this->addMessage($messages, $success, 'error', sprintf('Team %s has different award', $result->teamId), $result->award, $result2->award); + } + if ($result->numSolved !== $result2->numSolved) { + $this->addMessage($messages, $success, 'error', sprintf('Team %s has different num_solved', $result->teamId), (string)$result->numSolved, (string)$result2->numSolved); + } + if ($result->totalTime !== $result2->totalTime) { + $this->addMessage($messages, $success, 'error', sprintf('Team %s has different total_time', $result->teamId), (string)$result->totalTime, (string)$result2->totalTime); + } + if ($result->lastTime !== $result2->lastTime) { + $this->addMessage($messages, $success, 'error', sprintf('Team %s has different last_time', $result->teamId), (string)$result->lastTime, (string)$result2->lastTime); + } + if ($result->groupWinner !== $result2->groupWinner) { + $this->addMessage($messages, $success, 'warning', sprintf('Team %s has different group_winner', $result->teamId), (string)$result->groupWinner, (string)$result2->groupWinner); + } + } + } + + foreach ($results2 as $result) { + if (!isset($results1Indexed[$result->teamId])) { + $this->addMessage($messages, $success, 'error', sprintf('Team %s not found in first file', $result->teamId)); + } + } + } +} diff --git a/webapp/src/Command/CompareScoreboardCommand.php b/webapp/src/Command/CompareScoreboardCommand.php new file mode 100644 index 0000000000..6d5478911c --- /dev/null +++ b/webapp/src/Command/CompareScoreboardCommand.php @@ -0,0 +1,153 @@ +serializer->deserialize(file_get_contents($file1), Scoreboard::class, 'json', ['disable_type_enforcement' => true]); + } catch (ExceptionInterface $e) { + $this->addMessage($messages, $success, 'error', sprintf('Error deserializing file "%s": %s', $file1, $e->getMessage())); + } + try { + /** @var Scoreboard $scoreboard2 */ + $scoreboard2 = $this->serializer->deserialize(file_get_contents($file2), Scoreboard::class, 'json', ['disable_type_enforcement' => true]); + } catch (ExceptionInterface $e) { + $this->addMessage($messages, $success, 'error', sprintf('Error deserializing file "%s": %s', $file2, $e->getMessage())); + } + + if (!$success || !isset($scoreboard1) || !isset($scoreboard2)) { + return; + } + + if ($scoreboard1->eventId !== $scoreboard2->eventId) { + $this->addMessage($messages, $success, 'info', 'Event ID does not match', $scoreboard1->eventId, $scoreboard2->eventId); + } + + if ($scoreboard1->time !== $scoreboard2->time) { + $this->addMessage($messages, $success, 'info', 'Time does not match', $scoreboard1->time, $scoreboard2->time); + } + + if ($scoreboard1->contestTime !== $scoreboard2->contestTime) { + $this->addMessage($messages, $success, 'info', 'Contest time does not match', $scoreboard1->contestTime, $scoreboard2->contestTime); + } + + if (($scoreboard1->state->started ?? '') !== ($scoreboard2->state->started ?? '')) { + $this->addMessage($messages, $success, 'warning', 'State started does not match', $scoreboard1->state->started, $scoreboard2->state->started); + } + + if (($scoreboard1->state->ended ?? '') !== ($scoreboard2->state->ended ?? '')) { + $this->addMessage($messages, $success, 'warning', 'State ended does not match', $scoreboard1->state->ended, $scoreboard2->state->ended); + } + + if (($scoreboard1->state->frozen ?? '') !== ($scoreboard2->state->frozen ?? '')) { + $this->addMessage($messages, $success, 'warning', 'State frozen does not match', $scoreboard1->state->frozen, $scoreboard2->state->frozen); + } + + if (($scoreboard1->state->thawed ?? '') !== ($scoreboard2->state->thawed ?? '')) { + $this->addMessage($messages, $success, 'warning', 'State thawed does not match', $scoreboard1->state->thawed, $scoreboard2->state->thawed); + } + + if (($scoreboard1->state->finalized ?? '') !== ($scoreboard2->state->finalized ?? '')) { + $this->addMessage($messages, $success, 'warning', 'State finalized does not match', $scoreboard1->state->finalized, $scoreboard2->state->finalized); + } + + if (($scoreboard1->state->endOfUpdates ?? '') !== ($scoreboard2->state->endOfUpdates ?? '')) { + $this->addMessage($messages, $success, 'warning', 'State end of updates does not match', $scoreboard1->state->endOfUpdates, $scoreboard2->state->endOfUpdates); + } + + if (count($scoreboard1->rows) !== count($scoreboard2->rows)) { + $this->addMessage($messages, $success, 'error', 'Number of rows does not match', (string)count($scoreboard1->rows), (string)count($scoreboard2->rows)); + } + + foreach ($scoreboard1->rows as $index => $row) { + if ($row->teamId !== $scoreboard2->rows[$index]->teamId) { + $this->addMessage($messages, $success, 'error', sprintf('Row %d: Team ID does not match', $index), $row->teamId, $scoreboard2->rows[$index]->teamId); + } + + if ($row->rank !== $scoreboard2->rows[$index]->rank) { + $this->addMessage($messages, $success, 'error', sprintf('Row %d: Rank does not match', $index), (string)$row->rank, (string)$scoreboard2->rows[$index]->rank); + } + + if ($row->score->numSolved !== $scoreboard2->rows[$index]->score->numSolved) { + $this->addMessage($messages, $success, 'error', sprintf('Row %d: Num solved does not match', $index), (string)$row->score->numSolved, (string)$scoreboard2->rows[$index]->score->numSolved); + } + + if ($row->score->totalTime !== $scoreboard2->rows[$index]->score->totalTime) { + $this->addMessage($messages, $success, 'error', sprintf('Row %d: Total time does not match', $index), (string)$row->score->totalTime, (string)$scoreboard2->rows[$index]->score->totalTime); + } + + // Problem messages are mostly info for now, since PC^2 doesn't expose time info + foreach ($row->problems as $problem) { + /** @var Problem|null $problemForSecond */ + $problemForSecond = null; + + foreach ($scoreboard2->rows[$index]->problems as $problem2) { + // PC^2 uses different problem ID's. For now also match on `Id = {problemId}-{digits}` + if ($problem->problemId === $problem2->problemId) { + $problemForSecond = $problem2; + break; + } elseif (preg_match('/^Id = ' . preg_quote($problem->problemId, '/') . '-+\d+$/', $problem2->problemId)) { + $problemForSecond = $problem2; + break; + } + } + + if ($problemForSecond === null && $problem->solved) { + $this->addMessage($messages, $success, 'error', sprintf('Row %d: Problem %s solved in first file, but not in second file', $index, $problem->problemId)); + } elseif ($problemForSecond !== null && $problem->solved !== $problemForSecond->solved) { + $this->addMessage($messages, $success, 'error', sprintf('Row %d: Problem %s solved does not match', $index, $problem->problemId), (string)$problem->solved, (string)$problemForSecond->solved); + } + + if ($problemForSecond) { + if ($problem->numJudged !== $problemForSecond->numJudged) { + $this->addMessage($messages, $success, 'error', sprintf('Row %d: Problem %s num judged does not match', $index, $problem->problemId), (string)$problem->numJudged, (string)$problemForSecond->numJudged); + } + + if ($problem->numPending !== $problemForSecond->numPending) { + $this->addMessage($messages, $success, 'error', sprintf('Row %d: Problem %s num pending does not match', $index, $problem->problemId), (string)$problem->numPending, (string)$problemForSecond->numPending); + } + + if ($problem->time !== $problemForSecond->time) { + $this->addMessage($messages, $success, 'info', sprintf('Row %d: Problem %s time does not match', $index, $problem->problemId), (string)$problem->time, (string)$problemForSecond->time); + } + } + } + + foreach ($scoreboard2->rows[$index]->problems as $problem2) { + $problemForFirst = null; + + foreach ($row->problems as $problem) { + // PC^2 uses different problem ID's. For now also match on `Id = {problemId}-{digits}` + if ($problem->problemId === $problem2->problemId) { + $problemForFirst = $problem; + break; + } elseif (preg_match('/^Id = ' . preg_quote($problem->problemId, '/') . '-+\d+$/', $problem2->problemId)) { + $problemForFirst = $problem; + break; + } + } + + if ($problemForFirst === null && $problem2->solved) { + $this->addMessage($messages, $success, 'error', sprintf('Row %d: Problem %s solved in second file, but not in first file', $index, $problem2->problemId)); + } + } + } + } +} diff --git a/webapp/src/DataTransferObject/Award.php b/webapp/src/DataTransferObject/Award.php index eb5f2ade8d..fa3fc8faeb 100644 --- a/webapp/src/DataTransferObject/Award.php +++ b/webapp/src/DataTransferObject/Award.php @@ -11,7 +11,7 @@ class Award */ public function __construct( public readonly string $id, - public readonly string $citation, + public readonly ?string $citation, #[Serializer\Type('array')] public readonly array $teamIds, ) {} diff --git a/webapp/src/DataTransferObject/Result.php b/webapp/src/DataTransferObject/Result.php new file mode 100644 index 0000000000..580b5eb853 --- /dev/null +++ b/webapp/src/DataTransferObject/Result.php @@ -0,0 +1,16 @@ + Date: Fri, 14 Jun 2024 15:27:12 +0200 Subject: [PATCH 2/4] Move comparing to a service so we can reuse it (and test it) --- webapp/src/Command/AbstractCompareCommand.php | 89 ++++------- webapp/src/Command/CompareAwardsCommand.php | 77 ++------- webapp/src/Command/CompareResultsCommand.php | 114 ++------------ .../src/Command/CompareScoreboardCommand.php | 148 ++---------------- .../Compare/AbstractCompareService.php | 76 +++++++++ .../Service/Compare/AwardCompareService.php | 61 ++++++++ webapp/src/Service/Compare/Message.php | 13 ++ webapp/src/Service/Compare/MessageType.php | 10 ++ .../Service/Compare/ResultsCompareService.php | 85 ++++++++++ .../Compare/ScoreboardCompareService.php | 134 ++++++++++++++++ 10 files changed, 437 insertions(+), 370 deletions(-) create mode 100644 webapp/src/Service/Compare/AbstractCompareService.php create mode 100644 webapp/src/Service/Compare/AwardCompareService.php create mode 100644 webapp/src/Service/Compare/Message.php create mode 100644 webapp/src/Service/Compare/MessageType.php create mode 100644 webapp/src/Service/Compare/ResultsCompareService.php create mode 100644 webapp/src/Service/Compare/ScoreboardCompareService.php diff --git a/webapp/src/Command/AbstractCompareCommand.php b/webapp/src/Command/AbstractCompareCommand.php index e4654e6b84..99c0d2cfc4 100644 --- a/webapp/src/Command/AbstractCompareCommand.php +++ b/webapp/src/Command/AbstractCompareCommand.php @@ -2,6 +2,9 @@ namespace App\Command; +use App\Service\Compare\AbstractCompareService; +use App\Service\Compare\Message; +use App\Service\Compare\MessageType; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -9,10 +12,18 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Serializer\SerializerInterface; +/** + * @template T + */ abstract class AbstractCompareCommand extends Command { - public function __construct(protected readonly SerializerInterface $serializer) - { + /** + * @param AbstractCompareService $compareService + */ + public function __construct( + protected readonly SerializerInterface $serializer, + protected AbstractCompareService $compareService + ) { parent::__construct(); } @@ -26,57 +37,13 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $style = new SymfonyStyle($input, $output); - $messages = []; - $success = true; - if (!file_exists($input->getArgument('file1'))) { - $this->addMessage($messages, $success, 'error', sprintf('File "%s" does not exist', $input->getArgument('file1'))); - } - if (!file_exists($input->getArgument('file2'))) { - $this->addMessage($messages, $success, 'error', sprintf('File "%s" does not exist', $input->getArgument('file2'))); - } - if (!$success) { - return $this->displayMessages($style, $messages); - } - - $this->compare($messages, $success, $input->getArgument('file1'), $input->getArgument('file2')); + $messages = $this->compareService->compareFiles($input->getArgument('file1'), $input->getArgument('file2')); return $this->displayMessages($style, $messages) ?? Command::SUCCESS; } /** - * @param array $messages - */ - abstract protected function compare( - array &$messages, - bool &$success, - string $file1, - string $file2, - ): void; - - /** - * @param array $messages - */ - protected function addMessage( - array &$messages, - bool &$success, - string $type, - string $message, - ?string $source = null, - ?string $target = null, - ): void { - $messages[] = [ - 'type' => $type, - 'message' => $message, - 'source' => $source, - 'target' => $target, - ]; - if ($type === 'error') { - $success = false; - } - } - - /** - * @param array $messages + * @param Message[] $messages */ protected function displayMessages(SymfonyStyle $style, array $messages): ?int { @@ -89,22 +56,22 @@ protected function displayMessages(SymfonyStyle $style, array $messages): ?int $rows = []; $counts = []; foreach ($messages as $message) { - if (!isset($counts[$message['type']])) { - $counts[$message['type']] = 0; + if (!isset($counts[$message->type->value])) { + $counts[$message->type->value] = 0; } - $counts[$message['type']]++; + $counts[$message->type->value]++; $rows[] = [ - $this->formatMessage($message['type'], $message['type']), - $this->formatMessage($message['type'], $message['message']), - $this->formatMessage($message['type'], $message['source'] ?? ''), - $this->formatMessage($message['type'], $message['target'] ?? ''), + $this->formatMessage($message->type, $message->type->value), + $this->formatMessage($message->type, $message->message), + $this->formatMessage($message->type, $message->source ?? ''), + $this->formatMessage($message->type, $message->target ?? ''), ]; } $style->table($headers, $rows); $style->newLine(); foreach ($counts as $type => $count) { - $style->writeln($this->formatMessage($type, sprintf('Found %d %s(s)', $count, $type))); + $style->writeln($this->formatMessage(MessageType::from($type), sprintf('Found %d %s(s)', $count, $type))); } if (isset($counts['error'])) { @@ -117,13 +84,13 @@ protected function displayMessages(SymfonyStyle $style, array $messages): ?int return null; } - protected function formatMessage(string $level, string $message): string + protected function formatMessage(MessageType $level, string $message): string { $colors = [ - 'error' => 'red', - 'warning' => 'yellow', - 'info' => 'green', + MessageType::ERROR->value => 'red', + MessageType::WARNING->value => 'yellow', + MessageType::INFO->value => 'green', ]; - return sprintf('%s', $colors[$level], $message); + return sprintf('%s', $colors[$level->value], $message); } } diff --git a/webapp/src/Command/CompareAwardsCommand.php b/webapp/src/Command/CompareAwardsCommand.php index 6f23712947..757c73f29c 100644 --- a/webapp/src/Command/CompareAwardsCommand.php +++ b/webapp/src/Command/CompareAwardsCommand.php @@ -3,80 +3,23 @@ namespace App\Command; use App\DataTransferObject\Award; +use App\Service\Compare\AwardCompareService; use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Serializer\Exception\ExceptionInterface; +use Symfony\Component\Serializer\SerializerInterface; +/** + * @extends AbstractCompareCommand + */ #[AsCommand( name: 'compare:awards', description: 'Compare awards between two files' )] class CompareAwardsCommand extends AbstractCompareCommand { - protected function compare( - array &$messages, - bool &$success, - string $file1, - string $file2, - ): void { - try { - /** @var Award[] $awards1 */ - $awards1 = $this->serializer->deserialize(file_get_contents($file1), Award::class . '[]', 'json'); - } catch (ExceptionInterface $e) { - $this->addMessage($messages, $success, 'error', sprintf('Error deserializing file "%s": %s', $file1, $e->getMessage())); - } - try { - /** @var Award[] $awards2 */ - $awards2 = $this->serializer->deserialize(file_get_contents($file2), Award::class . '[]', 'json'); - } catch (ExceptionInterface $e) { - $this->addMessage($messages, $success, 'error', sprintf('Error deserializing file "%s": %s', $file2, $e->getMessage())); - } - - if (!$success || !isset($awards1) || !isset($awards2)) { - return; - } - - /** @var array $awards1Indexed */ - $awards1Indexed = []; - foreach ($awards1 as $award) { - $awards1Indexed[$award->id] = $award; - } - - /** @var array $awards2Indexed */ - $awards2Indexed = []; - foreach ($awards2 as $award) { - $awards2Indexed[$award->id] = $award; - } - - foreach ($awards1Indexed as $awardId => $award) { - if (!isset($awards2Indexed[$awardId])) { - if (!$award->teamIds) { - $this->addMessage($messages, $success, 'info', sprintf('Award "%s" not found in second file, but has no team ID\'s in first file', $awardId)); - } else { - $this->addMessage($messages, $success, 'error', sprintf('Award "%s" not found in second file', $awardId)); - } - } else { - $award2 = $awards2Indexed[$awardId]; - if ($award->citation !== $award2->citation) { - $this->addMessage($messages, $success, 'warning', sprintf('Award "%s" has different citation', $awardId), $award->citation, $award2->citation); - } - $award1TeamIds = $award->teamIds; - sort($award1TeamIds); - $award2TeamIds = $award2->teamIds; - sort($award2TeamIds); - if ($award1TeamIds !== $award2TeamIds) { - $this->addMessage($messages, $success, 'error', sprintf('Award "%s" has different team ID\'s', $awardId), implode(', ', $award->teamIds), implode(', ', $award2->teamIds)); - } - } - } - - foreach ($awards2Indexed as $awardId => $award) { - if (!isset($awards1Indexed[$awardId])) { - if (!$award->teamIds) { - $this->addMessage($messages, $success, 'info', sprintf('Award "%s" not found in first file, but has no team ID\'s in second file', $awardId)); - } else { - $this->addMessage($messages, $success, 'error', sprintf('Award "%s" not found in first file', $awardId)); - } - } - } + public function __construct( + SerializerInterface $serializer, + AwardCompareService $compareService + ) { + parent::__construct($serializer, $compareService); } } diff --git a/webapp/src/Command/CompareResultsCommand.php b/webapp/src/Command/CompareResultsCommand.php index 0b29a3b2a0..664c6ea86c 100644 --- a/webapp/src/Command/CompareResultsCommand.php +++ b/webapp/src/Command/CompareResultsCommand.php @@ -3,117 +3,23 @@ namespace App\Command; use App\DataTransferObject\Result; +use App\Service\Compare\ResultsCompareService; use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Serializer\Encoder\CsvEncoder; -use Symfony\Component\Serializer\Exception\ExceptionInterface; +use Symfony\Component\Serializer\SerializerInterface; +/** + * @extends AbstractCompareCommand + */ #[AsCommand( name: 'compare:results', description: 'Compare results between two files' )] class CompareResultsCommand extends AbstractCompareCommand { - - protected function compare( - array &$messages, - bool &$success, - string $file1, - string $file2, - ): void { - $results1Contents = file_get_contents($file1); - if (!str_starts_with($results1Contents, "results\t1")) { - $this->addMessage($messages, $success, 'error', sprintf("File \"%s\" does not start with \"results\t1\"", $file1)); - } - $results2Contents = file_get_contents($file2); - if (!str_starts_with($results2Contents, "results\t1")) { - $this->addMessage($messages, $success, 'error', sprintf("File \"%s\" does not start with \"results\t1\"", $file2)); - } - - if (!$success) { - return; - } - - $results1Contents = substr($results1Contents, strpos($results1Contents, "\n") + 1); - $results2Contents = substr($results2Contents, strpos($results2Contents, "\n") + 1); - - // Prefix both files with a fake header, so we can deserialize them - $results1Contents = "team_id\trank\taward\tnum_solved\ttotal_time\tlast_time\tgroup_winner\n" . $results1Contents; - $results2Contents = "team_id\trank\taward\tnum_solved\ttotal_time\tlast_time\tgroup_winner\n" . $results2Contents; - - try { - /** @var Result[] $results1 */ - $results1 = $this->serializer->deserialize($results1Contents, Result::class . '[]', 'csv', [ - CsvEncoder::DELIMITER_KEY => "\t", - ]); - } catch (ExceptionInterface $e) { - $this->addMessage($messages, $success, 'error', sprintf('Error deserializing file "%s": %s', $file1, $e->getMessage())); - } - - try { - /** @var Result[] $results2 */ - $results2 = $this->serializer->deserialize($results2Contents, Result::class . '[]', 'csv', [ - CsvEncoder::DELIMITER_KEY => "\t", - ]); - } catch (ExceptionInterface $e) { - $this->addMessage($messages, $success, 'error', sprintf('Error deserializing file "%s": %s', $file2, $e->getMessage())); - } - - if (!$success || !isset($results1) || !isset($results2)) { - return; - } - - // Sort results for both files: first by num_solved, then by total_time - usort($results1, fn( - Result $a, - Result $b - ) => $a->numSolved === $b->numSolved ? $a->totalTime <=> $b->totalTime : $b->numSolved <=> $a->numSolved); - usort($results2, fn( - Result $a, - Result $b - ) => $a->numSolved === $b->numSolved ? $a->totalTime <=> $b->totalTime : $b->numSolved <=> $a->numSolved); - - /** @var array $results1Indexed */ - $results1Indexed = []; - foreach ($results1 as $result) { - $results1Indexed[$result->teamId] = $result; - } - - /** @var array $results2Indexed */ - $results2Indexed = []; - foreach ($results2 as $result) { - $results2Indexed[$result->teamId] = $result; - } - - foreach ($results1 as $result) { - if (!isset($results2Indexed[$result->teamId])) { - $this->addMessage($messages, $success, 'error', sprintf('Team "%s" not found in second file', $result->teamId)); - } else { - $result2 = $results2Indexed[$result->teamId]; - if ($result->rank !== $result2->rank) { - $this->addMessage($messages, $success, 'error', sprintf('Team %s has different rank', $result->teamId), (string)$result->rank, (string)$result2->rank); - } - if ($result->award !== $result2->award) { - $this->addMessage($messages, $success, 'error', sprintf('Team %s has different award', $result->teamId), $result->award, $result2->award); - } - if ($result->numSolved !== $result2->numSolved) { - $this->addMessage($messages, $success, 'error', sprintf('Team %s has different num_solved', $result->teamId), (string)$result->numSolved, (string)$result2->numSolved); - } - if ($result->totalTime !== $result2->totalTime) { - $this->addMessage($messages, $success, 'error', sprintf('Team %s has different total_time', $result->teamId), (string)$result->totalTime, (string)$result2->totalTime); - } - if ($result->lastTime !== $result2->lastTime) { - $this->addMessage($messages, $success, 'error', sprintf('Team %s has different last_time', $result->teamId), (string)$result->lastTime, (string)$result2->lastTime); - } - if ($result->groupWinner !== $result2->groupWinner) { - $this->addMessage($messages, $success, 'warning', sprintf('Team %s has different group_winner', $result->teamId), (string)$result->groupWinner, (string)$result2->groupWinner); - } - } - } - - foreach ($results2 as $result) { - if (!isset($results1Indexed[$result->teamId])) { - $this->addMessage($messages, $success, 'error', sprintf('Team %s not found in first file', $result->teamId)); - } - } + public function __construct( + SerializerInterface $serializer, + ResultsCompareService $compareService + ) { + parent::__construct($serializer, $compareService); } } diff --git a/webapp/src/Command/CompareScoreboardCommand.php b/webapp/src/Command/CompareScoreboardCommand.php index 6d5478911c..c4eb4b8c03 100644 --- a/webapp/src/Command/CompareScoreboardCommand.php +++ b/webapp/src/Command/CompareScoreboardCommand.php @@ -2,152 +2,24 @@ namespace App\Command; -use App\DataTransferObject\Scoreboard\Problem; use App\DataTransferObject\Scoreboard\Scoreboard; +use App\Service\Compare\ScoreboardCompareService; use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Serializer\Exception\ExceptionInterface; +use Symfony\Component\Serializer\SerializerInterface; +/** + * @extends AbstractCompareCommand + */ #[AsCommand( name: 'compare:scoreboard', description: 'Compare scoreboard between two files' )] class CompareScoreboardCommand extends AbstractCompareCommand { - protected function compare( - array &$messages, - bool &$success, - string $file1, - string $file2, - ): void { - try { - /** @var Scoreboard $scoreboard1 */ - $scoreboard1 = $this->serializer->deserialize(file_get_contents($file1), Scoreboard::class, 'json', ['disable_type_enforcement' => true]); - } catch (ExceptionInterface $e) { - $this->addMessage($messages, $success, 'error', sprintf('Error deserializing file "%s": %s', $file1, $e->getMessage())); - } - try { - /** @var Scoreboard $scoreboard2 */ - $scoreboard2 = $this->serializer->deserialize(file_get_contents($file2), Scoreboard::class, 'json', ['disable_type_enforcement' => true]); - } catch (ExceptionInterface $e) { - $this->addMessage($messages, $success, 'error', sprintf('Error deserializing file "%s": %s', $file2, $e->getMessage())); - } - - if (!$success || !isset($scoreboard1) || !isset($scoreboard2)) { - return; - } - - if ($scoreboard1->eventId !== $scoreboard2->eventId) { - $this->addMessage($messages, $success, 'info', 'Event ID does not match', $scoreboard1->eventId, $scoreboard2->eventId); - } - - if ($scoreboard1->time !== $scoreboard2->time) { - $this->addMessage($messages, $success, 'info', 'Time does not match', $scoreboard1->time, $scoreboard2->time); - } - - if ($scoreboard1->contestTime !== $scoreboard2->contestTime) { - $this->addMessage($messages, $success, 'info', 'Contest time does not match', $scoreboard1->contestTime, $scoreboard2->contestTime); - } - - if (($scoreboard1->state->started ?? '') !== ($scoreboard2->state->started ?? '')) { - $this->addMessage($messages, $success, 'warning', 'State started does not match', $scoreboard1->state->started, $scoreboard2->state->started); - } - - if (($scoreboard1->state->ended ?? '') !== ($scoreboard2->state->ended ?? '')) { - $this->addMessage($messages, $success, 'warning', 'State ended does not match', $scoreboard1->state->ended, $scoreboard2->state->ended); - } - - if (($scoreboard1->state->frozen ?? '') !== ($scoreboard2->state->frozen ?? '')) { - $this->addMessage($messages, $success, 'warning', 'State frozen does not match', $scoreboard1->state->frozen, $scoreboard2->state->frozen); - } - - if (($scoreboard1->state->thawed ?? '') !== ($scoreboard2->state->thawed ?? '')) { - $this->addMessage($messages, $success, 'warning', 'State thawed does not match', $scoreboard1->state->thawed, $scoreboard2->state->thawed); - } - - if (($scoreboard1->state->finalized ?? '') !== ($scoreboard2->state->finalized ?? '')) { - $this->addMessage($messages, $success, 'warning', 'State finalized does not match', $scoreboard1->state->finalized, $scoreboard2->state->finalized); - } - - if (($scoreboard1->state->endOfUpdates ?? '') !== ($scoreboard2->state->endOfUpdates ?? '')) { - $this->addMessage($messages, $success, 'warning', 'State end of updates does not match', $scoreboard1->state->endOfUpdates, $scoreboard2->state->endOfUpdates); - } - - if (count($scoreboard1->rows) !== count($scoreboard2->rows)) { - $this->addMessage($messages, $success, 'error', 'Number of rows does not match', (string)count($scoreboard1->rows), (string)count($scoreboard2->rows)); - } - - foreach ($scoreboard1->rows as $index => $row) { - if ($row->teamId !== $scoreboard2->rows[$index]->teamId) { - $this->addMessage($messages, $success, 'error', sprintf('Row %d: Team ID does not match', $index), $row->teamId, $scoreboard2->rows[$index]->teamId); - } - - if ($row->rank !== $scoreboard2->rows[$index]->rank) { - $this->addMessage($messages, $success, 'error', sprintf('Row %d: Rank does not match', $index), (string)$row->rank, (string)$scoreboard2->rows[$index]->rank); - } - - if ($row->score->numSolved !== $scoreboard2->rows[$index]->score->numSolved) { - $this->addMessage($messages, $success, 'error', sprintf('Row %d: Num solved does not match', $index), (string)$row->score->numSolved, (string)$scoreboard2->rows[$index]->score->numSolved); - } - - if ($row->score->totalTime !== $scoreboard2->rows[$index]->score->totalTime) { - $this->addMessage($messages, $success, 'error', sprintf('Row %d: Total time does not match', $index), (string)$row->score->totalTime, (string)$scoreboard2->rows[$index]->score->totalTime); - } - - // Problem messages are mostly info for now, since PC^2 doesn't expose time info - foreach ($row->problems as $problem) { - /** @var Problem|null $problemForSecond */ - $problemForSecond = null; - - foreach ($scoreboard2->rows[$index]->problems as $problem2) { - // PC^2 uses different problem ID's. For now also match on `Id = {problemId}-{digits}` - if ($problem->problemId === $problem2->problemId) { - $problemForSecond = $problem2; - break; - } elseif (preg_match('/^Id = ' . preg_quote($problem->problemId, '/') . '-+\d+$/', $problem2->problemId)) { - $problemForSecond = $problem2; - break; - } - } - - if ($problemForSecond === null && $problem->solved) { - $this->addMessage($messages, $success, 'error', sprintf('Row %d: Problem %s solved in first file, but not in second file', $index, $problem->problemId)); - } elseif ($problemForSecond !== null && $problem->solved !== $problemForSecond->solved) { - $this->addMessage($messages, $success, 'error', sprintf('Row %d: Problem %s solved does not match', $index, $problem->problemId), (string)$problem->solved, (string)$problemForSecond->solved); - } - - if ($problemForSecond) { - if ($problem->numJudged !== $problemForSecond->numJudged) { - $this->addMessage($messages, $success, 'error', sprintf('Row %d: Problem %s num judged does not match', $index, $problem->problemId), (string)$problem->numJudged, (string)$problemForSecond->numJudged); - } - - if ($problem->numPending !== $problemForSecond->numPending) { - $this->addMessage($messages, $success, 'error', sprintf('Row %d: Problem %s num pending does not match', $index, $problem->problemId), (string)$problem->numPending, (string)$problemForSecond->numPending); - } - - if ($problem->time !== $problemForSecond->time) { - $this->addMessage($messages, $success, 'info', sprintf('Row %d: Problem %s time does not match', $index, $problem->problemId), (string)$problem->time, (string)$problemForSecond->time); - } - } - } - - foreach ($scoreboard2->rows[$index]->problems as $problem2) { - $problemForFirst = null; - - foreach ($row->problems as $problem) { - // PC^2 uses different problem ID's. For now also match on `Id = {problemId}-{digits}` - if ($problem->problemId === $problem2->problemId) { - $problemForFirst = $problem; - break; - } elseif (preg_match('/^Id = ' . preg_quote($problem->problemId, '/') . '-+\d+$/', $problem2->problemId)) { - $problemForFirst = $problem; - break; - } - } - - if ($problemForFirst === null && $problem2->solved) { - $this->addMessage($messages, $success, 'error', sprintf('Row %d: Problem %s solved in second file, but not in first file', $index, $problem2->problemId)); - } - } - } + public function __construct( + SerializerInterface $serializer, + ScoreboardCompareService $compareService + ) { + parent::__construct($serializer, $compareService); } } diff --git a/webapp/src/Service/Compare/AbstractCompareService.php b/webapp/src/Service/Compare/AbstractCompareService.php new file mode 100644 index 0000000000..b16e72c74e --- /dev/null +++ b/webapp/src/Service/Compare/AbstractCompareService.php @@ -0,0 +1,76 @@ +addMessage(MessageType::ERROR, sprintf('File "%s" does not exist', $file1)); + $success = false; + } + if (!file_exists($file2)) { + $this->addMessage(MessageType::ERROR, sprintf('File "%s" does not exist', $file2)); + $success = false; + } + if (!$success) { + return $this->messages; + } + + try { + $object1 = $this->parseFile($file1); + } catch (ExceptionInterface $e) { + $this->addMessage(MessageType::ERROR, sprintf('Error deserializing file "%s": %s', $file1, $e->getMessage())); + } + try { + $object2 = $this->parseFile($file2); + } catch (ExceptionInterface $e) { + $this->addMessage(MessageType::ERROR, sprintf('Error deserializing file "%s": %s', $file2, $e->getMessage())); + } + + if (!isset($object1) || !isset($object2)) { + return $this->messages; + } + + $this->compare($object1, $object2); + + return $this->messages; + } + + /** + * @return T|null + * @throws ExceptionInterface + */ + abstract protected function parseFile(string $file); + + /** + * @param T $object1 + * @param T $object2 + */ + abstract public function compare($object1, $object2): void; + + protected function addMessage( + MessageType $type, + string $message, + ?string $source = null, + ?string $target = null, + ): void { + $this->messages[] = new Message($type, $message, $source, $target); + } +} diff --git a/webapp/src/Service/Compare/AwardCompareService.php b/webapp/src/Service/Compare/AwardCompareService.php new file mode 100644 index 0000000000..961de608c7 --- /dev/null +++ b/webapp/src/Service/Compare/AwardCompareService.php @@ -0,0 +1,61 @@ + + */ +class AwardCompareService extends AbstractCompareService +{ + protected function parseFile(string $file) + { + return $this->serializer->deserialize(file_get_contents($file), Award::class . '[]', 'json'); + } + + public function compare($object1, $object2): void + { + $awards1Indexed = []; + foreach ($object1 as $award) { + $awards1Indexed[$award->id] = $award; + } + + $awards2Indexed = []; + foreach ($object2 as $award) { + $awards2Indexed[$award->id] = $award; + } + + foreach ($awards1Indexed as $awardId => $award) { + if (!isset($awards2Indexed[$awardId])) { + if (!$award->teamIds) { + $this->addMessage(MessageType::INFO, sprintf('Award "%s" not found in second file, but has no team ID\'s in first file', $awardId)); + } else { + $this->addMessage(MessageType::ERROR, sprintf('Award "%s" not found in second file', $awardId)); + } + } else { + $award2 = $awards2Indexed[$awardId]; + if ($award->citation !== $award2->citation) { + $this->addMessage(MessageType::WARNING, sprintf('Award "%s" has different citation', $awardId), $award->citation, $award2->citation); + } + $award1TeamIds = $award->teamIds; + sort($award1TeamIds); + $award2TeamIds = $award2->teamIds; + sort($award2TeamIds); + if ($award1TeamIds !== $award2TeamIds) { + $this->addMessage(MessageType::ERROR, sprintf('Award "%s" has different team ID\'s', $awardId), implode(', ', $award->teamIds), implode(', ', $award2->teamIds)); + } + } + } + + foreach ($awards2Indexed as $awardId => $award) { + if (!isset($awards1Indexed[$awardId])) { + if (!$award->teamIds) { + $this->addMessage(MessageType::INFO, sprintf('Award "%s" not found in first file, but has no team ID\'s in second file', $awardId)); + } else { + $this->addMessage(MessageType::ERROR, sprintf('Award "%s" not found in first file', $awardId)); + } + } + } + } +} diff --git a/webapp/src/Service/Compare/Message.php b/webapp/src/Service/Compare/Message.php new file mode 100644 index 0000000000..cd198a3436 --- /dev/null +++ b/webapp/src/Service/Compare/Message.php @@ -0,0 +1,13 @@ + + */ +class ResultsCompareService extends AbstractCompareService +{ + protected function parseFile(string $file) + { + $resultsContents = file_get_contents($file); + if (!str_starts_with($resultsContents, "results\t1")) { + $this->addMessage(MessageType::ERROR, sprintf("File \"%s\" does not start with \"results\t1\"", $file)); + return null; + } + + $resultsContents = substr($resultsContents, strpos($resultsContents, "\n") + 1); + + // Prefix file with a fake header, so we can deserialize them + $resultsContents = "team_id\trank\taward\tnum_solved\ttotal_time\tlast_time\tgroup_winner\n" . $resultsContents; + + $results = $this->serializer->deserialize($resultsContents, Result::class . '[]', 'csv', [ + CsvEncoder::DELIMITER_KEY => "\t", + ]); + + // Sort results: first by num_solved, then by total_time + usort($results, fn( + Result $a, + Result $b + ) => $a->numSolved === $b->numSolved ? $a->totalTime <=> $b->totalTime : $b->numSolved <=> $a->numSolved); + + return $results; + } + + public function compare($object1, $object2): void + { + /** @var array $results1Indexed */ + $results1Indexed = []; + foreach ($object1 as $result) { + $results1Indexed[$result->teamId] = $result; + } + + /** @var array $results2Indexed */ + $results2Indexed = []; + foreach ($object2 as $result) { + $results2Indexed[$result->teamId] = $result; + } + + foreach ($object1 as $result) { + if (!isset($results2Indexed[$result->teamId])) { + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" not found in second file', $result->teamId)); + } else { + $result2 = $results2Indexed[$result->teamId]; + if ($result->rank !== $result2->rank) { + $this->addMessage(MessageType::ERROR, sprintf('Team %s has different rank', $result->teamId), (string)$result->rank, (string)$result2->rank); + } + if ($result->award !== $result2->award) { + $this->addMessage(MessageType::ERROR, sprintf('Team %s has different award', $result->teamId), $result->award, $result2->award); + } + if ($result->numSolved !== $result2->numSolved) { + $this->addMessage(MessageType::ERROR, sprintf('Team %s has different num_solved', $result->teamId), (string)$result->numSolved, (string)$result2->numSolved); + } + if ($result->totalTime !== $result2->totalTime) { + $this->addMessage(MessageType::ERROR, sprintf('Team %s has different total_time', $result->teamId), (string)$result->totalTime, (string)$result2->totalTime); + } + if ($result->lastTime !== $result2->lastTime) { + $this->addMessage(MessageType::ERROR, sprintf('Team %s has different last_time', $result->teamId), (string)$result->lastTime, (string)$result2->lastTime); + } + if ($result->groupWinner !== $result2->groupWinner) { + $this->addMessage(MessageType::WARNING, sprintf('Team %s has different group_winner', $result->teamId), (string)$result->groupWinner, (string)$result2->groupWinner); + } + } + } + + foreach ($object2 as $result) { + if (!isset($results1Indexed[$result->teamId])) { + $this->addMessage(MessageType::ERROR, sprintf('Team %s not found in first file', $result->teamId)); + } + } + } +} diff --git a/webapp/src/Service/Compare/ScoreboardCompareService.php b/webapp/src/Service/Compare/ScoreboardCompareService.php new file mode 100644 index 0000000000..84e2afd40f --- /dev/null +++ b/webapp/src/Service/Compare/ScoreboardCompareService.php @@ -0,0 +1,134 @@ + + */ +class ScoreboardCompareService extends AbstractCompareService +{ + protected function parseFile(string $file) + { + return $this->serializer->deserialize(file_get_contents($file), Scoreboard::class, 'json', ['disable_type_enforcement' => true]); + } + + public function compare($object1, $object2): void + { + if ($object1->eventId !== $object2->eventId) { + $this->addMessage(MessageType::INFO, 'Event ID does not match', $object1->eventId, $object2->eventId); + } + + if ($object1->time !== $object2->time) { + $this->addMessage(MessageType::INFO, 'Time does not match', $object1->time, $object2->time); + } + + if ($object1->contestTime !== $object2->contestTime) { + $this->addMessage(MessageType::INFO, 'Contest time does not match', $object1->contestTime, $object2->contestTime); + } + + if (($object1->state->started ?? '') !== ($object2->state->started ?? '')) { + $this->addMessage(MessageType::WARNING, 'State started does not match', $object1->state->started, $object2->state->started); + } + + if (($object1->state->ended ?? '') !== ($object2->state->ended ?? '')) { + $this->addMessage(MessageType::WARNING, 'State ended does not match', $object1->state->ended, $object2->state->ended); + } + + if (($object1->state->frozen ?? '') !== ($object2->state->frozen ?? '')) { + $this->addMessage(MessageType::WARNING, 'State frozen does not match', $object1->state->frozen, $object2->state->frozen); + } + + if (($object1->state->thawed ?? '') !== ($object2->state->thawed ?? '')) { + $this->addMessage(MessageType::WARNING, 'State thawed does not match', $object1->state->thawed, $object2->state->thawed); + } + + if (($object1->state->finalized ?? '') !== ($object2->state->finalized ?? '')) { + $this->addMessage(MessageType::WARNING, 'State finalized does not match', $object1->state->finalized, $object2->state->finalized); + } + + if (($object1->state->endOfUpdates ?? '') !== ($object2->state->endOfUpdates ?? '')) { + $this->addMessage(MessageType::WARNING, 'State end of updates does not match', $object1->state->endOfUpdates, $object2->state->endOfUpdates); + } + + if (count($object1->rows) !== count($object2->rows)) { + $this->addMessage(MessageType::ERROR, 'Number of rows does not match', (string)count($object1->rows), (string)count($object2->rows)); + } + + foreach ($object1->rows as $index => $row) { + if ($row->teamId !== $object2->rows[$index]->teamId) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Team ID does not match', $index), $row->teamId, $object2->rows[$index]->teamId); + } + + if ($row->rank !== $object2->rows[$index]->rank) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Rank does not match', $index), (string)$row->rank, (string)$object2->rows[$index]->rank); + } + + if ($row->score->numSolved !== $object2->rows[$index]->score->numSolved) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Num solved does not match', $index), (string)$row->score->numSolved, (string)$object2->rows[$index]->score->numSolved); + } + + if ($row->score->totalTime !== $object2->rows[$index]->score->totalTime) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Total time does not match', $index), (string)$row->score->totalTime, (string)$object2->rows[$index]->score->totalTime); + } + + // Problem messages are mostly info for now, since PC^2 doesn't expose time info + foreach ($row->problems as $problem) { + /** @var Problem|null $problemForSecond */ + $problemForSecond = null; + + foreach ($object2->rows[$index]->problems as $problem2) { + // PC^2 uses different problem ID's. For now also match on `Id = {problemId}-{digits}` + if ($problem->problemId === $problem2->problemId) { + $problemForSecond = $problem2; + break; + } elseif (preg_match('/^Id = ' . preg_quote($problem->problemId, '/') . '-+\d+$/', $problem2->problemId)) { + $problemForSecond = $problem2; + break; + } + } + + if ($problemForSecond === null && $problem->solved) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s solved in first file, but not in second file', $index, $problem->problemId)); + } elseif ($problemForSecond !== null && $problem->solved !== $problemForSecond->solved) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s solved does not match', $index, $problem->problemId), (string)$problem->solved, (string)$problemForSecond->solved); + } + + if ($problemForSecond) { + if ($problem->numJudged !== $problemForSecond->numJudged) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s num judged does not match', $index, $problem->problemId), (string)$problem->numJudged, (string)$problemForSecond->numJudged); + } + + if ($problem->numPending !== $problemForSecond->numPending) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s num pending does not match', $index, $problem->problemId), (string)$problem->numPending, (string)$problemForSecond->numPending); + } + + if ($problem->time !== $problemForSecond->time) { + $this->addMessage(MessageType::INFO, sprintf('Row %d: Problem %s time does not match', $index, $problem->problemId), (string)$problem->time, (string)$problemForSecond->time); + } + } + } + + foreach ($object2->rows[$index]->problems as $problem2) { + $problemForFirst = null; + + foreach ($row->problems as $problem) { + // PC^2 uses different problem ID's. For now also match on `Id = {problemId}-{digits}` + if ($problem->problemId === $problem2->problemId) { + $problemForFirst = $problem; + break; + } elseif (preg_match('/^Id = ' . preg_quote($problem->problemId, '/') . '-+\d+$/', $problem2->problemId)) { + $problemForFirst = $problem; + break; + } + } + + if ($problemForFirst === null && $problem2->solved) { + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s solved in second file, but not in first file', $index, $problem2->problemId)); + } + } + } + } +} From ca0665fd23dfb3be4f8377eb1f3e7377c68e2a5a Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Mon, 17 Jun 2024 18:08:48 +0200 Subject: [PATCH 3/4] Add tests for compare services --- .../src/DataTransferObject/ContestState.php | 12 +- .../Compare/AbstractCompareService.php | 8 + .../Service/Compare/ResultsCompareService.php | 14 +- .../Compare/ScoreboardCompareService.php | 14 +- .../Compare/AwardCompareServiceTest.php | 67 +++++++ .../Compare/ResultsCompareServiceTest.php | 77 +++++++ .../Compare/ScoreboardCompareServiceTest.php | 188 ++++++++++++++++++ 7 files changed, 360 insertions(+), 20 deletions(-) create mode 100644 webapp/tests/Unit/Service/Compare/AwardCompareServiceTest.php create mode 100644 webapp/tests/Unit/Service/Compare/ResultsCompareServiceTest.php create mode 100644 webapp/tests/Unit/Service/Compare/ScoreboardCompareServiceTest.php diff --git a/webapp/src/DataTransferObject/ContestState.php b/webapp/src/DataTransferObject/ContestState.php index e10d0ef6ed..9296df9b5e 100644 --- a/webapp/src/DataTransferObject/ContestState.php +++ b/webapp/src/DataTransferObject/ContestState.php @@ -5,11 +5,11 @@ class ContestState { public function __construct( - public readonly ?string $started, - public readonly ?string $ended, - public readonly ?string $frozen, - public readonly ?string $thawed, - public readonly ?string $finalized, - public readonly ?string $endOfUpdates, + public readonly ?string $started = null, + public readonly ?string $ended = null, + public readonly ?string $frozen = null, + public readonly ?string $thawed = null, + public readonly ?string $finalized = null, + public readonly ?string $endOfUpdates = null, ) {} } diff --git a/webapp/src/Service/Compare/AbstractCompareService.php b/webapp/src/Service/Compare/AbstractCompareService.php index b16e72c74e..783bacba21 100644 --- a/webapp/src/Service/Compare/AbstractCompareService.php +++ b/webapp/src/Service/Compare/AbstractCompareService.php @@ -73,4 +73,12 @@ protected function addMessage( ): void { $this->messages[] = new Message($type, $message, $source, $target); } + + /** + * @return Message[] + */ + public function getMessages(): array + { + return $this->messages; + } } diff --git a/webapp/src/Service/Compare/ResultsCompareService.php b/webapp/src/Service/Compare/ResultsCompareService.php index f29f8b30d5..b74fe834ae 100644 --- a/webapp/src/Service/Compare/ResultsCompareService.php +++ b/webapp/src/Service/Compare/ResultsCompareService.php @@ -56,29 +56,29 @@ public function compare($object1, $object2): void } else { $result2 = $results2Indexed[$result->teamId]; if ($result->rank !== $result2->rank) { - $this->addMessage(MessageType::ERROR, sprintf('Team %s has different rank', $result->teamId), (string)$result->rank, (string)$result2->rank); + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" has different rank', $result->teamId), (string)$result->rank, (string)$result2->rank); } if ($result->award !== $result2->award) { - $this->addMessage(MessageType::ERROR, sprintf('Team %s has different award', $result->teamId), $result->award, $result2->award); + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" has different award', $result->teamId), $result->award, $result2->award); } if ($result->numSolved !== $result2->numSolved) { - $this->addMessage(MessageType::ERROR, sprintf('Team %s has different num_solved', $result->teamId), (string)$result->numSolved, (string)$result2->numSolved); + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" has different num solved', $result->teamId), (string)$result->numSolved, (string)$result2->numSolved); } if ($result->totalTime !== $result2->totalTime) { - $this->addMessage(MessageType::ERROR, sprintf('Team %s has different total_time', $result->teamId), (string)$result->totalTime, (string)$result2->totalTime); + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" has different total time', $result->teamId), (string)$result->totalTime, (string)$result2->totalTime); } if ($result->lastTime !== $result2->lastTime) { - $this->addMessage(MessageType::ERROR, sprintf('Team %s has different last_time', $result->teamId), (string)$result->lastTime, (string)$result2->lastTime); + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" has different last time', $result->teamId), (string)$result->lastTime, (string)$result2->lastTime); } if ($result->groupWinner !== $result2->groupWinner) { - $this->addMessage(MessageType::WARNING, sprintf('Team %s has different group_winner', $result->teamId), (string)$result->groupWinner, (string)$result2->groupWinner); + $this->addMessage(MessageType::WARNING, sprintf('Team "%s" has different group winner', $result->teamId), (string)$result->groupWinner, (string)$result2->groupWinner); } } } foreach ($object2 as $result) { if (!isset($results1Indexed[$result->teamId])) { - $this->addMessage(MessageType::ERROR, sprintf('Team %s not found in first file', $result->teamId)); + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" not found in first file', $result->teamId)); } } } diff --git a/webapp/src/Service/Compare/ScoreboardCompareService.php b/webapp/src/Service/Compare/ScoreboardCompareService.php index 84e2afd40f..1f05941ae9 100644 --- a/webapp/src/Service/Compare/ScoreboardCompareService.php +++ b/webapp/src/Service/Compare/ScoreboardCompareService.php @@ -59,22 +59,21 @@ public function compare($object1, $object2): void foreach ($object1->rows as $index => $row) { if ($row->teamId !== $object2->rows[$index]->teamId) { - $this->addMessage(MessageType::ERROR, sprintf('Row %d: Team ID does not match', $index), $row->teamId, $object2->rows[$index]->teamId); + $this->addMessage(MessageType::ERROR, sprintf('Row %d: team ID does not match', $index), $row->teamId, $object2->rows[$index]->teamId); } if ($row->rank !== $object2->rows[$index]->rank) { - $this->addMessage(MessageType::ERROR, sprintf('Row %d: Rank does not match', $index), (string)$row->rank, (string)$object2->rows[$index]->rank); + $this->addMessage(MessageType::ERROR, sprintf('Row %d: rank does not match', $index), (string)$row->rank, (string)$object2->rows[$index]->rank); } if ($row->score->numSolved !== $object2->rows[$index]->score->numSolved) { - $this->addMessage(MessageType::ERROR, sprintf('Row %d: Num solved does not match', $index), (string)$row->score->numSolved, (string)$object2->rows[$index]->score->numSolved); + $this->addMessage(MessageType::ERROR, sprintf('Row %d: num solved does not match', $index), (string)$row->score->numSolved, (string)$object2->rows[$index]->score->numSolved); } if ($row->score->totalTime !== $object2->rows[$index]->score->totalTime) { - $this->addMessage(MessageType::ERROR, sprintf('Row %d: Total time does not match', $index), (string)$row->score->totalTime, (string)$object2->rows[$index]->score->totalTime); + $this->addMessage(MessageType::ERROR, sprintf('Row %d: total time does not match', $index), (string)$row->score->totalTime, (string)$object2->rows[$index]->score->totalTime); } - // Problem messages are mostly info for now, since PC^2 doesn't expose time info foreach ($row->problems as $problem) { /** @var Problem|null $problemForSecond */ $problemForSecond = null; @@ -91,7 +90,7 @@ public function compare($object1, $object2): void } if ($problemForSecond === null && $problem->solved) { - $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s solved in first file, but not in second file', $index, $problem->problemId)); + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s solved in first file, but not found in second file', $index, $problem->problemId)); } elseif ($problemForSecond !== null && $problem->solved !== $problemForSecond->solved) { $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s solved does not match', $index, $problem->problemId), (string)$problem->solved, (string)$problemForSecond->solved); } @@ -106,6 +105,7 @@ public function compare($object1, $object2): void } if ($problem->time !== $problemForSecond->time) { + // This is an info message for now, since PC^2 doesn't expose time info $this->addMessage(MessageType::INFO, sprintf('Row %d: Problem %s time does not match', $index, $problem->problemId), (string)$problem->time, (string)$problemForSecond->time); } } @@ -126,7 +126,7 @@ public function compare($object1, $object2): void } if ($problemForFirst === null && $problem2->solved) { - $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s solved in second file, but not in first file', $index, $problem2->problemId)); + $this->addMessage(MessageType::ERROR, sprintf('Row %d: Problem %s solved in second file, but not found in first file', $index, $problem2->problemId)); } } } diff --git a/webapp/tests/Unit/Service/Compare/AwardCompareServiceTest.php b/webapp/tests/Unit/Service/Compare/AwardCompareServiceTest.php new file mode 100644 index 0000000000..55f7b03277 --- /dev/null +++ b/webapp/tests/Unit/Service/Compare/AwardCompareServiceTest.php @@ -0,0 +1,67 @@ +createMock(SerializerInterface::class); + $compareService = new AwardCompareService($serializer); + + $compareService->compare($awards1, $awards2); + $messages = $compareService->getMessages(); + + self::assertEquals($expectedMessages, $messages); + } + + public function provideCompare(): Generator + { + yield [[], [], []]; + yield [ + [new Award('award1', null, [])], + [], + [new Message(MessageType::INFO, 'Award "award1" not found in second file, but has no team ID\'s in first file', null, null)], + ]; + yield [ + [], + [new Award('award2', null, [])], + [new Message(MessageType::INFO, 'Award "award2" not found in first file, but has no team ID\'s in second file', null, null)], + ]; + yield [ + [new Award('award3', null, ["1", "2", "3"])], + [], + [new Message(MessageType::ERROR, 'Award "award3" not found in second file', null, null)], + ]; + yield [ + [], + [new Award('award4', null, ["1", "2", "3"])], + [new Message(MessageType::ERROR, 'Award "award4" not found in first file', null, null)], + ]; + yield [ + [new Award('award1', 'citation1', [])], + [new Award('award1', 'citation2', [])], + [new Message(MessageType::WARNING, 'Award "award1" has different citation', 'citation1', 'citation2')], + ]; + yield [ + [new Award('award1', 'citation1', ["1", "2"])], + [new Award('award1', 'citation1', ["2", "3"])], + [new Message(MessageType::ERROR, 'Award "award1" has different team ID\'s', '1, 2', '2, 3')], + ]; + } +} diff --git a/webapp/tests/Unit/Service/Compare/ResultsCompareServiceTest.php b/webapp/tests/Unit/Service/Compare/ResultsCompareServiceTest.php new file mode 100644 index 0000000000..da3f4d9bbb --- /dev/null +++ b/webapp/tests/Unit/Service/Compare/ResultsCompareServiceTest.php @@ -0,0 +1,77 @@ +createMock(SerializerInterface::class); + $compareService = new ResultsCompareService($serializer); + + $compareService->compare($results1, $results2); + $messages = $compareService->getMessages(); + + self::assertEquals($expectedMessages, $messages); + } + + public function provideCompare(): Generator + { + yield [[], [], []]; + yield [ + [new Result('team1', 1, '', 0, 0, 0)], + [], + [new Message(MessageType::ERROR, 'Team "team1" not found in second file', null, null)], + ]; + yield [ + [], + [new Result('team2', 1, '', 0, 0, 0)], + [new Message(MessageType::ERROR, 'Team "team2" not found in first file', null, null)], + ]; + yield [ + [new Result('team3', 1, '', 0, 0, 0)], + [new Result('team3', 2, '', 0, 0, 0)], + [new Message(MessageType::ERROR, 'Team "team3" has different rank', '1', '2')], + ]; + yield [ + [new Result('team4', 1, 'award1', 0, 0, 0)], + [new Result('team4', 1, 'award2', 0, 0, 0)], + [new Message(MessageType::ERROR, 'Team "team4" has different award', 'award1', 'award2')], + ]; + yield [ + [new Result('team5', 1, 'award3', 1, 0, 0)], + [new Result('team5', 1, 'award3', 2, 0, 0)], + [new Message(MessageType::ERROR, 'Team "team5" has different num solved', '1', '2')], + ]; + yield [ + [new Result('team6', 1, 'award4', 1, 100, 0)], + [new Result('team6', 1, 'award4', 1, 200, 0)], + [new Message(MessageType::ERROR, 'Team "team6" has different total time', '100', '200')], + ]; + yield [ + [new Result('team7', 1, 'award4', 1, 100, 10)], + [new Result('team7', 1, 'award4', 1, 100, 20)], + [new Message(MessageType::ERROR, 'Team "team7" has different last time', '10', '20')], + ]; + yield [ + [new Result('team8', 1, 'award4', 1, 100, 10, 'winner1')], + [new Result('team8', 1, 'award4', 1, 100, 10, 'winner2')], + [new Message(MessageType::WARNING, 'Team "team8" has different group winner', 'winner1', 'winner2')], + ]; + } +} diff --git a/webapp/tests/Unit/Service/Compare/ScoreboardCompareServiceTest.php b/webapp/tests/Unit/Service/Compare/ScoreboardCompareServiceTest.php new file mode 100644 index 0000000000..84879451e8 --- /dev/null +++ b/webapp/tests/Unit/Service/Compare/ScoreboardCompareServiceTest.php @@ -0,0 +1,188 @@ +createMock(SerializerInterface::class); + $compareService = new ScoreboardCompareService($serializer); + + $compareService->compare($scoreboard1, $scoreboard2); + $messages = $compareService->getMessages(); + + self::assertEquals($expectedMessages, $messages); + } + + public function provideCompare(): Generator + { + yield [new Scoreboard(), new Scoreboard(), []]; + yield [ + new Scoreboard('123'), + new Scoreboard('456'), + [new Message(MessageType::INFO, 'Event ID does not match', '123', '456')], + ]; + yield [ + new Scoreboard('123', '456'), + new Scoreboard('123', '123'), + [new Message(MessageType::INFO, 'Time does not match', '456', '123')], + ]; + yield [ + new Scoreboard('123', '456', '111'), + new Scoreboard('123', '456', '222'), + [new Message(MessageType::INFO, 'Contest time does not match', '111', '222')], + ]; + yield [ + new Scoreboard('123', '456', '111', new ContestState(started: '123')), + new Scoreboard('123', '456', '111', new ContestState(started: '456')), + [new Message(MessageType::WARNING, 'State started does not match', '123', '456')], + ]; + yield [ + new Scoreboard('123', '456', '111', new ContestState(ended: '123')), + new Scoreboard('123', '456', '111', new ContestState(ended: '456')), + [new Message(MessageType::WARNING, 'State ended does not match', '123', '456')], + ]; + yield [ + new Scoreboard('123', '456', '111', new ContestState(frozen: '123')), + new Scoreboard('123', '456', '111', new ContestState(frozen: '456')), + [new Message(MessageType::WARNING, 'State frozen does not match', '123', '456')], + ]; + yield [ + new Scoreboard('123', '456', '111', new ContestState(thawed: '123')), + new Scoreboard('123', '456', '111', new ContestState(thawed: '456')), + [new Message(MessageType::WARNING, 'State thawed does not match', '123', '456')], + ]; + yield [ + new Scoreboard('123', '456', '111', new ContestState(finalized: '123')), + new Scoreboard('123', '456', '111', new ContestState(finalized: '456')), + [new Message(MessageType::WARNING, 'State finalized does not match', '123', '456')], + ]; + yield [ + new Scoreboard('123', '456', '111', new ContestState(endOfUpdates: '123')), + new Scoreboard('123', '456', '111', new ContestState(endOfUpdates: '456')), + [new Message(MessageType::WARNING, 'State end of updates does not match', '123', '456')], + ]; + yield [ + new Scoreboard(rows: []), + new Scoreboard(rows: [new Row(1, '123', new Score(0), [])]), + [new Message(MessageType::ERROR, 'Number of rows does not match', '0', '1')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(0), [])]), + new Scoreboard(rows: [new Row(1, '456', new Score(0), [])]), + [new Message(MessageType::ERROR, 'Row 0: team ID does not match', '123', '456')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(0), [])]), + new Scoreboard(rows: [new Row(2, '123', new Score(0), [])]), + [new Message(MessageType::ERROR, 'Row 0: rank does not match', '1', '2')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [])]), + new Scoreboard(rows: [new Row(1, '123', new Score(2), [])]), + [new Message(MessageType::ERROR, 'Row 0: num solved does not match', '1', '2')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1, 123), [])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1, 456), [])]), + [new Message(MessageType::ERROR, 'Row 0: total time does not match', '123', '456')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 0, true), + ])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 0, true), + ])]), + [], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 0, true), + ])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 0, false), + ])]), + [new Message(MessageType::ERROR, 'Row 0: Problem a solved does not match', '1', '')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 0, true), + ])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1), [])]), + [new Message(MessageType::ERROR, 'Row 0: Problem a solved in first file, but not found in second file')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 0, true), + ])]), + [new Message(MessageType::ERROR, 'Row 0: Problem a solved in second file, but not found in first file')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 0, true), + ])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 2, 0, true), + ])]), + [new Message(MessageType::ERROR, 'Row 0: Problem a num judged does not match', '1', '2')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 3, true), + ])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 4, true), + ])]), + [new Message(MessageType::ERROR, 'Row 0: Problem a num pending does not match', '3', '4')], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 3, true, 123), + ])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 3, true, 456), + ])]), + [new Message(MessageType::INFO, 'Row 0: Problem a time does not match', '123', '456')], + ]; + // PC^2 uses different problem ID's. Also test on `Id = {problemId}-{digits}` + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'a', 1, 0, true), + ])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'Id = a-123', 1, 0, true), + ])]), + [], + ]; + yield [ + new Scoreboard(rows: [new Row(1, '123', new Score(1), [])]), + new Scoreboard(rows: [new Row(1, '123', new Score(1), [ + new Problem('A', 'Id = a-123', 1, 0, true), + ])]), + [new Message(MessageType::ERROR, 'Row 0: Problem Id = a-123 solved in second file, but not found in first file')], + ]; + } +} From 2e4abad0ac5cf14a404df4df967cab88b499b436 Mon Sep 17 00:00:00 2001 From: Nicky Gerritsen Date: Mon, 29 Jul 2024 20:15:08 +0200 Subject: [PATCH 4/4] Use ResultRow DTO now that we have it --- webapp/src/Command/CompareResultsCommand.php | 4 +-- webapp/src/DataTransferObject/Result.php | 16 --------- .../Service/Compare/ResultsCompareService.php | 20 +++++------ .../Compare/ResultsCompareServiceTest.php | 34 +++++++++---------- 4 files changed, 29 insertions(+), 45 deletions(-) delete mode 100644 webapp/src/DataTransferObject/Result.php diff --git a/webapp/src/Command/CompareResultsCommand.php b/webapp/src/Command/CompareResultsCommand.php index 664c6ea86c..f12d0ced8e 100644 --- a/webapp/src/Command/CompareResultsCommand.php +++ b/webapp/src/Command/CompareResultsCommand.php @@ -2,13 +2,13 @@ namespace App\Command; -use App\DataTransferObject\Result; +use App\DataTransferObject\ResultRow; use App\Service\Compare\ResultsCompareService; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Serializer\SerializerInterface; /** - * @extends AbstractCompareCommand + * @extends AbstractCompareCommand */ #[AsCommand( name: 'compare:results', diff --git a/webapp/src/DataTransferObject/Result.php b/webapp/src/DataTransferObject/Result.php deleted file mode 100644 index 580b5eb853..0000000000 --- a/webapp/src/DataTransferObject/Result.php +++ /dev/null @@ -1,16 +0,0 @@ - + * @extends AbstractCompareService */ class ResultsCompareService extends AbstractCompareService { @@ -21,16 +21,16 @@ protected function parseFile(string $file) $resultsContents = substr($resultsContents, strpos($resultsContents, "\n") + 1); // Prefix file with a fake header, so we can deserialize them - $resultsContents = "team_id\trank\taward\tnum_solved\ttotal_time\tlast_time\tgroup_winner\n" . $resultsContents; + $resultsContents = "team_id\trank\taward\tnum_solved\ttotal_time\ttime_of_last_submission\tgroup_winner\n" . $resultsContents; - $results = $this->serializer->deserialize($resultsContents, Result::class . '[]', 'csv', [ + $results = $this->serializer->deserialize($resultsContents, ResultRow::class . '[]', 'csv', [ CsvEncoder::DELIMITER_KEY => "\t", ]); // Sort results: first by num_solved, then by total_time usort($results, fn( - Result $a, - Result $b + ResultRow $a, + ResultRow $b ) => $a->numSolved === $b->numSolved ? $a->totalTime <=> $b->totalTime : $b->numSolved <=> $a->numSolved); return $results; @@ -38,13 +38,13 @@ protected function parseFile(string $file) public function compare($object1, $object2): void { - /** @var array $results1Indexed */ + /** @var array $results1Indexed */ $results1Indexed = []; foreach ($object1 as $result) { $results1Indexed[$result->teamId] = $result; } - /** @var array $results2Indexed */ + /** @var array $results2Indexed */ $results2Indexed = []; foreach ($object2 as $result) { $results2Indexed[$result->teamId] = $result; @@ -67,8 +67,8 @@ public function compare($object1, $object2): void if ($result->totalTime !== $result2->totalTime) { $this->addMessage(MessageType::ERROR, sprintf('Team "%s" has different total time', $result->teamId), (string)$result->totalTime, (string)$result2->totalTime); } - if ($result->lastTime !== $result2->lastTime) { - $this->addMessage(MessageType::ERROR, sprintf('Team "%s" has different last time', $result->teamId), (string)$result->lastTime, (string)$result2->lastTime); + if ($result->timeOfLastSubmission !== $result2->timeOfLastSubmission) { + $this->addMessage(MessageType::ERROR, sprintf('Team "%s" has different last time', $result->teamId), (string)$result->timeOfLastSubmission, (string)$result2->timeOfLastSubmission); } if ($result->groupWinner !== $result2->groupWinner) { $this->addMessage(MessageType::WARNING, sprintf('Team "%s" has different group winner', $result->teamId), (string)$result->groupWinner, (string)$result2->groupWinner); diff --git a/webapp/tests/Unit/Service/Compare/ResultsCompareServiceTest.php b/webapp/tests/Unit/Service/Compare/ResultsCompareServiceTest.php index da3f4d9bbb..4c5124ef01 100644 --- a/webapp/tests/Unit/Service/Compare/ResultsCompareServiceTest.php +++ b/webapp/tests/Unit/Service/Compare/ResultsCompareServiceTest.php @@ -2,7 +2,7 @@ namespace App\Tests\Unit\Service\Compare; -use App\DataTransferObject\Result; +use App\DataTransferObject\ResultRow; use App\Service\Compare\Message; use App\Service\Compare\MessageType; use App\Service\Compare\ResultsCompareService; @@ -13,8 +13,8 @@ class ResultsCompareServiceTest extends KernelTestCase { /** - * @param Result[] $results1 - * @param Result[] $results2 + * @param ResultRow[] $results1 + * @param ResultRow[] $results2 * @param Message[] $expectedMessages * * @dataProvider provideCompare @@ -34,43 +34,43 @@ public function provideCompare(): Generator { yield [[], [], []]; yield [ - [new Result('team1', 1, '', 0, 0, 0)], + [new ResultRow('team1', 1, '', 0, 0, 0)], [], [new Message(MessageType::ERROR, 'Team "team1" not found in second file', null, null)], ]; yield [ [], - [new Result('team2', 1, '', 0, 0, 0)], + [new ResultRow('team2', 1, '', 0, 0, 0)], [new Message(MessageType::ERROR, 'Team "team2" not found in first file', null, null)], ]; yield [ - [new Result('team3', 1, '', 0, 0, 0)], - [new Result('team3', 2, '', 0, 0, 0)], + [new ResultRow('team3', 1, '', 0, 0, 0)], + [new ResultRow('team3', 2, '', 0, 0, 0)], [new Message(MessageType::ERROR, 'Team "team3" has different rank', '1', '2')], ]; yield [ - [new Result('team4', 1, 'award1', 0, 0, 0)], - [new Result('team4', 1, 'award2', 0, 0, 0)], + [new ResultRow('team4', 1, 'award1', 0, 0, 0)], + [new ResultRow('team4', 1, 'award2', 0, 0, 0)], [new Message(MessageType::ERROR, 'Team "team4" has different award', 'award1', 'award2')], ]; yield [ - [new Result('team5', 1, 'award3', 1, 0, 0)], - [new Result('team5', 1, 'award3', 2, 0, 0)], + [new ResultRow('team5', 1, 'award3', 1, 0, 0)], + [new ResultRow('team5', 1, 'award3', 2, 0, 0)], [new Message(MessageType::ERROR, 'Team "team5" has different num solved', '1', '2')], ]; yield [ - [new Result('team6', 1, 'award4', 1, 100, 0)], - [new Result('team6', 1, 'award4', 1, 200, 0)], + [new ResultRow('team6', 1, 'award4', 1, 100, 0)], + [new ResultRow('team6', 1, 'award4', 1, 200, 0)], [new Message(MessageType::ERROR, 'Team "team6" has different total time', '100', '200')], ]; yield [ - [new Result('team7', 1, 'award4', 1, 100, 10)], - [new Result('team7', 1, 'award4', 1, 100, 20)], + [new ResultRow('team7', 1, 'award4', 1, 100, 10)], + [new ResultRow('team7', 1, 'award4', 1, 100, 20)], [new Message(MessageType::ERROR, 'Team "team7" has different last time', '10', '20')], ]; yield [ - [new Result('team8', 1, 'award4', 1, 100, 10, 'winner1')], - [new Result('team8', 1, 'award4', 1, 100, 10, 'winner2')], + [new ResultRow('team8', 1, 'award4', 1, 100, 10, 'winner1')], + [new ResultRow('team8', 1, 'award4', 1, 100, 10, 'winner2')], [new Message(MessageType::WARNING, 'Team "team8" has different group winner', 'winner1', 'winner2')], ]; }