diff --git a/classes/DataWarehouse/Query/Query.php b/classes/DataWarehouse/Query/Query.php index a3fe363bfd..97e4cb39a8 100644 --- a/classes/DataWarehouse/Query/Query.php +++ b/classes/DataWarehouse/Query/Query.php @@ -1157,10 +1157,6 @@ protected function setDuration($start_date, $end_date) $end_date_parsed['year'] ); - if ($this->_start_date_ts > $this->_end_date_ts) { - throw new \Exception("Invalid Date: start_date must be before end_date"); - } - list($this->_min_date_id, $this->_max_date_id) = $this->_aggregation_unit->getDateRangeIds($this->_start_date, $this->_end_date); if (!$start_date_given && !$end_date_given) { diff --git a/classes/Rest/Controllers/BaseControllerProvider.php b/classes/Rest/Controllers/BaseControllerProvider.php index ee3985ea5d..67549c3447 100644 --- a/classes/Rest/Controllers/BaseControllerProvider.php +++ b/classes/Rest/Controllers/BaseControllerProvider.php @@ -2,6 +2,7 @@ namespace Rest\Controllers; +use DateTime; use Rest\Utilities\Authentication; use Rest\Utilities\Authorization; use Silex\Application; @@ -551,7 +552,7 @@ protected function getDateTimeFromUnixParam(Request $request, $name, $mandatory FILTER_CALLBACK, array( "options" => function ($value) { - $value_dt = \DateTime::createFromFormat('U', $value); + $value_dt = DateTime::createFromFormat('U', $value); if ($value_dt === false) { return null; } @@ -705,4 +706,52 @@ public function formatLogMesssage($message, Request $request, $includeParams = f return $retval; } // formatLogMessage() + + /** + * Checks that the `$[start|end]Date` values are valid ( `Y-m-d` ) dates and that `$startDate` + * is before `$endDate`. + * + * @param string $startDate the beginning of the date range. + * @param string $endDate the end of the date range. + * @throws BadRequestHttpException if either start or end dates are not provided in the format + * `Y-m-d`, or if the start date is after the end date. + */ + protected function checkDateRange($startDate, $endDate) + { + $startTimestamp = $this->getTimestamp($startDate, 'start_date'); + $endTimestamp = $this->getTimestamp($endDate, 'end_date'); + + if ($startTimestamp > $endTimestamp) { + throw new BadRequestHttpException('Start Date must not be after End Date'); + } + } + + /** + * Attempt to convert the provided string $date value into an equivalent unix timestamp (int). + * + * @param string $date The value to be converted into a DateTime. + * @param string $paramName 'date', The name of the parameter to be included in the exception + * message if validation fails. + * @param string $format 'Y-m-d', The format that `$date` should be in. + * @return int created from the provided `$date` value. + * @throws BadRequestHttpException if the date is not in the form `Y-m-d`. + */ + protected function getTimestamp($date, $paramName = 'date', $format = 'Y-m-d') + { + $parsed = date_parse_from_format($format, $date); + $date = mktime( + $parsed['hour'], + $parsed['minute'], + $parsed['second'], + $parsed['month'], + $parsed['day'], + $parsed['year'] + ); + + if ($date === false || $parsed['error_count'] > 0) { + throw new BadRequestHttpException("Unable to parse $paramName"); + } + + return $date; + } } diff --git a/classes/Rest/Controllers/SummaryControllerProvider.php b/classes/Rest/Controllers/SummaryControllerProvider.php index e8df97d9ce..06d1c73af5 100644 --- a/classes/Rest/Controllers/SummaryControllerProvider.php +++ b/classes/Rest/Controllers/SummaryControllerProvider.php @@ -2,12 +2,15 @@ namespace Rest\Controllers; +use Configuration\XdmodConfiguration; +use Exception; +use PDOException; use Silex\Application; use Silex\ControllerCollection; use Symfony\Component\HttpFoundation\Request; -use DataWarehouse\Query\Exceptions\BadRequestException; use Models\Services\Acls; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; class SummaryControllerProvider extends BaseControllerProvider { @@ -23,6 +26,8 @@ public function setupRoutes(Application $app, ControllerCollection $controller) $controller->post("$root/layout", "$class::setLayout"); $controller->delete("$root/layout", "$class::resetLayout"); + + $controller->get("$root/statistics", "$class::getStatistics"); } /* @@ -191,4 +196,58 @@ public function resetLayout(Request $request, Application $app) 'total' => 1 )); } + + /** + * Retrieve summary statistics + * + * @param Request $request + * @param Application $app + * @return \Symfony\Component\HttpFoundation\JsonResponse + * @throws Exception + */ + public function getStatistics(Request $request, Application $app) + { + $user = $this->getUserFromRequest($request); + + $aggregationUnit = $request->get('aggregation_unit', 'auto'); + + $startDate = $this->getStringParam($request, 'start_date', true); + $endDate = $this->getStringParam($request, 'end_date', true); + + $this->checkDateRange($startDate, $endDate); + + // This try/catch block is intended to replace the "Base table or + // view not found: 1146 Table 'modw_aggregates.jobfact_by_day' + // doesn't exist" error message with something more informative for + // Open XDMoD users. + try { + $query = new \DataWarehouse\Query\Jobs\Aggregate($aggregationUnit, $startDate, $endDate, 'none', 'all'); + + $result = $query->execute(); + } catch (PDOException $e) { + if ($e->getCode() === '42S02' && strpos($e->getMessage(), 'modw_aggregates.jobfact_by_') !== false) { + $msg = 'Aggregate table not found, have you ingested your data?'; + throw new Exception($msg); + } else { + throw $e; + } + } catch (Exception $e) { + throw new BadRequestHttpException($e->getMessage()); + } + + $rawRoles = XdmodConfiguration::assocArrayFactory('roles.json', CONFIG_DIR); + + $mostPrivileged = $user->getMostPrivilegedRole()->getName(); + $formats = $rawRoles['roles'][$mostPrivileged]['statistics_formats']; + + return $app->json( + array( + 'totalCount' => 1, + 'success' => true, + 'message' => '', + 'formats' => $formats, + 'data' => array($result) + ) + ); + } } diff --git a/configuration/roles.d/statistics_format.json b/configuration/roles.d/statistics_format.json new file mode 100644 index 0000000000..100e9a0133 --- /dev/null +++ b/configuration/roles.d/statistics_format.json @@ -0,0 +1,262 @@ +{ + "+roles": { + "+default": { + "statistics_formats": [ + { + "title": "Activity", + "items": [ + { + "title": "Users", + "fieldName": "active_person_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "PIs", + "fieldName": "active_pi_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Allocations", + "fieldName": "active_allocation_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Institutions", + "fieldName": "active_institution_count", + "numberType": "int", + "numberFormat": "#,#" + } + ] + }, + { + "title": "Jobs", + "items": [ + { + "title": "Total", + "fieldName": "job_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Gateway", + "fieldName": "gateway_job_count", + "numberType": "int", + "numberFormat": "#,#" + } + ] + }, + { + "title": "Service (XD SU)", + "items": [ + { + "title": "Total", + "fieldName": "total_su", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_su", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "CPU Time (h)", + "items": [ + { + "title": "Total", + "fieldName": "total_cpu_hours", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_cpu_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Wait Time (h)", + "items": [ + { + "title": "Avg (Per Job)", + "fieldName": "avg_waitduration_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Wall Time (h)", + "items": [ + { + "title": "Total", + "fieldName": "total_wallduration_hours", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_wallduration_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Processors", + "items": [ + { + "title": "Max", + "fieldName": "max_processors", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_processors", + "numberType": "int", + "numberFormat": "#,#" + } + ] + } + ] + }, + "+pub": { + "statistics_formats": [ + { + "title": "Activity", + "items": [ + { + "title": "Users", + "fieldName": "active_person_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "PIs", + "fieldName": "active_pi_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Allocations", + "fieldName": "active_allocation_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Institutions", + "fieldName": "active_institution_count", + "numberType": "int", + "numberFormat": "#,#" + } + ] + }, + { + "title": "Jobs", + "items": [ + { + "title": "Total", + "fieldName": "job_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Gateway", + "fieldName": "gateway_job_count", + "numberType": "int", + "numberFormat": "#,#" + } + ] + }, + { + "title": "Service (XD SU)", + "items": [ + { + "title": "Total", + "fieldName": "total_su", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_su", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "CPU Time (h)", + "items": [ + { + "title": "Total", + "fieldName": "total_cpu_hours", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_cpu_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Wait Time (h)", + "items": [ + { + "title": "Avg (Per Job)", + "fieldName": "avg_waitduration_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Wall Time (h)", + "items": [ + { + "title": "Total", + "fieldName": "total_wallduration_hours", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_wallduration_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Processors", + "items": [ + { + "title": "Max", + "fieldName": "max_processors", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_processors", + "numberType": "int", + "numberFormat": "#,#" + } + ] + } + ] + } + } +} diff --git a/configuration/roles.d/summary.json b/configuration/roles.d/summary.json new file mode 100644 index 0000000000..0ee20ca9cd --- /dev/null +++ b/configuration/roles.d/summary.json @@ -0,0 +1,40 @@ +{ + "+roles": { + "+cd": { + "summary_portlets": [ + { + "name": "Summary Statistics", + "type": "SummaryStatisticsPortlet", + "region": "top", + "config": { + "timeframe": "Quarter to date" + } + } + ] + }, + "+cs": { + "summary_portlets": [ + { + "name": "Summary Statistics", + "type": "SummaryStatisticsPortlet", + "region": "top", + "config": { + "timeframe": "Month to date" + } + } + ] + }, + "+pub": { + "summary_portlets": [ + { + "name": "Summary Statistics", + "type": "SummaryStatisticsPortlet", + "region": "top", + "config": { + "timeframe": "Last month" + } + } + ] + } + } +} diff --git a/configuration/roles.json b/configuration/roles.json index e8ec94d2a7..4c5886f976 100644 --- a/configuration/roles.json +++ b/configuration/roles.json @@ -152,6 +152,7 @@ "show_filters": true, "start": 0, "timeseries": true, + "timeframe": "Year to date", "title": "Total CPU Hours By Resource (Top 10)" }, { diff --git a/html/gui/js/modules/summary/SummaryStatisticsPortlet.js b/html/gui/js/modules/summary/SummaryStatisticsPortlet.js new file mode 100644 index 0000000000..7a249ac8e3 --- /dev/null +++ b/html/gui/js/modules/summary/SummaryStatisticsPortlet.js @@ -0,0 +1,208 @@ +/** + * XDMoD.Modules.SummaryPortlets.SummaryStatisticsPortlet + * + * + */ + +Ext.namespace('XDMoD.Modules.SummaryPortlets'); + +XDMoD.Modules.SummaryPortlets.SummaryStatisticsPortlet = Ext.extend(Ext.ux.Portlet, { + + layout: 'fit', + autoScroll: true, + baseTitle: 'Summary Statistics', + tbar: { + border: false, + cls: 'xd-toolbar' + }, + + /** + * The styling that will be applied to the summary statistic toolbar item + * headers. + */ + keyStyle: { + marginLeft: '2px', + marginRight: '2px', + fontSize: '11px', + textAlign: 'center' + }, + + /** + * The styling that will be applied to the summary statistic toolbar item + * values. + */ + valueStyle: { + marginLeft: '2px', + marginRight: '2px', + textAlign: 'center', + fontFamily: 'arial,"Times New Roman",Times,serif', + fontSize: '11px', + letterSpacing: '0px' + }, + + /** + * + */ + initComponent: function () { + var self = this; + + var aspectRatio = 0.8; + this.height = this.width * aspectRatio; + var title = this.baseTitle; + + var dateRanges = CCR.xdmod.ui.DurationToolbar.getDateRanges(); + for (var i = 0; i < dateRanges.length; i++) { + var dateRange = dateRanges[i]; + if (dateRange.text === this.config.timeframe) { + this.config.start_date = this.formatDate(dateRange.start); + this.config.end_date = this.formatDate(dateRange.end); + title = this.baseTitle + ' - ' + this.config.start_date + ' to ' + this.config.end_date; + } + } + + this.setTitle(title); + + this.summaryStatisticsStore = new CCR.xdmod.CustomJsonStore({ + + root: 'data', + totalProperty: 'totalCount', + autoDestroy: true, + autoLoad: false, + successProperty: 'success', + messageProperty: 'message', + + fields: [ + 'job_count', + 'active_person_count', + 'active_pi_count', + 'total_waitduration_hours', + 'avg_waitduration_hours', + 'total_cpu_hours', + 'avg_cpu_hours', + 'total_su', + 'avg_su', + 'min_processors', + 'max_processors', + 'avg_processors', + 'total_wallduration_hours', + 'avg_wallduration_hours', + 'gateway_job_count', + 'active_allocation_count', + 'active_institution_count', + 'statistics_formats' + ], + + proxy: new Ext.data.HttpProxy({ + method: 'GET', + url: XDMoD.REST.url + '/summary/statistics', + listeners: { + + /** + * + * @param proxy {Ext.data.DataProxy} + * @param request {Ext.data.Request} + */ + load: function (proxy, request) { + var formats = request.reader.jsonData.formats; + var data = request.reader.jsonData.data; + + // only populate the statistics if we have all the data + // we require. + if (formats && data.length) { + self.populateSummaryStatistics(formats, data[0]); + } + } // load: function (proxy, request) { + } // listeners: { + }) // proxy: new Ext.data.HttpProxy({ + }); // this.summaryStatisticsStore + + XDMoD.Modules.SummaryPortlets.SummaryStatisticsPortlet.superclass.initComponent.apply(this, arguments); + }, // initComponent + + listeners: { + /** + * This event fires after the component has been rendered. This will only + * occur once per page refresh. + */ + afterrender: function () { + this.summaryStatisticsStore.load({ + params: { + start_date: this.config.start_date, + end_date: this.config.end_date + } + }); + } + }, // listeners { + + /** + * Populates this components top toolbar w/ the series of summary statistics + * as defined in `data`, formatted via the entries in `formats`. + * + * @param formats {object[]} + * @param data {object} + */ + populateSummaryStatistics: function (formats, data) { + // Clear the top toolbar before re-populating it. + this.getTopToolbar().removeAll(); + + Ext.each(formats, function (itemGroup) { + var itemTitles = []; + var items = []; + + Ext.each(itemGroup.items, function (item) { + var itemData = data[item.fieldName]; + var itemNumber; + + if (itemData) { + if (item.numberType === 'int') { + itemNumber = parseInt(itemData, 10); + } else if (item.numberType === 'float') { + itemNumber = parseFloat(itemData); + } + + itemTitles.push({ + xtype: 'tbtext', + text: item.title + ':', + style: this.keyStyle + }); + + items.push({ + xtype: 'tbtext', + text: itemNumber.numberFormat(item.numberFormat), + style: this.valueStyle + }); + } // if (itemData) + }, this); // Ext.each(itemGroup.items, ... + + if (items.length > 0) { + this.getTopToolbar().add({ + xtype: 'buttongroup', + columns: items.length, + title: itemGroup.title, + items: itemTitles.concat(items) + }); + } + }, this); + + // make sure that we force the toolbar to re-lay its self out. + this.getTopToolbar().doLayout(); + }, // populateSummaryStatistics + + /** + * Returns a consistently formatted string from the provided `date`. + * Ex. `2019-01-01` + * + * @param date {Date} the date to use when building the formatted string. + * @returns {string} a `YYYY-MM-DD` formatted string based on the `date` + * parameter. + */ + formatDate: function (date) { + return date.getFullYear() + '-' + ('' + (date.getMonth() + 1)).padStart(2, '0') + '-' + ('' + date.getDate()).padStart(2, '0'); + } // formatDate: function(date) { +}); // XDMoD.Modules.SummaryPortlets.CenterHealthPortlet = Ext.extend(Ext.ux.Portlet, { + +/** + * The Ext.reg call is used to register an xtype for this class so it + * can be dynamically instantiated + */ +Ext.reg('SummaryStatisticsPortlet', XDMoD.Modules.SummaryPortlets.SummaryStatisticsPortlet); diff --git a/open_xdmod/modules/xdmod/build.json b/open_xdmod/modules/xdmod/build.json index abf1bd25e4..d5c4a5a98a 100644 --- a/open_xdmod/modules/xdmod/build.json +++ b/open_xdmod/modules/xdmod/build.json @@ -85,6 +85,7 @@ "configuration/resource_types.json", "configuration/resources.json", "configuration/roles.json", + "configuration/roles.d", "configuration/rest.json", "configuration/rest.d", "configuration/setup.json", diff --git a/tests/artifacts/xdmod/rest/input/get_statistics.json b/tests/artifacts/xdmod/rest/input/get_statistics.json new file mode 100644 index 0000000000..3b8a2584c3 --- /dev/null +++ b/tests/artifacts/xdmod/rest/input/get_statistics.json @@ -0,0 +1,89 @@ +[ + [ + { + "username": "pub" + } + ], + [ + { + "username": "cd" + } + ], + [ + { + "username": "cs" + } + ], + [ + { + "username": "pi" + } + ], + [ + { + "username": "usr" + } + ], + [ + { + "username": "pub", + "end_date": "null", + "expected": { + "file": "get_statistics_no_end_date-pub", + "http_code": 400 + } + } + ], + [ + { + "username": "pub", + "start_date": "null", + "expected": { + "file": "get_statistics_no_start_date-pub", + "http_code": 400 + } + } + ], + [ + { + "username": "pub", + "start_date": "null", + "end_date": "null", + "expected": { + "file": "get_statistics_no_start_end_date-pub", + "http_code": 400 + } + } + ], + [ + { + "username": "pub", + "start_date": "2017-01-01", + "end_date": "2016-12-22", + "expected": { + "file": "get_statistics_start_date_after_end-pub", + "http_code": 400 + } + } + ], + [ + { + "username": "pub", + "start_date": "christmas", + "expected": { + "file": "get_statistics_start_date_invalid-pub", + "http_code": 400 + } + } + ], + [ + { + "username": "pub", + "end_date": "christmas", + "expected": { + "file": "get_statistics_end_date_invalid-pub", + "http_code": 400 + } + } + ] +] diff --git a/tests/artifacts/xdmod/rest/output/get_statistics-cd.json b/tests/artifacts/xdmod/rest/output/get_statistics-cd.json new file mode 100644 index 0000000000..1d36bf5b12 --- /dev/null +++ b/tests/artifacts/xdmod/rest/output/get_statistics-cd.json @@ -0,0 +1,230 @@ +{ + "totalCount": 1, + "success": true, + "message": "", + "formats": [ + { + "title": "Activity", + "items": [ + { + "title": "Users", + "fieldName": "active_person_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "PIs", + "fieldName": "active_pi_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Allocations", + "fieldName": "active_allocation_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Institutions", + "fieldName": "active_institution_count", + "numberType": "int", + "numberFormat": "#,#" + } + ] + }, + { + "title": "Jobs", + "items": [ + { + "title": "Total", + "fieldName": "job_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Gateway", + "fieldName": "gateway_job_count", + "numberType": "int", + "numberFormat": "#,#" + } + ] + }, + { + "title": "Service (XD SU)", + "items": [ + { + "title": "Total", + "fieldName": "total_su", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_su", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "CPU Time (h)", + "items": [ + { + "title": "Total", + "fieldName": "total_cpu_hours", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_cpu_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Wait Time (h)", + "items": [ + { + "title": "Avg (Per Job)", + "fieldName": "avg_waitduration_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Wall Time (h)", + "items": [ + { + "title": "Total", + "fieldName": "total_wallduration_hours", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_wallduration_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Processors", + "items": [ + { + "title": "Max", + "fieldName": "max_processors", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_processors", + "numberType": "int", + "numberFormat": "#,#" + } + ] + } + ], + "data": [ + { + "id": [ + "-9999" + ], + "name": [ + "Screwdriver" + ], + "short_name": [ + "Screwdriver" + ], + "order_id": [ + "Screwdriver" + ], + "job_count": [ + "71313" + ], + "running_job_count": [ + "71313" + ], + "started_job_count": [ + "71313" + ], + "submitted_job_count": [ + "64416" + ], + "active_person_count": [ + "66" + ], + "active_pi_count": [ + "41" + ], + "total_cpu_hours": [ + "840555.8369" + ], + "total_waitduration_hours": [ + "306345.3608" + ], + "total_node_hours": [ + "156303.7475" + ], + "total_wallduration_hours": [ + "127976.8064" + ], + "avg_cpu_hours": [ + "11.78685285" + ], + "sem_avg_cpu_hours": [ + "0.4535023959163972" + ], + "avg_node_hours": [ + "2.19179880" + ], + "sem_avg_node_hours": [ + "0.037808294679009785" + ], + "avg_waitduration_hours": [ + "4.29578563" + ], + "sem_avg_waitduration_hours": [ + "0.08956571094180898" + ], + "avg_wallduration_hours": [ + "1.79457892" + ], + "sem_avg_wallduration_hours": [ + "0.017929611668507588" + ], + "avg_processors": [ + "8.7346" + ], + "sem_avg_processors": [ + "0.02156312501879985" + ], + "min_processors": [ + "1" + ], + "max_processors": [ + "336" + ], + "utilization": [ + "15.919618124" + ], + "expansion_factor": [ + "3.1031" + ], + "normalized_avg_processors": [ + "0.043672753" + ], + "avg_job_size_weighted_by_cpu_hours": [ + "65.6161" + ], + "active_resource_count": [ + "5" + ], + "count": 1 + } + ] +} diff --git a/tests/artifacts/xdmod/rest/output/get_statistics-cs.json b/tests/artifacts/xdmod/rest/output/get_statistics-cs.json new file mode 100644 index 0000000000..1d36bf5b12 --- /dev/null +++ b/tests/artifacts/xdmod/rest/output/get_statistics-cs.json @@ -0,0 +1,230 @@ +{ + "totalCount": 1, + "success": true, + "message": "", + "formats": [ + { + "title": "Activity", + "items": [ + { + "title": "Users", + "fieldName": "active_person_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "PIs", + "fieldName": "active_pi_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Allocations", + "fieldName": "active_allocation_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Institutions", + "fieldName": "active_institution_count", + "numberType": "int", + "numberFormat": "#,#" + } + ] + }, + { + "title": "Jobs", + "items": [ + { + "title": "Total", + "fieldName": "job_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Gateway", + "fieldName": "gateway_job_count", + "numberType": "int", + "numberFormat": "#,#" + } + ] + }, + { + "title": "Service (XD SU)", + "items": [ + { + "title": "Total", + "fieldName": "total_su", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_su", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "CPU Time (h)", + "items": [ + { + "title": "Total", + "fieldName": "total_cpu_hours", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_cpu_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Wait Time (h)", + "items": [ + { + "title": "Avg (Per Job)", + "fieldName": "avg_waitduration_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Wall Time (h)", + "items": [ + { + "title": "Total", + "fieldName": "total_wallduration_hours", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_wallduration_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Processors", + "items": [ + { + "title": "Max", + "fieldName": "max_processors", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_processors", + "numberType": "int", + "numberFormat": "#,#" + } + ] + } + ], + "data": [ + { + "id": [ + "-9999" + ], + "name": [ + "Screwdriver" + ], + "short_name": [ + "Screwdriver" + ], + "order_id": [ + "Screwdriver" + ], + "job_count": [ + "71313" + ], + "running_job_count": [ + "71313" + ], + "started_job_count": [ + "71313" + ], + "submitted_job_count": [ + "64416" + ], + "active_person_count": [ + "66" + ], + "active_pi_count": [ + "41" + ], + "total_cpu_hours": [ + "840555.8369" + ], + "total_waitduration_hours": [ + "306345.3608" + ], + "total_node_hours": [ + "156303.7475" + ], + "total_wallduration_hours": [ + "127976.8064" + ], + "avg_cpu_hours": [ + "11.78685285" + ], + "sem_avg_cpu_hours": [ + "0.4535023959163972" + ], + "avg_node_hours": [ + "2.19179880" + ], + "sem_avg_node_hours": [ + "0.037808294679009785" + ], + "avg_waitduration_hours": [ + "4.29578563" + ], + "sem_avg_waitduration_hours": [ + "0.08956571094180898" + ], + "avg_wallduration_hours": [ + "1.79457892" + ], + "sem_avg_wallduration_hours": [ + "0.017929611668507588" + ], + "avg_processors": [ + "8.7346" + ], + "sem_avg_processors": [ + "0.02156312501879985" + ], + "min_processors": [ + "1" + ], + "max_processors": [ + "336" + ], + "utilization": [ + "15.919618124" + ], + "expansion_factor": [ + "3.1031" + ], + "normalized_avg_processors": [ + "0.043672753" + ], + "avg_job_size_weighted_by_cpu_hours": [ + "65.6161" + ], + "active_resource_count": [ + "5" + ], + "count": 1 + } + ] +} diff --git a/tests/artifacts/xdmod/rest/output/get_statistics-pi.json b/tests/artifacts/xdmod/rest/output/get_statistics-pi.json new file mode 100644 index 0000000000..1d36bf5b12 --- /dev/null +++ b/tests/artifacts/xdmod/rest/output/get_statistics-pi.json @@ -0,0 +1,230 @@ +{ + "totalCount": 1, + "success": true, + "message": "", + "formats": [ + { + "title": "Activity", + "items": [ + { + "title": "Users", + "fieldName": "active_person_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "PIs", + "fieldName": "active_pi_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Allocations", + "fieldName": "active_allocation_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Institutions", + "fieldName": "active_institution_count", + "numberType": "int", + "numberFormat": "#,#" + } + ] + }, + { + "title": "Jobs", + "items": [ + { + "title": "Total", + "fieldName": "job_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Gateway", + "fieldName": "gateway_job_count", + "numberType": "int", + "numberFormat": "#,#" + } + ] + }, + { + "title": "Service (XD SU)", + "items": [ + { + "title": "Total", + "fieldName": "total_su", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_su", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "CPU Time (h)", + "items": [ + { + "title": "Total", + "fieldName": "total_cpu_hours", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_cpu_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Wait Time (h)", + "items": [ + { + "title": "Avg (Per Job)", + "fieldName": "avg_waitduration_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Wall Time (h)", + "items": [ + { + "title": "Total", + "fieldName": "total_wallduration_hours", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_wallduration_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Processors", + "items": [ + { + "title": "Max", + "fieldName": "max_processors", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_processors", + "numberType": "int", + "numberFormat": "#,#" + } + ] + } + ], + "data": [ + { + "id": [ + "-9999" + ], + "name": [ + "Screwdriver" + ], + "short_name": [ + "Screwdriver" + ], + "order_id": [ + "Screwdriver" + ], + "job_count": [ + "71313" + ], + "running_job_count": [ + "71313" + ], + "started_job_count": [ + "71313" + ], + "submitted_job_count": [ + "64416" + ], + "active_person_count": [ + "66" + ], + "active_pi_count": [ + "41" + ], + "total_cpu_hours": [ + "840555.8369" + ], + "total_waitduration_hours": [ + "306345.3608" + ], + "total_node_hours": [ + "156303.7475" + ], + "total_wallduration_hours": [ + "127976.8064" + ], + "avg_cpu_hours": [ + "11.78685285" + ], + "sem_avg_cpu_hours": [ + "0.4535023959163972" + ], + "avg_node_hours": [ + "2.19179880" + ], + "sem_avg_node_hours": [ + "0.037808294679009785" + ], + "avg_waitduration_hours": [ + "4.29578563" + ], + "sem_avg_waitduration_hours": [ + "0.08956571094180898" + ], + "avg_wallduration_hours": [ + "1.79457892" + ], + "sem_avg_wallduration_hours": [ + "0.017929611668507588" + ], + "avg_processors": [ + "8.7346" + ], + "sem_avg_processors": [ + "0.02156312501879985" + ], + "min_processors": [ + "1" + ], + "max_processors": [ + "336" + ], + "utilization": [ + "15.919618124" + ], + "expansion_factor": [ + "3.1031" + ], + "normalized_avg_processors": [ + "0.043672753" + ], + "avg_job_size_weighted_by_cpu_hours": [ + "65.6161" + ], + "active_resource_count": [ + "5" + ], + "count": 1 + } + ] +} diff --git a/tests/artifacts/xdmod/rest/output/get_statistics-pub.json b/tests/artifacts/xdmod/rest/output/get_statistics-pub.json new file mode 100644 index 0000000000..1d36bf5b12 --- /dev/null +++ b/tests/artifacts/xdmod/rest/output/get_statistics-pub.json @@ -0,0 +1,230 @@ +{ + "totalCount": 1, + "success": true, + "message": "", + "formats": [ + { + "title": "Activity", + "items": [ + { + "title": "Users", + "fieldName": "active_person_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "PIs", + "fieldName": "active_pi_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Allocations", + "fieldName": "active_allocation_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Institutions", + "fieldName": "active_institution_count", + "numberType": "int", + "numberFormat": "#,#" + } + ] + }, + { + "title": "Jobs", + "items": [ + { + "title": "Total", + "fieldName": "job_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Gateway", + "fieldName": "gateway_job_count", + "numberType": "int", + "numberFormat": "#,#" + } + ] + }, + { + "title": "Service (XD SU)", + "items": [ + { + "title": "Total", + "fieldName": "total_su", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_su", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "CPU Time (h)", + "items": [ + { + "title": "Total", + "fieldName": "total_cpu_hours", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_cpu_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Wait Time (h)", + "items": [ + { + "title": "Avg (Per Job)", + "fieldName": "avg_waitduration_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Wall Time (h)", + "items": [ + { + "title": "Total", + "fieldName": "total_wallduration_hours", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_wallduration_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Processors", + "items": [ + { + "title": "Max", + "fieldName": "max_processors", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_processors", + "numberType": "int", + "numberFormat": "#,#" + } + ] + } + ], + "data": [ + { + "id": [ + "-9999" + ], + "name": [ + "Screwdriver" + ], + "short_name": [ + "Screwdriver" + ], + "order_id": [ + "Screwdriver" + ], + "job_count": [ + "71313" + ], + "running_job_count": [ + "71313" + ], + "started_job_count": [ + "71313" + ], + "submitted_job_count": [ + "64416" + ], + "active_person_count": [ + "66" + ], + "active_pi_count": [ + "41" + ], + "total_cpu_hours": [ + "840555.8369" + ], + "total_waitduration_hours": [ + "306345.3608" + ], + "total_node_hours": [ + "156303.7475" + ], + "total_wallduration_hours": [ + "127976.8064" + ], + "avg_cpu_hours": [ + "11.78685285" + ], + "sem_avg_cpu_hours": [ + "0.4535023959163972" + ], + "avg_node_hours": [ + "2.19179880" + ], + "sem_avg_node_hours": [ + "0.037808294679009785" + ], + "avg_waitduration_hours": [ + "4.29578563" + ], + "sem_avg_waitduration_hours": [ + "0.08956571094180898" + ], + "avg_wallduration_hours": [ + "1.79457892" + ], + "sem_avg_wallduration_hours": [ + "0.017929611668507588" + ], + "avg_processors": [ + "8.7346" + ], + "sem_avg_processors": [ + "0.02156312501879985" + ], + "min_processors": [ + "1" + ], + "max_processors": [ + "336" + ], + "utilization": [ + "15.919618124" + ], + "expansion_factor": [ + "3.1031" + ], + "normalized_avg_processors": [ + "0.043672753" + ], + "avg_job_size_weighted_by_cpu_hours": [ + "65.6161" + ], + "active_resource_count": [ + "5" + ], + "count": 1 + } + ] +} diff --git a/tests/artifacts/xdmod/rest/output/get_statistics-usr.json b/tests/artifacts/xdmod/rest/output/get_statistics-usr.json new file mode 100644 index 0000000000..1d36bf5b12 --- /dev/null +++ b/tests/artifacts/xdmod/rest/output/get_statistics-usr.json @@ -0,0 +1,230 @@ +{ + "totalCount": 1, + "success": true, + "message": "", + "formats": [ + { + "title": "Activity", + "items": [ + { + "title": "Users", + "fieldName": "active_person_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "PIs", + "fieldName": "active_pi_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Allocations", + "fieldName": "active_allocation_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Institutions", + "fieldName": "active_institution_count", + "numberType": "int", + "numberFormat": "#,#" + } + ] + }, + { + "title": "Jobs", + "items": [ + { + "title": "Total", + "fieldName": "job_count", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Gateway", + "fieldName": "gateway_job_count", + "numberType": "int", + "numberFormat": "#,#" + } + ] + }, + { + "title": "Service (XD SU)", + "items": [ + { + "title": "Total", + "fieldName": "total_su", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_su", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "CPU Time (h)", + "items": [ + { + "title": "Total", + "fieldName": "total_cpu_hours", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_cpu_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Wait Time (h)", + "items": [ + { + "title": "Avg (Per Job)", + "fieldName": "avg_waitduration_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Wall Time (h)", + "items": [ + { + "title": "Total", + "fieldName": "total_wallduration_hours", + "numberType": "float", + "numberFormat": "#,#.0" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_wallduration_hours", + "numberType": "float", + "numberFormat": "#,#.00" + } + ] + }, + { + "title": "Processors", + "items": [ + { + "title": "Max", + "fieldName": "max_processors", + "numberType": "int", + "numberFormat": "#,#" + }, + { + "title": "Avg (Per Job)", + "fieldName": "avg_processors", + "numberType": "int", + "numberFormat": "#,#" + } + ] + } + ], + "data": [ + { + "id": [ + "-9999" + ], + "name": [ + "Screwdriver" + ], + "short_name": [ + "Screwdriver" + ], + "order_id": [ + "Screwdriver" + ], + "job_count": [ + "71313" + ], + "running_job_count": [ + "71313" + ], + "started_job_count": [ + "71313" + ], + "submitted_job_count": [ + "64416" + ], + "active_person_count": [ + "66" + ], + "active_pi_count": [ + "41" + ], + "total_cpu_hours": [ + "840555.8369" + ], + "total_waitduration_hours": [ + "306345.3608" + ], + "total_node_hours": [ + "156303.7475" + ], + "total_wallduration_hours": [ + "127976.8064" + ], + "avg_cpu_hours": [ + "11.78685285" + ], + "sem_avg_cpu_hours": [ + "0.4535023959163972" + ], + "avg_node_hours": [ + "2.19179880" + ], + "sem_avg_node_hours": [ + "0.037808294679009785" + ], + "avg_waitduration_hours": [ + "4.29578563" + ], + "sem_avg_waitduration_hours": [ + "0.08956571094180898" + ], + "avg_wallduration_hours": [ + "1.79457892" + ], + "sem_avg_wallduration_hours": [ + "0.017929611668507588" + ], + "avg_processors": [ + "8.7346" + ], + "sem_avg_processors": [ + "0.02156312501879985" + ], + "min_processors": [ + "1" + ], + "max_processors": [ + "336" + ], + "utilization": [ + "15.919618124" + ], + "expansion_factor": [ + "3.1031" + ], + "normalized_avg_processors": [ + "0.043672753" + ], + "avg_job_size_weighted_by_cpu_hours": [ + "65.6161" + ], + "active_resource_count": [ + "5" + ], + "count": 1 + } + ] +} diff --git a/tests/artifacts/xdmod/rest/output/get_statistics_end_date_invalid-pub.json b/tests/artifacts/xdmod/rest/output/get_statistics_end_date_invalid-pub.json new file mode 100644 index 0000000000..b51c48cd52 --- /dev/null +++ b/tests/artifacts/xdmod/rest/output/get_statistics_end_date_invalid-pub.json @@ -0,0 +1,10 @@ +{ + "success": false, + "count": 0, + "total": 0, + "totalCount": 0, + "results": [], + "data": [], + "message": "Unable to parse end_date", + "code": 0 +} diff --git a/tests/artifacts/xdmod/rest/output/get_statistics_no_end_date-pub.json b/tests/artifacts/xdmod/rest/output/get_statistics_no_end_date-pub.json new file mode 100644 index 0000000000..dfb5348186 --- /dev/null +++ b/tests/artifacts/xdmod/rest/output/get_statistics_no_end_date-pub.json @@ -0,0 +1,10 @@ +{ + "success": false, + "count": 0, + "total": 0, + "totalCount": 0, + "results": [], + "data": [], + "message": "end_date is a required parameter.", + "code": 0 +} diff --git a/tests/artifacts/xdmod/rest/output/get_statistics_no_start_date-pub.json b/tests/artifacts/xdmod/rest/output/get_statistics_no_start_date-pub.json new file mode 100644 index 0000000000..b1e257a442 --- /dev/null +++ b/tests/artifacts/xdmod/rest/output/get_statistics_no_start_date-pub.json @@ -0,0 +1,10 @@ +{ + "success": false, + "count": 0, + "total": 0, + "totalCount": 0, + "results": [], + "data": [], + "message": "start_date is a required parameter.", + "code": 0 +} diff --git a/tests/artifacts/xdmod/rest/output/get_statistics_no_start_end_date-pub.json b/tests/artifacts/xdmod/rest/output/get_statistics_no_start_end_date-pub.json new file mode 100644 index 0000000000..b1e257a442 --- /dev/null +++ b/tests/artifacts/xdmod/rest/output/get_statistics_no_start_end_date-pub.json @@ -0,0 +1,10 @@ +{ + "success": false, + "count": 0, + "total": 0, + "totalCount": 0, + "results": [], + "data": [], + "message": "start_date is a required parameter.", + "code": 0 +} diff --git a/tests/artifacts/xdmod/rest/output/get_statistics_start_date_after_end-pub.json b/tests/artifacts/xdmod/rest/output/get_statistics_start_date_after_end-pub.json new file mode 100644 index 0000000000..f07d781a45 --- /dev/null +++ b/tests/artifacts/xdmod/rest/output/get_statistics_start_date_after_end-pub.json @@ -0,0 +1,10 @@ +{ + "success": false, + "count": 0, + "total": 0, + "totalCount": 0, + "results": [], + "data": [], + "message": "Start Date must not be after End Date", + "code": 0 +} diff --git a/tests/artifacts/xdmod/rest/output/get_statistics_start_date_invalid-pub.json b/tests/artifacts/xdmod/rest/output/get_statistics_start_date_invalid-pub.json new file mode 100644 index 0000000000..abdb031bc5 --- /dev/null +++ b/tests/artifacts/xdmod/rest/output/get_statistics_start_date_invalid-pub.json @@ -0,0 +1,10 @@ +{ + "success": false, + "count": 0, + "total": 0, + "totalCount": 0, + "results": [], + "data": [], + "message": "Unable to parse start_date", + "code": 0 +} diff --git a/tests/integration/lib/Rest/SummaryControllerProviderTest.php b/tests/integration/lib/Rest/SummaryControllerProviderTest.php new file mode 100644 index 0000000000..16e40cf466 --- /dev/null +++ b/tests/integration/lib/Rest/SummaryControllerProviderTest.php @@ -0,0 +1,131 @@ +helper->authenticate($username); + } + + $response = $this->helper->get('rest/v0.1/summary/statistics', $params); + if ($username !== ROLE_ID_PUBLIC) { + $this->helper->logout(); + } + + $defaultExpectedFile = "get_statistics-$username"; + $expected = \xd_utilities\array_get( + $options, + 'expected', + array( + 'file' => $defaultExpectedFile, + 'http_code' => 200, + 'content_type' => 'application/json' + ) + ); + $expectedHttpCode = \xd_utilities\array_get($expected, 'http_code', 200); + $expectedContentType = \xd_utilities\array_get($expected, 'content_type', 'application/json'); + + $this->validateResponse($response, $expectedHttpCode, $expectedContentType); + + $actual = $this->recursivelyFilter($response[0], array('query_string', 'query_time')); + + $expectedFileName = \xd_utilities\array_get($expected, 'file', $defaultExpectedFile); + $expectedFilePath = $this->getTestFiles()->getFile('rest', $expectedFileName); + + if (!is_file($expectedFilePath)) { + file_put_contents($expectedFilePath, sprintf("%s\n", json_encode($actual, JSON_PRETTY_PRINT))); + echo "Generated Expected File: $expectedFilePath\n"; + $this->assertTrue(true); + } else { + $expected = json_decode(file_get_contents($expectedFilePath), true); + + // If we have formats then the validation is a bit more complicated. + if (array_key_exists('formats', $expected)) { + + // Collect the `fieldName` => `numberType` for each of the items we expect to be + // present in the `data` section. This will let us detect when we have a `float` and + // include a delta for the equality test. + $expectedTypes = array(); + foreach ($expected['formats'] as $format) { + foreach ($format['items'] as $item) { + $expectedTypes[$item['fieldName']] = $item['numberType']; + } + } + + // These attributes can just straight up be checked for equality. + $equalAttributes = array('totalCount', 'success', 'message', 'formats'); + foreach ($equalAttributes as $attribute) { + $this->assertEquals($expected[$attribute], $actual[$attribute]); + } + + // For the `data` section we need to account for adding a `delta` to the equality test + // if it's expected that the data type is `float` or if the `$fieldName` is one of the + // `sem_` statistics. + $expectedData = $expected['data'][0]; + $actualData = $actual['data'][0]; + + foreach ($expectedData as $fieldName => $value) { + $this->assertArrayHasKey($fieldName, $actualData); + + if ((array_key_exists($fieldName, $expectedTypes) && $expectedTypes[$fieldName] === 'float') || + strpos($fieldName, 'sem_') !== false) { + // Make sure that the values we are validating are in the correct format. + $expectedValue = (float)$value[0]; + $actualValue = (float)$actualData[$fieldName][0]; + + $this->assertEquals($expectedValue, $actualValue, "Failed equivalency for: $fieldName", 1.0e-8); + } else { + $this->assertEquals($value, $actualData[$fieldName]); + } + } + } else { + $this->assertEquals($expected, $actual); + } + } + } // total_wallduration_hours + + public function provideTestGetStatistics() + { + return JSON::loadFile( + $this->getTestFiles()->getFile('rest', 'get_statistics', 'input') + ); + } + + private function recursivelyFilter(array $data, array $keys) + { + foreach ($data as $key => $value) { + if (in_array($key, $keys, true)) { + unset($data[$key]); + } elseif (is_array($value)) { + $data[$key] = $this->recursivelyFilter($value, $keys); + } + } + + return $data; + } +}