Commit 631564b8 authored by Son Nguyen's avatar Son Nguyen

Add new features for mahara behat

1. Data generator
2. Config manager
3. Form fixtures
4. Some sample test features

Add shell script for running Behat

Moving core feature files to 'test' folder outside the docroot

Fix Mysql bugs

Change-Id: I00f3558c178541ae7f81ce9fb1ce6226e7a9654e
Signed-off-by: 's avatarAaron Barnes <aaronb@catalyst.net.nz>
Signed-off-by: 's avatarSon Nguyen <son.nguyen@catalyst.net.nz>
parent d7485f0f
......@@ -14,6 +14,7 @@ mahara-*.zip
/test/.project
/test/.buildpath
/test/.settings
/test/behat/selenium-server-standalone*
/configure-stamp
/debian/files
/debian/mahara-apache.postinst.debhelper
......@@ -25,3 +26,4 @@ mahara-*.zip
/debian/mahara-apache2.substvars
/debian/mahara-apache2
.DS_Store
/external
{
"require": {
"php": ">=5.3.2",
"behat/behat": "2.5.1",
"behat/mink": "1.5.0",
"behat/behat": "~2.5",
"behat/mink": "*",
"behat/mink-extension": "*",
"behat/mink-goutte-driver": "*",
"behat/mink-selenium-driver": "*",
"behat/mink-selenium2-driver": "*"
"behat/mink-selenium2-driver": "*",
"fabpot/goutte": "~1.0"
},
"minimum-stability": "dev"
......
<?php
/**
* @package mahara
* @subpackage test/behat
* @author Son Nguyen, Catalyst IT Ltd
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
* @copyright For copyright information on Mahara, please see the README file distributed with this software.
* @copyright portions from mahara Behat, 2013 David Monllaó
*
*/
require_once(dirname(dirname(dirname(__DIR__))) . '/testing/frameworks/behat/classes/BehatBase.php');
use Behat\Behat\Context\Step\Given as Given,
Behat\Gherkin\Node\TableNode as TableNode,
Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
/**
* Account steps definitions.
*
*/
class BehatAccount extends BehatBase {
/**
* Sets the specified account settings. A table with | Setting label | value | is expected.
*
* @Given /^the following account settings have set:$/
* @param TableNode $table
*/
public function i_set_the_following_account_settings_values(TableNode $table) {
// @TODO implement this step definition
}
}
\ No newline at end of file
@javasript @plugin @artefact.blog
Feature: Mahara users can create their blogs
As a mahara user
I need to create blogs
Scenario: create blogs
Given the following "users" exist:
| user | password | | institution | role |
| userA | Password1 | | mahara | member |
And the following account settings have set:
| Multiple journals | ON |
When I log in as "userA" with password "Password1"
And I follow "Content"
And I follow "Journal"
Then I should see "Journals"
When I press "Create journal"
And I fill in the following:
| title | My new journal |
| description | <p>This is my new journal</p> |
| tags | blog |
And I press "Create journal"
Then I should see "My new journal"
\ No newline at end of file
......@@ -338,7 +338,7 @@ function xmldb_core_upgrade($oldversion=0) {
delete_records('usr_watchlist_view','view',$viewid);
if ($blockinstanceids = get_column('block_instance', 'id', 'view', $viewid)) {
foreach ($blockinstanceids as $id) {
if (table_exists('blocktype_wall_post')) {
if (table_exists(new XMLDBTable('blocktype_wall_post'))) {
delete_records('blocktype_wall_post', 'instance', $id);
}
delete_records('view_artefact', 'block', $id);
......
......@@ -682,7 +682,9 @@ function uninstall_from_xmldb_file($file) {
if ($tables = array_reverse($structure->getTables())) {
foreach ($tables as $table) {
if ($indexes = $table->getIndexes()) {
// for MySQL, skip dropping indexs and keys
// as they will be dropped when the table is dropped
if (!is_mysql() && $indexes = $table->getIndexes()) {
foreach ($indexes as $index) {
if ($index->getName() == 'usernameuk' && is_postgres()) {
// this is a giant hack, but adodb cannot handle resolving
......@@ -694,7 +696,7 @@ function uninstall_from_xmldb_file($file) {
drop_index($table, $index);
}
}
if ($keys = $table->getKeys()) {
if (!is_mysql() && $keys = $table->getKeys()) {
$sortkeys = array();
foreach ($keys as $key) {
$sortkeys[] = $key->type;
......@@ -1413,63 +1415,189 @@ function rename_index($table, $index, $newname, $continue=true, $feedback=true)
}
/**
* Return all tables in current db
* Return structure info of tables from a xmldb file
*
* @return array('tablename' => 'tablename', ...)
* Note all table names is in lower cases
* @param string $file
* @return array(XMLDBTable)
* @throws InstallationException
*/
function get_tables() {
function get_tables_from_xmldb_file($file) {
global $CFG, $db;
// Get all tables in current DB
$tables = $metatables = $db->MetaTables('TABLES');
if (!empty($CFG->prefix)) {
$tables = array();
foreach ($metatables as $mtable) {
if (strpos($mtable, $CFG->prefix) !== false) {
$tables[] = $mtable;
$status = true;
$xmldb_file = new XMLDBFile($file);
if (!$xmldb_file->fileExists()) {
throw new InstallationException($xmldb_file->path . " doesn't exist.");
}
$loaded = $xmldb_file->loadXMLStructure();
if (!$loaded || !$xmldb_file->isLoaded()) {
throw new InstallationException("Could not load " . $xmldb_file->path);
}
$structure = $xmldb_file->getStructure();
return array_reverse($structure->getTables());
}
/**
* Return structure info of tables from mahara xmldb files
*
* @return array(XMLDBTable)
*/
function get_tables_from_xmldb() {
static $tables = array();
if (!empty($tables)) {
return $tables;
}
// Get database structure from plugins' tables
foreach (array_reverse(plugin_types_installed()) as $t) {
if ($installed = plugins_installed($t, true)) {
foreach ($installed as $p) {
$location = get_config('docroot') . $t . '/' . $p->name. '/db/';
if (is_readable($location . 'install.xml')) {
$tables = array_merge($tables, get_tables_from_xmldb_file($location . 'install.xml'));
}
}
}
}
unset($metatables);
$tnames = array();
foreach ($tables as $t) {
$t = strtolower($t);
$tnames[$t] = $t;
}
$tables = array_merge($tables, get_tables_from_xmldb_file(get_config('docroot') . 'lib/db/install.xml'));
return $tnames;
return $tables;
}
/**
* Return all columns of a table in current db
*
* @param string $tablename should be a full name including the dbprefix
* @param string $tablename not including the dbprefix
* @return array of ADOFieldObject
*/
function get_columns($tablename) {
global $CFG, $db;
$columns = $db->MetaColumns($tablename);
// Update the field Auto_increment if postgres
// Only apply for "id" field
if (is_postgres()) {
if (isset($columns['id'])) {
$idcolumn = $columns['id'];
if (isset($idcolumn->primary_key) && ($idcolumn->primary_key === 1)
&& isset($idcolumn->default_value)
&& strpos($idcolumn->default_value, 'nextval(') !== false ) {
$rec = get_record_sql('SELECT last_value FROM '. "{$tablename}" . '_id_seq');
$idcolumn->Auto_increment = $rec->last_value + 1;
$fulltablename = $CFG->dbprefix . $tablename;
$columns = $db->MetaColumns($fulltablename);
// Update the field auto_increment if postgres
// Only apply for "ID" field
if (is_postgres() && isset($columns['ID'])) {
$idcolumn = $columns['ID'];
if (isset($idcolumn->default_value)
&& strpos($idcolumn->default_value, 'nextval(') !== false ) {
if (record_exists($tablename)) {
$rec = get_record_sql('SELECT last_value FROM "' . $fulltablename . '_id_seq"');
$idcolumn->auto_increment = $rec->last_value + 1;
}
else {
$idcolumn->auto_increment = 1;
}
$columns['id'] = $idcolumn;
}
$columns['ID'] = $idcolumn;
}
return $columns;
}
/**
* Return current foreign key constraints in given table
*
* @param string $tablename not including the dbprefix
* @return array of array(
* 'constraintname' => string
* 'table' => string
* 'fields' => array
* 'reftable' => string
* 'reffields' => array
* )
*/
function get_foreign_keys($tablename) {
global $CFG;
$tablename = $CFG->dbprefix . $tablename;
$foreignkeys = array();
// Get foreign key constraints from information_schema tables
if (is_postgres()) {
$dbfield = 'catalog';
// The query to find all the columns for a foreign key constraint
$fkcolsql = "
SELECT
ku.column_name,
ccu.table_name AS reftable_name,
ccu.column_name AS refcolumn_name
FROM
information_schema.key_column_usage ku
INNER JOIN information_schema.constraint_column_usage ccu
ON ku.constraint_name = ccu.constraint_name
AND ccu.constraint_schema = ku.constraint_schema
AND ccu.constraint_catalog = ku.constraint_catalog
AND ccu.table_catalog = ku.constraint_catalog
AND ccu.table_schema = ku.constraint_schema
WHERE
ku.constraint_catalog = ?
AND ku.constraint_name = ?
AND ku.table_name = ?
AND ku.table_catalog = ?
ORDER BY ku.ordinal_position, ku.position_in_unique_constraint
";
}
else {
$dbfield = 'schema';
// The query to find all the columns for a foreign key constraint
$fkcolsql = '
SELECT
ku.column_name,
ku.referenced_table_name AS reftable_name,
ku.referenced_column_name AS refcolumn_name
FROM information_schema.key_column_usage ku
WHERE
ku.constraint_schema = ?
AND ku.constraint_name = ?
AND ku.table_name = ?
AND ku.table_schema = ?
ORDER BY ku.ordinal_position, ku.position_in_unique_constraint
';
}
$sql = "
SELECT tc.constraint_name
FROM information_schema.table_constraints tc
WHERE
tc.table_name = ?
AND tc.table_{$dbfield} = ?
AND tc.constraint_{$dbfield} = ?
AND tc.constraint_type = ?
";
$dbname = get_config('dbname');
if ($constraintrec = get_records_sql_array($sql, array($tablename, $dbname, $dbname, 'FOREIGN KEY'))) {
// Get foreign key constraint info
foreach ($constraintrec as $c) {
$fields = array();
$reftable = '';
$reffields = array();
if ($colrecs = get_records_sql_array($fkcolsql, array($dbname, $c->constraint_name, $tablename, $dbname))) {
foreach ($colrecs as $colrec) {
if (empty($reftable)) {
$reftable = $colrec->reftable_name;
}
$fields[] = $colrec->column_name;
$reffields[] = $colrec->refcolumn_name;
}
}
if (!empty($fields) && !empty($reftable) && !empty($reffields)) {
$foreignkeys[] = array(
'table' => $tablename,
'constraintname' => $c->constraint_name,
'fields' => $fields,
'reftable' => $reftable,
'reffields' => $reffields,
);
}
}
}
return $foreignkeys;
}
/**
* Return the server info
*
......
......@@ -612,7 +612,7 @@ class View {
return new View($view->get('id')); // Reread to ensure defaults are set
}
public function default_columnsperrow() {
public static function default_columnsperrow() {
$default = array(1 => (object)array('row' => 1, 'columns' => 3, 'widths' => '33,33,33'));
if (!$id = get_field('view_layout_columns', 'id', 'columns', $default[1]->columns, 'widths', $default[1]->widths)) {
throw new SystemException("View::default_columnsperrow: Default columns = 3, widths = '33,33,33' not in view_layout_columns table");
......
......@@ -164,7 +164,7 @@ class XMLDBObject {
function checkName () {
$result = true;
if ($this->name != eregi_replace('[^a-z0-9_ -]', '', $this->name)) {
if ($this->name != preg_replace('/[^a-z0-9_ -]/i', '', $this->name)) {
$result = false;
}
return $result;
......
......@@ -16,5 +16,5 @@
// NOTE: MOODLE_INTERNAL is not verified here because we load this before setup.php!
require_once(dir(__FILE__) . '/testing_data_generator.php');
require_once(dir(__FILE__) . '/data_generator_base.php');
require_once(__DIR__ . '/TestingDataGenerator.php');
require_once(__DIR__ . '/DataGeneratorBase.php');
This diff is collapsed.
......@@ -10,22 +10,17 @@
*/
/**
* Base class of all steps definitions.
* Behat base class for mahara step definitions.
*
* All mahara step definitions should be extended from this class
*/
use Behat\Mink\Exception\ExpectationException as ExpectationException,
Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
Behat\Mink\Element\NodeElement as NodeElement;
/**
* Steps definitions base class.
*
* To extend by the steps definitions of the different Moodle components.
*
* It can not contain steps definitions to avoid duplicates, only utility
* methods shared between steps.
* Base class
*
* @method NodeElement find_field(string $locator) Finds a form element
* @method NodeElement find_button(string $locator) Finds a form input submit element or a button
......@@ -57,7 +52,7 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext {
/**
* The JS code to check that the page is ready.
*/
const PAGE_READY_JS = '(is_page_ready) && (document.readyState === "complete")';
const PAGE_READY_JS = '(document.readyState === "complete")';
/**
* Locates url, based on provided path.
......@@ -116,7 +111,8 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext {
$locator[1] = html_entity_decode($locator[1], ENT_NOQUOTES);
}
} else {
}
else {
$exceptiontype = $selector;
$exceptionlocator = $locator;
}
......@@ -151,7 +147,8 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext {
// We are in the container node.
if (strpos($union, '.') === 0) {
$union = substr($union, 1);
} else if (strpos($union, '/') !== 0) {
}
else if (strpos($union, '/') !== 0) {
// Adding the path separator in case it is not there.
$union = '/' . $union;
}
......@@ -266,7 +263,8 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext {
if ($microsleep) {
// Will sleep 1/10th of a second by default for self::TIMEOUT seconds.
$loops = $timeout * 10;
} else {
}
else {
// Will sleep for self::TIMEOUT seconds.
$loops = $timeout;
}
......@@ -280,7 +278,8 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext {
if ($return = call_user_func($lambda, $this, $args)) {
return $return;
}
} catch (Exception $e) {
}
catch (Exception $e) {
// We would use the first closure exception if no exception has been provided.
if (!$exception) {
$exception = $e;
......@@ -291,7 +290,8 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext {
if ($microsleep) {
usleep(100000);
} else {
}
else {
sleep(1);
}
}
......@@ -384,12 +384,12 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext {
protected function transform_selector($selectortype, $element) {
// Here we don't know if an allowed text selector is being used.
$selectors = behat_selectors::get_allowed_selectors();
$selectors = BehatSelectors::get_allowed_selectors();
if (!isset($selectors[$selectortype])) {
throw new ExpectationException('The "' . $selectortype . '" selector type does not exist', $this->getSession());
}
return behat_selectors::get_behat_selector($selectortype, $element, $this->getSession());
return BehatSelectors::get_behat_selector($selectortype, $element, $this->getSession());
}
/**
......@@ -405,7 +405,7 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext {
*/
protected function transform_text_selector($selectortype, $element) {
$selectors = behat_selectors::get_allowed_text_selectors();
$selectors = BehatSelectors::get_allowed_text_selectors();
if (empty($selectors[$selectortype])) {
throw new ExpectationException('The "' . $selectortype . '" selector can not be used to select text nodes', $this->getSession());
}
......@@ -562,7 +562,8 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext {
// If there are no editors we don't need to wait.
try {
$this->find('css', '.mceEditor');
} catch (ElementNotFoundException $e) {
}
catch (ElementNotFoundException $e) {
return;
}
......@@ -667,11 +668,13 @@ class BehatBase extends Behat\MinkExtension\Context\RawMinkContext {
try {
$jscode = 'return ' . self::PAGE_READY_JS;
$ready = $this->getSession()->evaluateScript($jscode);
} catch (NoSuchWindow $nsw) {
}
catch (NoSuchWindow $nsw) {
// We catch an exception here, in case we just closed the window we were interacting with.
// No javascript is running if there is no window right?
$ready = true;
} catch (UnknownError $e) {
}
catch (UnknownError $e) {
$ready = true;
}
......
......@@ -58,7 +58,7 @@ class BehatCommand {
$separator = DIRECTORY_SEPARATOR;
$exec = 'behat';
return 'vendor' . $separator . 'bin' . $separator . $exec;
return $separator . 'vendor' . $separator . 'bin' . $separator . $exec;
}
/**
......@@ -73,8 +73,9 @@ class BehatCommand {
global $CFG;
$currentcwd = getcwd();
// Change to composer installed directory
chdir($CFG->docroot);
exec(self::get_behat_command() . ' ' . $options, $output, $code);
exec(get_composerroot_dir() . self::get_behat_command() . ' ' . $options . ' 2>/dev/null', $output, $code);
chdir($currentcwd);
return array($output, $code);
......@@ -150,7 +151,7 @@ class BehatCommand {
* @return bool
*/
public static function are_behat_dependencies_installed() {
if (!is_dir(dirname(dirname(dirname(dirname(__DIR__)))) . '/vendor/behat')) {
if (!is_dir(get_composerroot_dir() . '/vendor/behat')) {
return false;
}
return true;
......@@ -176,7 +177,8 @@ class BehatCommand {
// Stopping execution.
exit(1);
} else {
}
else {
// We continue execution after this.
$clibehaterrorstr = "Ensure you set \$CFG->behat_* vars in config.php " .
......
......@@ -49,13 +49,15 @@ class BehatConfigManager {
// Behat must have a separate behat.yml to have access to the whole set of features and steps definitions.
if ($testsrunner === true) {
$configfilepath = BehatCommand::get_behat_dir() . '/behat.yml';
} else {
}
else {
// Alternative for steps definitions filtering, one for each user.
$configfilepath = self::get_steps_list_config_filepath();
}
// Get core features
$features = array(dirname(dirname(dirname(dirname(dirname(__DIR__))))) . '/test/behat/features');
// Gets all the plugins with features.
$features = array();
$plugins = TestsFinder::get_plugins_with_tests('features');
if ($plugins) {
foreach ($plugins as $pluginname => $path) {
......@@ -67,7 +69,7 @@ class BehatConfigManager {
$featurespaths[$uniquekey] = $path;
}
}
$features = array_values($featurespaths);
$features = array_merge($features, array_values($featurespaths));
}
// Optionally include features from additional directories.
......@@ -75,8 +77,16 @@ class BehatConfigManager {
$features = array_merge($features, array_map("realpath", $CFG->behat_additionalfeatures));
}
// Gets all the plugins with steps definitions.
$stepsdefinitions = array();
// Find step definitions from core. They must be in the folder $MAHARA_ROOT/test/behat/stepdefinitions
// The file name must be /^Behat[A-z0-9_]+\.php$/
$regite = new RegexIterator(new DirectoryIterator(get_mahararoot_dir() . '/test/behat/stepdefinitions'), '|^Behat[A-z0-9_\-]+\.php$|');
foreach ($regite as $file) {
$key = $file->getBasename('.php');
$stepsdefinitions[$key] = $file->getPathname();
}
// Gets all the plugins with steps definitions.
$steps = self::get_plugins_steps_definitions();
if ($steps) {
foreach ($steps as $key => $filepath) {
......@@ -120,6 +130,7 @@ class BehatConfigManager {
}
$stepsdefinitions = array();
// Find step definitions from plugins
foreach ($plugins as $pluginname => $pluginpath) {
$pluginpath = self::clean_path($pluginpath);
......@@ -127,7 +138,7 @@ class BehatConfigManager {
continue;
}
$diriterator = new DirectoryIterator($pluginpath . self::get_behat_tests_path());
$regite = new RegexIterator($diriterator, '|Behat.*\.php$|');
$regite = new RegexIterator($diriterator, '|^Behat.*\.php$|');
// All Behat*.php inside BehatConfigManager::get_behat_tests_path() are added as steps definitions files.
foreach ($regite as $file) {
......@@ -179,11 +190,11 @@ class BehatConfigManager {
global $CFG;
// We require here when we are sure behat dependencies are available.
require_once($CFG->docroot . '/vendor/autoload.php');
require_once($CFG->docroot . '../external/vendor/autoload.php');
// It is possible that it has no value as we don't require a full behat setup to list the step definitions.
if (empty($CFG->behat_wwwroot)) {
$CFG->behat_wwwroot = 'http://itwillnotbeused.com';
$CFG->behat_wwwroot = 'http://example.com';
}
$basedir = $CFG->docroot . 'testing' . DIRECTORY_SEPARATOR . 'frameworks' . DIRECTORY_SEPARATOR . 'behat';
......@@ -199,6 +210,7 @@ class BehatConfigManager {
'extensions' => array(
'Behat\MinkExtension\Extension' => array(
'base_url' => $CFG->behat_wwwroot,
'files_path' => get_mahararoot_dir() . '/test/behat/upload_files',
'goutte' => null,
'selenium2' => null
),
......@@ -254,7 +266,8 @@ class BehatConfigManager {
// Add the param if it doesn't exists or merge branches.
if (empty($config[$key])) {
$config[$key] = $value;
} else {
}
else {
$config[$key] = self::merge_config($config[$key], $localconfig[$key]);
}
}
......
<?php
/**
* @package mahara
* @subpackage test/behat
* @author Son Nguyen, Catalyst IT Ltd
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
* @copyright For copyright information on Mahara, please see the README file distributed with this software.
* @copyright portions from Moodle Behat, 2013 David Monllaó
*
*/
require_once(__DIR__ . '/BehatBase.php');
use Behat\Gherkin\Node\TableNode as TableNode;
use Behat\Behat\Exception\PendingException as PendingException;
/**
* Class to set up quickly a Given environment.
*
*/
class BehatDataGenerators extends BehatBase {
/**
* @var testing_data_generator
*/
protected $datagenerator;
/**
* Each element specifies:
* - The data generator suffix used.
* - The available fields array(fieldname=>fieldtype)
* - The required fields.
* - The mapping between other elements references and database field names.
* @var array
*/
protected static $elements = array(
'users' => array(
'datagenerator' => 'user',
'available' => array(
'username' => 'text',
'password' => 'text',
'email' => 'text',
'firstname' => 'text',
'lastname' => 'text',
'institution' => 'text',
'role' => 'text',
'authname' => 'text',
'remoteusername' => 'text',
),
'required' => array('username', 'password', 'email', 'firstname', 'lastname')
),
'groups' => array(
'datagenerator' => 'group',
'available' => array(
'name' => 'text',
'owner' => 'text',
'description' => 'text',
'grouptype' => 'text',
'open' => 'bool',
'controlled' => 'bool',
'request' => 'bool',
'invitefriends' => 'bool',
'suggestfriends' => 'bool',
'editroles' => 'text',
'submittableto' => 'bool',
'allowarchives' => 'bool',
'editwindowstart' => 'text',
'editwindowend' => 'text',