Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Steur <thomas.steur@googlemail.com>2014-10-22 08:19:08 +0400
committerThomas Steur <thomas.steur@googlemail.com>2014-10-28 06:49:03 +0300
commita6b2a6d09b06fe413190938b0c88df1c50df64f9 (patch)
tree9809507d17a0f5c133d28a00d5dc82f0675689c8 /plugins/TestRunner
parent13c771ee55252bb8a825f7d8474de7f704ec5fc5 (diff)
refs #6429 added a command to trigger tests on aws
Diffstat (limited to 'plugins/TestRunner')
-rw-r--r--plugins/TestRunner/Aws/CloudWatch.php111
-rw-r--r--plugins/TestRunner/Aws/Config.php99
-rw-r--r--plugins/TestRunner/Aws/Instance.php162
-rw-r--r--plugins/TestRunner/Aws/Ssh.php54
-rw-r--r--plugins/TestRunner/Aws/Tags.php43
-rw-r--r--plugins/TestRunner/Commands/TestRunOnAws.php141
-rw-r--r--plugins/TestRunner/Commands/TestsRun.php206
-rw-r--r--plugins/TestRunner/Commands/TestsRunUI.php83
-rw-r--r--plugins/TestRunner/Commands/TestsSetupFixture.php273
-rw-r--r--plugins/TestRunner/README.md18
-rw-r--r--plugins/TestRunner/Runner/InstanceLauncher.php52
-rw-r--r--plugins/TestRunner/Runner/Remote.php68
-rw-r--r--plugins/TestRunner/TestRunner.php15
-rw-r--r--plugins/TestRunner/plugin.json19
-rw-r--r--plugins/TestRunner/screenshots/.gitkeep0
15 files changed, 1344 insertions, 0 deletions
diff --git a/plugins/TestRunner/Aws/CloudWatch.php b/plugins/TestRunner/Aws/CloudWatch.php
new file mode 100644
index 0000000000..0748ac129f
--- /dev/null
+++ b/plugins/TestRunner/Aws/CloudWatch.php
@@ -0,0 +1,111 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\TestRunner\Aws;
+
+use Aws\CloudWatch\CloudWatchClient;
+use Aws\CloudWatch\Enum\ComparisonOperator;
+use Aws\CloudWatch\Enum\Statistic;
+use Aws\CloudWatch\Enum\Unit;
+
+class CloudWatch
+{
+ /**
+ * @var Config
+ */
+ private $config;
+
+ public function __construct(Config $awsConfig)
+ {
+ $this->config = $awsConfig;
+ }
+
+ public function terminateInstanceIfIdleForTooLong($instanceIds)
+ {
+ $client = $this->getCloudWatchClient();
+
+ $client->putMetricAlarm(array(
+ 'AlarmName' => 'TerminateInstanceBecauseIdle',
+ 'AlarmDescription' => 'Terminate instances if CPU is on average < 10% for 5 minutes in a row 8 times consecutively',
+ 'ActionsEnabled' => true,
+ 'OKActions' => array(),
+ 'AlarmActions' => $this->getAlarmActions(),
+ 'InsufficientDataActions' => array(),
+ 'MetricName' => 'CPUUtilization',
+ 'Namespace' => $this->getNamespace(),
+ 'Statistic' => Statistic::AVERAGE,
+ 'Dimensions' => $this->getDimensions($instanceIds),
+ 'Period' => 300,
+ 'Unit' => Unit::PERCENT,
+ 'EvaluationPeriods' => 8,
+ 'Threshold' => 10,
+ 'ComparisonOperator' => ComparisonOperator::LESS_THAN_THRESHOLD,
+ ));
+
+ $client->putMetricAlarm(array(
+ 'AlarmName' => 'TerminateInstanceIfStatusCheckFails',
+ 'AlarmDescription' => 'Terminate instances in case two status check fail within one minute',
+ 'ActionsEnabled' => true,
+ 'OKActions' => array(),
+ 'AlarmActions' => $this->getAlarmActions(),
+ 'InsufficientDataActions' => array(),
+ 'MetricName' => 'StatusCheckFailed',
+ 'Namespace' => $this->getNamespace(),
+ 'Statistic' => Statistic::AVERAGE,
+ 'Dimensions' => $this->getDimensions($instanceIds),
+ 'Period' => 60,
+ 'Unit' => Unit::PERCENT,
+ 'EvaluationPeriods' => 2,
+ 'Threshold' => 1,
+ 'ComparisonOperator' => ComparisonOperator::GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
+ ));
+ }
+
+ private function getCloudWatchClient()
+ {
+ return CloudWatchClient::factory($this->getConnectionOptions());
+ }
+
+ private function getConnectionOptions()
+ {
+ return array(
+ 'key' => $this->config->getAccessKey(),
+ 'secret' => $this->config->getSecretKey(),
+ 'region' => $this->config->getRegion()
+ );
+ }
+
+ private function getDimensions($instanceIds)
+ {
+ $dimensions = array();
+
+ foreach ($instanceIds as $instanceId) {
+ $dimensions[] = array(
+ 'Name' => 'InstanceId',
+ 'Value' => $instanceId,
+ );
+ }
+
+ return $dimensions;
+ }
+
+ private function getNamespace()
+ {
+ return 'AWS/EC2';
+ }
+
+ private function getAlarmActions()
+ {
+ return array(
+ 'arn:aws:automate:' . $this->config->getRegion() . ':ec2:terminate',
+ 'arn:aws:sns:' . $this->config->getRegion() . ':682510200394:TerminateInstanceBecauseIdle'
+ );
+ }
+
+}
diff --git a/plugins/TestRunner/Aws/Config.php b/plugins/TestRunner/Aws/Config.php
new file mode 100644
index 0000000000..3649cf10f4
--- /dev/null
+++ b/plugins/TestRunner/Aws/Config.php
@@ -0,0 +1,99 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\TestRunner\Aws;
+
+use \Piwik\Config as PiwikConfig;
+
+class Config
+{
+ public function getRegion()
+ {
+ return trim($this->getConfigValue('aws_region'));
+ }
+
+ public function getAmi()
+ {
+ return trim($this->getConfigValue('aws_ami'));
+ }
+
+ public function getInstanceType()
+ {
+ return trim($this->getConfigValue('aws_instance_type'));
+ }
+
+ public function getKeyName()
+ {
+ return $this->getConfigValue('aws_keyname');
+ }
+
+ public function getPemFile()
+ {
+ return trim($this->getConfigValue('aws_pem_file'));
+ }
+
+ public function getAccessKey()
+ {
+ return trim($this->getConfigValue('aws_accesskey'));
+ }
+
+ public function getSecretKey()
+ {
+ return trim($this->getConfigValue('aws_secret'));
+ }
+
+ public function getSecurityGroups()
+ {
+ $groups = $this->getConfigValue('aws_securitygroups');
+
+ if (empty($groups)) {
+ $groups = array();
+ }
+
+ return (array) $groups;
+ }
+
+ public function validate()
+ {
+ $configKeysToValidate = array(
+ 'aws_accesskey',
+ 'aws_secret',
+ 'aws_region',
+ 'aws_ami',
+ 'aws_instance_type',
+ 'aws_pem_file',
+ 'aws_keyname',
+ 'aws_securitygroups',
+ );
+
+ foreach ($configKeysToValidate as $key) {
+ if (!$this->getConfigValue($key)) {
+ throw new \RuntimeException("[tests]$key is not configured");
+ }
+ }
+
+ $pemFile = $this->getPemFile();
+
+ if (!file_exists($pemFile)) {
+ throw new \RuntimeException('[tests]aws_pem_file the file does not exist or is not readable');
+ }
+ }
+
+ private function getConfig()
+ {
+ return PiwikConfig::getInstance()->tests;
+ }
+
+ private function getConfigValue($key)
+ {
+ $config = $this->getConfig();
+
+ return $config[$key];
+ }
+}
diff --git a/plugins/TestRunner/Aws/Instance.php b/plugins/TestRunner/Aws/Instance.php
new file mode 100644
index 0000000000..e7bcb7a6ad
--- /dev/null
+++ b/plugins/TestRunner/Aws/Instance.php
@@ -0,0 +1,162 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\TestRunner\Aws;
+
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Aws\Ec2\Ec2Client;
+
+class Instance
+{
+
+ /**
+ * @var Config
+ */
+ private $config;
+
+ /**
+ * @var Ec2Client
+ */
+ private $client;
+
+ private $testSuite;
+
+ private $useOneInstancePerTestSuite = false;
+
+ public function __construct(Config $config, $testSuite)
+ {
+ $this->config = $config;
+ $this->testSuite = $testSuite;
+ $this->client = $this->createEc2Client();
+ }
+
+ public function enableUseOneInstancePerTestSuite()
+ {
+ $this->useOneInstancePerTestSuite = true;
+ }
+
+ public function findExisting()
+ {
+ $filters = array(
+ array('Name' => 'image-id', 'Values' => array($this->config->getAmi())),
+ array('Name' => 'key-name', 'Values' => array($this->config->getKeyName())),
+ array('Name' => 'instance-state-name', 'Values' => array('running')),
+ );
+
+ if (!empty($this->testSuite) && $this->useOneInstancePerTestSuite) {
+ $filters[] = array('Name' => 'tag:TestSuite', 'Values' => array($this->testSuite));
+ }
+
+ $instances = $this->client->describeInstances(array('Filters' => $filters));
+
+ $reservations = $instances->getPath('Reservations');
+
+ if (!empty($reservations)) {
+ $host = $this->getHostFromDescribedInstances($instances);
+
+ return $host;
+ }
+ }
+
+ public function terminate($instanceIds)
+ {
+ $this->client->terminateInstances(array(
+ 'InstanceIds' => $instanceIds
+ ));
+
+ $this->client->waitUntilInstanceTerminated(array(
+ 'InstanceIds' => $instanceIds
+ ));
+ }
+
+ public function launch()
+ {
+ $result = $this->client->runInstances(array(
+ 'ImageId' => $this->config->getAmi(),
+ 'MinCount' => 1,
+ 'MaxCount' => 1,
+ 'InstanceType' => $this->config->getInstanceType(),
+ 'KeyName' => $this->config->getKeyName(),
+ 'SecurityGroups' => $this->config->getSecurityGroups(),
+ 'InstanceInitiatedShutdownBehavior' => 'terminate'
+ ));
+
+ $instanceIds = $result->getPath('Instances/*/InstanceId');
+
+ return $instanceIds;
+ }
+
+ public function setup($instanceIds)
+ {
+ $this->client->waitUntilInstanceRunning(array(
+ 'InstanceIds' => $instanceIds,
+ ));
+
+ $awsCloudWatch = new CloudWatch($this->config);
+ $awsCloudWatch->terminateInstanceIfIdleForTooLong($instanceIds);
+
+ $awsTags = new Tags($this->client);
+ $awsTags->assignTagsToInstances($instanceIds, $this->testSuite);
+
+ $instances = $this->client->describeInstances(array(
+ 'InstanceIds' => $instanceIds,
+ ));
+
+ $host = $this->getHostFromDescribedInstances($instances);
+
+ return $host;
+ }
+
+ /**
+ * @param \Guzzle\Service\Resource\Model $resources
+ * @return mixed
+ */
+ private function getHostFromDescribedInstances($resources)
+ {
+ $instances = $resources->getPath('Reservations/*/Instances');
+
+ $instanceToUse = null;
+
+ foreach ($instances as $index => $instance) {
+ foreach ($instance['Tags'] as $tag) {
+ if (!empty($this->testSuite)
+ && $tag['Key'] === 'TestSuite'
+ && $tag['Value'] === $this->testSuite) {
+
+ $instanceToUse = $instance;
+ }
+ }
+ }
+
+ if (empty($instanceToUse)) {
+ $instanceToUse = array_shift($instances);
+ }
+
+ $host = $instanceToUse['PublicDnsName'];
+
+ return $host;
+ }
+
+ private function createEc2Client()
+ {
+ return Ec2Client::factory($this->getConnectionOptions());
+ }
+
+ private function getConnectionOptions()
+ {
+ return array(
+ 'key' => $this->config->getAccessKey(),
+ 'secret' => $this->config->getSecretKey(),
+ 'region' => $this->config->getRegion()
+ );
+ }
+} \ No newline at end of file
diff --git a/plugins/TestRunner/Aws/Ssh.php b/plugins/TestRunner/Aws/Ssh.php
new file mode 100644
index 0000000000..6e41eb0d37
--- /dev/null
+++ b/plugins/TestRunner/Aws/Ssh.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\TestRunner\Aws;
+use Symfony\Component\Console\Output\OutputInterface;
+use Crypt_RSA;
+use Net_SSH2;
+
+class Ssh extends Net_SSH2
+{
+ /**
+ * @var OutputInterface
+ */
+ private $output;
+
+ public static function connectToAws($host, $pemFile)
+ {
+ $key = new Crypt_RSA();
+ $key->loadKey(file_get_contents($pemFile));
+
+ $ssh = new Ssh($host);
+
+ if (!$ssh->login('ubuntu', $key)) {
+ throw new \RuntimeException("Login to $host using $pemFile failed");
+ }
+
+ return $ssh;
+ }
+
+ public function setOutput(OutputInterface $output)
+ {
+ $this->output = $output;
+ }
+
+ public function exec($command)
+ {
+ $command = 'cd www/piwik && ' . $command;
+ $output = $this->output;
+
+ $output->writeln("Executing <comment>$command</comment>");
+
+ return parent::exec($command, function($tempOutput) use ($output) {
+ if ($output) {
+ $output->write($tempOutput);
+ }
+ });
+ }
+}
diff --git a/plugins/TestRunner/Aws/Tags.php b/plugins/TestRunner/Aws/Tags.php
new file mode 100644
index 0000000000..aa6c156318
--- /dev/null
+++ b/plugins/TestRunner/Aws/Tags.php
@@ -0,0 +1,43 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\TestRunner\Aws;
+use Aws\Ec2\Ec2Client;
+
+class Tags
+{
+ /**
+ * @var Ec2Client
+ */
+ private $ec2Client;
+
+ public function __construct(Ec2Client $client)
+ {
+ $this->ec2Client = $client;
+ }
+
+ public function assignTagsToInstances($instanceIds, $testSuite)
+ {
+ $tags = array($this->buildTag('Name', 'PiwikTesting'));
+
+ if (!empty($testSuite)) {
+ $tags[] = $this->buildTag('TestSuite', $testSuite);
+ }
+
+ $this->ec2Client->createTags(array('Resources' => $instanceIds, 'Tags' => $tags));
+ }
+
+ private function buildTag($name, $value)
+ {
+ return array(
+ 'Key' => $name,
+ 'Value' => $value,
+ );
+ }
+} \ No newline at end of file
diff --git a/plugins/TestRunner/Commands/TestRunOnAws.php b/plugins/TestRunner/Commands/TestRunOnAws.php
new file mode 100644
index 0000000000..34a00cdae8
--- /dev/null
+++ b/plugins/TestRunner/Commands/TestRunOnAws.php
@@ -0,0 +1,141 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\TestRunner\Commands;
+
+use Piwik\Development;
+use Piwik\Plugin\ConsoleCommand;
+use Piwik\Plugins\TestRunner\Aws\Config;
+use Piwik\Plugins\TestRunner\Aws\Instance;
+use Piwik\Plugins\TestRunner\Aws\Ssh;
+use Piwik\Plugins\TestRunner\Runner\InstanceLauncher;
+use Piwik\Plugins\TestRunner\Runner\Remote;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class TestRunOnAws extends ConsoleCommand
+{
+ private $allowedTestSuites = array('integration', 'system', 'all', 'ui');
+
+ public function isEnabled()
+ {
+ return Development::isEnabled();
+ }
+
+ protected function configure()
+ {
+ $this->setName('tests:run-aws');
+ $this->addArgument('testsuite', InputArgument::OPTIONAL, 'Allowed values: ' . implode(', ', $this->allowedTestSuites));
+ $this->addOption('launch-only', null, InputOption::VALUE_NONE, 'Only launches an instance and outputs the connection parameters. Useful if you want to connect via SSH.');
+ $this->addOption('update-only', null, InputOption::VALUE_NONE, 'Launches an instance, outputs the connection parameters and prepares the instance for a test run but does not actually run the tests. It will also checkout the specified version.');
+ $this->addOption('one-instance-per-testsuite', null, InputOption::VALUE_NONE, 'Launches an instance, outputs the connection parameters and prepares the instance for a test run but does not actually run the tests. It will also checkout the specified version.');
+ $this->addOption('checkout', null, InputOption::VALUE_REQUIRED, 'Git hash, tag or branch to checkout. Defaults to current hash', $this->getCurrentGitHash());
+ $this->setDescription('Run a specific testsuite on AWS');
+ $this->setHelp('To use this command you have to configure the [tests]aws_* section in config/config.ini.php. See config/global.ini.php for all available options.
+
+To run a test simply specify the testsuite you want to run: <comment>./console tests:run-aws system</comment>. This will launch a new instance on AWS or reuse an already running one. We start one instance per keyname. This makes sure two different developers do not use the same instance at the same time.
+
+By default it will execute the tests of the git hash you are currently on. If this hash is not pushed yet or if you want to run tests of a specific git hash / branch / tag use the <comment>--checkout</comment> option: <comment>./console tests:run-aws --checkout="master" system</comment>.
+
+If you want to debug a problem and access the AWS instance using SSH you can specify the <comment>--launch-only</comment> or <comment>--update-only</comment> option.
+
+By default we will launch only one instance per keyname meaning you should not execute this command while another test is running. It would start the tests twice on the same instance and lead to errors. If you want to run two different testsuites at the same time (for instance <comment>system</comment> and <comment>ui</comment>) specify the <comment>one-instance-per-testsuite</comment> option. This will launch one instance for system tests and one for ui tests:
+<comment>./console tests:run-aws system</comment>
+<comment>./console tests:run-aws --one-instance-per-testsuite ui // will launch a new instance for ui testsuites</comment>
+');
+ }
+
+ /**
+ * Execute command like: ./console core:clear-caches
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $testSuite = $this->getTestSuite($input);
+ $launchOnly = $input->getOption('launch-only');
+ $updateOnly = $input->getOption('update-only');
+ $gitHash = $input->getOption('checkout');
+ $perTestsuite = $input->getOption('one-instance-per-testsuite');
+
+ if (empty($testSuite) && empty($launchOnly) && empty($updateOnly)) {
+ throw new \InvalidArgumentException('Either provide a testsuite argument or define <comment>--launch-only</comment> or <comment>--update-only</comment>');
+ }
+
+ $awsConfig = new Config();
+ $awsConfig->validate();
+
+ $host = $this->launchInstance($output, $perTestsuite, $awsConfig, $testSuite);
+
+ if ($launchOnly) {
+ return 0;
+ }
+
+ $ssh = Ssh::connectToAws($host, $awsConfig->getPemFile());
+ $ssh->setOutput($output);
+
+ $testRunner = new Remote($ssh);
+ $testRunner->updatePiwik($gitHash);
+
+ if ($updateOnly) {
+ $ssh->disconnect();
+
+ return 0;
+ }
+
+ $testRunner->runTests($host, $testSuite);
+
+ if (in_array($testSuite, array('system', 'all'))) {
+ $output->writeln("<info>Tests finished. You can browse processed files at </info><comment>http://$host/tests/PHPUnit/System/processed/</comment>");
+ } elseif ('ui' === $testSuite) {
+ $output->writeln("<info>Tests finished. You can browse processed screenshots at </info><comment>http://$host/tests/PHPUnit/UI/processed-ui-screenshots/</comment>");
+ } else {
+ $output->writeln("<info>Tests finished</info>");
+ }
+
+ $ssh->disconnect();
+ }
+
+ private function launchInstance(OutputInterface $output, $useOneInstancePerTestSuite, Config $awsConfig, $testSuite)
+ {
+ $awsInstance = new Instance($awsConfig, $testSuite);
+
+ if ($useOneInstancePerTestSuite) {
+ $awsInstance->enableUseOneInstancePerTestSuite();
+ }
+
+ $launcher = new InstanceLauncher($awsInstance);
+ $host = $launcher->launchOrResumeInstance();
+
+ $output->writeln(sprintf("Access instance using <comment>ssh -i %s ubuntu@%s</comment>", $awsConfig->getPemFile(), $host));
+ $output->writeln("You can log in to Piwik via root:secure at <comment>http://$host</comment>");
+ $output->writeln("You can access database via root:secure (<comment>mysql -uroot -psecure</comment>)");
+ $output->writeln("Files are located in <comment>~/www/piwik</comment>");
+ $output->writeln(' ');
+
+ return $host;
+ }
+
+ private function getTestSuite(InputInterface $input)
+ {
+ $testsuite = $input->getArgument('testsuite');
+
+ if (!empty($testsuite) && !in_array($testsuite, $this->allowedTestSuites)) {
+ throw new \InvalidArgumentException('Test suite argument is wrong, use one of following: ' . implode(', ', $this->allowedTestSuites));
+ }
+
+ return $testsuite;
+ }
+
+ private function getCurrentGitHash()
+ {
+ return trim(`git rev-parse HEAD`);
+ }
+
+}
diff --git a/plugins/TestRunner/Commands/TestsRun.php b/plugins/TestRunner/Commands/TestsRun.php
new file mode 100644
index 0000000000..e8b5e3ffec
--- /dev/null
+++ b/plugins/TestRunner/Commands/TestsRun.php
@@ -0,0 +1,206 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\CoreConsole\Commands;
+
+use Piwik\Common;
+use Piwik\Profiler;
+use Piwik\Plugin\ConsoleCommand;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Executes PHP tests.
+ */
+class TestsRun extends ConsoleCommand
+{
+ private $returnVar = 0;
+
+ protected function configure()
+ {
+ $this->setName('tests:run');
+ $this->setDescription('Run Piwik PHPUnit tests one testsuite after the other');
+ $this->addArgument('group', InputArgument::OPTIONAL, 'Run only a specific test group. Separate multiple groups by comma, for instance core,plugins', '');
+ $this->addOption('options', 'o', InputOption::VALUE_OPTIONAL, 'All options will be forwarded to phpunit', '');
+ $this->addOption('xhprof', null, InputOption::VALUE_NONE, 'Profile using xhprof.');
+ $this->addOption('file', null, InputOption::VALUE_REQUIRED, 'Execute tests within this file. Should be a path relative to the tests/PHPUnit directory.');
+ $this->addOption('testsuite', null, InputOption::VALUE_REQUIRED, 'Execute tests of a specific test suite, for instance UnitTests, IntegrationTests or SystemTests.');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $options = $input->getOption('options');
+ $groups = $input->getArgument('group');
+
+ $groups = explode(",", $groups);
+ $groups = array_filter($groups, 'strlen');
+
+ $command = '../../vendor/phpunit/phpunit/phpunit';
+
+ if (!$this->isCoverageEnabled($options) && $this->isXdebugLoaded()) {
+ $output->writeln('<comment>Did you know? You can run tests faster by disabling xdebug</comment>');
+ }
+
+ // force xdebug usage for coverage options
+ if ($this->isCoverageEnabled($options) && !$this->isXdebugLoaded()) {
+
+ $output->writeln('<info>xdebug extension required for code coverage.</info>');
+
+ $output->writeln('<info>searching for xdebug extension...</info>');
+
+ $extensionDir = shell_exec('php-config --extension-dir');
+ $xdebugFile = trim($extensionDir) . DIRECTORY_SEPARATOR . 'xdebug.so';
+
+ if (!file_exists($xdebugFile)) {
+
+ $dialog = $this->getHelperSet()->get('dialog');
+
+ $xdebugFile = $dialog->askAndValidate($output, 'xdebug not found. Please provide path to xdebug.so', function($xdebugFile) {
+ return file_exists($xdebugFile);
+ });
+ } else {
+
+ $output->writeln('<info>xdebug extension found in extension path.</info>');
+ }
+
+ $output->writeln("<info>using $xdebugFile as xdebug extension.</info>");
+
+ $phpunitPath = trim(shell_exec('which phpunit'));
+
+ $command = sprintf('php -d zend_extension=%s %s', $xdebugFile, $phpunitPath);
+ }
+
+ if ($input->getOption('xhprof')) {
+ Profiler::setupProfilerXHProf($isMainRun = true);
+
+ putenv('PIWIK_USE_XHPROF=1');
+ }
+
+ $testFile = $input->getOption('file');
+ if (!empty($testFile)) {
+ $this->executeTestFile($testFile, $options, $command, $output);
+ } else {
+ $suite = $this->getTestsuite($input);
+ $this->executeTestGroups($suite, $groups, $options, $command, $output);
+ }
+
+ return $this->returnVar;
+ }
+
+ private function executeTestFile($testFile, $options, $command, OutputInterface $output)
+ {
+ if ('/' !== substr($testFile, 0, 1)) {
+ $testFile = '../../' . $testFile;
+ }
+
+ $params = $options . " " . $testFile;
+ $this->executeTestRun($command, $params, $output);
+ }
+
+ private function executeTestGroups($suite, $groups, $options, $command, OutputInterface $output)
+ {
+ if (empty($suite) && empty($groups)) {
+ foreach ($this->getTestsSuites() as $suite) {
+ $suite = $this->buildTestSuiteName($suite);
+ $this->executeTestGroups($suite, $groups, $options, $command, $output);
+ }
+
+ return;
+ }
+
+ $params = $this->buildPhpUnitCliParams($suite, $groups, $options);
+
+ $this->executeTestRun($command, $params, $output);
+ }
+
+ private function executeTestRun($command, $params, OutputInterface $output)
+ {
+ $cmd = $this->getCommand($command, $params);
+ $output->writeln('Executing command: <info>' . $cmd . '</info>');
+ passthru($cmd, $returnVar);
+ $output->writeln("");
+
+ $this->returnVar += $returnVar;
+ }
+
+ private function getTestsSuites()
+ {
+ return array('unit', 'integration', 'system');
+ }
+
+ /**
+ * @param $command
+ * @param $params
+ * @return string
+ */
+ private function getCommand($command, $params)
+ {
+ return sprintf('cd %s/tests/PHPUnit && %s %s', PIWIK_DOCUMENT_ROOT, $command, $params);
+ }
+
+ private function buildPhpUnitCliParams($suite, $groups, $options)
+ {
+ $params = $options . " ";
+
+ if (!empty($groups)) {
+ $groups = implode(',', $groups);
+ $params .= '--group ' . $groups . ' ';
+ } else {
+ $groups = '';
+ }
+
+ if (!empty($suite)) {
+ $params .= ' --testsuite ' . $suite;
+ } else {
+ $suite = '';
+ }
+
+ $params = str_replace('%suite%', $suite, $params);
+ $params = str_replace('%group%', $groups, $params);
+
+ return $params;
+ }
+
+ private function getTestsuite(InputInterface $input)
+ {
+ $suite = $input->getOption('testsuite');
+
+ if (empty($suite)) {
+ return;
+ }
+
+ $availableSuites = $this->getTestsSuites();
+
+ if (!in_array($suite, $availableSuites)) {
+ throw new \InvalidArgumentException('Invalid testsuite specified. Use one of: ' . implode(', ', $availableSuites));
+ }
+
+ $suite = $this->buildTestSuiteName($suite);
+
+ return $suite;
+ }
+
+ private function buildTestSuiteName($suite)
+ {
+ return ucfirst($suite) . 'Tests';
+ }
+
+ private function isCoverageEnabled($options)
+ {
+ return false !== strpos($options, '--coverage');
+ }
+
+ private function isXdebugLoaded()
+ {
+ return extension_loaded('xdebug');
+ }
+
+} \ No newline at end of file
diff --git a/plugins/TestRunner/Commands/TestsRunUI.php b/plugins/TestRunner/Commands/TestsRunUI.php
new file mode 100644
index 0000000000..c3859579ed
--- /dev/null
+++ b/plugins/TestRunner/Commands/TestsRunUI.php
@@ -0,0 +1,83 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\CoreConsole\Commands;
+
+use Piwik\AssetManager;
+use Piwik\Plugin\ConsoleCommand;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class TestsRunUI extends ConsoleCommand
+{
+ protected function configure()
+ {
+ $this->setName('tests:run-ui');
+ $this->setDescription('Run screenshot tests');
+ $this->addArgument('specs', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Run only a specific test spec. Separate multiple specs by comma, for instance core,integration', array());
+ $this->addOption("persist-fixture-data", null, InputOption::VALUE_NONE, "Persist test data in a database and do not execute tear down.");
+ $this->addOption('keep-symlinks', null, InputOption::VALUE_NONE, "Keep recursive directory symlinks so test pages can be viewed in a browser.");
+ $this->addOption('print-logs', null, InputOption::VALUE_NONE, "Print webpage logs even if tests succeed.");
+ $this->addOption('drop', null, InputOption::VALUE_NONE, "Drop the existing database and re-setup a persisted fixture.");
+ $this->addOption('assume-artifacts', null, InputOption::VALUE_NONE, "Assume the diffviewer and processed screenshots will be stored on the builds artifacts server. For use with travis build.");
+ $this->addOption('plugin', null, InputOption::VALUE_REQUIRED, "Execute all tests for a plugin.");
+ $this->addOption('skip-delete-assets', null, InputOption::VALUE_NONE, "Skip deleting of merged assets (will speed up a test run, but not by a lot).");
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $specs = $input->getArgument('specs');
+ $persistFixtureData = $input->getOption("persist-fixture-data");
+ $keepSymlinks = $input->getOption('keep-symlinks');
+ $printLogs = $input->getOption('print-logs');
+ $drop = $input->getOption('drop');
+ $assumeArtifacts = $input->getOption('assume-artifacts');
+ $plugin = $input->getOption('plugin');
+ $skipDeleteAssets = $input->getOption('skip-delete-assets');
+
+ if (!$skipDeleteAssets) {
+ AssetManager::getInstance()->removeMergedAssets();
+ }
+
+ $options = array();
+ if ($persistFixtureData) {
+ $options[] = "--persist-fixture-data";
+ }
+
+ if ($keepSymlinks) {
+ $options[] = "--keep-symlinks";
+ }
+
+ if ($printLogs) {
+ $options[] = "--print-logs";
+ }
+
+ if ($drop) {
+ $options[] = "--drop";
+ }
+
+ if ($assumeArtifacts) {
+ $options[] = "--assume-artifacts";
+ }
+
+ if ($plugin) {
+ $options[] = "--plugin=" . $plugin;
+ }
+ $options = implode(" ", $options);
+
+ $specs = implode(" ", $specs);
+
+ $cmd = "phantomjs '" . PIWIK_INCLUDE_PATH . "/tests/lib/screenshot-testing/run-tests.js' $options $specs";
+
+ $output->writeln('Executing command: <info>' . $cmd . '</info>');
+ $output->writeln('');
+
+ passthru($cmd);
+ }
+}
diff --git a/plugins/TestRunner/Commands/TestsSetupFixture.php b/plugins/TestRunner/Commands/TestsSetupFixture.php
new file mode 100644
index 0000000000..8002eaaa7e
--- /dev/null
+++ b/plugins/TestRunner/Commands/TestsSetupFixture.php
@@ -0,0 +1,273 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\CoreConsole\Commands;
+
+use Piwik\Config;
+use Piwik\Plugin\ConsoleCommand;
+use Piwik\Url;
+use Piwik\Tests\Framework\Fixture;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Console commands that sets up a fixture either in a local MySQL database or a remote one.
+ *
+ * Examples:
+ *
+ * To setup a fixture provided by Piwik:
+ *
+ * ./console tests:setup-fixture UITestFixture
+ *
+ * To setup your own fixture created solely for test purposes and stored outside of Piwik:
+ *
+ * ./console tests:setup-fixture MyFixtureType --file=../devfixtures/MyFixtureType.php
+ *
+ * To setup a fixture or use existing data if present:
+ *
+ * ./console tests:setup-fixture UITestFixture --persist-fixture-data
+ *
+ * To re-setup a fixture that is already present:
+ *
+ * ./console tests:setup-fixture UITestFixture --persist-fixture-data --drop
+ *
+ * To create an SQL dump for a fixture:
+ *
+ * ./console tests:setup-fixture OmniFixture --sqldump=OmniFixtureDump.sql
+ */
+class TestsSetupFixture extends ConsoleCommand
+{
+ protected function configure()
+ {
+ $this->setName('tests:setup-fixture');
+ $this->setDescription('Create a database and fill it with data using a Piwik test fixture.');
+
+ $this->addArgument('fixture', InputArgument::REQUIRED,
+ "The class name of the fixture to apply. Doesn't need to have a namespace if it exists in the " .
+ "Piwik\\Tests\\Fixtures namespace.");
+
+ $this->addOption('db-name', null, InputOption::VALUE_REQUIRED,
+ "The name of the database that will contain the fixture data. This option is required to be set.");
+ $this->addOption('file', null, InputOption::VALUE_REQUIRED,
+ "The file location of the fixture. If this option is included the file will be required explicitly.");
+ $this->addOption('db-host', null, InputOption::VALUE_REQUIRED,
+ "The hostname of the MySQL database to use. Uses the default config value if not specified.");
+ $this->addOption('db-user', null, InputOption::VALUE_REQUIRED,
+ "The name of the MySQL user to use. Uses the default config value if not specified.");
+ $this->addOption('db-pass', null, InputOption::VALUE_REQUIRED,
+ "The MySQL user password to use. Uses the default config value if not specified.");
+ $this->addOption('teardown', null, InputOption::VALUE_NONE,
+ "If specified, the fixture will be torn down and the database deleted. Won't work if the --db-name " .
+ "option isn't supplied.");
+ $this->addOption('persist-fixture-data', null, InputOption::VALUE_NONE,
+ "If specified, the database will not be dropped after the fixture is setup. If the database already " .
+ "and the fixture was successfully setup before, nothing will happen.");
+ $this->addOption('drop', null, InputOption::VALUE_NONE,
+ "Forces the database to be dropped before setting up the fixture. Should be used in conjunction with" .
+ " --persist-fixture-data when updating a pre-existing test database.");
+ $this->addOption('sqldump', null, InputOption::VALUE_REQUIRED,
+ "Creates an SQL dump after setting up the fixture and outputs the dump to the file specified by this option.");
+ $this->addOption('save-config', null, InputOption::VALUE_NONE,
+ "Saves the current configuration file as a config for a new Piwik domain. For example save-config --piwik-domain=mytest.localhost.com will create "
+ . "a mytest.config.ini.php file in the config/ directory. Using /etc/hosts you can redirect to 127.0.0.1 and use the saved "
+ . "config.");
+ $this->addOption('set-phantomjs-symlinks', null, InputOption::VALUE_NONE,
+ "Used by UI tests. Creates symlinks to root directory in tests/PHPUnit/proxy.");
+ $this->addOption('server-global', null, InputOption::VALUE_REQUIRED,
+ "Used by UI tests. Sets the \$_SERVER global variable from a JSON string.");
+ $this->addOption('plugins', null, InputOption::VALUE_REQUIRED,
+ "Used by UI tests. Comma separated list of plugin names to activate and install when setting up a fixture.");
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $serverGlobal = $input->getOption('server-global');
+ if ($serverGlobal) {
+ $_SERVER = json_decode($serverGlobal, true);
+ }
+
+ $this->requireFixtureFiles($input);
+ $this->setIncludePathAsInTestBootstrap();
+
+ $host = Url::getHost();
+ if (empty($host)) {
+ $host = 'localhost';
+ Url::setHost('localhost');
+ }
+
+ $configDomainToSave = $input->getOption('save-config');
+ if (!empty($configDomainToSave)) {
+ $pathToDomainConfig = PIWIK_INCLUDE_PATH . '/config/' . $host . '.config.ini.php';
+
+ if (!file_exists($pathToDomainConfig)) {
+ link(PIWIK_INCLUDE_PATH . '/config/config.ini.php', $pathToDomainConfig);
+ }
+ }
+
+ $fixture = $this->createFixture($input);
+
+ $this->setupDatabaseOverrides($input, $fixture);
+
+ // perform setup and/or teardown
+ if ($input->getOption('teardown')) {
+ $fixture->getTestEnvironment()->save();
+ $fixture->performTearDown();
+ } else {
+ $fixture->performSetUp();
+ }
+
+ if ($input->getOption('set-phantomjs-symlinks')) {
+ $this->createSymbolicLinksForUITests();
+ }
+
+ $this->writeSuccessMessage($output, array("Fixture successfully setup!"));
+
+ $sqlDumpPath = $input->getOption('sqldump');
+ if ($sqlDumpPath) {
+ $this->createSqlDump($sqlDumpPath, $output);
+ }
+
+ if (!empty($configDomainToSave)) {
+ Config::getInstance()->forceSave();
+ }
+ }
+
+ private function createSymbolicLinksForUITests()
+ {
+ // make sure symbolic links exist (phantomjs doesn't support symlink-ing yet)
+ foreach (array('libs', 'plugins', 'tests', 'piwik.js') as $linkName) {
+ $linkPath = PIWIK_INCLUDE_PATH . '/tests/PHPUnit/proxy/' . $linkName;
+ if (!file_exists($linkPath)) {
+ symlink(PIWIK_INCLUDE_PATH . '/' . $linkName, $linkPath);
+ }
+ }
+ }
+
+ private function createSqlDump($sqlDumpPath, OutputInterface $output)
+ {
+ $output->writeln("<info>Creating SQL dump...</info>");
+
+ $databaseConfig = Config::getInstance()->database;
+ $dbUser = $databaseConfig['username'];
+ $dbPass = $databaseConfig['password'];
+ $dbHost = $databaseConfig['host'];
+ $dbName = $databaseConfig['dbname'];
+
+ $command = "mysqldump --user='$dbUser' --password='$dbPass' --host='$dbHost' '$dbName' > '$sqlDumpPath'";
+ $output->writeln("<info>Executing $command...</info>");
+ passthru($command);
+
+ $this->writeSuccessMessage($output, array("SQL dump created!"));
+ }
+
+ private function setupDatabaseOverrides(InputInterface $input, Fixture $fixture)
+ {
+ $testingEnvironment = $fixture->getTestEnvironment();
+
+ $optionsToOverride = array(
+ 'dbname' => $fixture->getDbName(),
+ 'host' => $input->getOption('db-host'),
+ 'username' => $input->getOption('db-user'),
+ 'password' => $input->getOption('db-pass')
+ );
+ foreach ($optionsToOverride as $configOption => $value) {
+ if ($value) {
+ $configOverride = $testingEnvironment->configOverride;
+ $configOverride['database_tests'][$configOption] = $configOverride['database'][$configOption] = $value;
+ $testingEnvironment->configOverride = $configOverride;
+
+ Config::getInstance()->database[$configOption] = $value;
+ }
+ }
+ }
+
+ private function createFixture(InputInterface $input)
+ {
+ $fixtureClass = $input->getArgument('fixture');
+ if (class_exists("Piwik\\Tests\\Fixtures\\" . $fixtureClass)) {
+ $fixtureClass = "Piwik\\Tests\\Fixtures\\" . $fixtureClass;
+ }
+
+ if (!class_exists($fixtureClass)) {
+ throw new \Exception("Cannot find fixture class '$fixtureClass'.");
+ }
+
+ $fixture = new $fixtureClass();
+ $fixture->printToScreen = true;
+
+ $dbName = $input->getOption('db-name');
+ if ($dbName) {
+ $fixture->dbName = $dbName;
+ }
+
+ if ($input->getOption('persist-fixture-data')) {
+ $fixture->persistFixtureData = true;
+ }
+
+ if ($input->getOption('drop')) {
+ $fixture->resetPersistedFixture = true;
+ }
+
+ $extraPluginsToLoad = $input->getOption('plugins');
+ if ($extraPluginsToLoad) {
+ $fixture->extraPluginsToLoad = explode(',', $extraPluginsToLoad);
+ }
+
+ if ($fixture->createConfig) {
+ Config::getInstance()->setTestEnvironment($pathLocal = null, $pathGlobal = null, $pathCommon = null, $allowSaving = true);
+ }
+
+ $fixture->createConfig = false;
+
+ return $fixture;
+ }
+
+ private function requireFixtureFiles(InputInterface $input)
+ {
+ require_once PIWIK_INCLUDE_PATH . '/libs/PiwikTracker/PiwikTracker.php';
+ require_once PIWIK_INCLUDE_PATH . '/tests/PHPUnit/FakeAccess.php';
+ require_once PIWIK_INCLUDE_PATH . '/tests/PHPUnit/TestingEnvironment.php';
+ require_once PIWIK_INCLUDE_PATH . '/tests/PHPUnit/IntegrationTestCase.php';
+
+ $fixturesToLoad = array(
+ '/tests/PHPUnit/UI/Fixtures/*.php',
+ '/plugins/*/tests/Fixtures/*.php',
+ '/plugins/*/Test/Fixtures/*.php',
+ );
+ foreach($fixturesToLoad as $fixturePath) {
+ foreach (glob(PIWIK_INCLUDE_PATH . $fixturePath) as $file) {
+ require_once $file;
+ }
+ }
+
+ $file = $input->getOption('file');
+ if ($file) {
+ if (is_file($file)) {
+ require_once $file;
+ } else if (is_file(PIWIK_INCLUDE_PATH . '/' . $file)) {
+ require_once PIWIK_INCLUDE_PATH . '/' . $file;
+ } else {
+ throw new \Exception("Cannot find --file option file '$file'.");
+ }
+ }
+ }
+
+ private function setIncludePathAsInTestBootstrap()
+ {
+ if (!defined('PIWIK_INCLUDE_SEARCH_PATH')) {
+ define('PIWIK_INCLUDE_SEARCH_PATH', get_include_path()
+ . PATH_SEPARATOR . PIWIK_INCLUDE_PATH . '/core'
+ . PATH_SEPARATOR . PIWIK_INCLUDE_PATH . '/libs'
+ . PATH_SEPARATOR . PIWIK_INCLUDE_PATH . '/plugins');
+ }
+ @ini_set('include_path', PIWIK_INCLUDE_SEARCH_PATH);
+ @set_include_path(PIWIK_INCLUDE_SEARCH_PATH);
+ }
+}
diff --git a/plugins/TestRunner/README.md b/plugins/TestRunner/README.md
new file mode 100644
index 0000000000..4005bb93b7
--- /dev/null
+++ b/plugins/TestRunner/README.md
@@ -0,0 +1,18 @@
+# Piwik TestRunner Plugin
+
+## Description
+
+Add your plugin description here.
+
+## FAQ
+
+__My question?__
+My answer
+
+## Changelog
+
+Here goes the changelog text.
+
+## Support
+
+Please direct any feedback to ... \ No newline at end of file
diff --git a/plugins/TestRunner/Runner/InstanceLauncher.php b/plugins/TestRunner/Runner/InstanceLauncher.php
new file mode 100644
index 0000000000..4f9faf97c7
--- /dev/null
+++ b/plugins/TestRunner/Runner/InstanceLauncher.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\TestRunner\Runner;
+
+use Piwik\Plugins\TestRunner\Aws\Instance;
+
+class InstanceLauncher {
+
+ /**
+ * @var Instance
+ */
+ private $instance;
+
+ public function __construct(Instance $instance)
+ {
+ $this->instance = $instance;
+ }
+
+ public function launchOrResumeInstance()
+ {
+ $host = $this->instance->findExisting();
+
+ if (empty($host)) {
+ $host = $this->launchInstance();
+ }
+
+ return $host;
+ }
+
+ private function launchInstance()
+ {
+ $instanceIds = $this->instance->launch();
+
+ try {
+ $host = $this->instance->setup($instanceIds);
+ } catch (\Exception $e) {
+ $this->instance->terminate($instanceIds);
+
+ throw new \RuntimeException('We failed to launch a new instance so we terminated it directly. Try again! Error Message: ' . $e->getMessage());
+ }
+
+ return $host;
+ }
+
+} \ No newline at end of file
diff --git a/plugins/TestRunner/Runner/Remote.php b/plugins/TestRunner/Runner/Remote.php
new file mode 100644
index 0000000000..d068aa2f93
--- /dev/null
+++ b/plugins/TestRunner/Runner/Remote.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\TestRunner\Runner;
+
+use \Net_SSH2;
+
+class Remote
+{
+ /**
+ * @var \Net_SSH2
+ */
+ private $ssh;
+
+ public function __construct(Net_SSH2 $ssh)
+ {
+ $this->ssh = $ssh;
+ }
+
+ public function updatePiwik($gitHash)
+ {
+ $this->ssh->exec('git reset --hard');
+ $this->ssh->exec('git clean -d -f');
+ $this->ssh->exec('git fetch --all');
+ $this->ssh->exec('git checkout ' . trim($gitHash));
+ $this->ssh->exec('sudo composer.phar self-update');
+ $this->ssh->exec('composer.phar install');
+ }
+
+ public function runTests($host, $testSuite)
+ {
+ $this->prepareTestRun($host);
+ $this->printVersionInfo();
+ $this->doRunTests($testSuite);
+ }
+
+ private function prepareTestRun($host)
+ {
+ $this->ssh->exec('cp ./tests/PHPUnit/phpunit.xml.dist ./tests/PHPUnit/phpunit.xml');
+ $this->ssh->exec("sed -i 's/@REQUEST_URI@/\\//g' ./tests/PHPUnit/phpunit.xml");
+ $this->ssh->exec("sed -i 's/amazonAwsUrl/$host/g' ./config/config.ini.php");
+ }
+
+ private function printVersionInfo()
+ {
+ $this->ssh->exec('php --version');
+ $this->ssh->exec('mysql --version');
+ $this->ssh->exec('phantomjs --version');
+ }
+
+ private function doRunTests($testSuite)
+ {
+ if ('all' === $testSuite) {
+ $this->ssh->exec('php console tests:run --options="--colors"');
+ } elseif ('ui' === $testSuite) {
+ $this->ssh->exec('php console tests:run-ui');
+ } else {
+ $this->ssh->exec('php console tests:run --options="--colors" --testsuite="unit"');
+ $this->ssh->exec('php console tests:run --options="--colors" --testsuite="' . $testSuite . '"');
+ }
+ }
+}
diff --git a/plugins/TestRunner/TestRunner.php b/plugins/TestRunner/TestRunner.php
new file mode 100644
index 0000000000..fc5462e536
--- /dev/null
+++ b/plugins/TestRunner/TestRunner.php
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\TestRunner;
+
+/**
+ */
+class TestRunner extends \Piwik\Plugin
+{
+}
diff --git a/plugins/TestRunner/plugin.json b/plugins/TestRunner/plugin.json
new file mode 100644
index 0000000000..b56ff70a21
--- /dev/null
+++ b/plugins/TestRunner/plugin.json
@@ -0,0 +1,19 @@
+{
+ "name": "TestRunner",
+ "version": "0.1.0",
+ "description": "Run Tests",
+ "theme": false,
+ "require": {
+ "piwik": ">=2.8.1-rc1"
+ },
+ "authors": [
+ {
+ "name": "Piwik",
+ "email": "hello@piwik.org",
+ "homepage": "http://piwik.org"
+ }
+ ],
+ "license": "GPL v3+",
+ "keywords": ["test", "runner"],
+ "homepage": "http://piwik.org"
+} \ No newline at end of file
diff --git a/plugins/TestRunner/screenshots/.gitkeep b/plugins/TestRunner/screenshots/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/plugins/TestRunner/screenshots/.gitkeep