moodle/reportbuilder/tests/local/report/column_test.php
Paul Holden 7c3b8c0a40
MDL-82068 reportbuilder: re-factor SQL JOIN collection to trait.
Replace identical implementation of the same in various classes with
usage of the new trait. Implement constructor property promotion in
modified classes at the same time, to help clean up/clarify class
properties.
2024-06-05 13:36:22 +01:00

534 lines
17 KiB
PHP

<?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
*/
final 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);
}
/**
* Data provider for {@see test_add_field}
*
* @return array
*/
public static 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 static 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 static 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
*/
public function test_format_value_callback_aggregation(): 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, $argument, ?string $aggregation): string {
// Simple callback to return the given value, and append type of aggregation parameter.
return "{$value} " . gettype($aggregation);
});
$this->assertEquals("42 NULL", $column->format_value(['c1_foo' => 42]));
}
/**
* Test adding multiple callbacks to a column
*/
public function test_add_multiple_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);
})
->add_callback(static function(string $value): string {
return strtoupper($value);
});
$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);
}
}