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 the condition tree class and related logic.
  19   *
  20   * @package core_availability
  21   * @copyright 2014 The Open University
  22   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  use core_availability\capability_checker;
  26  use \core_availability\tree;
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  /**
  31   * Unit tests for the condition tree class and related logic.
  32   *
  33   * @package core_availability
  34   * @copyright 2014 The Open University
  35   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  class tree_testcase extends \advanced_testcase {
  38      public function setUp(): void {
  39          // Load the mock classes so they can be used.
  40          require_once (__DIR__ . '/fixtures/mock_condition.php');
  41          require_once (__DIR__ . '/fixtures/mock_info.php');
  42      }
  43  
  44      /**
  45       * Tests constructing a tree with errors.
  46       */
  47      public function test_construct_errors() {
  48          try {
  49              new tree('frog');
  50              $this->fail();
  51          } catch (coding_exception $e) {
  52              $this->assertStringContainsString('not object', $e->getMessage());
  53          }
  54          try {
  55              new tree((object)array());
  56              $this->fail();
  57          } catch (coding_exception $e) {
  58              $this->assertStringContainsString('missing ->op', $e->getMessage());
  59          }
  60          try {
  61              new tree((object)array('op' => '*'));
  62              $this->fail();
  63          } catch (coding_exception $e) {
  64              $this->assertStringContainsString('unknown ->op', $e->getMessage());
  65          }
  66          try {
  67              new tree((object)array('op' => '|'));
  68              $this->fail();
  69          } catch (coding_exception $e) {
  70              $this->assertStringContainsString('missing ->show', $e->getMessage());
  71          }
  72          try {
  73              new tree((object)array('op' => '|', 'show' => 0));
  74              $this->fail();
  75          } catch (coding_exception $e) {
  76              $this->assertStringContainsString('->show not bool', $e->getMessage());
  77          }
  78          try {
  79              new tree((object)array('op' => '&'));
  80              $this->fail();
  81          } catch (coding_exception $e) {
  82              $this->assertStringContainsString('missing ->showc', $e->getMessage());
  83          }
  84          try {
  85              new tree((object)array('op' => '&', 'showc' => 0));
  86              $this->fail();
  87          } catch (coding_exception $e) {
  88              $this->assertStringContainsString('->showc not array', $e->getMessage());
  89          }
  90          try {
  91              new tree((object)array('op' => '&', 'showc' => array(0)));
  92              $this->fail();
  93          } catch (coding_exception $e) {
  94              $this->assertStringContainsString('->showc value not bool', $e->getMessage());
  95          }
  96          try {
  97              new tree((object)array('op' => '|', 'show' => true));
  98              $this->fail();
  99          } catch (coding_exception $e) {
 100              $this->assertStringContainsString('missing ->c', $e->getMessage());
 101          }
 102          try {
 103              new tree((object)array('op' => '|', 'show' => true,
 104                      'c' => 'side'));
 105              $this->fail();
 106          } catch (coding_exception $e) {
 107              $this->assertStringContainsString('->c not array', $e->getMessage());
 108          }
 109          try {
 110              new tree((object)array('op' => '|', 'show' => true,
 111                      'c' => array(3)));
 112              $this->fail();
 113          } catch (coding_exception $e) {
 114              $this->assertStringContainsString('child not object', $e->getMessage());
 115          }
 116          try {
 117              new tree((object)array('op' => '|', 'show' => true,
 118                      'c' => array((object)array('type' => 'doesnotexist'))));
 119              $this->fail();
 120          } catch (coding_exception $e) {
 121              $this->assertStringContainsString('Unknown condition type: doesnotexist', $e->getMessage());
 122          }
 123          try {
 124              new tree((object)array('op' => '|', 'show' => true,
 125                      'c' => array((object)array())));
 126              $this->fail();
 127          } catch (coding_exception $e) {
 128              $this->assertStringContainsString('missing ->op', $e->getMessage());
 129          }
 130          try {
 131              new tree((object)array('op' => '&',
 132                      'c' => array((object)array('op' => '&', 'c' => array())),
 133                      'showc' => array(true, true)
 134                      ));
 135              $this->fail();
 136          } catch (coding_exception $e) {
 137              $this->assertStringContainsString('->c, ->showc mismatch', $e->getMessage());
 138          }
 139      }
 140  
 141      /**
 142       * Tests constructing a tree with plugin that does not exist (ignored).
 143       */
 144      public function test_construct_ignore_missing_plugin() {
 145          // Construct a tree with & combination of one condition that doesn't exist.
 146          $tree = new tree(tree::get_root_json(array(
 147                  (object)array('type' => 'doesnotexist')), tree::OP_OR), true);
 148          // Expected result is an empty tree with | condition, shown.
 149          $this->assertEquals('+|()', (string)$tree);
 150      }
 151  
 152      /**
 153       * Tests constructing a tree with subtrees using all available operators.
 154       */
 155      public function test_construct_just_trees() {
 156          $structure = tree::get_root_json(array(
 157                  tree::get_nested_json(array(), tree::OP_OR),
 158                  tree::get_nested_json(array(
 159                      tree::get_nested_json(array(), tree::OP_NOT_OR)), tree::OP_NOT_AND)),
 160                  tree::OP_AND, array(true, true));
 161          $tree = new tree($structure);
 162          $this->assertEquals('&(+|(),+!&(!|()))', (string)$tree);
 163      }
 164  
 165      /**
 166       * Tests constructing tree using the mock plugin.
 167       */
 168      public function test_construct_with_mock_plugin() {
 169          $structure = tree::get_root_json(array(
 170                  self::mock(array('a' => true, 'm' => ''))), tree::OP_OR);
 171          $tree = new tree($structure);
 172          $this->assertEquals('+|({mock:y,})', (string)$tree);
 173      }
 174  
 175      /**
 176       * Tests the check_available and get_result_information functions.
 177       */
 178      public function test_check_available() {
 179          global $USER;
 180  
 181          // Setup.
 182          $this->resetAfterTest();
 183          $info = new \core_availability\mock_info();
 184          $this->setAdminUser();
 185          $information = '';
 186  
 187          // No conditions.
 188          $structure = tree::get_root_json(array(), tree::OP_OR);
 189          list ($available, $information) = $this->get_available_results(
 190                  $structure, $info, $USER->id);
 191          $this->assertTrue($available);
 192  
 193          // One condition set to yes.
 194          $structure->c = array(
 195                  self::mock(array('a' => true)));
 196          list ($available, $information) = $this->get_available_results(
 197                  $structure, $info, $USER->id);
 198          $this->assertTrue($available);
 199  
 200          // One condition set to no.
 201          $structure->c = array(
 202                  self::mock(array('a' => false, 'm' => 'no')));
 203          list ($available, $information) = $this->get_available_results(
 204                  $structure, $info, $USER->id);
 205          $this->assertFalse($available);
 206          $this->assertEquals('SA: no', $information);
 207  
 208          // Two conditions, OR, resolving as true.
 209          $structure->c = array(
 210                  self::mock(array('a' => false, 'm' => 'no')),
 211                  self::mock(array('a' => true)));
 212          list ($available, $information) = $this->get_available_results(
 213                  $structure, $info, $USER->id);
 214          $this->assertTrue($available);
 215          $this->assertEquals('', $information);
 216  
 217          // Two conditions, OR, resolving as false.
 218          $structure->c = array(
 219                  self::mock(array('a' => false, 'm' => 'no')),
 220                  self::mock(array('a' => false, 'm' => 'way')));
 221          list ($available, $information) = $this->get_available_results(
 222                  $structure, $info, $USER->id);
 223          $this->assertFalse($available);
 224          $this->assertRegExp('~any of.*no.*way~', $information);
 225  
 226          // Two conditions, OR, resolving as false, no display.
 227          $structure->show = false;
 228          list ($available, $information) = $this->get_available_results(
 229                  $structure, $info, $USER->id);
 230          $this->assertFalse($available);
 231          $this->assertEquals('', $information);
 232  
 233          // Two conditions, AND, resolving as true.
 234          $structure->op = '&';
 235          unset($structure->show);
 236          $structure->showc = array(true, true);
 237          $structure->c = array(
 238                  self::mock(array('a' => true)),
 239                  self::mock(array('a' => true)));
 240          list ($available, $information) = $this->get_available_results(
 241                  $structure, $info, $USER->id);
 242          $this->assertTrue($available);
 243  
 244          // Two conditions, AND, one false.
 245          $structure->c = array(
 246                  self::mock(array('a' => false, 'm' => 'wom')),
 247                  self::mock(array('a' => true, 'm' => '')));
 248          list ($available, $information) = $this->get_available_results(
 249                  $structure, $info, $USER->id);
 250          $this->assertFalse($available);
 251          $this->assertEquals('SA: wom', $information);
 252  
 253          // Two conditions, AND, both false.
 254          $structure->c = array(
 255                  self::mock(array('a' => false, 'm' => 'wom')),
 256                  self::mock(array('a' => false, 'm' => 'bat')));
 257          list ($available, $information) = $this->get_available_results(
 258                  $structure, $info, $USER->id);
 259          $this->assertFalse($available);
 260          $this->assertRegExp('~wom.*bat~', $information);
 261  
 262          // Two conditions, AND, both false, show turned off for one. When
 263          // show is turned off, that means if you don't have that condition
 264          // you don't get to see anything at all.
 265          $structure->showc[0] = false;
 266          list ($available, $information) = $this->get_available_results(
 267                  $structure, $info, $USER->id);
 268          $this->assertFalse($available);
 269          $this->assertEquals('', $information);
 270          $structure->showc[0] = true;
 271  
 272          // Two conditions, NOT OR, both false.
 273          $structure->op = '!|';
 274          list ($available, $information) = $this->get_available_results(
 275                  $structure, $info, $USER->id);
 276          $this->assertTrue($available);
 277  
 278          // Two conditions, NOT OR, one true.
 279          $structure->c[0]->a = true;
 280          list ($available, $information) = $this->get_available_results(
 281                  $structure, $info, $USER->id);
 282          $this->assertFalse($available);
 283          $this->assertEquals('SA: !wom', $information);
 284  
 285          // Two conditions, NOT OR, both true.
 286          $structure->c[1]->a = true;
 287          list ($available, $information) = $this->get_available_results(
 288                  $structure, $info, $USER->id);
 289          $this->assertFalse($available);
 290          $this->assertRegExp('~!wom.*!bat~', $information);
 291  
 292          // Two conditions, NOT AND, both true.
 293          $structure->op = '!&';
 294          unset($structure->showc);
 295          $structure->show = true;
 296          list ($available, $information) = $this->get_available_results(
 297                  $structure, $info, $USER->id);
 298          $this->assertFalse($available);
 299          $this->assertRegExp('~any of.*!wom.*!bat~', $information);
 300  
 301          // Two conditions, NOT AND, one true.
 302          $structure->c[1]->a = false;
 303          list ($available, $information) = $this->get_available_results(
 304                  $structure, $info, $USER->id);
 305          $this->assertTrue($available);
 306  
 307          // Nested NOT conditions; true.
 308          $structure->c = array(
 309                  tree::get_nested_json(array(
 310                      self::mock(array('a' => true, 'm' => 'no'))), tree::OP_NOT_AND));
 311          list ($available, $information) = $this->get_available_results(
 312                  $structure, $info, $USER->id);
 313          $this->assertTrue($available);
 314  
 315          // Nested NOT conditions; false (note no ! in message).
 316          $structure->c[0]->c[0]->a = false;
 317          list ($available, $information) = $this->get_available_results(
 318                  $structure, $info, $USER->id);
 319          $this->assertFalse($available);
 320          $this->assertEquals('SA: no', $information);
 321  
 322          // Nested condition groups, message test.
 323          $structure->op = '|';
 324          $structure->c = array(
 325                  tree::get_nested_json(array(
 326                      self::mock(array('a' => false, 'm' => '1')),
 327                      self::mock(array('a' => false, 'm' => '2'))
 328                      ), tree::OP_AND),
 329                  self::mock(array('a' => false, 'm' => 3)));
 330          list ($available, $information) = $this->get_available_results(
 331                  $structure, $info, $USER->id);
 332          $this->assertFalse($available);
 333          $this->assertRegExp('~<ul.*<ul.*<li.*1.*<li.*2.*</ul>.*<li.*3~', $information);
 334      }
 335  
 336      /**
 337       * Shortcut function to check availability and also get information.
 338       *
 339       * @param stdClass $structure Tree structure
 340       * @param \core_availability\info $info Location info
 341       * @param int $userid User id
 342       */
 343      protected function get_available_results($structure, \core_availability\info $info, $userid) {
 344          global $PAGE;
 345          $tree = new tree($structure);
 346          $result = $tree->check_available(false, $info, true, $userid);
 347          $information = $tree->get_result_information($info, $result);
 348          if (!is_string($information)) {
 349              $renderer = $PAGE->get_renderer('core', 'availability');
 350              $information = $renderer->render($information);
 351          }
 352          return array($result->is_available(), $information);
 353      }
 354  
 355      /**
 356       * Tests the is_available_for_all() function.
 357       */
 358      public function test_is_available_for_all() {
 359          // Empty tree is always available.
 360          $structure = tree::get_root_json(array(), tree::OP_OR);
 361          $tree = new tree($structure);
 362          $this->assertTrue($tree->is_available_for_all());
 363  
 364          // Tree with normal item in it, not always available.
 365          $structure->c[0] = (object)array('type' => 'mock');
 366          $tree = new tree($structure);
 367          $this->assertFalse($tree->is_available_for_all());
 368  
 369          // OR tree with one always-available item.
 370          $structure->c[1] = self::mock(array('all' => true));
 371          $tree = new tree($structure);
 372          $this->assertTrue($tree->is_available_for_all());
 373  
 374          // AND tree with one always-available and one not.
 375          $structure->op = '&';
 376          $structure->showc = array(true, true);
 377          unset($structure->show);
 378          $tree = new tree($structure);
 379          $this->assertFalse($tree->is_available_for_all());
 380  
 381          // Test NOT conditions (items not always-available).
 382          $structure->op = '!&';
 383          $structure->show = true;
 384          unset($structure->showc);
 385          $tree = new tree($structure);
 386          $this->assertFalse($tree->is_available_for_all());
 387  
 388          // Test again with one item always-available for NOT mode.
 389          $structure->c[1]->allnot = true;
 390          $tree = new tree($structure);
 391          $this->assertTrue($tree->is_available_for_all());
 392      }
 393  
 394      /**
 395       * Tests the get_full_information() function.
 396       */
 397      public function test_get_full_information() {
 398          global $PAGE;
 399          $renderer = $PAGE->get_renderer('core', 'availability');
 400          // Setup.
 401          $info = new \core_availability\mock_info();
 402  
 403          // No conditions.
 404          $structure = tree::get_root_json(array(), tree::OP_OR);
 405          $tree = new tree($structure);
 406          $this->assertEquals('', $tree->get_full_information($info));
 407  
 408          // Condition (normal and NOT).
 409          $structure->c = array(
 410                  self::mock(array('m' => 'thing')));
 411          $tree = new tree($structure);
 412          $this->assertEquals('SA: [FULL]thing',
 413                  $tree->get_full_information($info));
 414          $structure->op = '!&';
 415          $tree = new tree($structure);
 416          $this->assertEquals('SA: ![FULL]thing',
 417                  $tree->get_full_information($info));
 418  
 419          // Complex structure.
 420          $structure->op = '|';
 421          $structure->c = array(
 422                  tree::get_nested_json(array(
 423                      self::mock(array('m' => '1')),
 424                      self::mock(array('m' => '2'))), tree::OP_AND),
 425                  self::mock(array('m' => 3)));
 426          $tree = new tree($structure);
 427          $this->assertRegExp('~<ul.*<ul.*<li.*1.*<li.*2.*</ul>.*<li.*3~',
 428                  $renderer->render($tree->get_full_information($info)));
 429  
 430          // Test intro messages before list. First, OR message.
 431          $structure->c = array(
 432                  self::mock(array('m' => '1')),
 433                  self::mock(array('m' => '2'))
 434          );
 435          $tree = new tree($structure);
 436          $this->assertRegExp('~Not available unless any of:.*<ul>~',
 437                  $renderer->render($tree->get_full_information($info)));
 438  
 439          // Now, OR message when not shown.
 440          $structure->show = false;
 441          $tree = new tree($structure);
 442          $this->assertRegExp('~hidden.*<ul>~',
 443                  $renderer->render($tree->get_full_information($info)));
 444  
 445          // AND message.
 446          $structure->op = '&';
 447          unset($structure->show);
 448          $structure->showc = array(false, false);
 449          $tree = new tree($structure);
 450          $this->assertRegExp('~Not available unless:.*<ul>~',
 451                  $renderer->render($tree->get_full_information($info)));
 452  
 453          // Hidden markers on items.
 454          $this->assertRegExp('~1.*hidden.*2.*hidden~',
 455                  $renderer->render($tree->get_full_information($info)));
 456  
 457          // Hidden markers on child tree and items.
 458          $structure->c[1] = tree::get_nested_json(array(
 459                  self::mock(array('m' => '2')),
 460                  self::mock(array('m' => '3'))), tree::OP_AND);
 461          $tree = new tree($structure);
 462          $this->assertRegExp('~1.*hidden.*All of \(hidden.*2.*3~',
 463                  $renderer->render($tree->get_full_information($info)));
 464          $structure->c[1]->op = '|';
 465          $tree = new tree($structure);
 466          $this->assertRegExp('~1.*hidden.*Any of \(hidden.*2.*3~',
 467                  $renderer->render($tree->get_full_information($info)));
 468  
 469          // Hidden markers on single-item display, AND and OR.
 470          $structure->showc = array(false);
 471          $structure->c = array(
 472                  self::mock(array('m' => '1'))
 473          );
 474          $tree = new tree($structure);
 475          $this->assertRegExp('~1.*hidden~',
 476                  $tree->get_full_information($info));
 477  
 478          unset($structure->showc);
 479          $structure->show = false;
 480          $structure->op = '|';
 481          $tree = new tree($structure);
 482          $this->assertRegExp('~1.*hidden~',
 483                  $tree->get_full_information($info));
 484  
 485          // Hidden marker if single item is tree.
 486          $structure->c[0] = tree::get_nested_json(array(
 487                  self::mock(array('m' => '1')),
 488                  self::mock(array('m' => '2'))), tree::OP_AND);
 489          $tree = new tree($structure);
 490          $this->assertRegExp('~Not available \(hidden.*1.*2~',
 491                  $renderer->render($tree->get_full_information($info)));
 492  
 493          // Single item tree containing single item.
 494          unset($structure->c[0]->c[1]);
 495          $tree = new tree($structure);
 496          $this->assertRegExp('~SA.*1.*hidden~',
 497                  $tree->get_full_information($info));
 498      }
 499  
 500      /**
 501       * Tests the is_empty() function.
 502       */
 503      public function test_is_empty() {
 504          // Tree with nothing in should be empty.
 505          $structure = tree::get_root_json(array(), tree::OP_OR);
 506          $tree = new tree($structure);
 507          $this->assertTrue($tree->is_empty());
 508  
 509          // Tree with something in is not empty.
 510          $structure = tree::get_root_json(array(self::mock(array('m' => '1'))), tree::OP_OR);
 511          $tree = new tree($structure);
 512          $this->assertFalse($tree->is_empty());
 513      }
 514  
 515      /**
 516       * Tests the get_all_children() function.
 517       */
 518      public function test_get_all_children() {
 519          // Create a tree with nothing in.
 520          $structure = tree::get_root_json(array(), tree::OP_OR);
 521          $tree1 = new tree($structure);
 522  
 523          // Create second tree with complex structure.
 524          $structure->c = array(
 525                  tree::get_nested_json(array(
 526                      self::mock(array('m' => '1')),
 527                      self::mock(array('m' => '2'))
 528                  ), tree::OP_OR),
 529                  self::mock(array('m' => 3)));
 530          $tree2 = new tree($structure);
 531  
 532          // Check list of conditions from both trees.
 533          $this->assertEquals(array(), $tree1->get_all_children('core_availability\condition'));
 534          $result = $tree2->get_all_children('core_availability\condition');
 535          $this->assertEquals(3, count($result));
 536          $this->assertEquals('{mock:n,1}', (string)$result[0]);
 537          $this->assertEquals('{mock:n,2}', (string)$result[1]);
 538          $this->assertEquals('{mock:n,3}', (string)$result[2]);
 539  
 540          // Check specific type, should give same results.
 541          $result2 = $tree2->get_all_children('availability_mock\condition');
 542          $this->assertEquals($result, $result2);
 543      }
 544  
 545      /**
 546       * Tests the update_dependency_id() function.
 547       */
 548      public function test_update_dependency_id() {
 549          // Create tree with structure of 3 mocks.
 550          $structure = tree::get_root_json(array(
 551                  tree::get_nested_json(array(
 552                      self::mock(array('table' => 'frogs', 'id' => 9)),
 553                      self::mock(array('table' => 'zombies', 'id' => 9))
 554                  )),
 555                  self::mock(array('table' => 'frogs', 'id' => 9))));
 556  
 557          // Get 'before' value.
 558          $tree = new tree($structure);
 559          $before = $tree->save();
 560  
 561          // Try replacing a table or id that isn't used.
 562          $this->assertFalse($tree->update_dependency_id('toads', 9, 13));
 563          $this->assertFalse($tree->update_dependency_id('frogs', 7, 8));
 564          $this->assertEquals($before, $tree->save());
 565  
 566          // Replace the zombies one.
 567          $this->assertTrue($tree->update_dependency_id('zombies', 9, 666));
 568          $after = $tree->save();
 569          $this->assertEquals(666, $after->c[0]->c[1]->id);
 570  
 571          // And the frogs one.
 572          $this->assertTrue($tree->update_dependency_id('frogs', 9, 3));
 573          $after = $tree->save();
 574          $this->assertEquals(3, $after->c[0]->c[0]->id);
 575          $this->assertEquals(3, $after->c[1]->id);
 576      }
 577  
 578      /**
 579       * Tests the filter_users function.
 580       */
 581      public function test_filter_users() {
 582          $info = new \core_availability\mock_info();
 583          $checker = new capability_checker($info->get_context());
 584  
 585          // Don't need to create real users in database, just use these ids.
 586          $users = array(1 => null, 2 => null, 3 => null);
 587  
 588          // Test basic tree with one condition that doesn't filter.
 589          $structure = tree::get_root_json(array(self::mock(array())));
 590          $tree = new tree($structure);
 591          $result = $tree->filter_user_list($users, false, $info, $checker);
 592          ksort($result);
 593          $this->assertEquals(array(1, 2, 3), array_keys($result));
 594  
 595          // Now a tree with one condition that filters.
 596          $structure = tree::get_root_json(array(self::mock(array('filter' => array(2, 3)))));
 597          $tree = new tree($structure);
 598          $result = $tree->filter_user_list($users, false, $info, $checker);
 599          ksort($result);
 600          $this->assertEquals(array(2, 3), array_keys($result));
 601  
 602          // Tree with two conditions that both filter (|).
 603          $structure = tree::get_root_json(array(
 604                  self::mock(array('filter' => array(3))),
 605                  self::mock(array('filter' => array(1)))), tree::OP_OR);
 606          $tree = new tree($structure);
 607          $result = $tree->filter_user_list($users, false, $info, $checker);
 608          ksort($result);
 609          $this->assertEquals(array(1, 3), array_keys($result));
 610  
 611          // Tree with OR condition one of which doesn't filter.
 612          $structure = tree::get_root_json(array(
 613                  self::mock(array('filter' => array(3))),
 614                  self::mock(array())), tree::OP_OR);
 615          $tree = new tree($structure);
 616          $result = $tree->filter_user_list($users, false, $info, $checker);
 617          ksort($result);
 618          $this->assertEquals(array(1, 2, 3), array_keys($result));
 619  
 620          // Tree with two condition that both filter (&).
 621          $structure = tree::get_root_json(array(
 622                  self::mock(array('filter' => array(2, 3))),
 623                  self::mock(array('filter' => array(1, 2)))));
 624          $tree = new tree($structure);
 625          $result = $tree->filter_user_list($users, false, $info, $checker);
 626          ksort($result);
 627          $this->assertEquals(array(2), array_keys($result));
 628  
 629          // Tree with child tree with NOT condition.
 630          $structure = tree::get_root_json(array(
 631                  tree::get_nested_json(array(
 632                      self::mock(array('filter' => array(1)))), tree::OP_NOT_AND)));
 633          $tree = new tree($structure);
 634          $result = $tree->filter_user_list($users, false, $info, $checker);
 635          ksort($result);
 636          $this->assertEquals(array(2, 3), array_keys($result));
 637      }
 638  
 639      /**
 640       * Tests the get_json methods in tree (which are mainly for use in testing
 641       * but might be used elsewhere).
 642       */
 643      public function test_get_json() {
 644          // Create a simple child object (fake).
 645          $child = (object)array('type' => 'fake');
 646          $childstr = json_encode($child);
 647  
 648          // Minimal case.
 649          $this->assertEquals(
 650                  (object)array('op' => '&', 'c' => array()),
 651                  tree::get_nested_json(array()));
 652          // Children and different operator.
 653          $this->assertEquals(
 654                  (object)array('op' => '|', 'c' => array($child, $child)),
 655                  tree::get_nested_json(array($child, $child), tree::OP_OR));
 656  
 657          // Root empty.
 658          $this->assertEquals('{"op":"&","c":[],"showc":[]}',
 659                  json_encode(tree::get_root_json(array(), tree::OP_AND)));
 660          // Root with children (multi-show operator).
 661          $this->assertEquals('{"op":"&","c":[' . $childstr . ',' . $childstr .
 662                      '],"showc":[true,true]}',
 663                  json_encode(tree::get_root_json(array($child, $child), tree::OP_AND)));
 664          // Root with children (single-show operator).
 665          $this->assertEquals('{"op":"|","c":[' . $childstr . ',' . $childstr .
 666                      '],"show":true}',
 667                  json_encode(tree::get_root_json(array($child, $child), tree::OP_OR)));
 668          // Root with children (specified show boolean).
 669          $this->assertEquals('{"op":"&","c":[' . $childstr . ',' . $childstr .
 670                      '],"showc":[false,false]}',
 671                  json_encode(tree::get_root_json(array($child, $child), tree::OP_AND, false)));
 672          // Root with children (specified show array).
 673          $this->assertEquals('{"op":"&","c":[' . $childstr . ',' . $childstr .
 674                      '],"showc":[true,false]}',
 675                  json_encode(tree::get_root_json(array($child, $child), tree::OP_AND, array(true, false))));
 676      }
 677  
 678      /**
 679       * Tests the behaviour of the counter in unique_sql_parameter().
 680       *
 681       * There was a problem with static counters used to implement a sequence of
 682       * parameter placeholders (MDL-53481). As always with static variables, it
 683       * is a bit tricky to unit test the behaviour reliably as it depends on the
 684       * actual tests executed and also their order.
 685       *
 686       * To minimise risk of false expected behaviour, this test method should be
 687       * first one where {@link core_availability\tree::get_user_list_sql()} is
 688       * used. We also use higher number of condition instances to increase the
 689       * risk of the counter collision, should there remain a problem.
 690       */
 691      public function test_unique_sql_parameter_behaviour() {
 692          global $DB;
 693          $this->resetAfterTest();
 694          $generator = $this->getDataGenerator();
 695  
 696          // Create a test course with multiple groupings and groups and a student in each of them.
 697          $course = $generator->create_course();
 698          $user = $generator->create_user();
 699          $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
 700          $generator->enrol_user($user->id, $course->id, $studentroleid);
 701          // The total number of groupings and groups must not be greater than 61.
 702          // There is a limit in MySQL on the max number of joined tables.
 703          $groups = [];
 704          for ($i = 0; $i < 25; $i++) {
 705              $group = $generator->create_group(array('courseid' => $course->id));
 706              groups_add_member($group, $user);
 707              $groups[] = $group;
 708          }
 709          $groupings = [];
 710          for ($i = 0; $i < 25; $i++) {
 711              $groupings[] = $generator->create_grouping(array('courseid' => $course->id));
 712          }
 713          foreach ($groupings as $grouping) {
 714              foreach ($groups as $group) {
 715                  groups_assign_grouping($grouping->id, $group->id);
 716              }
 717          }
 718          $info = new \core_availability\mock_info($course);
 719  
 720          // Make a huge tree with 'AND' of all groups and groupings conditions.
 721          $conditions = [];
 722          foreach ($groups as $group) {
 723              $conditions[] = \availability_group\condition::get_json($group->id);
 724          }
 725          foreach ($groupings as $groupingid) {
 726              $conditions[] = \availability_grouping\condition::get_json($grouping->id);
 727          }
 728          shuffle($conditions);
 729          $tree = new tree(tree::get_root_json($conditions));
 730          list($sql, $params) = $tree->get_user_list_sql(false, $info, false);
 731          // This must not throw exception.
 732          $DB->fix_sql_params($sql, $params);
 733      }
 734  
 735      /**
 736       * Tests get_user_list_sql.
 737       */
 738      public function test_get_user_list_sql() {
 739          global $DB;
 740          $this->resetAfterTest();
 741          $generator = $this->getDataGenerator();
 742  
 743          // Create a test course with 2 groups and users in each combination of them.
 744          $course = $generator->create_course();
 745          $group1 = $generator->create_group(array('courseid' => $course->id));
 746          $group2 = $generator->create_group(array('courseid' => $course->id));
 747          $userin1 = $generator->create_user();
 748          $userin2 = $generator->create_user();
 749          $userinboth = $generator->create_user();
 750          $userinneither = $generator->create_user();
 751          $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
 752          foreach (array($userin1, $userin2, $userinboth, $userinneither) as $user) {
 753              $generator->enrol_user($user->id, $course->id, $studentroleid);
 754          }
 755          groups_add_member($group1, $userin1);
 756          groups_add_member($group2, $userin2);
 757          groups_add_member($group1, $userinboth);
 758          groups_add_member($group2, $userinboth);
 759          $info = new \core_availability\mock_info($course);
 760  
 761          // Tree with single group condition.
 762          $tree = new tree(tree::get_root_json(array(
 763              \availability_group\condition::get_json($group1->id)
 764              )));
 765          list($sql, $params) = $tree->get_user_list_sql(false, $info, false);
 766          $result = $DB->get_fieldset_sql($sql, $params);
 767          sort($result);
 768          $this->assertEquals(array($userin1->id, $userinboth->id), $result);
 769  
 770          // Tree with 'AND' of both group conditions.
 771          $tree = new tree(tree::get_root_json(array(
 772              \availability_group\condition::get_json($group1->id),
 773              \availability_group\condition::get_json($group2->id)
 774          )));
 775          list($sql, $params) = $tree->get_user_list_sql(false, $info, false);
 776          $result = $DB->get_fieldset_sql($sql, $params);
 777          sort($result);
 778          $this->assertEquals(array($userinboth->id), $result);
 779  
 780          // Tree with 'AND' of both group conditions.
 781          $tree = new tree(tree::get_root_json(array(
 782              \availability_group\condition::get_json($group1->id),
 783              \availability_group\condition::get_json($group2->id)
 784          ), tree::OP_OR));
 785          list($sql, $params) = $tree->get_user_list_sql(false, $info, false);
 786          $result = $DB->get_fieldset_sql($sql, $params);
 787          sort($result);
 788          $this->assertEquals(array($userin1->id, $userin2->id, $userinboth->id), $result);
 789  
 790          // Check with flipped logic (NOT above level of tree).
 791          list($sql, $params) = $tree->get_user_list_sql(true, $info, false);
 792          $result = $DB->get_fieldset_sql($sql, $params);
 793          sort($result);
 794          $this->assertEquals(array($userinneither->id), $result);
 795  
 796          // Tree with 'OR' of group conditions and a non-filtering condition.
 797          // The non-filtering condition should mean that ALL users are included.
 798          $tree = new tree(tree::get_root_json(array(
 799              \availability_group\condition::get_json($group1->id),
 800              \availability_date\condition::get_json(\availability_date\condition::DIRECTION_UNTIL, 3)
 801          ), tree::OP_OR));
 802          list($sql, $params) = $tree->get_user_list_sql(false, $info, false);
 803          $this->assertEquals('', $sql);
 804          $this->assertEquals(array(), $params);
 805      }
 806  
 807      /**
 808       * Utility function to build the PHP structure representing a mock condition.
 809       *
 810       * @param array $params Mock parameters
 811       * @return \stdClass Structure object
 812       */
 813      protected static function mock(array $params) {
 814          $params['type'] = 'mock';
 815          return (object)$params;
 816      }
 817  }