finish/sanityChecks.js

'use strict';

/**
 * #### Database Sanity Checks
 *
 * This function performs a set list of sanity checks on the compiled
 * agency database.  If any of these checks fail, the function will add the
 * error to the DB compilation exceptions and return false in the callback.
 * @module finish/sanityChecks
 */

const fs = require('fs');
const path = require('path');
const errors = require('../helpers/errors.js');
const log = require('../helpers/log.js');
const config = require('../../config.json');


// Min File Sizes (Bytes)
const MIN_DB_SIZE = 500000;
const MIN_ZIP_SIZE = 250000;


/**
 * List of sanity checks to perform on the agency database.  Each object in this
 * array contains three properties that define the sanity check.
 * @type {object[]} List of Sanity Checks
 * @property {string} name Check Name
 * @property {string} select SQL Select statement to perform
 * @property {function} test Function that accepts a single DB row and returns a boolean (pass/fail)
 * @property {boolean} test.row SQLite DB Row object returned from the SELECT query
 */
const CHECKS = [
  {
    "name": "agency count",
    "select": "SELECT COUNT(agency_id) AS count FROM " + config.tables.gtfs.agency + ";",
    "test": function(row) {
      return row.count > 0;
    }
  },
  {
    "name": "calendar table exists",
    "select": "SELECT COUNT(service_id) AS count FROM " + config.tables.gtfs.calendar + ";",
    "test": function(row) {
      return row.count >= 0;
    }
  },
  {
    "name": "calendar_dates table exists",
    "select": "SELECT COUNT(service_id) AS count FROM " + config.tables.gtfs.calendar_dates + ";",
    "test": function(row) {
      return row.count >= 0;
    }
  },
  {
    "name": "direction 0 updated",
    "select": "SELECT description FROM " + config.tables.gtfs.directions + " WHERE direction_id=0;",
    "test": function(row) {
      return row.description.toLowerCase() !== "this way";
    }
  },
  {
    "name": "direction 1 updated",
    "select": "SELECT description FROM " + config.tables.gtfs.directions + " WHERE direction_id=1;",
    "test": function(row) {
      return row.description.toLowerCase() !== "that way";
    }
  },
  {
    "name": "route count",
    "select": "SELECT COUNT(route_id) AS count FROM " + config.tables.gtfs.routes + ";",
    "test": function(row) {
      return row.count > 0;
    }
  },
  {
    "name": "stop_times count",
    "select": "SELECT COUNT(trip_id) AS count FROM " + config.tables.gtfs.stop_times + ";",
    "test": function(row) {
      return row.count > 0;
    }
  },
  {
    "name": "timetable arrival_time_seconds",
    "select": "SELECT COUNT(trip_id) AS count FROM " + config.tables.gtfs.stop_times + " WHERE arrival_time_seconds IS NULL OR arrival_time_seconds = '';",
    "test": function(row) {
      return row.count === 0;
    }
  },
  {
    "name": "timetable departure_time_seconds",
    "select": "SELECT COUNT(trip_id) AS count FROM " + config.tables.gtfs.stop_times + " WHERE departure_time_seconds IS NULL OR departure_time_seconds = '';",
    "test": function(row) {
      return row.count === 0;
    }
  },
  {
    "name": "stop count",
    "select": "SELECT COUNT(stop_id) AS count FROM " + config.tables.gtfs.stops + ";",
    "test": function(row) {
      return row.count > 0;
    }
  },
  {
    "name": "trip count",
    "select": "SELECT COUNT(trip_id) AS count FROM " + config.tables.gtfs.trips + ";",
    "test": function(row) {
      return row.count > 0;
    }
  },
  {
    "name": "version",
    "select": "SELECT version FROM " + config.tables.rt.about + ";",
    "test": function(row) {
      return row.version > 2017000000;
    }
  },
  {
    "name": "start date",
    "select": "SELECT start_date FROM " + config.tables.rt.about + ";",
    "test": function(row) {
      let today = _yyyymmdd(new Date());
      let start = row.start_date;
      return start > 20170000 && start <= today;
    }
  }
];




/**
 * SQLite Database for Agency
 * @private
 */
let DB = undefined;

/**
 * Right Track Agency
 * @private
 */
let AGENCY = undefined;

/**
 * Final callback function to return to wrap-up process
 * @param {boolean} sane DB Sanity Flag
 * @private
 */
let FINAL_CALLBACK = function(sane) {};

/**
 * Final Sanity Check Flag
 * @private
 */
let SANE = true;



/**
 * Run sanity checks on the specified agency's database
 * @param {object} db SQLite Database
 * @param {object} agencyOptions Agency Options
 * @param {function} callback Sanity check callback function
 * @param {boolean} callback.sane Sanity Check pass flag
 */
function sanityChecks(db, agencyOptions, callback) {
  log("--> Running Sanity Checks");

  // Set Database
  DB = db;

  // Set Agency
  AGENCY = agencyOptions.agency;

  // Set final callback
  FINAL_CALLBACK = callback;

  // Run the file checks
  _fileChecks();

  // Start running the sanity checks
  _runChecks();

}


/**
 * Run file checks
 * Make sure db and zip files exist and are at least a minimum size
 * @private
 */
function _fileChecks() {
  let status = "pass";
  let style = "bgGreen.black.bold";
  let msg = undefined;

  let dbPath = path.normalize(AGENCY.moduleDirectory + '/' + config.locations.files.db);
  let dbZipPath = path.normalize(AGENCY.moduleDirectory + '/' + config.locations.files.dbZip);

  // Make sure files exist
  if ( !fs.existsSync(dbPath) ) {
    status = "FAIL";
    msg = "DB file not found";
  }
  else if ( !fs.existsSync(dbZipPath) ) {
    status = "FAIL";
    msg = "DB Zip file not found";
  }
  else if ( fs.statSync(dbPath).size < MIN_DB_SIZE ) {
    status = "FAIL";
    msg = "DB file too small";
  }
  else if ( fs.statSync(dbPath).size < MIN_ZIP_SIZE ) {
    status = "FAIL";
    msg = "DB Zip file too small";
  }

  // Log the failed test
  if ( status !== "pass" ) {
    SANE = false;
    style = "bgRed.white.bold";
    errors.error("Fail file test: " + msg, undefined, AGENCY.id);
  }

  // Display test status
  log.raw([
    {
      "text": "    ... Checking files"
    },
    {
      "text": " " + status + " ",
      "chalk": style
    }
  ]);
}


/**
 * Run the sanity check specified by the count
 * @param count check counter
 * @private
 */
function _runChecks(count=0) {
  if ( count < CHECKS.length ) {
    _check(CHECKS[count], _nextCheck);
  }
  else {
    _finish();
  }

  function _nextCheck() {
    _runChecks(count+1);
  }
}

/**
 * Perform the specified sanity check
 * @param check Sanity Check
 * @param callback Check callback function
 * @private
 */
function _check(check, callback) {
  DB.get(check.select, function(err, row) {
    let status = "pass";
    let style = "bgGreen.black.bold";
    let msg = undefined;

    // Database Error
    if ( err ) {
      status = "ERROR";
      msg = err.message.toString();
    }

    // Perform the test
    else {
      let pass = check.test(row);
      if ( !pass ) {
        status = "FAIL";
        msg = "Test result: " + JSON.stringify(row);
      }
    }

    // Log the failed test
    if ( status !== "pass" ) {
      SANE = false;
      style = "bgRed.white.bold";
      errors.error("Fail DB test: " + check.name, msg, AGENCY.id);
    }

    // Display test status
    log.raw([
      {
        "text": "    ... Checking " + check.name
      },
      {
        "text": " " + status + " ",
        "chalk": style
      }
    ]);

    return callback();
  });
}


/**
 * Finish the sanity check process
 * @private
 */
function _finish() {
  FINAL_CALLBACK(SANE);
}


/**
 * Generate a Date Int from the specified date
 * @param date JS Date
 * @returns {string} Date Int
 * @private
 */
function _yyyymmdd(date) {
  let yyyy = date.getFullYear();
  let mm = date.getMonth()+1;
  let dd  = date.getDate();
  return String(10000*yyyy + 100*mm + dd);
}


module.exports = sanityChecks;