Procházet zdrojové kódy

Implements a general score calculator

Gaël Métais před 10 roky
rodič
revize
792f7d8e21

+ 1 - 1
Gruntfile.js

@@ -88,7 +88,7 @@ module.exports = function(grunt) {
                 options: {
                     reporter: 'spec',
                 },
-                src: ['coverage/test/api/apiTest.js']
+                src: ['coverage/test/core/scoreCalculatorTest.js']
             },
             coverage: {
                 options: {

+ 0 - 1
lib/rulesChecker.js

@@ -7,7 +7,6 @@ var RulesChecker = function() {
     this.check = function(data, policies) {
 
         var results = {};
-        var err = null;
 
         debug('Starting checking rules');
 

+ 24 - 4
lib/runner.js

@@ -1,8 +1,10 @@
-var Q = require('q');
-var debug = require('debug')('ylt:yellowlabtools');
+var Q                   = require('q');
+var debug               = require('debug')('ylt:runner');
+
+var phantomasWrapper    = require('./tools/phantomasWrapper');
+var rulesChecker        = require('./rulesChecker');
+var scoreCalculator     = require('./scoreCalculator');
 
-var phantomasWrapper = require('./tools/phantomasWrapper');
-var rulesChecker = require('./rulesChecker');
 
 var Runner = function(params) {
     'use strict';
@@ -27,6 +29,24 @@ var Runner = function(params) {
         data.rules = rulesChecker.check(data, policies);
 
 
+        // Scores calculator
+        var scoreProfileGeneric = require('./metadata/scoreProfileGeneric.json');
+        data.scoreProfiles = {
+            generic : scoreCalculator.calculate(data, scoreProfileGeneric)
+        };
+
+
+        // Get the JS Execution Tree from offenders and put in the main object
+        try {
+            data.javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);    
+        } catch(e) {
+            debug('Could not find nor parse phantomas.offenders.javascriptExecutionTree');
+        }
+        
+        delete data.toolsResults.phantomas.metrics.javascriptExecutionTree;
+        delete data.toolsResults.phantomas.offenders.javascriptExecutionTree;
+
+        //Finished!
         deferred.resolve(data);
 
     }).fail(function(err) {

+ 83 - 0
lib/scoreCalculator.js

@@ -0,0 +1,83 @@
+var Q = require('q');
+var debug = require('debug')('ylt:scoreCalculator');
+
+var ScoreCalculator = function() {
+    'use strict';
+
+    this.calculate = function(data, profile) {
+
+        var results = {
+            categories: {}
+        };
+        var weight;
+        var categoryName;
+
+        debug('Starting calculating scores');
+
+        // Calculate categories
+        for (categoryName in profile.categories) {
+            var categoryResult = {
+                label: profile.categories[categoryName].label
+            };
+
+            var sum = 0;
+            var totalWeight = 0;
+            var rules = [];
+
+            for (var policyName in profile.categories[categoryName].policies) {
+                weight = profile.categories[categoryName].policies[policyName];
+                
+                if (data.rules[policyName]) {
+                    sum += data.rules[policyName].score * weight;
+                } else {
+                    // Max value if rule is not here
+                    sum += 100 * weight;
+                    debug('Warning: could not find rule %s', policyName);
+                }
+
+                totalWeight += weight;
+                rules.push(policyName);
+            }
+
+            if (totalWeight === 0) {
+                categoryResult.categoryScore = 100;
+            } else {
+                categoryResult.categoryScore = Math.round(sum / totalWeight);
+            }
+
+            categoryResult.rules = rules;
+            results.categories[categoryName] = categoryResult;
+        }
+
+
+        // Calculate general score
+        var globalSum = 0;
+        var globalTotalWeight = 0;
+
+        for (categoryName in profile.globalScore) {
+            weight = profile.globalScore[categoryName];
+
+            if (results.categories[categoryName]) {
+                globalSum += results.categories[categoryName].categoryScore * weight;
+            } else {
+                globalSum += 100 * weight;
+            }
+            globalTotalWeight += profile.globalScore[categoryName];
+        }
+
+        if (globalTotalWeight === 0) {
+            results.globalScore = 100;
+        } else {
+            results.globalScore = Math.round(globalSum / globalTotalWeight);
+        }
+
+
+
+        debug('Score calculation finished:');
+        debug(results);
+
+        return results;
+    };
+};
+
+module.exports = new ScoreCalculator();

+ 2 - 2
lib/server/middlewares/apiLimitsMiddleware.js

@@ -16,7 +16,7 @@ var apiLimitsMiddleware = function(req, res, next) {
             if (!runsTable.accepts(req.connection.remoteAddress)) {
                 // Sorry :/
                 debug('Too many tests launched from IP address %s', req.connection.remoteAddress);
-                res.status(429).send('Too Many Requests');
+                res.status(429).send('Too many requests');
                 return;
             }
 
@@ -25,7 +25,7 @@ var apiLimitsMiddleware = function(req, res, next) {
         if (!callsTable.accepts(req.connection.remoteAddress)) {
             // Sorry :/
             debug('Too many API requests from IP address %s', req.connection.remoteAddress);
-            res.status(429).send('Too Many Requests');
+            res.status(429).send('Too many requests');
             return;
         }
 

+ 202 - 8
test/api/apiTest.js

@@ -8,25 +8,30 @@ var config = {
     }
 };
 
-var apiUrl = 'http://localhost:8387/api';
+var serverUrl = 'http://localhost:8387';
 var wwwUrl = 'http://localhost:8388';
 
 describe('api', function() {
 
-    var runId;
+
+    var syncRunResultUrl;
+    var asyncRunId;
     var apiServer;
 
+
+    // Start the server
     before(function(done) {
         apiServer = require('../../bin/server.js');
         apiServer.startTests = done;
     });
 
+
     it('should refuse a query with an invalid key', function(done) {
         this.timeout(5000);
 
         request({
             method: 'POST',
-            url: apiUrl + '/runs',
+            url: serverUrl + '/api/runs',
             body: {
                 url: wwwUrl + '/simple-page.html',
                 waitForResponse: false
@@ -44,12 +49,67 @@ describe('api', function() {
         });
     });
 
-    it('should accept a query with a valid key', function(done) {
+
+    it('should launch a synchronous run', function(done) {
+        this.timeout(15000);
+
+        request({
+            method: 'POST',
+            url: serverUrl + '/api/runs',
+            body: {
+                url: wwwUrl + '/simple-page.html',
+                waitForResponse: true
+            },
+            json: true,
+            headers: {
+                'X-Api-Key': Object.keys(config.authorizedKeys)[0]
+            }
+        }, function(error, response, body) {
+            if (!error && response.statusCode === 302) {
+
+                response.headers.should.have.a.property('location').that.is.a('string');
+                syncRunResultUrl = response.headers.location;
+
+                done();
+            } else {
+                done(error || response.statusCode);
+            }
+        });
+    });
+
+
+    it('should retrieve the results for the synchronous run', function(done) {
+        this.timeout(15000);
+
+        request({
+            method: 'GET',
+            url: serverUrl + syncRunResultUrl,
+            json: true,
+        }, function(error, response, body) {
+            if (!error && response.statusCode === 200) {
+
+                body.should.have.a.property('runId').that.is.a('string');
+                body.should.have.a.property('params').that.is.an('object');
+                body.should.have.a.property('scoreProfiles').that.is.an('object');
+                body.should.have.a.property('rules').that.is.an('object');
+                body.should.have.a.property('toolsResults').that.is.an('object');
+                body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
+
+                done();
+
+            } else {
+                done(error || response.statusCode);
+            }
+        });
+    });
+
+
+    it('should launch a run without waiting for the response', function(done) {
         this.timeout(5000);
 
         request({
             method: 'POST',
-            url: apiUrl + '/runs',
+            url: serverUrl + '/api/runs',
             body: {
                 url: wwwUrl + '/simple-page.html',
                 waitForResponse: false
@@ -61,8 +121,35 @@ describe('api', function() {
         }, function(error, response, body) {
             if (!error && response.statusCode === 200) {
 
-                runId = body.runId;
-                runId.should.be.a('string');
+                asyncRunId = body.runId;
+                asyncRunId.should.be.a('string');
+                done();
+
+            } else {
+                done(error || response.statusCode);
+            }
+        });
+    });
+
+
+    it('should respond run status: running', function(done) {
+        this.timeout(5000);
+
+        request({
+            method: 'GET',
+            url: serverUrl + '/api/runs/' + asyncRunId,
+            json: true,
+            headers: {
+                'X-Api-Key': Object.keys(config.authorizedKeys)[0]
+            }
+        }, function(error, response, body) {
+            if (!error && response.statusCode === 200) {
+
+                body.runId.should.equal(asyncRunId);
+                body.status.should.deep.equal({
+                    statusCode: 'running'
+                });
+
                 done();
 
             } else {
@@ -79,13 +166,16 @@ describe('api', function() {
 
             request({
                 method: 'POST',
-                url: apiUrl + '/runs',
+                url: serverUrl + '/api/runs',
                 body: {
                     url: wwwUrl + '/simple-page.html',
                     waitForResponse: false
                 },
                 json: true
             }, function(error, response, body) {
+
+                lastRunId = body.runId;
+
                 if (error) {
                     deferred.reject(error);
                 } else {
@@ -131,6 +221,110 @@ describe('api', function() {
         
     });
 
+
+    it('should respond 404 to unknown runId', function(done) {
+        this.timeout(5000);
+
+        request({
+            method: 'GET',
+            url: serverUrl + '/api/runs/unknown',
+            json: true
+        }, function(error, response, body) {
+            if (!error && response.statusCode === 404) {
+                done();
+            } else {
+                done(error || response.statusCode);
+            }
+        });
+    });
+
+
+    it('should respond 404 to unknown result', function(done) {
+        this.timeout(5000);
+
+        request({
+            method: 'GET',
+            url: serverUrl + '/api/results/unknown',
+            json: true
+        }, function(error, response, body) {
+            if (!error && response.statusCode === 404) {
+                done();
+            } else {
+                done(error || response.statusCode);
+            }
+        });
+    });
+
+    
+    it('should respond status complete to the first run', function(done) {
+        this.timeout(12000);
+
+        function checkStatus() {
+            request({
+                method: 'GET',
+                url: serverUrl + '/api/runs/' + asyncRunId,
+                json: true
+            }, function(error, response, body) {
+                if (!error && response.statusCode === 200) {
+
+                    body.runId.should.equal(asyncRunId);
+                    
+                    if (body.status.statusCode === 'running') {
+                        setTimeout(checkStatus, 250);
+                    } else if (body.status.statusCode === 'complete') {
+                        done();
+                    } else {
+                        done(body.status.statusCode);
+                    }
+
+                } else {
+                    done(error || response.statusCode);
+                }
+            });
+        }
+
+        checkStatus();
+    });
+
+
+    it('should find the result of the async run', function(done) {
+        this.timeout(5000);
+
+        request({
+            method: 'GET',
+            url: serverUrl + '/api/results/' + asyncRunId,
+            json: true,
+        }, function(error, response, body) {
+            if (!error && response.statusCode === 200) {
+
+                body.should.have.a.property('runId').that.equals(asyncRunId);
+                body.should.have.a.property('params').that.is.an('object');
+                body.params.url.should.equal(wwwUrl + '/simple-page.html');
+
+                body.should.have.a.property('scoreProfiles').that.is.an('object');
+                body.scoreProfiles.should.have.a.property('generic').that.is.an('object');
+                body.scoreProfiles.generic.should.have.a.property('globalScore').that.is.a('number');
+                body.scoreProfiles.generic.should.have.a.property('categories').that.is.an('object');
+
+                body.should.have.a.property('rules').that.is.an('object');
+
+                body.should.have.a.property('toolsResults').that.is.an('object');
+                body.toolsResults.should.have.a.property('phantomas').that.is.an('object');
+
+                body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
+                body.javascriptExecutionTree.should.have.a.property('data').that.is.an('object');
+                body.javascriptExecutionTree.data.should.have.a.property('type').that.equals('main');
+
+                done();
+
+            } else {
+                done(error || response.statusCode);
+            }
+        });
+    });
+
+
+    // Stop the server
     after(function() {
         console.log('Closing the server');
         apiServer.close();

+ 18 - 0
test/core/scoreCalculatorTest.js

@@ -0,0 +1,18 @@
+var should = require('chai').should();
+var scoreCalculator = require('../../lib/scoreCalculator');
+
+describe('scoreCalculator', function() {
+    
+    it('should have a method calculate', function() {
+        scoreCalculator.should.have.property('calculate').that.is.a('function');
+    });
+    
+    it('should produce a nice rules object', function() {
+        var data = require('../fixtures/scoreInput.json');
+        var profile = require('../fixtures/scoreProfile.json');
+        var expected = require('../fixtures/scoreOutput.json');
+
+        var results = scoreCalculator.calculate(data, profile);
+        results.should.deep.equals(expected);
+    });
+});

+ 11 - 4
test/core/yellowlabtoolsTest.js

@@ -9,28 +9,28 @@ chai.use(sinonChai);
 
 describe('yellowlabtools', function() {
 
-    it('returns a promise', function() {
+    it('should return a promise', function() {
         var ylt = new YellowLabTools();
 
         ylt.should.have.property('then').that.is.a('function');
         ylt.should.have.property('fail').that.is.a('function');
     });
 
-    it('fails an undefined url', function(done) {
+    it('should fail an undefined url', function(done) {
         var ylt = new YellowLabTools().fail(function(err) {
             err.should.be.a('string').that.equals('URL missing');
             done();
         });
     });
 
-    it('fails with an empty url string', function(done) {
+    it('should fail with an empty url string', function(done) {
         var ylt = new YellowLabTools('').fail(function(err) {
             err.should.be.a('string').that.equals('URL missing');
             done();
         });
     });
 
-    it('succeeds on simple-page.html', function(done) {
+    it('should succeeds on simple-page.html', function(done) {
         this.timeout(15000);
 
         // Check if console.log is called
@@ -75,6 +75,13 @@ describe('yellowlabtools', function() {
                     "offenders": ["body > h1[1]"]
                 });
 
+                // Test javascriptExecutionTree
+                data.toolsResults.phantomas.metrics.should.not.have.a.property('javascriptExecutionTree');
+                data.toolsResults.phantomas.offenders.should.not.have.a.property('javascriptExecutionTree');
+                data.should.have.a.property('javascriptExecutionTree').that.is.an('object');
+                data.javascriptExecutionTree.should.have.a.property('data');
+                data.javascriptExecutionTree.data.should.have.a.property('type').that.equals('main');
+
                 /*jshint expr: true*/
                 console.log.should.not.have.been.called;
 

+ 68 - 0
test/fixtures/scoreInput.json

@@ -0,0 +1,68 @@
+{
+    "rules": {
+        "metric1": {
+            "policy": {
+                "tool": "tool1",
+                "label": "The metric 1",
+                "message": "A great message",
+                "isOkThreshold": 1000,
+                "isBadThreshold": 3000,
+                "isAbnormalThreshold": 5000
+            },
+            "value": 1236,
+            "bad": true,
+            "abnormal": false,
+            "score": 88,
+            "abnormalityScore": 0
+        },
+        "metric2": {
+            "value": 222,
+            "bad": false,
+            "abnormal": false,
+            "score": 100,
+            "abnormalityScore": 0
+        },
+        "metric3": {
+            "value": 6666,
+            "bad": true,
+            "abnormal": true,
+            "score": 0,
+            "abnormalityScore": -42
+        },
+        "metric4": {
+            "value": 1000,
+            "bad": false,
+            "abnormal": false,
+            "score": 100,
+            "abnormalityScore": 0
+        },
+        "metric5": {
+            "value": 3000,
+            "bad": true,
+            "abnormal": false,
+            "score": 0,
+            "abnormalityScore": 0
+        },
+        "metric6": {
+            "value": 0,
+            "bad": false,
+            "abnormal": false,
+            "score": 100,
+            "abnormalityScore": 0
+        },
+        "metric7": {
+            "value": 5000,
+            "bad": true,
+            "abnormal": true,
+            "score": 0,
+            "abnormalityScore": 0
+        },
+        "metric8": {
+            "value": 22,
+            "bad": true,
+            "abnormal": true,
+            "score": 0,
+            "abnormalityScore": -100
+        }
+    }
+}

+ 34 - 0
test/fixtures/scoreOutput.json

@@ -0,0 +1,34 @@
+{
+    "globalScore": 69,
+    "categories": {
+        "category1": {
+            "label": "Category 1",
+            "categoryScore": 87,
+            "rules": [
+                "metric1",
+                "metric2",
+                "metric3",
+                "metric4"
+            ]
+        },
+        "category2": {
+            "label": "Category 2",
+            "categoryScore": 31,
+            "rules": [
+                "metric5",
+                "metric6",
+                "metric7",
+                "metric8",
+                "unexistantMetric1"
+            ]
+        },
+        "category3": {
+            "label": "Category 3",
+            "categoryScore": 100,
+            "rules": [
+                "unexistantMetric1",
+                "unexistantMetric2"
+            ]
+        }
+    }
+}

+ 35 - 0
test/fixtures/scoreProfile.json

@@ -0,0 +1,35 @@
+{
+    "categories": {
+        "category1": {
+            "label": "Category 1",
+            "policies": {
+                "metric1": 2,
+                "metric2": 1,
+                "metric3": 0.5,
+                "metric4": 2
+            }
+        },
+        "category2": {
+            "label": "Category 2",
+            "policies": {
+                "metric5": 2,
+                "metric6": 1,
+                "metric7": 0.5,
+                "metric8": 2,
+                "unexistantMetric1": 1
+            }
+        },
+        "category3": {
+            "label": "Category 3",
+            "policies": {
+                "unexistantMetric1": 2,
+                "unexistantMetric2": 1
+            }
+        }
+    },
+    "globalScore": {
+        "category1": 2,
+        "category2": 1,
+        "category3": 0.1
+    }
+}