Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]

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