1/*
2 * Jake JavaScript build tool
3 * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *         http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17*/
18
19let path = require('path');
20let currDir = process.cwd();
21
22/**
23  @name jake
24  @namespace jake
25*/
26/**
27  @name jake.TestTask
28  @constructor
29  @description Instantiating a TestTask creates a number of Jake
30  Tasks that make running tests for your software easy.
31
32  @param {String} name The name of the project
33  @param {Function} definition Defines the list of files containing the tests,
34  and the name of the namespace/task for running them. Will be executed on the
35  instantiated TestTask (i.e., 'this', will be the TestTask instance), to set
36  the various instance-propertiess.
37
38  @example
39  let t = new jake.TestTask('bij-js', function () {
40    this.testName = 'testSpecial';
41    this.testFiles.include('test/**');
42  });
43
44 */
45let TestTask = function () {
46  let self = this;
47  let args = Array.prototype.slice.call(arguments);
48  let name = args.shift();
49  let definition = args.pop();
50  let prereqs = args.pop() || [];
51
52  /**
53    @name jake.TestTask#testNam
54    @public
55    @type {String}
56    @description The name of the namespace to place the tests in, and
57    the top-level task for running tests. Defaults to "test"
58   */
59  this.testName = 'test';
60
61  /**
62    @name jake.TestTask#testFiles
63    @public
64    @type {jake.FileList}
65    @description The list of files containing tests to load
66   */
67  this.testFiles = new jake.FileList();
68
69  /**
70    @name jake.TestTask#showDescription
71    @public
72    @type {Boolean}
73    @description Show the created task when doing Jake -T
74   */
75  this.showDescription = true;
76
77  /*
78    @name jake.TestTask#totalTests
79    @public
80    @type {Number}
81    @description The total number of tests to run
82  */
83  this.totalTests = 0;
84
85  /*
86    @name jake.TestTask#executedTests
87    @public
88    @type {Number}
89    @description The number of tests successfully run
90  */
91  this.executedTests = 0;
92
93  if (typeof definition == 'function') {
94    definition.call(this);
95  }
96
97  if (this.showDescription) {
98    desc('Run the tests for ' + name);
99  }
100
101  task(this.testName, prereqs, {async: true}, function () {
102    let t = jake.Task[this.fullName + ':run'];
103    t.on('complete', function () {
104      complete();
105    });
106    // Pass args to the namespaced test
107    t.invoke.apply(t, arguments);
108  });
109
110  namespace(self.testName, function () {
111
112    let runTask = task('run', {async: true}, function (pat) {
113      let re;
114      let testFiles;
115
116      // Don't nest; make a top-level namespace. Don't want
117      // re-calling from inside to nest infinitely
118      jake.currentNamespace = jake.defaultNamespace;
119
120      re = new RegExp(pat);
121      // Get test files that match the passed-in pattern
122      testFiles = self.testFiles.toArray()
123        .filter(function (f) {
124          return (re).test(f);
125        }) // Don't load the same file multiple times -- should this be in FileList?
126        .reduce(function (p, c) {
127          if (p.indexOf(c) < 0) {
128            p.push(c);
129          }
130          return p;
131        }, []);
132
133      // Create a namespace for all the testing tasks to live in
134      namespace(self.testName + 'Exec', function () {
135        // Each test will be a prereq for the dummy top-level task
136        let prereqs = [];
137        // Continuation to pass to the async tests, wrapping `continune`
138        let next = function () {
139          complete();
140        };
141        // Create the task for this test-function
142        let createTask = function (name, action) {
143          // If the test-function is defined with a continuation
144          // param, flag the task as async
145          let t;
146          let isAsync = !!action.length;
147
148          // Define the actual namespaced task with the name, the
149          // wrapped action, and the correc async-flag
150          t = task(name, createAction(name, action), {
151            async: isAsync
152          });
153          t.once('complete', function () {
154            self.executedTests++;
155          });
156          t._internal = true;
157          return t;
158        };
159        // Used as the action for the defined task for each test.
160        let createAction = function (n, a) {
161          // A wrapped function that passes in the `next` function
162          // for any tasks that run asynchronously
163          return function () {
164            let cb;
165            if (a.length) {
166              cb = next;
167            }
168            if (!(n == 'before' || n == 'after' ||
169                    /_beforeEach$/.test(n) || /_afterEach$/.test(n))) {
170              jake.logger.log(n);
171            }
172            // 'this' will be the task when action is run
173            return a.call(this, cb);
174          };
175        };
176          // Dummy top-level task for everything to be prereqs for
177        let topLevel;
178
179        // Pull in each test-file, and iterate over any exported
180        // test-functions. Register each test-function as a prereq task
181        testFiles.forEach(function (file) {
182          let exp = require(path.join(currDir, file));
183
184          // Create a namespace for each filename, so test-name collisions
185          // won't be a problem
186          namespace(file, function () {
187            let testPrefix = self.testName + 'Exec:' + file + ':';
188            let testName;
189            // Dummy task for displaying file banner
190            testName = '*** Running ' + file + ' ***';
191            prereqs.push(testPrefix + testName);
192            createTask(testName, function () {});
193
194            // 'before' setup
195            if (typeof exp.before == 'function') {
196              prereqs.push(testPrefix + 'before');
197              // Create the task
198              createTask('before', exp.before);
199            }
200
201            // Walk each exported function, and create a task for each
202            for (let p in exp) {
203              if (p == 'before' || p == 'after' ||
204                  p == 'beforeEach' || p == 'afterEach') {
205                continue;
206              }
207
208              if (typeof exp.beforeEach == 'function') {
209                prereqs.push(testPrefix + p + '_beforeEach');
210                // Create the task
211                createTask(p + '_beforeEach', exp.beforeEach);
212              }
213
214              // Add the namespace:name of this test to the list of prereqs
215              // for the dummy top-level task
216              prereqs.push(testPrefix + p);
217              // Create the task
218              createTask(p, exp[p]);
219
220              if (typeof exp.afterEach == 'function') {
221                prereqs.push(testPrefix + p + '_afterEach');
222                // Create the task
223                createTask(p + '_afterEach', exp.afterEach);
224              }
225            }
226
227            // 'after' teardown
228            if (typeof exp.after == 'function') {
229              prereqs.push(testPrefix + 'after');
230              // Create the task
231              let afterTask = createTask('after', exp.after);
232              afterTask._internal = true;
233            }
234
235          });
236        });
237
238        self.totalTests = prereqs.length;
239        process.on('exit', function () {
240          // Throw in the case where the process exits without
241          // finishing tests, but no error was thrown
242          if (!jake.errorCode && (self.totalTests > self.executedTests)) {
243            throw new Error('Process exited without all tests completing.');
244          }
245        });
246
247        // Create the dummy top-level task. When calling a task internally
248        // with `invoke` that is async (or has async prereqs), have to listen
249        // for the 'complete' event to know when it's done
250        topLevel = task('__top__', prereqs);
251        topLevel._internal = true;
252        topLevel.addListener('complete', function () {
253          jake.logger.log('All tests ran successfully');
254          complete();
255        });
256
257        topLevel.invoke(); // Do the thing!
258      });
259
260    });
261    runTask._internal = true;
262
263  });
264
265
266};
267
268jake.TestTask = TestTask;
269exports.TestTask = TestTask;
270
271