Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Unit tests for Random allocation
  19   *
  20   * @package    workshopallocation_random
  21   * @category   phpunit
  22   * @copyright  2009 David Mudrak <david.mudrak@gmail.com>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  // Include the code to test
  29  global $CFG;
  30  require_once($CFG->dirroot . '/mod/workshop/locallib.php');
  31  require_once($CFG->dirroot . '/mod/workshop/allocation/random/lib.php');
  32  
  33  
  34  class workshopallocation_random_testcase extends advanced_testcase {
  35  
  36      /** workshop instance emulation */
  37      protected $workshop;
  38  
  39      /** allocator instance */
  40      protected $allocator;
  41  
  42      protected function setUp(): void {
  43          parent::setUp();
  44          $this->resetAfterTest();
  45          $this->setAdminUser();
  46          $course = $this->getDataGenerator()->create_course();
  47          $workshop = $this->getDataGenerator()->create_module('workshop', array('course' => $course));
  48          $cm = get_fast_modinfo($course)->instances['workshop'][$workshop->id];
  49          $this->workshop = new workshop($workshop, $cm, $course);
  50          $this->allocator = new testable_workshop_random_allocator($this->workshop);
  51      }
  52  
  53      protected function tearDown(): void {
  54          $this->allocator    = null;
  55          $this->workshop     = null;
  56          parent::tearDown();
  57      }
  58  
  59      public function test_self_allocation_empty_values() {
  60          // fixture setup & exercise SUT & verify
  61          $this->assertEquals(array(), $this->allocator->self_allocation());
  62      }
  63  
  64      public function test_self_allocation_equal_user_groups() {
  65          // fixture setup
  66          $authors    = array(0 => array_fill_keys(array(4, 6, 10), new stdclass()));
  67          $reviewers  = array(0 => array_fill_keys(array(4, 6, 10), new stdclass()));
  68          // exercise SUT
  69          $newallocations = $this->allocator->self_allocation($authors, $reviewers);
  70          // verify
  71          $this->assertEquals(array(array(4 => 4), array(6 => 6), array(10 => 10)), $newallocations);
  72      }
  73  
  74      public function test_self_allocation_different_user_groups() {
  75          // fixture setup
  76          $authors    = array(0 => array_fill_keys(array(1, 4, 5, 10, 13), new stdclass()));
  77          $reviewers  = array(0 => array_fill_keys(array(4, 7, 10), new stdclass()));
  78          // exercise SUT
  79          $newallocations = $this->allocator->self_allocation($authors, $reviewers);
  80          // verify
  81          $this->assertEquals(array(array(4 => 4), array(10 => 10)), $newallocations);
  82      }
  83  
  84      public function test_self_allocation_skip_existing() {
  85          // fixture setup
  86          $authors        = array(0 => array_fill_keys(array(3, 4, 10), new stdclass()));
  87          $reviewers      = array(0 => array_fill_keys(array(3, 4, 10), new stdclass()));
  88          $assessments    = array(23 => (object)array('authorid' => 3, 'reviewerid' => 3));
  89          // exercise SUT
  90          $newallocations = $this->allocator->self_allocation($authors, $reviewers, $assessments);
  91          // verify
  92          $this->assertEquals(array(array(4 => 4), array(10 => 10)), $newallocations);
  93      }
  94  
  95      public function test_get_author_ids() {
  96          // fixture setup
  97          $newallocations = array(array(1 => 3), array(2 => 1), array(3 => 1));
  98          // exercise SUT & verify
  99          $this->assertEquals(array(3, 1), $this->allocator->get_author_ids($newallocations));
 100      }
 101  
 102      public function test_index_submissions_by_authors() {
 103          // fixture setup
 104          $submissions = array(
 105              676 => (object)array('id' => 676, 'authorid' => 23),
 106              121 => (object)array('id' => 121, 'authorid' => 56),
 107          );
 108          // exercise SUT
 109          $submissions = $this->allocator->index_submissions_by_authors($submissions);
 110          // verify
 111          $this->assertEquals(array(
 112              23 => (object)array('id' => 676, 'authorid' => 23),
 113              56 => (object)array('id' => 121, 'authorid' => 56),
 114          ), $submissions);
 115      }
 116  
 117      public function test_index_submissions_by_authors_duplicate_author() {
 118          // fixture setup
 119          $submissions = array(
 120              14 => (object)array('id' => 676, 'authorid' => 3),
 121              87 => (object)array('id' => 121, 'authorid' => 3),
 122          );
 123          // exercise SUT
 124          $this->expectException(moodle_exception::class);
 125          $submissions = $this->allocator->index_submissions_by_authors($submissions);
 126      }
 127  
 128      public function test_get_unique_allocations() {
 129          // fixture setup
 130          $newallocations = array(array(4 => 5), array(6 => 6), array(1 => 16), array(4 => 5), array(16 => 1));
 131          // exercise SUT
 132          $newallocations = $this->allocator->get_unique_allocations($newallocations);
 133          // verify
 134          $this->assertEquals(array(array(4 => 5), array(6 => 6), array(1 => 16), array(16 => 1)), $newallocations);
 135      }
 136  
 137      public function test_get_unkept_assessments_no_keep_selfassessments() {
 138          // fixture setup
 139          $assessments = array(
 140              23 => (object)array('authorid' => 3, 'reviewerid' => 3),
 141              45 => (object)array('authorid' => 5, 'reviewerid' => 11),
 142              12 => (object)array('authorid' => 6, 'reviewerid' => 3),
 143          );
 144          $newallocations = array(array(4 => 5), array(11 => 5), array(1 => 16), array(4 => 5), array(16 => 1));
 145          // exercise SUT
 146          $delassessments = $this->allocator->get_unkept_assessments($assessments, $newallocations, false);
 147          // verify
 148          // we want to keep $assessments[45] because it has been re-allocated
 149          $this->assertEquals(array(23, 12), $delassessments);
 150      }
 151  
 152      public function test_get_unkept_assessments_keep_selfassessments() {
 153          // fixture setup
 154          $assessments = array(
 155              23 => (object)array('authorid' => 3, 'reviewerid' => 3),
 156              45 => (object)array('authorid' => 5, 'reviewerid' => 11),
 157              12 => (object)array('authorid' => 6, 'reviewerid' => 3),
 158          );
 159          $newallocations = array(array(4 => 5), array(11 => 5), array(1 => 16), array(4 => 5), array(16 => 1));
 160          // exercise SUT
 161          $delassessments = $this->allocator->get_unkept_assessments($assessments, $newallocations, true);
 162          // verify
 163          // we want to keep $assessments[45] because it has been re-allocated
 164          // we want to keep $assessments[23] because if is self assessment
 165          $this->assertEquals(array(12), $delassessments);
 166      }
 167  
 168      /**
 169       * Aggregates assessment info per author and per reviewer
 170       */
 171      public function test_convert_assessments_to_links() {
 172          // fixture setup
 173          $assessments = array(
 174              23 => (object)array('authorid' => 3, 'reviewerid' => 3),
 175              45 => (object)array('authorid' => 5, 'reviewerid' => 11),
 176              12 => (object)array('authorid' => 5, 'reviewerid' => 3),
 177          );
 178          // exercise SUT
 179          list($authorlinks, $reviewerlinks) = $this->allocator->convert_assessments_to_links($assessments);
 180          // verify
 181          $this->assertEquals(array(3 => array(3), 5 => array(11, 3)), $authorlinks);
 182          $this->assertEquals(array(3 => array(3, 5), 11 => array(5)), $reviewerlinks);
 183      }
 184  
 185      /**
 186       * Trivial case
 187       */
 188      public function test_convert_assessments_to_links_empty() {
 189          // fixture setup
 190          $assessments = array();
 191          // exercise SUT
 192          list($authorlinks, $reviewerlinks) = $this->allocator->convert_assessments_to_links($assessments);
 193          // verify
 194          $this->assertEquals(array(), $authorlinks);
 195          $this->assertEquals(array(), $reviewerlinks);
 196      }
 197  
 198      /**
 199       * If there is a single element with the lowest workload, it should be chosen
 200       */
 201      public function test_get_element_with_lowest_workload_deterministic() {
 202          // fixture setup
 203          $workload = array(4 => 6, 9 => 1, 10 => 2);
 204          // exercise SUT
 205          $chosen = $this->allocator->get_element_with_lowest_workload($workload);
 206          // verify
 207          $this->assertEquals(9, $chosen);
 208      }
 209  
 210      /**
 211       * If there are no elements available, must return false
 212       */
 213      public function test_get_element_with_lowest_workload_impossible() {
 214          // fixture setup
 215          $workload = array();
 216          // exercise SUT
 217          $chosen = $this->allocator->get_element_with_lowest_workload($workload);
 218          // verify
 219          $this->assertTrue($chosen === false);
 220      }
 221  
 222      /**
 223       * If there are several elements with the lowest workload, one of them should be chosen randomly
 224       */
 225      public function test_get_element_with_lowest_workload_random() {
 226          // fixture setup
 227          $workload = array(4 => 6, 9 => 2, 10 => 2);
 228          // exercise SUT
 229          $elements = $this->allocator->get_element_with_lowest_workload($workload);
 230          // verify
 231          // in theory, this test can fail even if the function works well. However, the probability of getting
 232          // a row of a hundred same ids in this use case is 1/pow(2, 100)
 233          // also, this just tests that each of the two elements has been chosen at least once. this is not to
 234          // measure the quality or randomness of the algorithm
 235          $counts = array(4 => 0, 9 => 0, 10 => 0);
 236          for ($i = 0; $i < 100; $i++) {
 237              $chosen = $this->allocator->get_element_with_lowest_workload($workload);
 238              if (!in_array($chosen, array(4, 9, 10))) {
 239                  $this->fail('Invalid element ' . var_export($chosen, true) . ' chosen');
 240                  break;
 241              } else {
 242                  $counts[$this->allocator->get_element_with_lowest_workload($workload)]++;
 243              }
 244          }
 245          $this->assertTrue(($counts[9] > 0) && ($counts[10] > 0));
 246      }
 247  
 248      /**
 249       * Floats should be rounded before they are compared
 250       *
 251       * This should test
 252       */
 253      public function test_get_element_with_lowest_workload_random_floats() {
 254          // fixture setup
 255          $workload = array(1 => 1/13, 2 => 0.0769230769231); // should be considered as the same value
 256          // exercise SUT
 257          $elements = $this->allocator->get_element_with_lowest_workload($workload);
 258          // verify
 259          $counts = array(1 => 0, 2 => 0);
 260          for ($i = 0; $i < 100; $i++) {
 261              $chosen = $this->allocator->get_element_with_lowest_workload($workload);
 262              if (!in_array($chosen, array(1, 2))) {
 263                  $this->fail('Invalid element ' . var_export($chosen, true) . ' chosen');
 264                  break;
 265              } else {
 266                  $counts[$this->allocator->get_element_with_lowest_workload($workload)]++;
 267              }
 268          }
 269          $this->assertTrue(($counts[1] > 0) && ($counts[2] > 0));
 270  
 271      }
 272  
 273      /**
 274       * Filter new assessments so they do not contain existing
 275       */
 276      public function test_filter_current_assessments() {
 277          // fixture setup
 278          $newallocations = array(array(3 => 5), array(11 => 5), array(2 => 9), array(3 => 5));
 279          $assessments = array(
 280              23 => (object)array('authorid' => 3, 'reviewerid' => 3),
 281              45 => (object)array('authorid' => 5, 'reviewerid' => 11),
 282              12 => (object)array('authorid' => 5, 'reviewerid' => 3),
 283          );
 284          // exercise SUT
 285          $this->allocator->filter_current_assessments($newallocations, $assessments);
 286          // verify
 287          $this->assertEquals(array_values($newallocations), array(array(2 => 9)));
 288      }
 289  
 290  
 291  }
 292  
 293  
 294  /**
 295   * Make protected methods we want to test public
 296   */
 297  class testable_workshop_random_allocator extends workshop_random_allocator {
 298      public function self_allocation($authors=array(), $reviewers=array(), $assessments=array()) {
 299          return parent::self_allocation($authors, $reviewers, $assessments);
 300      }
 301      public function get_author_ids($newallocations) {
 302          return parent::get_author_ids($newallocations);
 303      }
 304      public function index_submissions_by_authors($submissions) {
 305          return parent::index_submissions_by_authors($submissions);
 306      }
 307      public function get_unique_allocations($newallocations) {
 308          return parent::get_unique_allocations($newallocations);
 309      }
 310      public function get_unkept_assessments($assessments, $newallocations, $keepselfassessments) {
 311          return parent::get_unkept_assessments($assessments, $newallocations, $keepselfassessments);
 312      }
 313      public function convert_assessments_to_links($assessments) {
 314          return parent::convert_assessments_to_links($assessments);
 315      }
 316      public function get_element_with_lowest_workload($workload) {
 317          return parent::get_element_with_lowest_workload($workload);
 318      }
 319      public function filter_current_assessments(&$newallocations, $assessments) {
 320          return parent::filter_current_assessments($newallocations, $assessments);
 321      }
 322  }