Skip to content

Commit

Permalink
Merge pull request #74 from vzDevelopment/vz/unit-tests
Browse files Browse the repository at this point in the history
Add unit tests to Fn UI
  • Loading branch information
rdallman authored Jul 8, 2019
2 parents 6edfa3f + ca76e29 commit 55d8894
Show file tree
Hide file tree
Showing 12 changed files with 593 additions and 233 deletions.
8 changes: 7 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ jobs:
name: "Lint the code"
command: |
npm install --loglevel error --only dev
./node_modules/.bin/eslint .
npm run lint
- run:
name: "Run the unit tests"
command: |
npm install --loglevel silent --no-save
npm test
- run:
name: "Update Docker"
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"private": true,
"scripts": {
"start": "node server",
"lint": "./node_modules/.bin/eslint .",
"test": "./node_modules/.bin/mocha test/test_*.js",
"test-integration": "./node_modules/.bin/mocha test_integration/test_*.js"
},
"dependencies": {
Expand Down
235 changes: 3 additions & 232 deletions server/controllers/stats.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
var express = require('express');
var router = express.Router();

var AppStatsParser = require('../helpers/app-stats-parser.js');
var GeneralStatsParser = require('../helpers/general-stats-parser.js');
var helpers = require('../helpers/app-helpers.js');
var logger = require('config-logger');

router.get('/', function(req, res) {
var successcb = function(data){
Expand All @@ -23,235 +25,4 @@ router.get('/', function(req, res) {

});

/**
* The Stats Parser class provides base functionality that individual stats
* parsers can reuse. It's designed for individual parsers to inherit from.
*/
class StatsParser {
constructor() {
// Captures the key in a Javascript Object Literal (i.e. everything before the equals sign)
var labelNameRE = '([^=]+)';

// Captures the value in a Javascript Object Literal (i.e. anything between the double quotes)
var labelValueRE = '"([^"]*)"';

// Matches the key/value pair in a Javascript Object Literal
this._labelRE = labelNameRE + '=' + labelValueRE;

this._spacesRE = '\\s+';
this._valueRE = '(\\d+)';
}
}

/**
* This class is used to parse Javascript literal object data e.g.
* {appId="01FDACB01",fnID="01FDACC02",image="fnproject/hello"}
*/
class LabelParser extends StatsParser {
constructor() {
super();

this._regex = RegExp(this._labelRE + ',?', 'gm');

this._WHOLE_MATCH = 0;
this._LABEL_KEY = 1;
this._LABEL_VALUE = 2;
}

parse(data) {
// Remove the start and end curly braces as they're not part of the data
data = data.replace(/^{|}$/g, '');

var jsonData = {};

var labelData;
while((labelData = this._regex.exec(data)) !== null) {
var labelKey = labelData[this._LABEL_KEY];
var labelValue = labelData[this._LABEL_VALUE];

jsonData[labelKey] = labelValue;
}

return jsonData;
}
}

/**
* This parser is used to parse the fn_queue, fn_running and fn_complete data
* from the Fn server's metrics API. This data isn't app specific, but is for
* the whole system.
*/
class GeneralStatsParser extends StatsParser {
constructor() {
super();

this._metricNames = {
'fn_queued': 'Queue',
'fn_running': 'Running',
'fn_completed': 'Complete',
};

var metricNameRE = '(' + Object.keys(this._metricNames).join('|') + ')';
var metricsRE = '^' + metricNameRE + this._spacesRE + this._valueRE;

this._regex = RegExp(metricsRE, 'gm');

//regexMatch[0] = the whole match
//regexMatch[1] = metric name (e.g. fn_completed)
//regexMatch[2] = metric value (integer)
this._WHOLE_MATCH = 0;
this._METRIC_NAME = 1;
this._METRIC_VALUE = 2;
}

/*
* Parse the Fn server stats data and return an object containing the
* results.
*
* Example data structure for object being returned:
* Complete: 3
* Queue: 0
* Running: 1
*
* @param {String} data the data to parse.
*
* @return {Object} an object representing the parsed data as per the
* documentation above.
*/
parse(data) {
var jsonData = {};

var metricData;
while((metricData = this._regex.exec(data)) !== null) {
logger.debug(
"Processing General Stat: " + metricData[this._WHOLE_MATCH]
);

var metricsName = metricData[this._METRIC_NAME];
var metricsHumanName = this._metricNames[metricsName];
var metricsValue = parseInt(metricData[this._METRIC_VALUE]);

jsonData[metricsHumanName] = metricsValue;
}

return jsonData;
}
}

/**
* This class is used to parse app specific stats from the Fn server's metrics
* API.
*/
class AppStatsParser extends StatsParser {
constructor() {
super();

this._metricNames = {
'fn_container_start_total': 'Starting',
'fn_container_busy_total': 'Busy',
'fn_container_idle_total': 'Idling',
'fn_container_paused_total': 'Paused',
'fn_container_wait_total': 'Waiting',
};

var metricNameRE = '(' + Object.keys(this._metricNames).join('|') + ')';

// Match fn container info e.g. {app_id="01D8JQSKDENG8G00GZJ000000B"}
var fnDataRE = '{' + '[^}]+' + '}';

// unfortunately we cannot use ((?:, labelRE)+) as it won't capture the
// middle key-value pair
var metricsRE = '^' + metricNameRE + '(' + fnDataRE + ')' + this._spacesRE + this._valueRE;

this._regex = RegExp(metricsRE, 'gm');

//regexMatch[0] = the whole match
//regexMatch[1] = metric name (e.g. fn_container_busy_total)
//regexMatch[2] = fn data in javascript object notation (e.g. {app_id="01D7"})
//regexMatch[3] = metric value (integer)
this._WHOLE_MATCH = 0;
this._METRIC_NAME = 1;
this._FN_DATA = 2;
this._METRIC_VALUE = 3;
}

/*
* Parse the stats data and return an object containing the app specific
* stats.
*
* Example data structure for object being returned:
* 01D8JQSKDENG8G00GZJ000000B
* Functions
* 01D8JQSQ2VNG8G00GZJ000000C
* Busy: 1
* Idling: 0
* Paused: 0
* Starting: 0
* Waiting: 0
* 01D8JQSQ2VNG8G00GZJ000000D
* Busy: 0
* Idling: 0
* Paused: 0
* Starting: 0
* Waiting: 1
*
* @param {String} data the data to parse.
*
* @return {Object} an object representing the parsed data as per the
* documentation above.
*/
parse(data) {
var jsonData = {};

var labelParser = new LabelParser();
var metricData;
while((metricData = this._regex.exec(data)) !== null) {
logger.debug("Processing App Stat: " + metricData[0]);

var metricsName = metricData[this._METRIC_NAME];
var metricsHumanName = this._metricNames[metricsName];
var metricsValue = parseInt(metricData[this._METRIC_VALUE]);

var rawFnData = metricData[this._FN_DATA];
var fnData = labelParser.parse(rawFnData);

jsonData = this._addData(jsonData, fnData.app_id, fnData.fn_id,
metricsHumanName, metricsValue
);
}

return jsonData;
}

/**
* Adds App Data to the object that we're going to return.
*
* @param {Object} data the object to append the data to.
* @param {String} appId the ID of the Fn App which this data belongs to.
* @param {String} fnId the ID of the Fn function which this data belongs to.
* @param {String} metricsHumanName the human readable name of the metric being recorded.
* @param {Int} metricsValue the value of the metric that was parsed.
*
* @return {Object} the data object with the app data added.
*/
_addData(data, appId, fnId, metricsHumanName, metricsValue) {
if(data[appId] === undefined) {
data[appId] = {'Functions': {}};
}

if(data[appId].Functions[fnId] === undefined) {
data[appId].Functions[fnId] = {};
}

// Aggregate data for all fn images
if(metricsHumanName in data[appId].Functions[fnId]) {
data[appId].Functions[fnId][metricsHumanName] += metricsValue;
} else {
data[appId].Functions[fnId][metricsHumanName] = metricsValue;
}

return data;
}
}

module.exports = router;
120 changes: 120 additions & 0 deletions server/helpers/app-stats-parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
const logger = require('config-logger');

const LabelParser = require('./label-parser.js');
const StatsParser = require('./stats-parser.js');

/**
* This class is used to parse app specific stats from the Fn server's metrics
* API.
*/
module.exports = class AppStatsParser extends StatsParser {
constructor() {
super();

this._metricNames = {
'fn_container_start_total': 'Starting',
'fn_container_busy_total': 'Busy',
'fn_container_idle_total': 'Idling',
'fn_container_paused_total': 'Paused',
'fn_container_wait_total': 'Waiting',
};

var metricNameRE = '(' + Object.keys(this._metricNames).join('|') + ')';

// Match fn container info e.g. {app_id="01D8JQSKDENG8G00GZJ000000B"}
var fnDataRE = '{' + '[^}]+' + '}';

// unfortunately we cannot use ((?:, labelRE)+) as it won't capture the
// middle key-value pair
var metricsRE = '^' + metricNameRE + '(' + fnDataRE + ')' + this._spacesRE + this._valueRE;

this._regex = RegExp(metricsRE, 'gm');

//regexMatch[0] = the whole match
//regexMatch[1] = metric name (e.g. fn_container_busy_total)
//regexMatch[2] = fn data in javascript object notation (e.g. {app_id="01D7"})
//regexMatch[3] = metric value (integer)
this._WHOLE_MATCH = 0;
this._METRIC_NAME = 1;
this._FN_DATA = 2;
this._METRIC_VALUE = 3;
}

/*
* Parse the stats data and return an object containing the app specific
* stats.
*
* Example data structure for object being returned:
* 01D8JQSKDENG8G00GZJ000000B
* Functions
* 01D8JQSQ2VNG8G00GZJ000000C
* Busy: 1
* Idling: 0
* Paused: 0
* Starting: 0
* Waiting: 0
* 01D8JQSQ2VNG8G00GZJ000000D
* Busy: 0
* Idling: 0
* Paused: 0
* Starting: 0
* Waiting: 1
*
* @param {String} data the data to parse.
*
* @return {Object} an object representing the parsed data as per the
* documentation above.
*/
parse(data) {
var jsonData = {};

var labelParser = new LabelParser();
var metricData;
while((metricData = this._regex.exec(data)) !== null) {
logger.debug("Processing App Stat: " + metricData[0]);

var metricsName = metricData[this._METRIC_NAME];
var metricsHumanName = this._metricNames[metricsName];
var metricsValue = parseInt(metricData[this._METRIC_VALUE]);

var rawFnData = metricData[this._FN_DATA];
var fnData = labelParser.parse(rawFnData);

jsonData = this._addData(jsonData, fnData.app_id, fnData.fn_id,
metricsHumanName, metricsValue
);
}

return jsonData;
}

/**
* Adds App Data to the object that we're going to return.
*
* @param {Object} data the object to append the data to.
* @param {String} appId the ID of the Fn App which this data belongs to.
* @param {String} fnId the ID of the Fn function which this data belongs to.
* @param {String} metricsHumanName the human readable name of the metric being recorded.
* @param {Int} metricsValue the value of the metric that was parsed.
*
* @return {Object} the data object with the app data added.
*/
_addData(data, appId, fnId, metricsHumanName, metricsValue) {
if(data[appId] === undefined) {
data[appId] = {'Functions': {}};
}

if(data[appId].Functions[fnId] === undefined) {
data[appId].Functions[fnId] = {};
}

// Aggregate data for all fn images
if(metricsHumanName in data[appId].Functions[fnId]) {
data[appId].Functions[fnId][metricsHumanName] += metricsValue;
} else {
data[appId].Functions[fnId][metricsHumanName] = metricsValue;
}

return data;
}
};
Loading

0 comments on commit 55d8894

Please sign in to comment.