UNPKG

11 kBJavaScriptView Raw
1/**
2 * @fileoverview Mocha test wrapper
3 * @author Ilya Volodin
4 * @copyright 2015 Nicholas C. Zakas. All rights reserved.
5 * @copyright 2014 Ilya Volodin. All rights reserved.
6 * See LICENSE file in root directory for full license.
7 */
8"use strict";
9
10/* global describe, it */
11
12/*
13 * This is a wrapper around mocha to allow for DRY unittests for eslint
14 * Format:
15 * RuleTester.add("{ruleName}", {
16 * valid: [
17 * "{code}",
18 * { code: "{code}", options: {options}, global: {globals}, globals: {globals}, parser: "{parser}", settings: {settings} }
19 * ],
20 * invalid: [
21 * { code: "{code}", errors: {numErrors} },
22 * { code: "{code}", options: {options}, global: {globals}, parser: "{parser}", settings: {settings}, errors: [{ message: "{errorMessage}", type: "{errorNodeType}"}] }
23 * ]
24 * });
25 *
26 * Variables:
27 * {code} - String that represents the code to be tested
28 * {options} - Arguments that are passed to the configurable rules.
29 * {globals} - An object representing a list of variables that are
30 * registered as globals
31 * {parser} - String representing the parser to use
32 * {settings} - An object representing global settings for all rules
33 * {numErrors} - If failing case doesn't need to check error message,
34 * this integer will specify how many errors should be
35 * received
36 * {errorMessage} - Message that is returned by the rule on failure
37 * {errorNodeType} - AST node type that is returned by they rule as
38 * a cause of the failure.
39 *
40 * Requirements:
41 * Each rule has to have at least one invalid and at least one valid check.
42 * If one of them is missing, the test will be marked as failed.
43 */
44
45//------------------------------------------------------------------------------
46// Requirements
47//------------------------------------------------------------------------------
48
49var assert = require("assert"),
50 util = require("util"),
51 merge = require("lodash.merge"),
52 omit = require("lodash.omit"),
53 clone = require("lodash.clonedeep"),
54 validator = require("../config-validator"),
55 validate = require("is-my-json-valid"),
56 eslint = require("../eslint"),
57 metaSchema = require("../../conf/json-schema-schema.json"),
58 SourceCodeFixer = require("../util/source-code-fixer");
59
60//------------------------------------------------------------------------------
61// Private Members
62//------------------------------------------------------------------------------
63// testerDefaultConfig must not be modified as it allows to reset the tester to
64// the initial default configuration
65var testerDefaultConfig = { rules: {} };
66var defaultConfig = { rules: {} };
67// List every parameters possible on a test case that are not related to eslint
68// configuration
69var RuleTesterParameters = [
70 "code",
71 "filename",
72 "options",
73 "args",
74 "errors"
75];
76
77var validateSchema = validate(metaSchema, { verbose: true });
78
79//------------------------------------------------------------------------------
80// Public Interface
81//------------------------------------------------------------------------------
82
83/**
84 * Creates a new instance of RuleTester.
85 * @param {Object} [testerConfig] Optional, extra configuration for the tester
86 * @constructor
87 */
88function RuleTester(testerConfig) {
89
90 /**
91 * The configuration to use for this tester. Combination of the tester
92 * configuration and the default configuration.
93 * @type {Object}
94 */
95 this.testerConfig = merge(
96 // we have to clone because merge uses the object on the left for
97 // recipient
98 clone(defaultConfig),
99 testerConfig
100 );
101}
102
103/**
104 * Set the configuration to use for all future tests
105 * @param {Object} config the configuration to use.
106 * @returns {void}
107 */
108RuleTester.setDefaultConfig = function(config) {
109 if (typeof config !== "object") {
110 throw new Error("RuleTester.setDefaultConfig: config must be an object");
111 }
112 defaultConfig = config;
113
114 // Make sure the rules object exists since it is assumed to exist later
115 defaultConfig.rules = defaultConfig.rules || {};
116};
117
118/**
119 * Get the current configuration used for all tests
120 * @returns {Object} the current configuration
121 */
122RuleTester.getDefaultConfig = function() {
123 return defaultConfig;
124};
125
126/**
127 * Reset the configuration to the initial configuration of the tester removing
128 * any changes made until now.
129 * @returns {void}
130 */
131RuleTester.resetDefaultConfig = function() {
132 defaultConfig = clone(testerDefaultConfig);
133};
134
135// default separators for testing
136RuleTester.describe = (typeof describe === "function") ? describe : function(text, method) {
137 return method.apply(this);
138};
139
140RuleTester.it = (typeof it === "function") ? it : function(text, method) {
141 return method.apply(this);
142};
143
144RuleTester.prototype = {
145
146 /**
147 * Define a rule for one particular run of tests.
148 * @param {string} name The name of the rule to define.
149 * @param {Function} rule The rule definition.
150 * @returns {void}
151 */
152 defineRule: function(name, rule) {
153 eslint.defineRule(name, rule);
154 },
155
156 /**
157 * Adds a new rule test to execute.
158 * @param {string} ruleName The name of the rule to run.
159 * @param {Function} rule The rule to test.
160 * @param {Object} test The collection of tests to run.
161 * @returns {void}
162 */
163 run: function(ruleName, rule, test) {
164
165 var testerConfig = this.testerConfig,
166 result = {};
167
168 /* eslint-disable no-shadow */
169
170 /**
171 * Run the rule for the given item
172 * @param {string} ruleName name of the rule
173 * @param {string|object} item Item to run the rule against
174 * @returns {object} Eslint run result
175 * @private
176 */
177 function runRuleForItem(ruleName, item) {
178 var config = clone(testerConfig),
179 code, filename, schema;
180
181 if (typeof item === "string") {
182 code = item;
183 } else {
184 code = item.code;
185 // Assumes everything on the item is a config except for the
186 // parameters used by this tester
187 var itemConfig = omit(item, RuleTesterParameters);
188 // Create the config object from the tester config and this item
189 // specific configurations.
190 config = merge(
191 config,
192 itemConfig
193 );
194 }
195
196 if (item.filename) {
197 filename = item.filename;
198 }
199
200 if (item.options) {
201 var options = item.options.concat();
202 options.unshift(1);
203 config.rules[ruleName] = options;
204 } else {
205 config.rules[ruleName] = 1;
206 }
207
208 eslint.defineRule(ruleName, rule);
209
210 schema = validator.getRuleOptionsSchema(ruleName);
211
212 validateSchema(schema);
213
214 if (validateSchema.errors) {
215 throw new Error([
216 "Schema for rule " + ruleName + " is invalid:"
217 ].concat(validateSchema.errors.map(function(error) {
218 return "\t" + error.field + ": " + error.message;
219 })).join("\n"));
220 }
221
222 validator.validate(config, "rule-tester");
223
224 return eslint.verify(code, config, filename);
225 }
226
227 /**
228 * Check if the template is valid or not
229 * all valid cases go through this
230 * @param {string} ruleName name of the rule
231 * @param {string|object} item Item to run the rule against
232 * @returns {void}
233 * @private
234 */
235 function testValidTemplate(ruleName, item) {
236 var messages = runRuleForItem(ruleName, item);
237
238 assert.equal(messages.length, 0, util.format("Should have no errors but had %d: %s",
239 messages.length, util.inspect(messages)));
240 }
241
242 /**
243 * Check if the template is invalid or not
244 * all invalid cases go through this.
245 * @param {string} ruleName name of the rule
246 * @param {string|object} item Item to run the rule against
247 * @returns {void}
248 * @private
249 */
250 function testInvalidTemplate(ruleName, item) {
251 var messages = runRuleForItem(ruleName, item);
252
253 if (typeof item.errors === "number") {
254 assert.equal(messages.length, item.errors, util.format("Should have %d errors but had %d: %s",
255 item.errors, messages.length, util.inspect(messages)));
256 } else {
257 assert.equal(messages.length, item.errors.length,
258 util.format("Should have %d errors but had %d: %s",
259 item.errors.length, messages.length, util.inspect(messages)));
260
261 if (item.hasOwnProperty("output")) {
262 var result = SourceCodeFixer.applyFixes(eslint.getSourceCode(), messages);
263 assert.equal(result.output, item.output, "Output is incorrect.");
264 }
265
266
267 for (var i = 0, l = item.errors.length; i < l; i++) {
268 assert.ok(!("fatal" in messages[i]), "A fatal parsing error occurred: " + messages[i].message);
269 assert.equal(messages[i].ruleId, ruleName, "Error rule name should be the same as the name of the rule being tested");
270
271 if (item.errors[i].message) {
272 assert.equal(messages[i].message, item.errors[i].message, "Error message should be " + item.errors[i].message);
273 }
274
275 if (item.errors[i].type) {
276 assert.equal(messages[i].nodeType, item.errors[i].type, "Error type should be " + item.errors[i].type);
277 }
278
279 if (item.errors[i].hasOwnProperty("line")) {
280 assert.equal(messages[i].line, item.errors[i].line, "Error line should be " + item.errors[i].line);
281 }
282
283 if (item.errors[i].hasOwnProperty("column")) {
284 assert.equal(messages[i].column, item.errors[i].column, "Error column should be " + item.errors[i].column);
285 }
286 }
287 }
288 }
289
290 // this creates a mocha test suite and pipes all supplied info
291 // through one of the templates above.
292 RuleTester.describe(ruleName, function() {
293 test.valid.forEach(function(valid) {
294 RuleTester.it(valid.code || valid, function() {
295 testValidTemplate(ruleName, valid);
296 });
297 });
298
299 test.invalid.forEach(function(invalid) {
300 RuleTester.it(invalid.code, function() {
301 testInvalidTemplate(ruleName, invalid);
302 });
303 });
304 });
305
306 return result.suite;
307 }
308};
309
310
311module.exports = RuleTester;