Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

   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;
 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              $renderer = $PAGE->get_renderer('core', 'availability');
 339              $information = $renderer->render($information);
 340          }
 341          return array($result->is_available(), $information);
 342      }
 343  
 344      /**
 345       * Tests the is_available_for_all() function.
 346       */
 347      public function test_is_available_for_all() {
 348          // Empty tree is always available.
 349          $structure = tree::get_root_json(array(), tree::OP_OR);
 350          $tree = new tree($structure);
 351          $this->assertTrue($tree->is_available_for_all());
 352  
 353          // Tree with normal item in it, not always available.
 354          $structure->c[0] = (object)array('type' => 'mock');
 355          $tree = new tree($structure);
 356          $this->assertFalse($tree->is_available_for_all());
 357  
 358          // OR tree with one always-available item.
 359          $structure->c[1] = self::mock(array('all' => true));
 360          $tree = new tree($structure);
 361          $this->assertTrue($tree->is_available_for_all());
 362  
 363          // AND tree with one always-available and one not.
 364          $structure->op = '&';
 365          $structure->showc = array(true, true);
 366          unset($structure->show);
 367          $tree = new tree($structure);
 368          $this->assertFalse($tree->is_available_for_all());
 369  
 370          // Test NOT conditions (items not always-available).
 371          $structure->op = '!&';
 372          $structure->show = true;
 373          unset($structure->showc);
 374          $tree = new tree($structure);
 375          $this->assertFalse($tree->is_available_for_all());
 376  
 377          // Test again with one item always-available for NOT mode.
 378          $structure->c[1]->allnot = true;
 379          $tree = new tree($structure);
 380          $this->assertTrue($tree->is_available_for_all());
 381      }
 382  
 383      /**
 384       * Tests the get_full_information() function.
 385       */
 386      public function test_get_full_information() {
 387          global $PAGE;
 388          $renderer = $PAGE->get_renderer('core', 'availability');
 389          // Setup.
 390          $info = new \core_availability\mock_info();
 391  
 392          // No conditions.
 393          $structure = tree::get_root_json(array(), tree::OP_OR);
 394          $tree = new tree($structure);
 395          $this->assertEquals('', $tree->get_full_information($info));
 396  
 397          // Condition (normal and NOT).
 398          $structure->c = array(
 399                  self::mock(array('m' => 'thing')));
 400          $tree = new tree($structure);
 401          $this->assertEquals('SA: [FULL]thing',
 402                  $tree->get_full_information($info));
 403          $structure->op = '!&';
 404          $tree = new tree($structure);
 405          $this->assertEquals('SA: ![FULL]thing',
 406                  $tree->get_full_information($info));
 407  
 408          // Complex structure.
 409          $structure->op = '|';
 410          $structure->c = array(
 411                  tree::get_nested_json(array(
 412                      self::mock(array('m' => '1')),
 413                      self::mock(array('m' => '2'))), tree::OP_AND),
 414                  self::mock(array('m' => 3)));
 415          $tree = new tree($structure);
 416          $this->assertMatchesRegularExpression('~<ul.*<ul.*<li.*1.*<li.*2.*</ul>.*<li.*3~',
 417                  $renderer->render($tree->get_full_information($info)));
 418  
 419          // Test intro messages before list. First, OR message.
 420          $structure->c = array(
 421                  self::mock(array('m' => '1')),
 422                  self::mock(array('m' => '2'))
 423          );
 424          $tree = new tree($structure);
 425          $this->assertMatchesRegularExpression('~Not available unless any of:.*<ul>~',
 426                  $renderer->render($tree->get_full_information($info)));
 427  
 428          // Now, OR message when not shown.
 429          $structure->show = false;
 430          $tree = new tree($structure);
 431          $this->assertMatchesRegularExpression('~hidden.*<ul>~',
 432                  $renderer->render($tree->get_full_information($info)));
 433  
 434          // AND message.
 435          $structure->op = '&';
 436          unset($structure->show);
 437          $structure->showc = array(false, false);
 438          $tree = new tree($structure);
 439          $this->assertMatchesRegularExpression('~Not available unless:.*<ul>~',
 440                  $renderer->render($tree->get_full_information($info)));
 441  
 442          // Hidden markers on items.
 443          $this->assertMatchesRegularExpression('~1.*hidden.*2.*hidden~',
 444                  $renderer->render($tree->get_full_information($info)));
 445  
 446          // Hidden markers on child tree and items.
 447          $structure->c[1] = tree::get_nested_json(array(
 448                  self::mock(array('m' => '2')),
 449                  self::mock(array('m' => '3'))), tree::OP_AND);
 450          $tree = new tree($structure);
 451          $this->assertMatchesRegularExpression('~1.*hidden.*All of \(hidden.*2.*3~',
 452                  $renderer->render($tree->get_full_information($info)));
 453          $structure->c[1]->op = '|';
 454          $tree = new tree($structure);
 455          $this->assertMatchesRegularExpression('~1.*hidden.*Any of \(hidden.*2.*3~',
 456                  $renderer->render($tree->get_full_information($info)));
 457  
 458          // Hidden markers on single-item display, AND and OR.
 459          $structure->showc = array(false);
 460          $structure->c = array(
 461                  self::mock(array('m' => '1'))
 462          );
 463          $tree = new tree($structure);
 464          $this->assertMatchesRegularExpression('~1.*hidden~',
 465                  $tree->get_full_information($info));
 466  
 467          unset($structure->showc);
 468          $structure->show = false;
 469          $structure->op = '|';
 470          $tree = new tree($structure);
 471          $this->assertMatchesRegularExpression('~1.*hidden~',
 472                  $tree->get_full_information($info));
 473  
 474          // Hidden marker if single item is tree.
 475          $structure->c[0] = tree::get_nested_json(array(
 476                  self::mock(array('m' => '1')),
 477                  self::mock(array('m' => '2'))), tree::OP_AND);
 478          $tree = new tree($structure);
 479          $this->assertMatchesRegularExpression('~Not available \(hidden.*1.*2~',
 480                  $renderer->render($tree->get_full_information($info)));
 481  
 482          // Single item tree containing single item.
 483          unset($structure->c[0]->c[1]);
 484          $tree = new tree($structure);
 485          $this->assertMatchesRegularExpression('~SA.*1.*hidden~',
 486                  $tree->get_full_information($info));
 487      }
 488  
 489      /**
 490       * Tests the is_empty() function.
 491       */
 492      public function test_is_empty() {
 493          // Tree with nothing in should be empty.
 494          $structure = tree::get_root_json(array(), tree::OP_OR);
 495          $tree = new tree($structure);
 496          $this->assertTrue($tree->is_empty());
 497  
 498          // Tree with something in is not empty.
 499          $structure = tree::get_root_json(array(self::mock(array('m' => '1'))), tree::OP_OR);
 500          $tree = new tree($structure);
 501          $this->assertFalse($tree->is_empty());
 502      }
 503  
 504      /**
 505       * Tests the get_all_children() function.
 506       */
 507      public function test_get_all_children() {
 508          // Create a tree with nothing in.
 509          $structure = tree::get_root_json(array(), tree::OP_OR);
 510          $tree1 = new tree($structure);
 511  
 512          // Create second tree with complex structure.
 513          $structure->c = array(
 514                  tree::get_nested_json(array(
 515                      self::mock(array('m' => '1')),
 516                      self::mock(array('m' => '2'))
 517                  ), tree::OP_OR),
 518                  self::mock(array('m' => 3)));
 519          $tree2 = new tree($structure);
 520  
 521          // Check list of conditions from both trees.
 522          $this->assertEquals(array(), $tree1->get_all_children('core_availability\condition'));
 523          $result = $tree2->get_all_children('core_availability\condition');
 524          $this->assertEquals(3, count($result));
 525          $this->assertEquals('{mock:n,1}', (string)$result[0]);
 526          $this->assertEquals('{mock:n,2}', (string)$result[1]);
 527          $this->assertEquals('{mock:n,3}', (string)$result[2]);
 528  
 529          // Check specific type, should give same results.
 530          $result2 = $tree2->get_all_children('availability_mock\condition');
 531          $this->assertEquals($result, $result2);
 532      }
 533  
 534      /**
 535       * Tests the update_dependency_id() function.
 536       */
 537      public function test_update_dependency_id() {
 538          // Create tree with structure of 3 mocks.
 539          $structure = tree::get_root_json(array(
 540                  tree::get_nested_json(array(
 541                      self::mock(array('table' => 'frogs', 'id' => 9)),
 542                      self::mock(array('table' => 'zombies', 'id' => 9))
 543                  )),
 544                  self::mock(array('table' => 'frogs', 'id' => 9))));
 545  
 546          // Get 'before' value.
 547          $tree = new tree($structure);
 548          $before = $tree->save();
 549  
 550          // Try replacing a table or id that isn't used.
 551          $this->assertFalse($tree->update_dependency_id('toads', 9, 13));
 552          $this->assertFalse($tree->update_dependency_id('frogs', 7, 8));
 553          $this->assertEquals($before, $tree->save());
 554  
 555          // Replace the zombies one.
 556          $this->assertTrue($tree->update_dependency_id('zombies', 9, 666));
 557          $after = $tree->save();
 558          $this->assertEquals(666, $after->c[0]->c[1]->id);
 559  
 560          // And the frogs one.
 561          $this->assertTrue($tree->update_dependency_id('frogs', 9, 3));
 562          $after = $tree->save();
 563          $this->assertEquals(3, $after->c[0]->c[0]->id);
 564          $this->assertEquals(3, $after->c[1]->id);
 565      }
 566  
 567      /**
 568       * Tests the filter_users function.
 569       */
 570      public function test_filter_users() {
 571          $info = new \core_availability\mock_info();
 572          $checker = new capability_checker($info->get_context());
 573  
 574          // Don't need to create real users in database, just use these ids.
 575          $users = array(1 => null, 2 => null, 3 => null);
 576  
 577          // Test basic tree with one condition that doesn't filter.
 578          $structure = tree::get_root_json(array(self::mock(array())));
 579          $tree = new tree($structure);
 580          $result = $tree->filter_user_list($users, false, $info, $checker);
 581          ksort($result);
 582          $this->assertEquals(array(1, 2, 3), array_keys($result));
 583  
 584          // Now a tree with one condition that filters.
 585          $structure = tree::get_root_json(array(self::mock(array('filter' => array(2, 3)))));
 586          $tree = new tree($structure);
 587          $result = $tree->filter_user_list($users, false, $info, $checker);
 588          ksort($result);
 589          $this->assertEquals(array(2, 3), array_keys($result));
 590  
 591          // Tree with two conditions that both filter (|).
 592          $structure = tree::get_root_json(array(
 593                  self::mock(array('filter' => array(3))),
 594                  self::mock(array('filter' => array(1)))), tree::OP_OR);
 595          $tree = new tree($structure);
 596          $result = $tree->filter_user_list($users, false, $info, $checker);
 597          ksort($result);
 598          $this->assertEquals(array(1, 3), array_keys($result));
 599  
 600          // Tree with OR condition one of which doesn't filter.
 601          $structure = tree::get_root_json(array(
 602                  self::mock(array('filter' => array(3))),
 603                  self::mock(array())), tree::OP_OR);
 604          $tree = new tree($structure);
 605          $result = $tree->filter_user_list($users, false, $info, $checker);
 606          ksort($result);
 607          $this->assertEquals(array(1, 2, 3), array_keys($result));
 608  
 609          // Tree with two condition that both filter (&).
 610          $structure = tree::get_root_json(array(
 611                  self::mock(array('filter' => array(2, 3))),
 612                  self::mock(array('filter' => array(1, 2)))));
 613          $tree = new tree($structure);
 614          $result = $tree->filter_user_list($users, false, $info, $checker);
 615          ksort($result);
 616          $this->assertEquals(array(2), array_keys($result));
 617  
 618          // Tree with child tree with NOT condition.
 619          $structure = tree::get_root_json(array(
 620                  tree::get_nested_json(array(
 621                      self::mock(array('filter' => array(1)))), tree::OP_NOT_AND)));
 622          $tree = new tree($structure);
 623          $result = $tree->filter_user_list($users, false, $info, $checker);
 624          ksort($result);
 625          $this->assertEquals(array(2, 3), array_keys($result));
 626      }
 627  
 628      /**
 629       * Tests the get_json methods in tree (which are mainly for use in testing
 630       * but might be used elsewhere).
 631       */
 632      public function test_get_json() {
 633          // Create a simple child object (fake).
 634          $child = (object)array('type' => 'fake');
 635          $childstr = json_encode($child);
 636  
 637          // Minimal case.
 638          $this->assertEquals(
 639                  (object)array('op' => '&', 'c' => array()),
 640                  tree::get_nested_json(array()));
 641          // Children and different operator.
 642          $this->assertEquals(
 643                  (object)array('op' => '|', 'c' => array($child, $child)),
 644                  tree::get_nested_json(array($child, $child), tree::OP_OR));
 645  
 646          // Root empty.
 647          $this->assertEquals('{"op":"&","c":[],"showc":[]}',
 648                  json_encode(tree::get_root_json(array(), tree::OP_AND)));
 649          // Root with children (multi-show operator).
 650          $this->assertEquals('{"op":"&","c":[' . $childstr . ',' . $childstr .
 651                      '],"showc":[true,true]}',
 652                  json_encode(tree::get_root_json(array($child, $child), tree::OP_AND)));
 653          // Root with children (single-show operator).
 654          $this->assertEquals('{"op":"|","c":[' . $childstr . ',' . $childstr .
 655                      '],"show":true}',
 656                  json_encode(tree::get_root_json(array($child, $child), tree::OP_OR)));
 657          // Root with children (specified show boolean).
 658          $this->assertEquals('{"op":"&","c":[' . $childstr . ',' . $childstr .
 659                      '],"showc":[false,false]}',
 660                  json_encode(tree::get_root_json(array($child, $child), tree::OP_AND, false)));
 661          // Root with children (specified show array).
 662          $this->assertEquals('{"op":"&","c":[' . $childstr . ',' . $childstr .
 663                      '],"showc":[true,false]}',
 664                  json_encode(tree::get_root_json(array($child, $child), tree::OP_AND, array(true, false))));
 665      }
 666  
 667      /**
 668       * Tests the behaviour of the counter in unique_sql_parameter().
 669       *
 670       * There was a problem with static counters used to implement a sequence of
 671       * parameter placeholders (MDL-53481). As always with static variables, it
 672       * is a bit tricky to unit test the behaviour reliably as it depends on the
 673       * actual tests executed and also their order.
 674       *
 675       * To minimise risk of false expected behaviour, this test method should be
 676       * first one where {@link core_availability\tree::get_user_list_sql()} is
 677       * used. We also use higher number of condition instances to increase the
 678       * risk of the counter collision, should there remain a problem.
 679       */
 680      public function test_unique_sql_parameter_behaviour() {
 681          global $DB;
 682          $this->resetAfterTest();
 683          $generator = $this->getDataGenerator();
 684  
 685          // Create a test course with multiple groupings and groups and a student in each of them.
 686          $course = $generator->create_course();
 687          $user = $generator->create_user();
 688          $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
 689          $generator->enrol_user($user->id, $course->id, $studentroleid);
 690          // The total number of groupings and groups must not be greater than 61.
 691          // There is a limit in MySQL on the max number of joined tables.
 692          $groups = [];
 693          for ($i = 0; $i < 25; $i++) {
 694              $group = $generator->create_group(array('courseid' => $course->id));
 695              groups_add_member($group, $user);
 696              $groups[] = $group;
 697          }
 698          $groupings = [];
 699          for ($i = 0; $i < 25; $i++) {
 700              $groupings[] = $generator->create_grouping(array('courseid' => $course->id));
 701          }
 702          foreach ($groupings as $grouping) {
 703              foreach ($groups as $group) {
 704                  groups_assign_grouping($grouping->id, $group->id);
 705              }
 706          }
 707          $info = new \core_availability\mock_info($course);
 708  
 709          // Make a huge tree with 'AND' of all groups and groupings conditions.
 710          $conditions = [];
 711          foreach ($groups as $group) {
 712              $conditions[] = \availability_group\condition::get_json($group->id);
 713          }
 714          foreach ($groupings as $groupingid) {
 715              $conditions[] = \availability_grouping\condition::get_json($grouping->id);
 716          }
 717          shuffle($conditions);
 718          $tree = new tree(tree::get_root_json($conditions));
 719          list($sql, $params) = $tree->get_user_list_sql(false, $info, false);
 720          // This must not throw exception.
 721          $DB->fix_sql_params($sql, $params);
 722      }
 723  
 724      /**
 725       * Tests get_user_list_sql.
 726       */
 727      public function test_get_user_list_sql() {
 728          global $DB;
 729          $this->resetAfterTest();
 730          $generator = $this->getDataGenerator();
 731  
 732          // Create a test course with 2 groups and users in each combination of them.
 733          $course = $generator->create_course();
 734          $group1 = $generator->create_group(array('courseid' => $course->id));
 735          $group2 = $generator->create_group(array('courseid' => $course->id));
 736          $userin1 = $generator->create_user();
 737          $userin2 = $generator->create_user();
 738          $userinboth = $generator->create_user();
 739          $userinneither = $generator->create_user();
 740          $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
 741          foreach (array($userin1, $userin2, $userinboth, $userinneither) as $user) {
 742              $generator->enrol_user($user->id, $course->id, $studentroleid);
 743          }
 744          groups_add_member($group1, $userin1);
 745          groups_add_member($group2, $userin2);
 746          groups_add_member($group1, $userinboth);
 747          groups_add_member($group2, $userinboth);
 748          $info = new \core_availability\mock_info($course);
 749  
 750          // Tree with single group condition.
 751          $tree = new tree(tree::get_root_json(array(
 752              \availability_group\condition::get_json($group1->id)
 753              )));
 754          list($sql, $params) = $tree->get_user_list_sql(false, $info, false);
 755          $result = $DB->get_fieldset_sql($sql, $params);
 756          sort($result);
 757          $this->assertEquals(array($userin1->id, $userinboth->id), $result);
 758  
 759          // Tree with 'AND' of both group conditions.
 760          $tree = new tree(tree::get_root_json(array(
 761              \availability_group\condition::get_json($group1->id),
 762              \availability_group\condition::get_json($group2->id)
 763          )));
 764          list($sql, $params) = $tree->get_user_list_sql(false, $info, false);
 765          $result = $DB->get_fieldset_sql($sql, $params);
 766          sort($result);
 767          $this->assertEquals(array($userinboth->id), $result);
 768  
 769          // Tree with 'AND' of both group conditions.
 770          $tree = new tree(tree::get_root_json(array(
 771              \availability_group\condition::get_json($group1->id),
 772              \availability_group\condition::get_json($group2->id)
 773          ), tree::OP_OR));
 774          list($sql, $params) = $tree->get_user_list_sql(false, $info, false);
 775          $result = $DB->get_fieldset_sql($sql, $params);
 776          sort($result);
 777          $this->assertEquals(array($userin1->id, $userin2->id, $userinboth->id), $result);
 778  
 779          // Check with flipped logic (NOT above level of tree).
 780          list($sql, $params) = $tree->get_user_list_sql(true, $info, false);
 781          $result = $DB->get_fieldset_sql($sql, $params);
 782          sort($result);
 783          $this->assertEquals(array($userinneither->id), $result);
 784  
 785          // Tree with 'OR' of group conditions and a non-filtering condition.
 786          // The non-filtering condition should mean that ALL users are included.
 787          $tree = new tree(tree::get_root_json(array(
 788              \availability_group\condition::get_json($group1->id),
 789              \availability_date\condition::get_json(\availability_date\condition::DIRECTION_UNTIL, 3)
 790          ), tree::OP_OR));
 791          list($sql, $params) = $tree->get_user_list_sql(false, $info, false);
 792          $this->assertEquals('', $sql);
 793          $this->assertEquals(array(), $params);
 794      }
 795  
 796      /**
 797       * Utility function to build the PHP structure representing a mock condition.
 798       *
 799       * @param array $params Mock parameters
 800       * @return \stdClass Structure object
 801       */
 802      protected static function mock(array $params) {
 803          $params['type'] = 'mock';
 804          return (object)$params;
 805      }
 806  }