1// Copyright 2014 the V8 project authors. All rights reserved.
2// Redistribution and use in source and binary forms, with or without
3// modification, are permitted provided that the following conditions are
4// met:
5//
6//     * Redistributions of source code must retain the above copyright
7//       notice, this list of conditions and the following disclaimer.
8//     * Redistributions in binary form must reproduce the above
9//       copyright notice, this list of conditions and the following
10//       disclaimer in the documentation and/or other materials provided
11//       with the distribution.
12//     * Neither the name of Google Inc. nor the names of its
13//       contributors may be used to endorse or promote products derived
14//       from this software without specific prior written permission.
15//
16// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
28// This file emulates Mocha test framework used in promises-aplus tests.
29
30var describe;
31var it;
32var specify;
33var before;
34var after;
35var beforeEach;
36var afterEach;
37var RunAllTests;
38
39var assert = require('assert');
40
41(function() {
42var TIMEOUT = 1000;
43
44function PostMicrotask(fn) {
45  var o = {};
46  Object.observe(o, function() {
47    fn();
48  });
49  // Change something to enqueue a microtask.
50  o.x = 'hello';
51}
52
53var context = {
54  beingDescribed: undefined,
55  currentSuiteIndex: 0,
56  suites: []
57};
58
59function Run() {
60  function current() {
61    while (context.currentSuiteIndex < context.suites.length &&
62           context.suites[context.currentSuiteIndex].hasRun) {
63      ++context.currentSuiteIndex;
64    }
65    if (context.suites.length == context.currentSuiteIndex) {
66      return undefined;
67    }
68    return context.suites[context.currentSuiteIndex];
69  }
70  var suite = current();
71  if (!suite) {
72    // done
73    print('All tests have run.');
74    return;
75  }
76  suite.Run();
77}
78
79RunAllTests = function() {
80  context.currentSuiteIndex = 0;
81  var numRegularTestCases = 0;
82  for (var i = 0; i < context.suites.length; ++i) {
83    numRegularTestCases += context.suites[i].numRegularTestCases();
84  }
85  print(context.suites.length + ' suites and ' + numRegularTestCases +
86        ' test cases are found');
87  Run();
88};
89
90function TestCase(name, before, fn, after, isRegular) {
91  this.name = name;
92  this.before = before;
93  this.fn = fn;
94  this.after = after;
95  this.isRegular = isRegular;
96  this.hasDone = false;
97}
98
99TestCase.prototype.RunFunction = function(suite, fn, postAction) {
100  if (!fn) {
101    postAction();
102    return;
103  }
104  try {
105    if (fn.length === 0) {
106      // synchronous
107      fn();
108      postAction();
109    } else {
110      // asynchronous
111      fn(postAction);
112    }
113  } catch (e) {
114    suite.ReportError(this, e);
115  }
116}
117
118TestCase.prototype.MarkAsDone = function() {
119  this.hasDone = true;
120  clearTimeout(this.timer);
121}
122
123TestCase.prototype.Run = function(suite, postAction) {
124  print('Running ' + suite.description + '#' + this.name + ' ...');
125  assert.clear();
126
127  this.timer = setTimeout(function() {
128    suite.ReportError(this, Error('timeout'));
129  }.bind(this), TIMEOUT);
130
131  this.RunFunction(suite, this.before, function(e) {
132    if (this.hasDone) {
133      return;
134    }
135    if (e instanceof Error) {
136      return suite.ReportError(this, e);
137    }
138    if (assert.fails.length > 0) {
139      return suite.ReportError(this, assert.fails[0]);
140    }
141    this.RunFunction(suite, this.fn, function(e) {
142      if (this.hasDone) {
143        return;
144      }
145      if (e instanceof Error) {
146        return suite.ReportError(this, e);
147      }
148      if (assert.fails.length > 0) {
149        return suite.ReportError(this, assert.fails[0]);
150      }
151      this.RunFunction(suite, this.after, function(e) {
152        if (this.hasDone) {
153          return;
154        }
155        if (e instanceof Error) {
156          return suite.ReportError(this, e);
157        }
158        if (assert.fails.length > 0) {
159          return suite.ReportError(this, assert.fails[0]);
160        }
161        this.MarkAsDone();
162        if (this.isRegular) {
163          print('PASS: ' + suite.description + '#' + this.name);
164        }
165        PostMicrotask(postAction);
166      }.bind(this));
167    }.bind(this));
168  }.bind(this));
169};
170
171function TestSuite(described) {
172  this.description = described.description;
173  this.cases = [];
174  this.currentIndex = 0;
175  this.hasRun = false;
176
177  if (described.before) {
178    this.cases.push(new TestCase(this.description + ' :before', undefined,
179                                 described.before, undefined, false));
180  }
181  for (var i = 0; i < described.cases.length; ++i) {
182    this.cases.push(new TestCase(described.cases[i].description,
183                                 described.beforeEach,
184                                 described.cases[i].fn,
185                                 described.afterEach,
186                                 true));
187  }
188  if (described.after) {
189    this.cases.push(new TestCase(this.description + ' :after',
190                                 undefined, described.after, undefined, false));
191  }
192}
193
194TestSuite.prototype.Run = function() {
195  this.hasRun = this.currentIndex === this.cases.length;
196  if (this.hasRun) {
197    PostMicrotask(Run);
198    return;
199  }
200
201  // TestCase.prototype.Run cannot throw an exception.
202  this.cases[this.currentIndex].Run(this, function() {
203    ++this.currentIndex;
204    PostMicrotask(Run);
205  }.bind(this));
206};
207
208TestSuite.prototype.numRegularTestCases = function() {
209  var n = 0;
210  for (var i = 0; i < this.cases.length; ++i) {
211    if (this.cases[i].isRegular) {
212      ++n;
213    }
214  }
215  return n;
216}
217
218TestSuite.prototype.ReportError = function(testCase, e) {
219  if (testCase.hasDone) {
220    return;
221  }
222  testCase.MarkAsDone();
223  this.hasRun = this.currentIndex === this.cases.length;
224  print('FAIL: ' + this.description + '#' + testCase.name + ': ' +
225        e.name  + ' (' + e.message + ')');
226  ++this.currentIndex;
227  PostMicrotask(Run);
228};
229
230describe = function(description, fn) {
231  var parent = context.beingDescribed;
232  var incomplete = {
233    cases: [],
234    description: parent ? parent.description + ' ' + description : description,
235    parent: parent,
236  };
237  context.beingDescribed = incomplete;
238  fn();
239  context.beingDescribed = parent;
240
241  context.suites.push(new TestSuite(incomplete));
242}
243
244specify = it = function(description, fn) {
245  context.beingDescribed.cases.push({description: description, fn: fn});
246}
247
248before = function(fn) {
249  context.beingDescribed.before = fn;
250}
251
252after = function(fn) {
253  context.beingDescribed.after = fn;
254}
255
256beforeEach = function(fn) {
257  context.beingDescribed.beforeEach = fn;
258}
259
260afterEach = function(fn) {
261  context.beingDescribed.afterEach = fn;
262}
263
264}());
265