UNPKG

12.1 kBJavaScriptView Raw
1/**
2 * Kettle Test Utilities
3 *
4 * Contains base utilities for node.js based tests in Kettle
5 *
6 * Copyright 2013-2015 Raising the Floor (International)
7 * Copyright 2013 OCAD University
8 *
9 * Licensed under the New BSD license. You may not use this file except in
10 * compliance with this License.
11 *
12 * You may obtain a copy of the License at
13 * https://github.com/gpii/universal/LICENSE.txt
14 */
15
16"use strict";
17
18var fluid = require("infusion"),
19 kettle = fluid.registerNamespace("kettle"),
20 fs = require("fs"),
21 QUnit = fluid.registerNamespace("QUnit");
22
23fluid.require("ws", require, "kettle.npm.ws");
24
25fluid.registerNamespace("kettle.test");
26
27// Register an uncaught exception handler that will cause any active test fixture to unconditionally fail
28
29kettle.test.handleUncaughtException = function (err) {
30 if (QUnit.config && QUnit.config.current) {
31 QUnit.ok(false, "Unexpected failure in test case (see following log for more details): " + err.message);
32 } else {
33 process.exit(1);
34 }
35};
36
37fluid.onUncaughtException.addListener(kettle.test.handleUncaughtException, "fail",
38 fluid.handlerPriorities.uncaughtException.fail);
39
40/*
41 * Some low-quality synchronous file utilities, suitable for use in test fixtures
42 */
43
44// Utility to recursively delete a directory and its contents from http://www.geedew.com/2012/10/24/remove-a-directory-that-is-not-empty-in-nodejs/
45// Useful for cleaning up before and after test cases
46
47kettle.test.deleteFolderRecursive = function (path) {
48 if (fs.existsSync(path)) {
49 fs.readdirSync(path).forEach(function (file) {
50 var curPath = path + "/" + file;
51 if (fs.lstatSync(curPath).isDirectory()) {
52 kettle.test.deleteFolderRecursive(curPath);
53 } else { // delete file
54 fs.unlinkSync(curPath);
55 }
56 });
57 fs.rmdirSync(path);
58 }
59};
60
61kettle.test.copyFileSync = function (sourceFile, targetFile) {
62 fs.writeFileSync(targetFile, fs.readFileSync(sourceFile));
63};
64
65// Two utilities which aid working with "sequences" in IoC testing fixtures
66
67// returns subarray including only elements between start and end (non-inclusive)
68kettle.test.elementsBetween = function (origArray, start, end) {
69 var array = fluid.makeArray(origArray);
70 start = start || 0;
71 if (!end && end !== 0) {
72 end = array.length;
73 }
74 array.length = end;
75 array.splice(0, start);
76 return array;
77};
78
79// insert the supplied elements into the array at position index (DESTRUCTIVE)
80kettle.test.insertIntoArray = function (origArray, index, elements) {
81 var spliceArgs = [index || 0, 0].concat(elements);
82 origArray.splice.apply(origArray, spliceArgs);
83};
84
85/** Some definitions for testing Kettle error handlers **/
86
87kettle.test.pushInstrumentedErrors = function (globalErrorHandler) {
88 // Beat jqUnit's exception handler so that we can test kettle's instead
89 fluid.failureEvent.addListener(fluid.identity, "jqUnit", "before:fail");
90 // Beat the existing global exception handler for the duration of these tests
91 fluid.onUncaughtException.addListener(globalErrorHandler, "fail", fluid.handlerPriorities.uncaughtException.fail);
92};
93
94kettle.test.popInstrumentedErrors = function () {
95 fluid.failureEvent.removeListener("jqUnit");
96 // restore whatever was the old listener in this namespace, as per FLUID-5506 implementation
97 fluid.onUncaughtException.removeListener("fail");
98};
99
100
101/** Definitions for driving IoC Testing framework fixtures **/
102
103// Component that contains the Kettle configuration (server) under test.
104fluid.defaults("kettle.test.configuration", {
105 gradeNames: ["fluid.component", "{testEnvironment}.options.configurationName"],
106 components: {
107 server: {
108 createOnEvent: "{testEnvironment}.events.constructServer",
109 options: {
110 gradeNames: "kettle.test.server",
111 listeners: {
112 onListen: "{testEnvironment}.events.onServerReady"
113 }
114 }
115 }
116 }
117});
118
119fluid.defaults("kettle.test.server", {
120 listeners: {
121 "onCreate.upgradeError": {
122 funcName: "kettle.test.server.upgradeError",
123 priority: "before:listen"
124 }
125 }
126});
127
128// Forward any errors which reach the end of express' handling chain to our builtin uncaught exception handler to
129// enable more easy interception during testing
130kettle.test.server.upgradeError = function (server) {
131 if (!server.expressApp) { // TODO: more principled approach to "fake server" in multi-config
132 return;
133 }
134 // we MUST supply 4 arguments here
135 server.expressApp.use(function (err, req, res, next) { // eslint-disable-line no-unused-vars
136 fluid.log("kettle.tests.server.upgradeError received error ", err);
137 if (err) {
138 fluid.log("kettle.tests.server.upgradeError throwing uncaught exception error ", err);
139 fluid.onUncaughtException.fire(err);
140 }
141 });
142};
143
144
145// The two core grades (serverEnvironment and testCaseHolder) for kettle server-aware fixtures.
146// There are currently two reasons for separation and locating most material with the testCaseHolder
147// based on framework limitations:
148// i) An environment can't be its own TestCaseHolder (IoC testing framework limitation)
149// ii) The subcomponents of "tests" must be siblings of the fixtures themselves otherwise they
150// couldn't be addressed by distributeOptions etc. (FLUID-5495)
151
152fluid.defaults("kettle.test.testCaseHolder", {
153 gradeNames: ["fluid.test.testCaseHolder"],
154 secret: "kettle tests secret",
155 distributeOptions: [{
156 source: "{that}.options.secret",
157 target: "{that > cookieJar}.options.secret"
158 }, {
159 source: "{that}.options.secret",
160 target: "{that server}.options.secret"
161 }],
162 components: {
163 // The server and all its tree lie under here
164 // TODO: It's a big problem to have this site fused with the place where "module/sequence" are etc.
165 // It prevents other producers of sequences from participating
166 configuration: {
167 type: "kettle.test.configuration"
168 },
169 cookieJar: {
170 type: "kettle.test.cookieJar"
171 }
172 }
173});
174
175fluid.defaults("kettle.test.serverEnvironment", {
176 gradeNames: ["fluid.test.testEnvironment"],
177 events: {
178 onServerReady: null,
179 constructServer: null
180 },
181 components: {
182 // Aligned with the name generated in kettle.test.testDefToServerEnvironment
183 tests: {
184 type: "kettle.test.testCaseHolder"
185 // configuration is child of this testCaseHolder, then server etc.
186 }
187 }
188});
189
190kettle.test.startServerSequence = fluid.freezeRecursive([
191 { // This sequence point is required because of a QUnit bug - it defers the start of sequence by 13ms "to avoid any current callbacks" in its words
192 func: "{testEnvironment}.events.constructServer.fire"
193 }, {
194 event: "{testEnvironment}.events.onServerReady",
195 listener: "fluid.identity"
196 }
197]);
198
199kettle.test.stopServerSequence = fluid.freezeRecursive([
200 {
201 func: "{testCaseHolder}.configuration.server.stop"
202 }, {
203 event: "{testCaseHolder}.configuration.server.events.onStopped",
204 listener: "fluid.identity"
205 }
206]);
207
208/** Builds a Fluid IoC testing framework fixture (in fact, the "options" to a TestCaseHolder) given a configuration
209 * name and a "testDef". This fixture will automatically be supplied as a subcomponent of an environment of type
210 * <code>kettle.test.serverEnvironment</code>.
211 * The testDef must include a <code>sequence</code> element which will be fleshed out with the following
212 * additions - i) At the front, two elements - firstly a firing of the <code>constructServer</code> event of the TestEnvironment,
213 * secondly, a listener for the <code>onServerReady</code> event of the TestEnvironment - ii) at the back, two elements - firstly,
214 * an invocation of the <code>stop</code> method of the server. The resulting holder will be a <code>kettle.test.testCaseHolder</code> holding
215 * a Kettle server as a subcomponent of its <code>configuration</code> component.
216 * @param {String} configurationName - A configuration name which will become the "name" (in QUnit terms, "module name") of the
217 * resulting fixture
218 * @param {Object} testDefIn - A partial test fixture specification. This includes most of the elements expected in a Fluid IoC testing
219 * framework "module" specification, with required elements <code>sequence</code>, <code>name</code> and optional element <code>expect</code>. It may
220 * also include any configuration directed at the <code>TestCaseHolder</code> component, including some <code>gradeNames</code> to supply some reusable
221 * component material.
222 * @return {Object} a fully-fleshed out set of options for a TestCaseHolder, incuding extra sequence elements as described above.
223 */
224kettle.test.testDefToCaseHolder = function (configurationName, testDefIn) {
225 var testDef = fluid.copy(testDefIn);
226 var sequence = testDef.sequence;
227 delete testDef.sequence;
228 delete testDef.config;
229 sequence.unshift.apply(sequence, kettle.test.startServerSequence);
230 sequence.push.apply(sequence, kettle.test.stopServerSequence);
231
232 testDef.modules = [{
233 name: configurationName + " tests",
234 tests: [{
235 name: testDef.name,
236 expect: testDef.expect,
237 sequence: sequence
238 }]
239 }];
240 return testDef;
241};
242
243/* Given a "short testDef" record (with top-level sequence, name, expect, config|configType), will construct a "type record" for
244 * a `kettle.test.serverEnvironment` suitable to be sent directly to `fluid.test.runTests`. This will include the contents of the
245 * "short testDef" expanded into a full `kettle.test.testCaseHolder` held at a child component named `tests`. The property named
246 * `configurationName` will be generated on the testCaseHolder and will be pulled in to produce a `config` grade as parent of the server.
247 */
248kettle.test.testDefToServerEnvironment = function (testDef) {
249 var configurationName = testDef.configType || kettle.config.createDefaults(testDef.config);
250 return {
251 type: "kettle.test.serverEnvironment",
252 options: {
253 configurationName: configurationName,
254 components: {
255 tests: {
256 options: kettle.test.testDefToCaseHolder(configurationName, testDef)
257 }
258 }
259 }
260 };
261};
262
263/** These functions assist the use of individual files run as tests, as well as assisting a complete
264 * module's test suites run in aggregate. The test definitions will be transformed and then contributed
265 * to the current queue of asynchronously resolving tests.
266 *
267 * @param {Object|Array<Object>} testDefs - An array of objects, each representing a test fixture
268 * @param {Function|Array<Function>} transformer - an array of transform functions, accepting an object representing a test fixture and returning a "more processed" one. The entire chain
269 * of functions will be applied to each member of <code>testDefs</code> with the result that it becomes a fully fleshed out TestCaseHolder as required by Fluid's
270 * <a href="http://wiki.fluidproject.org/display/docs/The+IoC+Testing+Framework">IoC Testing Framework</a>
271 */
272kettle.test.bootstrap = function (testDefs, transformer) {
273 var transformArgs = [fluid.makeArray(testDefs)].concat(fluid.makeArray(transformer));
274 var tests = fluid.transform.apply(null, transformArgs);
275 fluid.test.runTests(tests);
276};
277
278/* As for kettle.test.bootstrap, only transform the supplied definitions by converting them into kettle
279 * server tests, bracketed by special server start and stop sequence points. Any supplied transforms in the 2nd
280 * argument will be run before the standard transform to construct server-aware test cases */
281kettle.test.bootstrapServer = function (testDefs, transformer) {
282 kettle.test.bootstrap(testDefs, fluid.makeArray(transformer).concat([kettle.test.testDefToServerEnvironment]));
283};