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