405 lines
15 KiB
PHP
405 lines
15 KiB
PHP
|
<?php
|
||
|
|
||
|
declare(strict_types=1);
|
||
|
|
||
|
namespace CondorcetPHP\Condorcet\Tests\Console\Commands;
|
||
|
|
||
|
use CondorcetPHP\Condorcet\Console\Commands\ElectionCommand;
|
||
|
use CondorcetPHP\Condorcet\Throwable\{CandidateExistsException, ResultRequestedWithoutVotesException};
|
||
|
use PHPUnit\Framework\TestCase;
|
||
|
use CondorcetPHP\Condorcet\Console\CondorcetApplication;
|
||
|
use CondorcetPHP\Condorcet\Console\Style\CondorcetStyle;
|
||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||
|
|
||
|
class ElectionCommandTest extends TestCase
|
||
|
{
|
||
|
private readonly CommandTester $electionCommand;
|
||
|
|
||
|
protected function setUp(): void
|
||
|
{
|
||
|
CondorcetApplication::create();
|
||
|
|
||
|
$this->electionCommand = new CommandTester(CondorcetApplication::$SymfonyConsoleApplication->find('election'));
|
||
|
}
|
||
|
|
||
|
public function testConsoleSimpleElection(): void
|
||
|
{
|
||
|
$this->electionCommand->execute(
|
||
|
[
|
||
|
'--candidates' => 'A;B;C',
|
||
|
'--votes' => 'A>B>C;C>B>A;B>A>C',
|
||
|
'--stats' => null,
|
||
|
'--natural-condorcet' => null,
|
||
|
'--allows-votes-weight' => null,
|
||
|
'--no-tie' => null,
|
||
|
'--list-votes' => null,
|
||
|
'--deactivate-implicit-ranking' => null,
|
||
|
'--show-pairwise' => null,
|
||
|
],
|
||
|
[
|
||
|
'verbosity' => OutputInterface::VERBOSITY_VERBOSE,
|
||
|
]
|
||
|
);
|
||
|
|
||
|
$output = $this->electionCommand->getDisplay();
|
||
|
|
||
|
self::assertStringContainsString('3 candidates registered || 3 votes registered', $output);
|
||
|
|
||
|
self::assertStringContainsString('Schulze', $output);
|
||
|
self::assertStringContainsString('Registered candidates', $output);
|
||
|
self::assertStringContainsString('Stats - votes registration', $output);
|
||
|
self::assertStringContainsString('Registered Votes List', $output);
|
||
|
self::assertStringContainsString('Pairwise', $output);
|
||
|
self::assertStringContainsString('Stats:', $output);
|
||
|
|
||
|
self::assertMatchesRegularExpression('/Is vote weight allowed\?( )+TRUE/', $output);
|
||
|
self::assertMatchesRegularExpression('/Votes are evaluated according to the implicit ranking rule\?( )+FALSE./', $output);
|
||
|
self::assertMatchesRegularExpression('/Is vote tie in rank allowed\?( )+FALSE/', $output);
|
||
|
}
|
||
|
|
||
|
public function testConsoleSeats(): void
|
||
|
{
|
||
|
$this->electionCommand->execute(
|
||
|
[
|
||
|
'--candidates' => 'A;B;C',
|
||
|
'--votes' => 'A>B>C;C>B>A;B>A>C',
|
||
|
'--stats' => null,
|
||
|
'--natural-condorcet' => null,
|
||
|
'--allows-votes-weight' => null,
|
||
|
'--no-tie' => null,
|
||
|
'--list-votes' => null,
|
||
|
'--deactivate-implicit-ranking' => null,
|
||
|
'--show-pairwise' => null,
|
||
|
|
||
|
'--seats' => 42,
|
||
|
'methods' => ['STV'],
|
||
|
],
|
||
|
[
|
||
|
'verbosity' => OutputInterface::VERBOSITY_VERBOSE,
|
||
|
]
|
||
|
);
|
||
|
|
||
|
$output = $this->electionCommand->getDisplay();
|
||
|
|
||
|
self::assertStringContainsString('3 candidates registered || 3 votes registered', $output);
|
||
|
|
||
|
self::assertStringContainsString('Seats:', $output);
|
||
|
self::assertStringContainsString('42', $output);
|
||
|
}
|
||
|
|
||
|
public function testQuotas(): void
|
||
|
{
|
||
|
$this->electionCommand->execute([
|
||
|
'--candidates' => 'A;B;C',
|
||
|
'--votes' => 'A>B>C;C>B>A;B>A>C',
|
||
|
|
||
|
'methods' => ['STV'],
|
||
|
]);
|
||
|
|
||
|
$output = $this->electionCommand->getDisplay();
|
||
|
|
||
|
self::assertMatchesRegularExpression('/Is vote tie in rank allowed\?( )+TRUE/', $output);
|
||
|
self::assertStringContainsString('Droop Quota', $output);
|
||
|
|
||
|
$this->electionCommand->execute([
|
||
|
'--candidates' => 'A;B;C',
|
||
|
'--votes' => 'A>B>C;C>B>A;B>A>C',
|
||
|
|
||
|
'methods' => ['STV'],
|
||
|
'--quota' => 'imperiali',
|
||
|
]);
|
||
|
|
||
|
$output = $this->electionCommand->getDisplay();
|
||
|
|
||
|
self::assertStringContainsString('Imperiali', $output);
|
||
|
}
|
||
|
|
||
|
public function testConsoleAllMethodsArgument(): void
|
||
|
{
|
||
|
$this->electionCommand->execute([
|
||
|
'--candidates' => 'A;B;C',
|
||
|
'--votes' => 'A>B>C;C>B>A;B>A>C',
|
||
|
|
||
|
'methods' => ['all'],
|
||
|
]);
|
||
|
|
||
|
$output = $this->electionCommand->getDisplay();
|
||
|
// \var_dump($output);
|
||
|
|
||
|
self::assertStringContainsString('Copeland', $output);
|
||
|
}
|
||
|
|
||
|
public function testConsoleMultiplesMethods(): void
|
||
|
{
|
||
|
$this->electionCommand->execute([
|
||
|
'--candidates' => 'A;B;C',
|
||
|
'--votes' => 'A>B>C;C>B>A;B>A>C',
|
||
|
|
||
|
'methods' => ['Copeland', 'RankedPairs', 'Minimax'],
|
||
|
]);
|
||
|
|
||
|
$output = $this->electionCommand->getDisplay();
|
||
|
// \var_dump($output);
|
||
|
|
||
|
self::assertStringContainsString('Copeland', $output);
|
||
|
self::assertStringContainsString('Ranked Pairs M', $output);
|
||
|
self::assertStringContainsString('Minimax Winning', $output);
|
||
|
}
|
||
|
|
||
|
public function testConsoleFileInput(): void
|
||
|
{
|
||
|
$this->electionCommand->execute([
|
||
|
'--candidates' => __DIR__.'/data.candidates',
|
||
|
'--votes' => __DIR__.'/data.votes',
|
||
|
]);
|
||
|
|
||
|
$output = $this->electionCommand->getDisplay();
|
||
|
// \var_dump($output);
|
||
|
|
||
|
self::assertStringContainsString('Schulze', $output);
|
||
|
self::assertStringContainsString('A,B', $output);
|
||
|
self::assertStringContainsString('C '.CondorcetStyle::CONDORCET_LOSER_SYMBOL, $output);
|
||
|
}
|
||
|
|
||
|
public function testInteractiveCommand(): void
|
||
|
{
|
||
|
$this->electionCommand->setInputs([
|
||
|
'A',
|
||
|
'B',
|
||
|
'C',
|
||
|
'',
|
||
|
'A>B>C',
|
||
|
'B>A>C',
|
||
|
'A>C>B',
|
||
|
'',
|
||
|
]);
|
||
|
|
||
|
$this->electionCommand->execute([
|
||
|
'command' => 'election',
|
||
|
]);
|
||
|
|
||
|
$output = $this->electionCommand->getDisplay();
|
||
|
// \var_dump($output);
|
||
|
|
||
|
self::assertStringContainsString('Results: Schulze Winning', $output);
|
||
|
}
|
||
|
|
||
|
public function testNonInteractionMode(): never
|
||
|
{
|
||
|
$this->expectException(ResultRequestedWithoutVotesException::class);
|
||
|
$this->expectExceptionMessage('The result cannot be requested without votes');
|
||
|
|
||
|
$this->electionCommand->execute([], ['interactive' => false]);
|
||
|
|
||
|
// $output = $this->electionCommand->getDisplay();
|
||
|
// \var_dump($output);
|
||
|
}
|
||
|
|
||
|
public function testCustomizeVotesPerMb(): void
|
||
|
{
|
||
|
$this->electionCommand->execute([
|
||
|
'--candidates' => 'A;B;C',
|
||
|
'--votes' => 'A>B>C;C>B>A;B>A>C',
|
||
|
'--votes-per-mb' => 42,
|
||
|
]);
|
||
|
|
||
|
self::assertSame(42, \CondorcetPHP\Condorcet\Console\Commands\ElectionCommand::$VotesPerMB);
|
||
|
|
||
|
// $output = $this->electionCommand->getDisplay();
|
||
|
// \var_dump($output);
|
||
|
}
|
||
|
|
||
|
public function testVoteWithDb1(): void
|
||
|
{
|
||
|
ElectionCommand::$forceIniMemoryLimitTo = '128M';
|
||
|
|
||
|
$this->electionCommand->execute([
|
||
|
'--candidates' => 'A;B;C',
|
||
|
'--votes-per-mb' => 1,
|
||
|
'--votes' => 'A>B>C * '.(((int) preg_replace('`[^0-9]`', '', ElectionCommand::$forceIniMemoryLimitTo)) + 1), # Must be superior to memory limit in MB
|
||
|
], [
|
||
|
'verbosity' => OutputInterface::VERBOSITY_DEBUG,
|
||
|
]);
|
||
|
|
||
|
$output = $this->electionCommand->getDisplay();
|
||
|
|
||
|
self::assertMatchesRegularExpression('/Votes per Mb +1/', $output);
|
||
|
self::assertMatchesRegularExpression('/Db is used +yes, using path\\:/', $output);
|
||
|
|
||
|
ElectionCommand::$forceIniMemoryLimitTo = null;
|
||
|
|
||
|
# And absence of this error: unlink(path): Resource temporarily unavailable
|
||
|
}
|
||
|
|
||
|
|
||
|
public function testNaturalCondorcet(): void
|
||
|
{
|
||
|
$this->electionCommand->execute([
|
||
|
'--candidates' => 'A;B;C',
|
||
|
'--votes' => 'A=B=C',
|
||
|
'--natural-condorcet' => true,
|
||
|
]);
|
||
|
|
||
|
$output = $this->electionCommand->getDisplay();
|
||
|
|
||
|
self::assertStringContainsString(CondorcetStyle::CONDORCET_WINNER_SYMBOL.' Condorcet Winner | -', $output);
|
||
|
self::assertStringContainsString(CondorcetStyle::CONDORCET_LOSER_SYMBOL.' Condorcet Loser | -', $output);
|
||
|
}
|
||
|
|
||
|
public function testFromCondorcetElectionFormat_DoubleCandidates(): void
|
||
|
{
|
||
|
$this->expectException(CandidateExistsException::class);
|
||
|
|
||
|
$this->electionCommand->execute(
|
||
|
[
|
||
|
'--candidates' => 'A;B;C',
|
||
|
'--import-condorcet-election-format' => __DIR__.'/../../Tools/Converters/CondorcetElectionFormatData/test1.cvotes',
|
||
|
],
|
||
|
[
|
||
|
'verbosity' => OutputInterface::VERBOSITY_VERBOSE,
|
||
|
]
|
||
|
);
|
||
|
}
|
||
|
|
||
|
public function testFromCondorcetElectionFormat_ArgumentpriorityAndDoubleVoteArgument(): void
|
||
|
{
|
||
|
$this->electionCommand->execute(
|
||
|
[
|
||
|
'--import-condorcet-election-format' => __DIR__.'/../../Tools/Converters/CondorcetElectionFormatData/test1.cvotes',
|
||
|
'--votes' => 'C>A',
|
||
|
'--deactivate-implicit-ranking' => null,
|
||
|
'--no-tie' => null,
|
||
|
'--allows-votes-weight' => null,
|
||
|
],
|
||
|
[
|
||
|
'verbosity' => OutputInterface::VERBOSITY_VERBOSE,
|
||
|
]
|
||
|
);
|
||
|
|
||
|
$output = $this->electionCommand->getDisplay();
|
||
|
|
||
|
self::assertStringContainsString('3 candidates registered || 2 votes registered', $output);
|
||
|
|
||
|
self::assertStringContainsString('Schulze', $output);
|
||
|
self::assertStringContainsString('Registered candidates', $output);
|
||
|
self::assertStringContainsString('Stats - votes registration', $output);
|
||
|
|
||
|
self::assertMatchesRegularExpression('/Is vote weight allowed\?( )+TRUE/', $output);
|
||
|
self::assertMatchesRegularExpression('/Votes are evaluated according to the implicit ranking rule\?( )+FALSE./', $output);
|
||
|
self::assertMatchesRegularExpression('/Is vote tie in rank allowed\?( )+FALSE/', $output);
|
||
|
|
||
|
self::assertStringContainsString('Sum vote weight | 3', $output);
|
||
|
}
|
||
|
|
||
|
public function testFromCondorcetElectionFormat_Arguments(): void
|
||
|
{
|
||
|
$this->electionCommand->execute(
|
||
|
[
|
||
|
'--import-condorcet-election-format' => __DIR__.'/../../Tools/Converters/CondorcetElectionFormatData/test2.cvotes',
|
||
|
],
|
||
|
[
|
||
|
'verbosity' => OutputInterface::VERBOSITY_VERBOSE,
|
||
|
]
|
||
|
);
|
||
|
|
||
|
$output = $this->electionCommand->getDisplay();
|
||
|
|
||
|
self::assertStringContainsString('3 candidates registered || 2 votes registered', $output);
|
||
|
|
||
|
self::assertStringContainsString('Schulze', $output);
|
||
|
self::assertStringContainsString('Registered candidates', $output);
|
||
|
self::assertStringContainsString('Stats - votes registration', $output);
|
||
|
|
||
|
self::assertMatchesRegularExpression('/Is vote weight allowed\?( )+FALSE/', $output);
|
||
|
self::assertMatchesRegularExpression('/Votes are evaluated according to the implicit ranking rule\?( )+FALSE./', $output);
|
||
|
self::assertMatchesRegularExpression('/Is vote tie in rank allowed\?( )+TRUE/', $output);
|
||
|
|
||
|
self::assertStringContainsString('Sum vote weight | 2', $output);
|
||
|
|
||
|
self::assertStringContainsString('B '.CondorcetStyle::CONDORCET_WINNER_SYMBOL, $output); # Condorcet Winner
|
||
|
}
|
||
|
|
||
|
public function testVoteWithDb_CondorcetElectionFormat(): void
|
||
|
{
|
||
|
ElectionCommand::$forceIniMemoryLimitTo = '128M';
|
||
|
|
||
|
$this->electionCommand->execute([
|
||
|
'--votes-per-mb' => 1,
|
||
|
'--import-condorcet-election-format' => __DIR__.'/../../Tools/Converters/CondorcetElectionFormatData/test3.cvotes',
|
||
|
], [
|
||
|
'verbosity' => OutputInterface::VERBOSITY_DEBUG,
|
||
|
]);
|
||
|
|
||
|
$output = $this->electionCommand->getDisplay();
|
||
|
|
||
|
self::assertMatchesRegularExpression('/Votes per Mb +1/', $output);
|
||
|
self::assertStringContainsString('Db is used', $output);
|
||
|
self::assertStringContainsString('yes, using path:', $output);
|
||
|
|
||
|
ElectionCommand::$forceIniMemoryLimitTo = null;
|
||
|
|
||
|
# And absence of this error: unlink(path): Resource temporarily unavailable
|
||
|
}
|
||
|
|
||
|
public function testFromDebianFormat(): void
|
||
|
{
|
||
|
$this->electionCommand->execute(
|
||
|
[
|
||
|
'--import-debian-format' => __DIR__.'/../../Tools/Converters/DebianData/leader2020_tally.txt',
|
||
|
'methods' => ['STV'],
|
||
|
],
|
||
|
[
|
||
|
'verbosity' => OutputInterface::VERBOSITY_VERBOSE,
|
||
|
]
|
||
|
);
|
||
|
|
||
|
$output = $this->electionCommand->getDisplay();
|
||
|
|
||
|
self::assertStringContainsString('4 candidates registered || 339 votes registered', $output);
|
||
|
|
||
|
self::assertStringContainsString('STV', $output);
|
||
|
self::assertStringContainsString('Registered candidates', $output);
|
||
|
self::assertStringContainsString('Stats - votes registration', $output);
|
||
|
|
||
|
self::assertMatchesRegularExpression('/Is vote weight allowed\?( )+FALSE/', $output);
|
||
|
self::assertMatchesRegularExpression('/Votes are evaluated according to the implicit ranking rule\?( )+TRUE./', $output);
|
||
|
self::assertMatchesRegularExpression('/Is vote tie in rank allowed\?( )+TRUE/', $output);
|
||
|
|
||
|
self::assertStringContainsString('Sum vote weight | 339', $output);
|
||
|
|
||
|
self::assertStringContainsString('Jonathan Carter '.CondorcetStyle::CONDORCET_WINNER_SYMBOL, $output); # Condorcet Winner
|
||
|
self::assertMatchesRegularExpression('/Seats: *\| 1/', $output);
|
||
|
}
|
||
|
|
||
|
public function testFromDavidHillFormat(): void
|
||
|
{
|
||
|
$this->electionCommand->execute(
|
||
|
[
|
||
|
'--import-david-hill-format' => __DIR__.'/../../Tools/Converters/TidemanData/A1.HIL',
|
||
|
'methods' => ['STV'],
|
||
|
],
|
||
|
[
|
||
|
'verbosity' => OutputInterface::VERBOSITY_VERBOSE,
|
||
|
]
|
||
|
);
|
||
|
|
||
|
$output = $this->electionCommand->getDisplay();
|
||
|
|
||
|
self::assertStringContainsString('10 candidates registered || 380 votes registered', $output);
|
||
|
|
||
|
self::assertStringContainsString('STV', $output);
|
||
|
self::assertStringContainsString('Registered candidates', $output);
|
||
|
self::assertStringContainsString('Stats - votes registration', $output);
|
||
|
|
||
|
self::assertMatchesRegularExpression('/Is vote weight allowed\?( )+FALSE/', $output);
|
||
|
self::assertMatchesRegularExpression('/Votes are evaluated according to the implicit ranking rule\?( )+TRUE./', $output);
|
||
|
self::assertMatchesRegularExpression('/Is vote tie in rank allowed\?( )+TRUE/', $output);
|
||
|
|
||
|
self::assertStringContainsString('Sum vote weight | 380', $output);
|
||
|
|
||
|
self::assertStringContainsString('Candidate 1 '.CondorcetStyle::CONDORCET_WINNER_SYMBOL, $output); # Condorcet Winner
|
||
|
self::assertMatchesRegularExpression('/Seats: *\| 3/', $output);
|
||
|
}
|
||
|
}
|