lib/util/statistics.js

Summary

Maintainability
C
1 day
Test Coverage
//////////////////////////////////////////
// Requirements                         //
//////////////////////////////////////////

var fs          = require('fs-extra');
var path        = require('path');
var execSync    = require('child_process').execSync;
var _           = require('lodash');

var semlog     = require('semlog');
var log        = semlog.log;


//////////////////////////////////////////
// CORE FUNCTIONS                       //
//////////////////////////////////////////

/**
 * Calculates statistics from the registry object
 * They will be displayed in the CLI and stored to the logfiles
 *
 * @param {{}} settings
 * @param {{}} registry
 */
exports.registryStatistics = function(settings, registry) {
    'use strict';

    exports.settings = settings;

    var s = registry.statistics; // Shortcut


    //////////////////////////////////////////
    // CALCULATING STATISTICS               //
    //////////////////////////////////////////

    s.inputStats = exports.calculateInputStatistics(registry);
    s.specificsStats = exports.calculateSpecificsStatistics(registry);
    s.graphStats = exports.calculateGraphStatistics(registry);
    s.complexity = exports.calculateComplexity(registry);
    s.log = exports.calculateLogStatistics();
    s.git = exports.calculateGitStatistics();


    //////////////////////////////////////////
    // PRINT STATISTICS                     //
    //////////////////////////////////////////

    // INPUT STATISTICS
    exports.printStatistics('IN    | ', {
        'Field': s.inputStats.field,
        'Model': s.inputStats.model,
        'Form': s.inputStats.form,
        'Template': s.inputStats.smw_template,
        'Query': s.inputStats.smw_query,
        'Page': s.inputStats.smw_page
    });

    // OUTPUT STATISTICS
    exports.printStatistics('OUT   | ', {
        'Property': s.outputStats.property,
        'Template': s.outputStats.template,
        'Form': s.outputStats.form,
        'Category': s.outputStats.category,
        'Page': s.outputStats.page
    });

    // COMPLEXITY STATISTICS
    exports.printStatistics('MODEL | ', {
        'Dev': semlog.prettyNumber(s.complexity.devModelSize),
        'Proc': semlog.prettyNumber(s.complexity.processedModelSize) +
            ' (' + s.complexity.processedModelPercentage + '%)',
        'Final': semlog.prettyNumber(s.complexity.generatedSize) +
            ' (' + s.complexity.generatedPercentage + '%)'
    });

    exports.printStatistics('SPECF | ', {
        'Intermed.': s.specificsStats.intermediaryPercentage + '%',
        'Domain': s.specificsStats.domainPercentage + '%',
        'Platform': s.specificsStats.platformPercentage + '%',
        'Implementation': s.specificsStats.implementationPercentage + '%'
    });

    // GRAPH STATISTICS
    if (settings.buildGraph) {
        exports.printStatistics('GRAPH | ', {
            'Node': s.graphStats.nodes,
            'Edge': s.graphStats.edges
        });
    }

    return registry;
};

/**
 * Calculates the input stats, counted in files
 *
 * @param registry
 *
 * @returns {{}}
 */
exports.calculateInputStatistics = function(registry) {

    // in numbers of files
    var inputStats = {
        field: Object.keys(registry.field).length,
        model: Object.keys(registry.model).length,
        form: Object.keys(registry.form).length,
        smw_template: Object.keys(registry.smw_template).length,
        smw_query: Object.keys(registry.smw_query).length,
        smw_page: Object.keys(registry.smw_page).length
    };

    // Calculate total inputStats
    inputStats.total = 0;
    for (var categoryName in inputStats) {
        inputStats.total += inputStats[categoryName];
    }

    return inputStats;
};

exports.calculateSpecificsStatistics = function(registry) {

    var moboSchema   = require('./../schema/moboSchema');
    var schema = moboSchema.formSchema.properties;

    var specificsStats = {
        intermediary: 0,
        domain: 0,
        platform: 0,
        implementation: 0
    };

    specificsStats.implementation += JSON.stringify(registry.smw_template).length;
    specificsStats.implementation += JSON.stringify(registry.smw_query).length;
    specificsStats.implementation += JSON.stringify(registry.smw_page).length;

    var todo = ['field', 'model', 'form'];
    for (var i = 0; i < todo.length; i++) {
        var todoItem = todo[i];

        for (var name in registry[todoItem]) {
            var obj =  registry[todoItem][name];
            for (var propName in obj) {

                var propValue = obj[propName];

                if (propName.charAt(0) === '$') {
                    specificsStats.intermediary += JSON.stringify(propValue).length;
                } else if (schema[propName] && schema[propName].specific === 'platform') {
                    specificsStats.platform += JSON.stringify(propValue).length;
                } else if (schema[propName] && schema[propName].specific === 'implementation') {
                    specificsStats.implementation += JSON.stringify(propValue).length;
                } else {
                    specificsStats.domain += JSON.stringify(propValue).length;
                }
            }
        }
    }

    specificsStats.total = specificsStats.intermediary +
        specificsStats.domain +
        specificsStats.platform +
        specificsStats.implementation;

    specificsStats.intermediaryPercentage = Math.round(
        (specificsStats.intermediary / specificsStats.total) * 1000
    ) / 10;

    specificsStats.domainPercentage = Math.round(
        (specificsStats.domain / specificsStats.total) * 1000
    ) / 10;

    specificsStats.platformPercentage = Math.round(
        (specificsStats.platform / specificsStats.total) * 1000
    ) / 10;

    specificsStats.implementationPercentage = Math.round(
        (specificsStats.implementation / specificsStats.total) * 1000
    ) / 10;

    return specificsStats;
};

/**
 * Calculates the graph stats, counted in number of nodes / edges
 *
 * @param registry
 *
 * @returns {{}}
 */
exports.calculateGraphStatistics = function(registry) {

    return {
        nodes: _.size(registry.graph.nodes) || 0,
        edges: _.size(registry.graph.edges) || 0
    };
};

/**
 * Calculates the project complexity, counted in characters
 *
 * @param registry
 *
 * @returns {{}}
 */
exports.calculateComplexity = function(registry) {

    // In number of characters
    var complexity = {
        devModelSize: registry.statistics.inputSize.total,
        processedModelSize: JSON.stringify({
            // Only count the expandedForms, since they already include the expanded fields and models
            expandedForm: registry.expandedForm,
            smw_template: registry.smw_template,
            smw_query: registry.smw_query,
            smw_page: registry.smw_page
        }).length,
        generatedSize: JSON.stringify(registry.generated).length
    };

    // Calculate the difference to the development model in percentage
    complexity.processedModelPercentage = Math.round(
        (complexity.processedModelSize / complexity.devModelSize) * 1000
    ) / 10;
    complexity.generatedPercentage = Math.round(
        (complexity.generatedSize / complexity.devModelSize) * 1000
    ) / 10;

    return complexity;
};

/**
 * Calculates the logging statistics, counted in number of log messages
 *
 * @returns {{}}
 */
exports.calculateLogStatistics = function() {

    var logHistory = semlog.getLogHistory();
    var logStats = {
        total: logHistory.length,
        warning: 0,
        error: 0,
        todo: 0
    };

    for (var i = 0; i < logHistory.length; i++) {
        var entry = logHistory[i];
        if (typeof entry === 'string') {
            if (entry.indexOf('[E] ') > -1) {
                logStats.error += 1;
            } else if (entry.indexOf('[W] ') > -1) {
                logStats.warning += 1;
            } else if (entry.indexOf('[TODO] ') > -1) {
                logStats.todo += 1;
            }
        }
    }

    return logStats;
};

/**
 * Returns some git statistics / informations about the current revision
 *
 * @returns {{}}
 */
exports.calculateGitStatistics = function() {

    var git = {
        logMessage: '',
        shortHash: '',
        timestamp: ''
    };

    // Supported by Node.js >= 0.12
    if (exports.settings.gitStatistics && execSync) {

        try {
            git.logMessage = execSync('git log -1 --pretty=%B').toString().trim();
            git.shortHash = execSync('git rev-parse --short HEAD').toString().trim();
            git.timestamp = execSync('git show -s --format=%ct').toString().trim();
        } catch (e) {
            log('[i] Could not find git, skipping git statistics');
        }
    }

    return git;
};

/**
 * Prints a statistic object to the console output
 * @param prefix
 * @param statObj
 */
exports.printStatistics = function(prefix, statObj) {

    var print = prefix;
    var separator = ' | ';
    for (var key in statObj) {
        var value = statObj[key];
        print += value + ' ' + key + separator;
    }

    log(print.substring(0, print.length - separator.length)); // Omit last separator
    log('--------------------------------------------------------------------------------');
};

/**
 * Writes the statistics object to the logfile and a CSV history
 *
 * @param {{}}      statistics
 * @param {string}  directory
 */
exports.writeStatistics = function(statistics, directory) {

    // To Logobject / report
    log(statistics, true);

    // To CSV File (/_processed/_statistics.csv)
    var statisticsCsvPath = path.join(directory, '/_statistics.csv');

    var header = [];
    var content = [];

    var statisticsSortedOrder = Object.keys(statistics).sort();

    // Convert the statistics object to two flattened arrays
    // They will be used to generate the csv file
    // Categories and subcategories will be sorted alphabetically
    for (var i = 0; i < statisticsSortedOrder.length; i++) {
        var statCategory = statisticsSortedOrder[i];
        var statSortedOrder = Object.keys(statistics[statCategory]).sort();

        for (var j = 0; j < statSortedOrder.length; j++) {
            var statName = statSortedOrder[j];
            var statValue = statistics[statCategory][statName];

            // If it is a number, use ',' as the floating point
            if (!isNaN(statValue) && statValue.toString().indexOf('.') > -1) {
                statValue = statValue.toString();
                statValue = statValue.split('.').join(',');
            }

            header.push('"' + statCategory + '_' + statName + '"');
            content.push('"' + statValue + '"'); // Use , for separating numbers in excel
        }
    }

    // Write / append statistics to historical CSV file
    try {

        // If file does not exist, create it with the header first
        if (!fs.existsSync(statisticsCsvPath)) {
            fs.outputFileSync(statisticsCsvPath, header.join(';') + '\n');
        }
        // Append to CSV Value
        var file = fs.openSync(statisticsCsvPath, 'a'); // Append only
        fs.writeSync(file, content.join(';') + '\n');
        fs.closeSync(file);

    } catch (e) {
        log('[E] Could not write statistics to ' + statisticsCsvPath);
    }
};