Commit e1a9e49c authored by Ivan Chepurnyi's avatar Ivan Chepurnyi

+ Shared Fixtures

! Improved performance of EAV and scope fixtures loading
parent f3bb0dac
...@@ -48,6 +48,8 @@ class EcomDev_PHPUnit_Model_App extends Mage_Core_Model_App ...@@ -48,6 +48,8 @@ class EcomDev_PHPUnit_Model_App extends Mage_Core_Model_App
const REGISTRY_PATH_LAYOUT_SINGLETON = '_singleton/core/layout'; const REGISTRY_PATH_LAYOUT_SINGLETON = '_singleton/core/layout';
const REGISTRY_PATH_DESIGN_PACKAGE_SINGLETON = '_singleton/core/design_package'; const REGISTRY_PATH_DESIGN_PACKAGE_SINGLETON = '_singleton/core/design_package';
const REGISTRY_PATH_SHARED_STORAGE = 'test_suite_shared_storage';
const XML_PATH_LAYOUT_MODEL_FOR_TEST = 'phpunit/suite/layout/model'; const XML_PATH_LAYOUT_MODEL_FOR_TEST = 'phpunit/suite/layout/model';
const XML_PATH_DESIGN_PACKAGE_MODEL_FOR_TEST = 'phpunit/suite/design/package/model'; const XML_PATH_DESIGN_PACKAGE_MODEL_FOR_TEST = 'phpunit/suite/design/package/model';
...@@ -221,6 +223,8 @@ class EcomDev_PHPUnit_Model_App extends Mage_Core_Model_App ...@@ -221,6 +223,8 @@ class EcomDev_PHPUnit_Model_App extends Mage_Core_Model_App
$designPackageModel); $designPackageModel);
$this->loadAreaPart(self::AREA_TEST, self::AREA_PART_EVENTS); $this->loadAreaPart(self::AREA_TEST, self::AREA_PART_EVENTS);
$this->replaceRegistry(self::REGISTRY_PATH_SHARED_STORAGE, new Varien_Object());
return $this; return $this;
} }
......
...@@ -28,12 +28,18 @@ class EcomDev_PHPUnit_Model_Config extends Mage_Core_Model_Config ...@@ -28,12 +28,18 @@ class EcomDev_PHPUnit_Model_Config extends Mage_Core_Model_Config
const CHANGE_ME = '[change me]'; const CHANGE_ME = '[change me]';
/** /**
* Scope snapshot without applied configurations, * Scope snapshot with different levels of saving configuration
* It is used for proper store/website/default loading on per store basis
* *
* @var Mage_Core_Model_Config_Base * @var Mage_Core_Model_Config_Base
*/ */
protected $_scopeSnapshot = null; protected $_scopeSnapshot = array();
/**
* Scope snapshot for a particular test case
*
* @var Mage_Core_Model_Config_Base
*/
protected $_localScopeSnapshot = null;
/** /**
* List of replaced instance creation * List of replaced instance creation
...@@ -51,7 +57,7 @@ class EcomDev_PHPUnit_Model_Config extends Mage_Core_Model_Config ...@@ -51,7 +57,7 @@ class EcomDev_PHPUnit_Model_Config extends Mage_Core_Model_Config
{ {
if ($this->_isLocalConfigLoaded if ($this->_isLocalConfigLoaded
&& Mage::isInstalled() && Mage::isInstalled()
&& $this->_scopeSnapshot === null) { && empty($this->_scopeSnapshot)) {
$this->saveScopeSnapshot(); $this->saveScopeSnapshot();
} }
parent::loadDb(); parent::loadDb();
...@@ -141,19 +147,27 @@ class EcomDev_PHPUnit_Model_Config extends Mage_Core_Model_Config ...@@ -141,19 +147,27 @@ class EcomDev_PHPUnit_Model_Config extends Mage_Core_Model_Config
*/ */
public function loadScopeSnapshot() public function loadScopeSnapshot()
{ {
if ($this->_scopeSnapshot === null) { if (empty($this->_scopeSnapshot)) {
throw new RuntimeException('Cannot load scope snapshot, because it was not saved before'); throw new RuntimeException('Cannot load scope snapshot, because it was not saved before');
} }
$scopeNode = $this->_scopeSnapshot->getNode(); $scope = clone end($this->_scopeSnapshot);
foreach ($scopeNode->children() as $nodeName => $values) {
// Remove somehow modified before xml node $this->_xml = $scope;
unset($this->getNode()->$nodeName); return $this;
// Add saved snapshot of configuration node
$this->getNode()->addChild($nodeName);
$this->getNode()->$nodeName->extend($values);
} }
/**
* Flushes current scope snapshot level if it is not the last one
*
* @return EcomDev_PHPUnit_Model_Config
*/
public function flushScopeSnapshot()
{
if (count($this->_scopeSnapshot) > 1) {
array_pop($this->_scopeSnapshot);
memory_get_usage(); // Memory GC
}
return $this; return $this;
} }
...@@ -161,23 +175,11 @@ class EcomDev_PHPUnit_Model_Config extends Mage_Core_Model_Config ...@@ -161,23 +175,11 @@ class EcomDev_PHPUnit_Model_Config extends Mage_Core_Model_Config
* Saves current configuration snapshot, * Saves current configuration snapshot,
* for pussible restoring in feature * for pussible restoring in feature
* *
* @param array $nodesToSave list of nodes for saving data, by default it is 'default', 'webistes', 'stores'
* @return EcomDev_PHPUnit_Model_Config * @return EcomDev_PHPUnit_Model_Config
*/ */
public function saveScopeSnapshot($nodesToSave = array('default', 'websites', 'stores')) public function saveScopeSnapshot()
{ {
$this->_scopeSnapshot = clone $this->_prototype; $this->_scopeSnapshot[] = clone $this->_xml;
$this->_scopeSnapshot->loadString('<config />');
$scopeNode = $this->_scopeSnapshot->getNode();
foreach ($nodesToSave as $node) {
$scopeNode->addChild($node);
$scopeNode->{$node}->extend(
$this->getNode($node),
true
);
}
return $this; return $this;
} }
......
...@@ -36,9 +36,24 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -36,9 +36,24 @@ class EcomDev_PHPUnit_Model_Fixture
// Default eav loader class node in loaders configuration // Default eav loader class node in loaders configuration
const DEFAULT_EAV_LOADER_NODE = 'default'; const DEFAULT_EAV_LOADER_NODE = 'default';
// Default shared fixture name
const DEFAULT_SHARED_FIXTURE_NAME = 'default';
// Default eav loader class alias // Default eav loader class alias
const DEFAULT_EAV_LOADER_CLASS = 'ecomdev_phpunit/fixture_eav_default'; const DEFAULT_EAV_LOADER_CLASS = 'ecomdev_phpunit/fixture_eav_default';
// Key for storing fixture data into storage
const STORAGE_KEY_FIXTURE = 'fixture';
// Key for loaded tables into database
const STORAGE_KEY_TABLES = 'tables';
// Key for loaded entities by EAV loaders
const STORAGE_KEY_ENTITIES = 'entities';
// Key for created scope models
const STORAGE_KEY_SCOPE = 'scope';
/** /**
* Fixtures array, contains config, * Fixtures array, contains config,
* table and eav keys. * table and eav keys.
...@@ -70,6 +85,20 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -70,6 +85,20 @@ class EcomDev_PHPUnit_Model_Fixture
*/ */
protected $_fixture = array(); protected $_fixture = array();
/**
* Storage object, for storing data between tests
*
* @var Varien_Object
*/
protected $_storage = null;
/**
* Scope of the fixture,
* used for different logic depending on
*
* @var string
*/
protected $_scope = self::SCOPE_LOCAL;
/** /**
* Fixture options * Fixture options
...@@ -78,9 +107,21 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -78,9 +107,21 @@ class EcomDev_PHPUnit_Model_Fixture
*/ */
protected $_options = array(); protected $_options = array();
/**
* List of scope model aliases by scope type
*
* @var array
*/
protected static $_scopeModelByType = array(
'store' => 'core/store',
'group' => 'core/store_group',
'website' => 'core/website'
);
/** /**
* Associative array of configuration nodes xml that was changed by fixture, * Associative array of configuration nodes xml that was changed by fixture,
* it is used to preserve * it is used to preserve
* @deprecated since 0.2.1
* *
* @var array * @var array
*/ */
...@@ -89,6 +130,7 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -89,6 +130,7 @@ class EcomDev_PHPUnit_Model_Fixture
/** /**
* Hash of current scope instances (store, website, group) * Hash of current scope instances (store, website, group)
* *
* @deprecated since 0.2.1
* @return array * @return array
*/ */
protected $_currentScope = array(); protected $_currentScope = array();
...@@ -117,7 +159,112 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -117,7 +159,112 @@ class EcomDev_PHPUnit_Model_Fixture
} }
/** /**
* Loads fixture from test case annotations * Sets storage for fixutures
*
* @param Varien_Object $storage
* @return EcomDev_PHPUnit_Model_Fixture
*/
public function setStorage(Varien_Object $storage)
{
$this->_storage = $storage;
return $this;
}
/**
* Retrieve fixture storage
*
* @return Varien_Object
*/
public function getStorage()
{
return $this->_storage;
}
/**
* Retrieves storage data for a particular fixture scope
*
* @param string $key
* @param string|null $scope
* @return mixed
*/
public function getStorageData($key, $scope = null)
{
if ($scope === null) {
$scope = $this->getScope();
}
$dataKey = sprintf('%s_%s', $scope, $key);
return $this->getStorage()->getData($dataKey);
}
/**
* Sets storage data for a particular fixture scope
*
* @param string $key
* @param mixed $value
* @param string|null $scope
* @return EcomDev_PHPUnit_Model_Fixture
*/
public function setStorageData($key, $value, $scope = null)
{
if ($scope === null) {
$scope = $this->getScope();
}
$dataKey = sprintf('%s_%s', $scope, $key);
$this->getStorage()->setData($dataKey, $value);
return $this;
}
/**
* Returns current fixture scope
*
* @return string
*/
public function getScope()
{
return $this->_scope;
}
/**
* Sets current fixture scope
*
* @param string $scope EcomDev_PHPUnit_Model_Fixture_Interface::SCOPE_LOCAL|EcomDev_PHPUnit_Model_Fixture_Interface::SCOPE_SHARED
* @return EcomDev_PHPUnit_Model_Fixture
*/
public function setScope($scope)
{
$this->_scope = $scope;
return $this;
}
/**
* Check that current fixture scope is equal to SCOPE_SHARED
*
* @return boolean
*/
public function isScopeShared()
{
return $this->getScope() === self::SCOPE_SHARED;
}
/**
* Check that current fixture scope is equal to SCOPE_LOCAL
*
* @return boolean
*/
public function isScopeLocal()
{
return $this->getScope() === self::SCOPE_LOCAL;
}
/**
* Loads fixture files from test case annotations
* *
* @param EcomDev_PHPUnit_Test_Case $testCase * @param EcomDev_PHPUnit_Test_Case $testCase
* @return EcomDev_PHPUnit_Model_Fixture * @return EcomDev_PHPUnit_Model_Fixture
...@@ -129,12 +276,63 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -129,12 +276,63 @@ class EcomDev_PHPUnit_Model_Fixture
array('class', 'method') array('class', 'method')
); );
$this->_loadFixtureFiles($fixtures, $testCase);
return $this;
}
/**
* Loads fixture files from test class annotations
*
* @param string $className
* @return EcomDev_PHPUnit_Model_Fixture
*/
public function loadForClass($className)
{
$reflection = EcomDev_Utils_Reflection::getRelflection($className);
$method = $reflection->getMethod('getAnnotationByNameFromClass');
if (!$method instanceof ReflectionMethod) {
throw new RuntimeException('Unable to read class annotations, because it is not extended from EcomDev_PHPUnit_Test_Case');
}
$fixtures = $method->invokeArgs(
null, array($className, 'loadSharedFixture', 'class')
);
$this->_loadFixtureFiles($fixtures, $className);
return $this;
}
/**
* Loads fixture files
*
* @param string $fixtures
* @param string|EcomDev_PHPUnit_Test_Case $classOrInstance
* @return EcomDev_PHPUnit_Model_Fixture
*/
protected function _loadFixtureFiles(array $fixtures, $classOrInstance)
{
$isShared = ($this->isScopeShared() || !$classOrInstance instanceof EcomDev_PHPUnit_Test_Case);
foreach ($fixtures as $fixture) { foreach ($fixtures as $fixture) {
if (empty($fixture)) { if (empty($fixture) && $isShared) {
$fixture = self::DEFAULT_SHARED_FIXTURE_NAME;
} elseif (empty($fixture)) {
$fixture = null; $fixture = null;
} }
$filePath = $testCase->getYamlFilePath('fixtures', $fixture); $filePath = false;
if ($isShared) {
$reflection = EcomDev_Utils_Reflection::getRelflection($classOrInstance);
$method = $reflection->getMethod('getYamlFilePathByClass');
if ($method instanceof ReflectionMethod) {
$filePath = $method->invokeArgs(null, array($classOrInstance, 'fixtures', $fixture));
}
} else {
$filePath = $classOrInstance->getYamlFilePath('fixtures', $fixture);
}
if (!$filePath) { if (!$filePath) {
throw new RuntimeException('Unable to load fixture for test'); throw new RuntimeException('Unable to load fixture for test');
...@@ -173,7 +371,9 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -173,7 +371,9 @@ class EcomDev_PHPUnit_Model_Fixture
*/ */
public function apply() public function apply()
{ {
$this->setStorageData(self::STORAGE_KEY_FIXTURE, $this->_fixture);
$reflection = EcomDev_Utils_Reflection::getRelflection($this); $reflection = EcomDev_Utils_Reflection::getRelflection($this);
foreach ($this->_fixture as $part => $data) { foreach ($this->_fixture as $part => $data) {
$method = '_apply' . uc_words($part, '', '_'); $method = '_apply' . uc_words($part, '', '_');
if ($reflection->hasMethod($method)) { if ($reflection->hasMethod($method)) {
...@@ -181,6 +381,8 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -181,6 +381,8 @@ class EcomDev_PHPUnit_Model_Fixture
} }
} }
// Clear fixture for getting rid of duoble processing
$this->_fixture = array();
return $this; return $this;
} }
...@@ -191,6 +393,14 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -191,6 +393,14 @@ class EcomDev_PHPUnit_Model_Fixture
*/ */
public function discard() public function discard()
{ {
$fixture = $this->getStorageData(self::STORAGE_KEY_FIXTURE);
if (!is_array($fixture)) {
$fixture = array();
}
$this->_fixture = $fixture;
$this->setStorageData(self::STORAGE_KEY_FIXTURE, null);
$reflection = EcomDev_Utils_Reflection::getRelflection($this); $reflection = EcomDev_Utils_Reflection::getRelflection($this);
foreach ($this->_fixture as $part => $data) { foreach ($this->_fixture as $part => $data) {
$method = '_discard' . uc_words($part, '', '_'); $method = '_discard' . uc_words($part, '', '_');
...@@ -213,12 +423,14 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -213,12 +423,14 @@ class EcomDev_PHPUnit_Model_Fixture
if (!is_array($configuration)) { if (!is_array($configuration)) {
throw new InvalidArgumentException('Configuration part should be an associative list'); throw new InvalidArgumentException('Configuration part should be an associative list');
} }
Mage::getConfig()->loadScopeSnapshot(); Mage::getConfig()->loadScopeSnapshot();
foreach ($configuration as $path => $value) { foreach ($configuration as $path => $value) {
$this->_setConfigNodeValue($path, $value); $this->_setConfigNodeValue($path, $value);
} }
Mage::getConfig()->loadDb(); Mage::getConfig()->loadDb();
Mage::app()->reinitStores();
return $this; return $this;
} }
...@@ -250,7 +462,6 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -250,7 +462,6 @@ class EcomDev_PHPUnit_Model_Fixture
throw new InvalidArgumentException('Configuration value should be a valid xml string'); throw new InvalidArgumentException('Configuration value should be a valid xml string');
} }
$this->_originalConfigurationXml[$path] = $node->asNiceXml();
$node->extend($xmlElement, true); $node->extend($xmlElement, true);
} }
...@@ -258,15 +469,25 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -258,15 +469,25 @@ class EcomDev_PHPUnit_Model_Fixture
} }
/** /**
* Reverts fixture configuration values in Mage_Core_Model_Config * Restores config to a previous configuration scope
* *
* @return EcomDev_PHPUnit_Model_Fixture * @return EcomDev_PHPUnit_Model_Fixture
*/ */
protected function _discardConfig() protected function _restoreConfig()
{ {
Mage::getConfig()->loadScopeSnapshot(); Mage::getConfig()->loadScopeSnapshot();
Mage::getConfig()->loadDb(); Mage::getConfig()->loadDb();
Mage::app()->reinitStores(); return $this;
}
/**
* Reverts fixture configuration values in Mage_Core_Model_Config
*
* @return EcomDev_PHPUnit_Model_Fixture
*/
protected function _discardConfig()
{
$this->_restoreConfig();
return $this; return $this;
} }
...@@ -277,15 +498,9 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -277,15 +498,9 @@ class EcomDev_PHPUnit_Model_Fixture
*/ */
protected function _discardConfigXml() protected function _discardConfigXml()
{ {
foreach ($this->_originalConfigurationXml as $path => $value) { if (!isset($this->_fixture['config'])) {
$node = Mage::getConfig()->getNode($path); $this->_resetConfig();
$parentNode = $node->getParent();
unset($parentNode->{$node->getName()});
$oldXml = new Varien_Simplexml_Element($value);
$parentNode->appendChild($oldXml);
} }
$this->_originalConfigurationXml = array();
return $this; return $this;
} }
...@@ -303,12 +518,24 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -303,12 +518,24 @@ class EcomDev_PHPUnit_Model_Fixture
); );
} }
$ignoreCleanUp = array();
// Ignore cleaning of tables if shared fixture loaded something
if ($this->isScopeLocal() && $this->getStorageData(self::STORAGE_KEY_TABLES, self::SCOPE_SHARED)) {
$ignoreCleanUp = array_keys($this->getStorageData(self::STORAGE_KEY_TABLES, self::SCOPE_SHARED));
}
foreach ($tables as $tableEntity => $data) { foreach ($tables as $tableEntity => $data) {
if (!in_array($tableEntity, $ignoreCleanUp)) {
$this->getResource()->cleanTable($tableEntity); $this->getResource()->cleanTable($tableEntity);
}
if (!empty($data)) { if (!empty($data)) {
$this->getResource()->loadTableData($tableEntity, $data); $this->getResource()->loadTableData($tableEntity, $data);
} }
} }
$this->setStorageData(self::STORAGE_KEY_TABLES, $tables);
} }
/** /**
...@@ -325,11 +552,24 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -325,11 +552,24 @@ class EcomDev_PHPUnit_Model_Fixture
); );
} }
foreach ($tables as $tableEntity => $data) { $restoreTableData = array();
// Data for tables used in shared fixture
if ($this->isScopeLocal() && $this->getStorageData(self::STORAGE_KEY_TABLES, self::SCOPE_SHARED)) {
$restoreTableData = $this->getStorageData(self::STORAGE_KEY_TABLES, self::SCOPE_SHARED);
}
foreach (array_keys($tables) as $tableEntity) {
$this->getResource()->cleanTable($tableEntity); $this->getResource()->cleanTable($tableEntity);
if (isset($restoreTableData[$tableEntity])) {
$this->getResource()->loadTableData($tableEntity, $restoreTableData[$tableEntity]);
} }
} }
$this->setStorageData(self::STORAGE_KEY_TABLES, null);
}
/** /**
* Setting config value with applying the values to stores and websites * Setting config value with applying the values to stores and websites
* *
...@@ -357,7 +597,7 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -357,7 +597,7 @@ class EcomDev_PHPUnit_Model_Fixture
EcomDev_Utils_Reflection::setRestrictedPropertyValue( EcomDev_Utils_Reflection::setRestrictedPropertyValue(
$website, '_configCache', array() $website, '_configCache', array()
); );
// Should change value
default: default:
Mage::getConfig()->setNode($path, $value); Mage::getConfig()->setNode($path, $value);
...@@ -402,10 +642,13 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -402,10 +642,13 @@ class EcomDev_PHPUnit_Model_Fixture
foreach ($entities as $entityType => $values) { foreach ($entities as $entityType => $values) {
$this->_getEavLoader($entityType) $this->_getEavLoader($entityType)
->setFixture($this)
->setOptions($this->_options) ->setOptions($this->_options)
->loadEntity($entityType, $values); ->loadEntity($entityType, $values);
} }
$this->setStorageData(self::STORAGE_KEY_ENTITIES, array_keys($entities));
return $this; return $this;
} }
...@@ -417,7 +660,17 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -417,7 +660,17 @@ class EcomDev_PHPUnit_Model_Fixture
*/ */
protected function _discardEav($entities) protected function _discardEav($entities)
{ {
$ignoreCleanUp = array();
// Ignore cleaning of entities if shared fixture loaded something for them
if ($this->isScopeLocal() && $this->getStorageData(self::STORAGE_KEY_ENTITIES, self::SCOPE_SHARED)) {
$ignoreCleanUp = $this->getStorageData(self::STORAGE_KEY_ENTITIES, self::SCOPE_SHARED);
}
foreach (array_keys($entities) as $entityType) { foreach (array_keys($entities) as $entityType) {
if (in_array($entityType, $ignoreCleanUp)) {
continue;
}
$this->_getEavLoader($entityType) $this->_getEavLoader($entityType)
->cleanEntity($entityType); ->cleanEntity($entityType);
} }
...@@ -434,38 +687,99 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -434,38 +687,99 @@ class EcomDev_PHPUnit_Model_Fixture
*/ */
protected function _applyScope($types) protected function _applyScope($types)
{ {
$modelByType = array(
'store' => 'core/store',
'group' => 'core/store_group',
'website' => 'core/website'
);
Mage::app()->disableEvents(); Mage::app()->disableEvents();
// Validate received fixture data
$this->_validateScope($types);
foreach ($types as $type => $rows) { if ($this->getStorageData(self::STORAGE_KEY_SCOPE) !== null) {
if (!isset($modelByType[$type])) { throw new RuntimeException('Scope data was not cleared after previous test');
throw new RuntimeException(sprintf('Unknown "%s" scope type specified', $type));
} }
$scopeModels = array();
foreach ($types as $type => $rows) {
foreach ($rows as $row) { foreach ($rows as $row) {
$scopeModel = Mage::getModel($modelByType[$type]); $model = $this->_handleScopeRow($type, $row);
$this->_currentScope[$type][] = $scopeModel; if ($model) {
$scopeModels[$type][$model->getId()] = $model;
}
}
}
$this->setStorageData(self::STORAGE_KEY_SCOPE, $scopeModels);
Mage::app()->enableEvents();
Mage::app()->reinitStores();
return $this;
}
/**
* Handle scope row data
*
* @param string $type
* @param array $row
* @return boolean|Mage_Core_Model_Abstract
*/
protected function _handleScopeRow($type, $row)
{
$previousScope = array();
if ($this->isScopeLocal() && $this->getStorageData(self::STORAGE_KEY_SCOPE, self::SCOPE_SHARED) !== null) {
$previousScope = $this->getStorageData(self::STORAGE_KEY_SCOPE, self::SCOPE_SHARED);
}
if (isset($previousScope[$type][$row[$type . '_id']])) {
return false;
}
$scopeModel = Mage::getModel(self::$_scopeModelByType[$type]);
$scopeModel->setData($row); $scopeModel->setData($row);
// Change property for saving new objects with specified ids // Change property for saving new objects with specified ids
EcomDev_Utils_Reflection::setRestrictedPropertyValue( EcomDev_Utils_Reflection::setRestrictedPropertyValues(
$scopeModel->getResource(), '_useIsObjectNew', true $scopeModel->getResource(),
array(
'_useIsObjectNew' => true,
'_isPkAutoIncrement' => false
)
); );
$scopeModel->isObjectNew(true); $scopeModel->isObjectNew(true);
$scopeModel->save(); $scopeModel->save();
// Revert changed property // Revert changed property
EcomDev_Utils_Reflection::setRestrictedPropertyValue( EcomDev_Utils_Reflection::setRestrictedPropertyValues(
$scopeModel->getResource(), '_useIsObjectNew', false $scopeModel->getResource(),
array(
'_useIsObjectNew' => false,
'_isPkAutoIncrement' => true
)
); );
return $scopeModel;
} }
/**
* Validate scope data
*
* @param array $types
* @param array $modelByType
* @return EcomDev_PHPUnit_Model_Fixture
*/
protected function _validateScope($types)
{
foreach ($types as $type => $rows) {
if (!isset(self::$_scopeModelByType[$type])) {
throw new RuntimeException(sprintf('Unknown "%s" scope type specified', $type));
} }
Mage::app()->enableEvents();
Mage::app()->reinitStores(); foreach ($rows as $rowNumber => $row) {
$scopeModel = Mage::getModel($modelByType[$type]);
if (!isset($row[$type . '_id'])) {
throw new RuntimeException(sprintf('Missing primary key for "%s" scope entity at #%d row', $type, $rowNumber + 1));
}
}
}
return $this; return $this;
} }
...@@ -477,19 +791,29 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -477,19 +791,29 @@ class EcomDev_PHPUnit_Model_Fixture
*/ */
protected function _discardScope() protected function _discardScope()
{ {
if ($this->getStorageData(self::STORAGE_KEY_SCOPE) === null) {
return $this;
}
Mage::app()->disableEvents(); Mage::app()->disableEvents();
$scope = array_reverse($this->_currentScope); $scope = array_reverse($this->getStorageData(self::STORAGE_KEY_SCOPE));
foreach ($scope as $models) { foreach ($scope as $models) {
foreach ($models as $model) { foreach ($models as $model) {
$model->delete(); $model->delete();
} }
} }
$this->_currentScope = array(); $this->setStorageData(self::STORAGE_KEY_SCOPE, null);
Mage::app()->getCache()->clean( Mage::app()->getCache()->clean(
Zend_Cache::CLEANING_MODE_MATCHING_TAG, Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG,
array(Mage_Core_Model_Mysql4_Collection_Abstract::CACHE_TAG) array(
Mage_Core_Model_Store::CACHE_TAG,
Mage_Core_Model_Store_Group::CACHE_TAG,
Mage_Core_Model_Website::CACHE_TAG
)
); );
Mage::app()->enableEvents(); Mage::app()->enableEvents();
Mage::app()->reinitStores(); Mage::app()->reinitStores();
return $this; return $this;
......
...@@ -25,6 +25,9 @@ ...@@ -25,6 +25,9 @@
*/ */
interface EcomDev_PHPUnit_Model_Fixture_Interface extends EcomDev_PHPUnit_Model_Test_Loadable_Interface interface EcomDev_PHPUnit_Model_Fixture_Interface extends EcomDev_PHPUnit_Model_Test_Loadable_Interface
{ {
const SCOPE_LOCAL = 'local';
const SCOPE_SHARED = 'shared';
/** /**
* Sets fixture options * Sets fixture options
* *
...@@ -32,4 +35,74 @@ interface EcomDev_PHPUnit_Model_Fixture_Interface extends EcomDev_PHPUnit_Model_ ...@@ -32,4 +35,74 @@ interface EcomDev_PHPUnit_Model_Fixture_Interface extends EcomDev_PHPUnit_Model_
* @return EcomDev_PHPUnit_Model_Fixture_Interface * @return EcomDev_PHPUnit_Model_Fixture_Interface
*/ */
public function setOptions(array $options); public function setOptions(array $options);
/**
* Sets storage for fixutures
*
* @param Varien_Object $storage
* @return EcomDev_PHPUnit_Model_Fixture_Interface
*/
public function setStorage(Varien_Object $storage);
/**
* Retrieve fixture storage
*
* @return Varien_Object
*/
public function getStorage();
/**
* Retrieves storage data for a particular fixture scope
*
* @param string $key
* @param string|null $scope
*/
public function getStorageData($key, $scope = null);
/**
* Sets storage data for a particular fixture scope
*
* @param string $key
* @param mixed $value
* @param string|null $scope
*/
public function setStorageData($key, $value, $scope = null);
/**
* Returns current fixture scope
*
* @return string
*/
public function getScope();
/**
* Sets current fixture scope
*
*
* @param string $scope EcomDev_PHPUnit_Model_Fixture_Interface::SCOPE_LOCAL|EcomDev_PHPUnit_Model_Fixture_Interface::SCOPE_SHARED
*/
public function setScope($scope);
/**
* Check that current fixture scope is equal to SCOPE_SHARED
*
* @return boolean
*/
public function isScopeShared();
/**
* Check that current fixture scope is equal to SCOPE_LOCAL
*
* @return boolean
*/
public function isScopeLocal();
/**
* Loads fixture files from test class annotations
*
* @param string $className
* @return EcomDev_PHPUnit_Model_Fixture_Interface
*/
public function loadForClass($className);
} }
\ No newline at end of file
...@@ -58,7 +58,7 @@ class EcomDev_PHPUnit_Model_Mysql4_Fixture extends Mage_Core_Model_Mysql4_Abstra ...@@ -58,7 +58,7 @@ class EcomDev_PHPUnit_Model_Mysql4_Fixture extends Mage_Core_Model_Mysql4_Abstra
$records[] = $this->_getTableRecord($row, $tableColumns); $records[] = $this->_getTableRecord($row, $tableColumns);
} }
$this->_getWriteAdapter()->insertMultiple( $this->_getWriteAdapter()->insertOnDuplicate(
$this->getTable($tableEntity), $this->getTable($tableEntity),
$records $records
); );
......
...@@ -36,6 +36,13 @@ abstract class EcomDev_PHPUnit_Model_Mysql4_Fixture_Eav_Abstract extends EcomDev ...@@ -36,6 +36,13 @@ abstract class EcomDev_PHPUnit_Model_Mysql4_Fixture_Eav_Abstract extends EcomDev
*/ */
protected $_options = array(); protected $_options = array();
/**
* Fixture model
*
* @var EcomDev_PHPUnit_Model_Fixture_Interface
*/
protected $_fixture = null;
/** /**
* Retrieve required indexers for re-building * Retrieve required indexers for re-building
* *
...@@ -46,6 +53,17 @@ abstract class EcomDev_PHPUnit_Model_Mysql4_Fixture_Eav_Abstract extends EcomDev ...@@ -46,6 +53,17 @@ abstract class EcomDev_PHPUnit_Model_Mysql4_Fixture_Eav_Abstract extends EcomDev
return $this->_requiredIndexers; return $this->_requiredIndexers;
} }
/**
* Sets fixture model to EAV loader
*
* @param EcomDev_PHPUnit_Model_Fixture_Interface $fixture
*/
public function setFixture($fixture)
{
$this->_fixture = $fixture;
return $this;
}
/** /**
* Set fixture options * Set fixture options
* *
...@@ -95,8 +113,19 @@ abstract class EcomDev_PHPUnit_Model_Mysql4_Fixture_Eav_Abstract extends EcomDev ...@@ -95,8 +113,19 @@ abstract class EcomDev_PHPUnit_Model_Mysql4_Fixture_Eav_Abstract extends EcomDev
*/ */
public function loadEntity($entityType, $values) public function loadEntity($entityType, $values)
{ {
$originalRequiredIndexers = $this->_requiredIndexers;
if (!empty($this->_options['addRequiredIndex'])) {
foreach ($this->_options['addRequiredIndex'] as $data) {
if (preg_match('/^([a-z0-9_\\-])+\\s+([a-z0-9_\\-])\s*$/i', $data, $match)
&& $match[1] == $entityType) {
$this->_requiredIndexers[] = $match[2];
}
}
}
$entityTypeModel = Mage::getSingleton('eav/config')->getEntityType($entityType); $entityTypeModel = Mage::getSingleton('eav/config')->getEntityType($entityType);
$entityTableColumns = $this->_getWriteAdapter()->describeTable( $entityTableColumns = $this->_getWriteAdapter()->describeTable(
$this->getTable($entityTypeModel->getEntityTable()) $this->getTable($entityTypeModel->getEntityTable())
); );
...@@ -197,6 +226,8 @@ abstract class EcomDev_PHPUnit_Model_Mysql4_Fixture_Eav_Abstract extends EcomDev ...@@ -197,6 +226,8 @@ abstract class EcomDev_PHPUnit_Model_Mysql4_Fixture_Eav_Abstract extends EcomDev
} }
} }
// Restoring original required indexers for making tests isolated
$this->_requiredIndexers = $originalRequiredIndexers;
return $this; return $this;
} }
......
...@@ -25,7 +25,6 @@ class EcomDev_PHPUnit_Model_Mysql4_Fixture_Eav_Catalog_Product extends EcomDev_P ...@@ -25,7 +25,6 @@ class EcomDev_PHPUnit_Model_Mysql4_Fixture_Eav_Catalog_Product extends EcomDev_P
{ {
protected $_requiredIndexers = array( protected $_requiredIndexers = array(
'cataloginventory_stock', 'cataloginventory_stock',
'catalog_product_flat',
'catalog_product_attribute', 'catalog_product_attribute',
'catalog_product_price' 'catalog_product_price'
); );
......
...@@ -271,12 +271,28 @@ abstract class EcomDev_PHPUnit_Test_Case extends PHPUnit_Framework_TestCase ...@@ -271,12 +271,28 @@ abstract class EcomDev_PHPUnit_Test_Case extends PHPUnit_Framework_TestCase
* @return array * @return array
*/ */
public function getAnnotationByName($name, $sources = 'method') public function getAnnotationByName($name, $sources = 'method')
{
return self::getAnnotationByNameFromClass(get_class($this), $name, $sources, $this->getName(false));
}
/**
* Retrieves annotation by its name from different sources (class, method) based on meta information
*
* @param string $className
* @param string $name annotation name
* @param array|string $sources
* @param string $testName test method name
*/
public static function getAnnotationByNameFromClass($className, $name, $sources = 'class', $testName = '')
{ {
if (is_string($sources)) { if (is_string($sources)) {
$sources = array($sources); $sources = array($sources);
} }
$allAnnotations = $this->getAnnotations(); $allAnnotations = PHPUnit_Util_Test::parseTestMethodAnnotations(
$className, $testName
);
$annotation = array(); $annotation = array();
// Walkthrough sources for annotation retrieval // Walkthrough sources for annotation retrieval
...@@ -627,12 +643,30 @@ abstract class EcomDev_PHPUnit_Test_Case extends PHPUnit_Framework_TestCase ...@@ -627,12 +643,30 @@ abstract class EcomDev_PHPUnit_Test_Case extends PHPUnit_Framework_TestCase
* *
* @return EcomDev_PHPUnit_Model_Fixture * @return EcomDev_PHPUnit_Model_Fixture
*/ */
protected function getFixture() protected static function getFixture()
{ {
return Mage::getSingleton($this->getLoadableClassAlias( $fixture = Mage::getSingleton(
self::getLoadableClassAlias(
'fixture', 'fixture',
self::XML_PATH_DEFAULT_FIXTURE_MODEL self::XML_PATH_DEFAULT_FIXTURE_MODEL
));; )
);
if (!$fixture instanceof EcomDev_PHPUnit_Model_Fixture_Interface) {
throw new RuntimeException('Fixture model should implement EcomDev_PHPUnit_Model_Fixture_Interface interface');
}
$storage = Mage::registry(EcomDev_PHPUnit_Model_App::REGISTRY_PATH_SHARED_STORAGE);
if (!$storage instanceof Varien_Object) {
throw new RuntimeException('Fixture storage object was not initialized during test application setup');
}
$fixture->setStorage(
Mage::registry(EcomDev_PHPUnit_Model_App::REGISTRY_PATH_SHARED_STORAGE)
);
return $fixture;
} }
/** /**
...@@ -642,10 +676,12 @@ abstract class EcomDev_PHPUnit_Test_Case extends PHPUnit_Framework_TestCase ...@@ -642,10 +676,12 @@ abstract class EcomDev_PHPUnit_Test_Case extends PHPUnit_Framework_TestCase
*/ */
protected function getExpectation() protected function getExpectation()
{ {
return Mage::getSingleton($this->getLoadableClassAlias( return Mage::getSingleton(
self::getLoadableClassAlias(
'expectation', 'expectation',
self::XML_PATH_DEFAULT_EXPECTATION_MODEL self::XML_PATH_DEFAULT_EXPECTATION_MODEL
)); )
);
} }
...@@ -656,14 +692,17 @@ abstract class EcomDev_PHPUnit_Test_Case extends PHPUnit_Framework_TestCase ...@@ -656,14 +692,17 @@ abstract class EcomDev_PHPUnit_Test_Case extends PHPUnit_Framework_TestCase
* @param string $type * @param string $type
* @param string $configPath * @param string $configPath
*/ */
protected function getLoadableClassAlias($type, $configPath) protected static function getLoadableClassAlias($type, $configPath)
{ {
$annotationValue = $this->getAnnotationByName($type .'Model' , 'class'); $annotationValue = self::getAnnotationByNameFromClass(
get_called_class(),
$type .'Model'
);
if (current($annotationValue)) { if (current($annotationValue)) {
$classAlias = current($annotationValue); $classAlias = current($annotationValue);
} else { } else {
$classAlias = $this->app()->getConfig()->getNode($configPath); $classAlias = self::app()->getConfig()->getNode($configPath);
} }
return $classAlias; return $classAlias;
...@@ -699,12 +738,25 @@ abstract class EcomDev_PHPUnit_Test_Case extends PHPUnit_Framework_TestCase ...@@ -699,12 +738,25 @@ abstract class EcomDev_PHPUnit_Test_Case extends PHPUnit_Framework_TestCase
$name = $this->getName(false); $name = $this->getName(false);
} }
return self::getYamlFilePathByClass(get_called_class(), $type, $name);
}
/**
* Loads YAML file from directory inside of the unit test class
*
* @param string $className class name for looking fixture files
* @param string $type type of YAML data (fixtures,expectations,dataproviders)
* @param string $name the file name for loading
* @return string|boolean
*/
public static function getYamlFilePathByClass($className, $type, $name)
{
if (strrpos($name, '.yaml') !== strlen($name) - 5) { if (strrpos($name, '.yaml') !== strlen($name) - 5) {
$name .= '.yaml'; $name .= '.yaml';
} }
$classFileObject = new SplFileInfo( $classFileObject = new SplFileInfo(
EcomDev_Utils_Reflection::getRelflection($this)->getFileName() EcomDev_Utils_Reflection::getRelflection($className)->getFileName()
); );
$filePath = $classFileObject->getPath() . DS $filePath = $classFileObject->getPath() . DS
...@@ -726,15 +778,38 @@ abstract class EcomDev_PHPUnit_Test_Case extends PHPUnit_Framework_TestCase ...@@ -726,15 +778,38 @@ abstract class EcomDev_PHPUnit_Test_Case extends PHPUnit_Framework_TestCase
*/ */
protected function setUp() protected function setUp()
{ {
$this->getFixture()->loadByTestCase($this); self::getFixture()
->setScope(EcomDev_PHPUnit_Model_Fixture_Interface::SCOPE_LOCAL)
->loadByTestCase($this);
$annotations = $this->getAnnotations(); $annotations = $this->getAnnotations();
$this->getFixture()->setOptions($annotations['method']); self::getFixture()
$this->getFixture()->apply(); ->setOptions($annotations['method'])
->apply();
$this->app()->resetDispatchedEvents(); $this->app()->resetDispatchedEvents();
parent::setUp(); parent::setUp();
} }
/**
* Initializes test environment for subset of tests
*
*/
public static function setUpBeforeClass()
{
self::getFixture()
->setScope(EcomDev_PHPUnit_Model_Fixture_Interface::SCOPE_SHARED)
->loadForClass(get_called_class());
$annotations = PHPUnit_Util_Test::parseTestMethodAnnotations(
get_called_class()
);
self::getFixture()
->setOptions($annotations['class'])
->apply();
parent::setUpBeforeClass();
}
/** /**
* Implements default data provider functionality, * Implements default data provider functionality,
* returns array data loaded from Yaml file with the same name as test method * returns array data loaded from Yaml file with the same name as test method
...@@ -796,9 +871,24 @@ abstract class EcomDev_PHPUnit_Test_Case extends PHPUnit_Framework_TestCase ...@@ -796,9 +871,24 @@ abstract class EcomDev_PHPUnit_Test_Case extends PHPUnit_Framework_TestCase
$this->app()->replaceRegistry($registryPath, $originalValue); $this->app()->replaceRegistry($registryPath, $originalValue);
} }
$this->getFixture()->discard(); // Clear applied fixture self::getFixture()
->setScope(EcomDev_PHPUnit_Model_Fixture_Interface::SCOPE_LOCAL)
->discard(); // Clear applied fixture
parent::tearDown(); parent::tearDown();
} }
/**
* Clean up all the shared fixture data
*
* @return void
*/
public static function tearDownAfterClass()
{
self::getFixture()
->setScope(EcomDev_PHPUnit_Model_Fixture_Interface::SCOPE_SHARED)
->discard();
parent::tearDownAfterClass();
}
} }
\ No newline at end of file
...@@ -44,6 +44,23 @@ class EcomDev_Utils_Reflection ...@@ -44,6 +44,23 @@ class EcomDev_Utils_Reflection
$reflectionProperty->setValue((is_string($object) ? null : $object), $value); $reflectionProperty->setValue((is_string($object) ? null : $object), $value);
} }
/**
* Sets multiple restricted property values for an object
*
* @param string|object $object class name
* @param array $properties
*/
public static function setRestrictedPropertyValues($object, array $properties)
{
if (version_compare(PHP_VERSION, '5.3.0', '<')) {
throw new RuntimeException('For setting of restricted properties via Reflection, PHP version should be 5.3.0 or later');
}
foreach ($properties as $property => $value) {
self::setRestrictedPropertyValue($object, $property, $value);
}
}
/** /**
* Gets protected or private property value * Gets protected or private property value
* *
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment