diff --git a/admin/tests/reportbuilder/datasource/task_logs_test.php b/admin/tests/reportbuilder/datasource/task_logs_test.php index 0c77fea4c34..0c5e20f8336 100644 --- a/admin/tests/reportbuilder/datasource/task_logs_test.php +++ b/admin/tests/reportbuilder/datasource/task_logs_test.php @@ -221,6 +221,19 @@ class task_logs_test extends core_reportbuilder_testcase { } } + /** + * Stress test datasource + */ + public function test_stress_datasource(): void { + $this->resetAfterTest(); + + $this->generate_task_log_data(true, 3, 2, 1654038000, 1654038060, 'hi', 'core_reportbuilder', 'test', 43); + + $this->datasource_stress_test_columns(task_logs::class); + $this->datasource_stress_test_columns_aggregation(task_logs::class); + $this->datasource_stress_test_conditions(task_logs::class, 'task_log:name'); + } + /** * Helper to generate some task logs data * diff --git a/badges/classes/reportbuilder/local/entities/badge.php b/badges/classes/reportbuilder/local/entities/badge.php index 5fceb3980bc..339d5404358 100644 --- a/badges/classes/reportbuilder/local/entities/badge.php +++ b/badges/classes/reportbuilder/local/entities/badge.php @@ -92,6 +92,8 @@ class badge extends base { * @return column[] */ protected function get_all_columns(): array { + global $DB; + $badgealias = $this->get_table_alias('badge'); $contextalias = $this->get_table_alias('context'); @@ -107,6 +109,10 @@ class badge extends base { ->set_is_sortable(true); // Description (note, this column contains plaintext so requires no post-processing). + $descriptionfieldsql = "{$badgealias}.description"; + if ($DB->get_dbfamily() === 'oracle') { + $descriptionfieldsql = $DB->sql_order_by_text($descriptionfieldsql, 1024); + } $columns[] = (new column( 'description', new lang_string('description', 'core_badges'), @@ -114,7 +120,7 @@ class badge extends base { )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_LONGTEXT) - ->add_field("{$badgealias}.description"); + ->add_field($descriptionfieldsql, 'description'); // Criteria. $columns[] = (new column( @@ -146,7 +152,8 @@ class badge extends base { ON {$contextalias}.contextlevel = " . CONTEXT_COURSE . " AND {$contextalias}.instanceid = {$badgealias}.courseid") ->set_type(column::TYPE_INTEGER) - ->add_fields("{$badgealias}.id, {$badgealias}.type, {$badgealias}.courseid, {$badgealias}.imagecaption") + ->add_fields("{$badgealias}.id, {$badgealias}.type, {$badgealias}.courseid") + ->add_field($DB->sql_cast_to_char("{$badgealias}.imagecaption"), 'imagecaption') ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->set_disabled_aggregation_all() ->add_callback(static function(int $badgeid, stdClass $badge): string { diff --git a/badges/tests/reportbuilder/datasource/badges_test.php b/badges/tests/reportbuilder/datasource/badges_test.php index 7687a8ca7cf..1353015c47d 100644 --- a/badges/tests/reportbuilder/datasource/badges_test.php +++ b/badges/tests/reportbuilder/datasource/badges_test.php @@ -94,4 +94,23 @@ class badges_test extends core_reportbuilder_testcase { return array_values($row); }, $content)); } + + /** + * Stress test datasource + */ + public function test_stress_datasource(): void { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $user = $this->getDataGenerator()->create_and_enrol($course); + + /** @var core_badges_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_badges'); + $badge = $generator->create_badge(['name' => 'Course badge', 'type' => BADGE_TYPE_COURSE, 'courseid' => $course->id]); + $badge->issue($user->id, true); + + $this->datasource_stress_test_columns(badges::class); + $this->datasource_stress_test_columns_aggregation(badges::class); + $this->datasource_stress_test_conditions(badges::class, 'badge:name'); + } } diff --git a/cohort/classes/local/entities/cohort.php b/cohort/classes/local/entities/cohort.php index df8f11a8cb5..a086ca81b03 100644 --- a/cohort/classes/local/entities/cohort.php +++ b/cohort/classes/local/entities/cohort.php @@ -88,6 +88,8 @@ class cohort extends base { * @return column[] */ protected function get_all_columns(): array { + global $DB; + $tablealias = $this->get_table_alias('cohort'); $contextalias = $this->get_table_alias('context'); @@ -130,6 +132,10 @@ class cohort extends base { ->set_is_sortable(true); // Description column. + $descriptionfieldsql = "{$tablealias}.description"; + if ($DB->get_dbfamily() === 'oracle') { + $descriptionfieldsql = $DB->sql_order_by_text($descriptionfieldsql, 1024); + } $columns[] = (new column( 'description', new lang_string('description'), @@ -138,7 +144,8 @@ class cohort extends base { ->add_joins($this->get_joins()) ->add_join("JOIN {context} {$contextalias} ON {$contextalias}.id = {$tablealias}.contextid") ->set_type(column::TYPE_LONGTEXT) - ->add_fields("{$tablealias}.description, {$tablealias}.descriptionformat, {$tablealias}.id, {$tablealias}.contextid") + ->add_field($descriptionfieldsql, 'description') + ->add_fields("{$tablealias}.descriptionformat, {$tablealias}.id, {$tablealias}.contextid") ->add_fields(context_helper::get_preload_record_columns_sql($contextalias)) ->add_callback(static function(?string $description, stdClass $cohort): string { global $CFG; diff --git a/cohort/tests/datasource_test.php b/cohort/tests/reportbuilder/datasource/cohorts_test.php similarity index 89% rename from cohort/tests/datasource_test.php rename to cohort/tests/reportbuilder/datasource/cohorts_test.php index 0d47358b27f..777d874f696 100644 --- a/cohort/tests/datasource_test.php +++ b/cohort/tests/reportbuilder/datasource/cohorts_test.php @@ -37,7 +37,7 @@ require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); * @copyright 2021 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class datasource_test extends core_reportbuilder_testcase { +class cohorts_test extends core_reportbuilder_testcase { /** * Test cohorts datasource @@ -132,4 +132,19 @@ class datasource_test extends core_reportbuilder_testcase { $contentrow = array_values(reset($content)); $this->assertEquals([$expectedcohort, $username], $contentrow); } + + /** + * Stress test datasource + */ + public function test_stress_datasource(): void { + $this->resetAfterTest(); + + $cohort = $this->getDataGenerator()->create_cohort(); + $user = $this->getDataGenerator()->create_user(); + cohort_add_member($cohort->id, $user->id); + + $this->datasource_stress_test_columns(cohorts::class); + $this->datasource_stress_test_columns_aggregation(cohorts::class); + $this->datasource_stress_test_conditions(cohorts::class, 'cohort:name'); + } } diff --git a/course/classes/local/entities/course_category.php b/course/classes/local/entities/course_category.php index 53aa1452a37..4794cd2f154 100644 --- a/course/classes/local/entities/course_category.php +++ b/course/classes/local/entities/course_category.php @@ -88,6 +88,8 @@ class course_category extends base { * @return column[] */ protected function get_all_columns(): array { + global $DB; + $tablealias = $this->get_table_alias('course_categories'); $tablealiascontext = $this->get_table_alias('context'); @@ -157,6 +159,10 @@ class course_category extends base { ->set_is_sortable(true); // Description column (note we need to join/select from the context table in order to format the column). + $descriptionfieldsql = "{$tablealias}.description"; + if ($DB->get_dbfamily() === 'oracle') { + $descriptionfieldsql = $DB->sql_order_by_text($descriptionfieldsql, 1024); + } $columns[] = (new column( 'description', new lang_string('description'), @@ -165,7 +171,8 @@ class course_category extends base { ->add_joins($this->get_joins()) ->add_join($this->get_context_join()) ->set_type(column::TYPE_LONGTEXT) - ->add_fields("{$tablealias}.description, {$tablealias}.descriptionformat, {$tablealias}.id") + ->add_field($descriptionfieldsql, 'description') + ->add_fields("{$tablealias}.descriptionformat, {$tablealias}.id") ->add_fields(context_helper::get_preload_record_columns_sql($tablealiascontext)) ->add_callback(static function(?string $description, stdClass $category): string { global $CFG; diff --git a/course/tests/reportbuilder/datasource/courses_test.php b/course/tests/reportbuilder/datasource/courses_test.php index 77c728f5fc4..7cae62a4807 100644 --- a/course/tests/reportbuilder/datasource/courses_test.php +++ b/course/tests/reportbuilder/datasource/courses_test.php @@ -18,6 +18,7 @@ declare(strict_types=1); namespace core_course\reportbuilder\datasource; +use core_customfield_generator; use core_reportbuilder_testcase; use core_reportbuilder_generator; use core_reportbuilder\local\filters\tags; @@ -192,4 +193,23 @@ class courses_test extends core_reportbuilder_testcase { $this->assertEmpty($content); } } + + /** + * Stress test datasource + */ + public function test_stress_datasource(): void { + $this->resetAfterTest(); + + /** @var core_customfield_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_customfield'); + $customfieldcategory = $generator->create_category(); + $generator->create_field(['categoryid' => $customfieldcategory->get('id'), 'shortname' => 'hi']); + + $category = $this->getDataGenerator()->create_category(); + $course = $this->getDataGenerator()->create_course(['category' => $category->id, 'customfield_hi' => 'Hello']); + + $this->datasource_stress_test_columns(courses::class); + $this->datasource_stress_test_columns_aggregation(courses::class); + $this->datasource_stress_test_conditions(courses::class, 'course:idnumber'); + } } diff --git a/course/tests/reportbuilder/datasource/participants_test.php b/course/tests/reportbuilder/datasource/participants_test.php index 5bc5bc3e545..e059d376493 100644 --- a/course/tests/reportbuilder/datasource/participants_test.php +++ b/course/tests/reportbuilder/datasource/participants_test.php @@ -277,4 +277,18 @@ class participants_test extends core_reportbuilder_testcase { $this->assertCount(1, $content); $this->assertEquals($expected, $content[0]['c0_firstname']); } + + /** + * Stress test datasource + */ + public function test_stress_datasource(): void { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $this->getDataGenerator()->create_and_enrol($course); + + $this->datasource_stress_test_columns(participants::class); + $this->datasource_stress_test_columns_aggregation(participants::class); + $this->datasource_stress_test_conditions(participants::class, 'course:idnumber'); + } } diff --git a/reportbuilder/classes/local/entities/course.php b/reportbuilder/classes/local/entities/course.php index 3a01a16b436..aabadc09362 100644 --- a/reportbuilder/classes/local/entities/course.php +++ b/reportbuilder/classes/local/entities/course.php @@ -223,7 +223,8 @@ class course extends base { * @return column[] */ protected function get_all_columns(): array { - $columns = []; + global $DB; + $coursefields = $this->get_course_fields(); $tablealias = $this->get_table_alias('course'); $contexttablealias = $this->get_table_alias('context'); @@ -269,14 +270,21 @@ class course extends base { } foreach ($coursefields as $coursefield => $coursefieldlang) { + $columntype = $this->get_course_field_type($coursefield); + + $columnfieldsql = "{$tablealias}.{$coursefield}"; + if ($columntype === column::TYPE_LONGTEXT && $DB->get_dbfamily() === 'oracle') { + $columnfieldsql = $DB->sql_order_by_text($columnfieldsql, 1024); + } + $column = (new column( $coursefield, $coursefieldlang, $this->get_entity_name() )) ->add_joins($this->get_joins()) - ->set_type($this->get_course_field_type($coursefield)) - ->add_field("$tablealias.$coursefield") + ->set_type($columntype) + ->add_field($columnfieldsql, $coursefield) ->add_callback([$this, 'format'], $coursefield) ->set_is_sortable($this->is_sortable($coursefield)); diff --git a/reportbuilder/classes/local/entities/user.php b/reportbuilder/classes/local/entities/user.php index fa9e371f4f1..119f099d21f 100644 --- a/reportbuilder/classes/local/entities/user.php +++ b/reportbuilder/classes/local/entities/user.php @@ -164,6 +164,8 @@ class user extends base { * @return column[] */ protected function get_all_columns(): array { + global $DB; + $usertablealias = $this->get_table_alias('user'); $contexttablealias = $this->get_table_alias('context'); @@ -275,14 +277,19 @@ class user extends base { foreach ($userfields as $userfield => $userfieldlang) { $columntype = $this->get_user_field_type($userfield); + $columnfieldsql = "{$usertablealias}.{$userfield}"; + if ($columntype === column::TYPE_LONGTEXT && $DB->get_dbfamily() === 'oracle') { + $columnfieldsql = $DB->sql_order_by_text($columnfieldsql, 1024); + } + $column = (new column( $userfield, $userfieldlang, $this->get_entity_name() )) ->add_joins($this->get_joins()) - ->add_field("{$usertablealias}.{$userfield}") ->set_type($columntype) + ->add_field($columnfieldsql, $userfield) ->set_is_sortable($this->is_sortable($userfield)) ->add_callback([$this, 'format'], $userfield); diff --git a/reportbuilder/classes/local/filters/autocomplete.php b/reportbuilder/classes/local/filters/autocomplete.php index dd94c3f8b22..d0d5fe24a88 100644 --- a/reportbuilder/classes/local/filters/autocomplete.php +++ b/reportbuilder/classes/local/filters/autocomplete.php @@ -75,4 +75,15 @@ class autocomplete extends base { return ["{$fieldsql} $insql", array_merge($params, $inparams)]; } + + /** + * Return sample filter values + * + * @return array + */ + public function get_sample_values(): array { + return [ + "{$this->name}_values" => [1], + ]; + } } diff --git a/reportbuilder/classes/local/filters/base.php b/reportbuilder/classes/local/filters/base.php index 9e94c06b0d9..5316a656913 100644 --- a/reportbuilder/classes/local/filters/base.php +++ b/reportbuilder/classes/local/filters/base.php @@ -120,4 +120,14 @@ abstract class base { return $filtersql !== ''; } + + /** + * Return sample filter values, that when applied to a report would activate the filter - that is, cause the filter to return + * SQL snippet. Should be overridden in child classes, to ensure compatibility with stress tests of reports + * + * @return array + */ + public function get_sample_values(): array { + return []; + } } diff --git a/reportbuilder/classes/local/filters/boolean_select.php b/reportbuilder/classes/local/filters/boolean_select.php index 92ae16b8daf..5bc4022f678 100644 --- a/reportbuilder/classes/local/filters/boolean_select.php +++ b/reportbuilder/classes/local/filters/boolean_select.php @@ -101,4 +101,15 @@ class boolean_select extends base { return [$fieldsql, $params]; } + + /** + * Return sample filter values + * + * @return array + */ + public function get_sample_values(): array { + return [ + "{$this->name}_operator" => self::CHECKED, + ]; + } } diff --git a/reportbuilder/classes/local/filters/category.php b/reportbuilder/classes/local/filters/category.php index 223634f7471..1cb4552f024 100644 --- a/reportbuilder/classes/local/filters/category.php +++ b/reportbuilder/classes/local/filters/category.php @@ -90,4 +90,15 @@ class category extends base { return [$sql, $params]; } + + /** + * Return sample filter values + * + * @return array + */ + public function get_sample_values(): array { + return [ + "{$this->name}_value" => 1, + ]; + } } diff --git a/reportbuilder/classes/local/filters/course_selector.php b/reportbuilder/classes/local/filters/course_selector.php index 21060b24a83..6a4747ac6fa 100644 --- a/reportbuilder/classes/local/filters/course_selector.php +++ b/reportbuilder/classes/local/filters/course_selector.php @@ -67,4 +67,15 @@ class course_selector extends base { return ["{$fieldsql} $courseselect", array_merge($params, $courseparams)]; } + + /** + * Return sample filter values + * + * @return array + */ + public function get_sample_values(): array { + return [ + "{$this->name}_values" => [1], + ]; + } } diff --git a/reportbuilder/classes/local/filters/date.php b/reportbuilder/classes/local/filters/date.php index 4d292298a8d..13f87683962 100644 --- a/reportbuilder/classes/local/filters/date.php +++ b/reportbuilder/classes/local/filters/date.php @@ -281,4 +281,16 @@ class date extends base { $dateend->getTimestamp(), ]; } + + /** + * Return sample filter values + * + * @return array + */ + public function get_sample_values(): array { + return [ + "{$this->name}_operator" => self::DATE_CURRENT, + "{$this->name}_unit" => self::DATE_UNIT_WEEK, + ]; + } } diff --git a/reportbuilder/classes/local/filters/duration.php b/reportbuilder/classes/local/filters/duration.php index 66c10b2aa4f..8c3c4951c85 100644 --- a/reportbuilder/classes/local/filters/duration.php +++ b/reportbuilder/classes/local/filters/duration.php @@ -134,4 +134,17 @@ class duration extends base { return [$sql, $params]; } + + /** + * Return sample filter values + * + * @return array + */ + public function get_sample_values(): array { + return [ + "{$this->name}_operator" => self::DURATION_MAXIMUM, + "{$this->name}_value" => 2, + "{$this->name}_unit" => MINSECS, + ]; + } } diff --git a/reportbuilder/classes/local/filters/number.php b/reportbuilder/classes/local/filters/number.php index fa1245eb657..034c2792c8c 100644 --- a/reportbuilder/classes/local/filters/number.php +++ b/reportbuilder/classes/local/filters/number.php @@ -197,4 +197,16 @@ class number extends base { return true; } + + /** + * Return sample filter values + * + * @return array + */ + public function get_sample_values(): array { + return [ + "{$this->name}_operator" => self::GREATER_THAN, + "{$this->name}_value1" => 1, + ]; + } } diff --git a/reportbuilder/classes/local/filters/select.php b/reportbuilder/classes/local/filters/select.php index 5aea41dcf3a..f46aaed3656 100644 --- a/reportbuilder/classes/local/filters/select.php +++ b/reportbuilder/classes/local/filters/select.php @@ -137,4 +137,16 @@ class select extends base { private function validate_filter_values(?int $operator, $value): bool { return !($operator === null || $value === ''); } + + /** + * Return sample filter values + * + * @return array + */ + public function get_sample_values(): array { + return [ + "{$this->name}_operator" => self::EQUAL_TO, + "{$this->name}_value" => 1, + ]; + } } diff --git a/reportbuilder/classes/local/filters/tags.php b/reportbuilder/classes/local/filters/tags.php index e8ff43616bc..b670b431c7a 100644 --- a/reportbuilder/classes/local/filters/tags.php +++ b/reportbuilder/classes/local/filters/tags.php @@ -117,4 +117,16 @@ class tags extends base { return [$select, $params]; } + + /** + * Return sample filter values + * + * @return array + */ + public function get_sample_values(): array { + return [ + "{$this->name}_operator" => self::EQUAL_TO, + "{$this->name}_value" => [1], + ]; + } } diff --git a/reportbuilder/classes/local/filters/text.php b/reportbuilder/classes/local/filters/text.php index d41e7995a36..791a92598dd 100644 --- a/reportbuilder/classes/local/filters/text.php +++ b/reportbuilder/classes/local/filters/text.php @@ -192,4 +192,16 @@ class text extends base { return true; } + + /** + * Return sample filter values + * + * @return array + */ + public function get_sample_values(): array { + return [ + "{$this->name}_operator" => self::IS_EQUAL_TO, + "{$this->name}_value" => 'test', + ]; + } } diff --git a/reportbuilder/classes/local/filters/user.php b/reportbuilder/classes/local/filters/user.php index 033f6bbf6ca..ed311606eb0 100644 --- a/reportbuilder/classes/local/filters/user.php +++ b/reportbuilder/classes/local/filters/user.php @@ -119,4 +119,16 @@ class user extends base { return [$sql, $params]; } + + /** + * Return sample filter values + * + * @return array + */ + public function get_sample_values(): array { + return [ + "{$this->name}_operator" => self::USER_SELECT, + "{$this->name}_value" => [1], + ]; + } } diff --git a/reportbuilder/classes/local/helpers/user_profile_fields.php b/reportbuilder/classes/local/helpers/user_profile_fields.php index cea9e225f64..119529213b7 100644 --- a/reportbuilder/classes/local/helpers/user_profile_fields.php +++ b/reportbuilder/classes/local/helpers/user_profile_fields.php @@ -118,13 +118,19 @@ class user_profile_fields { * @return column[] */ public function get_columns(): array { - $columns = []; + global $DB; + $columns = []; foreach ($this->userprofilefields as $profilefield) { $userinfotablealias = database::generate_alias(); $columntype = $this->get_user_field_type($profilefield->field->datatype); + $columnfieldsql = "{$userinfotablealias}.data"; + if ($DB->get_dbfamily() === 'oracle') { + $columnfieldsql = $DB->sql_order_by_text($columnfieldsql, 1024); + } + $column = (new column( 'profilefield_' . core_text::strtolower($profilefield->field->shortname), new lang_string('customfieldcolumn', 'core_reportbuilder', @@ -136,7 +142,7 @@ class user_profile_fields { ->add_join("LEFT JOIN {user_info_data} {$userinfotablealias} " . "ON {$userinfotablealias}.userid = {$this->usertablefieldalias} " . "AND {$userinfotablealias}.fieldid = {$profilefield->fieldid}") - ->add_field("{$userinfotablealias}.data") + ->add_field($columnfieldsql, 'data') ->set_type($columntype) ->set_is_sortable($columntype !== column::TYPE_LONGTEXT) ->add_callback([$this, 'format_profile_field'], $profilefield); diff --git a/reportbuilder/tests/helpers.php b/reportbuilder/tests/helpers.php index 38ceb04a716..3edbe3d0238 100644 --- a/reportbuilder/tests/helpers.php +++ b/reportbuilder/tests/helpers.php @@ -16,6 +16,9 @@ declare(strict_types=1); +use core_reportbuilder\manager; +use core_reportbuilder\local\helpers\aggregation; +use core_reportbuilder\local\helpers\report; use core_reportbuilder\local\helpers\user_filter_manager; use core_reportbuilder\table\custom_report_table_view; @@ -56,4 +59,120 @@ abstract class core_reportbuilder_testcase extends advanced_testcase { return $records; } + + /** + * Stress test a report source by iterating over all it's columns and asserting we can create a report for each + * + * @param string $source + */ + protected function datasource_stress_test_columns(string $source): void { + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + $report = $generator->create_report(['name' => 'Stress columns', 'source' => $source, 'default' => 0]); + $instance = manager::get_report_from_persistent($report); + + // Iterate over each available column, ensure each works correctly independent of any others. + $columnidentifiers = array_keys($instance->get_columns()); + foreach ($columnidentifiers as $columnidentifier) { + $column = report::add_report_column($report->get('id'), $columnidentifier); + + // We are only asserting the report returns content without errors, not the content itself. + try { + $content = $this->get_custom_report_content($report->get('id')); + $this->assertNotEmpty($content); + } catch (Throwable $exception) { + $this->fail("Error for column '{$columnidentifier}': " . $exception->getMessage()); + } + + report::delete_report_column($report->get('id'), $column->get('id')); + } + } + + /** + * Stress test a report source by iterating over all columns and asserting we can create a report while aggregating each + * + * @param string $source + */ + protected function datasource_stress_test_columns_aggregation(string $source): void { + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + $report = $generator->create_report(['name' => 'Stress aggregation', 'source' => $source, 'default' => 0]); + $instance = manager::get_report_from_persistent($report); + + // Add every column. + $columnidentifiers = array_keys($instance->get_columns()); + foreach ($columnidentifiers as $columnidentifier) { + report::add_report_column($report->get('id'), $columnidentifier); + } + + // Now iterate over each column, and apply all suitable aggregation types. + foreach ($instance->get_active_columns() as $column) { + $aggregations = aggregation::get_column_aggregations($column->get_type(), $column->get_disabled_aggregation()); + foreach (array_keys($aggregations) as $aggregation) { + $column->get_persistent()->set('aggregation', $aggregation)->update(); + + // We are only asserting the report returns content without errors, not the content itself. + try { + $content = $this->get_custom_report_content($report->get('id')); + $this->assertNotEmpty($content); + } catch (Throwable $exception) { + $this->fail("Error for column '{$column->get_unique_identifier()}' with aggregation '{$aggregation}': " . + $exception->getMessage()); + } + } + + // Reset the column aggregation. + $column->get_persistent()->set('aggregation', null)->update(); + } + } + + /** + * Stress test a report source by iterating over all it's conditions and asserting we can create a report using each + * + * @param string $source + * @param string $columnidentifier Should be a simple column, with as few fields and joins as possible, ideally selected + * from the base table itself + */ + protected function datasource_stress_test_conditions(string $source, string $columnidentifier): void { + + /** @var core_reportbuilder_generator $generator */ + $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder'); + + $report = $generator->create_report(['name' => 'Stress conditions', 'source' => $source, 'default' => 0]); + $instance = manager::get_report_from_persistent($report); + + // Add single column only (to ensure no conditions have reliance on any columns). + report::add_report_column($report->get('id'), $columnidentifier); + + // Iterate over each available condition, ensure each works correctly independent of any others. + $conditionidentifiers = array_keys($instance->get_conditions()); + foreach ($conditionidentifiers as $conditionidentifier) { + $condition = report::add_report_condition($report->get('id'), $conditionidentifier); + $conditioninstance = $instance->get_condition($condition->get('uniqueidentifier')); + + /** @var \core_reportbuilder\local\filters\base $conditionclass */ + $conditionclass = $conditioninstance->get_filter_class(); + + // Set report condition values in order to activate it. + $conditionvalues = $conditionclass::create($conditioninstance)->get_sample_values(); + if (empty($conditionvalues)) { + debugging("Missing sample values from filter '{$conditionclass}'", DEBUG_DEVELOPER); + } + $instance->set_condition_values($conditionvalues); + + // We are only asserting the report returns content without errors, not the content itself. + try { + $content = $this->get_custom_report_content($report->get('id')); + $this->assertIsArray($content); + } catch (Throwable $exception) { + $this->fail("Error for condition '{$conditionidentifier}': " . $exception->getMessage()); + } + + report::delete_report_condition($report->get('id'), $condition->get('id')); + } + } } diff --git a/reportbuilder/upgrade.txt b/reportbuilder/upgrade.txt index 7f76b9f0a38..f16934e7564 100644 --- a/reportbuilder/upgrade.txt +++ b/reportbuilder/upgrade.txt @@ -39,4 +39,9 @@ Information provided here is intended especially for developers. - `category` for reports containing course categories - `tags` for reports containing entities with support for core_tag API - `autocomplete` for reports that contain pre-defined values for selection. -* The helper method `get_custom_report_content()` now accepts a list of filters and applies them to the report +* New method `get_sample_values()` added to base filter class, to be overridden in all filter types to support stress testing +* New test helpers for automated stress testing of report sources: + - `datasource_stress_test_columns` + - `datasource_stress_test_columns_aggregation` + - `datasource_stress_test_conditions` +* The test helper method `get_custom_report_content()` now accepts a list of filter values and applies them to the report diff --git a/tag/classes/reportbuilder/local/entities/tag.php b/tag/classes/reportbuilder/local/entities/tag.php index f15d18691f7..ea93d2a46b6 100644 --- a/tag/classes/reportbuilder/local/entities/tag.php +++ b/tag/classes/reportbuilder/local/entities/tag.php @@ -83,6 +83,8 @@ class tag extends base { * @return column[] */ protected function get_all_columns(): array { + global $DB; + $tagalias = $this->get_table_alias('tag'); // Name. @@ -121,6 +123,10 @@ class tag extends base { }); // Description. + $descriptionfieldsql = "{$tagalias}.description"; + if ($DB->get_dbfamily() === 'oracle') { + $descriptionfieldsql = $DB->sql_order_by_text($descriptionfieldsql, 1024); + } $columns[] = (new column( 'description', new lang_string('tagdescription', 'core_tag'), @@ -128,7 +134,8 @@ class tag extends base { )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_LONGTEXT) - ->add_fields("{$tagalias}.description, {$tagalias}.descriptionformat, {$tagalias}.id") + ->add_field($descriptionfieldsql, 'description') + ->add_fields("{$tagalias}.descriptionformat, {$tagalias}.id") ->add_callback(static function(?string $description, stdClass $tag): string { global $CFG; require_once("{$CFG->libdir}/filelib.php"); diff --git a/tag/tests/reportbuilder/datasource/tags_test.php b/tag/tests/reportbuilder/datasource/tags_test.php index 7fe2fcb81ef..5257ea9e26f 100644 --- a/tag/tests/reportbuilder/datasource/tags_test.php +++ b/tag/tests/reportbuilder/datasource/tags_test.php @@ -258,4 +258,17 @@ class tags_test extends core_reportbuilder_testcase { $this->assertEmpty($content); } } + + /** + * Stress test datasource + */ + public function test_stress_datasource(): void { + $this->resetAfterTest(); + + $this->getDataGenerator()->create_course(['tags' => ['Horses']]); + + $this->datasource_stress_test_columns(tags::class); + $this->datasource_stress_test_columns_aggregation(tags::class); + $this->datasource_stress_test_conditions(tags::class, 'tag:name'); + } } diff --git a/user/tests/reportbuilder/datasource/users_test.php b/user/tests/reportbuilder/datasource/users_test.php index 983c77ffb71..36ab13b33ae 100644 --- a/user/tests/reportbuilder/datasource/users_test.php +++ b/user/tests/reportbuilder/datasource/users_test.php @@ -29,7 +29,7 @@ global $CFG; require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php"); /** - * Unit tests for users datasources + * Unit tests for users datasource * * @package core_user * @covers \core_user\reportbuilder\datasource\users @@ -155,4 +155,18 @@ class users_test extends core_reportbuilder_testcase { $this->assertEmpty($content); } } + + /** + * Stress test datasource + */ + public function test_stress_datasource(): void { + $this->resetAfterTest(); + + $this->getDataGenerator()->create_custom_profile_field(['datatype' => 'text', 'name' => 'Hi', 'shortname' => 'hi']); + $user = $this->getDataGenerator()->create_user(['profile_field_hi' => 'Hello']); + + $this->datasource_stress_test_columns(users::class); + $this->datasource_stress_test_columns_aggregation(users::class); + $this->datasource_stress_test_conditions(users::class, 'user:username'); + } }