diff options
author | Matthieu Aubry <matt@piwik.org> | 2015-03-13 05:24:12 +0300 |
---|---|---|
committer | Matthieu Aubry <matt@piwik.org> | 2015-03-13 05:24:12 +0300 |
commit | ac8793318daa2fc009439a5918ae9129c1dfd472 (patch) | |
tree | 51a339b55ac9bfd0ff72279df48992a14306dc1b /plugins | |
parent | f95d109fb120c97828a5a908a88075e97d3f8b2e (diff) | |
parent | f19f7fe42fbd129e7b28813c4dd110d9d0e8b558 (diff) |
Merge pull request #7377 from piwik/7181_isolated_archive_purging
refactor archive purging for clarity and resilience.
Diffstat (limited to 'plugins')
9 files changed, 547 insertions, 9 deletions
diff --git a/plugins/CoreAdminHome/API.php b/plugins/CoreAdminHome/API.php index 60ba204d45..6329fc8993 100644 --- a/plugins/CoreAdminHome/API.php +++ b/plugins/CoreAdminHome/API.php @@ -10,7 +10,7 @@ namespace Piwik\Plugins\CoreAdminHome; use Exception; use Piwik\Container\StaticContainer; -use Piwik\DataAccess\ArchiveInvalidator; +use Piwik\Archive\ArchiveInvalidator; use Piwik\Db; use Piwik\Piwik; use Piwik\Scheduler\Scheduler; diff --git a/plugins/CoreAdminHome/Commands/FixDuplicateLogActions.php b/plugins/CoreAdminHome/Commands/FixDuplicateLogActions.php index 47a058c7ad..be9c74a152 100644 --- a/plugins/CoreAdminHome/Commands/FixDuplicateLogActions.php +++ b/plugins/CoreAdminHome/Commands/FixDuplicateLogActions.php @@ -11,7 +11,7 @@ namespace Piwik\Plugins\CoreAdminHome\Commands; use Piwik\Common; use Piwik\Container\StaticContainer; use Piwik\DataAccess\Actions; -use Piwik\DataAccess\ArchiveInvalidator; +use Piwik\Archive\ArchiveInvalidator; use Piwik\Plugin\ConsoleCommand; use Piwik\Plugins\CoreAdminHome\Model\DuplicateActionRemover; use Piwik\Timer; diff --git a/plugins/CoreAdminHome/Commands/PurgeOldArchiveData.php b/plugins/CoreAdminHome/Commands/PurgeOldArchiveData.php new file mode 100644 index 0000000000..89696eaa9b --- /dev/null +++ b/plugins/CoreAdminHome/Commands/PurgeOldArchiveData.php @@ -0,0 +1,180 @@ +<?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\CoreAdminHome\Commands; + +use Piwik\Archive; +use Piwik\Archive\ArchivePurger; +use Piwik\DataAccess\ArchiveTableCreator; +use Piwik\Date; +use Piwik\Db; +use Piwik\Plugin\ConsoleCommand; +use Piwik\Timer; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Command that allows users to force purge old or invalid archive data. In the event of a failure + * in the archive purging scheduled task, this command can be used to manually delete old/invalid archives. + */ +class PurgeOldArchiveData extends ConsoleCommand +{ + const ALL_DATES_STRING = 'all'; + + /** + * @var ArchivePurger + */ + private $archivePurger; + + /** + * For tests. + * + * @var Date + */ + public static $todayOverride = null; + + public function __construct(ArchivePurger $archivePurger = null) + { + parent::__construct(); + + $this->archivePurger = $archivePurger ?: new ArchivePurger(); + } + + protected function configure() + { + $this->setName('core:purge-old-archive-data'); + $this->setDescription('Purges out of date and invalid archive data from archive tables.'); + $this->addArgument("dates", InputArgument::IS_ARRAY | InputArgument::OPTIONAL, + "The months of the archive tables to purge data from. By default, only deletes from the current month. Use '" . self::ALL_DATES_STRING. "' for all dates.", + array(self::getToday()->toString())); + $this->addOption('exclude-outdated', null, InputOption::VALUE_NONE, "Do not purge outdated archive data."); + $this->addOption('exclude-invalidated', null, InputOption::VALUE_NONE, "Do not purge invalidated archive data."); + $this->addOption('exclude-ranges', null, InputOption::VALUE_NONE, "Do not purge custom ranges."); + $this->addOption('skip-optimize-tables', null, InputOption::VALUE_NONE, "Do not run OPTIMIZE TABLES query on affected archive tables."); + $this->setHelp("By default old and invalidated archives are purged. Custom ranges are also purged with outdated archives.\n\n" + . "Note: archive purging is done during scheduled task execution, so under normal circumstances, you should not need to " + . "run this command manually."); + + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $archivePurger = $this->archivePurger; + + $dates = $this->getDatesToPurgeFor($input); + + $excludeOutdated = $input->getOption('exclude-outdated'); + if ($excludeOutdated) { + $output->writeln("Skipping purge outdated archive data."); + } else { + foreach ($dates as $date) { + $message = sprintf("Purging outdated archives for %s...", $date->toString('Y_m')); + $this->performTimedPurging($output, $message, function () use ($date, $archivePurger) { + $archivePurger->purgeOutdatedArchives($date); + }); + } + } + + $excludeInvalidated = $input->getOption('exclude-invalidated'); + if ($excludeInvalidated) { + $output->writeln("Skipping purge invalidated archive data."); + } else { + foreach ($dates as $date) { + $message = sprintf("Purging invalidated archives for %s...", $date->toString('Y_m')); + $this->performTimedPurging($output, $message, function () use ($archivePurger, $date) { + $archivePurger->purgeInvalidatedArchivesFrom($date); + }); + } + } + + $excludeCustomRanges = $input->getOption('exclude-ranges'); + if ($excludeCustomRanges) { + $output->writeln("Skipping purge custom range archives."); + } else { + foreach ($dates as $date) { + $message = sprintf("Purging custom range archives for %s...", $date->toString('Y_m')); + $this->performTimedPurging($output, $message, function () use ($date, $archivePurger) { + $archivePurger->purgeArchivesWithPeriodRange($date); + }); + } + } + + $skipOptimizeTables = $input->getOption('skip-optimize-tables'); + if ($skipOptimizeTables) { + $output->writeln("Skipping OPTIMIZE TABLES."); + } else { + $this->optimizeArchiveTables($output, $dates); + } + } + + /** + * @param InputInterface $input + * @return Date[] + */ + private function getDatesToPurgeFor(InputInterface $input) + { + $dates = array(); + + $dateSpecifier = $input->getArgument('dates'); + if (count($dateSpecifier) === 1 + && reset($dateSpecifier) == self::ALL_DATES_STRING + ) { + foreach (ArchiveTableCreator::getTablesArchivesInstalled() as $table) { + $tableDate = ArchiveTableCreator::getDateFromTableName($table); + + list($year, $month) = explode('_', $tableDate); + + $dates[] = Date::factory($year . '-' . $month . '-' . '01'); + } + } else { + foreach ($dateSpecifier as $date) { + $dates[] = Date::factory($date); + } + } + + return $dates; + } + + private function performTimedPurging(OutputInterface $output, $startMessage, $callback) + { + $timer = new Timer(); + + $output->write($startMessage); + + $callback(); + + $output->writeln("Done. <comment>[" . $timer->__toString() . "]</comment>"); + } + + /** + * @param Date[] $dates + */ + private function optimizeArchiveTables(OutputInterface $output, $dates) + { + $output->writeln("Optimizing archive tables..."); + + foreach ($dates as $date) { + $numericTable = ArchiveTableCreator::getNumericTable($date); + $this->performTimedPurging($output, "Optimizing table $numericTable...", function () use ($numericTable) { + Db::optimizeTables($numericTable); + }); + + $blobTable = ArchiveTableCreator::getBlobTable($date); + $this->performTimedPurging($output, "Optimizing table $blobTable...", function () use ($blobTable) { + Db::optimizeTables($blobTable); + }); + } + } + + private static function getToday() + { + return self::$todayOverride ?: Date::today(); + } +}
\ No newline at end of file diff --git a/plugins/CoreAdminHome/Tasks.php b/plugins/CoreAdminHome/Tasks.php index d633b9fd5d..700c7d69c2 100644 --- a/plugins/CoreAdminHome/Tasks.php +++ b/plugins/CoreAdminHome/Tasks.php @@ -8,13 +8,28 @@ */ namespace Piwik\Plugins\CoreAdminHome; -use Piwik\DataAccess\ArchivePurger; +use Piwik\ArchiveProcessor\Rules; +use Piwik\Archive\ArchivePurger; +use Piwik\Container\StaticContainer; use Piwik\DataAccess\ArchiveTableCreator; use Piwik\Date; use Piwik\Db; +use Piwik\Log; +use Piwik\Plugins\CoreAdminHome\Tasks\ArchivesToPurgeDistributedList; +use Piwik\SettingsServer; class Tasks extends \Piwik\Plugin\Tasks { + /** + * @var ArchivePurger + */ + private $archivePurger; + + public function __construct(ArchivePurger $archivePurger = null) + { + $this->archivePurger = $archivePurger ?: new ArchivePurger(); + } + public function schedule() { // general data purge on older archive tables, executed daily @@ -29,21 +44,47 @@ class Tasks extends \Piwik\Plugin\Tasks public function purgeOutdatedArchives() { + $logger = StaticContainer::get('Psr\Log\LoggerInterface'); + + if ($this->willPurgingCausePotentialProblemInUI()) { + $logger->info("Purging temporary archives: skipped (browser triggered archiving not enabled & not running after core:archive)"); + return false; + } + $archiveTables = ArchiveTableCreator::getTablesArchivesInstalled(); + + $logger->info("Purging archives in {tableCount} archive tables.", array('tableCount' => count($archiveTables))); + + // keep track of dates we purge for, since getTablesArchivesInstalled() will return numeric & blob + // tables (so dates will appear two times, and we should only purge once per date) + $datesPurged = array(); + foreach ($archiveTables as $table) { $date = ArchiveTableCreator::getDateFromTableName($table); list($year, $month) = explode('_', $date); // Somehow we may have archive tables created with older dates, prevent exception from being thrown - if ($year > 1990) { - ArchivePurger::purgeOutdatedArchives(Date::factory("$year-$month-15")); + if ($year > 1990 + && empty($datesPurged[$date]) + ) { + $dateObj = Date::factory("$year-$month-15"); + + $this->archivePurger->purgeOutdatedArchives($dateObj); + $this->archivePurger->purgeArchivesWithPeriodRange($dateObj); + + $datesPurged[$date] = true; } } } public function purgeInvalidatedArchives() { - ArchivePurger::purgeInvalidatedArchives(); + $archivesToPurge = new ArchivesToPurgeDistributedList(); + foreach ($archivesToPurge->getAllAsDates() as $date) { + $this->archivePurger->purgeInvalidatedArchivesFrom($date); + + $archivesToPurge->removeDate($date); + } } public function optimizeArchiveTable() @@ -51,4 +92,18 @@ class Tasks extends \Piwik\Plugin\Tasks $archiveTables = ArchiveTableCreator::getTablesArchivesInstalled(); Db::optimizeTables($archiveTables); } + + /** + * we should only purge outdated & custom range archives if we know cron archiving has just run, + * or if browser triggered archiving is enabled. if cron archiving has run, then we know the latest + * archives are in the database, and we can remove temporary ones. if browser triggered archiving is + * enabled, then we know any archives that are wrongly purged, can be re-archived on demand. + * this prevents some situations where "no data" is displayed for reports that should have data. + * + * @return bool + */ + private function willPurgingCausePotentialProblemInUI() + { + return !Rules::isRequestAuthorizedToArchive(); + } }
\ No newline at end of file diff --git a/plugins/CoreAdminHome/Tasks/ArchivesToPurgeDistributedList.php b/plugins/CoreAdminHome/Tasks/ArchivesToPurgeDistributedList.php new file mode 100644 index 0000000000..6d56286632 --- /dev/null +++ b/plugins/CoreAdminHome/Tasks/ArchivesToPurgeDistributedList.php @@ -0,0 +1,64 @@ +<?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\CoreAdminHome\Tasks; + +use Piwik\Concurrency\DistributedList; +use Piwik\Date; + +/** + * Distributed list that holds a list of year-month archive table identifiers (eg, 2015_01 or 2014_11). Each item in the + * list is expected to identify a pair of archive tables that contain invalidated archives. + * + * The archiving purging scheduled task will read items in this list when executing the daily purge. + * + * This class is necessary in order to keep the archive purging scheduled task fast. W/o a way to keep track of + * tables w/ invalid data, the task would have to iterate over every table, which is not desired for a task that + * is executed daily. + * + * If users find other tables contain invalidated archives, they can use the core:purge-old-archive-data command + * to manually purge them. + */ +class ArchivesToPurgeDistributedList extends DistributedList +{ + const OPTION_INVALIDATED_DATES_SITES_TO_PURGE = 'InvalidatedOldReports_DatesWebsiteIds'; + + public function __construct() + { + parent::__construct(self::OPTION_INVALIDATED_DATES_SITES_TO_PURGE); + } + + /** + * @inheritdoc + */ + public function setAll($yearMonths) + { + $yearMonths = array_unique($yearMonths); + parent::setAll($yearMonths); + } + + public function getAllAsDates() + { + $dates = array(); + foreach ($this->getAll() as $yearMonth) { + try { + $date = Date::factory(str_replace('_', '-', $yearMonth) . '-01'); + } catch (\Exception $ex) { + continue; // invalid year month in distributed list + } + + $dates[] = $date; + } + return $dates; + } + + public function removeDate(Date $date) + { + $yearMonth = $date->toString('Y_m'); + $this->remove($yearMonth); + } +}
\ No newline at end of file diff --git a/plugins/CoreAdminHome/tests/Integration/Commands/PurgeOldArchiveDataTest.php b/plugins/CoreAdminHome/tests/Integration/Commands/PurgeOldArchiveDataTest.php new file mode 100644 index 0000000000..ecb07aa438 --- /dev/null +++ b/plugins/CoreAdminHome/tests/Integration/Commands/PurgeOldArchiveDataTest.php @@ -0,0 +1,152 @@ +<?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\CoreAdminHome\tests\Integration\Commands; + +use Piwik\Archive\ArchivePurger; +use Piwik\Console; +use Piwik\Date; +use Piwik\Plugins\CoreAdminHome\Commands\PurgeOldArchiveData; +use Piwik\Tests\Fixtures\RawArchiveDataWithTempAndInvalidated; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; +use Symfony\Component\Console\Tester\ApplicationTester; + +/** + * @group Core + */ +class PurgeOldArchiveDataTest extends IntegrationTestCase +{ + /** + * @var RawArchiveDataWithTempAndInvalidated + */ + public static $fixture = null; + + /** + * @var ApplicationTester + */ + protected $applicationTester = null; + + /** + * @var Console + */ + protected $application; + + public function setUp() + { + parent::setUp(); + + PurgeOldArchiveData::$todayOverride = Date::factory('2015-02-27'); + + $archivePurger = new ArchivePurger(); + $archivePurger->setTodayDate(Date::factory('2015-02-27')); + $archivePurger->setYesterdayDate(Date::factory('2015-02-26')); + $archivePurger->setNow(Date::factory('2015-02-27 08:00:00')->getTimestamp()); + + $this->application = new Console(); + $this->application->setAutoExit(false); + $this->application->add(new PurgeOldArchiveData($archivePurger)); + + $this->applicationTester = new ApplicationTester($this->application); + + // assert the test data was setup correctly + self::$fixture->assertInvalidatedArchivesNotPurged(self::$fixture->january); + self::$fixture->assertInvalidatedArchivesNotPurged(self::$fixture->february); + } + + public function tearDown() + { + PurgeOldArchiveData::$todayOverride = null; + + parent::tearDown(); + } + + public function test_ExecutingCommandWithAllDates_PurgesAllExistingArchiveTables() + { + $result = $this->applicationTester->run(array( + 'command' => 'core:purge-old-archive-data', + 'dates' => array('all'), + '-vvv' => true + )); + + $this->assertEquals(0, $result, $this->getCommandDisplayOutputErrorMessage()); + + self::$fixture->assertInvalidatedArchivesPurged(self::$fixture->february); + self::$fixture->assertTemporaryArchivesPurged($isBrowserTriggeredArchivingEnabled = true, self::$fixture->february); + self::$fixture->assertCustomRangesPurged(self::$fixture->february); + + self::$fixture->assertInvalidatedArchivesPurged(self::$fixture->january); + self::$fixture->assertTemporaryArchivesPurged($isBrowserTriggeredArchivingEnabled = true, self::$fixture->january); + self::$fixture->assertCustomRangesPurged(self::$fixture->january); + } + + public function test_ExecutingCommandWithNoDate_PurgesArchiveTableForToday() + { + $result = $this->applicationTester->run(array( + 'command' => 'core:purge-old-archive-data', + '-vvv' => true + )); + + $this->assertEquals(0, $result, $this->getCommandDisplayOutputErrorMessage()); + + self::$fixture->assertInvalidatedArchivesPurged(self::$fixture->february); + self::$fixture->assertTemporaryArchivesPurged($isBrowserTriggeredArchivingEnabled = true, self::$fixture->february); + self::$fixture->assertCustomRangesPurged(self::$fixture->february); + + self::$fixture->assertInvalidatedArchivesNotPurged(self::$fixture->january); + self::$fixture->assertTemporaryArchivesNotPurged(self::$fixture->january); + self::$fixture->assertCustomRangesNotPurged(self::$fixture->january); + } + + public function test_ExecutingCommandWithSpecificDate_PurgesArchiveTableForDate() + { + $result = $this->applicationTester->run(array( + 'command' => 'core:purge-old-archive-data', + 'dates' => array('2015-01-14'), + '-vvv' => true + )); + + $this->assertEquals(0, $result, $this->getCommandDisplayOutputErrorMessage()); + + self::$fixture->assertInvalidatedArchivesPurged(self::$fixture->january); + self::$fixture->assertTemporaryArchivesPurged($isBrowserTriggeredArchivingEnabled = true, self::$fixture->january); + self::$fixture->assertCustomRangesPurged(self::$fixture->january); + + self::$fixture->assertInvalidatedArchivesNotPurged(self::$fixture->february); + self::$fixture->assertTemporaryArchivesNotPurged(self::$fixture->february); + self::$fixture->assertCustomRangesNotPurged(self::$fixture->february); + } + + public function test_ExecutingCommandWithExcludeOptions_SkipsAppropriatePurging() + { + $result = $this->applicationTester->run(array( + 'command' => 'core:purge-old-archive-data', + 'dates' => array('2015-01-14'), + '--exclude-outdated' => true, + '--exclude-invalidated' => true, + '--exclude-ranges' => true, + '--skip-optimize-tables' => true, + '-vvv' => true + )); + + $this->assertEquals(0, $result, $this->getCommandDisplayOutputErrorMessage()); + + self::$fixture->assertInvalidatedArchivesNotPurged(self::$fixture->january); + self::$fixture->assertTemporaryArchivesNotPurged(self::$fixture->january); + self::$fixture->assertCustomRangesNotPurged(self::$fixture->january); + + $this->assertContains("Skipping purge outdated archive data.", $this->applicationTester->getDisplay()); + $this->assertContains("Skipping purge invalidated archive data.", $this->applicationTester->getDisplay()); + $this->assertContains("Skipping OPTIMIZE TABLES.", $this->applicationTester->getDisplay()); + } + + protected function getCommandDisplayOutputErrorMessage() + { + return "Command did not behave as expected. Command output: " . $this->applicationTester->getDisplay(); + } +} + +PurgeOldArchiveDataTest::$fixture = new RawArchiveDataWithTempAndInvalidated();
\ No newline at end of file diff --git a/plugins/CoreAdminHome/tests/Integration/TasksTest.php b/plugins/CoreAdminHome/tests/Integration/TasksTest.php new file mode 100644 index 0000000000..da3800e9c1 --- /dev/null +++ b/plugins/CoreAdminHome/tests/Integration/TasksTest.php @@ -0,0 +1,88 @@ +<?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\CoreAdminHome\tests\Integration; + +use Piwik\Archive\ArchivePurger; +use Piwik\Date; +use Piwik\Plugins\CoreAdminHome\Tasks; +use Piwik\Plugins\CoreAdminHome\Tasks\ArchivesToPurgeDistributedList; +use Piwik\Tests\Fixtures\RawArchiveDataWithTempAndInvalidated; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; + +/** + * @group Core + */ +class TasksTest extends IntegrationTestCase +{ + /** + * @var RawArchiveDataWithTempAndInvalidated + */ + public static $fixture; + + /** + * @var Tasks + */ + private $tasks; + + /** + * @var Date + */ + private $january; + + /** + * @var Date + */ + private $february; + + public function setUp() + { + parent::setUp(); + + $this->january = Date::factory('2015-01-01'); + $this->february = Date::factory('2015-02-01'); + + $archivePurger = new ArchivePurger(); + $archivePurger->setTodayDate(Date::factory('2015-02-27')); + $archivePurger->setYesterdayDate(Date::factory('2015-02-26')); + $archivePurger->setNow(Date::factory('2015-02-27 08:00:00')->getTimestamp()); + + $this->tasks = new Tasks($archivePurger); + } + + public function test_purgeInvalidatedArchives_PurgesCorrectInvalidatedArchives_AndOnlyPurgesDataForDatesAndSites_InInvalidatedReportsDistributedList() + { + $this->setUpInvalidatedReportsDistributedList($dates = array($this->february)); + + $this->tasks->purgeInvalidatedArchives(); + + self::$fixture->assertInvalidatedArchivesPurged($this->february); + self::$fixture->assertInvalidatedArchivesNotPurged($this->january); + + // assert invalidated reports distributed list has changed + $archivesToPurgeDistributedList = new ArchivesToPurgeDistributedList(); + $yearMonths = $archivesToPurgeDistributedList->getAll(); + + $this->assertEmpty($yearMonths); + } + + /** + * @param Date[] $dates + */ + private function setUpInvalidatedReportsDistributedList($dates) + { + $yearMonths = array(); + foreach ($dates as $date) { + $yearMonths[] = $date->toString('Y_m'); + } + + $archivesToPurgeDistributedList = new ArchivesToPurgeDistributedList(); + $archivesToPurgeDistributedList->add($yearMonths); + } +} + +TasksTest::$fixture = new RawArchiveDataWithTempAndInvalidated();
\ No newline at end of file diff --git a/plugins/SitesManager/SitesManager.php b/plugins/SitesManager/SitesManager.php index b9c5fa0736..93e1d8c2e8 100644 --- a/plugins/SitesManager/SitesManager.php +++ b/plugins/SitesManager/SitesManager.php @@ -9,8 +9,7 @@ namespace Piwik\Plugins\SitesManager; use Piwik\Common; -use Piwik\DataAccess\ArchiveInvalidator; -use Piwik\Db; +use Piwik\Archive\ArchiveInvalidator; use Piwik\Tracker\Cache; use Piwik\Tracker\Model as TrackerModel; diff --git a/plugins/SitesManager/tests/Integration/SitesManagerTest.php b/plugins/SitesManager/tests/Integration/SitesManagerTest.php index 135f505f01..1ba60ce972 100644 --- a/plugins/SitesManager/tests/Integration/SitesManagerTest.php +++ b/plugins/SitesManager/tests/Integration/SitesManagerTest.php @@ -10,7 +10,7 @@ namespace Piwik\Plugins\SitesManager\tests\Integration; use Piwik\Access; use Piwik\Cache; -use Piwik\DataAccess\ArchiveInvalidator; +use Piwik\Archive\ArchiveInvalidator; use Piwik\Date; use Piwik\Plugins\SitesManager\SitesManager; use Piwik\Tests\Framework\Fixture; |