Skip to content
Extraits de code Groupes Projets
SystemTestCase.php 20 ko
Newer Older
 * 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\Tests\Framework\TestCase;
use Exception;
use Piwik\ArchiveProcessor\Rules;
use Piwik\Config;
use Piwik\DataAccess\ArchiveTableCreator;
mattab's avatar
mattab a validé
use Piwik\DbHelper;
use Piwik\Tests\Framework\TestRequest\ApiTestConfig;
use Piwik\Tests\Framework\TestRequest\Collection;
use Piwik\Tests\Framework\TestRequest\Response;
use PHPUnit_Framework_TestCase;
require_once PIWIK_INCLUDE_PATH . '/libs/PiwikTracker/PiwikTracker.php';

/**
 *
 * Provides helpers to track data and then call API get* methods to check outputs automatically.
 *
abstract class SystemTestCase extends PHPUnit_Framework_TestCase
diosmosis's avatar
diosmosis a validé
     * Identifies the last language used in an API/Controller call.
diosmosis's avatar
diosmosis a validé
     * @var string
diosmosis's avatar
diosmosis a validé
    protected $lastLanguage;

    protected $missingExpectedFiles = array();
    protected $comparisonFailures = array();

diosmosis's avatar
diosmosis a validé
    public static function setUpBeforeClass()
diosmosis's avatar
diosmosis a validé
        if (!isset(static::$fixture)) {
            $fixture = new Fixture();
        } else {
            $fixture = static::$fixture;
        $fixture->testCaseClass = get_called_class();

diosmosis's avatar
diosmosis a validé
        try {
diosmosis's avatar
diosmosis a validé
        } catch (Exception $e) {
            static::fail("Failed to setup fixture: " . $e->getMessage() . "\n" . $e->getTraceAsString());
diosmosis's avatar
diosmosis a validé
    public static function tearDownAfterClass()
diosmosis's avatar
diosmosis a validé
        if (!isset(static::$fixture)) {
            $fixture = new Fixture();
        } else {
            $fixture = static::$fixture;
    /**
     * Returns true if continuous integration running this request
     * Useful to exclude tests which may fail only on this setup
     */
    public static function isTravisCI()
    {
        $travis = getenv('TRAVIS');
        return !empty($travis);
    }

    public static function isPhpVersion53()
    {
        return strpos(PHP_VERSION, '5.3') === 0;
    }

    public static function isMysqli()
    {
        return getenv('MYSQL_ADAPTER') == 'MYSQLI';
    }

    protected function alertWhenImagesExcludedFromTests()
    {
diosmosis's avatar
diosmosis a validé
        if (!Fixture::canImagesBeIncludedInScheduledReports()) {
mattab's avatar
mattab a validé
                'Scheduled reports generated during integration tests will not contain the image graphs. ' .
                    'For tests to generate images, use a machine with the following specifications : ' .
diosmosis's avatar
diosmosis a validé
                    'OS = '.Fixture::IMAGES_GENERATED_ONLY_FOR_OS.', PHP = '.Fixture::IMAGES_GENERATED_FOR_PHP .
                    ' and GD = ' . Fixture::IMAGES_GENERATED_FOR_GD
            );
        }
    }

    /**
     * Return 4 Api Urls for testing scheduled reports :
     * - one in HTML format with all available reports
     * - one in PDF format with all available reports
     * - two in SMS (one for each available report: MultiSites.getOne & MultiSites.getAll)
     *
     * @param string $dateTime eg '2010-01-01 12:34:56'
     * @param string $period eg 'day', 'week', 'month', 'year'
sgiehl's avatar
sgiehl a validé
     * @return array
     */
    protected static function getApiForTestingScheduledReports($dateTime, $period)
    {
        $apiCalls = array();

        // HTML Scheduled Report
        array_push(
            $apiCalls,
            array(
mattab's avatar
mattab a validé
                'ScheduledReports.generateReport',
                array(
                    'testSuffix'             => '_scheduled_report_in_html_tables_only',
                    'date'                   => $dateTime,
                    'periods'                => array($period),
                    'format'                 => 'original',
                    'fileExtension'          => 'html',
                    'otherRequestParameters' => array(
                        'idReport'     => 1,
                        'reportFormat' => ReportRenderer::HTML_FORMAT,
                        'outputType'   => \Piwik\Plugins\ScheduledReports\API::OUTPUT_RETURN
                    )
                )
            )
        );

        // CSV Scheduled Report
        array_push(
            $apiCalls,
            array(
                'ScheduledReports.generateReport',
                array(
                    'testSuffix'             => '_scheduled_report_in_csv',
                    'date'                   => $dateTime,
                    'periods'                => array($period),
                    'format'                 => 'original',
                    'fileExtension'          => 'csv',
                    'otherRequestParameters' => array(
                        'idReport'     => 1,
                        'reportFormat' => ReportRenderer::CSV_FORMAT,
                        'outputType'   => \Piwik\Plugins\ScheduledReports\API::OUTPUT_RETURN
                    )
                )
        if (Fixture::canImagesBeIncludedInScheduledReports()) {
            // PDF Scheduled Report
            // tests/PHPUnit/System/processed/test_ecommerceOrderWithItems_scheduled_report_in_pdf_tables_only__ScheduledReports.generateReport_week.original.pdf
            array_push(
                $apiCalls,
                array(
                     'ScheduledReports.generateReport',
                     array(
                         'testSuffix'             => '_scheduled_report_in_pdf_tables_only',
                         'date'                   => $dateTime,
                         'periods'                => array($period),
                         'format'                 => 'original',
                         'fileExtension'          => 'pdf',
                         'otherRequestParameters' => array(
                             'idReport'     => 1,
                             'reportFormat' => ReportRenderer::PDF_FORMAT,
                             'outputType'   => \Piwik\Plugins\ScheduledReports\API::OUTPUT_RETURN
                         )

        // SMS Scheduled Report, one site
        array_push(
            $apiCalls,
            array(
                 'ScheduledReports.generateReport',
                 array(
                     'testSuffix'             => '_scheduled_report_via_sms_one_site',
                     'date'                   => $dateTime,
                     'periods'                => array($period),
                     'format'                 => 'original',
                     'fileExtension'          => 'sms.txt',
                     'otherRequestParameters' => array(
                         'idReport'   => 2,
                         'outputType' => \Piwik\Plugins\ScheduledReports\API::OUTPUT_RETURN
                     )
                 )
            )
        );

        // SMS Scheduled Report, all sites
        array_push(
            $apiCalls,
            array(
                 'ScheduledReports.generateReport',
                 array(
                     'testSuffix'             => '_scheduled_report_via_sms_all_sites',
                     'date'                   => $dateTime,
                     'periods'                => array($period),
                     'format'                 => 'original',
                     'fileExtension'          => 'sms.txt',
                     'otherRequestParameters' => array(
                         'idReport'   => 3,
                         'outputType' => \Piwik\Plugins\ScheduledReports\API::OUTPUT_RETURN
diosmosis's avatar
diosmosis a validé
        if (Fixture::canImagesBeIncludedInScheduledReports()) {
            // HTML Scheduled Report with images
            array_push(
                $apiCalls,
                array(
                     'ScheduledReports.generateReport',
                     array(
                         'testSuffix'             => '_scheduled_report_in_html_tables_and_graph',
                         'date'                   => $dateTime,
                         'periods'                => array($period),
                         'format'                 => 'original',
                         'fileExtension'          => 'html',
                         'otherRequestParameters' => array(
                             'idReport'     => 4,
                             'reportFormat' => ReportRenderer::HTML_FORMAT,
                             'outputType'   => \Piwik\Plugins\ScheduledReports\API::OUTPUT_RETURN

            // mail report with one row evolution based png graph
            array_push(
                $apiCalls,
                array(
                     'ScheduledReports.generateReport',
                     array(
                         'testSuffix'             => '_scheduled_report_in_html_row_evolution_graph',
                         'date'                   => $dateTime,
                         'periods'                => array($period),
                         'format'                 => 'original',
                         'fileExtension'          => 'html',
                         'otherRequestParameters' => array(
                             'idReport'     => 5,
                             'outputType'   => \Piwik\Plugins\ScheduledReports\API::OUTPUT_RETURN
    protected function _testApiUrl($testName, $apiId, $requestUrl, $compareAgainst, $xmlFieldsToRemove = array(), $params = array())
        list($processedFilePath, $expectedFilePath) =
            $this->getProcessedAndExpectedPaths($testName, $apiId, $format = null, $compareAgainst);
        $originalGET = $_GET;
        $_GET = $requestUrl;
        unset($_GET['serialize']);

        $processedResponse = Response::loadFromApi($params, $requestUrl);
        if (empty($compareAgainst)) {
            $processedResponse->save($processedFilePath);
            $expectedResponse = Response::loadFromFile($expectedFilePath, $params, $requestUrl);
        } catch (Exception $ex) {
            $this->handleMissingExpectedFile($expectedFilePath, $processedResponse);
            $errorMessage = get_class($this) . ": Differences with expected in '$processedFilePath'";
            Response::assertEquals($expectedResponse, $processedResponse, $errorMessage);
        } catch (Exception $ex) {
            $this->comparisonFailures[] = $ex;
        }
    private function handleMissingExpectedFile($expectedFilePath, Response $processedResponse)
        $this->missingExpectedFiles[] = $expectedFilePath;
        print("The expected file is not found at '$expectedFilePath'. The Processed response was:");
        print("\n----------------------------\n\n");
        var_dump($processedResponse->getResponseText());
        print("\n----------------------------\n");
    public static function assertApiResponseHasNoError($response)
        if(!is_string($response)) {
            $response = json_encode($response);
        self::assertTrue(stripos($response, 'error') === false, "error in $response");
        self::assertTrue(stripos($response, 'exception') === false, "exception in $response");
    protected static function getProcessedAndExpectedDirs()
        $path = static::getPathToTestDirectory();
        $processedPath = $path . '/processed/';
        if (!is_dir($processedPath)) {
            mkdir($processedPath, $mode = 0777, $recursive = true);
        }

        if (!is_writable($processedPath)) {
            self::fail('To run the tests, you need to give write permissions to the following directory (create it if '
                      . 'it doesn\'t exist).<code><br/>mkdir ' . $processedPath . '<br/>chmod 777 ' . $processedPath
                      . '</code><br/>');
        return array($processedPath, $path . '/expected/');
    private function getProcessedAndExpectedPaths($testName, $testId, $format = null, $compareAgainst = false)
        $filenameSuffix = '__' . $testId;
            $filenameSuffix .= ".$format";
        $processedFilename = $testName . $filenameSuffix;

        $expectedFilename = $compareAgainst ? ('test_' . $compareAgainst) : $testName;
        $expectedFilename .= $filenameSuffix;
        list($processedDir, $expectedDir) = static::getProcessedAndExpectedDirs();
        return array($processedDir . $processedFilename, $expectedDir . $expectedFilename);
    }

    /**
     * Returns an array describing the API methods to call & compare with
     * expected output.
     *
     * The returned array must be of the following format:
     * <code>
     * array(
     *     array('SomeAPI.method', array('testOption1' => 'value1', 'testOption2' => 'value2'),
     *     array(array('SomeAPI.method', 'SomeOtherAPI.method'), array(...)),
     *     .
     *     .
     *     .
     * )
     * </code>
     *
     * Valid test options are described in the ApiTestConfig class docs.
     *
     * All test options are optional, except 'idSite' & 'date'.
     */
    public function getApiForTesting()
    {
        return array();
    }

    /**
     * Gets the string prefix used in the name of the expected/processed output files.
     */
    public static function getOutputPrefix()
        $parts = explode("\\", get_called_class());
        $result = end($parts);
        $result = str_replace('Test_Piwik_Integration_', '', $result);
        return $result;
    /**
     * Assert that the response of an API method call is the same as the contents in an
     * expected file.
     *
     * @param string $api ie, `"UserSettings.getBrowser"`
     * @param array $queryParams Query parameters to send to the API.
     */
    public function assertApiResponseEqualsExpected($apiMethod, $queryParams)
    {
        $this->runApiTests($apiMethod, array(
            'idSite' => $queryParams['idSite'],
            'date' => $queryParams['date'],
            'periods' => $queryParams['period'],
            'format' => isset($queryParams['format']) ? $queryParams['format'] : 'xml',
            'testSuffix' => '_' . $this->getName(), // TODO: instead of using a test suffix, the whole file name should just be the test method
            'otherRequestParameters' => $queryParams
        ));
    }

    /**
     * Runs API tests.
     */
    protected function runApiTests($api, $params)
    {
        $testConfig = new ApiTestConfig($params);

        $testName = 'test_' . static::getOutputPrefix();
        $this->missingExpectedFiles = array();
        $this->comparisonFailures = array();
        if ($testConfig->disableArchiving) {
            Rules::$archivingDisabledByTests = true;
            Config::getInstance()->General['browser_archiving_disabled_enforce'] = 1;
            Rules::$archivingDisabledByTests = false;
            Config::getInstance()->General['browser_archiving_disabled_enforce'] = 0;
        if ($testConfig->language) {
            $this->changeLanguage($testConfig->language);
        $testRequests = new Collection($api, $testConfig, $api);
        foreach ($testRequests->getRequestUrls() as $apiId => $requestUrl) {
            $this->_testApiUrl($testName . $testConfig->testSuffix, $apiId, $requestUrl, $testConfig->compareAgainst, $testConfig->xmlFieldsToRemove, $params);
        // change the language back to en
        if ($this->lastLanguage != 'en') {

        if (!empty($this->missingExpectedFiles)) {
            $expectedDir = dirname(reset($this->missingExpectedFiles));
            $this->fail(" ERROR: Could not find expected API output '"
                . implode("', '", $this->missingExpectedFiles)
                . "'. For new tests, to pass the test, you can copy files from the processed/ directory into"
                . " $expectedDir  after checking that the output is valid. %s ");
        // Display as one error all sub-failures
        if (!empty($this->comparisonFailures)) {
            $this->printComparisonFailures();
            throw reset($this->comparisonFailures);
mattab's avatar
mattab a validé

        return count($this->comparisonFailures) == 0;
    private function printComparisonFailures()
    {
        $messages = '';
        foreach ($this->comparisonFailures as $index => $failure) {
            $msg = $failure->getMessage();
            $msg = strtok($msg, "\n");
            $messages .= "\n#" . ($index + 1) . ": " . $msg;
        }
        $messages .= " \n ";

        print($messages);
    }

    /**
     * changing the language within one request is a bit fancy
     * in order to keep the core clean, we need a little hack here
     *
     * @param string $langId
     */
    protected function changeLanguage($langId)
        if ($this->lastLanguage != $langId) {
            Translate::reloadLanguage($langId);
     * Path where expected/processed output files are stored.
    public static function getPathToTestDirectory()
        $up = DIRECTORY_SEPARATOR . '..';

        return dirname(__FILE__) . $up . $up . DIRECTORY_SEPARATOR . 'System';
    /**
     * Returns an array associating table names w/ lists of row data.
     * @return array
     */
    protected static function getDbTablesWithData()
    {
        foreach (DbHelper::getTablesInstalled() as $tableName) {
            $result[$tableName] = Db::fetchAll("SELECT * FROM `$tableName`");
    /**
     * Truncates all tables then inserts the data in $tables into each
     * mapped table.
     * @param array $tables Array mapping table names with arrays of row data.
     */
    protected static function restoreDbTables($tables)
    {
        $existingTables = DbHelper::getTablesInstalled();
        foreach ($tables as $table => $rows) {
            // create table if it's an archive table
            if (strpos($table, 'archive_') !== false && !in_array($table, $existingTables)) {
                $tableType = strpos($table, 'archive_numeric') !== false ? 'archive_numeric' : 'archive_blob';

mattab's avatar
mattab a validé
                $createSql = DbHelper::getTableCreateSql($tableType);
                $createSql = str_replace(Common::prefixTable($tableType), $table, $createSql);
            foreach ($rows as $row) {
                $values = array();
                foreach ($row as $value) {
                    if (is_null($value)) {
                        $values[] = 'NULL';
                    } else if (is_numeric($value)) {
                        $values[] = $value;
                    } else if (!ctype_print($value)) {
                        $values[] = "x'" . bin2hex(substr($value, 1)) . "'";
                    } else {
                        $values[] = "?";
                        $bind[] = $value;
                    }
                }

                $rowsSql[] = "(" . implode(',', $values) . ")";
            }

            $sql = "INSERT INTO `$table` VALUES " . implode(',', $rowsSql);
            Db::query($sql, $bind);
        }
    }

    /**
     * Drops all archive tables.
     */
    public static function deleteArchiveTables()
        DbHelper::deleteArchiveTables();

    protected function skipWhenPhp53()
    {
            $this->markTestSkipped('Sometimes fail on php 5.3');
        }
    }