This commit is contained in:
Ilya Tregubov 2022-09-06 14:28:53 +04:00
commit d3a7f73f50
28 changed files with 434 additions and 14 deletions

View File

@ -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
*

View File

@ -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 {

View File

@ -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');
}
}

View File

@ -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;

View File

@ -37,7 +37,7 @@ require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php");
* @copyright 2021 Paul Holden <paulh@moodle.com>
* @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');
}
}

View File

@ -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;

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -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));

View File

@ -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);

View File

@ -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],
];
}
}

View File

@ -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 [];
}
}

View File

@ -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,
];
}
}

View File

@ -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,
];
}
}

View File

@ -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],
];
}
}

View File

@ -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,
];
}
}

View File

@ -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,
];
}
}

View File

@ -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,
];
}
}

View File

@ -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,
];
}
}

View File

@ -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],
];
}
}

View File

@ -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',
];
}
}

View File

@ -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],
];
}
}

View File

@ -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);

View File

@ -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'));
}
}
}

View File

@ -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

View File

@ -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");

View File

@ -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');
}
}

View File

@ -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');
}
}