Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

declare(strict_types=1);

namespace core_reportbuilder\local\report;

use advanced_testcase;
use coding_exception;
use lang_string;
use stdClass;
use core_reportbuilder\local\helpers\database;

/**
 * Unit tests for a report column
 *
 * @package     core_reportbuilder
 * @covers      \core_reportbuilder\local\report\column
 * @copyright   2020 Paul Holden <paulh@moodle.com>
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class column_test extends advanced_testcase {

    /**
     * Test column name getter/setter
     */
    public function test_name(): void {
        $column = $this->create_column('test');
        $this->assertEquals('test', $column->get_name());

        $this->assertEquals('another', $column
            ->set_name('another')
            ->get_name()
        );
    }

    /**
     * Test column title getter/setter
     */
    public function test_title(): void {
        $column = $this->create_column('test', new lang_string('show'));
        $this->assertEquals('Show', $column->get_title());
        $this->assertFalse($column->has_custom_title());

        $this->assertEquals('Hide', $column
            ->set_title(new lang_string('hide'))
            ->get_title()
        );
        $this->assertTrue($column->has_custom_title());

        // Column titles can also be empty.
        $this->assertEmpty($column
            ->set_title(null)
            ->get_title());
    }

    /**
     * Test entity name getter
     */
    public function test_get_entity_name(): void {
        $column = $this->create_column('test', null, 'entityname');
        $this->assertEquals('entityname', $column->get_entity_name());
    }

    /**
     * Test getting unique identifier
     */
    public function test_get_unique_identifier(): void {
        $column = $this->create_column('test', null, 'entityname');
        $this->assertEquals('entityname:test', $column->get_unique_identifier());
    }

    /**
     * Test column type getter/setter
     */
    public function test_type(): void {
        $column = $this->create_column('test');
        $this->assertEquals(column::TYPE_INTEGER, $column
            ->set_type(column::TYPE_INTEGER)
            ->get_type());
    }

    /**
     * Test column default type
     */
    public function test_type_default(): void {
        $column = $this->create_column('test');
        $this->assertEquals(column::TYPE_TEXT, $column->get_type());
    }

    /**
     * Test column type with invalid value
     */
    public function test_type_invalid(): void {
        $column = $this->create_column('test');

        $this->expectException(coding_exception::class);
        $this->expectExceptionMessage('Invalid column type');
        $column->set_type(-1);
    }

    /**
     * Test adding single join
     */
    public function test_add_join(): void {
        $column = $this->create_column('test');
        $this->assertEquals([], $column->get_joins());

        $column->add_join('JOIN {user} u ON u.id = table.userid');
        $this->assertEquals(['JOIN {user} u ON u.id = table.userid'], $column->get_joins());
    }

    /**
     * Test adding multiple joins
     */
    public function test_add_joins(): void {
        $tablejoins = [
            "JOIN {course} c2 ON c2.id = c1.id",
            "JOIN {course} c3 ON c3.id = c1.id",
        ];

        $column = $this->create_column('test')
            ->add_joins($tablejoins);

        $this->assertEquals($tablejoins, $column->get_joins());
    }

    /**
     * Data provider for {@see test_add_field}
     *
     * @return array
     */
    public function add_field_provider(): array {
        return [
            ['foo', '', ['foo AS c1_foo']],
            ['foo', 'bar', ['foo AS c1_bar']],
            ['t.foo', '', ['t.foo AS c1_foo']],
            ['t.foo', 'bar', ['t.foo AS c1_bar']],
        ];
    }

    /**
     * Test adding single field, and retrieving it
     *
     * @param string $sql
     * @param string $alias
     * @param array $expectedselect
     *
     * @dataProvider add_field_provider
     */
    public function test_add_field(string $sql, string $alias, array $expectedselect): void {
        $column = $this->create_column('test')
            ->set_index(1)
            ->add_field($sql, $alias);

        $this->assertEquals($expectedselect, $column->get_fields());
    }

    /**
     * Test adding params to field, and retrieving them
     */
    public function test_add_field_with_params(): void {
        [$param0, $param1] = database::generate_param_names(2);

        $column = $this->create_column('test')
            ->set_index(1)
            ->add_field(":{$param0}", 'foo', [$param0 => 'foo'])
            ->add_field(":{$param1}", 'bar', [$param1 => 'bar']);

        // Select will look like the following: "p<index>_rbparam<counter>", where index is the column index and counter is
        // a static value of the report helper class.
        $fields = $column->get_fields();
        $this->assertCount(2, $fields);

        preg_match('/:(?<paramname>p1_rbparam[\d]+) AS c1_foo/', $fields[0], $matches);
        $this->assertArrayHasKey('paramname', $matches);
        $fieldparam0 = $matches['paramname'];

        preg_match('/:(?<paramname>p1_rbparam[\d]+) AS c1_bar/', $fields[1], $matches);
        $this->assertArrayHasKey('paramname', $matches);
        $fieldparam1 = $matches['paramname'];

        // Ensure column parameters have been renamed appropriately.
        $this->assertEquals([
            $fieldparam0 => 'foo',
            $fieldparam1 => 'bar',
        ], $column->get_params());
    }

    /**
     * Test adding field with alias as part of SQL throws an exception
     */
    public function test_add_field_alias_in_sql(): void {
        $column = $this->create_column('test')
            ->set_index(1);

        $this->expectException(coding_exception::class);
        $this->expectExceptionMessage('Column alias must be passed as a separate argument');
        $column->add_field('foo AS bar');
    }

    /**
     * Test adding field with complex SQL without an alias throws an exception
     */
    public function test_add_field_complex_without_alias(): void {
        global $DB;

        $column = $this->create_column('test')
            ->set_index(1);

        $this->expectException(coding_exception::class);
        $this->expectExceptionMessage('Complex columns must have an alias');
        $column->add_field($DB->sql_concat('foo', 'bar'));
    }

    /**
     * Data provider for {@see test_add_fields}
     *
     * @return array
     */
    public function add_fields_provider(): array {
        return [
            ['t.foo', ['t.foo AS c1_foo']],
            ['t.foo bar', ['t.foo AS c1_bar']],
            ['t.foo AS bar', ['t.foo AS c1_bar']],
            ['t.foo1, t.foo2 bar, t.foo3 AS baz', ['t.foo1 AS c1_foo1', 't.foo2 AS c1_bar', 't.foo3 AS c1_baz']],
        ];
    }

    /**
     * Test adding fields to a column, and retrieving them
     *
     * @param string $sql
     * @param array $expectedselect
     *
     * @dataProvider add_fields_provider
     */
    public function test_add_fields(string $sql, array $expectedselect): void {
        $column = $this->create_column('test')
            ->set_index(1)
            ->add_fields($sql);

        $this->assertEquals($expectedselect, $column->get_fields());
    }

    /**
     * Test column alias
     */
    public function test_column_alias(): void {
        $column = $this->create_column('test')
            ->set_index(1)
            ->add_fields('t.foo, t.bar');

        $this->assertEquals('c1_foo', $column->get_column_alias());
    }

    /**
     * Test column alias with a field containing an alias
     */
    public function test_column_alias_with_field_alias(): void {
        $column = $this->create_column('test')
            ->set_index(1)
            ->add_field('COALESCE(t.foo, t.bar)', 'lionel');

        $this->assertEquals('c1_lionel', $column->get_column_alias());
    }

    /**
     * Test alias of column without any fields throws exception
     */
    public function test_column_alias_no_fields(): void {
        $column = $this->create_column('test');

        $this->expectException(coding_exception::class);
        $this->expectExceptionMessage('Column ' . $column->get_unique_identifier() . ' contains no fields');
        $column->add_field($column->get_column_alias());
    }

    /**
     * Test setting column group by SQL
     */
    public function test_set_groupby_sql(): void {
        $column = $this->create_column('test')
            ->set_index(1)
            ->add_field('COALESCE(t.foo, t.bar)', 'lionel')
            ->set_groupby_sql('t.id');

        $this->assertEquals(['t.id'], $column->get_groupby_sql());
    }

    /**
     * Test getting default column group by SQL
     */
    public function test_get_groupby_sql(): void {
        global $DB;

        $column = $this->create_column('test')
            ->set_index(1)
            ->add_fields('t.foo, t.bar');

        // The behaviour of this method differs due to DB limitations.
        $usealias = in_array($DB->get_dbfamily(), ['mysql', 'postgres']);
        if ($usealias) {
            $expected = ['c1_foo', 'c1_bar'];
        } else {
            $expected = ['t.foo', 't.bar'];
        }

        $this->assertEquals($expected, $column->get_groupby_sql());
    }

    /**
     * Data provider for {@see test_get_default_value} and {@see test_format_value}
     *
     * @return array[]
     */
    public function column_type_provider(): array {
        return [
            [column::TYPE_INTEGER, 42],
            [column::TYPE_TEXT, 'Hello'],
            [column::TYPE_TIMESTAMP, HOURSECS],
            [column::TYPE_BOOLEAN, 1, true],
            [column::TYPE_FLOAT, 1.23],
            [column::TYPE_LONGTEXT, 'Amigos'],
        ];
    }

    /**
     * Test default value is returned from selected values, with correct type
     *
     * @param int $columntype
     * @param mixed $value
     * @param mixed|null $expected Expected value, or null to indicate it should be identical to value
     *
     * @dataProvider column_type_provider
     */
    public function test_get_default_value(int $columntype, $value, $expected = null): void {
        $defaultvalue = column::get_default_value([
            'value' => $value,
            'foo' => 'bar',
        ], $columntype);

        $this->assertSame($expected ?? $value, $defaultvalue);
    }

    /**
     * Test that column value is returned correctly, with correct type
     *
     * @param int $columntype
     * @param mixed $value
     * @param mixed|null $expected Expected value, or null to indicate it should be identical to value
     *
     * @dataProvider column_type_provider
     */
    public function test_format_value(int $columntype, $value, $expected = null): void {
        $column = $this->create_column('test')
            ->set_index(1)
            ->set_type($columntype)
            ->add_field('t.foo');

        $this->assertSame($expected ?? $value, $column->format_value([
            'c1_foo' => $value,
        ]));
    }

    /**
     * Test that column value with callback is returned
     */
    public function test_format_value_callback(): void {
        $column = $this->create_column('test')
            ->set_index(1)
            ->add_field('t.foo')
            ->set_type(column::TYPE_INTEGER)
            ->add_callback(static function(int $value, stdClass $values) {
                return $value * 2;
            });

        $this->assertEquals(84, $column->format_value([
            'c1_bar' => 10,
            'c1_foo' => 42,
        ]));
    }

    /**
     * Test that column value with callback (using all fields) is returned
     */
    public function test_format_value_callback_fields(): void {
        $column = $this->create_column('test')
            ->set_index(1)
            ->add_fields('t.foo, t.baz')
            ->set_type(column::TYPE_INTEGER)
            ->add_callback(static function(int $value, stdClass $values) {
                return $values->foo + $values->baz;
            });

        $this->assertEquals(60, $column->format_value([
            'c1_bar' => 10,
            'c1_foo' => 42,
            'c1_baz' => 18,
        ]));
    }

    /**
     * Test that column value with callback (using arguments) is returned
     */
    public function test_format_value_callback_arguments(): void {
        $column = $this->create_column('test')
            ->set_index(1)
            ->add_field('t.foo')
            ->set_type(column::TYPE_INTEGER)
            ->add_callback(static function(int $value, stdClass $values, int $argument) {
                return $value - $argument;
            }, 10);

        $this->assertEquals(32, $column->format_value([
            'c1_bar' => 10,
            'c1_foo' => 42,
        ]));
    }

    /**
> * Test that column value with callback (where aggregation is not set) is returned * Test adding multiple callbacks to a column > */ */ > public function test_format_value_callback_aggregation(): void { public function test_add_multiple_callback(): void { > $column = $this->create_column('test') $column = $this->create_column('test') > ->set_index(1) ->set_index(1) > ->add_field('t.foo') ->add_field('t.foo') > ->set_type(column::TYPE_INTEGER) ->set_type(column::TYPE_TEXT) > ->add_callback(static function(int $value, stdClass $values, $argument, ?string $aggregation): string { ->add_callback(static function(string $value): string { > // Simple callback to return the given value, and append type of aggregation parameter. return strrev($value); > return "{$value} " . gettype($aggregation); }) > }); ->add_callback(static function(string $value): string { > return strtoupper($value); > $this->assertEquals("42 NULL", $column->format_value(['c1_foo' => 42])); }); > } > $this->assertEquals('LIONEL', $column->format_value([ > /**
'c1_foo' => 'lenoil', ])); } /** * Test that setting column callback overwrites previous callbacks */ public function test_set_callback(): void { $column = $this->create_column('test') ->set_index(1) ->add_field('t.foo') ->set_type(column::TYPE_TEXT) ->add_callback(static function(string $value): string { return strrev($value); }) ->set_callback(static function(string $value): string { return strtoupper($value); }); $this->assertEquals('LENOIL', $column->format_value([ 'c1_foo' => 'lenoil', ])); } /** * Test is sortable */ public function test_is_sortable(): void { $column = $this->create_column('test'); $this->assertFalse($column->get_is_sortable()); $column->set_is_sortable(true); $this->assertTrue($column->get_is_sortable()); } /** * Test retrieving sort fields */ public function test_get_sortfields(): void { $column = $this->create_column('test') ->set_index(1) ->add_fields('t.foo, t.bar, t.baz') ->set_is_sortable(true, ['t.baz', 't.bar']); $this->assertEquals(['c1_baz', 'c1_bar'], $column->get_sort_fields()); } /** * Test retrieving sort fields when an aliased field is set as sortable */ public function test_get_sortfields_with_field_alias(): void { $column = $this->create_column('test') ->set_index(1) ->add_field('t.foo') ->add_field('COALESCE(t.foo, t.bar)', 'lionel') ->set_is_sortable(true, ['lionel']); $this->assertEquals(['c1_lionel'], $column->get_sort_fields()); } /** * Test retrieving sort fields when an unknown field is set as sortable */ public function test_get_sortfields_unknown_field(): void { $column = $this->create_column('test') ->set_index(1) ->add_fields('t.foo') ->set_is_sortable(true, ['t.baz']); $this->assertEquals(['t.baz'], $column->get_sort_fields()); } /** * Test is available */ public function test_is_available(): void { $column = $this->create_column('test'); $this->assertTrue($column->get_is_available()); $column->set_is_available(true); $this->assertTrue($column->get_is_available()); } /** * Helper method to create a column instance * * @param string $name * @param lang_string|null $title * @param string $entityname * @return column */ private function create_column(string $name, ?lang_string $title = null, string $entityname = 'column_testcase'): column { return new column($name, $title, $entityname); } }