From 285c703685a6d89cf73e6900df8168dc6a1f511b Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Thu, 25 Apr 2019 20:57:44 +0100 Subject: [PATCH] MDL-48024 behat: allow plugins to have data generators This extends the step Given the following "users" exist: to also support things like Given the following "mod_quiz > user overrides" exist: Instructions are on the behat_data_generators and behat_generator_base classes. --- lib/behat/classes/behat_core_generator.php | 804 ++++++++++++ lib/behat/classes/behat_generator_base.php | 527 ++++++++ lib/tests/behat/behat_data_generators.php | 1154 ++--------------- mod/quiz/tests/behat/quiz_reset.feature | 26 +- .../generator/behat_mod_quiz_generator.php | 66 + mod/quiz/tests/generator/lib.php | 23 + 6 files changed, 1508 insertions(+), 1092 deletions(-) create mode 100644 lib/behat/classes/behat_core_generator.php create mode 100644 lib/behat/classes/behat_generator_base.php create mode 100644 mod/quiz/tests/generator/behat_mod_quiz_generator.php diff --git a/lib/behat/classes/behat_core_generator.php b/lib/behat/classes/behat_core_generator.php new file mode 100644 index 00000000000..84af165af56 --- /dev/null +++ b/lib/behat/classes/behat_core_generator.php @@ -0,0 +1,804 @@ +. + +/** + * Data generators for acceptance testing. + * + * @package core + * @category test + * @copyright 2012 David Monllaó + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Behat data generator class for core entities. + * + * @package core + * @category test + * @copyright 2012 David Monllaó + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_core_generator extends behat_generator_base { + + protected function get_creatable_entities(): array { + return [ + 'users' => [ + 'datagenerator' => 'user', + 'required' => ['username'], + ], + 'categories' => [ + 'datagenerator' => 'category', + 'required' => ['idnumber'], + 'switchids' => ['category' => 'parent'], + ], + 'courses' => [ + 'datagenerator' => 'course', + 'required' => ['shortname'], + 'switchids' => ['category' => 'category'], + ], + 'groups' => [ + 'datagenerator' => 'group', + 'required' => ['idnumber', 'course'], + 'switchids' => ['course' => 'courseid'], + ], + 'groupings' => [ + 'datagenerator' => 'grouping', + 'required' => ['idnumber', 'course'], + 'switchids' => ['course' => 'courseid'], + ], + 'course enrolments' => [ + 'datagenerator' => 'enrol_user', + 'required' => ['user', 'course', 'role'], + 'switchids' => ['user' => 'userid', 'course' => 'courseid', 'role' => 'roleid'], + ], + 'custom field categories' => [ + 'datagenerator' => 'custom_field_category', + 'required' => ['name', 'component', 'area', 'itemid'], + 'switchids' => [], + ], + 'custom fields' => [ + 'datagenerator' => 'custom_field', + 'required' => ['name', 'category', 'type', 'shortname'], + 'switchids' => [], + ], + 'permission overrides' => [ + 'datagenerator' => 'permission_override', + 'required' => ['capability', 'permission', 'role', 'contextlevel', 'reference'], + 'switchids' => ['role' => 'roleid'], + ], + 'system role assigns' => [ + 'datagenerator' => 'system_role_assign', + 'required' => ['user', 'role'], + 'switchids' => ['user' => 'userid', 'role' => 'roleid'], + ], + 'role assigns' => [ + 'datagenerator' => 'role_assign', + 'required' => ['user', 'role', 'contextlevel', 'reference'], + 'switchids' => ['user' => 'userid', 'role' => 'roleid'], + ], + 'activities' => [ + 'datagenerator' => 'activity', + 'required' => ['activity', 'idnumber', 'course'], + 'switchids' => ['course' => 'course', 'gradecategory' => 'gradecat', 'grouping' => 'groupingid'], + ], + 'blocks' => [ + 'datagenerator' => 'block_instance', + 'required' => ['blockname', 'contextlevel', 'reference'], + ], + 'group members' => [ + 'datagenerator' => 'group_member', + 'required' => ['user', 'group'], + 'switchids' => ['user' => 'userid', 'group' => 'groupid'], + ], + 'grouping groups' => [ + 'datagenerator' => 'grouping_group', + 'required' => ['grouping', 'group'], + 'switchids' => ['grouping' => 'groupingid', 'group' => 'groupid'], + ], + 'cohorts' => [ + 'datagenerator' => 'cohort', + 'required' => ['idnumber'], + ], + 'cohort members' => [ + 'datagenerator' => 'cohort_member', + 'required' => ['user', 'cohort'], + 'switchids' => ['user' => 'userid', 'cohort' => 'cohortid'], + ], + 'roles' => [ + 'datagenerator' => 'role', + 'required' => ['shortname'], + ], + 'grade categories' => [ + 'datagenerator' => 'grade_category', + 'required' => ['fullname', 'course'], + 'switchids' => ['course' => 'courseid', 'gradecategory' => 'parent'], + ], + 'grade items' => [ + 'datagenerator' => 'grade_item', + 'required' => ['course'], + 'switchids' => [ + 'scale' => 'scaleid', + 'outcome' => 'outcomeid', + 'course' => 'courseid', + 'gradecategory' => 'categoryid', + ], + ], + 'grade outcomes' => [ + 'datagenerator' => 'grade_outcome', + 'required' => ['shortname', 'scale'], + 'switchids' => ['course' => 'courseid', 'gradecategory' => 'categoryid', 'scale' => 'scaleid'], + ], + 'scales' => [ + 'datagenerator' => 'scale', + 'required' => ['name', 'scale'], + 'switchids' => ['course' => 'courseid'], + ], + 'question categories' => [ + 'datagenerator' => 'question_category', + 'required' => ['name', 'contextlevel', 'reference'], + 'switchids' => ['questioncategory' => 'parent'], + ], + 'questions' => [ + 'datagenerator' => 'question', + 'required' => ['qtype', 'questioncategory', 'name'], + 'switchids' => ['questioncategory' => 'category', 'user' => 'createdby'], + ], + 'tags' => [ + 'datagenerator' => 'tag', + 'required' => ['name'], + ], + 'events' => [ + 'datagenerator' => 'event', + 'required' => ['name', 'eventtype'], + 'switchids' => [ + 'user' => 'userid', + 'course' => 'courseid', + 'category' => 'categoryid', + ], + ], + 'message contacts' => [ + 'datagenerator' => 'message_contacts', + 'required' => ['user', 'contact'], + 'switchids' => ['user' => 'userid', 'contact' => 'contactid'], + ], + 'private messages' => [ + 'datagenerator' => 'private_messages', + 'required' => ['user', 'contact', 'message'], + 'switchids' => ['user' => 'userid', 'contact' => 'contactid'], + ], + 'favourite conversations' => [ + 'datagenerator' => 'favourite_conversations', + 'required' => ['user', 'contact'], + 'switchids' => ['user' => 'userid', 'contact' => 'contactid'], + ], + 'group messages' => [ + 'datagenerator' => 'group_messages', + 'required' => ['user', 'group', 'message'], + 'switchids' => ['user' => 'userid', 'group' => 'groupid'], + ], + 'muted group conversations' => [ + 'datagenerator' => 'mute_group_conversations', + 'required' => ['user', 'group', 'course'], + 'switchids' => ['user' => 'userid', 'group' => 'groupid', 'course' => 'courseid'], + ], + 'muted private conversations' => [ + 'datagenerator' => 'mute_private_conversations', + 'required' => ['user', 'contact'], + 'switchids' => ['user' => 'userid', 'contact' => 'contactid'], + ], + 'language customisations' => [ + 'datagenerator' => 'customlang', + 'required' => ['component', 'stringid', 'value'], + ], + 'analytics model' => [ + 'datagenerator' => 'analytics_model', + 'required' => ['target', 'indicators', 'timesplitting', 'enabled'], + ], + ]; + } + + /** + * Remove any empty custom fields, to avoid errors when creating the course. + * + * @param array $data + * @return array + */ + protected function preprocess_course($data) { + foreach ($data as $fieldname => $value) { + if ($value === '' && strpos($fieldname, 'customfield_') === 0) { + unset($data[$fieldname]); + } + } + return $data; + } + + /** + * If password is not set it uses the username. + * + * @param array $data + * @return array + */ + protected function preprocess_user($data) { + if (!isset($data['password'])) { + $data['password'] = $data['username']; + } + return $data; + } + + /** + * If contextlevel and reference are specified for cohort, transform them to the contextid. + * + * @param array $data + * @return array + */ + protected function preprocess_cohort($data) { + if (isset($data['contextlevel'])) { + if (!isset($data['reference'])) { + throw new Exception('If field contextlevel is specified, field reference must also be present'); + } + $context = $this->get_context($data['contextlevel'], $data['reference']); + unset($data['contextlevel']); + unset($data['reference']); + $data['contextid'] = $context->id; + } + return $data; + } + + /** + * Preprocesses the creation of a grade item. Converts gradetype text to a number. + * + * @param array $data + * @return array + */ + protected function preprocess_grade_item($data) { + global $CFG; + require_once("$CFG->libdir/grade/constants.php"); + + if (isset($data['gradetype'])) { + $data['gradetype'] = constant("GRADE_TYPE_" . strtoupper($data['gradetype'])); + } + + if (!empty($data['category']) && !empty($data['courseid'])) { + $cat = grade_category::fetch(array('fullname' => $data['category'], 'courseid' => $data['courseid'])); + if (!$cat) { + throw new Exception('Could not resolve category with name "' . $data['category'] . '"'); + } + unset($data['category']); + $data['categoryid'] = $cat->id; + } + + return $data; + } + + /** + * Adapter to modules generator. + * + * @throws Exception Custom exception for test writers + * @param array $data + * @return void + */ + protected function process_activity($data) { + global $DB, $CFG; + + // The the_following_exists() method checks that the field exists. + $activityname = $data['activity']; + unset($data['activity']); + + // Convert scale name into scale id (negative number indicates using scale). + if (isset($data['grade']) && strlen($data['grade']) && !is_number($data['grade'])) { + $data['grade'] = - $this->get_scale_id($data['grade']); + require_once("$CFG->libdir/grade/constants.php"); + + if (!isset($data['gradetype'])) { + $data['gradetype'] = GRADE_TYPE_SCALE; + } + } + + // We split $data in the activity $record and the course module $options. + $cmoptions = array(); + $cmcolumns = $DB->get_columns('course_modules'); + foreach ($cmcolumns as $key => $value) { + if (isset($data[$key])) { + $cmoptions[$key] = $data[$key]; + } + } + + // Custom exception. + try { + $this->datagenerator->create_module($activityname, $data, $cmoptions); + } catch (coding_exception $e) { + throw new Exception('\'' . $activityname . '\' activity can not be added using this step,' . + ' use the step \'I add a "ACTIVITY_OR_RESOURCE_NAME_STRING" to section "SECTION_NUMBER"\' instead'); + } + } + + /** + * Add a block to a page. + * + * @param array $data should mostly match the fields of the block_instances table. + * The block type is specified by blockname. + * The parentcontextid is set from contextlevel and reference. + * Missing values are filled in by testing_block_generator::prepare_record. + * $data is passed to create_block as both $record and $options. Normally + * the keys are different, so this is a way to let people set values in either place. + */ + protected function process_block_instance($data) { + + if (empty($data['blockname'])) { + throw new Exception('\'blocks\' requires the field \'block\' type to be specified'); + } + + if (empty($data['contextlevel'])) { + throw new Exception('\'blocks\' requires the field \'contextlevel\' to be specified'); + } + + if (!isset($data['reference'])) { + throw new Exception('\'blocks\' requires the field \'reference\' to be specified'); + } + + $context = $this->get_context($data['contextlevel'], $data['reference']); + $data['parentcontextid'] = $context->id; + + // Pass $data as both $record and $options. I think that is unlikely to + // cause problems since the relevant key names are different. + // $options is not used in most blocks I have seen, but where it is, it is necessary. + $this->datagenerator->create_block($data['blockname'], $data, $data); + } + + /** + * Creates language customisation. + * + * @throws Exception + * @throws dml_exception + * @param array $data + * @return void + */ + protected function process_customlang($data) { + global $CFG, $DB, $USER; + + require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/customlang/locallib.php'); + require_once($CFG->libdir . '/adminlib.php'); + + if (empty($data['component'])) { + throw new Exception('\'customlang\' requires the field \'component\' type to be specified'); + } + + if (empty($data['stringid'])) { + throw new Exception('\'customlang\' requires the field \'stringid\' to be specified'); + } + + if (!isset($data['value'])) { + throw new Exception('\'customlang\' requires the field \'value\' to be specified'); + } + + $now = time(); + + tool_customlang_utils::checkout($USER->lang); + + $record = $DB->get_record_sql("SELECT s.* + FROM {tool_customlang} s + JOIN {tool_customlang_components} c ON s.componentid = c.id + WHERE c.name = ? AND s.lang = ? AND s.stringid = ?", + array($data['component'], $USER->lang, $data['stringid'])); + + if (empty($data['value']) && !is_null($record->local)) { + $record->local = null; + $record->modified = 1; + $record->outdated = 0; + $record->timecustomized = null; + $DB->update_record('tool_customlang', $record); + tool_customlang_utils::checkin($USER->lang); + } + + if (!empty($data['value']) && $data['value'] != $record->local) { + $record->local = $data['value']; + $record->modified = 1; + $record->outdated = 0; + $record->timecustomized = $now; + $DB->update_record('tool_customlang', $record); + tool_customlang_utils::checkin($USER->lang); + } + } + + /** + * Adapter to enrol_user() data generator. + * + * @throws Exception + * @param array $data + * @return void + */ + protected function process_enrol_user($data) { + global $SITE; + + if (empty($data['roleid'])) { + throw new Exception('\'course enrolments\' requires the field \'role\' to be specified'); + } + + if (!isset($data['userid'])) { + throw new Exception('\'course enrolments\' requires the field \'user\' to be specified'); + } + + if (!isset($data['courseid'])) { + throw new Exception('\'course enrolments\' requires the field \'course\' to be specified'); + } + + if (!isset($data['enrol'])) { + $data['enrol'] = 'manual'; + } + + if (!isset($data['timestart'])) { + $data['timestart'] = 0; + } + + if (!isset($data['timeend'])) { + $data['timeend'] = 0; + } + + if (!isset($data['status'])) { + $data['status'] = null; + } + + // If the provided course shortname is the site shortname we consider it a system role assign. + if ($data['courseid'] == $SITE->id) { + // Frontpage course assign. + $context = context_course::instance($data['courseid']); + role_assign($data['roleid'], $data['userid'], $context->id); + + } else { + // Course assign. + $this->datagenerator->enrol_user($data['userid'], $data['courseid'], $data['roleid'], $data['enrol'], + $data['timestart'], $data['timeend'], $data['status']); + } + + } + + /** + * Allows/denies a capability at the specified context + * + * @throws Exception + * @param array $data + * @return void + */ + protected function process_permission_override($data) { + + // Will throw an exception if it does not exist. + $context = $this->get_context($data['contextlevel'], $data['reference']); + + switch ($data['permission']) { + case get_string('allow', 'role'): + $permission = CAP_ALLOW; + break; + case get_string('prevent', 'role'): + $permission = CAP_PREVENT; + break; + case get_string('prohibit', 'role'): + $permission = CAP_PROHIBIT; + break; + default: + throw new Exception('The \'' . $data['permission'] . '\' permission does not exist'); + break; + } + + if (is_null(get_capability_info($data['capability']))) { + throw new Exception('The \'' . $data['capability'] . '\' capability does not exist'); + } + + role_change_permission($data['roleid'], $context, $data['capability'], $permission); + } + + /** + * Assigns a role to a user at system context + * + * Used by "system role assigns" can be deleted when + * system role assign will be deprecated in favour of + * "role assigns" + * + * @throws Exception + * @param array $data + * @return void + */ + protected function process_system_role_assign($data) { + + if (empty($data['roleid'])) { + throw new Exception('\'system role assigns\' requires the field \'role\' to be specified'); + } + + if (!isset($data['userid'])) { + throw new Exception('\'system role assigns\' requires the field \'user\' to be specified'); + } + + $context = context_system::instance(); + + $this->datagenerator->role_assign($data['roleid'], $data['userid'], $context->id); + } + + /** + * Assigns a role to a user at the specified context + * + * @throws Exception + * @param array $data + * @return void + */ + protected function process_role_assign($data) { + + if (empty($data['roleid'])) { + throw new Exception('\'role assigns\' requires the field \'role\' to be specified'); + } + + if (!isset($data['userid'])) { + throw new Exception('\'role assigns\' requires the field \'user\' to be specified'); + } + + if (empty($data['contextlevel'])) { + throw new Exception('\'role assigns\' requires the field \'contextlevel\' to be specified'); + } + + if (!isset($data['reference'])) { + throw new Exception('\'role assigns\' requires the field \'reference\' to be specified'); + } + + // Getting the context id. + $context = $this->get_context($data['contextlevel'], $data['reference']); + + $this->datagenerator->role_assign($data['roleid'], $data['userid'], $context->id); + } + + /** + * Creates a role. + * + * @param array $data + * @return void + */ + protected function process_role($data) { + + // We require the user to fill the role shortname. + if (empty($data['shortname'])) { + throw new Exception('\'role\' requires the field \'shortname\' to be specified'); + } + + $this->datagenerator->create_role($data); + } + + /** + * Adds members to cohorts + * + * @param array $data + * @return void + */ + protected function process_cohort_member($data) { + cohort_add_member($data['cohortid'], $data['userid']); + } + + /** + * Create a question category. + * + * @param array $data the row of data from the behat script. + */ + protected function process_question_category($data) { + global $DB; + + $context = $this->get_context($data['contextlevel'], $data['reference']); + + // The way this class works, we have already looked up the given parent category + // name and found a matching category. However, it is possible, particularly + // for the 'top' category, for there to be several categories with the + // same name. So far one will have been picked at random, but we need + // the one from the right context. So, if we have the wrong category, try again. + // (Just fixing it here, rather than getting it right first time, is a bit + // of a bodge, but in general this class assumes that names are unique, + // and normally they are, so this was the easiest fix.) + if (!empty($data['parent'])) { + $foundparent = $DB->get_record('question_categories', ['id' => $data['parent']], '*', MUST_EXIST); + if ($foundparent->contextid != $context->id) { + $rightparentid = $DB->get_field('question_categories', 'id', + ['contextid' => $context->id, 'name' => $foundparent->name]); + if (!$rightparentid) { + throw new Exception('The specified question category with name "' . $foundparent->name . + '" does not exist in context "' . $context->get_context_name() . '"."'); + } + $data['parent'] = $rightparentid; + } + } + + $data['contextid'] = $context->id; + $this->datagenerator->get_plugin_generator('core_question')->create_question_category($data); + } + + /** + * Create a question. + * + * Creating questions relies on the question/type/.../tests/helper.php mechanism. + * We start with test_question_maker::get_question_form_data($data['qtype'], $data['template']) + * and then overlay the values from any other fields of $data that are set. + * + * @param array $data the row of data from the behat script. + */ + protected function process_question($data) { + if (array_key_exists('questiontext', $data)) { + $data['questiontext'] = array( + 'text' => $data['questiontext'], + 'format' => FORMAT_HTML, + ); + } + + if (array_key_exists('generalfeedback', $data)) { + $data['generalfeedback'] = array( + 'text' => $data['generalfeedback'], + 'format' => FORMAT_HTML, + ); + } + + $which = null; + if (!empty($data['template'])) { + $which = $data['template']; + } + + $this->datagenerator->get_plugin_generator('core_question')->create_question($data['qtype'], $which, $data); + } + + /** + * Adds user to contacts + * + * @param array $data + * @return void + */ + protected function process_message_contacts($data) { + \core_message\api::add_contact($data['userid'], $data['contactid']); + } + + /** + * Send a new message from user to contact in a private conversation + * + * @param array $data + * @return void + */ + protected function process_private_messages(array $data) { + if (empty($data['format'])) { + $data['format'] = 'FORMAT_PLAIN'; + } + + if (!$conversationid = \core_message\api::get_conversation_between_users([$data['userid'], $data['contactid']])) { + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [$data['userid'], $data['contactid']] + ); + $conversationid = $conversation->id; + } + \core_message\api::send_message_to_conversation( + $data['userid'], + $conversationid, + $data['message'], + constant($data['format']) + ); + } + + /** + * Send a new message from user to a group conversation + * + * @param array $data + * @return void + */ + protected function process_group_messages(array $data) { + global $DB; + + if (empty($data['format'])) { + $data['format'] = 'FORMAT_PLAIN'; + } + + $group = $DB->get_record('groups', ['id' => $data['groupid']]); + $coursecontext = context_course::instance($group->courseid); + if (!$conversation = \core_message\api::get_conversation_by_area('core_group', 'groups', $data['groupid'], + $coursecontext->id)) { + $members = $DB->get_records_menu('groups_members', ['groupid' => $data['groupid']], '', 'userid, id'); + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, + array_keys($members), + $group->name, + \core_message\api::MESSAGE_CONVERSATION_ENABLED, + 'core_group', + 'groups', + $group->id, + $coursecontext->id); + } + \core_message\api::send_message_to_conversation( + $data['userid'], + $conversation->id, + $data['message'], + constant($data['format']) + ); + } + + /** + * Mark a private conversation as favourite for user + * + * @param array $data + * @return void + */ + protected function process_favourite_conversations(array $data) { + if (!$conversationid = \core_message\api::get_conversation_between_users([$data['userid'], $data['contactid']])) { + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [$data['userid'], $data['contactid']] + ); + $conversationid = $conversation->id; + } + \core_message\api::set_favourite_conversation($conversationid, $data['userid']); + } + + /** + * Mute an existing group conversation for user + * + * @param array $data + * @return void + */ + protected function process_mute_group_conversations(array $data) { + if (groups_is_member($data['groupid'], $data['userid'])) { + $context = context_course::instance($data['courseid']); + $conversation = \core_message\api::get_conversation_by_area( + 'core_group', + 'groups', + $data['groupid'], + $context->id + ); + if ($conversation) { + \core_message\api::mute_conversation($data['userid'], $conversation->id); + } + } + } + + /** + * Mute a private conversation for user + * + * @param array $data + * @return void + */ + protected function process_mute_private_conversations(array $data) { + if (!$conversationid = \core_message\api::get_conversation_between_users([$data['userid'], $data['contactid']])) { + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [$data['userid'], $data['contactid']] + ); + $conversationid = $conversation->id; + } + \core_message\api::mute_conversation($data['userid'], $conversationid); + } + + /** + * Transform indicators string into array. + * + * @param array $data + * @return array + */ + protected function preprocess_analytics_model($data) { + $data['indicators'] = explode(',', $data['indicators']); + return $data; + } + + /** + * Creates an analytics model + * + * @param target $data + * @return void + */ + protected function process_analytics_model($data) { + \core_analytics\manager::create_declared_model($data); + } +} diff --git a/lib/behat/classes/behat_generator_base.php b/lib/behat/classes/behat_generator_base.php new file mode 100644 index 00000000000..98d16bc584c --- /dev/null +++ b/lib/behat/classes/behat_generator_base.php @@ -0,0 +1,527 @@ +. + +/** + * Base class for data generators component support for acceptance testing. + * + * @package core + * @category test + * @copyright 2012 David Monllaó + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use Behat\Gherkin\Node\TableNode as TableNode; +use Behat\Behat\Tester\Exception\PendingException as PendingException; + + +/** + * Class to quickly create Behat test data using component data generators. + * + * There is a subclass of class for each component that wants to be able to + * generate entities using the Behat step + * Given the following "entity types" exist: + * | test | data | + * + * For core entities, the entity type is like "courses" or "users" and + * generating those is handled by behat_core_generator. For other components + * the entity type is like "mod_quiz > User override" and that is handled by + * behat_mod_quiz_generator defined in mod/quiz/tests/generator/behat_mod_quiz_generator.php. + * + * The types of entities that can be generated are described by the array returned + * by the {@link get_generateable_entities()} method. The list in + * {@link behat_core_generator} is a good (if complex) example. + * + * How things work is best explained with a few examples. All this is implemented + * in the {@link generate_items()} method below, if you want to see every detail of + * how it works. + * + * Simple example from behat_core_generator: + * 'users' => [ + * 'datagenerator' => 'user', + * 'required' => ['username'], + * ], + * The steps performed are: + * + * 1. 'datagenerator' => 'user' means that the word used in the method names below is 'user'. + * + * 2. Because 'required' is present, check the supplied data exists 'username' column is present + * in the supplied data table and if not display an error. + * + * 3. Then for each row in the table as an array $elementdata (array keys are column names) + * and process it as follows + * + * 4. (Not used in this example.) + * + * 5. If the method 'preprocess_user' exists, then call it to update $elementdata. + * (It does, in this case it sets the password to the username, if password was not given.) + * + * We then do one of 4 things: + * + * 6a. If there is a method 'process_user' we call it. (It doesn't for user, + * but there are other examples like process_enrol_user() in behat_core_generator.) + * + * 6b. (Not used in this example.) + * + * 6c. Else, if testing_data_generator::create_user exists, we call it with $elementdata. (it does.) + * + * 6d. If none of these three things work. an error is thrown. + * + * To understand the missing steps above, consider the example from behat_mod_quiz_generator: + * 'group override' => [ + * 'datagenerator' => 'override', + * 'required' => ['quiz', 'group'], + * 'switchids' => ['quiz' => 'quiz', 'group' => 'groupid'], + * ], + * Processing is as above, except that: + * + * 1. Note 'datagenerator' is 'override' (not group_override). 'user override' maps to the + * same datagenerator. This works fine. + * + * 4. Because 'switchids' is present, human-readable data in the table gets converted to ids. + * They array key 'group' refers to a column which may be present in the table (it will be + * here because it is required, but it does not have to be in general). If that column + * is present and contains a value, then the method matching name like get_group_id() is + * called with the value from that column in the data table. You must implement this + * method. You can see several examples of this sort of method below. + * + * If that method returns a group id, then $elementdata['group'] is unset and + * $elementdata['groupid'] is set to the result of the get_group_id() call. 'groupid' here + * because of the definition is 'switchids' => [..., 'group' => 'groupid']. + * If get_group_id() cannot find the group, it should throw a helpful exception. + * + * Similarly, 'quiz' (the quiz name) is looked up with a call to get_quiz_id(). Here, the + * new array key set matches the old one removed. This is fine. + * + * 6b. We are in a plugin, so before checking whether testing_data_generator::create_override + * exists we first check whether mod_quiz_generator::create_override() exists. It does, + * and this is what gets called. + * + * This second example shows why the get_..._id methods for core entities are in this base + * class, not in behat_core_generator. Plugins may need to look up the ids of + * core entities. + * + * behat_core_generator is defined in lib/behat/classes/behat_core_generator.php + * and for components, behat_..._generator is defined in tests/generator/behat_..._generator.php + * inside the plugin. For example behat_mod_quiz_generator is defined in + * mod/quiz/tests/generator/behat_mod_quiz_generator.php. + * + * @package core + * @category test + * @copyright 2012 David Monllaó + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class behat_generator_base { + + /** + * @var string the name of the component we belong to. + * + * This should probably only be used to make error messages clearer. + */ + protected $component; + + /** + * @var testing_data_generator the core data generator + */ + protected $datagenerator; + + /** + * @var testing_data_generator the data generator for this component. + */ + protected $componentdatagenerator; + + /** + * Constructor. + * + * @param string $component component name, to make error messages more readable. + */ + public function __construct(string $component) { + $this->component = $component; + } + + /** + * Get a list of the entities that can be created for this component. + * + * This function must be overridden in subclasses. See class comment + * above for a description of the data structure. + * See {@link behat_core_generator} for an example. + * + * @return array entity name => information about how to generate. + */ + protected abstract function get_creatable_entities(): array; + + /** + * Do the work to generate an entity. + * + * This is called by {@link behat_data_generators::the_following_entities_exist()}. + * + * @param string $generatortype The name of the entity to create. + * @param TableNode $data from the step. + */ + public function generate_items(string $generatortype, TableNode $data) { + // Now that we need them require the data generators. + require_once(__DIR__ . '/../../testing/generator/lib.php'); + + $elements = $this->get_creatable_entities(); + + if (!isset($elements[$generatortype])) { + throw new PendingException($this->name_for_errors($generatortype) . + ' is not a known type of entity that can be generated.'); + } + $entityinfo = $elements[$generatortype]; + + $this->datagenerator = testing_util::get_data_generator(); + if ($this->component === 'core') { + $this->componentdatagenerator = $this->datagenerator; + } else { + $this->componentdatagenerator = $this->datagenerator->get_plugin_generator($this->component); + } + + $generatortype = $entityinfo['datagenerator']; + + foreach ($data->getHash() as $elementdata) { + + // Check if all the required fields are there. + foreach ($entityinfo['required'] as $requiredfield) { + if (!isset($elementdata[$requiredfield])) { + throw new Exception($this->name_for_errors($generatortype) . + ' requires the field ' . $requiredfield . ' to be specified'); + } + } + + // Switch from human-friendly references to ids. + if (!empty($entityinfo['switchids'])) { + foreach ($entityinfo['switchids'] as $element => $field) { + $methodname = 'get_' . $element . '_id'; + + // Not all the switch fields are required, default vars will be assigned by data generators. + if (isset($elementdata[$element])) { + if (!method_exists($this, $methodname)) { + throw new coding_exception('The generator for ' . + $this->name_for_errors($generatortype) . + ' entities specifies \'switchids\' => [..., \'' . $element . + '\' => \'' . $field . '\', ...] but the required method ' . + $methodname . '() has not been defined in ' . + get_class($this) . '.'); + } + // Temp $id var to avoid problems when $element == $field. + $id = $this->{$methodname}($elementdata[$element]); + unset($elementdata[$element]); + $elementdata[$field] = $id; + } + } + } + + // Preprocess the entities that requires a special treatment. + if (method_exists($this, 'preprocess_' . $generatortype)) { + $elementdata = $this->{'preprocess_' . $generatortype}($elementdata); + } + + // Creates element. + if (method_exists($this, 'process_' . $generatortype)) { + // Use a method on this class to do the work. + $this->{'process_' . $generatortype}($elementdata); + + } else if (method_exists($this->componentdatagenerator, 'create_' . $generatortype)) { + // Using the component't own data generator if it exists. + $this->componentdatagenerator->{'create_' . $generatortype}($elementdata); + + } else if (method_exists($this->datagenerator, 'create_' . $generatortype)) { + // Use a method on the core data geneator, if there is one. + $this->datagenerator->{'create_' . $generatortype}($elementdata); + + } else { + // Give up. + throw new PendingException($this->name_for_errors($generatortype) . + ' data generator is not implemented'); + } + } + } + + /** + * Helper for formatting error messages. + * + * @param string $entitytype entity type without prefix, e.g. 'frog'. + * @return string either 'frog' for core entities, or 'mod_mymod > frog' for components. + */ + protected function name_for_errors(string $entitytype): string { + if ($this->component === 'core') { + return '"' . $entitytype . '"'; + } else { + return '"' . $this->component . ' > ' . $entitytype . '"'; + } + } + + /** + * Gets the grade category id from the grade category fullname + * + * @param string $fullname the grade category name. + * @return int corresponding id. + */ + protected function get_gradecategory_id($fullname) { + global $DB; + + if (!$id = $DB->get_field('grade_categories', 'id', array('fullname' => $fullname))) { + throw new Exception('The specified grade category with fullname "' . $fullname . '" does not exist'); + } + return $id; + } + + /** + * Gets the user id from it's username. + * @throws Exception + * @param string $username + * @return int + */ + protected function get_user_id($username) { + global $DB; + + if (!$id = $DB->get_field('user', 'id', array('username' => $username))) { + throw new Exception('The specified user with username "' . $username . '" does not exist'); + } + return $id; + } + + /** + * Gets the role id from it's shortname. + * @throws Exception + * @param string $roleshortname + * @return int + */ + protected function get_role_id($roleshortname) { + global $DB; + + if (!$id = $DB->get_field('role', 'id', array('shortname' => $roleshortname))) { + throw new Exception('The specified role with shortname "' . $roleshortname . '" does not exist'); + } + + return $id; + } + + /** + * Gets the category id from it's idnumber. + * @throws Exception + * @param string $idnumber + * @return int + */ + protected function get_category_id($idnumber) { + global $DB; + + // If no category was specified use the data generator one. + if ($idnumber == false) { + return null; + } + + if (!$id = $DB->get_field('course_categories', 'id', array('idnumber' => $idnumber))) { + throw new Exception('The specified category with idnumber "' . $idnumber . '" does not exist'); + } + + return $id; + } + + /** + * Gets the course id from it's shortname. + * @throws Exception + * @param string $shortname + * @return int + */ + protected function get_course_id($shortname) { + global $DB; + + if (!$id = $DB->get_field('course', 'id', array('shortname' => $shortname))) { + throw new Exception('The specified course with shortname "' . $shortname . '" does not exist'); + } + return $id; + } + + /** + * Gets the group id from it's idnumber. + * @throws Exception + * @param string $idnumber + * @return int + */ + protected function get_group_id($idnumber) { + global $DB; + + if (!$id = $DB->get_field('groups', 'id', array('idnumber' => $idnumber))) { + throw new Exception('The specified group with idnumber "' . $idnumber . '" does not exist'); + } + return $id; + } + + /** + * Gets the grouping id from it's idnumber. + * @throws Exception + * @param string $idnumber + * @return int + */ + protected function get_grouping_id($idnumber) { + global $DB; + + // Do not fetch grouping ID for empty grouping idnumber. + if (empty($idnumber)) { + return null; + } + + if (!$id = $DB->get_field('groupings', 'id', array('idnumber' => $idnumber))) { + throw new Exception('The specified grouping with idnumber "' . $idnumber . '" does not exist'); + } + return $id; + } + + /** + * Gets the cohort id from it's idnumber. + * @throws Exception + * @param string $idnumber + * @return int + */ + protected function get_cohort_id($idnumber) { + global $DB; + + if (!$id = $DB->get_field('cohort', 'id', array('idnumber' => $idnumber))) { + throw new Exception('The specified cohort with idnumber "' . $idnumber . '" does not exist'); + } + return $id; + } + + /** + * Gets the outcome item id from its shortname. + * @throws Exception + * @param string $shortname + * @return int + */ + protected function get_outcome_id($shortname) { + global $DB; + + if (!$id = $DB->get_field('grade_outcomes', 'id', array('shortname' => $shortname))) { + throw new Exception('The specified outcome with shortname "' . $shortname . '" does not exist'); + } + return $id; + } + + /** + * Get the id of a named scale. + * @param string $name the name of the scale. + * @return int the scale id. + */ + protected function get_scale_id($name) { + global $DB; + + if (!$id = $DB->get_field('scale', 'id', array('name' => $name))) { + throw new Exception('The specified scale with name "' . $name . '" does not exist'); + } + return $id; + } + + /** + * Get the id of a named question category (must be globally unique). + * Note that 'Top' is a special value, used when setting the parent of another + * category, meaning top-level. + * + * @param string $name the question category name. + * @return int the question category id. + */ + protected function get_questioncategory_id($name) { + global $DB; + + if ($name == 'Top') { + return 0; + } + + if (!$id = $DB->get_field('question_categories', 'id', array('name' => $name))) { + throw new Exception('The specified question category with name "' . $name . '" does not exist'); + } + return $id; + } + + /** + * Gets the internal context id from the context reference. + * + * The context reference changes depending on the context + * level, it can be the system, a user, a category, a course or + * a module. + * + * @throws Exception + * @param string $levelname The context level string introduced by the test writer + * @param string $contextref The context reference introduced by the test writer + * @return context + */ + protected function get_context($levelname, $contextref) { + global $DB; + + // Getting context levels and names (we will be using the English ones as it is the test site language). + $contextlevels = context_helper::get_all_levels(); + $contextnames = array(); + foreach ($contextlevels as $level => $classname) { + $contextnames[context_helper::get_level_name($level)] = $level; + } + + if (empty($contextnames[$levelname])) { + throw new Exception('The specified "' . $levelname . '" context level does not exist'); + } + $contextlevel = $contextnames[$levelname]; + + // Return it, we don't need to look for other internal ids. + if ($contextlevel == CONTEXT_SYSTEM) { + return context_system::instance(); + } + + switch ($contextlevel) { + + case CONTEXT_USER: + $instanceid = $DB->get_field('user', 'id', array('username' => $contextref)); + break; + + case CONTEXT_COURSECAT: + $instanceid = $DB->get_field('course_categories', 'id', array('idnumber' => $contextref)); + break; + + case CONTEXT_COURSE: + $instanceid = $DB->get_field('course', 'id', array('shortname' => $contextref)); + break; + + case CONTEXT_MODULE: + $instanceid = $DB->get_field('course_modules', 'id', array('idnumber' => $contextref)); + break; + + default: + break; + } + + $contextclass = $contextlevels[$contextlevel]; + if (!$context = $contextclass::instance($instanceid, IGNORE_MISSING)) { + throw new Exception('The specified "' . $contextref . '" context reference does not exist'); + } + + return $context; + } + + /** + * Gets the contact id from it's username. + * @throws Exception + * @param string $username + * @return int + */ + protected function get_contact_id($username) { + global $DB; + + if (!$id = $DB->get_field('user', 'id', array('username' => $username))) { + throw new Exception('The specified user with username "' . $username . '" does not exist'); + } + return $id; + } +} diff --git a/lib/tests/behat/behat_data_generators.php b/lib/tests/behat/behat_data_generators.php index 8034382057b..2b805ed1f8e 100644 --- a/lib/tests/behat/behat_data_generators.php +++ b/lib/tests/behat/behat_data_generators.php @@ -33,16 +33,23 @@ use Behat\Behat\Tester\Exception\PendingException as PendingException; /** * Class to set up quickly a Given environment. * - * Acceptance tests are block-boxed, so this steps definitions should only - * be used to set up the test environment as we are not replicating user steps. + * The entry point is the Behat steps: + * the following "entity types" exist: + * | test | data | * - * All data generators should be in lib/testing/generator/*, shared between phpunit - * and behat and they should be called from here, if possible using the standard - * 'create_$elementname($options)' and if it's not possible (data generators arguments will not be - * always the same) or the element is not suitable to be a data generator, create a - * 'process_$elementname($options)' method and use the data generator from there if possible. + * Entity type will either look like "users" or "activities" for core entities, or + * "mod_forum > subscription" or "core_message > message" for entities belonging + * to components. + * + * Generally, you only need to specify properties relevant to your test, + * and everything else gets set to sensible defaults. + * + * The actual generation of entities is done by {@link behat_generator_base}. + * There is one subclass for each component, e.g. {@link behat_core_generator} + * or {@link behat_mod_quiz_generator}. To see the types of entity + * that can be created for each component, look at the arrays returned + * by the get_creatable_entities() method in each class. * - * @todo If the available elements list grows too much this class must be split into smaller pieces * @package core * @category test * @copyright 2012 David Monllaó @@ -51,1101 +58,98 @@ use Behat\Behat\Tester\Exception\PendingException as PendingException; class behat_data_generators extends behat_base { /** - * @var testing_data_generator - */ - protected $datagenerator; - - /** - * Each element specifies: - * - The data generator sufix used. - * - The required fields. - * - The mapping between other elements references and database field names. - * @var array - */ - protected static $elements = array( - 'users' => array( - 'datagenerator' => 'user', - 'required' => array('username') - ), - 'categories' => array( - 'datagenerator' => 'category', - 'required' => array('idnumber'), - 'switchids' => array('category' => 'parent') - ), - 'courses' => array( - 'datagenerator' => 'course', - 'required' => array('shortname'), - 'switchids' => array('category' => 'category') - ), - 'groups' => array( - 'datagenerator' => 'group', - 'required' => array('idnumber', 'course'), - 'switchids' => array('course' => 'courseid') - ), - 'groupings' => array( - 'datagenerator' => 'grouping', - 'required' => array('idnumber', 'course'), - 'switchids' => array('course' => 'courseid') - ), - 'course enrolments' => array( - 'datagenerator' => 'enrol_user', - 'required' => array('user', 'course', 'role'), - 'switchids' => array('user' => 'userid', 'course' => 'courseid', 'role' => 'roleid') - ), - 'custom field categories' => array( - 'datagenerator' => 'custom_field_category', - 'required' => array('name', 'component', 'area', 'itemid'), - 'switchids' => array() - ), - 'custom fields' => array( - 'datagenerator' => 'custom_field', - 'required' => array('name', 'category', 'type', 'shortname'), - 'switchids' => array() - ), - 'permission overrides' => array( - 'datagenerator' => 'permission_override', - 'required' => array('capability', 'permission', 'role', 'contextlevel', 'reference'), - 'switchids' => array('role' => 'roleid') - ), - 'system role assigns' => array( - 'datagenerator' => 'system_role_assign', - 'required' => array('user', 'role'), - 'switchids' => array('user' => 'userid', 'role' => 'roleid') - ), - 'role assigns' => array( - 'datagenerator' => 'role_assign', - 'required' => array('user', 'role', 'contextlevel', 'reference'), - 'switchids' => array('user' => 'userid', 'role' => 'roleid') - ), - 'activities' => array( - 'datagenerator' => 'activity', - 'required' => array('activity', 'idnumber', 'course'), - 'switchids' => array('course' => 'course', 'gradecategory' => 'gradecat', 'grouping' => 'groupingid') - ), - 'blocks' => array( - 'datagenerator' => 'block_instance', - 'required' => array('blockname', 'contextlevel', 'reference'), - ), - 'group members' => array( - 'datagenerator' => 'group_member', - 'required' => array('user', 'group'), - 'switchids' => array('user' => 'userid', 'group' => 'groupid') - ), - 'grouping groups' => array( - 'datagenerator' => 'grouping_group', - 'required' => array('grouping', 'group'), - 'switchids' => array('grouping' => 'groupingid', 'group' => 'groupid') - ), - 'cohorts' => array( - 'datagenerator' => 'cohort', - 'required' => array('idnumber') - ), - 'cohort members' => array( - 'datagenerator' => 'cohort_member', - 'required' => array('user', 'cohort'), - 'switchids' => array('user' => 'userid', 'cohort' => 'cohortid') - ), - 'roles' => array( - 'datagenerator' => 'role', - 'required' => array('shortname') - ), - 'grade categories' => array( - 'datagenerator' => 'grade_category', - 'required' => array('fullname', 'course'), - 'switchids' => array('course' => 'courseid', 'gradecategory' => 'parent') - ), - 'grade items' => array( - 'datagenerator' => 'grade_item', - 'required' => array('course'), - 'switchids' => array('scale' => 'scaleid', 'outcome' => 'outcomeid', 'course' => 'courseid', - 'gradecategory' => 'categoryid') - ), - 'grade outcomes' => array( - 'datagenerator' => 'grade_outcome', - 'required' => array('shortname', 'scale'), - 'switchids' => array('course' => 'courseid', 'gradecategory' => 'categoryid', 'scale' => 'scaleid') - ), - 'scales' => array( - 'datagenerator' => 'scale', - 'required' => array('name', 'scale'), - 'switchids' => array('course' => 'courseid') - ), - 'question categories' => array( - 'datagenerator' => 'question_category', - 'required' => array('name', 'contextlevel', 'reference'), - 'switchids' => array('questioncategory' => 'parent') - ), - 'questions' => array( - 'datagenerator' => 'question', - 'required' => array('qtype', 'questioncategory', 'name'), - 'switchids' => array('questioncategory' => 'category', 'user' => 'createdby') - ), - 'tags' => array( - 'datagenerator' => 'tag', - 'required' => array('name') - ), - 'events' => array( - 'datagenerator' => 'event', - 'required' => array('name', 'eventtype'), - 'switchids' => array( - 'user' => 'userid', - 'course' => 'courseid', - 'category' => 'categoryid', - ) - ), - 'message contacts' => array( - 'datagenerator' => 'message_contacts', - 'required' => array('user', 'contact'), - 'switchids' => array('user' => 'userid', 'contact' => 'contactid') - ), - 'private messages' => array( - 'datagenerator' => 'private_messages', - 'required' => array('user', 'contact', 'message'), - 'switchids' => array('user' => 'userid', 'contact' => 'contactid') - ), - 'favourite conversations' => array( - 'datagenerator' => 'favourite_conversations', - 'required' => array('user', 'contact'), - 'switchids' => array('user' => 'userid', 'contact' => 'contactid') - ), - 'group messages' => array( - 'datagenerator' => 'group_messages', - 'required' => array('user', 'group', 'message'), - 'switchids' => array('user' => 'userid', 'group' => 'groupid') - ), - 'muted group conversations' => array( - 'datagenerator' => 'mute_group_conversations', - 'required' => array('user', 'group', 'course'), - 'switchids' => array('user' => 'userid', 'group' => 'groupid', 'course' => 'courseid') - ), - 'muted private conversations' => array( - 'datagenerator' => 'mute_private_conversations', - 'required' => array('user', 'contact'), - 'switchids' => array('user' => 'userid', 'contact' => 'contactid') - ), - 'language customisations' => array( - 'datagenerator' => 'customlang', - 'required' => array('component', 'stringid', 'value'), - ), - 'analytics model' => array ( - 'datagenerator' => 'analytics_model', - 'required' => array('target', 'indicators', 'timesplitting', 'enabled'), - ), - ); - - /** - * Creates the specified element. - * - * The most reliable list of what types of thing can be created is the - * $elements array defined above. - * - * @Given /^the following "(?P(?:[^"]|\\")*)" exist:$/ - * - * @throws Exception - * @throws PendingException - * @param string $elementname The name of the entity to add - * @param TableNode $data - */ - public function the_following_exist($elementname, TableNode $data) { - - // Now that we need them require the data generators. - require_once(__DIR__ . '/../../testing/generator/lib.php'); - - if (empty(self::$elements[$elementname])) { - throw new PendingException($elementname . ' data generator is not implemented'); - } - - $this->datagenerator = testing_util::get_data_generator(); - - $elementdatagenerator = self::$elements[$elementname]['datagenerator']; - $requiredfields = self::$elements[$elementname]['required']; - if (!empty(self::$elements[$elementname]['switchids'])) { - $switchids = self::$elements[$elementname]['switchids']; - } - - foreach ($data->getHash() as $elementdata) { - - // Check if all the required fields are there. - foreach ($requiredfields as $requiredfield) { - if (!isset($elementdata[$requiredfield])) { - throw new Exception($elementname . ' requires the field ' . $requiredfield . ' to be specified'); - } - } - - // Switch from human-friendly references to ids. - if (isset($switchids)) { - foreach ($switchids as $element => $field) { - $methodname = 'get_' . $element . '_id'; - - // Not all the switch fields are required, default vars will be assigned by data generators. - if (isset($elementdata[$element])) { - // Temp $id var to avoid problems when $element == $field. - $id = $this->{$methodname}($elementdata[$element]); - unset($elementdata[$element]); - $elementdata[$field] = $id; - } - } - } - - // Preprocess the entities that requires a special treatment. - if (method_exists($this, 'preprocess_' . $elementdatagenerator)) { - $elementdata = $this->{'preprocess_' . $elementdatagenerator}($elementdata); - } - - // Creates element. - $methodname = 'create_' . $elementdatagenerator; - if (method_exists($this->datagenerator, $methodname)) { - // Using data generators directly. - $this->datagenerator->{$methodname}($elementdata); - - } else if (method_exists($this, 'process_' . $elementdatagenerator)) { - // Using an alternative to the direct data generator call. - $this->{'process_' . $elementdatagenerator}($elementdata); - } else { - throw new PendingException($elementname . ' data generator is not implemented'); - } - } - - } - - /** - * Remove any empty custom fields, to avoid errors when creating the course. - * @param array $data - * @return array - */ - protected function preprocess_course($data) { - foreach ($data as $fieldname => $value) { - if ($value === '' && strpos($fieldname, 'customfield_') === 0) { - unset($data[$fieldname]); - } - } - return $data; - } - - /** - * If password is not set it uses the username. - * @param array $data - * @return array - */ - protected function preprocess_user($data) { - if (!isset($data['password'])) { - $data['password'] = $data['username']; - } - return $data; - } - - /** - * If contextlevel and reference are specified for cohort, transform them to the contextid. - * - * @param array $data - * @return array - */ - protected function preprocess_cohort($data) { - if (isset($data['contextlevel'])) { - if (!isset($data['reference'])) { - throw new Exception('If field contextlevel is specified, field reference must also be present'); - } - $context = $this->get_context($data['contextlevel'], $data['reference']); - unset($data['contextlevel']); - unset($data['reference']); - $data['contextid'] = $context->id; - } - return $data; - } - - /** - * Preprocesses the creation of a grade item. Converts gradetype text to a number. - * @param array $data - * @return array - */ - protected function preprocess_grade_item($data) { - global $CFG; - require_once("$CFG->libdir/grade/constants.php"); - - if (isset($data['gradetype'])) { - $data['gradetype'] = constant("GRADE_TYPE_" . strtoupper($data['gradetype'])); - } - - if (!empty($data['category']) && !empty($data['courseid'])) { - $cat = grade_category::fetch(array('fullname' => $data['category'], 'courseid' => $data['courseid'])); - if (!$cat) { - throw new Exception('Could not resolve category with name "' . $data['category'] . '"'); - } - unset($data['category']); - $data['categoryid'] = $cat->id; - } - - return $data; - } - - /** - * Adapter to modules generator - * @throws Exception Custom exception for test writers - * @param array $data - * @return void - */ - protected function process_activity($data) { - global $DB, $CFG; - - // The the_following_exists() method checks that the field exists. - $activityname = $data['activity']; - unset($data['activity']); - - // Convert scale name into scale id (negative number indicates using scale). - if (isset($data['grade']) && strlen($data['grade']) && !is_number($data['grade'])) { - $data['grade'] = - $this->get_scale_id($data['grade']); - require_once("$CFG->libdir/grade/constants.php"); - - if (!isset($data['gradetype'])) { - $data['gradetype'] = GRADE_TYPE_SCALE; - } - } - - // We split $data in the activity $record and the course module $options. - $cmoptions = array(); - $cmcolumns = $DB->get_columns('course_modules'); - foreach ($cmcolumns as $key => $value) { - if (isset($data[$key])) { - $cmoptions[$key] = $data[$key]; - } - } - - // Custom exception. - try { - $this->datagenerator->create_module($activityname, $data, $cmoptions); - } catch (coding_exception $e) { - throw new Exception('\'' . $activityname . '\' activity can not be added using this step,' . - ' use the step \'I add a "ACTIVITY_OR_RESOURCE_NAME_STRING" to section "SECTION_NUMBER"\' instead'); - } - } - - /** - * Add a block to a page. + * Convert legacy entity names to the new component-specific form. * - * @param array $data should mostly match the fields of the block_instances table. - * The block type is specified by blockname. - * The parentcontextid is set from contextlevel and reference. - * Missing values are filled in by testing_block_generator::prepare_record. - * $data is passed to create_block as both $record and $options. Normally - * the keys are different, so this is a way to let people set values in either place. - */ - protected function process_block_instance($data) { - - if (empty($data['blockname'])) { - throw new Exception('\'blocks\' requires the field \'block\' type to be specified'); - } - - if (empty($data['contextlevel'])) { - throw new Exception('\'blocks\' requires the field \'contextlevel\' to be specified'); - } - - if (!isset($data['reference'])) { - throw new Exception('\'blocks\' requires the field \'reference\' to be specified'); - } - - $context = $this->get_context($data['contextlevel'], $data['reference']); - $data['parentcontextid'] = $context->id; - - // Pass $data as both $record and $options. I think that is unlikely to - // cause problems since the relevant key names are different. - // $options is not used in most blocks I have seen, but where it is, it is necessary. - $this->datagenerator->create_block($data['blockname'], $data, $data); - } - - /** - * Creates language customisation. + * In the past, there was no support for plugins, and everything that + * could be created was handled by the core generator. Now, we can + * support plugins, and so some thing should probably be moved. * - * @throws Exception - * @throws dml_exception - * @param array $data - * @return void - */ - protected function process_customlang($data) { - global $CFG, $DB, $USER; - - require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/customlang/locallib.php'); - require_once($CFG->libdir . '/adminlib.php'); - - if (empty($data['component'])) { - throw new Exception('\'customlang\' requires the field \'component\' type to be specified'); - } - - if (empty($data['stringid'])) { - throw new Exception('\'customlang\' requires the field \'stringid\' to be specified'); - } - - if (!isset($data['value'])) { - throw new Exception('\'customlang\' requires the field \'value\' to be specified'); - } - - $now = time(); - - tool_customlang_utils::checkout($USER->lang); - - $record = $DB->get_record_sql("SELECT s.* - FROM {tool_customlang} s - JOIN {tool_customlang_components} c ON s.componentid = c.id - WHERE c.name = ? AND s.lang = ? AND s.stringid = ?", - array($data['component'], $USER->lang, $data['stringid'])); - - if (empty($data['value']) && !is_null($record->local)) { - $record->local = null; - $record->modified = 1; - $record->outdated = 0; - $record->timecustomized = null; - $DB->update_record('tool_customlang', $record); - tool_customlang_utils::checkin($USER->lang); - } - - if (!empty($data['value']) && $data['value'] != $record->local) { - $record->local = $data['value']; - $record->modified = 1; - $record->outdated = 0; - $record->timecustomized = $now; - $DB->update_record('tool_customlang', $record); - tool_customlang_utils::checkin($USER->lang); - } - } - - /** - * Adapter to enrol_user() data generator. - * @throws Exception - * @param array $data - * @return void - */ - protected function process_enrol_user($data) { - global $SITE; - - if (empty($data['roleid'])) { - throw new Exception('\'course enrolments\' requires the field \'role\' to be specified'); - } - - if (!isset($data['userid'])) { - throw new Exception('\'course enrolments\' requires the field \'user\' to be specified'); - } - - if (!isset($data['courseid'])) { - throw new Exception('\'course enrolments\' requires the field \'course\' to be specified'); - } - - if (!isset($data['enrol'])) { - $data['enrol'] = 'manual'; - } - - if (!isset($data['timestart'])) { - $data['timestart'] = 0; - } - - if (!isset($data['timeend'])) { - $data['timeend'] = 0; - } - - if (!isset($data['status'])) { - $data['status'] = null; - } - - // If the provided course shortname is the site shortname we consider it a system role assign. - if ($data['courseid'] == $SITE->id) { - // Frontpage course assign. - $context = context_course::instance($data['courseid']); - role_assign($data['roleid'], $data['userid'], $context->id); - - } else { - // Course assign. - $this->datagenerator->enrol_user($data['userid'], $data['courseid'], $data['roleid'], $data['enrol'], - $data['timestart'], $data['timeend'], $data['status']); - } - - } - - /** - * Allows/denies a capability at the specified context + * For example, in the future we should probably add + * 'message contacts' => 'core_message > contact'] to + * this array, and move generation of message contact + * from core to core_message. * - * @throws Exception - * @param array $data - * @return void + * @var array old entity type => new entity type. */ - protected function process_permission_override($data) { - - // Will throw an exception if it does not exist. - $context = $this->get_context($data['contextlevel'], $data['reference']); - - switch ($data['permission']) { - case get_string('allow', 'role'): - $permission = CAP_ALLOW; - break; - case get_string('prevent', 'role'): - $permission = CAP_PREVENT; - break; - case get_string('prohibit', 'role'): - $permission = CAP_PROHIBIT; - break; - default: - throw new Exception('The \'' . $data['permission'] . '\' permission does not exist'); - break; - } - - if (is_null(get_capability_info($data['capability']))) { - throw new Exception('The \'' . $data['capability'] . '\' capability does not exist'); - } - - role_change_permission($data['roleid'], $context, $data['capability'], $permission); - } + protected $movedentitytypes = [ + ]; /** - * Assigns a role to a user at system context - * - * Used by "system role assigns" can be deleted when - * system role assign will be deprecated in favour of - * "role assigns" + * Creates the specified element. * - * @throws Exception - * @param array $data - * @return void - */ - protected function process_system_role_assign($data) { - - if (empty($data['roleid'])) { - throw new Exception('\'system role assigns\' requires the field \'role\' to be specified'); - } - - if (!isset($data['userid'])) { - throw new Exception('\'system role assigns\' requires the field \'user\' to be specified'); - } - - $context = context_system::instance(); - - $this->datagenerator->role_assign($data['roleid'], $data['userid'], $context->id); - } - - /** - * Assigns a role to a user at the specified context + * See the class comment for an overview. * - * @throws Exception - * @param array $data - * @return void - */ - protected function process_role_assign($data) { - - if (empty($data['roleid'])) { - throw new Exception('\'role assigns\' requires the field \'role\' to be specified'); - } - - if (!isset($data['userid'])) { - throw new Exception('\'role assigns\' requires the field \'user\' to be specified'); - } - - if (empty($data['contextlevel'])) { - throw new Exception('\'role assigns\' requires the field \'contextlevel\' to be specified'); - } - - if (!isset($data['reference'])) { - throw new Exception('\'role assigns\' requires the field \'reference\' to be specified'); - } - - // Getting the context id. - $context = $this->get_context($data['contextlevel'], $data['reference']); - - $this->datagenerator->role_assign($data['roleid'], $data['userid'], $context->id); - } - - /** - * Creates a role. + * @Given /^the following "(?P(?:[^"]|\\")*)" exist:$/ * - * @param array $data - * @return void + * @param string $entitytype The name of the type entity to add + * @param TableNode $data */ - protected function process_role($data) { - - // We require the user to fill the role shortname. - if (empty($data['shortname'])) { - throw new Exception('\'role\' requires the field \'shortname\' to be specified'); + public function the_following_entities_exist($entitytype, TableNode $data) { + if (isset($this->movedentitytypes[$entitytype])) { + $entitytype = $this->movedentitytypes[$entitytype]; } - - $this->datagenerator->create_role($data); + list($component, $entity) = $this->parse_entity_type($entitytype); + $this->get_instance_for_component($component)->generate_items($entity, $data); } /** - * Adds members to cohorts + * Parse a full entity type like 'users' or 'mod_forum > subscription'. * - * @param array $data - * @return void - */ - protected function process_cohort_member($data) { - cohort_add_member($data['cohortid'], $data['userid']); - } - - /** - * Create a question category. + * E.g. parsing 'course' gives ['core', 'course'] and + * parsing 'core_message > message' gives ['core_message', 'message']. * - * @param array $data the row of data from the behat script. + * @param string $entitytype the entity type + * @return string[] with two elements, component and entity type. */ - protected function process_question_category($data) { - global $DB; - - $context = $this->get_context($data['contextlevel'], $data['reference']); - - // The way this class works, we have already looked up the given parent category - // name and found a matching category. However, it is possible, particularly - // for the 'top' category, for there to be several categories with the - // same name. So far one will have been picked at random, but we need - // the one from the right context. So, if we have the wrong category, try again. - // (Just fixing it here, rather than getting it right first time, is a bit - // of a bodge, but in general this class assumes that names are unique, - // and normally they are, so this was the easiest fix.) - if (!empty($data['parent'])) { - $foundparent = $DB->get_record('question_categories', ['id' => $data['parent']], '*', MUST_EXIST); - if ($foundparent->contextid != $context->id) { - $rightparentid = $DB->get_field('question_categories', 'id', - ['contextid' => $context->id, 'name' => $foundparent->name]); - if (!$rightparentid) { - throw new Exception('The specified question category with name "' . $foundparent->name . - '" does not exist in context "' . $context->get_context_name() . '"."'); - } - $data['parent'] = $rightparentid; + protected function parse_entity_type(string $entitytype): array { + $dividercount = substr_count($entitytype, ' > '); + if ($dividercount === 0) { + return ['core', $entitytype]; + } else if ($dividercount === 1) { + list($component, $type) = explode(' > ', $entitytype); + if ($component === 'core') { + throw new coding_exception('Do not specify the component "core > ..." for entity types.'); } + return [$component, $type]; + } else { + throw new coding_exception('The entity type must be in the form ' . + '"{entity-type}" for core entities, or "{component} > {entity-type}" ' . + 'for entities belonging to other components. ' . + 'For example "users" or "mod_forum > subscriptions".'); } - - $data['contextid'] = $context->id; - $this->datagenerator->get_plugin_generator('core_question')->create_question_category($data); - } - - /** - * Create a question. - * - * Creating questions relies on the question/type/.../tests/helper.php mechanism. - * We start with test_question_maker::get_question_form_data($data['qtype'], $data['template']) - * and then overlay the values from any other fields of $data that are set. - * - * @param array $data the row of data from the behat script. - */ - protected function process_question($data) { - if (array_key_exists('questiontext', $data)) { - $data['questiontext'] = array( - 'text' => $data['questiontext'], - 'format' => FORMAT_HTML, - ); - } - - if (array_key_exists('generalfeedback', $data)) { - $data['generalfeedback'] = array( - 'text' => $data['generalfeedback'], - 'format' => FORMAT_HTML, - ); - } - - $which = null; - if (!empty($data['template'])) { - $which = $data['template']; - } - - $this->datagenerator->get_plugin_generator('core_question')->create_question($data['qtype'], $which, $data); - } - - /** - * Gets the grade category id from the grade category fullname - * @throws Exception - * @param string $username - * @return int - */ - protected function get_gradecategory_id($fullname) { - global $DB; - - if (!$id = $DB->get_field('grade_categories', 'id', array('fullname' => $fullname))) { - throw new Exception('The specified grade category with fullname "' . $fullname . '" does not exist'); - } - return $id; - } - - /** - * Gets the user id from it's username. - * @throws Exception - * @param string $username - * @return int - */ - protected function get_user_id($username) { - global $DB; - - if (!$id = $DB->get_field('user', 'id', array('username' => $username))) { - throw new Exception('The specified user with username "' . $username . '" does not exist'); - } - return $id; - } - - /** - * Gets the role id from it's shortname. - * @throws Exception - * @param string $roleshortname - * @return int - */ - protected function get_role_id($roleshortname) { - global $DB; - - if (!$id = $DB->get_field('role', 'id', array('shortname' => $roleshortname))) { - throw new Exception('The specified role with shortname "' . $roleshortname . '" does not exist'); - } - - return $id; - } - - /** - * Gets the category id from it's idnumber. - * @throws Exception - * @param string $idnumber - * @return int - */ - protected function get_category_id($idnumber) { - global $DB; - - // If no category was specified use the data generator one. - if ($idnumber == false) { - return null; - } - - if (!$id = $DB->get_field('course_categories', 'id', array('idnumber' => $idnumber))) { - throw new Exception('The specified category with idnumber "' . $idnumber . '" does not exist'); - } - - return $id; - } - - /** - * Gets the course id from it's shortname. - * @throws Exception - * @param string $shortname - * @return int - */ - protected function get_course_id($shortname) { - global $DB; - - if (!$id = $DB->get_field('course', 'id', array('shortname' => $shortname))) { - throw new Exception('The specified course with shortname "' . $shortname . '" does not exist'); - } - return $id; - } - - /** - * Gets the group id from it's idnumber. - * @throws Exception - * @param string $idnumber - * @return int - */ - protected function get_group_id($idnumber) { - global $DB; - - if (!$id = $DB->get_field('groups', 'id', array('idnumber' => $idnumber))) { - throw new Exception('The specified group with idnumber "' . $idnumber . '" does not exist'); - } - return $id; - } - - /** - * Gets the grouping id from it's idnumber. - * @throws Exception - * @param string $idnumber - * @return int - */ - protected function get_grouping_id($idnumber) { - global $DB; - - // Do not fetch grouping ID for empty grouping idnumber. - if (empty($idnumber)) { - return null; - } - - if (!$id = $DB->get_field('groupings', 'id', array('idnumber' => $idnumber))) { - throw new Exception('The specified grouping with idnumber "' . $idnumber . '" does not exist'); - } - return $id; - } - - /** - * Gets the cohort id from it's idnumber. - * @throws Exception - * @param string $idnumber - * @return int - */ - protected function get_cohort_id($idnumber) { - global $DB; - - if (!$id = $DB->get_field('cohort', 'id', array('idnumber' => $idnumber))) { - throw new Exception('The specified cohort with idnumber "' . $idnumber . '" does not exist'); - } - return $id; - } - - /** - * Gets the outcome item id from its shortname. - * @throws Exception - * @param string $shortname - * @return int - */ - protected function get_outcome_id($shortname) { - global $DB; - - if (!$id = $DB->get_field('grade_outcomes', 'id', array('shortname' => $shortname))) { - throw new Exception('The specified outcome with shortname "' . $shortname . '" does not exist'); - } - return $id; - } - - /** - * Get the id of a named scale. - * @param string $name the name of the scale. - * @return int the scale id. - */ - protected function get_scale_id($name) { - global $DB; - - if (!$id = $DB->get_field('scale', 'id', array('name' => $name))) { - throw new Exception('The specified scale with name "' . $name . '" does not exist'); - } - return $id; - } - - /** - * Get the id of a named question category (must be globally unique). - * Note that 'Top' is a special value, used when setting the parent of another - * category, meaning top-level. - * - * @param string $name the question category name. - * @return int the question category id. - */ - protected function get_questioncategory_id($name) { - global $DB; - - if ($name == 'Top') { - return 0; - } - - if (!$id = $DB->get_field('question_categories', 'id', array('name' => $name))) { - throw new Exception('The specified question category with name "' . $name . '" does not exist'); - } - return $id; - } - - /** - * Gets the internal context id from the context reference. - * - * The context reference changes depending on the context - * level, it can be the system, a user, a category, a course or - * a module. - * - * @throws Exception - * @param string $levelname The context level string introduced by the test writer - * @param string $contextref The context reference introduced by the test writer - * @return context - */ - protected function get_context($levelname, $contextref) { - global $DB; - - // Getting context levels and names (we will be using the English ones as it is the test site language). - $contextlevels = context_helper::get_all_levels(); - $contextnames = array(); - foreach ($contextlevels as $level => $classname) { - $contextnames[context_helper::get_level_name($level)] = $level; - } - - if (empty($contextnames[$levelname])) { - throw new Exception('The specified "' . $levelname . '" context level does not exist'); - } - $contextlevel = $contextnames[$levelname]; - - // Return it, we don't need to look for other internal ids. - if ($contextlevel == CONTEXT_SYSTEM) { - return context_system::instance(); - } - - switch ($contextlevel) { - - case CONTEXT_USER: - $instanceid = $DB->get_field('user', 'id', array('username' => $contextref)); - break; - - case CONTEXT_COURSECAT: - $instanceid = $DB->get_field('course_categories', 'id', array('idnumber' => $contextref)); - break; - - case CONTEXT_COURSE: - $instanceid = $DB->get_field('course', 'id', array('shortname' => $contextref)); - break; - - case CONTEXT_MODULE: - $instanceid = $DB->get_field('course_modules', 'id', array('idnumber' => $contextref)); - break; - - default: - break; - } - - $contextclass = $contextlevels[$contextlevel]; - if (!$context = $contextclass::instance($instanceid, IGNORE_MISSING)) { - throw new Exception('The specified "' . $contextref . '" context reference does not exist'); - } - - return $context; - } - - /** - * Adds user to contacts - * - * @param array $data - * @return void - */ - protected function process_message_contacts($data) { - \core_message\api::add_contact($data['userid'], $data['contactid']); - } - - /** - * Gets the contact id from it's username. - * @throws Exception - * @param string $username - * @return int - */ - protected function get_contact_id($username) { - global $DB; - - if (!$id = $DB->get_field('user', 'id', array('username' => $username))) { - throw new Exception('The specified user with username "' . $username . '" does not exist'); - } - return $id; - } - - /** - * Send a new message from user to contact in a private conversation - * - * @param array $data - * @return void - */ - protected function process_private_messages(array $data) { - if (empty($data['format'])) { - $data['format'] = 'FORMAT_PLAIN'; - } - - if (!$conversationid = \core_message\api::get_conversation_between_users([$data['userid'], $data['contactid']])) { - $conversation = \core_message\api::create_conversation( - \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, - [$data['userid'], $data['contactid']] - ); - $conversationid = $conversation->id; - } - \core_message\api::send_message_to_conversation( - $data['userid'], - $conversationid, - $data['message'], - constant($data['format']) - ); - } - - /** - * Send a new message from user to a group conversation - * - * @param array $data - * @return void - */ - protected function process_group_messages(array $data) { - global $DB; - - if (empty($data['format'])) { - $data['format'] = 'FORMAT_PLAIN'; - } - - $group = $DB->get_record('groups', ['id' => $data['groupid']]); - $coursecontext = context_course::instance($group->courseid); - if (!$conversation = \core_message\api::get_conversation_by_area('core_group', 'groups', $data['groupid'], - $coursecontext->id)) { - $members = $DB->get_records_menu('groups_members', ['groupid' => $data['groupid']], '', 'userid, id'); - $conversation = \core_message\api::create_conversation( - \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, - array_keys($members), - $group->name, - \core_message\api::MESSAGE_CONVERSATION_ENABLED, - 'core_group', - 'groups', - $group->id, - $coursecontext->id); - } - \core_message\api::send_message_to_conversation( - $data['userid'], - $conversation->id, - $data['message'], - constant($data['format']) - ); } /** - * Mark a private conversation as favourite for user + * Get an instance of the appropriate subclass of this class for a given component. * - * @param array $data - * @return void + * @param string $component The name of the component to generate entities for. + * @return behat_generator_base the subclass of this class for the requested component. */ - protected function process_favourite_conversations(array $data) { - if (!$conversationid = \core_message\api::get_conversation_between_users([$data['userid'], $data['contactid']])) { - $conversation = \core_message\api::create_conversation( - \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, - [$data['userid'], $data['contactid']] - ); - $conversationid = $conversation->id; - } - \core_message\api::set_favourite_conversation($conversationid, $data['userid']); - } + protected function get_instance_for_component(string $component): behat_generator_base { + global $CFG; - /** - * Mute an existing group conversation for user - * - * @param array $data - * @return void - */ - protected function process_mute_group_conversations(array $data) { - if (groups_is_member($data['groupid'], $data['userid'])) { - $context = context_course::instance($data['courseid']); - $conversation = \core_message\api::get_conversation_by_area( - 'core_group', - 'groups', - $data['groupid'], - $context->id - ); - if ($conversation) { - \core_message\api::mute_conversation($data['userid'], $conversation->id); + // Ensure the generator class is loaded. + require_once($CFG->libdir . '/behat/classes/behat_generator_base.php'); + if ($component === 'core') { + $lib = $CFG->libdir . '/behat/classes/behat_core_generator.php'; + } else { + $dir = core_component::get_component_directory($component); + $lib = $dir . '/tests/generator/behat_' . $component . '_generator.php'; + if (!$dir || !is_readable($lib)) { + throw new coding_exception("Component {$component} does not support " . + "behat generators yet. Missing {$lib}."); } } - } + require_once($lib); - /** - * Mute a private conversation for user - * - * @param array $data - * @return void - */ - protected function process_mute_private_conversations(array $data) { - if (!$conversationid = \core_message\api::get_conversation_between_users([$data['userid'], $data['contactid']])) { - $conversation = \core_message\api::create_conversation( - \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, - [$data['userid'], $data['contactid']] - ); - $conversationid = $conversation->id; + // Create an instance. + $componentclass = "behat_{$component}_generator"; + if (!class_exists($componentclass)) { + throw new PendingException($component . + ' does not yet support the Behat data generator mechanism. Class ' . + $componentclass . ' not found in file ' . $lib . '.'); } - \core_message\api::mute_conversation($data['userid'], $conversationid); - } - - /** - * Transform indicators string into array. - * - * @param array $data - * @return array - */ - protected function preprocess_analytics_model($data) { - $data['indicators'] = explode(',', $data['indicators']); - return $data; - } - - /** - * Creates an analytics model - * - * @param target $data - * @return void - */ - protected function process_analytics_model($data) { - \core_analytics\manager::create_declared_model($data); + $instance = new $componentclass($component); + return $instance; } } diff --git a/mod/quiz/tests/behat/quiz_reset.feature b/mod/quiz/tests/behat/quiz_reset.feature index ade7c7605c0..71724d79986 100644 --- a/mod/quiz/tests/behat/quiz_reset.feature +++ b/mod/quiz/tests/behat/quiz_reset.feature @@ -47,32 +47,24 @@ Feature: Quiz reset And I am on the "Test quiz name" "mod_quiz > Grades report" page Then I should see "Attempts: 0" - @javascript Scenario: Use course reset to remove user overrides. - When I am on the "Test quiz name" "mod_quiz > User overrides" page logged in as "teacher1" - And I press "Add user override" - And I set the following fields to these values: - | Override user | Student1 | - | Attempts allowed | 2 | - And I press "Save" - And I should see "Sam1 Student1" + Given the following "mod_quiz > user overrides" exist: + | quiz | user | attempts | + | Test quiz name | student1 | 2 | + When I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to "Reset" in current page administration - And I set the following fields to these values: - | Delete all user overrides | 1 | + And I set the field "Delete all user overrides" to "1" And I press "Reset course" And I press "Continue" And I am on the "Test quiz name" "mod_quiz > User overrides" page Then I should not see "Sam1 Student1" Scenario: Use course reset to remove group overrides. - When I am on the "Test quiz name" "mod_quiz > Group overrides" page logged in as "teacher1" - And I press "Add group override" - And I set the following fields to these values: - | Override group | Group 1 | - | Attempts allowed | 2 | - And I press "Save" - And I should see "Group 1" + Given the following "mod_quiz > group overrides" exist: + | quiz | group | attempts | + | Test quiz name | G1 | 2 | + When I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to "Reset" in current page administration And I set the following fields to these values: diff --git a/mod/quiz/tests/generator/behat_mod_quiz_generator.php b/mod/quiz/tests/generator/behat_mod_quiz_generator.php new file mode 100644 index 00000000000..55a814534f6 --- /dev/null +++ b/mod/quiz/tests/generator/behat_mod_quiz_generator.php @@ -0,0 +1,66 @@ +. + +/** + * Behat data generator for mod_quiz. + * + * @package mod_quiz + * @category test + * @copyright 2019 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Behat data generator for mod_quiz. + * + * @copyright 2019 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_mod_quiz_generator extends behat_generator_base { + + protected function get_creatable_entities(): array { + return [ + 'group overrides' => [ + 'datagenerator' => 'override', + 'required' => ['quiz', 'group'], + 'switchids' => ['quiz' => 'quiz', 'group' => 'groupid'], + ], + 'user overrides' => [ + 'datagenerator' => 'override', + 'required' => ['quiz', 'user'], + 'switchids' => ['quiz' => 'quiz', 'user' => 'userid'], + ], + ]; + } + + /** + * Look up the id of a quiz from its name. + * + * @param string $quizname the quiz name, for example 'Test quiz'. + * @return int corresponding id. + */ + protected function get_quiz_id(string $quizname): int { + global $DB; + + if (!$id = $DB->get_field('quiz', 'id', ['name' => $quizname])) { + throw new Exception('There is no quiz with name "' . $quizname . '" does not exist'); + } + return $id; + } +} diff --git a/mod/quiz/tests/generator/lib.php b/mod/quiz/tests/generator/lib.php index 5b18decb333..d45e8e79ca8 100644 --- a/mod/quiz/tests/generator/lib.php +++ b/mod/quiz/tests/generator/lib.php @@ -170,4 +170,27 @@ class mod_quiz_generator extends testing_module_generator { $attemptobj->process_finish(time(), false); } } + + /** + * Create a quiz override (either user or group). + * + * @param array $data must specify quizid, and one of userid or groupid. + */ + public function create_override(array $data): void { + global $DB; + + if (!isset($data['quiz'])) { + throw new coding_exception('Must specify quiz (id) when creating a quiz override.'); + } + + if (!isset($data['userid']) && !isset($data['groupid'])) { + throw new coding_exception('Must specify one of userid or groupid when creating a quiz override.'); + } + + if (isset($data['userid']) && isset($data['groupid'])) { + throw new coding_exception('Cannot specify both userid and groupid when creating a quiz override.'); + } + + $DB->insert_record('quiz_overrides', (object) $data); + } } -- 2.43.0