Commit e9104eec authored by Ivan Chepurnyi's avatar Ivan Chepurnyi

Merge branch 'release/0.3.5'

parents 1cd444fe 460f4a42
...@@ -372,7 +372,7 @@ class EcomDev_PHPUnit_Controller_Request_Http ...@@ -372,7 +372,7 @@ class EcomDev_PHPUnit_Controller_Request_Http
* *
* @see Mage_Core_Controller_Request_Http::getBaseUrl() * @see Mage_Core_Controller_Request_Http::getBaseUrl()
*/ */
public function getBaseUrl() public function getBaseUrl($raw = false)
{ {
return $this->_baseUrl; return $this->_baseUrl;
} }
......
...@@ -124,11 +124,11 @@ class EcomDev_PHPUnit_Model_Config extends Mage_Core_Model_Config ...@@ -124,11 +124,11 @@ class EcomDev_PHPUnit_Model_Config extends Mage_Core_Model_Config
*/ */
public function getModelInstance($modelClass='', $constructArguments=array()) public function getModelInstance($modelClass='', $constructArguments=array())
{ {
if (!isset($this->_replaceInstanceCreation['model'][$modelClass])) { if (!isset($this->_replaceInstanceCreation['model'][(string)$modelClass])) {
return parent::getModelInstance($modelClass, $constructArguments); return parent::getModelInstance((string)$modelClass, $constructArguments);
} }
return $this->_replaceInstanceCreation['model'][$modelClass]; return $this->_replaceInstanceCreation['model'][(string)$modelClass];
} }
/** /**
......
...@@ -301,7 +301,7 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -301,7 +301,7 @@ class EcomDev_PHPUnit_Model_Fixture
public function loadByTestCase(PHPUnit_Framework_TestCase $testCase) public function loadByTestCase(PHPUnit_Framework_TestCase $testCase)
{ {
$fixtures = EcomDev_PHPUnit_Test_Case_Util::getAnnotationByNameFromClass( $fixtures = EcomDev_PHPUnit_Test_Case_Util::getAnnotationByNameFromClass(
get_class($testCase), 'loadFixture', array('class', 'method'), $testCase->getName(false) get_class($testCase), 'loadFixture', array('method', 'class'), $testCase->getName(false)
); );
$this->_loadFixtureFiles($fixtures, $testCase); $this->_loadFixtureFiles($fixtures, $testCase);
...@@ -412,7 +412,7 @@ class EcomDev_PHPUnit_Model_Fixture ...@@ -412,7 +412,7 @@ class EcomDev_PHPUnit_Model_Fixture
if (empty($this->_fixture)) { if (empty($this->_fixture)) {
$this->_fixture = $data; $this->_fixture = $data;
} else { } else {
$this->_fixture = array_merge_recursive($this->_fixture, $data); $this->_fixture = array_replace_recursive($this->_fixture, $data);
} }
return $this; return $this;
......
...@@ -139,6 +139,19 @@ class EcomDev_PHPUnit_Model_Fixture_Processor_Config ...@@ -139,6 +139,19 @@ class EcomDev_PHPUnit_Model_Fixture_Processor_Config
{ {
Mage::getConfig()->loadScopeSnapshot(); Mage::getConfig()->loadScopeSnapshot();
Mage::getConfig()->loadDb(); Mage::getConfig()->loadDb();
// Flush website and store configuration caches
foreach (Mage::app()->getWebsites(true) as $website) {
EcomDev_Utils_Reflection::setRestrictedPropertyValue(
$website, '_configCache', array()
);
}
foreach (Mage::app()->getStores(true) as $store) {
EcomDev_Utils_Reflection::setRestrictedPropertyValue(
$store, '_configCache', array()
);
}
return $this; return $this;
} }
...@@ -160,6 +173,16 @@ class EcomDev_PHPUnit_Model_Fixture_Processor_Config ...@@ -160,6 +173,16 @@ class EcomDev_PHPUnit_Model_Fixture_Processor_Config
$value = $backend->getValue(); $value = $backend->getValue();
} }
if (is_array($value)) {
Mage::throwException(
sprintf(
'There is a collision in configuration value %s. Got: %s',
$path,
print_r($value, true)
)
);
}
Mage::getConfig()->setNode($path, $value); Mage::getConfig()->setNode($path, $value);
return $this; return $this;
} }
......
...@@ -89,10 +89,15 @@ class EcomDev_PHPUnit_Model_Fixture_Processor_Eav ...@@ -89,10 +89,15 @@ class EcomDev_PHPUnit_Model_Fixture_Processor_Eav
$this->getResource()->beginTransaction(); $this->getResource()->beginTransaction();
foreach ($data as $entityType => $values) { foreach ($data as $entityType => $values) {
$eavLoaders[] = $this->_getEavLoader($entityType) $eavLoaders[$entityType] = $this->_getEavLoader($entityType)
->setFixture($fixture) ->setFixture($fixture)
->setOptions($fixture->getOptions()) ->setOptions($fixture->getOptions());
->loadEntity($entityType, $values);
if ($eavLoaders[$entityType] instanceof EcomDev_PHPUnit_Model_Mysql4_Fixture_RestoreAwareInterface) {
$eavLoaders[$entityType]->saveData($entityType);
}
$eavLoaders[$entityType]->loadEntity($entityType, $values);
} }
$this->getResource()->commit(); $this->getResource()->commit();
...@@ -126,16 +131,35 @@ class EcomDev_PHPUnit_Model_Fixture_Processor_Eav ...@@ -126,16 +131,35 @@ class EcomDev_PHPUnit_Model_Fixture_Processor_Eav
EcomDev_PHPUnit_Model_FixtureInterface::SCOPE_SHARED); EcomDev_PHPUnit_Model_FixtureInterface::SCOPE_SHARED);
} }
$typesToRestore = array();
$this->getResource()->beginTransaction(); $this->getResource()->beginTransaction();
foreach (array_keys($data) as $entityType) { foreach (array_keys($data) as $entityType) {
$eavLoader = $this->_getEavLoader($entityType);
if (in_array($entityType, $ignoreCleanUp)) { if (in_array($entityType, $ignoreCleanUp)) {
if ($eavLoader instanceof EcomDev_PHPUnit_Model_Mysql4_Fixture_RestoreAwareInterface) {
$eavLoader->clearData($entityType);
}
continue; continue;
} }
$this->_getEavLoader($entityType)
->cleanEntity($entityType); $eavLoader->cleanEntity($entityType);
if ($eavLoader instanceof EcomDev_PHPUnit_Model_Mysql4_Fixture_RestoreAwareInterface) {
$typesToRestore[$entityType] = $eavLoader;
} }
}
$this->getResource()->commit();
if ($typesToRestore) {
$this->getResource()->beginTransaction();
foreach ($typesToRestore as $entityType => $eavLoader) {
$eavLoader->restoreData($entityType)
->clearData($entityType);
}
$this->getResource()->commit(); $this->getResource()->commit();
}
return $this; return $this;
} }
} }
\ No newline at end of file
...@@ -22,7 +22,10 @@ ...@@ -22,7 +22,10 @@
*/ */
abstract class EcomDev_PHPUnit_Model_Mysql4_Fixture_AbstractEav abstract class EcomDev_PHPUnit_Model_Mysql4_Fixture_AbstractEav
extends EcomDev_PHPUnit_Model_Mysql4_Fixture_AbstractComplex extends EcomDev_PHPUnit_Model_Mysql4_Fixture_AbstractComplex
implements EcomDev_PHPUnit_Model_Mysql4_Fixture_RestoreAwareInterface
{ {
const RESTORE_KEY = 'restore_%s_data';
/** /**
* List of indexers required to build * List of indexers required to build
* *
...@@ -37,6 +40,20 @@ abstract class EcomDev_PHPUnit_Model_Mysql4_Fixture_AbstractEav ...@@ -37,6 +40,20 @@ abstract class EcomDev_PHPUnit_Model_Mysql4_Fixture_AbstractEav
*/ */
protected $_originalIndexers = array(); protected $_originalIndexers = array();
/**
* List of tables that should be restored after run
*
* @var string[]
*/
protected $_restoreTables = array();
/**
* Default data for eav entity
*
* @var array
*/
protected $_defaultData = array();
/** /**
* Retrieve required indexers for re-building * Retrieve required indexers for re-building
* *
...@@ -99,6 +116,69 @@ abstract class EcomDev_PHPUnit_Model_Mysql4_Fixture_AbstractEav ...@@ -99,6 +116,69 @@ abstract class EcomDev_PHPUnit_Model_Mysql4_Fixture_AbstractEav
return $this; return $this;
} }
/**
* Saves data for restoring it after fixture has been cleaned up
*
* @param string $code storage code
* @return $this
*/
public function saveData($code)
{
if ($this->_restoreTables) {
$storageKey = sprintf(self::RESTORE_KEY, $code);
$data = array();
foreach ($this->_restoreTables as $table) {
$select = $this->_getReadAdapter()->select();
$select->from($table);
$data[$table] = $this->_getReadAdapter()->fetchAll($select);
}
$this->_fixture->setStorageData($storageKey, $data);
}
return $this;
}
/**
* Restored saved data
*
* @param string $code storage code
* @return $this
*/
public function restoreData($code)
{
if ($this->_restoreTables) {
$storageKey = sprintf(self::RESTORE_KEY, $code);
$data = $this->_fixture->getStorageData($storageKey);
foreach ($this->_restoreTables as $table) {
if (!empty($data[$table])) {
$this->_getWriteAdapter()->insertOnDuplicate(
$table,
$data[$table]
);
}
}
}
return $this;
}
/**
* Clears storage from stored backup data
*
* @param $code
* @return $this
*/
public function clearData($code)
{
if ($this->_restoreTables) {
$storageKey = sprintf(self::RESTORE_KEY, $code);
$this->_fixture->setStorageData($storageKey, array());
}
return $this;
}
/** /**
* Loads EAV data into DB tables * Loads EAV data into DB tables
* *
...@@ -112,7 +192,7 @@ abstract class EcomDev_PHPUnit_Model_Mysql4_Fixture_AbstractEav ...@@ -112,7 +192,7 @@ abstract class EcomDev_PHPUnit_Model_Mysql4_Fixture_AbstractEav
$this->_originalIndexers = $this->_requiredIndexers; $this->_originalIndexers = $this->_requiredIndexers;
if (!empty($this->_options['addRequiredIndex'])) { if (!empty($this->_options['addRequiredIndex'])) {
foreach ($this->_options['addRequiredIndex'] as $data) { foreach ($this->_options['addRequiredIndex'] as $data) {
if (preg_match('/^([a-z0-9_\\-])+\\s+([a-z0-9_\\-])\s*$/i', $data, $match) if (preg_match('/^([a-z0-9_\\-]+)\\s+([a-z0-9_\\-]+)\s*$/i', $data, $match)
&& $match[1] == $entityType) { && $match[1] == $entityType) {
$this->_requiredIndexers[] = $match[2]; $this->_requiredIndexers[] = $match[2];
} }
...@@ -140,15 +220,42 @@ abstract class EcomDev_PHPUnit_Model_Mysql4_Fixture_AbstractEav ...@@ -140,15 +220,42 @@ abstract class EcomDev_PHPUnit_Model_Mysql4_Fixture_AbstractEav
// See getCustomTableRecords // See getCustomTableRecords
$customValues = array(); $customValues = array();
foreach ($values as $index => &$row) { if ($this->_defaultData) {
$dataToInsert = $this->_defaultData;
// Prevent insertion of default data,
// if there is already data available
foreach ($values as $index => $row) {
if (isset($row[$this->_getEntityIdField($entityTypeModel)])
&& isset($dataToInsert[$this->_getEntityIdField($entityTypeModel)])) {
$dataToInsert = array();
break;
}
}
foreach ($dataToInsert as $row) {
array_unshift($values, $row);
}
}
foreach ($values as $index => $row) {
if (!isset($row[$this->_getEntityIdField($entityTypeModel)])) { if (!isset($row[$this->_getEntityIdField($entityTypeModel)])) {
throw new RuntimeException('Entity Id should be specified in EAV fixture'); throw new RuntimeException('Entity Id should be specified in EAV fixture');
} }
// Fulfill necessary information // Fulfill necessary information
$row['entity_type_id'] = $entityTypeModel->getEntityTypeId(); $values[$index]['entity_type_id'] = $entityTypeModel->getEntityTypeId();
$row = $values[$index];
if (!isset($row['attribute_set_id'])) { if (!isset($row['attribute_set_id'])) {
$row['attribute_set_id'] = $entityTypeModel->getDefaultAttributeSetId(); $defaultAttributeSet = $entityTypeModel->getDefaultAttributeSetId();
// Fix Magento core issue with attribute set information for customer and its address
if (in_array($entityType, array('customer', 'customer_address'))) {
$defaultAttributeSet = 0;
}
$values[$index]['attribute_set_id'] = $defaultAttributeSet;
} }
// Preparing entity table record // Preparing entity table record
......
...@@ -23,10 +23,37 @@ ...@@ -23,10 +23,37 @@
*/ */
class EcomDev_PHPUnit_Model_Mysql4_Fixture_Eav_Catalog_Category extends EcomDev_PHPUnit_Model_Mysql4_Fixture_Eav_Catalog_Abstract class EcomDev_PHPUnit_Model_Mysql4_Fixture_Eav_Catalog_Category extends EcomDev_PHPUnit_Model_Mysql4_Fixture_Eav_Catalog_Abstract
{ {
const XML_PATH_DEFAULT_DATA = 'phpunit/suite/fixture/default_data/category';
protected $_requiredIndexers = array( protected $_requiredIndexers = array(
'catalog_category_flat' 'catalog_category_flat'
); );
protected function _construct()
{
parent::_construct();
$defaultData = Mage::getConfig()->getNode(self::XML_PATH_DEFAULT_DATA);
if ($defaultData) {
foreach ($defaultData->children() as $item) {
if (!isset($item->entity_id)) {
continue;
}
$entityId = (string)$item->entity_id;
$this->_defaultData[$entityId] = array();
foreach ($item->children() as $value) {
$this->_defaultData[$entityId][$value->getName()] = (string)$value;
}
}
}
$this->_restoreTables[] = $this->getTable('catalog/category');
foreach (array('datetime', 'decimal', 'int', 'text', 'varchar') as $suffix) {
$this->_restoreTables[] = $this->getTable(array('catalog/category', $suffix));
}
}
/** /**
* Overridden to add easy fixture loading for product associations * Overridden to add easy fixture loading for product associations
* (non-PHPdoc) * (non-PHPdoc)
......
<?php
interface EcomDev_PHPUnit_Model_Mysql4_Fixture_RestoreAwareInterface
{
/**
* Saves data for restoring it after fixture has been cleaned up
*
* @param string $code storage code
* @return $this
*/
public function saveData($code);
/**
* Restored saved data
*
* @param string $code storage code
* @return $this
*/
public function restoreData($code);
/**
* Clears storage from stored backup data
*
* @param $code
* @return $this
*/
public function clearData($code);
}
\ No newline at end of file
...@@ -43,10 +43,19 @@ class EcomDev_PHPUnit_Test_Case_Helper_Session ...@@ -43,10 +43,19 @@ class EcomDev_PHPUnit_Test_Case_Helper_Session
*/ */
public function helperMockSession($classAlias, array $methods = array()) public function helperMockSession($classAlias, array $methods = array())
{ {
if (!empty($methods) && !in_array('start', $methods, true)) {
$methods[] = 'start';
}
$sessionMock = EcomDev_PHPUnit_Helper::invoke('mockModel', $classAlias, $methods) $sessionMock = EcomDev_PHPUnit_Helper::invoke('mockModel', $classAlias, $methods)
->disableOriginalConstructor(); ->disableOriginalConstructor();
TestUtil::replaceByMock('singleton', $classAlias, $sessionMock); TestUtil::replaceByMock('singleton', $classAlias, $sessionMock);
$sessionMock->expects($this->testCase->any())
->method('start')
->willReturnSelf();
return $sessionMock; return $sessionMock;
} }
...@@ -58,6 +67,8 @@ class EcomDev_PHPUnit_Test_Case_Helper_Session ...@@ -58,6 +67,8 @@ class EcomDev_PHPUnit_Test_Case_Helper_Session
*/ */
public function helperAdminSession(array $resources = array()) public function helperAdminSession(array $resources = array())
{ {
$this->helperMockSession('core/session', array('renew'));
$this->helperMockSession('adminhtml/session', array('renew'));
$session = $this->helperMockSession('admin/session', array('refreshAcl')); $session = $this->helperMockSession('admin/session', array('refreshAcl'));
$user = $this->createUser(); $user = $this->createUser();
$this->loadRules($user, $this->getAcl(), $resources); $this->loadRules($user, $this->getAcl(), $resources);
......
...@@ -50,6 +50,8 @@ class EcomDev_PHPUnit_Test_Suite extends PHPUnit_Framework_TestSuite ...@@ -50,6 +50,8 @@ class EcomDev_PHPUnit_Test_Suite extends PHPUnit_Framework_TestSuite
$suite = new self('Magento Test Suite'); $suite = new self('Magento Test Suite');
$excludedModules = Mage::getConfig()->getNode('phpunit/suite/exclude');
// Walk through different groups in modules for finding test cases // Walk through different groups in modules for finding test cases
foreach ($groups->children() as $group) { foreach ($groups->children() as $group) {
foreach ($modules->children() as $module) { foreach ($modules->children() as $module) {
...@@ -59,6 +61,10 @@ class EcomDev_PHPUnit_Test_Suite extends PHPUnit_Framework_TestSuite ...@@ -59,6 +61,10 @@ class EcomDev_PHPUnit_Test_Suite extends PHPUnit_Framework_TestSuite
continue; continue;
} }
if (isset($excludedModules->{$module->getName()})) {
continue;
}
$moduleCodeDir = Mage::getBaseDir('code') . DS . (string) $realModule->codePool; $moduleCodeDir = Mage::getBaseDir('code') . DS . (string) $realModule->codePool;
$searchPath = Mage::getModuleDir('', $module->getName()) . DS . 'Test' . DS . (string) $group; $searchPath = Mage::getModuleDir('', $module->getName()) . DS . 'Test' . DS . (string) $group;
......
...@@ -68,9 +68,18 @@ class EcomDev_PHPUnit_Test_Suite_Group extends PHPUnit_Framework_TestSuite ...@@ -68,9 +68,18 @@ class EcomDev_PHPUnit_Test_Suite_Group extends PHPUnit_Framework_TestSuite
* impossibility for specifying group by parent test case * impossibility for specifying group by parent test case
* Because it is a very dirty hack :( * Because it is a very dirty hack :(
**/ **/
$testGroups = array(); $testGroups = EcomDev_Utils_Reflection::getRestrictedPropertyValue($test, 'groups');
foreach ($groups as $group) { foreach ($groups as $group) {
if(!isset($testGroups[$group])) {
$testGroups[$group] = $test->tests(); $testGroups[$group] = $test->tests();
} else {
foreach($test->tests() as $subTest) {
if(!in_array($subTest, $testGroups[$group], true)) {
$testGroups[$group][] = $subTest;
}
}
}
} }
EcomDev_Utils_Reflection::setRestrictedPropertyValue( EcomDev_Utils_Reflection::setRestrictedPropertyValue(
......
...@@ -55,6 +55,8 @@ ...@@ -55,6 +55,8 @@
</global> </global>
<phpunit> <phpunit>
<suite> <suite>
<modules /> <!-- List of modules included into test suite -->
<exclude /> <!-- List of modules that are excluded from test suite -->
<yaml> <yaml>
<model>ecomdev_phpunit/yaml_loader</model> <model>ecomdev_phpunit/yaml_loader</model>
<loaders> <loaders>
...@@ -112,6 +114,37 @@ ...@@ -112,6 +114,37 @@
<catalog_product>ecomdev_phpunit/fixture_eav_catalog_product</catalog_product> <catalog_product>ecomdev_phpunit/fixture_eav_catalog_product</catalog_product>
<catalog_category>ecomdev_phpunit/fixture_eav_catalog_category</catalog_category> <catalog_category>ecomdev_phpunit/fixture_eav_catalog_category</catalog_category>
</eav> </eav>
<default_data>
<category>
<root>
<entity_id>1</entity_id>
<parent_id>0</parent_id>
<path>1</path>
<position>0</position>
<level>0</level>
<children_count>1</children_count>
<name>Root Catalog</name>
<url_key>root-catalog</url_key>
<is_active>1</is_active>
<is_anchor>0</is_anchor>
<attribute_set_id>0</attribute_set_id>
</root>
<default_category>
<entity_id>2</entity_id>
<parent_id>1</parent_id>
<path>1/2</path>
<position>1</position>
<level>1</level>
<children_count>0</children_count>
<name>Default Category</name>
<url_key>default-category</url_key>
<is_active>1</is_active>
<is_anchor>0</is_anchor>
<display_mode>PRODUCTS</display_mode>
<include_in_menu>1</include_in_menu>
</default_category>
</category>
</default_data>
</fixture> </fixture>
<app> <app>
<!-- Application class name for running tests --> <!-- Application class name for running tests -->
......
<?php
/**
* @loadSharedFixture testFixtureArrayMerge.yaml
*/
class EcomDev_PHPUnitTest_Test_Model_Fixture extends EcomDev_PHPUnit_Test_Case
{
public function testFixtureArrayMerge()
{
require_once($this->_getVfsUrl('app/code/community/EcomDev/PHPUnit/Test/Model/ExampleClass.php'));
$testCase = new EcomDev_PHPUnitTest_Test_Model_ExampleClass();
$testCase->setName('testLoadFixtureOrder');
$this->getFixture()->loadForClass(get_class($testCase));
$this->getFixture()->loadByTestCase($testCase);
$this->getFixture()->apply();
}
public function testLoadClassBeforeMethodFixtures()
{
require_once($this->_getVfsUrl('app/code/community/EcomDev/PHPUnit/Test/Model/ExampleClass.php'));
$testCase = new EcomDev_PHPUnitTest_Test_Model_ExampleClass();
$testCase->setName('testLoadFixtureOrder');
$this->getFixture()->loadForClass(get_class($testCase));
$this->getFixture()->loadByTestCase($testCase);
$this->getFixture()->apply();
$this->assertEquals('methodFixtureValue', Mage::getStoreConfig('sample/path'));
}
protected function _getVfsUrl($path)
{
return $this->getFixture()->getVfs()->url($path);
}
}
vfs:
app:
code:
community:
EcomDev:
PHPUnit:
Test:
Model:
ExampleClass.php: |
<?php
/**
* @loadSharedFixture sharedClassFixture.yaml
* @loadFixture classFixture.yaml
*/
class EcomDev_PHPUnitTest_Test_Model_ExampleClass extends EcomDev_PHPUnit_Test_Case
{
/**
* @loadSharedFixture sharedMethodFixture.yaml
* @loadFixture methodFixture.yaml
*/
public function testLoadFixtureOrder()
{
}
}
fixtures:
classFixture.yaml: >
config:
default/sample/path: classFixtureValue
sharedClassFixture.yaml: >
config:
default/sample/path: sharedClassFixtureValue
sharedMethodFixture.yaml: >
config:
default/sample/path: sharedMethodFixtureValue
methodFixture.yaml: >
config:
default/sample/path: methodFixtureValue
...@@ -8,6 +8,9 @@ ...@@ -8,6 +8,9 @@
"magento-hackathon/magento-composer-installer": "*", "magento-hackathon/magento-composer-installer": "*",
"phpunit/phpunit": "4.1.*" "phpunit/phpunit": "4.1.*"
}, },
"replace": {
"ivanchepurnyi/ecomdev_phpunit":"*"
},
"authors":[ "authors":[
{ {
"name":"Ivan Chepurnyi" "name":"Ivan Chepurnyi"
......
...@@ -292,7 +292,7 @@ class EcomDev_PHPUnit_Constraint_Layout_Block extends EcomDev_PHPUnit_Constraint ...@@ -292,7 +292,7 @@ class EcomDev_PHPUnit_Constraint_Layout_Block extends EcomDev_PHPUnit_Constraint
return false; return false;
} }
return $blockInfo['root'] === true; return $blockInfo['is_root'] === true;
} }
/** /**
......
...@@ -81,7 +81,7 @@ class EcomDev_PHPUnit_Constraint_Layout_Block_Property ...@@ -81,7 +81,7 @@ class EcomDev_PHPUnit_Constraint_Layout_Block_Property
* (non-PHPdoc) * (non-PHPdoc)
* @see EcomDev_PHPUnit_ConstraintAbstract::getActualValue() * @see EcomDev_PHPUnit_ConstraintAbstract::getActualValue()
*/ */
protected function getActualValue($other) protected function getActualValue($other = null)
{ {
if ($this->_useActualValue) { if ($this->_useActualValue) {
if ($this->_actualValue instanceof Varien_Object) { if ($this->_actualValue instanceof Varien_Object) {
......
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