Browse files

fix bug 747166, fix bug 757151: Improved caching

Now using memcache for response and template loader caching.

* Moved page inclusion helper from dekiwiki API stub into kuma API.

* Reload (Cache-Control: max-age=0) will cause just the current page to
  be re-evaluated

* Shift-reload (Cache-Control: no-cache) will cause the current page and
  all cached includes to be revalidated - eg. auto-required template
  libs, content included from other pages, etc.

* Use memcache for response caching and template loaders, instead of
  process-local memory. This will allow both multiple node processes
  and the Django app to share common cache data and invalidation cues.

* Honor Cache-Control: no-cache down at the loader cache level, so that
  auto-required libs and content included from other pages gets
  revalidated when a user shift-reloads

* Separate template source loading from compilation, to make source
  caching easier.

* Move FakeMemcached to utils.js, since it's used by more than api.js
  now. Also, md5 function added.

* Fixes to template loading in test-macros.js broken test.

* Remove local memory caching test in test-loaders.js, since that's no
  longer applicable.
  • Loading branch information...
1 parent 050da53 commit f05d343b04005d2bb9e688ca7d72130535e380c6 @lmorchard lmorchard committed Apr 23, 2012
View
26 TODO.md
@@ -3,31 +3,17 @@ TODO
## v1.0
+* Cache invalidation in KS
+ * In Django, set kuma:modified:{full path hash} = modified whenever a Document is saved
+ * In KS, store cached-at times for everything cached
+ * Consider KS cached data invalid if the cached-at time is older than the
+ modified-at time of the page on which the cached data is based.
+
* Problem with kuma page slugs containing spaces
* Problem with HTML encoding, can't use an URL with ampersands as the parameter
to a macro.
-* bug 730707: Complete the MindTouch-compat API
- * Continue burning through most-used and longest-source MDN templates
-
-* General kumascript env and metadata vars in headers with JSON-encoded values?
- * For use by the API methods.
- * Examples:
- * x-kumascript-var-locale: "en-US"
- * x-kumascript-var-slug: "DOM/Storage"
- * x-kumascript-var-title: "DOM Storage"
- * x-kumascript-var-username: "lmorchard"
- * x-kumascript-var-userlocale: "en-US"
- * x-kumascript-var-tags: [ "JavaScript", "HTML5", "CSS" ]
-
-* bug 731655: Handle language alternates in markup?
- * eg. span lang="en-US"; lang="zh-CN"; lang="*"
- * See also Template:JSInherits
-
-* More backends for response caching
- * memcache backend, local memory with LRU backend
-
## Future
* Revisit Sandbox, or some kind of process separation for executing templates
View
80 lib/kumascript/api.js
@@ -109,51 +109,45 @@ var KumaAPI = ks_utils.Class(BaseAPI, {
var doc_url = $this.baseUrl() + path + '?raw';
var opts = { method: 'HEAD', url: doc_url };
request(opts, function (err, resp, body) {
+ var result = '';
if (resp && 200 == resp.statusCode) {
result = true;
}
next(result);
});
- })
- }
-
-});
+ });
+ },
-// ### PageAPI
-// <http://developer.mindtouch.com/en/docs/DekiScript/Reference/Wiki_Functions_and_Variables/Page>
-var PageAPI = ks_utils.Class(BaseAPI, {
-
- initialize: function (options) {
- BaseAPI.prototype.initialize.call(this, options);
+ // #### include
+ // Include content rendered from another page
+ // TODO: Support more DekiScript params like:
+ // * section - Section name
+ // * revision - revision #
+ // * heading - (1-5) set the current heading level, adjust headings in included content
+ // [See also](http://developer.mindtouch.com/en/docs/DekiScript/Reference/Wiki_Functions_and_Variables/Wiki.Page)
+ include: function (path) {
var $this = this;
-
- // TODO: Need to thread through page details from Server to APIContext to here.
- this.setVars({
- uri: 'http://example.com/en/HTML/FakePage',
- language: 'en'
+ var key = 'kumascript:include:' + $this.parent.md5(path.toLowerCase());
+ return this.parent.cacheFn(key, 3600, function (next) {
+ var doc_url = $this.baseUrl() + path + '?raw=1&macros=1&include=1';
+ var opts = {
+ method: 'GET',
+ headers: { 'Cache-Control': $this.parent.env.cache_control },
+ url: doc_url
+ };
+ request(opts, function (err, resp, body) {
+ var result = '';
+ if (resp && 200 == resp.statusCode) {
+ result = body;
+ }
+ next(result);
+ });
});
+
}
});
-// ### FakeMemcached
-//
-// A minimal stub replacement for Memcached, in case it's missing from the
-// config. That way, kumascript can be used without memcache, even if that's
-// not recommended.
-var FakeMemcached = ks_utils.Class({
- initialize: function (options) {
- this._cache = {};
- },
- set: function (key, value, tm_out, next) {
- this._cache[key] = value;
- next(null, true);
- },
- get: function (key, next) {
- next(null, this._cache[key]);
- }
-});
-
// ### APIContext
//
// Instances of this class manage instances of sub-APIs, supplying them with
@@ -187,7 +181,7 @@ var APIContext = ks_utils.Class({
this.memcached = new Memcached(mo.server, mo.options || {});
} else {
// If the configuration is missing, use the fake stub cache
- this.memcached = new FakeMemcached();
+ this.memcached = new ks_utils.FakeMemcached();
}
// Create a new cache for required templates.
@@ -209,7 +203,11 @@ var APIContext = ks_utils.Class({
//
// TODO: Very permissive. Should there be more restrictions on net access?
request: request,
-
+
+ // #### md5
+ // Return the MD5 hex digest of the given utf8 string
+ md5: ks_utils.md5,
+
// Install a new instance of the given API class, with the given name.
installAPI: function (cls, name) {
setCaseVariantAliases(this, name, new cls({parent: this}));
@@ -286,22 +284,14 @@ var APIContext = ks_utils.Class({
mc.set(key, val, tm_out, function (err, c_result) {
result = val;
f['return']();
- })
- })
+ });
+ });
}
});
f.wait();
return result;
},
- // #### md5
- // Return the MD5 hex digest of the given utf8 string
- md5: function (str) {
- var md5 = crypto.createHash('md5')
- md5.update(str, 'utf8');
- return md5.digest('hex')
- },
-
// #### template(name, arguments)
//
// Attempt to load and execute a template with the given name and
View
147 lib/kumascript/caching.js
@@ -6,13 +6,13 @@
var util = require('util'),
url = require('url'),
crypto = require('crypto'),
+ Memcached = require('memcached'),
_ = require('underscore'),
// TODO: Someday, strip out kumascript dependencies and spin this off into
// its own package.
ks_utils = require(__dirname + '/utils');
-
// ## ResponseCache
//
// Caching machinery for HTTP responses
@@ -29,22 +29,16 @@ module.exports.ResponseCache = ks_utils.Class({
ERR_MISS: 'MISS',
initialize: function (options) {
- this.meta_cache = {};
- this.content_cache = {};
+ // Create a memcache instance, if necessary
+ if (this.options.memcache) {
+ var mo = this.options.memcache;
+ this.memcached = new Memcached(mo.server, mo.options || {});
+ } else {
+ // If the configuration is missing, use the fake stub cache
+ this.memcached = new ks_utils.FakeMemcached();
+ }
},
- // These methods are crappy, and need to be replaced with switchable
- // backends. (eg. memcache, filesystem, local memory with LRU)
- // TODO: Need a way to clear a cache entry
- // TODO: Implement LRU logic! use connect/lib/cache.js?
- // TODO: Separate into switchable backends (eg. locmem, memcache, fs)
- _setMeta: function (key, obj) { this.meta_cache[key] = obj; },
- _hasMeta: function (key) { return (key in this.meta_cache); },
- _getMeta: function (key) { return this.meta_cache[key]; },
- _setContent: function (key, obj) { this.content_cache[key] = obj; },
- _hasContent: function (key) { return (key in this.content_cache); },
- _getContent: function (key) { return this.content_cache[key]; },
-
// ### cacheResponse
//
// Wrapper for a response handler. Will cache fresh responses from the
@@ -62,7 +56,7 @@ module.exports.ResponseCache = ks_utils.Class({
var method = req.method.toUpperCase(),
supported_methods = $this.methods;
if (supported_methods.indexOf(method) === -1) {
- return $this.revalidate(null, req, res, null, response_cb);
+ return $this.revalidate(null, {}, req, res, null, response_cb);
}
// Start building a cache key with the URL path.
@@ -100,13 +94,13 @@ module.exports.ResponseCache = ks_utils.Class({
$this.options.max_age : ua_cc['max-age']
};
- $this.get(cache_key, opts, function (err, entry, meta) {
+ $this.get(cache_key, opts, function (err, headers, body, meta) {
if (err == $this.ERR_MISS || err == $this.ERR_STALE) {
- return $this.revalidate(cache_key, req, res, err, response_cb);
+ return $this.revalidate(cache_key, opts, req, res, err, response_cb);
} else if (err == $this.ERR_NOT_MODIFIED) {
return $this.sendNotModified(req, res, meta);
} else {
- return $this.sendCacheEntry(cache_key, req, res, entry, meta);
+ return $this.sendCacheEntry(cache_key, req, res, headers, body, meta);
}
});
},
@@ -140,7 +134,7 @@ module.exports.ResponseCache = ks_utils.Class({
// Lots of monkeypatching here to intercept the response events and build
// the cache entry for storage.
//
- revalidate: function (cache_key, req, res, err, response_cb) {
+ revalidate: function (cache_key, opts, req, res, err, response_cb) {
var $this = this,
cached_headers = [],
cached_meta = { },
@@ -177,24 +171,33 @@ module.exports.ResponseCache = ks_utils.Class({
orig_writeHead.apply(this, arguments);
};
+ // Cache body chunks as they come in.
+ var cache_chunk = function (chunk, encoding) {
+ // TODO: This *could* be trouble, if the response encoding isn't
+ // UTF-8. The cache will have it encoded as UTF-8, but headers will
+ // say otherwise. Probably good for now, though, since we're using
+ // UTF-8 end-to-end in kumascript.
+ cached_body_chunks.push(new Buffer(chunk, encoding)
+ .toString('utf-8'));
+ };
+
// Monkeypatch to capture written body chunks
var orig_write = res.write;
res.write = function (chunk, encoding) {
orig_write.apply(this, arguments);
- cached_body_chunks.push([chunk, encoding]);
+ cache_chunk(chunk, encoding);
};
// Monkeypatch to capture the final send and cache the response
var orig_send = res.send;
res.send = function (chunk, encoding) {
orig_send.apply(this, arguments);
if (200 == status_code) {
- if (chunk) {
- cached_body_chunks.push([chunk, encoding]);
- }
- var entry = [ cached_headers, cached_body_chunks ];
- $this.set(cache_key, entry, cached_meta,
- function (err, entry, meta) {
+ // Catch the last chunk, if any.
+ if (chunk) { cache_chunk(chunk, encoding); }
+ var cached_body = cached_body_chunks.join('');
+ $this.set(cache_key, opts.max_age, cached_headers, cached_body, cached_meta,
+ function (err, headers, body, meta) {
/* No-op, fire and forget. */
}
);
@@ -211,10 +214,8 @@ module.exports.ResponseCache = ks_utils.Class({
//
// Send the content from the cached response entry
//
- sendCacheEntry: function (cache_key, req, res, entry, meta) {
- var $this = this,
- headers = entry[0],
- chunks = entry[1];
+ sendCacheEntry: function (cache_key, req, res, headers, body, meta) {
+ var $this = this;
$this.sendCommonHeaders(req, res, meta);
@@ -226,9 +227,7 @@ module.exports.ResponseCache = ks_utils.Class({
var method = req.method.toUpperCase();
if ('HEAD' != method) {
- _.each(chunks, function (chunk) {
- res.write(chunk[0], chunk[1]);
- });
+ res.write(body, 'utf-8');
}
res.end();
@@ -266,11 +265,17 @@ module.exports.ResponseCache = ks_utils.Class({
//
// Store a cache entry
//
- set: function (key, content, meta, next) {
+ set: function (key, max_age, headers, body, meta, next) {
+ var $this = this;
meta = meta || {};
- this._setMeta(key, meta);
- this._setContent(key, content);
- next(null, content, meta);
+ max_age = max_age || this.options.max_age;
+ $this.memcached.set(key + ':headers', headers, max_age, function (e, r) {
+ $this.memcached.set(key + ':body', body, max_age, function (e, r) {
+ $this.memcached.set(key + ':meta', meta, max_age, function (e, r) {
+ next(null, headers, body, meta);
+ });
+ });
+ });
},
// ### get
@@ -282,48 +287,50 @@ module.exports.ResponseCache = ks_utils.Class({
//
get: function (key, options, next) {
options = options || {};
+ var $this = this;
if (0 === options.max_age) {
- // If I really wanted to, a this._hasMeta(key) would let me report
+ // If I really wanted to, a $this._hasMeta(key) would let me report
// ERR_STALE instead of ERR_MISS. But, I wanted to skip hitting the
// cache backend altogether (eg. memcache or filesystem)
- return next(this.ERR_MISS);
+ return next($this.ERR_MISS);
}
- if (options.no_cache) {
- // See above.
- return next(this.ERR_MISS);
- }
-
- // Punt, if this key is not even in the cache
- if (!this._hasMeta(key)) {
- return next(this.ERR_MISS);
- }
- var meta = this._getMeta(key);
-
- // Check if the cache is too old
- if (typeof(options.max_age) != 'undefined') {
- var now = (new Date()).getTime(),
- last_modified = meta.last_modified,
- age = (now - last_modified) / 1000;
- if (age > options.max_age) {
- return next(this.ERR_STALE, null, meta);
+ // See above.
+ if (options.no_cache) { return next($this.ERR_MISS); }
+
+ $this.memcached.get(key + ':meta', function (err, meta) {
+ // Punt, if $this key is not even in the cache
+ if (!meta) { return next($this.ERR_MISS); }
+
+ // Check if the cache is too old
+ if (typeof(options.max_age) != 'undefined') {
+ var now = (new Date()).getTime(),
+ last_modified = meta.last_modified,
+ age = (now - last_modified) / 1000;
+ if (age > options.max_age) {
+ return next($this.ERR_STALE, null, null, meta);
+ }
}
- }
- // Check modified since, if necessary
- if (typeof(options.if_modified_since) != 'undefined' &&
- (meta.last_modified <= options.if_modified_since)) {
- return next(this.ERR_NOT_MODIFIED, null, meta);
- }
+ // Check modified since, if necessary
+ if (typeof(options.if_modified_since) != 'undefined' &&
+ (meta.last_modified <= options.if_modified_since)) {
+ return next($this.ERR_NOT_MODIFIED, null, null, meta);
+ }
- // Check the content etag, if necessary
- if (typeof(options.if_none_match) != 'undefined' &&
- (options.if_none_match == meta.etag)) {
- return next(this.ERR_NOT_MODIFIED, null, meta);
- }
+ // Check the content etag, if necessary
+ if (typeof(options.if_none_match) != 'undefined' &&
+ (options.if_none_match == meta.etag)) {
+ return next($this.ERR_NOT_MODIFIED, null, null, meta);
+ }
- next(null, this._getContent(key), meta);
+ $this.memcached.get(key + ':headers', function (err, headers) {
+ $this.memcached.get(key + ':body', function (err, body) {
+ next(err, headers, body, meta);
+ });
+ });
+ });
}
});
View
104 lib/kumascript/loaders.js
@@ -8,8 +8,11 @@
// ### Prerequisites
var util = require('util'),
fs = require('fs'),
+ crypto = require('crypto'),
_ = require('underscore'),
request = require('request'),
+ Memcached = require('memcached'),
+
ks_templates = require(__dirname + '/templates'),
ks_utils = require(__dirname + '/utils');
@@ -19,7 +22,14 @@ var util = require('util'),
var BaseLoader = ks_utils.Class({
initialize: function (options) {
- this.cache = {};
+ // Create a memcache instance, if necessary
+ if (this.options.memcache) {
+ var mo = this.options.memcache;
+ this.memcached = new Memcached(mo.server, mo.options || {});
+ } else {
+ // If the configuration is missing, use the fake stub cache
+ this.memcached = new ks_utils.FakeMemcached();
+ }
},
// #### get(name, cb)
@@ -33,39 +43,67 @@ var BaseLoader = ks_utils.Class({
get: function (name, cb) {
var $this = this;
try {
- $this.cache_fetch(name, function (err, tmpl) {
- if (!err) { return cb(null, tmpl); }
- $this.load(name, function (err, tmpl) {
- if (err) { return cb(err, null); }
- $this.cache_store(name, tmpl, cb);
+ $this.cache_fetch(name, function (err, source) {
+ if (source && !err && $this.options.cache_control != 'no-cache') {
+ return $this.compile(source, cb);
+ }
+ $this.load(name, function (err, source) {
+ if (!source || err) {
+ return cb(err, null);
+ }
+ $this.cache_store(name, source, function (err) {
+ return $this.compile(source, cb);
+ });
});
});
} catch (e) {
return cb(e, null);
}
},
+ // #### compile(source, cb)
+ //
+ // Compile the given source using the configured template class, call
+ // cb(error, tmpl_instance)
+ compile: function (source, cb) {
+ var $this = this;
+ try {
+ var tmpl_cls = $this.options.template_class;
+ cb(null, new tmpl_cls({ source: source }));
+ } catch (e) {
+ cb(e, null);
+ }
+ },
+
// #### cache_fetch(name, cb)
//
// Attempt to fetch the named template from cache.
//
// If it's a miss, the callback gets an error. Otherwise, the callback gets
// the cached template.
cache_fetch: function (name, cb) {
- if (name in this.cache) {
- cb(null, this.cache[name]);
- } else {
- cb('cache miss', null);
- }
+ var key = 'kumascript:loader:' + ks_utils.md5(name);
+ this.memcached.get(key, function (err, result) {
+ if (err || !result) {
+ cb(err, null);
+ } else {
+ var source = (new Buffer(result, 'base64')).toString('utf-8');
+ cb(null, source);
+ }
+ });
},
// #### cache_store(name, cb)
//
// Store the named template into the cache. This should really just be a
// pass-through and not telegraph any errors onto the callback.
- cache_store: function (name, fn, cb) {
- this.cache[name] = fn;
- cb(null, fn);
+ cache_store: function (name, source, cb) {
+ var key = 'kumascript:loader:' + ks_utils.md5(name);
+ var timeout = this.options.cache_timeout || 3600;
+ var cache_source = (new Buffer(source,'utf8')).toString('base64');
+ this.memcached.set(key, cache_source, timeout, function (err, result) {
+ return (err) ? cb(err, null) : cb(null, source);
+ });
}
});
@@ -80,21 +118,14 @@ var FileLoader = ks_utils.Class(BaseLoader, {
template_class: ks_templates.EJSTemplate
},
- load: function (name, loaded_cb) {
+ load: function (name, cb) {
var $this = this,
tmpl_fn = ks_utils.tmpl($this.options.filename_template,
{name: name})
.toLowerCase();
- fs.readFile(tmpl_fn, 'utf8', function (err, body) {
- // TODO: Do something more graceful with errors
- if (err) {
- return loaded_cb("error " + err, null);
- }
- // TODO: Map response content-type and headers to choose template class
- var tmpl_cls = $this.options.template_class;
- // TODO: Do something graceful with template parsing errors?
- var tmpl = new tmpl_cls({ source: body });
- loaded_cb(null, tmpl);
+ fs.readFile(tmpl_fn, 'utf8', function (err, source) {
+ if (err) { cb(err, null); }
+ else { cb(null, source); }
});
}
@@ -113,24 +144,17 @@ var HTTPLoader = ks_utils.Class(BaseLoader, {
template_class: ks_templates.EJSTemplate
},
- load: function (name, loaded_cb) {
+ load: function (name, cb) {
var $this = this,
tmpl_url = ks_utils.tmpl($this.options.url_template,
{name: name.toLowerCase()});
- request(tmpl_url, function (err, resp, body) {
- try {
- // TODO: Do something more graceful with errors and 404's
- if (200 !== resp.statusCode) {
- return loaded_cb("status " + resp.statusCode, null);
- }
- // TODO: Map response content-type and headers to choose template class
- var tmpl_cls = $this.options.template_class;
- // TODO: Do something graceful with template parsing errors?
- var tmpl = new tmpl_cls({ source: body });
- loaded_cb(null, tmpl);
- } catch (e) {
- // Something blew up during template compilation
- loaded_cb(e, null);
+ request(tmpl_url, function (err, resp, source) {
+ if (200 !== resp.statusCode) {
+ cb("status " + resp.statusCode, null);
+ } else if (err) {
+ cb(err, null);
+ } else {
+ cb(null, source);
}
});
}
View
12 lib/kumascript/macros.js
@@ -48,8 +48,13 @@ var MacroProcessor = ks_utils.Class({
templates = {},
macros = {};
- var loader_options = $this.options.loader_options,
- loader = new $this.options.loader_class(loader_options);
+ // Clone general loader options, since we'll tweak them per-request.
+ var loader_options = _.clone($this.options.loader_options);
+ if (api_ctx.env && api_ctx.env.cache_control) {
+ // Pass along Cache-Control header, if any.
+ loader_options.cache_control = api_ctx.env.cache_control;
+ }
+ var loader = new $this.options.loader_class(loader_options);
// Attempt to parse the document, trap errors
var tokens = [];
@@ -111,7 +116,6 @@ var MacroProcessor = ks_utils.Class({
var $this = this,
errors = [],
names = _.keys(templates);
-
if (!names.length) { return next_cb(errors); }
var template_q = async.queue(function (name, q_next) {
@@ -136,8 +140,8 @@ var MacroProcessor = ks_utils.Class({
}
}, $this.options.queue_concurrency);
- names.forEach(function (name) { template_q.push(name); });
template_q.drain = function (err) { next_cb(errors); };
+ names.forEach(function (name) { template_q.push(name); });
},
// #### Evaluate macros
View
5 lib/kumascript/server.js
@@ -52,7 +52,9 @@ var Server = ks_utils.Class({
log.info('Service pid ' + process.pid + ' initializing');
// Create a response cache instance
- $this.cache = new ks_caching.ResponseCache();
+ $this.cache = new ks_caching.ResponseCache({
+ memcache: this.options.memcache
+ });
// Configure the HTTP server...
app.configure(function () {
@@ -88,6 +90,7 @@ var Server = ks_utils.Class({
this.macro_processor = new ks_macros.MacroProcessor({
loader_class: template_loader_class,
loader_options: {
+ memcache: this.options.memcache,
url_template: this.options.template_url_template,
template_class: template_class
}
View
16 lib/kumascript/test-utils.js
@@ -27,8 +27,11 @@ var JSONifyTemplate = ks_utils.Class(ks_templates.BaseTemplate, {
// Simple loader subclass that builds JSONifyTemplates.
var JSONifyLoader = ks_utils.Class(ks_loaders.BaseLoader, {
- load: function (name, loaded_cb) {
- loaded_cb(null, new JSONifyTemplate({name: name}));
+ load: function (name, cb) {
+ cb(null, name);
+ },
+ compile: function (name, cb) {
+ cb(null, new JSONifyTemplate({name: name}));
}
});
@@ -37,12 +40,15 @@ var LocalLoader = ks_utils.Class(ks_loaders.BaseLoader, {
default_options: {
templates: { }
},
- load: function (name, loaded_cb) {
+ load: function (name, cb) {
if (name in this.options.templates) {
- loaded_cb(null, this.options.templates[name]);
+ cb(null, this.options.templates[name]);
} else {
- loaded_cb("not found", null);
+ cb("not found", null);
}
+ },
+ compile: function (source, cb) {
+ cb(null, source);
}
});
View
31 lib/kumascript/utils.js
@@ -6,6 +6,7 @@
// ### Prerequisites
var util = require('util'),
+ crypto = require('crypto'),
_ = require('underscore');
// ### Class
@@ -54,6 +55,32 @@ var tmpl = function (tmpl, ctx) {
});
};
+// ### FakeMemcached
+//
+// A minimal stub replacement for Memcached, in case it's missing from the
+// config. That way, kumascript can be used without memcache, even if that's
+// not recommended.
+var FakeMemcached = Class({
+ initialize: function (options) {
+ this._cache = {};
+ },
+ set: function (key, value, tm_out, next) {
+ this._cache[key] = value;
+ next(null, true);
+ },
+ get: function (key, next) {
+ next(null, this._cache[key]);
+ }
+});
+
+// ### md5
+// Quick shortcut to produce an MD5 hash of a utf-8 string.
+function md5 (str) {
+ var hash = crypto.createHash('md5');
+ hash.update(str, 'utf8');
+ return hash.digest('hex');
+}
+
// ### Underscore.js extensions
_.mixin({
@@ -78,5 +105,7 @@ _.mixin({
// ### Exported public API
module.exports = {
Class: Class,
- tmpl: tmpl
+ tmpl: tmpl,
+ FakeMemcached: FakeMemcached,
+ md5: md5
};
View
31 tests/test-caching.js
@@ -15,12 +15,12 @@ var util = require('util'),
ks_caching = kumascript.caching,
ks_test_utils = kumascript.test_utils;
-
var TEST_PORT = 9001;
var TEST_BASE_URL = 'http://localhost:' + TEST_PORT;
-var TEST_CONTENT = "TEST CONTENT #1";
-var TEST_ETAG = '8675309JENNY';
+var TEST_ETAG = '8675309JENNY';
+// Let's throw some utf-8 torture through the pipes
+var TEST_CONTENT = "Community Communauté Сообщество コミュニティ 커뮤니티";
module.exports = nodeunit.testCase({
@@ -386,7 +386,7 @@ module.exports = nodeunit.testCase({
"Cache internals should support some HTTP caching semantics": function (test) {
var test_key = "/docs/en-US/testdoc",
- expected_content = [ [], ["THIS", "IS A", "TEST"] ];
+ expected_content = "THIS IS A TEST";
expected_etag= "8675309JENNY";
var now = (new Date()).getTime(),
@@ -406,8 +406,8 @@ module.exports = nodeunit.testCase({
last_modified: then,
etag: expected_etag
};
- cache.set(test_key, expected_content, opts,
- function (err, content, meta) {
+ cache.set(test_key, 3600, null, expected_content, opts,
+ function (err, headers, content, meta) {
test.equal(meta.last_modified, opts.last_modified);
test.equal(meta.etag, expected_etag);
content_etag = meta.etag;
@@ -416,70 +416,70 @@ module.exports = nodeunit.testCase({
}, function (wf_next) {
// Try a get for something not found
var opts = {};
- cache.get("/lol/wut", opts, function (err, result_content) {
+ cache.get("/lol/wut", opts, function (err, headers, result_content, meta) {
test.equal(err, cache.ERR_MISS);
test.equal(result_content, null);
wf_next();
});
}, function (wf_next) {
// Try an unconditional get
var opts = {};
- cache.get(test_key, opts, function (err, result_content) {
+ cache.get(test_key, opts, function (err, headers, result_content, meta) {
test.equal(result_content, expected_content);
wf_next();
});
}, function (wf_next) {
// Try a get with a max_age condition that should pass
var opts = { max_age: ancient_age };
- cache.get(test_key, opts, function (err, result_content) {
+ cache.get(test_key, opts, function (err, headers, result_content, meta) {
test.equal(err, null);
test.equal(result_content, expected_content);
wf_next();
});
}, function (wf_next) {
// Try a get with a max_age of 0
var opts = { max_age: 0 };
- cache.get(test_key, opts, function (err, result_content) {
+ cache.get(test_key, opts, function (err, headers, result_content, meta) {
test.equal(err, cache.ERR_MISS);
test.equal(result_content, null);
wf_next();
});
}, function (wf_next) {
// Try a get with a max_age condition that should fail
var opts = { max_age: (then_age / 2) };
- cache.get(test_key, opts, function (err, result_content) {
+ cache.get(test_key, opts, function (err, headers, result_content, meta) {
test.equal(err, cache.ERR_STALE);
test.equal(result_content, null);
wf_next();
});
}, function (wf_next) {
// Try a get with a if_none_match condition that should pass
var opts = { if_none_match: "BADHASH" };
- cache.get(test_key, opts, function (err, result_content) {
+ cache.get(test_key, opts, function (err, headers, result_content, meta) {
test.equal(err, null);
test.equal(result_content, expected_content);
wf_next();
});
}, function (wf_next) {
// Try a get with a if_none_match condition that should fail
var opts = { if_none_match: content_etag };
- cache.get(test_key, opts, function (err, result_content) {
+ cache.get(test_key, opts, function (err, headers, result_content, meta) {
test.equal(err, cache.ERR_NOT_MODIFIED);
test.equal(result_content, null);
wf_next();
});
}, function (wf_next) {
// Try a get with a if_modified_since condition that should pass
var opts = { if_modified_since: ancient };
- cache.get(test_key, opts, function (err, result_content) {
+ cache.get(test_key, opts, function (err, headers, result_content, meta) {
test.equal(err, null);
test.equal(result_content, expected_content);
wf_next();
});
}, function (wf_next) {
// Try a get with a if_modified_since condition that should fail
var opts = { if_modified_since: now - (then_age/2) };
- cache.get(test_key, opts, function (err, result_content) {
+ cache.get(test_key, opts, function (err, headers, result_content, meta) {
test.equal(err, cache.ERR_NOT_MODIFIED);
test.equal(result_content, null);
wf_next();
@@ -488,7 +488,6 @@ module.exports = nodeunit.testCase({
], function () {
test.done();
});
-
}
});
View
24 tests/test-loaders.js
@@ -32,30 +32,6 @@ module.exports = nodeunit.testCase({
},
- "Template loading with local caching should work": function (test) {
-
- var loader = new ks_test_utils.JSONifyLoader(),
- data = ["test123", ["alpha", "beta", "gamma"]],
- expected = JSON.stringify(data);
-
- loader.get(data[0], function (err, tmpl) {
-
- test.ok(!err);
- test.notEqual(typeof(tmpl), 'undefined');
-
- tmpl.execute(data[1], {}, function (err, result) {
- test.equal(result, expected);
-
- // Ensure the cache is present, and populated
- test.notEqual(typeof(loader.cache), 'undefined');
- test.ok(data[0] in loader.cache);
-
- test.done();
- });
-
- });
- },
-
"Template loading via HTTP should work": function (test) {
var test_server = ks_test_utils.createTestServer();
var loader = new ks_loaders.HTTPLoader({
View
46 tests/test-macros.js
@@ -90,28 +90,39 @@ module.exports = nodeunit.testCase({
}
});
- var BrokenTemplateLoader = ks_utils.Class(ks_loaders.BaseLoader, {
- broken_templates: {
- 'broken1': null,
- 'broken2': BrokenCompilationTemplate,
- 'broken3': BrokenExecutionTemplate
+ var LocalClassLoader = ks_utils.Class(ks_loaders.BaseLoader, {
+ load: function (name, cb) {
+ if (!this.options.templates[name]) {
+ cb('NOT FOUND', null);
+ } else {
+ cb(null, name);
+ }
},
- load: function (name, loaded_cb) {
- var cls = (name in this.broken_templates) ?
- this.broken_templates[name] :
+ compile: function (name, cb) {
+ var cls = (name in this.options.templates) ?
+ this.options.templates[name] :
JSONifyTemplate;
- if (null === cls) {
- loaded_cb("NOT FOUND", null);
- } else {
- loaded_cb(null, new cls({ name: name }));
+ try {
+ cb(null, new cls({ name: name }));
+ } catch (e) {
+ cb(e, null);
}
}
});
var mp = new ks_macros.MacroProcessor({
- loader_class: BrokenTemplateLoader
+ loader_class: LocalClassLoader,
+ loader_options: {
+ templates: {
+ 'broken1': null,
+ 'broken2': BrokenCompilationTemplate,
+ 'broken3': BrokenExecutionTemplate,
+ 'MacroUsingParams': JSONifyTemplate,
+ 'AnotherFoundMacro': JSONifyTemplate
+ }
+ }
});
-
+
processFixture(test, mp, 'macros-broken-templates.txt',
function (errors, result) {
var expected_errors = [
@@ -148,9 +159,12 @@ module.exports = nodeunit.testCase({
}
});
var CounterTemplateLoader = ks_utils.Class(ks_loaders.BaseLoader, {
- load: function (name, loaded_cb) {
+ load: function (name, cb) {
load_count++;
- loaded_cb(null, new CounterTemplate());
+ cb(null, new CounterTemplate());
+ },
+ compile: function (obj, cb) {
+ cb(null, obj);
}
});
var mp = new ks_macros.MacroProcessor({
View
15 tests/test-server.js
@@ -100,14 +100,23 @@ module.exports = nodeunit.testCase({
'broken2': BrokenCompilationTemplate,
'broken3': BrokenExecutionTemplate
},
- load: function (name, loaded_cb) {
+ load: function (name, cb) {
var cls = (name in this.broken_templates) ?
this.broken_templates[name] :
JSONifyTemplate;
if (null === cls) {
- loaded_cb("NOT FOUND", null);
+ cb("NOT FOUND", null);
} else {
- loaded_cb(null, new cls({ name: name }));
+ cb(null, [cls, name]);
+ }
+ },
+ compile: function (src, cb) {
+ var cls = src[0],
+ name = src[1];
+ try {
+ cb(null, new cls({ name: name }));
+ } catch (e) {
+ cb(e, null);
}
}
});

0 comments on commit f05d343

Please sign in to comment.