Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unit tests to Fn UI #74

Merged
merged 4 commits into from
Jul 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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