diff --git a/.travis.yml b/.travis.yml index f1e1380..23273b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ language: node_js node_js: - - "12" - "10" - "8" diff --git a/README.md b/README.md index 88f5797..f34d0ac 100644 --- a/README.md +++ b/README.md @@ -177,18 +177,6 @@ helper.readDimensions(function(err, dimensions) { YCB Config lets you read just the dimensions that are available for you to contextualize a request that's coming in. This can be an array of properties such as device type, language, feature bucket, or more. -## Scheduled Configs - -To support scheduled configs as described in [ycb](https://github.com/yahoo/ycb) ycb-config must be set to time aware mode via option flag and the time must be passed as a special dimension of the context when in this mode. -``` -let helper = new ConfigHelper({timeAware: true}); -let context = req.context; -context.time = Date.now(); //{device: 'mobile', time: 1573235678929} -helper.read(bundle, config, context, callback); -``` -The time value in the context should be a millisecond timestamp. To use a custom time dimension -it may specified asn an option:`new ConfigHelper({timeDimension: 'my-time-key'})`. - ## License This software is free to use under the Yahoo Inc. BSD license. See the [LICENSE file][] for license text and copyright information. diff --git a/lib/cache.js b/lib/cache.js deleted file mode 100644 index 2b0f6e3..0000000 --- a/lib/cache.js +++ /dev/null @@ -1,154 +0,0 @@ - -/*jslint nomen:true, anon:true, node:true, esversion:6 */ -'use strict'; - -/** - * Entry class used as map values and intrusive linked list nodes. - */ -function Entry(key, value, setAt, expiresAt, groupId) { - this.next = null; - this.prev = null; - this.key = key; - this.value = value; - this.setAt = setAt; - this.expiresAt = expiresAt; - this.groupId = groupId; -} - -/** - * LRU cache. - * Supported options are {max: int} which will set the max capacity of the cache. - */ -function ConfigCache(options) { - options = options || {}; - this.max = options.max; - if(!Number.isInteger(options.max) || options.max < 0) { - console.log('WARNING: no valid cache capacity given, defaulting to 100. %s', JSON.stringify(options)); - this.max = 100; - } - if(this.max === 0) { - this.get = this.set = this.getTimeAware = this.setTimeAware = function(){}; - } - this.size = 0; - this.map = new Map(); //key -> Entry - this.youngest = null; - this.oldest = null; -} - -ConfigCache.prototype = { - /** - * Set a cache entry. - * @param {string} key Key mapping to this value. - * @param {*} value Value to be cached. - * @param {number} groupId Id of the entry's group, used to lazily invalidate subsets of the cache. - */ - set(key, value, groupId) { - this.setTimeAware(key, value, 0, 0, groupId); - }, - - /** - * Set a time aware cache entry. - * @param {string} key Key mapping to this value. - * @param {*} value Value to be cached. - * @param {number} now Current time. - * @param {number} expiresAt Time at which entry will become stale. - * @param {number} groupId Id of the entry's group, used to lazily invalidate subsets of the cache. - */ - setTimeAware(key, value, now, expiresAt, groupId) { - var entry = this.map.get(key); - if(entry !== undefined) { - entry.value = value; - entry.setAt = now; - entry.expiresAt = expiresAt; - entry.groupId = groupId; - this._makeYoungest(entry); - return; - } - if(this.size === this.max) { - entry = this.oldest; - this.map.delete(entry.key); - entry.key = key; - entry.value = value; - entry.setAt = now; - entry.expiresAt = expiresAt; - entry.groupId = groupId; - this.map.set(key, entry); - this._makeYoungest(entry); - return; - } - entry = new Entry(key, value, now, expiresAt, groupId); - this.map.set(key, entry); - if(this.size === 0) { - this.youngest = entry; - this.oldest = entry; - this.size = 1; - return; - } - entry.next = this.youngest; - this.youngest.prev = entry; - this.youngest = entry; - this.size++; - }, - - /** - * Get value from the cache. Will return undefined if entry is not in cache or is stale. - * @param {string} key Key to look up in cache. - * @param {number} groupId Group id to check if value is stale. - * @returns {*} - */ - get(key, groupId) { - var entry = this.map.get(key); - if(entry !== undefined) { - if(groupId !== entry.groupId) { - return undefined; //do not clean up stale entry as we know client code will set this key - } - this._makeYoungest(entry); - return entry.value; - } - return undefined; - }, - - /** - * Get value from the cache with time awareness. Will return undefined if entry is not in cache or is stale. - * @param {string} key Key to look up in cache. - * @param {number} now Current time to check if value is stale. - * @param {number} groupId Group id to check if value is stale. - * @returns {*} - */ - getTimeAware(key, now, groupId) { - var entry = this.map.get(key); - if(entry !== undefined) { - if(groupId !== entry.groupId || now < entry.setAt || now >= entry.expiresAt){ - return undefined; //do not clean up stale entry as we know client code will set this key - } - this._makeYoungest(entry); - return entry.value; - } - return undefined; - }, - - /** - * Move entry to the head of the list and set as youngest. - * @param {Entry} entry - * @private - */ - _makeYoungest(entry) { - if(entry === this.youngest) { - return; - } - var prev = entry.prev; - if(entry === this.oldest) { - prev.next = null; - this.oldest = prev; - } else { - prev.next = entry.next; - entry.next.prev = prev; - } - entry.prev = null; - this.youngest.prev = entry; - entry.next = this.youngest; - this.youngest = entry; - } -}; - -module.exports = ConfigCache; diff --git a/lib/index.js b/lib/index.js index 75ab6b0..2bd4601 100644 --- a/lib/index.js +++ b/lib/index.js @@ -14,7 +14,7 @@ var libfs = require('fs'), libycb = require('ycb'), libjson5 = require('json5'), libyaml = require('yamljs'), - libcache = require('./cache'), + libcache = require('lru-cache'), deepFreeze = require('deep-freeze'), MESSAGES = { @@ -22,8 +22,7 @@ var libfs = require('fs'), 'unknown config': 'Unknown config "%s" in bundle "%s"', 'unknown cache data': 'Unknown cache data with config "%s" in bundle "%s"', 'missing dimensions': 'Failed to find a dimensions.json file', - 'parse error': 'Failed to parse "%s"\n%s', - 'missing time': 'No time dimension, %s, in context, %s, during time aware mode' + 'parse error': 'Failed to parse "%s"\n%s' }, DEFAULT_CACHE_OPTIONS = { max: 250 @@ -70,12 +69,43 @@ function contentsIsYCB(contents) { if (!section.settings) { return false; } + if (!Array.isArray(section.settings)) { + return false; + } } return true; } return false; } +/** + * Creates a cache key that will be one-to-one for each context object, + * based on their contents. JSON.stringify does not guarantee order for + * two objects that have the same contents, so we need to have this. + * @private + * @static + * @method getCacheKey + * @param {mixed} context The context object. + * @return {string} A JSON-parseable string that will be the same for all equivalent context objects. + */ + +function getCacheKey(context) { + var a = [], + KV_START = '"', + KV_END = '"', + JSON_START = '{', + JSON_END = '}', + key; + + for (key in context) { + a.push([KV_START + key + KV_END, + KV_START + context[key] + KV_END].join(':')); + } + + a.sort(); + return JSON_START + a.toString() + JSON_END; +} + /** * Create the YCB object. * @private @@ -88,11 +118,25 @@ function contentsIsYCB(contents) { */ function makeYCB(config, dimensions, contents) { var ycbBundle, - ycb; + ycb, + originalRead, + originalReadNoMerge; + contents = contents || {}; ycbBundle = [{dimensions: dimensions}]; + // need to copy contents, since YCB messes with it ycbBundle = ycbBundle.concat(clone(contents)); ycb = new libycb.Ycb(ycbBundle); + + // monkey-patch to apply baseContext + originalRead = ycb.read; + ycb.read = function (context, options) { + return originalRead.call(ycb, config._mergeBaseContext(context), options); + }; + originalReadNoMerge = ycb.readNoMerge; + ycb.readNoMerge = function (context, options) { + return originalReadNoMerge.call(ycb, config._mergeBaseContext(context), options); + }; return ycb; } @@ -119,15 +163,6 @@ function makeFakeYCB(dimensions, contents) { }, readNoMerge: function () { return [contents]; - }, - readTimeAware: function () { - return contents; - }, - readNoMergeTimeAware: function () { - return [contents]; - }, - getCacheKey: function () { - return ''; } }; } @@ -152,491 +187,476 @@ function makeFakeYCB(dimensions, contents) { function Config(options) { this._options = options || {}; this._dimensionsPath = this._options.dimensionsPath; - this._configPaths = {}; // bundle: config: fullpath this._configContents = {}; // fullpath: contents + this._configPaths = {}; // bundle: config: fullpath + // cached data: this._configYCBs = {}; // fullpath: YCB object - this._pathCount = {}; // fullpath: number of configs using this path - this._configCache = {}; // unused object for compability - //cache fields: - this._configIdCounter = 0; //incrementing counter assigned to config bundles to uniquely identify them for cache invalidation. - this._configIdMap = {}; //id = _configIdMap[bundleName][configName] - this._cache = new libcache(this._options.cache || DEFAULT_CACHE_OPTIONS); - //time fields: - this.timeAware = false; - this.timeDimension = 'time'; - this.expiresKey = libycb.expirationKey; - if(this._options.timeAware) { - this.timeAware = true; - } - if(this._options.timeDimension) { - this.timeAware = true; - this.timeDimension = this._options.timeDimension; - } + this._configCache = {}; // bundle: config: context: config } -Config.prototype = { - - /** - * Registers a configuration file. - * @method addConfig - * @async - * @param {string} bundleName Name of the bundle to which this config file belongs. - * @param {string} configName Name of the config file. - * @param {string} fullPath Full filesystem path to the config file. - * @param {Function} [callback] Called once the config has been added to the helper. - * @param {Error|null} callback.err If an error occurred, then this parameter will - * contain the error. If the operation succeeded, then `err` will be null. - * @param {Object} callback.contents The contents of the config file, as a - * JavaScript object. - */ - addConfig: function (bundleName, configName, fullPath, callback) { - var self = this; - callback = callback || function(){}; - self._readConfigContents(fullPath, function (err, contents) { - if (err) { - return callback(err); - } - self.addConfigContents(bundleName, configName, fullPath, contents, callback); - }); - }, +Config.prototype = {}; - /** - * Registers a configuration file and its contents. - * @method addConfigContents - * @param {string} bundleName Name of the bundle to which this config file belongs. - * @param {string} configName Name of the config file. - * @param {string} fullPath Full filesystem path to the config file. - * @param {string|Object} contents The contents for the config file at the path. - * This will be parsed into an object (via JSON or YAML depending on the file extension) - * unless it is already an object. - * @param {Function} [callback] Called once the config has been added to the helper. - * @param {Error|null} callback.err If an error occurred, then this parameter will - * contain the error. If the operation succeeded, then `err` will be null. - * @param {Object} callback.contents The contents of the config file, as a - * JavaScript object. - */ - addConfigContents: function (bundleName, configName, fullPath, contents, callback) { - var self = this; - - contents = this._parseConfigContents(fullPath, contents); - - // register so that _readConfigContents() will use - self._configContents[fullPath] = contents; - - // deregister old config (if any) - self.deleteConfig(bundleName, configName, fullPath); - - if (!self._configPaths[bundleName]) { - self._configPaths[bundleName] = {}; +/** + * Registers a configuration file. + * @method addConfig + * @async + * @param {string} bundleName Name of the bundle to which this config file belongs. + * @param {string} configName Name of the config file. + * @param {string} fullPath Full filesystem path to the config file. + * @param {Function} [callback] Called once the config has been added to the helper. + * @param {Error|null} callback.err If an error occurred, then this parameter will + * contain the error. If the operation succeeded, then `err` will be null. + * @param {Object} callback.contents The contents of the config file, as a + * JavaScript object. + */ +Config.prototype.addConfig = function (bundleName, configName, fullPath, callback) { + var self = this; + callback = callback || function() {}; + self._readConfigContents(fullPath, function (err, contents) { + if (err) { + return callback(err); } - self._configPaths[bundleName][configName] = fullPath; + self.addConfigContents(bundleName, configName, fullPath, contents, callback); + }); +}; - if (!self._pathCount[fullPath]) { - self._pathCount[fullPath] = 0; - } - self._pathCount[fullPath]++; - //assign new config bundle a unique id by incrementing counter - if (!self._configIdMap[bundleName]) { - self._configIdMap[bundleName] = {}; - } - self._configIdCounter = (self._configIdCounter+1) % Number.MAX_SAFE_INTEGER; - self._configIdMap[bundleName][configName] = self._configIdCounter; +/** + * Registers a configuration file and its contents. + * @method addConfigContents + * @param {string} bundleName Name of the bundle to which this config file belongs. + * @param {string} configName Name of the config file. + * @param {string} fullPath Full filesystem path to the config file. + * @param {string|Object} contents The contents for the config file at the path. + * This will be parsed into an object (via JSON or YAML depending on the file extension) + * unless it is already an object. + * @param {Function} [callback] Called once the config has been added to the helper. + * @param {Error|null} callback.err If an error occurred, then this parameter will + * contain the error. If the operation succeeded, then `err` will be null. + * @param {Object} callback.contents The contents of the config file, as a + * JavaScript object. + */ +Config.prototype.addConfigContents = function (bundleName, configName, fullPath, contents, callback) { + var self = this; + + contents = this._parseConfigContents(fullPath, contents); - // keep path to dimensions file up-to-date - if ('dimensions' === configName && !self._options.dimensionsPath) { - if (self._options.dimensionsBundle) { - if (bundleName === self._options.dimensionsBundle) { + // register so that _readConfigContents() will use + self._configContents[fullPath] = contents; + + // deregister old config (if any) + self.deleteConfig(bundleName, configName, fullPath); + + if (!self._configPaths[bundleName]) { + self._configPaths[bundleName] = {}; + } + self._configPaths[bundleName][configName] = fullPath; + + // keep path to dimensions file up-to-date + if ('dimensions' === configName && !self._options.dimensionsPath) { + if (self._options.dimensionsBundle) { + if (bundleName === self._options.dimensionsBundle) { + self._dimensionsPath = fullPath; + } + } else { + if (self._dimensionsPath) { + if (fullPath.length < self._dimensionsPath.length) { self._dimensionsPath = fullPath; } } else { - if (self._dimensionsPath) { - if (fullPath.length < self._dimensionsPath.length) { - self._dimensionsPath = fullPath; - } - } else { - self._dimensionsPath = fullPath; - } + self._dimensionsPath = fullPath; } } + } - if (callback) { - callback(null, contents); - } - }, - + if (callback) { + callback(null, contents); + } +}; - /** - * Deregisters a configuration file. - * @method deleteConfig - * @param {string} bundleName Name of the bundle to which this config file belongs. - * @param {string} configName Name of the config file. - * @param {string} fullPath Full filesystem path to the config file. - * @return {undefined} Nothing appreciable is returned. - */ - deleteConfig: function (bundleName, configName, fullPath) { - var bundleMap = this._configPaths[bundleName]; - if(bundleMap) { - var path = bundleMap[configName]; - if(path) { - this._pathCount[path]--; - if(this._pathCount[path] === 0) { - delete this._configYCBs[path]; - delete this._configContents[path]; - delete this._pathCount[path]; - } - } - delete bundleMap[configName]; - delete this._configIdMap[bundleName][configName]; - if(Object.keys(bundleMap).length === 0) { - delete this._configPaths[bundleName]; - } - if(Object.keys(this._configIdMap[bundleName]).length === 0) { - delete this._configIdMap[bundleName]; - } - } - }, - /** - * Generates a cache key based on config and bundle names, a separator, and a context based key. - * Distinct separators can be used to distinguish distinct types of keys, e.g., merged and unmerged reads. - * - * @param {string} bundleName Name of bundle. - * @param {string} separator Separator string to join bundle and config names. - * @param {string} configName Name of config. - * @param {string} contextKey Key based on the context. - * @returns {string} cache key - * @private - */ - _getCacheKey: function (bundleName, separator, configName, contextKey) { - return bundleName + separator + configName + contextKey; - }, +/** + * Deregisters a configuration file. + * @method deleteConfig + * @param {string} bundleName Name of the bundle to which this config file belongs. + * @param {string} configName Name of the config file. + * @param {string} fullPath Full filesystem path to the config file. + * @return {undefined} Nothing appreciable is returned. + */ +Config.prototype.deleteConfig = function (bundleName, configName, fullPath) { + if (this._configPaths[bundleName]) { + this._configPaths[bundleName][configName] = undefined; + } +}; - /** - * Reads the contents of the named configuration file. - * This will auto-detect if the configuration file is YCB and read it in a context-sensitive way if so. - * - * This can possibly return the configuration object that is stored in a cache, so the caller should - * copy it if they intend to make a modifications. - * @method read - * @async - * @param {string} bundleName The bundle in which to find the configuration file. - * @param {string} configName Which configuration to read. - * @param {object} [context] The runtime context. - * @param {Function} callback - * @param {Error|null} callback.err If an error occurred, then this parameter will - * contain the error. If the operation succeeded, then `err` will be null. - * @param {Object} callback.config The merged configuration object, based on the - * provided context. - */ - read: function (bundleName, configName, context, callback) { - var self = this; - self._getYCB(bundleName, configName, function (err, ycb) { - if (err) { - callback(err); - return; - } - if(self._options.baseContext) { - context = self._mergeBaseContext(context); - } - var key, config, groupId; - groupId = self._configIdMap[bundleName][configName]; - key = self._getCacheKey(bundleName, ':m:', configName, ycb.getCacheKey(context)); - if (self.timeAware) { - var now = context[self.timeDimension]; - if (now === undefined) { - callback(new Error(util.format(MESSAGES['missing time'], self.timeDimension, JSON.stringify(context)))); +/** + * Reads the contents of the named configuration file. + * This will auto-detect if the configuration file is YCB and read it in a context-sensitive way if so. + * + * This can possibly return the configuration object that is stored in a cache, so the caller should + * copy it if they intend to make a modifications. + * @method read + * @async + * @param {string} bundleName The bundle in which to find the configuration file. + * @param {string} configName Which configuration to read. + * @param {object} [context] The runtime context. + * @param {Function} callback + * @param {Error|null} callback.err If an error occurred, then this parameter will + * contain the error. If the operation succeeded, then `err` will be null. + * @param {Object} callback.config The merged configuration object, based on the + * provided context. + */ +Config.prototype.read = function (bundleName, configName, context, callback) { + var self = this; + self._getConfigCache(bundleName, configName, context, true, function (err, config) { + if (err) { + self._getYCB(bundleName, configName, function (err, ycb) { + if (err) { + callback(err); return; } - config = self._cache.getTimeAware(key, now, groupId); - if(config === undefined) { - config = ycb.readTimeAware(context, now, {cacheInfo: true}); - var expiresAt = config[self.expiresKey]; - if(expiresAt === undefined) { - expiresAt = Number.POSITIVE_INFINITY; - } - if(self._options.safeMode) { - config = deepFreeze(config); - } - self._cache.setTimeAware(key, config, now, expiresAt, groupId); - } - } else { - config = self._cache.get(key, groupId); - if(config === undefined) { - config = ycb.read(context, {}); - if(self._options.safeMode) { - config = deepFreeze(config); - } - self._cache.set(key, config, groupId); - } - } - callback(null, config); - }); - }, + config = ycb.read(context, {}); + return self._setConfigCache(bundleName, configName, context, config, true, callback); + }); + } else { + return callback(null, config); + } + }); +}; - /** - * Reads the contents of the named configuration file and returns the sections - * appropriate to the context. The sections are returned in priority order - * (most important first). - * - * If the file is not context sensitive then the list will contain a single section. - * - * This can possibly return the configuration object that is stored in a cache, so the caller should - * copy it if they intend to make a modifications. - * @method readNoMerge - * @async - * @param {string} bundleName The bundle in which to find the configuration file. - * @param {string} configName Which configuration to read. - * @param {object} [context] The runtime context. - * @return {Function} callback - * @param {Error|null} callback.err If an error occurred, then this parameter will - * contain the error. If the operation succeeded, then the `err` will be null. - * @param {Object} callback.contents The object containing the prioritized sections - * of the configuration file appropriate to the provided context. - */ - readNoMerge: function (bundleName, configName, context, callback) { - var self = this; - self._getYCB(bundleName, configName, function (err, ycb) { - if (err) { - callback(err); - return; - } - if(self._options.baseContext) { - context = self._mergeBaseContext(context); - } - var key, config, groupId; - groupId = self._configIdMap[bundleName][configName]; - key = self._getCacheKey(bundleName, ':um:', configName, ycb.getCacheKey(context)); - if(self.timeAware) { - var now = context[self.timeDimension]; - if(now === undefined) { - callback(new Error(util.format(MESSAGES['missing time'], self.timeDimension, JSON.stringify(context)))); - return; - } - config = self._cache.getTimeAware(key, now, groupId); - if(config === undefined) { - config = config = ycb.readNoMergeTimeAware(context, now, {cacheInfo: true}); - var expiresAt = config.length > 0 ? config[0][self.expiresKey] : undefined; - if(expiresAt === undefined) { - expiresAt = Number.POSITIVE_INFINITY; - } - if(self._options.safeMode) { - config = deepFreeze(config); - } - self._cache.setTimeAware(key, config, now, expiresAt, groupId); - } - } else { - config = self._cache.get(key, groupId); - if(config === undefined) { - config = ycb.readNoMerge(context, {}); - if(self._options.safeMode) { - config = deepFreeze(config); - } - self._cache.set(key, config, groupId); +/** + * Reads the contents of the named configuration file and returns the sections + * appropriate to the context. The sections are returned in priority order + * (most important first). + * + * If the file is not context sensitive then the list will contain a single section. + * + * This can possibly return the configuration object that is stored in a cache, so the caller should + * copy it if they intend to make a modifications. + * @method readNoMerge + * @async + * @param {string} bundleName The bundle in which to find the configuration file. + * @param {string} configName Which configuration to read. + * @param {object} [context] The runtime context. + * @return {Function} callback + * @param {Error|null} callback.err If an error occurred, then this parameter will + * contain the error. If the operation succeeded, then the `err` will be null. + * @param {Object} callback.contents The object containing the prioritized sections + * of the configuration file appropriate to the provided context. + */ +Config.prototype.readNoMerge = function (bundleName, configName, context, callback) { + var self = this; + self._getConfigCache(bundleName, configName, context, false, function (err, config) { + if (err) { + self._getYCB(bundleName, configName, function (err, ycb) { + if (err) { + return callback(err); } - } - callback(null, config); - }); - }, - /** - * Reads the dimensions file for the application. - * - * If `options.dimensionsPath` is given to the constructor that'll be used. - * Otherwise, the dimensions file found in `options.dimensionsBundle` will be used. - * Otherwise, the dimensions file with the shortest path will be used. - * - * The returned dimensions object is shared, so it should not be modified. - * @method readDimensions - * @param {Function} callback - * @param {Error|null} callback.err If an error occurred, then this parameter will - * contain the error. If the operation succeeded, then `err` will be null. - * @param {array} callback.dimensions The returned dimensions array. - */ - readDimensions: function (callback) { - var self = this; - if (!self._dimensionsPath) { - return callback(new Error(MESSAGES['missing dimensions'])); - } - if (self._cachedDimensions) { - return callback(null, self._cachedDimensions); + config = ycb.readNoMerge(context, {}); + return self._setConfigCache(bundleName, configName, context, config, false, callback); + }); + } else { + return callback(null, config); } + }); +}; - self._readConfigContents(self._dimensionsPath, function (err, body) { - if (err) { - return callback(err); - } - self._cachedDimensions = body[0].dimensions; - delete self._configContents[self._dimensionsPath]; // no longer need this copy of dimensions - return callback(null, self._cachedDimensions); - }); - }, +/** + * Provides a method that should get and return a cached configuration object, + * given the bundle name, configuration name, context object, and whether or + * not the configuration was merged. + * + * The default implementation uses the LRU cache from `node-lru-cache`, and options + * to the cache can be passed through the Config constructor. + * + * This can be overridden if a custom caching method is provided. + * @method _getConfigCache + * @async + * @private + * @param {string} bundleName The bundle in which to find the configuration file. + * @param {string} configName Which configuration to read. + * @param {object} context The runtime context. + * @param {boolean} hasMerge Whether or not the configuration data will be merged. + * @param {Function} callback + * @param {Error|null} callback.err If an error occurred, then this parameter will + * contain the error. If the operation succeeded, then `err` will be null. + * @param {Object} callback.config The merged or unmerged cached configuration data. + */ +Config.prototype._getConfigCache = function (bundleName, configName, context, hasMerge, callback) { + var self = this, + bundlePath, + configPath, + mergePath, + mergeName = hasMerge ? 'merge' : 'no-merge', + config; + + bundlePath = self._configCache[bundleName]; + if (!bundlePath) { + return callback(new Error(util.format(MESSAGES['unknown bundle'], bundleName))); + } + configPath = bundlePath[configName]; + if (!configPath) { + return callback(new Error(util.format(MESSAGES['unknown config'], configName, bundleName))); + } - /** - * Provides a YCB object for the configuration file. - * This returns a YCB object even if the configuration file isn't a YCB file. - * @private - * @method _getYCB - * @async - * @param {string} bundleName The bundle in which to find the configuration file. - * @param {string} configName Which configuration to read. - * @param {object} [context] The runtime context. - * @param {Function} callback - * @param {Error|null} callback.err If an error occurred, then this parameter will - * contain the error. If the operation succeeded, then `err` will be null. - * @param {Object} callback.ycb The returned YCB object. - */ - _getYCB: function (bundleName, configName, callback) { - var self = this, - path, - contents, - dimensions, - ycb, - isYCB; - - if (!self._configPaths[bundleName]) { - return callback(new Error(util.format(MESSAGES['unknown bundle'], bundleName))); - } - path = self._configPaths[bundleName][configName]; - if (!path) { - return callback(new Error(util.format(MESSAGES['unknown config'], configName, bundleName))); - } + mergePath = configPath[mergeName]; + if (!mergePath) { + return callback(new Error(util.format(MESSAGES['unknown cache data'], configName, bundleName))); + } - if (self._configYCBs[path]) { - return callback(null, self._configYCBs[path]); - } + config = mergePath.get(getCacheKey(context)); + if (config) { + return callback(null, config); + } else { + return callback(new Error(util.format(MESSAGES['unknown cache data'], configName, bundleName))); + } +}; - self._readConfigContents(path, function (err, contents) { - if (err) { - return callback(err); - } +/** + * Provides a method that should set and return a cached configuration object, + * given the bundle name, configuration name, context object, configuration, + * and whether or not it was merged. + * + * The default implementation uses the LRU cache from `node-lru-cache`, and options + * to the cache can be passed through the Config constructor. + * + * This can be overridden if a custom caching method is provided. + * @method _setConfigCache + * @async + * @private + * @param {string} bundleName The bundle in which to find the configuration file. + * @param {string} configName Which configuration to read. + * @param {object} context The runtime context. + * @param {object} config The configuration data. + * @param {boolean} hasMerge Whether or not the configuration data will be merged. + * @param {Function} callback + * @param {Error|null} callback.err If an error occurred, then this parameter will + * contain the error. If the operation succeeded, then `err` will be null. + * @param {Object} callback.config The merged or unmerged cached configuration data. + */ +Config.prototype._setConfigCache = function (bundleName, configName, context, config, hasMerge, callback) { + var self = this, + LRU = libcache, + mergeName = hasMerge ? 'merge' : 'no-merge', + cacheOptions = self._options.cache || DEFAULT_CACHE_OPTIONS, + bundlePath, + configPath, + cache, + configClone, + configCache = self._configCache; + + bundlePath = configCache[bundleName] = (configCache[bundleName] || {}); + configPath = bundlePath[configName] = (bundlePath[configName] || {}); + cache = configPath[mergeName] = (configPath[mergeName] || new LRU(cacheOptions)); + + config = this._options.safeMode ? deepFreeze(config) : config; + + cache.set(getCacheKey(context), config); + + return callback(null, config); +}; - isYCB = contentsIsYCB(contents); +/** + * Reads the dimensions file for the application. + * + * If `options.dimensionsPath` is given to the constructor that'll be used. + * Otherwise, the dimensions file found in `options.dimensionsBundle` will be used. + * Otherwise, the dimensions file with the shortest path will be used. + * + * The returned dimensions object is shared, so it should not be modified. + * @method readDimensions + * @param {Function} callback + * @param {Error|null} callback.err If an error occurred, then this parameter will + * contain the error. If the operation succeeded, then `err` will be null. + * @param {array} callback.dimensions The returned dimensions array. + */ +Config.prototype.readDimensions = function (callback) { + var self = this; + if (!self._dimensionsPath) { + return callback(new Error(MESSAGES['missing dimensions'])); + } + if (self._cachedDimensions) { + return callback(null, self._cachedDimensions); + } - if (isYCB) { - self.readDimensions(function (err, data) { - if (err) { - return callback(err); - } + self._readConfigContents(self._dimensionsPath, function (err, body) { + if (err) { + return callback(err); + } - dimensions = data; - ycb = self._makeYCBFromDimensions(path, dimensions, contents); - callback(null, ycb); - }); - } else { - ycb = self._makeYCBFromDimensions(path, dimensions, contents); - callback(null, ycb); - } - }); - }, + self._cachedDimensions = body[0].dimensions; + return callback(null, self._cachedDimensions); + }); +}; - /** - * Determines whether to make a YCB or fake YCB object from - * a dimensions object. - * @private - * @method _makeYCBFromDimensions - */ - _makeYCBFromDimensions: function (path, dimensions, contents) { - var ycb; +/** + * Provides a YCB object for the configuration file. + * This returns a YCB object even if the configuration file isn't a YCB file. + * @private + * @method _getYCB + * @async + * @param {string} bundleName The bundle in which to find the configuration file. + * @param {string} configName Which configuration to read. + * @param {object} [context] The runtime context. + * @param {Function} callback + * @param {Error|null} callback.err If an error occurred, then this parameter will + * contain the error. If the operation succeeded, then `err` will be null. + * @param {Object} callback.ycb The returned YCB object. + */ +Config.prototype._getYCB = function (bundleName, configName, callback) { + var self = this, + path, + contents, + dimensions, + ycb, + isYCB; + + if (!self._configPaths[bundleName]) { + return callback(new Error(util.format(MESSAGES['unknown bundle'], bundleName))); + } + path = self._configPaths[bundleName][configName]; + if (!path) { + return callback(new Error(util.format(MESSAGES['unknown config'], configName, bundleName))); + } - if (dimensions) { - ycb = makeYCB(this, dimensions, contents); - } else { - ycb = makeFakeYCB(dimensions, contents); - } - this._configYCBs[path] = ycb; - delete this._configContents[path]; // no longer need to keep a copy of the config - return ycb; - }, + if (self._configYCBs[path]) { + return callback(null, self._configYCBs[path]); + } - /** - * Reads the contents of a configuration file. - * @private - * @method _readConfigContents - * @async - * @param {string} path Full path to the file. - * @param {Function} callback - * @param {Error|null} callback.err If an error occurred, then this parameter will - * contain the error. If the operation succeeded, then `err` will be null. - * @param {Object} callback.contents The returned contents of the configuration file. - */ - _readConfigContents: function (path, callback) { - var self = this, - ext = libpath.extname(path), - contents; - - if (this._configContents[path]) { - callback(null, this._configContents[path]); - return; + self._readConfigContents(path, function (err, contents) { + if (err) { + return callback(err); } - // really try to do things async as much as possible - if ('.json' === ext || '.json5' === ext || '.yaml' === ext || '.yml' === ext) { - libfs.readFile(path, 'utf8', function (err, contents) { + isYCB = contentsIsYCB(contents); + + if (isYCB) { + self.readDimensions(function (err, data) { if (err) { return callback(err); } - self._parseConfigContents(path, contents, function (err, contents) { - return callback(err, contents); - }); + + dimensions = data; + ycb = self._makeYCBFromDimensions(path, dimensions, contents); + callback(null, ycb); }); } else { - try { - contents = require(path); - } catch (e) { - return callback(new Error(util.format(MESSAGES['parse error'], path, e.message))); - } - - return callback(null, contents); + ycb = self._makeYCBFromDimensions(path, dimensions, contents); + callback(null, ycb); } - }, + }); +}; + +/** + * Determines whether to make a YCB or fake YCB object from + * a dimensions object. + * @private + * @method _makeYCBFromDimensions + */ + Config.prototype._makeYCBFromDimensions = function (path, dimensions, contents) { + var ycb; - _parseConfigContents: function (path, contents, callback) { - var ext, - error; - // Sometimes the contents are already parsed. - if ('object' !== typeof contents) { - ext = libpath.extname(path); - try { - if ('.json' === ext) { - contents = JSON.parse(contents); - } else if ('.json5' === ext) { - contents = libjson5.parse(contents); - } else if ('.js' === ext) { - contents = require(path); - } else { - contents = libyaml.parse(contents); - } - } catch (e) { - error = new Error(util.format(MESSAGES['parse error'], path, e.message)); - if (callback) { - return callback(error); - } else { - return error; - } + if (dimensions) { + ycb = makeYCB(this, dimensions, contents); + } else { + ycb = makeFakeYCB(dimensions, contents); + } + this._configYCBs[path] = ycb; + + return ycb; + }; + +/** + * Reads the contents of a configuration file. + * @private + * @method _readConfigContents + * @async + * @param {string} path Full path to the file. + * @param {Function} callback + * @param {Error|null} callback.err If an error occurred, then this parameter will + * contain the error. If the operation succeeded, then `err` will be null. + * @param {Object} callback.contents The returned contents of the configuration file. + */ +Config.prototype._readConfigContents = function (path, callback) { + var self = this, + ext = libpath.extname(path), + contents; + + if (this._configContents[path]) { + callback(null, this._configContents[path]); + return; + } + + // really try to do things async as much as possible + if ('.json' === ext || '.json5' === ext || '.yaml' === ext || '.yml' === ext) { + libfs.readFile(path, 'utf8', function (err, contents) { + if (err) { + return callback(err); } + self._parseConfigContents(path, contents, function(err, contents) { + // TODO -- cache in _configContents? + return callback(err, contents); + }); + }); + } else { + try { + contents = require(path); + // TODO -- cache in _configContents? + } catch (e) { + return callback(new Error(util.format(MESSAGES['parse error'], path, e.message))); } - return callback ? callback(null, contents) : contents; - }, + return callback(null, contents); + } +}; - /** - * Merges the base context under the runtime context. - * @private - * @method _mergeBaseContext - * @param {object} context The runtime context. - * @return {object} A new object with the context expanded with the base merged under. - */ - _mergeBaseContext: function (context) { - context = context || {}; - return mix(clone(context), this._options.baseContext); +Config.prototype._parseConfigContents = function (path, contents, callback) { + var ext, + error; + // Sometimes the contents are already parsed. + if ('object' !== typeof contents) { + ext = libpath.extname(path); + try { + if ('.json' === ext) { + contents = JSON.parse(contents); + } else if ('.json5' === ext) { + contents = libjson5.parse(contents); + } else if ('.js' === ext) { + contents = require(path); + } else { + contents = libyaml.parse(contents); + } + } catch (e) { + error = new Error(util.format(MESSAGES['parse error'], path, e.message)); + if (callback) { + return callback(error); + } else { + return error; + } + } } + + return callback ? callback(null, contents) : contents; +}; + + +/** + * Merges the base context under the runtime context. + * @private + * @method _mergeBaseContext + * @param {object} context The runtime context. + * @return {object} A new object with the context expanded with the base merged under. + */ +Config.prototype._mergeBaseContext = function (context) { + context = context || {}; + return mix(clone(context), this._options.baseContext); }; @@ -647,4 +667,4 @@ Config.test = { contentsIsYCB: contentsIsYCB, makeYCB: makeYCB, makeFakeYCB: makeFakeYCB -}; \ No newline at end of file +}; diff --git a/package.json b/package.json index 6bd2f2a..063a7d5 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,10 @@ { "name": "ycb-config", - "version": "1.1.4", + "version": "1.2.0", "description": "Configuration manager for Yahoo configuration bundles", "author": "Drew Folta ", "contributors": [], "main": "./lib/index.js", - "files": [ - "lib" - ], "directories": { "lib": "./lib" }, @@ -17,9 +14,10 @@ }, "homepage": "https://github.com/yahoo/ycb-config", "dependencies": { - "ycb": "^2.1.1", + "ycb": "^2.0.0", "json5": "~0.4.0", "yamljs": "^0.2.3", + "lru-cache": "^2.3.1", "deep-freeze": "~0.0.1" }, "devDependencies": { @@ -31,7 +29,7 @@ }, "scripts": { "cover": "./node_modules/istanbul/lib/cli.js cover -- ./node_modules/mocha/bin/_mocha tests/lib/*.js --reporter spec", - "test": "jshint lib/index.js lib/cache.js tests/lib/index.js tests/lib/cache-test.js && _mocha tests/lib/index.js tests/lib/cache-test.js --reporter spec" + "test": "jshint lib/index.js tests/lib/index.js && _mocha tests/lib/index.js --reporter spec" }, "license": "BSD", "repository": { diff --git a/tests/fixtures/time/time-test-configs.json b/tests/fixtures/time/time-test-configs.json deleted file mode 100644 index 02fc034..0000000 --- a/tests/fixtures/time/time-test-configs.json +++ /dev/null @@ -1,1103 +0,0 @@ -[ - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "end": "2019-08-19T20:26:12.247Z" - } - }, - "name": "config_0", - "intervals": { - "config_0": { - "end": 1566246372247 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:12.162Z", - "end": "2019-08-19T20:26:18.177Z" - } - }, - "name": "config_1", - "intervals": { - "config_1": { - "start": 1566246372162, - "end": 1566246378177 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:11.998Z", - "end": "2019-08-19T20:26:13.706Z" - } - }, - "name": "config_2", - "intervals": { - "config_2": { - "start": 1566246371998, - "end": 1566246373706 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:12.186Z", - "end": "2019-08-19T20:26:15.698Z" - } - }, - "name": "config_3", - "intervals": { - "config_3": { - "start": 1566246372186, - "end": 1566246375698 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:10.910Z", - "end": "2019-08-19T20:26:12.521Z" - } - }, - "name": "config_4", - "intervals": { - "config_4": { - "start": 1566246370910, - "end": 1566246372521 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:13.535Z", - "end": "2019-08-19T20:26:15.330Z" - } - }, - "name": "config_5", - "intervals": { - "config_5": { - "start": 1566246373535, - "end": 1566246375330 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:10.918Z", - "end": "2019-08-19T20:26:12.198Z" - } - }, - "name": "config_6", - "intervals": { - "config_6": { - "start": 1566246370918, - "end": 1566246372198 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "end": "2019-08-19T20:26:16.702Z" - } - }, - "name": "config_7", - "intervals": { - "config_7": { - "end": 1566246376702 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:16.316Z", - "end": "2019-08-19T20:26:19.804Z" - } - }, - "name": "config_8", - "intervals": { - "config_8": { - "start": 1566246376316, - "end": 1566246379804 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:10.607Z", - "end": "2019-08-19T20:26:10.817Z" - } - }, - "name": "config_9", - "intervals": { - "config_9": { - "start": 1566246370607, - "end": 1566246370817 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:12.020Z" - } - }, - "name": "config_10", - "intervals": { - "config_10": { - "start": 1566246372020 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:10.239Z", - "end": "2019-08-19T20:26:19.483Z" - } - }, - "name": "config_11", - "intervals": { - "config_11": { - "start": 1566246370239, - "end": 1566246379483 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:10.086Z", - "end": "2019-08-19T20:26:10.307Z" - } - }, - "name": "config_12", - "intervals": { - "config_12": { - "start": 1566246370086, - "end": 1566246370307 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:16.912Z" - } - }, - "name": "config_13", - "intervals": { - "config_13": { - "start": 1566246376912 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:13.364Z", - "end": "2019-08-19T20:26:13.388Z" - } - }, - "name": "config_14", - "intervals": { - "config_14": { - "start": 1566246373364, - "end": 1566246373388 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:12.985Z", - "end": "2019-08-19T20:26:16.035Z" - } - }, - "name": "config_15", - "intervals": { - "config_15": { - "start": 1566246372985, - "end": 1566246376035 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:12.913Z", - "end": "2019-08-19T20:26:16.893Z" - } - }, - "name": "config_16", - "intervals": { - "config_16": { - "start": 1566246372913, - "end": 1566246376893 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:15.609Z", - "end": "2019-08-19T20:26:18.796Z" - } - }, - "name": "config_17", - "intervals": { - "config_17": { - "start": 1566246375609, - "end": 1566246378796 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "end": "2019-08-19T20:26:10.480Z" - } - }, - "name": "config_18", - "intervals": { - "config_18": { - "end": 1566246370480 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:11.656Z", - "end": "2019-08-19T20:26:13.125Z" - } - }, - "name": "config_19", - "intervals": { - "config_19": { - "start": 1566246371656, - "end": 1566246373125 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:11.535Z", - "end": "2019-08-19T20:26:13.485Z" - } - }, - "name": "config_20", - "intervals": { - "config_20": { - "start": 1566246371535, - "end": 1566246373485 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:16.579Z", - "end": "2019-08-19T20:26:17.862Z" - } - }, - "name": "config_21", - "intervals": { - "config_21": { - "start": 1566246376579, - "end": 1566246377862 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:14.949Z", - "end": "2019-08-19T20:26:15.477Z" - } - }, - "name": "config_22", - "intervals": { - "config_22": { - "start": 1566246374949, - "end": 1566246375477 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:12.686Z", - "end": "2019-08-19T20:26:16.979Z" - } - }, - "name": "config_23", - "intervals": { - "config_23": { - "start": 1566246372686, - "end": 1566246376979 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:10.204Z", - "end": "2019-08-19T20:26:10.241Z" - } - }, - "name": "config_24", - "intervals": { - "config_24": { - "start": 1566246370204, - "end": 1566246370241 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:15.130Z", - "end": "2019-08-19T20:26:16.132Z" - } - }, - "name": "config_25", - "intervals": { - "config_25": { - "start": 1566246375130, - "end": 1566246376132 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:10.275Z", - "end": "2019-08-19T20:26:11.531Z" - } - }, - "name": "config_26", - "intervals": { - "config_26": { - "start": 1566246370275, - "end": 1566246371531 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:15.332Z", - "end": "2019-08-19T20:26:16.175Z" - } - }, - "name": "config_27", - "intervals": { - "config_27": { - "start": 1566246375332, - "end": 1566246376175 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:18.160Z", - "end": "2019-08-19T20:26:18.459Z" - } - }, - "name": "config_28", - "intervals": { - "config_28": { - "start": 1566246378160, - "end": 1566246378459 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "end": "2019-08-19T20:26:16.232Z" - } - }, - "name": "config_29", - "intervals": { - "config_29": { - "end": 1566246376232 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:10.935Z", - "end": "2019-08-19T20:26:12.079Z" - } - }, - "name": "config_30", - "intervals": { - "config_30": { - "start": 1566246370935, - "end": 1566246372079 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:10.535Z", - "end": "2019-08-19T20:26:14.902Z" - } - }, - "name": "config_31", - "intervals": { - "config_31": { - "start": 1566246370535, - "end": 1566246374902 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:10.504Z", - "end": "2019-08-19T20:26:11.168Z" - } - }, - "name": "config_32", - "intervals": { - "config_32": { - "start": 1566246370504, - "end": 1566246371168 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:10.451Z", - "end": "2019-08-19T20:26:12.556Z" - } - }, - "name": "config_33", - "intervals": { - "config_33": { - "start": 1566246370451, - "end": 1566246372556 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "end": "2019-08-19T20:26:12.074Z" - } - }, - "name": "config_34", - "intervals": { - "config_34": { - "end": 1566246372074 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:15.979Z", - "end": "2019-08-19T20:26:17.239Z" - } - }, - "name": "config_35", - "intervals": { - "config_35": { - "start": 1566246375979, - "end": 1566246377239 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:16.705Z" - } - }, - "name": "config_36", - "intervals": { - "config_36": { - "start": 1566246376705 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:10.488Z", - "end": "2019-08-19T20:26:10.558Z" - } - }, - "name": "config_37", - "intervals": { - "config_37": { - "start": 1566246370488, - "end": 1566246370558 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:12.666Z", - "end": "2019-08-19T20:26:14.697Z" - } - }, - "name": "config_38", - "intervals": { - "config_38": { - "start": 1566246372666, - "end": 1566246374697 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "end": "2019-08-19T20:26:11.724Z" - } - }, - "name": "config_39", - "intervals": { - "config_39": { - "end": 1566246371724 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:12.247Z", - "end": "2019-08-19T20:26:16.598Z" - } - }, - "name": "config_40", - "intervals": { - "config_40": { - "start": 1566246372247, - "end": 1566246376598 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:18.177Z", - "end": "2019-08-19T20:26:19.141Z" - } - }, - "name": "config_41", - "intervals": { - "config_41": { - "start": 1566246378177, - "end": 1566246379141 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:13.706Z", - "end": "2019-08-19T20:26:17.062Z" - } - }, - "name": "config_42", - "intervals": { - "config_42": { - "start": 1566246373706, - "end": 1566246377062 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:15.698Z", - "end": "2019-08-19T20:26:16.650Z" - } - }, - "name": "config_43", - "intervals": { - "config_43": { - "start": 1566246375698, - "end": 1566246376650 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:12.521Z", - "end": "2019-08-19T20:26:18.073Z" - } - }, - "name": "config_44", - "intervals": { - "config_44": { - "start": 1566246372521, - "end": 1566246378073 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:15.330Z", - "end": "2019-08-19T20:26:16.078Z" - } - }, - "name": "config_45", - "intervals": { - "config_45": { - "start": 1566246375330, - "end": 1566246376078 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:12.198Z", - "end": "2019-08-19T20:26:17.684Z" - } - }, - "name": "config_46", - "intervals": { - "config_46": { - "start": 1566246372198, - "end": 1566246377684 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:16.702Z", - "end": "2019-08-19T20:26:18.731Z" - } - }, - "name": "config_47", - "intervals": { - "config_47": { - "start": 1566246376702, - "end": 1566246378731 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:19.804Z", - "end": "2019-08-19T20:26:19.844Z" - } - }, - "name": "config_48", - "intervals": { - "config_48": { - "start": 1566246379804, - "end": 1566246379844 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:10.817Z", - "end": "2019-08-19T20:26:12.877Z" - } - }, - "name": "config_49", - "intervals": { - "config_49": { - "start": 1566246370817, - "end": 1566246372877 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:13.486Z", - "end": "2019-08-19T20:26:13.780Z" - } - }, - "name": "config_50", - "intervals": { - "config_50": { - "start": 1566246373486, - "end": 1566246373780 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:17.863Z", - "end": "2019-08-19T20:26:19.968Z" - } - }, - "name": "config_51", - "intervals": { - "config_51": { - "start": 1566246377863, - "end": 1566246379968 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:15.478Z", - "end": "2019-08-19T20:26:18.256Z" - } - }, - "name": "config_52", - "intervals": { - "config_52": { - "start": 1566246375478, - "end": 1566246378256 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:16.980Z", - "end": "2019-08-19T20:26:18.154Z" - } - }, - "name": "config_53", - "intervals": { - "config_53": { - "start": 1566246376980, - "end": 1566246378154 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:10.242Z", - "end": "2019-08-19T20:26:15.825Z" - } - }, - "name": "config_54", - "intervals": { - "config_54": { - "start": 1566246370242, - "end": 1566246375825 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:16.133Z", - "end": "2019-08-19T20:26:19.608Z" - } - }, - "name": "config_55", - "intervals": { - "config_55": { - "start": 1566246376133, - "end": 1566246379608 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:11.532Z", - "end": "2019-08-19T20:26:16.209Z" - } - }, - "name": "config_56", - "intervals": { - "config_56": { - "start": 1566246371532, - "end": 1566246376209 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:16.176Z", - "end": "2019-08-19T20:26:17.106Z" - } - }, - "name": "config_57", - "intervals": { - "config_57": { - "start": 1566246376176, - "end": 1566246377106 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:18.460Z", - "end": "2019-08-19T20:26:19.501Z" - } - }, - "name": "config_58", - "intervals": { - "config_58": { - "start": 1566246378460, - "end": 1566246379501 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:16.233Z", - "end": "2019-08-19T20:26:17.937Z" - } - }, - "name": "config_59", - "intervals": { - "config_59": { - "start": 1566246376233, - "end": 1566246377937 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": {} - }, - "name": "config_60", - "intervals": { - "config_60": {} - } - } -] \ No newline at end of file diff --git a/tests/fixtures/time/time-test-dimensions.json b/tests/fixtures/time/time-test-dimensions.json deleted file mode 100644 index 527f8ee..0000000 --- a/tests/fixtures/time/time-test-dimensions.json +++ /dev/null @@ -1,21 +0,0 @@ -[ - { - "dimensions": [ - { - "environment": { - "testing": null, - "prod": null - } - }, - { - "device": { - "desktop": null, - "mobile": { - "table": null, - "smartphone": null - } - } - } - ] - } -] \ No newline at end of file diff --git a/tests/fixtures/time/time-test.json b/tests/fixtures/time/time-test.json deleted file mode 100644 index fd2f859..0000000 --- a/tests/fixtures/time/time-test.json +++ /dev/null @@ -1,1122 +0,0 @@ -[ - { - "dimensions": [ - { - "environment": { - "testing": null, - "prod": null - } - }, - { - "device": { - "desktop": null, - "mobile": { - "table": null, - "smartphone": null - } - } - } - ] - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "end": "2019-08-19T20:26:12.247Z" - } - }, - "name": "config_0", - "intervals": { - "config_0": { - "end": 1566246372247 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:12.162Z", - "end": "2019-08-19T20:26:18.177Z" - } - }, - "name": "config_1", - "intervals": { - "config_1": { - "start": 1566246372162, - "end": 1566246378177 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:11.998Z", - "end": "2019-08-19T20:26:13.706Z" - } - }, - "name": "config_2", - "intervals": { - "config_2": { - "start": 1566246371998, - "end": 1566246373706 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:12.186Z", - "end": "2019-08-19T20:26:15.698Z" - } - }, - "name": "config_3", - "intervals": { - "config_3": { - "start": 1566246372186, - "end": 1566246375698 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:10.910Z", - "end": "2019-08-19T20:26:12.521Z" - } - }, - "name": "config_4", - "intervals": { - "config_4": { - "start": 1566246370910, - "end": 1566246372521 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:13.535Z", - "end": "2019-08-19T20:26:15.330Z" - } - }, - "name": "config_5", - "intervals": { - "config_5": { - "start": 1566246373535, - "end": 1566246375330 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:10.918Z", - "end": "2019-08-19T20:26:12.198Z" - } - }, - "name": "config_6", - "intervals": { - "config_6": { - "start": 1566246370918, - "end": 1566246372198 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "end": "2019-08-19T20:26:16.702Z" - } - }, - "name": "config_7", - "intervals": { - "config_7": { - "end": 1566246376702 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:16.316Z", - "end": "2019-08-19T20:26:19.804Z" - } - }, - "name": "config_8", - "intervals": { - "config_8": { - "start": 1566246376316, - "end": 1566246379804 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:10.607Z", - "end": "2019-08-19T20:26:10.817Z" - } - }, - "name": "config_9", - "intervals": { - "config_9": { - "start": 1566246370607, - "end": 1566246370817 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:12.020Z" - } - }, - "name": "config_10", - "intervals": { - "config_10": { - "start": 1566246372020 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:10.239Z", - "end": "2019-08-19T20:26:19.483Z" - } - }, - "name": "config_11", - "intervals": { - "config_11": { - "start": 1566246370239, - "end": 1566246379483 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:10.086Z", - "end": "2019-08-19T20:26:10.307Z" - } - }, - "name": "config_12", - "intervals": { - "config_12": { - "start": 1566246370086, - "end": 1566246370307 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:16.912Z" - } - }, - "name": "config_13", - "intervals": { - "config_13": { - "start": 1566246376912 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:13.364Z", - "end": "2019-08-19T20:26:13.388Z" - } - }, - "name": "config_14", - "intervals": { - "config_14": { - "start": 1566246373364, - "end": 1566246373388 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:12.985Z", - "end": "2019-08-19T20:26:16.035Z" - } - }, - "name": "config_15", - "intervals": { - "config_15": { - "start": 1566246372985, - "end": 1566246376035 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:12.913Z", - "end": "2019-08-19T20:26:16.893Z" - } - }, - "name": "config_16", - "intervals": { - "config_16": { - "start": 1566246372913, - "end": 1566246376893 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:15.609Z", - "end": "2019-08-19T20:26:18.796Z" - } - }, - "name": "config_17", - "intervals": { - "config_17": { - "start": 1566246375609, - "end": 1566246378796 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "end": "2019-08-19T20:26:10.480Z" - } - }, - "name": "config_18", - "intervals": { - "config_18": { - "end": 1566246370480 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:11.656Z", - "end": "2019-08-19T20:26:13.125Z" - } - }, - "name": "config_19", - "intervals": { - "config_19": { - "start": 1566246371656, - "end": 1566246373125 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:11.535Z", - "end": "2019-08-19T20:26:13.485Z" - } - }, - "name": "config_20", - "intervals": { - "config_20": { - "start": 1566246371535, - "end": 1566246373485 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:16.579Z", - "end": "2019-08-19T20:26:17.862Z" - } - }, - "name": "config_21", - "intervals": { - "config_21": { - "start": 1566246376579, - "end": 1566246377862 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:14.949Z", - "end": "2019-08-19T20:26:15.477Z" - } - }, - "name": "config_22", - "intervals": { - "config_22": { - "start": 1566246374949, - "end": 1566246375477 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:12.686Z", - "end": "2019-08-19T20:26:16.979Z" - } - }, - "name": "config_23", - "intervals": { - "config_23": { - "start": 1566246372686, - "end": 1566246376979 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:10.204Z", - "end": "2019-08-19T20:26:10.241Z" - } - }, - "name": "config_24", - "intervals": { - "config_24": { - "start": 1566246370204, - "end": 1566246370241 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:15.130Z", - "end": "2019-08-19T20:26:16.132Z" - } - }, - "name": "config_25", - "intervals": { - "config_25": { - "start": 1566246375130, - "end": 1566246376132 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:10.275Z", - "end": "2019-08-19T20:26:11.531Z" - } - }, - "name": "config_26", - "intervals": { - "config_26": { - "start": 1566246370275, - "end": 1566246371531 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:15.332Z", - "end": "2019-08-19T20:26:16.175Z" - } - }, - "name": "config_27", - "intervals": { - "config_27": { - "start": 1566246375332, - "end": 1566246376175 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:18.160Z", - "end": "2019-08-19T20:26:18.459Z" - } - }, - "name": "config_28", - "intervals": { - "config_28": { - "start": 1566246378160, - "end": 1566246378459 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "end": "2019-08-19T20:26:16.232Z" - } - }, - "name": "config_29", - "intervals": { - "config_29": { - "end": 1566246376232 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:10.935Z", - "end": "2019-08-19T20:26:12.079Z" - } - }, - "name": "config_30", - "intervals": { - "config_30": { - "start": 1566246370935, - "end": 1566246372079 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:10.535Z", - "end": "2019-08-19T20:26:14.902Z" - } - }, - "name": "config_31", - "intervals": { - "config_31": { - "start": 1566246370535, - "end": 1566246374902 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:10.504Z", - "end": "2019-08-19T20:26:11.168Z" - } - }, - "name": "config_32", - "intervals": { - "config_32": { - "start": 1566246370504, - "end": 1566246371168 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:10.451Z", - "end": "2019-08-19T20:26:12.556Z" - } - }, - "name": "config_33", - "intervals": { - "config_33": { - "start": 1566246370451, - "end": 1566246372556 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "end": "2019-08-19T20:26:12.074Z" - } - }, - "name": "config_34", - "intervals": { - "config_34": { - "end": 1566246372074 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:15.979Z", - "end": "2019-08-19T20:26:17.239Z" - } - }, - "name": "config_35", - "intervals": { - "config_35": { - "start": 1566246375979, - "end": 1566246377239 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:16.705Z" - } - }, - "name": "config_36", - "intervals": { - "config_36": { - "start": 1566246376705 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:10.488Z", - "end": "2019-08-19T20:26:10.558Z" - } - }, - "name": "config_37", - "intervals": { - "config_37": { - "start": 1566246370488, - "end": 1566246370558 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:12.666Z", - "end": "2019-08-19T20:26:14.697Z" - } - }, - "name": "config_38", - "intervals": { - "config_38": { - "start": 1566246372666, - "end": 1566246374697 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "end": "2019-08-19T20:26:11.724Z" - } - }, - "name": "config_39", - "intervals": { - "config_39": { - "end": 1566246371724 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:12.247Z", - "end": "2019-08-19T20:26:16.598Z" - } - }, - "name": "config_40", - "intervals": { - "config_40": { - "start": 1566246372247, - "end": 1566246376598 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:18.177Z", - "end": "2019-08-19T20:26:19.141Z" - } - }, - "name": "config_41", - "intervals": { - "config_41": { - "start": 1566246378177, - "end": 1566246379141 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:13.706Z", - "end": "2019-08-19T20:26:17.062Z" - } - }, - "name": "config_42", - "intervals": { - "config_42": { - "start": 1566246373706, - "end": 1566246377062 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:15.698Z", - "end": "2019-08-19T20:26:16.650Z" - } - }, - "name": "config_43", - "intervals": { - "config_43": { - "start": 1566246375698, - "end": 1566246376650 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:12.521Z", - "end": "2019-08-19T20:26:18.073Z" - } - }, - "name": "config_44", - "intervals": { - "config_44": { - "start": 1566246372521, - "end": 1566246378073 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:15.330Z", - "end": "2019-08-19T20:26:16.078Z" - } - }, - "name": "config_45", - "intervals": { - "config_45": { - "start": 1566246375330, - "end": 1566246376078 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:12.198Z", - "end": "2019-08-19T20:26:17.684Z" - } - }, - "name": "config_46", - "intervals": { - "config_46": { - "start": 1566246372198, - "end": 1566246377684 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:16.702Z", - "end": "2019-08-19T20:26:18.731Z" - } - }, - "name": "config_47", - "intervals": { - "config_47": { - "start": 1566246376702, - "end": 1566246378731 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:19.804Z", - "end": "2019-08-19T20:26:19.844Z" - } - }, - "name": "config_48", - "intervals": { - "config_48": { - "start": 1566246379804, - "end": 1566246379844 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:10.817Z", - "end": "2019-08-19T20:26:12.877Z" - } - }, - "name": "config_49", - "intervals": { - "config_49": { - "start": 1566246370817, - "end": 1566246372877 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:13.486Z", - "end": "2019-08-19T20:26:13.780Z" - } - }, - "name": "config_50", - "intervals": { - "config_50": { - "start": 1566246373486, - "end": 1566246373780 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:17.863Z", - "end": "2019-08-19T20:26:19.968Z" - } - }, - "name": "config_51", - "intervals": { - "config_51": { - "start": 1566246377863, - "end": 1566246379968 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:15.478Z", - "end": "2019-08-19T20:26:18.256Z" - } - }, - "name": "config_52", - "intervals": { - "config_52": { - "start": 1566246375478, - "end": 1566246378256 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:16.980Z", - "end": "2019-08-19T20:26:18.154Z" - } - }, - "name": "config_53", - "intervals": { - "config_53": { - "start": 1566246376980, - "end": 1566246378154 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:10.242Z", - "end": "2019-08-19T20:26:15.825Z" - } - }, - "name": "config_54", - "intervals": { - "config_54": { - "start": 1566246370242, - "end": 1566246375825 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:16.133Z", - "end": "2019-08-19T20:26:19.608Z" - } - }, - "name": "config_55", - "intervals": { - "config_55": { - "start": 1566246376133, - "end": 1566246379608 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": { - "start": "2019-08-19T20:26:11.532Z", - "end": "2019-08-19T20:26:16.209Z" - } - }, - "name": "config_56", - "intervals": { - "config_56": { - "start": 1566246371532, - "end": 1566246376209 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:16.176Z", - "end": "2019-08-19T20:26:17.106Z" - } - }, - "name": "config_57", - "intervals": { - "config_57": { - "start": 1566246376176, - "end": 1566246377106 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod", - "device:smartphone" - ], - "schedule": { - "start": "2019-08-19T20:26:18.460Z", - "end": "2019-08-19T20:26:19.501Z" - } - }, - "name": "config_58", - "intervals": { - "config_58": { - "start": 1566246378460, - "end": 1566246379501 - } - } - }, - { - "settings": { - "dimensions": [ - "master" - ], - "schedule": { - "start": "2019-08-19T20:26:16.233Z", - "end": "2019-08-19T20:26:17.937Z" - } - }, - "name": "config_59", - "intervals": { - "config_59": { - "start": 1566246376233, - "end": 1566246377937 - } - } - }, - { - "settings": { - "dimensions": [ - "environment:prod" - ], - "schedule": {} - }, - "name": "config_60", - "intervals": { - "config_60": {} - } - } -] \ No newline at end of file diff --git a/tests/fixtures/touchdown-simple/configs/no-master.js b/tests/fixtures/touchdown-simple/configs/no-master.js deleted file mode 100644 index 192039d..0000000 --- a/tests/fixtures/touchdown-simple/configs/no-master.js +++ /dev/null @@ -1,25 +0,0 @@ - -module.exports = [ - { - settings: [ 'device:mobile' ], - selector: 'mobile' - }, - { - settings: { - dimensions: ["device:mobile"], - schedule: { - end: "2010-11-29T00:04:00Z" - } - }, - name: 'old' - }, - { - settings: { - dimensions: ["device:mobile"], - schedule: { - start: "2010-11-29T00:04:00Z" - } - }, - name: 'new' - } -]; diff --git a/tests/fixtures/touchdown-simple/configs/undefined-config.js b/tests/fixtures/touchdown-simple/configs/undefined-config.js deleted file mode 100644 index 2e4de5f..0000000 --- a/tests/fixtures/touchdown-simple/configs/undefined-config.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2013 Yahoo! Inc. All rights reserved. - * Copyrights licensed under the BSD License. - * See the accompanying LICENSE.txt file for terms. - */ - - -module.exports = [ - { - settings: [ 'master' ], - TODO: 'TODO' - }, - { - settings: [ 'device:mobile' ], - selector: 'mobile', - foo: undefined - } -]; diff --git a/tests/lib/cache-test.js b/tests/lib/cache-test.js deleted file mode 100644 index 605343e..0000000 --- a/tests/lib/cache-test.js +++ /dev/null @@ -1,307 +0,0 @@ - -/*jslint nomen:true, anon:true, node:true, esversion:6 */ -/*globals describe, it */ -"use strict"; - - -var expect = require('chai').expect, - LRU = require('../../lib/cache'); - -// expect().to.deep.equal() cares about order of keys -// but very often we don't -function compareObjects(have, want) { - expect(typeof have).to.equal(typeof want); - if ('object' === typeof want) { - // order of keys doesn't matter - if (Object.keys(want).length) { - expect(have).to.have.keys(Object.keys(want)); - } - if (Object.keys(have).length) { - expect(want).to.have.keys(Object.keys(have)); - } - Object.keys(want).forEach(function (key) { - compareObjects(have[key], want[key]); - }); - } else { - expect(have).to.deep.equal(want); - } -} - -function assertEqual(a,b) { - expect(a).to.equal(b); -} - -var p = [91, 7, 20, 50]; -//public domain http://pracrand.sourceforge.net/license.txt -function prng() { - for(var i=0; i<4; i++) { - p[i] >>>= 0; - } - var x = p[0] + p[1] | 0; - p[0] = p[1] ^ (p[1] >>> 9); - p[1] = p[2] + 8*p[2] | 0; - p[2] = p[2] * 2097152 | p[2] >>> 11; - p[3]++; - p[3] |= 0; - x += p[3]; - x |= 0; - p[2] += x; - p[2] |= 0; - return (x >>> 0) / 4294967296; -} - -function getRandomInt(max) { - return Math.floor(prng() * max); -} - -//verify internal structure of the cache -function validateCacheStructure(cache) { - var i; - var refs = new Map(); - var current = cache.youngest; - assertEqual(cache.size >= 0, true); - assertEqual(cache.size <= cache.max, true); - assertEqual(cache.size === cache.map.size, true); - if(cache.size === 0) { - assertEqual(cache.youngest, null); - assertEqual(cache.oldest, null); - return; - } - refs.set(current, 1); - for(i=0; i current.next.value, true); - current = current.next; - } - validateCacheStructure(cache); - } - }); - it('entries should be ordered by age of set', function () { - var counter = 0; - var cache = new LRU({max: 40}); - for(var i=0; i<1000; i++) { - cache.set(getRandomInt(150), counter++, 1); - var current = cache.youngest; - while(current.next !== null) { - assertEqual(current.value > current.next.value, true); - current = current.next; - } - validateCacheStructure(cache); - } - }); - it('entries should be ordered by age of set and get', function () { - var counter = 0; - var cache = new LRU({max: 40}); - for(var i=0; i<1000; i++) { - var key = getRandomInt(150); - if(prng() > 0.5) { - if(cache.get(key, 1) !== undefined) { - cache.map.get(key).value = counter++; //manually update entries age value - } - } else { - cache.set(key, counter++, 1); - } - if(cache.size > 0) { - var current = cache.youngest; - while (current.next !== null) { - assertEqual(current.value > current.next.value, true); - current = current.next; - } - } - validateCacheStructure(cache); - } - }); - }); - - describe('staleness', function () { - it('should not return mismatched groups', function () { - var cache = new LRU({max: 20}); - cache.set('foo', 'bar', 1); - assertEqual(cache.get('foo', 2), undefined); - }); - it('should not return mismatched groups time aware', function () { - var cache = new LRU({max: 20}); - cache.setTimeAware('foo', 'bar', 10, 1000, 1); - assertEqual(cache.getTimeAware('foo', 500, 2), undefined); - }); - it('should not return expired keys', function () { - var cache = new LRU({max: 20}); - cache.setTimeAware('foo', 'bar', 10, 1000, 1); - assertEqual(cache.getTimeAware('foo', 2000, 1), undefined); - }); - it('should not return keys that expire at same time', function () { - var cache = new LRU({max: 20}); - cache.setTimeAware('foo', 'bar', 10, 1000, 1); - assertEqual(cache.getTimeAware('foo', 1000, 1), undefined); - }); - it('should not return keys set in the future', function () { - var cache = new LRU({max: 20}); - cache.setTimeAware('foo', 'bar', 10, 1000, 1); - assertEqual(cache.getTimeAware('foo', 5, 1), undefined); - }); - it('should return keys set at same time', function () { - var cache = new LRU({max: 20}); - cache.setTimeAware('foo', 'bar', 10, 1000, 1); - assertEqual(cache.getTimeAware('foo', 10, 1), 'bar'); - }); - it('should return keys expiring in one tick', function () { - var cache = new LRU({max: 20}); - cache.setTimeAware('foo', 'bar', 10, 1000, 1); - assertEqual(cache.getTimeAware('foo', 999, 1), 'bar'); - }); - }); - - describe('time', function () { - it('expire entries', function () { - var cache = new LRU({max: 40}); - var maxTime = 2000; - for(var i=0; i<1000; i++) { - var key = getRandomInt(150); - var now = getRandomInt(maxTime); - if (prng() > 0.5) { - var val = cache.getTimeAware(key, now, 1); - if (val !== undefined) { - assertEqual(val.set <= now, true); - assertEqual(val.expires > now, true); - } - } else { - var expiresAt = now + getRandomInt(maxTime-now); - cache.setTimeAware(key, {set:now, expires:expiresAt}, now, expiresAt, 1); - } - validateCacheStructure(cache); - } - }); - it('expire entries with realistic times', function () { - var cache = new LRU({max: 40}); - var maxTime = 1567586543000; - for(var i=0; i<1000; i++) { - var key = getRandomInt(150); - var now = getRandomInt(maxTime) + 1566586543000; - if (prng() > 0.5) { - var val = cache.getTimeAware(key, now, 1); - if (val !== undefined) { - assertEqual(val.set <= now, true); - assertEqual(val.expires > now, true); - } - } else { - var expiresAt = now + getRandomInt(maxTime-now); - cache.setTimeAware(key, {set:now, expires:expiresAt}, now, expiresAt, 1); - } - validateCacheStructure(cache); - } - }); - }); - - describe('internal structure', function () { - it('should be a mapped doubly linked list', function () { - var n = 10; - var cache = new LRU({max: n}); - for(var i=0; i time && interval.start < next) { - next = interval.start; - } - } - if(interval.end) { - valid = valid && interval.end >= time; - if(valid && interval.end < next) { - next = interval.end+1; - } - } - if(valid) { - applicable.push(interval.name); - } - } - next = next === Number.POSITIVE_INFINITY ? undefined : next; - return {configs: applicable, next: next}; - } - - it('scheduled configs should match timestamp', function (done) { - var bundle, ycb; - var path = libpath.join(__dirname, '..', 'fixtures' , 'time', 'time-test.json'); - var data = libfs.readFileSync(path, 'utf8'); - bundle = JSON.parse(data); - var intervals = []; - var minTime = Number.POSITIVE_INFINITY; - var maxTime = 0; - bundle.forEach(function(config) { - if(config.settings) { - var name = config.name; - var interval = config.intervals[name]; - if(interval.start || interval.end) { - if(interval.start && interval.start < minTime) { - minTime = interval.start; - } - if(interval.end && interval.end > maxTime) { - maxTime = interval.end; - } - interval = {start: interval.start, end: interval.end, name:name}; - intervals.push(interval); - } - } - }); - var config = new Config({'timeAware': true, cache: {max: 1000}}); - config.addConfig( - 'test', - 'dimensions', - libpath.join(__dirname, '..', 'fixtures' , 'time', 'time-test-dimensions.json'), - function() { - config.addConfig( - 'test', - 'configs', - libpath.join(__dirname, '..', 'fixtures' , 'time', 'time-test-configs.json'), - function(err) { - var context = {environment: 'prod', device: 'smartphone'}; - var times = []; - for(var t=minTime-2; t