See Release Notes
Long Term Support Release
Differences Between: [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]
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 declare(strict_types=1); 18 19 namespace core_reportbuilder\local\report; 20 21 use advanced_testcase; 22 use coding_exception; 23 use lang_string; 24 use stdClass; 25 use core_reportbuilder\local\helpers\database; 26 27 /** 28 * Unit tests for a report column 29 * 30 * @package core_reportbuilder 31 * @covers \core_reportbuilder\local\report\column 32 * @copyright 2020 Paul Holden <paulh@moodle.com> 33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 34 */ 35 class column_test extends advanced_testcase { 36 37 /** 38 * Test column name getter/setter 39 */ 40 public function test_name(): void { 41 $column = $this->create_column('test'); 42 $this->assertEquals('test', $column->get_name()); 43 44 $this->assertEquals('another', $column 45 ->set_name('another') 46 ->get_name() 47 ); 48 } 49 50 /** 51 * Test column title getter/setter 52 */ 53 public function test_title(): void { 54 $column = $this->create_column('test', new lang_string('show')); 55 $this->assertEquals('Show', $column->get_title()); 56 $this->assertFalse($column->has_custom_title()); 57 58 $this->assertEquals('Hide', $column 59 ->set_title(new lang_string('hide')) 60 ->get_title() 61 ); 62 $this->assertTrue($column->has_custom_title()); 63 64 // Column titles can also be empty. 65 $this->assertEmpty($column 66 ->set_title(null) 67 ->get_title()); 68 } 69 70 /** 71 * Test entity name getter 72 */ 73 public function test_get_entity_name(): void { 74 $column = $this->create_column('test', null, 'entityname'); 75 $this->assertEquals('entityname', $column->get_entity_name()); 76 } 77 78 /** 79 * Test getting unique identifier 80 */ 81 public function test_get_unique_identifier(): void { 82 $column = $this->create_column('test', null, 'entityname'); 83 $this->assertEquals('entityname:test', $column->get_unique_identifier()); 84 } 85 86 /** 87 * Test column type getter/setter 88 */ 89 public function test_type(): void { 90 $column = $this->create_column('test'); 91 $this->assertEquals(column::TYPE_INTEGER, $column 92 ->set_type(column::TYPE_INTEGER) 93 ->get_type()); 94 } 95 96 /** 97 * Test column default type 98 */ 99 public function test_type_default(): void { 100 $column = $this->create_column('test'); 101 $this->assertEquals(column::TYPE_TEXT, $column->get_type()); 102 } 103 104 /** 105 * Test column type with invalid value 106 */ 107 public function test_type_invalid(): void { 108 $column = $this->create_column('test'); 109 110 $this->expectException(coding_exception::class); 111 $this->expectExceptionMessage('Invalid column type'); 112 $column->set_type(-1); 113 } 114 115 /** 116 * Test adding single join 117 */ 118 public function test_add_join(): void { 119 $column = $this->create_column('test'); 120 $this->assertEquals([], $column->get_joins()); 121 122 $column->add_join('JOIN {user} u ON u.id = table.userid'); 123 $this->assertEquals(['JOIN {user} u ON u.id = table.userid'], $column->get_joins()); 124 } 125 126 /** 127 * Test adding multiple joins 128 */ 129 public function test_add_joins(): void { 130 $tablejoins = [ 131 "JOIN {course} c2 ON c2.id = c1.id", 132 "JOIN {course} c3 ON c3.id = c1.id", 133 ]; 134 135 $column = $this->create_column('test') 136 ->add_joins($tablejoins); 137 138 $this->assertEquals($tablejoins, $column->get_joins()); 139 } 140 141 /** 142 * Data provider for {@see test_add_field} 143 * 144 * @return array 145 */ 146 public function add_field_provider(): array { 147 return [ 148 ['foo', '', ['foo AS c1_foo']], 149 ['foo', 'bar', ['foo AS c1_bar']], 150 ['t.foo', '', ['t.foo AS c1_foo']], 151 ['t.foo', 'bar', ['t.foo AS c1_bar']], 152 ]; 153 } 154 155 /** 156 * Test adding single field, and retrieving it 157 * 158 * @param string $sql 159 * @param string $alias 160 * @param array $expectedselect 161 * 162 * @dataProvider add_field_provider 163 */ 164 public function test_add_field(string $sql, string $alias, array $expectedselect): void { 165 $column = $this->create_column('test') 166 ->set_index(1) 167 ->add_field($sql, $alias); 168 169 $this->assertEquals($expectedselect, $column->get_fields()); 170 } 171 172 /** 173 * Test adding params to field, and retrieving them 174 */ 175 public function test_add_field_with_params(): void { 176 [$param0, $param1] = database::generate_param_names(2); 177 178 $column = $this->create_column('test') 179 ->set_index(1) 180 ->add_field(":{$param0}", 'foo', [$param0 => 'foo']) 181 ->add_field(":{$param1}", 'bar', [$param1 => 'bar']); 182 183 // Select will look like the following: "p<index>_rbparam<counter>", where index is the column index and counter is 184 // a static value of the report helper class. 185 $fields = $column->get_fields(); 186 $this->assertCount(2, $fields); 187 188 preg_match('/:(?<paramname>p1_rbparam[\d]+) AS c1_foo/', $fields[0], $matches); 189 $this->assertArrayHasKey('paramname', $matches); 190 $fieldparam0 = $matches['paramname']; 191 192 preg_match('/:(?<paramname>p1_rbparam[\d]+) AS c1_bar/', $fields[1], $matches); 193 $this->assertArrayHasKey('paramname', $matches); 194 $fieldparam1 = $matches['paramname']; 195 196 // Ensure column parameters have been renamed appropriately. 197 $this->assertEquals([ 198 $fieldparam0 => 'foo', 199 $fieldparam1 => 'bar', 200 ], $column->get_params()); 201 } 202 203 /** 204 * Test adding field with alias as part of SQL throws an exception 205 */ 206 public function test_add_field_alias_in_sql(): void { 207 $column = $this->create_column('test') 208 ->set_index(1); 209 210 $this->expectException(coding_exception::class); 211 $this->expectExceptionMessage('Column alias must be passed as a separate argument'); 212 $column->add_field('foo AS bar'); 213 } 214 215 /** 216 * Test adding field with complex SQL without an alias throws an exception 217 */ 218 public function test_add_field_complex_without_alias(): void { 219 global $DB; 220 221 $column = $this->create_column('test') 222 ->set_index(1); 223 224 $this->expectException(coding_exception::class); 225 $this->expectExceptionMessage('Complex columns must have an alias'); 226 $column->add_field($DB->sql_concat('foo', 'bar')); 227 } 228 229 /** 230 * Data provider for {@see test_add_fields} 231 * 232 * @return array 233 */ 234 public function add_fields_provider(): array { 235 return [ 236 ['t.foo', ['t.foo AS c1_foo']], 237 ['t.foo bar', ['t.foo AS c1_bar']], 238 ['t.foo AS bar', ['t.foo AS c1_bar']], 239 ['t.foo1, t.foo2 bar, t.foo3 AS baz', ['t.foo1 AS c1_foo1', 't.foo2 AS c1_bar', 't.foo3 AS c1_baz']], 240 ]; 241 } 242 243 /** 244 * Test adding fields to a column, and retrieving them 245 * 246 * @param string $sql 247 * @param array $expectedselect 248 * 249 * @dataProvider add_fields_provider 250 */ 251 public function test_add_fields(string $sql, array $expectedselect): void { 252 $column = $this->create_column('test') 253 ->set_index(1) 254 ->add_fields($sql); 255 256 $this->assertEquals($expectedselect, $column->get_fields()); 257 } 258 259 /** 260 * Test column alias 261 */ 262 public function test_column_alias(): void { 263 $column = $this->create_column('test') 264 ->set_index(1) 265 ->add_fields('t.foo, t.bar'); 266 267 $this->assertEquals('c1_foo', $column->get_column_alias()); 268 } 269 270 /** 271 * Test column alias with a field containing an alias 272 */ 273 public function test_column_alias_with_field_alias(): void { 274 $column = $this->create_column('test') 275 ->set_index(1) 276 ->add_field('COALESCE(t.foo, t.bar)', 'lionel'); 277 278 $this->assertEquals('c1_lionel', $column->get_column_alias()); 279 } 280 281 /** 282 * Test alias of column without any fields throws exception 283 */ 284 public function test_column_alias_no_fields(): void { 285 $column = $this->create_column('test'); 286 287 $this->expectException(coding_exception::class); 288 $this->expectExceptionMessage('Column ' . $column->get_unique_identifier() . ' contains no fields'); 289 $column->add_field($column->get_column_alias()); 290 } 291 292 /** 293 * Test setting column group by SQL 294 */ 295 public function test_set_groupby_sql(): void { 296 $column = $this->create_column('test') 297 ->set_index(1) 298 ->add_field('COALESCE(t.foo, t.bar)', 'lionel') 299 ->set_groupby_sql('t.id'); 300 301 $this->assertEquals(['t.id'], $column->get_groupby_sql()); 302 } 303 304 /** 305 * Test getting default column group by SQL 306 */ 307 public function test_get_groupby_sql(): void { 308 global $DB; 309 310 $column = $this->create_column('test') 311 ->set_index(1) 312 ->add_fields('t.foo, t.bar'); 313 314 // The behaviour of this method differs due to DB limitations. 315 $usealias = in_array($DB->get_dbfamily(), ['mysql', 'postgres']); 316 if ($usealias) { 317 $expected = ['c1_foo', 'c1_bar']; 318 } else { 319 $expected = ['t.foo', 't.bar']; 320 } 321 322 $this->assertEquals($expected, $column->get_groupby_sql()); 323 } 324 325 /** 326 * Data provider for {@see test_get_default_value} and {@see test_format_value} 327 * 328 * @return array[] 329 */ 330 public function column_type_provider(): array { 331 return [ 332 [column::TYPE_INTEGER, 42], 333 [column::TYPE_TEXT, 'Hello'], 334 [column::TYPE_TIMESTAMP, HOURSECS], 335 [column::TYPE_BOOLEAN, 1, true], 336 [column::TYPE_FLOAT, 1.23], 337 [column::TYPE_LONGTEXT, 'Amigos'], 338 ]; 339 } 340 341 /** 342 * Test default value is returned from selected values, with correct type 343 * 344 * @param int $columntype 345 * @param mixed $value 346 * @param mixed|null $expected Expected value, or null to indicate it should be identical to value 347 * 348 * @dataProvider column_type_provider 349 */ 350 public function test_get_default_value(int $columntype, $value, $expected = null): void { 351 $defaultvalue = column::get_default_value([ 352 'value' => $value, 353 'foo' => 'bar', 354 ], $columntype); 355 356 $this->assertSame($expected ?? $value, $defaultvalue); 357 } 358 359 /** 360 * Test that column value is returned correctly, with correct type 361 * 362 * @param int $columntype 363 * @param mixed $value 364 * @param mixed|null $expected Expected value, or null to indicate it should be identical to value 365 * 366 * @dataProvider column_type_provider 367 */ 368 public function test_format_value(int $columntype, $value, $expected = null): void { 369 $column = $this->create_column('test') 370 ->set_index(1) 371 ->set_type($columntype) 372 ->add_field('t.foo'); 373 374 $this->assertSame($expected ?? $value, $column->format_value([ 375 'c1_foo' => $value, 376 ])); 377 } 378 379 /** 380 * Test that column value with callback is returned 381 */ 382 public function test_format_value_callback(): void { 383 $column = $this->create_column('test') 384 ->set_index(1) 385 ->add_field('t.foo') 386 ->set_type(column::TYPE_INTEGER) 387 ->add_callback(static function(int $value, stdClass $values) { 388 return $value * 2; 389 }); 390 391 $this->assertEquals(84, $column->format_value([ 392 'c1_bar' => 10, 393 'c1_foo' => 42, 394 ])); 395 } 396 397 /** 398 * Test that column value with callback (using all fields) is returned 399 */ 400 public function test_format_value_callback_fields(): void { 401 $column = $this->create_column('test') 402 ->set_index(1) 403 ->add_fields('t.foo, t.baz') 404 ->set_type(column::TYPE_INTEGER) 405 ->add_callback(static function(int $value, stdClass $values) { 406 return $values->foo + $values->baz; 407 }); 408 409 $this->assertEquals(60, $column->format_value([ 410 'c1_bar' => 10, 411 'c1_foo' => 42, 412 'c1_baz' => 18, 413 ])); 414 } 415 416 /** 417 * Test that column value with callback (using arguments) is returned 418 */ 419 public function test_format_value_callback_arguments(): void { 420 $column = $this->create_column('test') 421 ->set_index(1) 422 ->add_field('t.foo') 423 ->set_type(column::TYPE_INTEGER) 424 ->add_callback(static function(int $value, stdClass $values, int $argument) { 425 return $value - $argument; 426 }, 10); 427 428 $this->assertEquals(32, $column->format_value([ 429 'c1_bar' => 10, 430 'c1_foo' => 42, 431 ])); 432 } 433 434 /** 435 * Test adding multiple callbacks to a column 436 */ 437 public function test_add_multiple_callback(): void { 438 $column = $this->create_column('test') 439 ->set_index(1) 440 ->add_field('t.foo') 441 ->set_type(column::TYPE_TEXT) 442 ->add_callback(static function(string $value): string { 443 return strrev($value); 444 }) 445 ->add_callback(static function(string $value): string { 446 return strtoupper($value); 447 }); 448 449 $this->assertEquals('LIONEL', $column->format_value([ 450 'c1_foo' => 'lenoil', 451 ])); 452 } 453 454 /** 455 * Test that setting column callback overwrites previous callbacks 456 */ 457 public function test_set_callback(): void { 458 $column = $this->create_column('test') 459 ->set_index(1) 460 ->add_field('t.foo') 461 ->set_type(column::TYPE_TEXT) 462 ->add_callback(static function(string $value): string { 463 return strrev($value); 464 }) 465 ->set_callback(static function(string $value): string { 466 return strtoupper($value); 467 }); 468 469 $this->assertEquals('LENOIL', $column->format_value([ 470 'c1_foo' => 'lenoil', 471 ])); 472 } 473 474 /** 475 * Test is sortable 476 */ 477 public function test_is_sortable(): void { 478 $column = $this->create_column('test'); 479 $this->assertFalse($column->get_is_sortable()); 480 481 $column->set_is_sortable(true); 482 $this->assertTrue($column->get_is_sortable()); 483 } 484 485 /** 486 * Test retrieving sort fields 487 */ 488 public function test_get_sortfields(): void { 489 $column = $this->create_column('test') 490 ->set_index(1) 491 ->add_fields('t.foo, t.bar, t.baz') 492 ->set_is_sortable(true, ['t.baz', 't.bar']); 493 494 $this->assertEquals(['c1_baz', 'c1_bar'], $column->get_sort_fields()); 495 } 496 497 /** 498 * Test retrieving sort fields when an aliased field is set as sortable 499 */ 500 public function test_get_sortfields_with_field_alias(): void { 501 $column = $this->create_column('test') 502 ->set_index(1) 503 ->add_field('t.foo') 504 ->add_field('COALESCE(t.foo, t.bar)', 'lionel') 505 ->set_is_sortable(true, ['lionel']); 506 507 $this->assertEquals(['c1_lionel'], $column->get_sort_fields()); 508 } 509 510 /** 511 * Test retrieving sort fields when an unknown field is set as sortable 512 */ 513 public function test_get_sortfields_unknown_field(): void { 514 $column = $this->create_column('test') 515 ->set_index(1) 516 ->add_fields('t.foo') 517 ->set_is_sortable(true, ['t.baz']); 518 519 $this->assertEquals(['t.baz'], $column->get_sort_fields()); 520 } 521 522 /** 523 * Test is available 524 */ 525 public function test_is_available(): void { 526 $column = $this->create_column('test'); 527 $this->assertTrue($column->get_is_available()); 528 529 $column->set_is_available(true); 530 $this->assertTrue($column->get_is_available()); 531 } 532 533 /** 534 * Helper method to create a column instance 535 * 536 * @param string $name 537 * @param lang_string|null $title 538 * @param string $entityname 539 * @return column 540 */ 541 private function create_column(string $name, ?lang_string $title = null, string $entityname = 'column_testcase'): column { 542 return new column($name, $title, $entityname); 543 } 544 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body