Skip to content

Commit

Permalink
Support parallel limit in execution loop (lmc-eu#210)
Browse files Browse the repository at this point in the history
  • Loading branch information
OndraM committed Jul 17, 2018
1 parent 7d0cbf6 commit b9ada6c
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Lmc\Steward\Console\Command\Fixtures\SimpleTests\SimpleTest> [%s] [WebDriver] Ex
Lmc\Steward\Console\Command\Fixtures\SimpleTests\SimpleTest> [%s] --- Finished execution of test "testWebpage" ---%A
Lmc\Steward\Console\Command\Fixtures\SimpleTests\SimpleTest> OK (1 test, 1 assertion)%A
[%s] Finished execution of testcase "Lmc\Steward\Console\Command\Fixtures\SimpleTests\SimpleTest" (result: passed, time: %f sec)%A
[%s] Unqueing testcase "Lmc\Steward\Console\Command\Fixtures\SimpleTests\DependantTest"%A
[%s] Dequeing testcase "Lmc\Steward\Console\Command\Fixtures\SimpleTests\DependantTest"%A
[%s] Execution of testcase "Lmc\Steward\Console\Command\Fixtures\SimpleTests\DependantTest" started with command:
'%s/steward/bin/phpunit-steward' '--log-junit=%s/logs/Lmc-Steward-Console-Command-Fixtures-SimpleTests-DependantTest.xml' '--configuration=%s/phpunit.xml' '%s/DependantTest.php'
Lmc\Steward\Console\Command\Fixtures\SimpleTests\DependantTest> [%s] Registering test results publisher "Lmc\Steward\Publisher\XmlPublisher"%a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Starting execution of testcases
-------------------------------
[%s] Execution of testcase "Lmc\Steward\Console\Command\Fixtures\SimpleTests\SimpleTest" started%A
[%s] Finished execution of testcase "Lmc\Steward\Console\Command\Fixtures\SimpleTests\SimpleTest" (result: passed, time: %f sec)%A
[%s] Unqueing testcase "Lmc\Steward\Console\Command\Fixtures\SimpleTests\DependantTest"%A
[%s] Dequeing testcase "Lmc\Steward\Console\Command\Fixtures\SimpleTests\DependantTest"%A
[%s] Execution of testcase "Lmc\Steward\Console\Command\Fixtures\SimpleTests\DependantTest" started%A
[%s] Finished execution of testcase "Lmc\Steward\Console\Command\Fixtures\SimpleTests\DependantTest" (result: passed, time: %f sec)
[%s] Waiting (running: 0, queued: 0, done: 2 [passed: 2])
Expand Down
43 changes: 40 additions & 3 deletions src-tests/Process/ExecutionLoopTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\Process;

/**
* @covers \Lmc\Steward\Process\ExecutionLoop
Expand All @@ -17,17 +18,53 @@ class ExecutionLoopTest extends TestCase
public function shouldExecuteEmptyProcessSet(): void
{
$emptyProcessSet = new ProcessSet();
$output = new BufferedOutput(OutputInterface::VERBOSITY_DEBUG);
$outputBuffer = new BufferedOutput(OutputInterface::VERBOSITY_DEBUG);

$loop = new ExecutionLoop(
$emptyProcessSet,
new StewardStyle(new StringInput(''), $output),
new StewardStyle(new StringInput(''), $outputBuffer),
new MaxTotalDelayStrategy()
);

$result = $loop->start();

$this->assertTrue($result);
$this->assertContains('[OK] Testcases executed: 0', $output->fetch());
$this->assertContains('[OK] Testcases executed: 0', $outputBuffer->fetch());
}

/** @test */
public function shouldDequeueProcessesWithoutDelayOnStartup(): void
{
$noDelayTest = new ProcessWrapper(new Process(''), 'NoDelay');
$delayedTest = new ProcessWrapper(new Process(''), 'Delayed');
$delayedTest->setDelay('NoDelay', 0.001);

$processSet = new ProcessSet();
$processSet->add($noDelayTest);
$processSet->add($delayedTest);

// Preconditions - both processes should be queued after being added
$processes = $processSet->get(ProcessWrapper::PROCESS_STATUS_QUEUED);
$this->assertCount(2, $processes);

$outputBuffer = new BufferedOutput(OutputInterface::VERBOSITY_DEBUG);
$loop = new ExecutionLoop(
$processSet,
new StewardStyle(new StringInput(''), $outputBuffer),
new MaxTotalDelayStrategy()
);

$result = $loop->start();
$this->assertTrue($result);

$output = $outputBuffer->fetch();

$this->assertContains('Testcase "NoDelay" is prepared to be run', $output);
$this->assertContains(
'Testcase "Delayed" is queued to be run 0.0 minutes after testcase "NoDelay" is finished',
$output
);

$this->assertContains('Dequeing testcase "Delayed"', $output);
}
}
36 changes: 0 additions & 36 deletions src-tests/Process/ProcessSetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
use Lmc\Steward\Process\Fixtures\MockOrderStrategy;
use Lmc\Steward\Publisher\XmlPublisher;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\Output;
use Symfony\Component\Process\Process;

class ProcessSetTest extends TestCase
Expand Down Expand Up @@ -196,40 +194,6 @@ public function testShouldCountResultsOfDoneProcesses(): void
);
}

public function testShouldDequeueProcessesWithoutDelay(): void
{
$noDelayTest = new ProcessWrapper(new Process(''), 'NoDelay');
$delayedTest = new ProcessWrapper(new Process(''), 'Delayed');
$delayedTest->setDelay('NoDelay', 3.3);
$this->set->add($noDelayTest);
$this->set->add($delayedTest);
$outputBuffer = new BufferedOutput(Output::VERBOSITY_DEBUG);

// Preconditions - both processes should be queued after being added
$processes = $this->set->get(ProcessWrapper::PROCESS_STATUS_QUEUED);
$this->assertCount(2, $processes);

// Should Dequeue process without delay
$this->set->dequeueProcessesWithoutDelay($outputBuffer);

// The process without delay should be prepared now
$prepared = $this->set->get(ProcessWrapper::PROCESS_STATUS_PREPARED);
$this->assertCount(1, $prepared);
$this->assertSame($noDelayTest, $prepared['NoDelay']);

// The other process with delay should be kept as queued
$queued = $this->set->get(ProcessWrapper::PROCESS_STATUS_QUEUED);
$this->assertCount(1, $queued);
$this->assertSame($delayedTest, $queued['Delayed']);

$output = $outputBuffer->fetch();
$this->assertContains('Testcase "NoDelay" is prepared to be run', $output);
$this->assertContains(
'Testcase "Delayed" is queued to be run 3.3 minutes after testcase "NoDelay" is finished',
$output
);
}

public function testShouldFailBuildingTreeIfTestHasDependencyOnNotExistingTest(): void
{
$process = new ProcessWrapper(new Process(''), 'Foo');
Expand Down
74 changes: 67 additions & 7 deletions src/Process/ExecutionLoop.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Lmc\Steward\Process;

use Lmc\Steward\Console\Style\StewardStyle;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Stopwatch\Stopwatch;

class ExecutionLoop
Expand All @@ -13,14 +14,21 @@ class ExecutionLoop
private $io;
/** @var OptimizeOrderInterface */
private $optimizeStrategy;
/** @var int */
private $parallelLimit;
/** @var Stopwatch */
protected $stopwatch;

public function __construct(ProcessSet $processSet, StewardStyle $io, OptimizeOrderInterface $optimizeStrategy)
{
public function __construct(
ProcessSet $processSet,
StewardStyle $io,
OptimizeOrderInterface $optimizeStrategy,
int $parallelLimit = 50
) {
$this->processSet = $processSet;
$this->io = $io;
$this->optimizeStrategy = $optimizeStrategy;
$this->parallelLimit = $parallelLimit;
$this->stopwatch = new Stopwatch();
}

Expand All @@ -32,6 +40,7 @@ public function start(): bool

$counterWaitingOutput = 1;
$statusesCountLast = [];

// Iterate over prepared and queued until everything is done
while (true) {
$prepared = $this->processSet->get(ProcessWrapper::PROCESS_STATUS_PREPARED);
Expand All @@ -40,7 +49,6 @@ public function start(): bool
if (count($prepared) === 0 && count($queued) === 0) {
break;
}

// Start all prepared tasks and set status of not running as finished
foreach ($prepared as $testClass => $processWrapper) {
if (!$processWrapper->getProcess()->isStarted()) {
Expand Down Expand Up @@ -114,7 +122,7 @@ public function start(): bool
}
}
}

$this->dequeueParallelProcesses();
$this->dequeueDependentProcesses();

$statusesCount = $this->processSet->countStatuses();
Expand Down Expand Up @@ -158,7 +166,40 @@ protected function initialize(): void
$this->processSet->optimizeOrder($this->optimizeStrategy);

// Initialize first processes that should be run
$this->processSet->dequeueProcessesWithoutDelay($this->io);
$this->dequeueProcessesWithoutDelay();
}

/**
* Set queued processes without delay as prepared
*/
protected function dequeueProcessesWithoutDelay(): void
{
$queuedProcesses = $this->processSet->get(ProcessWrapper::PROCESS_STATUS_QUEUED);

foreach ($queuedProcesses as $className => $processWrapper) {
if ($this->parallelLimitReached()) {
$this->io->writeln(
sprintf('Max parallel processes limit reached, testcase "%s" is queued', $className),
OutputInterface::VERBOSITY_QUIET
);
} elseif (!$processWrapper->isDelayed()) {
$this->io->writeln(
sprintf('Testcase "%s" is prepared to be run', $className),
OutputInterface::VERBOSITY_DEBUG
);
$processWrapper->setStatus(ProcessWrapper::PROCESS_STATUS_PREPARED);
} else {
$this->io->writeln(
sprintf(
'Testcase "%s" is queued to be run %01.1f minutes after testcase "%s" is finished',
$className,
$processWrapper->getDelayMinutes(),
$processWrapper->getDelayAfter()
),
OutputInterface::VERBOSITY_DEBUG
);
}
}
}

protected function dequeueDependentProcesses(): void
Expand All @@ -175,11 +216,25 @@ protected function dequeueDependentProcesses(): void
foreach ($queued as $testClass => $processWrapper) {
$delaySeconds = $processWrapper->getDelayMinutes() * 60;

if (in_array($processWrapper->getDelayAfter(), $doneClasses, true)
if (!$this->parallelLimitReached() && in_array($processWrapper->getDelayAfter(), $doneClasses, true)
&& (time() - $done[$processWrapper->getDelayAfter()]->getFinishedTime()) > $delaySeconds
) {
if ($this->io->isVeryVerbose()) {
$this->io->runStatus(sprintf('Unqueing testcase "%s"', $testClass));
$this->io->runStatus(sprintf('Dequeing testcase "%s"', $testClass));
}
$processWrapper->setStatus(ProcessWrapper::PROCESS_STATUS_PREPARED);
}
}
}

protected function dequeueParallelProcesses(): void
{
$queued = $this->processSet->get(ProcessWrapper::PROCESS_STATUS_QUEUED);

foreach ($queued as $testClass => $processWrapper) {
if (!$processWrapper->isDelayed() && !$this->parallelLimitReached()) {
if ($this->io->isVeryVerbose()) {
$this->io->runStatus(sprintf('Dequeing testcase "%s" from parallel limit', $testClass));
}
$processWrapper->setStatus(ProcessWrapper::PROCESS_STATUS_PREPARED);
}
Expand Down Expand Up @@ -245,4 +300,9 @@ protected function flushProcessOutput(ProcessWrapper $processWrapper): void
$processWrapper->getClassName()
);
}

protected function parallelLimitReached(): bool
{
return count($this->processSet->get(ProcessWrapper::PROCESS_STATUS_PREPARED)) >= $this->parallelLimit;
}
}
31 changes: 0 additions & 31 deletions src/Process/ProcessSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use Fhaculty\Graph\Vertex;
use Graphp\Algorithms\Tree\OutTree;
use Lmc\Steward\Publisher\AbstractPublisher;
use Symfony\Component\Console\Output\OutputInterface;

/**
* Set of Test processes.
Expand Down Expand Up @@ -97,36 +96,6 @@ public function get(string $status): array
return $return;
}

/**
* Set queued processes without delay as prepared
*
* @param OutputInterface $output Where list of dequeued and queued processes will be printed
*/
public function dequeueProcessesWithoutDelay(OutputInterface $output): void
{
$queuedProcesses = $this->get(ProcessWrapper::PROCESS_STATUS_QUEUED);

foreach ($queuedProcesses as $className => $processWrapper) {
if (!$processWrapper->isDelayed()) {
$output->writeln(
sprintf('Testcase "%s" is prepared to be run', $className),
OutputInterface::VERBOSITY_DEBUG
);
$processWrapper->setStatus(ProcessWrapper::PROCESS_STATUS_PREPARED);
} else {
$output->writeln(
sprintf(
'Testcase "%s" is queued to be run %01.1f minutes after testcase "%s" is finished',
$className,
$processWrapper->getDelayMinutes(),
$processWrapper->getDelayAfter()
),
OutputInterface::VERBOSITY_DEBUG
);
}
}
}

/**
* Build out-tree graph from defined Processes and their relations.
*
Expand Down

0 comments on commit b9ada6c

Please sign in to comment.