Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 9 May 2022 (12 months).
  • Bug fixes for security issues in 3.11.x will end 14 November 2022 (18 months).
  • 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 35 and 311] [Versions 36 and 311] [Versions 37 and 311] [Versions 38 and 311] [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  /**
      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->assertMatchesRegularExpression('~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->assertMatchesRegularExpression('~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->assertMatchesRegularExpression('~!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->assertMatchesRegularExpression('~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->assertMatchesRegularExpression('~<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->assertMatchesRegularExpression('~<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->assertMatchesRegularExpression('~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->assertMatchesRegularExpression('~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->assertMatchesRegularExpression('~Not available unless:.*<ul>~',
     451                  $renderer->render($tree->get_full_information($info)));
     452  
     453          // Hidden markers on items.
     454          $this->assertMatchesRegularExpression('~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->assertMatchesRegularExpression('~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->assertMatchesRegularExpression('~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->assertMatchesRegularExpression('~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->assertMatchesRegularExpression('~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->assertMatchesRegularExpression('~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->assertMatchesRegularExpression('~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  }