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.

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  }