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