diff options
author | Matthieu Aubry <matt@piwik.org> | 2014-08-11 04:22:30 +0400 |
---|---|---|
committer | Matthieu Aubry <matt@piwik.org> | 2014-08-11 04:22:30 +0400 |
commit | f69187c3633ee951c9c7643b03492474ab420c14 (patch) | |
tree | 8ee9f378e5075715507f7eed3c33a5a6741a5dd6 /plugins | |
parent | 487d0cdd0f80c56ceb78903473feb638841a2298 (diff) | |
parent | 63eaed6f7cfd30fde46724018a67fb8712bbb2d6 (diff) |
Merge pull request #5962 from piwik/travis_system
Created unified system for running tests in travis for Piwik Core and any Piwik plugin.
Diffstat (limited to 'plugins')
-rw-r--r-- | plugins/CoreConsole/Commands/GenerateTravisYmlFile.php | 112 | ||||
-rw-r--r-- | plugins/CoreConsole/TravisYmlView.php | 238 | ||||
-rw-r--r-- | plugins/CoreConsole/templates/travis.yml.twig | 153 |
3 files changed, 503 insertions, 0 deletions
diff --git a/plugins/CoreConsole/Commands/GenerateTravisYmlFile.php b/plugins/CoreConsole/Commands/GenerateTravisYmlFile.php new file mode 100644 index 0000000000..dc5ccf306f --- /dev/null +++ b/plugins/CoreConsole/Commands/GenerateTravisYmlFile.php @@ -0,0 +1,112 @@ +<?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\View; +use Piwik\Plugin\ConsoleCommand; +use Piwik\Plugins\CoreConsole\TravisYmlView; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Output\OutputInterface; +use Exception; + +/** + * Command to generate an self-updating .travis.yml file either for Piwik Core or + * an individual Piwik plugin. + */ +class GenerateTravisYmlFile extends ConsoleCommand +{ + protected function configure() + { + $this->setName('generate:travis-yml') + ->setDescription('Generates a .travis.yml file for a plugin. The file can be auto-updating based on the parameters supplied.') + ->addOption('plugin', null, InputOption::VALUE_REQUIRED, 'The plugin for whom a .travis.yml file should be generated.') + ->addOption('core', null, InputOption::VALUE_NONE, 'Supplied when generating the .travis.yml file for Piwik core.' + . ' Should only be used by core developers.') + ->addOption('artifacts-pass', null, InputOption::VALUE_REQUIRED, + "Password to the Piwik build artifacts server. Will be encrypted in the .travis.yml file.") + ->addOption('github-token', null, InputOption::VALUE_REQUIRED, + "Github token of a user w/ push access to this repository. Used to auto-commit updates to the " + . ".travis.yml file and checkout dependencies. Will be encrypted in the .travis.yml file.\n\n" + . "If not supplied, the .travis.yml will fail the build if it needs updating.") + ->addOption('dump', null, InputOption::VALUE_REQUIRED, "Debugging option. Saves the output .travis.yml to the specified file."); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $targetPlugin = $input->getOption('plugin'); + $artifactsPass = $input->getOption('artifacts-pass'); + $githubToken = $input->getOption('github-token'); + $outputYmlPath = $this->getTravisYmlOutputPath($input, $targetPlugin); + $thisConsoleCommand = $this->getExecutedConsoleCommandForTravis($input); + + $view = new TravisYmlView(); + $view->processExistingTravisYml($outputYmlPath); + $view->configure($targetPlugin, $artifactsPass, $githubToken, $thisConsoleCommand, $output); + $travisYmlContents = $view->render(); + + $writePath = $input->getOption('dump'); + if (empty($writePath)) { + $writePath = $outputYmlPath; + } + + file_put_contents($writePath, $travisYmlContents); + + $this->writeSuccessMessage($output, array("Generated .travis.yml file at '$writePath'!")); + } + + private function getTravisYmlOutputPath(InputInterface $input, $targetPlugin) + { + if ($input->getOption('core')) { + return PIWIK_INCLUDE_PATH . '/.travis.yml'; + } else if ($targetPlugin) { + $pluginDirectory = PIWIK_INCLUDE_PATH . '/plugins/' . $targetPlugin; + if (!is_writable($pluginDirectory)) { + throw new Exception("Cannot write to '$pluginDirectory'!"); + } + + return $pluginDirectory . '/.travis.yml'; + } else { + throw new Exception("Neither --plugin option or --core option specified; don't know where to generate .travis.yml." + . " Execute './console help generate:travis-yml' for more info"); + } + } + + private function getExecutedConsoleCommandForTravis(InputInterface $input) + { + $command = "php ./console " . $this->getName(); + + $arguments = $input->getOptions(); + if (isset($arguments['github-token'])) { + $arguments['github-token'] = '$GITHUB_USER_TOKEN'; + } + if (isset($arguments['artifacts-pass'])) { + $arguments['artifacts-pass'] = '$ARTIFACTS_PASS'; + } + unset($arguments['dump']); + + foreach ($arguments as $name => $value) { + if ($value === false + || $value === null + ) { + continue; + } + + if ($value === true) { + $command .= " --$name"; + } else { + $command .= " --$name=\"". addslashes($value) . "\""; + } + } + + return $command; + } +}
\ No newline at end of file diff --git a/plugins/CoreConsole/TravisYmlView.php b/plugins/CoreConsole/TravisYmlView.php new file mode 100644 index 0000000000..ee74d7c6ad --- /dev/null +++ b/plugins/CoreConsole/TravisYmlView.php @@ -0,0 +1,238 @@ +<?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; + +use Piwik\View; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * View class for the travis.yml.twig template file. Generates the contents for a .travis.yml file. + */ +class TravisYmlView extends View +{ + /** + * The .travis.yml section names that are overwritten by this command. + * + * @var string[] + */ + private static $travisYmlSectionNames = array( + 'php', + 'language', + 'script', + 'before_install', + 'install', + 'before_script', + 'after_script', + 'after_success' + ); + + /** + * Constructor. + */ + public function __construct() + { + parent::__construct("@CoreConsole/travis.yml"); + } + + /** + * Parse existing data in a .travis.yml file that should be preserved in the output .travis.yml. + * Includes comments. + * + * @var string $existingYmlPath The path to the existing .travis.yml file. + */ + public function processExistingTravisYml($existingYmlPath) + { + if (!file_exists($existingYmlPath)) { + return; + } + + $existingYamlText = file_get_contents($existingYmlPath); + foreach ($this->getRootSectionsFromYaml($existingYamlText) as $sectionName => $offset) { + $section = $this->getRootSectionText($existingYamlText, $offset); + if ($sectionName == 'env') { + $this->existingEnv = $section; + } else if ($sectionName == 'matrix') { + $this->existingMatrix = $section; + } else if (!in_array($sectionName, self::$travisYmlSectionNames)) { + $this->extraSections .= "\n\n$sectionName:" . $section; + } + } + } + + /** + * Configures the view for generation. + * + * @param string|null $targetPlugin The plugin target or `null` if generating for core. + * @param string|null $artifactsPass The password for the builds artifacts server. Encrypted in output. + * @param string $generateYmlCommand The command to use in travis when checking if a .travis.yml file is out + * of date. + * @param OutputInterface $output OutputInterface to output warnings and the like. + */ + public function configure($targetPlugin, $artifactsPass, $githubToken, $generateYmlCommand, OutputInterface $output) + { + $this->pluginName = $targetPlugin; + + if (empty($this->existingEnv)) { + $artifactsPass = $artifactsPass; + if (!empty($artifactsPass)) { + $this->artifactsPass = $this->travisEncrypt("ARTIFACTS_PASS=" . $artifactsPass, $output); + } + + $githubToken = $githubToken; + if (!empty($githubToken)) { + $this->githubToken = $this->travisEncrypt("GITHUB_USER_TOKEN=" . $githubToken, $output); + } + } else { + $output->writeln("<info>Existing .yml files found, ignoring global variables specified on command line.</info>"); + } + + list($this->testsToRun, $this->testsToExclude) = $this->getTestsToRun(); + + $this->consoleCommand = $generateYmlCommand; + } + + /** + * Extracts the name and offset of all root elements of a YAML document. This method does this by + * checking for text that starts at the beginning of a line and ends with a ':'. + * + * @param string $yamlText The YAML text to search through. + * @return array Array mapping string section names with the starting offset of the text in the YAML. + */ + private function getRootSectionsFromYaml($yamlText) + { + preg_match_all("/^[a-zA-Z_]+:/m", $yamlText, $allMatches, PREG_OFFSET_CAPTURE); + + $result = array(); + foreach ($allMatches[0] as $match) { + $matchLength = strlen($match[0]); + $sectionName = substr($match[0], 0, $matchLength - 1); + + $result[$sectionName] = $match[1] + $matchLength; + } + return $result; + } + + /** + * Gets the text of a root YAML element in a YAML doc using the name of the element and the starting + * offset of the element's text. This is accomplished by searching for the first line that doesn't + * start with whitespace after the given offset and using the text between the given offset and the + * line w/o starting whitespace. + * + * @param string $yamlText The YAML text to search through. + * @param int $offset The offset start of the YAML text (does not include the element name and colon, ie + * the offset is after `'element:'`). + * @return string + */ + private function getRootSectionText($yamlText, $offset) + { + preg_match("/^[^\s]/m", $yamlText, $endMatches, PREG_OFFSET_CAPTURE, $offset); + + $endPos = isset($endMatches[0][1]) ? $endMatches[0][1] : strlen($yamlText); + + return substr($yamlText, $offset, $endPos - $offset); + } + + private function travisEncrypt($data, OutputInterface $output) + { + $output->writeln("Encrypting \"$data\"..."); + + $command = "travis encrypt \"$data\""; + + // change dir to target plugin since plugin will be in its own git repo + if (!empty($this->pluginName)) { + $command = "cd \"" . $this->getPluginRootFolder() . "\" && " . $command; + } + + exec($command, $output, $returnCode); + if ($returnCode !== 0) { + throw new Exception("Cannot encrypt \"$data\" for travis! Please make sure you have the travis command line " + . "utility installed (see http://blog.travis-ci.com/2013-01-14-new-client/).\n\n" + . "return code: $returnCode\n\n" + . "travis output:\n\n" . implode("\n", $output)); + } + + if (empty($output)) { + throw new Exception("Cannot parse travis encrypt output:\n\n" . implode("\n", $output)); + } + + // when not executed from a command line travis encrypt will return only the encrypted data + $encryptedData = $output[0]; + if (substr($encryptedData, 0, 1) == '"') { + $encryptedData = substr($encryptedData, 1); + } + if (substr($encryptedData, -1) == '"') { + $encryptedData = substr($encryptedData, 0, strlen($encryptedData) - 1); + } + + return $encryptedData; + } + + private function getTestsToRun() + { + $testsToRun = array(); + $testsToExclude = array(); + + if ($this->isTargetPluginContainsPluginTests()) { + $testsToRun[] = array('name' => 'PluginTests', + 'vars' => "MYSQL_ADAPTER=PDO_MYSQL"); + $testsToRun[] = array('name' => 'PluginTests', + 'vars' => "MYSQL_ADAPTER=PDO_MYSQL TEST_AGAINST_CORE=latest_stable"); + + $testsToExclude[] = array('description' => 'execute latest stable tests only w/ PHP 5.5', + 'php' => '5.3', + 'env' => 'TEST_SUITE=PluginTests MYSQL_ADAPTER=PDO_MYSQL TEST_AGAINST_CORE=latest_stable'); + $testsToExclude[] = array('php' => '5.4', + 'env' => 'TEST_SUITE=PluginTests MYSQL_ADAPTER=PDO_MYSQL TEST_AGAINST_CORE=latest_stable'); + } + if ($this->isTargetPluginContainsUITests()) { + $testsToRun[] = array('name' => 'UITests', + 'vars' => "MYSQL_ADAPTER=PDO_MYSQL"); + + $testsToExclude[] = array('description' => 'execute UI tests only w/ PHP 5.5', + 'php' => '5.3', + 'env' => 'TEST_SUITE=UITests MYSQL_ADAPTER=PDO_MYSQL'); + $testsToExclude[] = array('php' => '5.4', + 'env' => 'TEST_SUITE=UITests MYSQL_ADAPTER=PDO_MYSQL'); + } + + return array($testsToRun, $testsToExclude); + } + + private function isTargetPluginContainsPluginTests() + { + $pluginPath = $this->getPluginRootFolder(); + return $this->doesFolderContainPluginTests($pluginPath . "/tests") + || $this->doesFolderContainPluginTests($pluginPath . "/Test"); + } + + private function doesFolderContainPluginTests($folderPath) + { + $testFiles = glob($folderPath . "/**/*Test.php"); + return !empty($testFiles); + } + + private function isTargetPluginContainsUITests() + { + $pluginPath = $this->getPluginRootFolder(); + return $this->doesFolderContainUITests($pluginPath . "/tests") + || $this->doesFolderContainUITests($pluginPath . "/Test"); + } + + private function doesFolderContainUITests($folderPath) + { + $testFiles = glob($folderPath . "/**/*_spec.js"); + return !empty($testFiles); + } + + private function getPluginRootFolder() + { + return PIWIK_INCLUDE_PATH . "/plugins/{$this->pluginName}"; + } +}
\ No newline at end of file diff --git a/plugins/CoreConsole/templates/travis.yml.twig b/plugins/CoreConsole/templates/travis.yml.twig new file mode 100644 index 0000000000..6d25790458 --- /dev/null +++ b/plugins/CoreConsole/templates/travis.yml.twig @@ -0,0 +1,153 @@ +# do not edit this file manually, instead run the generate:travis-yml console command + +language: php + +# We want to test against PHP 5.3.3/5.4/5.5 +php: + - 5.5 + - 5.4 + - 5.3.3 +# - 5.6 +# - hhvm + +# Separate different test suites +{% if existingEnv|default is empty -%} +env: + global: + - PLUGIN_NAME={{ pluginName|raw }} +{% if pluginName is empty %} + - PIWIK_ROOT_DIR=$TRAVIS_BUILD_DIR +{% else %} + - PIWIK_ROOT_DIR=$TRAVIS_BUILD_DIR/piwik +{% endif %} + {% if artifactsPass|default is not empty %}secure: "{{ artifactsPass|raw }}"{% endif %} + {% if githubToken|default is not empty %}secure: "{{ githubToken|raw }}"{% endif %} + matrix: +{% for test in testsToRun %} - TEST_SUITE={{ test.name|raw }} {{ test.vars|raw }} +{% endfor %} +{%- else -%} +env: + {{ existingEnv|trim|raw }} +{% endif %} + +{% if existingMatrix|default is empty -%} +{%- if testsToExclude is not empty -%} +matrix: + exclude: +{% for testExclude in testsToExclude %} +{% if testExclude.description|default is not empty %} # {{ testExclude.description|raw }} +{% endif %} + - php: {{ testExclude.php|raw }} + env: {{ testExclude.env|raw }} +{% endfor %} +{%- endif -%} +{%- else -%} +matrix: + {{ existingMatrix|trim|raw }} +{% endif %} + +script: $PIWIK_ROOT_DIR/tests/travis/travis.sh + +before_install: + # do not use the Zend allocator on PHP 5.3 since it will randomly segfault after program execution + - '[[ "$TRAVIS_PHP_VERSION" == 5.3* ]] && export USE_ZEND_ALLOC=0 || true' +{% if pluginName is not empty %} + +install: + # move all contents of current repo (which contains the plugin) to a new directory + - mkdir $PLUGIN_NAME + - cp -R !($PLUGIN_NAME) $PLUGIN_NAME + - cp -R .git/ $PLUGIN_NAME/ + + # checkout piwik in the current directory + - git clone https://github.com/piwik/piwik.git piwik + - cd piwik + - git fetch --all + - | + if [ "$TEST_AGAINST_PIWIK_BRANCH" == "" ]; then + if [ "$TEST_AGAINST_CORE" == "latest_stable" ]; then + export TEST_AGAINST_PIWIK_BRANCH=$(git describe --tags `git rev-list --tags --max-count=1`) + export TEST_AGAINST_PIWIK_BRANCH=`echo $TEST_AGAINST_PIWIK_BRANCH | tr -d ' ' | tr -d '\n'` + else + export TEST_AGAINST_PIWIK_BRANCH=master + fi + fi + - echo "Testing against '$TEST_AGAINST_PIWIK_BRANCH'" + - git checkout "$TEST_AGAINST_PIWIK_BRANCH" + - git submodule init + - git submodule update || true + + # move plugin contents to folder in the plugins subdirectory + - rm -rf plugins/$PLUGIN_NAME + - mv ../$PLUGIN_NAME plugins +{% endif %} + +before_script: + - ./tests/travis/configure_git.sh + + # print out mysql information + - mysql --version + - mysql -e "SELECT VERSION();" + + # configure mysql + - mysql -e "SET GLOBAL sql_mode = 'NO_ENGINE_SUBSTITUTION'" # Travis default + + # Uncomment to enable sql_mode STRICT_TRANS_TABLES (new default in Mysql 5.6) + #- mysql -e "SET GLOBAL sql_mode = 'NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES'" + - mysql -e "SELECT @@sql_mode;" + - mysql -e "SHOW GLOBAL VARIABLES;" +{% if pluginName is empty %} + + # Start UI tests + - ./tests/travis/initiate_ui_tests.sh +{%- endif %} + + # travis now complains about this failing 9 times out of 10, so removing it. hopefully the random failures it prevented won't come back + # - travis_retry composer self-update + + - travis_retry composer install + + # print out more debugging info + - uname -a + - date + - php -r "var_dump(gd_info());" + - mysql -e 'create database piwik_tests;' + + # Make sure we use Python 2.6 + - travis_retry sudo add-apt-repository ppa:fkrull/deadsnakes -y + - travis_retry sudo apt-get update + - travis_retry sudo apt-get install python2.6 python2.6-dev + + # Log Analytics works with Python 2.6 or 2.7 but we want to test on 2.6 + - python2.6 --version + - python --version + + - ./tests/travis/prepare.sh + - ./tests/travis/setup_webserver.sh + + - export GENERATE_TRAVIS_YML_COMMAND='{{ consoleCommand|raw }}' + - ./tests/travis/autoupdate_travis_yml.sh + + - cd tests/PHPUnit + +after_script: + # change directory back to root travis dir + - cd $PIWIK_ROOT_DIR + + # output contents of files w/ debugging info to screen + - cat /var/log/nginx/error.log + - cat $PIWIK_ROOT_DIR/tmp/php-fpm.log + - cat $PIWIK_ROOT_DIR/tmp/logs/piwik.log + - cat $PIWIK_ROOT_DIR/config/config.ini.php + + # upload test artifacts (for debugging travis failures) + - ./tests/travis/upload_artifacts.sh + +after_success: + - cd $PIWIK_ROOT_DIR +{% if pluginName is empty %} - ./tests/travis/generate_docs.sh{%- endif %} + + +{% if extraSections|default is not empty %} +{{ extraSections|trim|raw }} +{%- endif -%}
\ No newline at end of file |