Reports: Building a custom report plugin (2/3)

A report form is usually divided into two sections. The first section configures the so called custom options for the report and this section looks different for every report plugin. The second section defines the system options (access, notifications and scheduling) and is the same for all report plugins. This part explains how to populate the report form and how to use these options to customize generated reports.

Form options

All built-in report plugins in TestRail are highly customizable and you can often configure the scope, layout and level of detail to include in the generated reports. A good idea to structure and group the different options is to use a tab control and that's also usually the first step when designing a new report form:

<div class="tabs">
	<div class="tab-header">
		<a href="javascript:void(0)" class="tab1 current" rel="1"
			onclick="App.Tabs.activate(this)">
				<?= lang('reports_tpr_form_details') ?></a>
		<a href="javascript:void(0)" class="tab2" rel="2"
			onclick="App.Tabs.activate(this)">
				<?= lang('reports_tpr_form_runs') ?></a>
	</div>
	<div class="tab-body tab-frame">
		<div class="tab tab1">
			<!-- The content of tab 1 goes here -->
		</div>
		<div class="tab tab2 hidden">
			<!-- The content of tab 2 goes here -->
		</div>
	</div>
</div>

For report plugins that serve more than one purpose, it is usually a good idea to make the level of detail configurable. Recall that our report plugin is supposed to render reports that display the result distribution for test case types and priorities. Useful options in this case would be the inclusion/exclusion of each of these entities. To do this, we go back to report.php and fill the form related functions. We begin with prepare_form which, well, prepares the form and registers our options:

public function prepare_form($context, $validation)
{
	// Assign the validation rules for the fields on the form.
	$validation->add_rules(
		array(
			'custom_types_include' => array(
				'type' => 'bool',
				'default' => false
			),
			'custom_priorities_include' => array(
				'type' => 'bool',
				'default' => false
			)
		)
	);
 
	if (request::is_post())
	{
		return;
	}
 
	// We assign the default values for the form depending on the
	// event. For 'add', we use the default values of this plugin.
	// For 'edit/rerun', we use the previously saved values of
	// the report/report job to initialize the form. Please note
	// that we prefix all fields in the form with 'custom_' and
	// that the storage format omits this prefix (validate_form).
 
	if ($context['event'] == 'add')
	{
		$defaults = array(
			'types_include' => true,
			'priorities_include' => true
		);
	}
	else
	{
		$defaults = $context['custom_options'];
	}
 
	foreach ($defaults as $field => $value)
	{
		$validation->set_default('custom_' . $field, $value);
	}
}

This function does two things. The first step is to set the validation rules for the options using the passed $validation object and the second is to set the default values for these options. Note how we use the $context parameter to decide whether we add a new report or edit/rerun an existing one and how the handling of the default values differs between these two choices.

We can then go on by filling validate_form to validate/return the options:

public function validate_form($context, $input, $validation)
{
	// At least one detail entity option must be selected (types or
	// priorities).
	if (!$input['custom_types_include'] &&
		!$input['custom_priorities_include'])
	{
		$validation->add_error(
			lang('reports_tpr_form_details_include_required')
		);
 
		return false;
	}
 
	$values = array();
 
	static $fields = array(
		'types_include',
		'priorities_include'
	);
 
	foreach ($fields as $field)
	{
		$key = 'custom_' . $field;
		$values[$field] = arr::get($input, $key);
	}
 
	return $values;
}

Simple validation scenarios are already covered by the validation rules set in prepare_form and validate_form can be used to handle more complex scenarios. For example, we use this here to verify that at least one entity is selected on the form (types or priorities). We then just return the report options as they should be stored for the rendering step.

We then switch back to form.php again and can add the corresponding code to the form:

..
<!-- The content of tab 1 goes here -->
<p class="top"><?= lang('reports_tpr_form_details_include') ?></p>
<div class="checkbox form-checkbox" style="margin-left: 15px">
	<label>
		<?= lang('reports_tpr_form_details_include_types') ?>
		<input type="checkbox" id="custom_types_include"
			name="custom_types_include" value="1"
			<?= validation::get_checked('custom_types_include',1) ?> />
	</label>
</div>
<div class="checkbox" style="margin-left: 15px">
	<label>
		<?= lang('reports_tpr_form_details_include_priorities') ?>
		<input type="checkbox" id="custom_priorities_include"
			name="custom_priorities_include" value="1"
			<?= validation::get_checked('custom_priorities_include',1) ?> />
	</label>
</div>
..

All put together, the report form should now look as follows:

Form controls

While it is possible to build even complex forms with the custom options approach, many report plugins share the same options and it wouldn't make sense to implement those options over and over again. Fortunately, TestRail's reporting engine comes with a feature called “form controls” that implement common options and are reusable from built-in as well as custom report plugins. There are controls for selecting a set of statuses, users, test suites, test runs and much more.

This section explains how to use form controls to configure the “scope” of the report plugin. In our case we refer to the scope as the test runs that are part of the generated reports and we use the run selection and run limit form controls for this. We simply switch back to report.php and add/modify the following code:

class Tests_property_results_report_plugin extends Report_plugin
{
	private $_controls;
 
	// The controls and options for those controls that are used on
	// the form of this report.
	private static $_control_schema = array(
		'runs_select' => array(
			'namespace' => 'custom_runs',
			'multiple_suites' => true
		),
		'runs_limit' => array(
			'type' => 'limits_select',
			'namespace' => 'custom_runs',
			'min' => 0,
			'max' => 100,
			'default' => 25
		)
	);
 
	..
 
	public function __construct()
	{
		parent::__construct();
		$this->_controls = $this->create_controls(
			self::$_control_schema
		);
	}
 
	public function prepare_form($context, $validation)
	{
		// Assign the validation rules for the controls used on the
		// form.
		$this->prepare_controls($this->_controls, $context, 
			$validation);
 
		..
	}
 
	public function validate_form($context, $input, $validation)
	{
		..
 
		// We begin with validating the controls used on the form.
		$values = $this->validate_controls(
			$this->_controls,
			$context,
			$input,
			$validation);
 
		if (!$values)
		{
			return false;
		}
 
		..
	}
 
	public function render_form($context)
	{
		$params = array(
			'controls' => $this->_controls,
			'project' => $context['project']
		);
 
		..
	}
}

We basically create the controls in our constructor based on the defined schema, register our controls in prepare_form and validate them in validate_form. We also make sure to pass the controls to the form view. All that's now left to do is to embed the corresponding front-end code into form.php:

<!-- The content of tab 2 goes here -->
<? $report_obj->render_control(
	$controls,
	'runs_select',
	array(
		'top' => true,
		'project' => $project
	)
) ?>
<? $report_obj->render_control(
	$controls,
	'runs_limit',
	array(
		'intro' => lang('report_plugins_runs_limit'),
		'limits' => array(5, 10, 25, 50, 100, 0)
	)
) ?>

When you then now switch to the Test Suites & Runs tab in our report form, you should see something like the following:

Note that by adding control support to the form methods in report.php, the selected control values are now automatically saved in the report options and made available via the standard $options parameter when rendering the report. This will be demonstrated in the next section.

Report helper

Similar to form controls, TestRail's reporting engine also provides access to common database related queries to read the actual data from the database. These queries are made available via object methods contained in a so called “report helper”. This helper is accessible via $this→_helper in report.php and $report_helper in the report views. Many functions of this helper are designed to be used in conjunction with form controls and this will be made clear in the following example. We now switch to the actual rendering of the report (“run” method) and set up the scope of the report:

public function run($context, $options)
{
	$project = $context['project'];
 
	// Read the test suites first.
	$suites = $this->_helper->get_suites_by_include(
		$project->id,
		$options['runs_suites_ids'],
		$options['runs_suites_include']
	);
 
	$suite_ids = obj::get_ids($suites);
 
	// We then get the actual list of test runs used, depending on
	// the report options.
	if ($suite_ids)
	{
		$runs = $this->_helper->get_runs_by_include(
			$project->id,
			$suite_ids,
			$options['runs_include'],
			$options['runs_ids'],
			$options['runs_filters'],
			null, // Active and completed
			$options['runs_limit'],
			$run_rels,
			$run_count
		);
	}
	else
	{
		$runs = array();
		$run_rels = array();
		$run_count = 0;
	}
 
	$run_ids = obj::get_ids($runs);
 
	..
}

This uses the options of the form controls (part of the $options parameter) to read the relevant test suites and runs for the report from the database. The report helper provides many additional methods and it is recommended to take a look at the source code of the built-in report plugins at app/plugins/reports for a complete overview.

Next steps

Read on and learn in the last part how to access the database directly for custom queries and how to render your data: