UNPKG

8.64 kBJavaScriptView Raw
1/**
2 * @file Parses simple markdown text
3 *
4 * The syntax is described in doc-syntax.md
5 *
6 * Paragraph text is ignored (it can be used for documentation)
7 */
8'use strict'
9
10var Test = require('./classes/Test'),
11 Header = require('./classes/Header'),
12 Obj = require('./classes/Obj'),
13 Insertion = require('./classes/Insertion'),
14 Clear = require('./classes/Clear'),
15 Declaration = require('./classes/Declaration'),
16 Case = require('./classes/Case'),
17 Find = require('./classes/Find'),
18 ParseError = require('./classes/ParseError')
19
20/**
21 * @param {string} path
22 * @param {string} text
23 * @param {function(Array<Header|Obj>, Test)} preParse
24 * @returns {Test}
25 * @throws if the syntax is invalid
26 */
27module.exports = function (path, text, preParse) {
28 var originalLines, i, line, els, lastObj, test
29
30 // First pass: break into lines
31 originalLines = text.split(/\r?\n/)
32
33 // Second pass: parse headers and group object lines
34 els = []
35 for (i = 0; i < originalLines.length; i++) {
36 line = originalLines[i]
37 if (line[0] === '#') {
38 // Header line
39 els.push(new Header(line, i))
40 lastObj = null
41 } else if (line[0] === '\t') {
42 // Object line
43 if (!lastObj) {
44 lastObj = new Obj(i)
45 els.push(lastObj)
46 }
47 lastObj.push(line.substr(1))
48 } else {
49 // Ignored line
50 lastObj = null
51 }
52 }
53
54 // Third pass: extract main sections (header, setup, test cases)
55 // Also recursively parse their content
56 test = new Test(path)
57 try {
58 preParse(els, test)
59 i = parseHeader(test, els, 0)
60 i = parseSetups(test, els, i)
61 i = parseCases(test, els, i)
62 } catch (e) {
63 if (e instanceof ParseError) {
64 console.log('\n> In %s', path)
65 e.logSourceContext(originalLines)
66 }
67 throw e
68 }
69
70 return test
71}
72
73/**
74 * Try to parse the test header
75 * @param {Test} test
76 * @param {Object[]} els
77 * @param {number} i
78 * @returns {number}
79 * @throws if the syntax is invalid
80 */
81function parseHeader(test, els, i) {
82 if (!checkHeader(els[i], 1)) {
83 throw new ParseError('Expected a header', els[i])
84 }
85 if (/ \(skip\)$/.test(els[i].value)) {
86 test.name = els[i].value.substr(0, els[i].value.length - 7).trimRight()
87 test.skip = true
88 } else {
89 test.name = els[i].value
90 test.skip = false
91 }
92 return i + 1
93}
94
95/**
96 * Try to parse the setup section
97 * @param {Test} test
98 * @param {Object[]} els
99 * @param {number} i
100 * @returns {number}
101 * @throws if the syntax is invalid
102 */
103function parseSetups(test, els, i) {
104 var i2
105
106 // Skip everything up to the 'Setup' header
107 for (; i < els.length; i++) {
108 if (checkHeader(els[i], 2, 'Setup')) {
109 break
110 }
111 }
112
113 if (i === els.length) {
114 throw new ParseError('Test cases must follow a "## Setup" header')
115 }
116
117 i++
118 while ((i2 = parseSetupItem(test, els, i))) {
119 i = i2
120 }
121 return i
122}
123
124/**
125 * Try to parse the test cases
126 * @param {Test} test
127 * @param {Object[]} els
128 * @param {number} i
129 * @returns {number}
130 * @throws if the syntax is invalid
131 */
132function parseCases(test, els, i) {
133 while (i < els.length) {
134 i = parseCase(test, els, i)
135 }
136 return i
137}
138
139/**
140 * Try to parse a DB insertion/clear or variable declaration
141 * @param {Test} test
142 * @param {Object[]} els
143 * @param {number} i
144 * @returns {number} 0 if there is no more setup item to parse
145 * @throws if the syntax is invalid
146 */
147function parseSetupItem(test, els, i) {
148 var match, header, coll, el, msg
149
150 if (i >= els.length || checkHeader(els[i], 2)) {
151 // Out of setup section
152 return 0
153 } else if (!checkHeader(els[i], 3)) {
154 throw new ParseError('Expected "### ..."', els[i])
155 }
156
157 header = els[i].value
158 if ((match = header.match(/^Clear ([a-zA-Z_$][a-zA-Z0-9_$]*)$/))) {
159 // Clear a collection
160 coll = match[1]
161 if (coll in test.collections) {
162 el = test.collections[coll]
163 if (el.value.indexOf('Clear ') === 0) {
164 msg = 'No need to clear the same collection twice'
165 } else {
166 msg = 'Clearing the collection after insertion is not a good idea'
167 }
168 throw new ParseError(msg, el, els[i])
169 }
170 test.collections[coll] = els[i]
171 test.setups.push(new Clear(coll))
172 return i + 1
173 } else if ((match = header.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*) is$/))) {
174 // Declare a variable
175 if (!(els[i + 1] instanceof Obj)) {
176 throw new ParseError('Expected an {obj}', els[i + 1])
177 }
178 test.setups.push(new Declaration(match[1], els[i + 1].parse()))
179 return i + 2
180 } else if ((match = header.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*) in ([a-zA-Z_$][a-zA-Z0-9_$]*)$/))) {
181 // Insert a document (clear the collection implicitly)
182 if (!(els[i + 1] instanceof Obj)) {
183 throw new ParseError('Expected an {obj}', els[i + 1])
184 }
185 coll = match[2]
186 if (!(coll in test.collections)) {
187 // Push implicit clear
188 test.collections[coll] = els[i]
189 test.setups.push(new Clear(coll))
190 } else if (test.collections[coll].value.indexOf('Clear ') === 0) {
191 el = test.collections[coll]
192 throw new ParseError('No need to clear the collection before insertion, this is done automatically for you', el, els[i])
193 }
194 test.setups.push(new Insertion(match[1], coll, els[i + 1].parse()))
195 return i + 2
196 } else {
197 throw new ParseError('Expected either "### _docName_ in _collection_", "### Clear _collection_" or "### _varName_ is"', els[i])
198 }
199}
200
201/**
202 * Try to parse a test case
203 * @param {Test} test
204 * @param {Object[]} els
205 * @param {number} i
206 * @returns {number}
207 * @throws if the syntax is invalid
208 */
209function parseCase(test, els, i) {
210 var testCase = new Case
211
212 // Test case name
213 if (!checkHeader(els[i], 2)) {
214 throw new ParseError('Expected "## _caseName_"', els[i])
215 }
216 if (/ \(skip\)$/.test(els[i].value)) {
217 testCase.name = els[i].value.substr(0, els[i].value.length - 7).trimRight()
218 testCase.skip = true
219 } else {
220 testCase.name = els[i].value
221 testCase.skip = false
222 }
223 i++
224
225 i = parseCasePost(testCase, els, i)
226 i = parseCaseOut(testCase, els, i)
227 i = parseCaseFinds(test.collections, testCase, els, i)
228
229 test.cases.push(testCase)
230 return i
231}
232
233/**
234 * Try to parse a test case post
235 * @param {Case} testCase
236 * @param {Object[]} els
237 * @param {number} i
238 * @returns {number}
239 * @throws if the syntax is invalid
240 */
241function parseCasePost(testCase, els, i) {
242 if (checkHeader(els[i], 3, /^Post( |$)/)) {
243 if (!(els[i + 1] instanceof Obj)) {
244 throw new ParseError('Expected an {obj}', els[i + 1])
245 }
246 testCase.postUrl = els[i].value === 'Post' ? '' : els[i].value.substr(4).trim()
247 testCase.post = els[i + 1].parse()
248 i += 2
249 } else {
250 testCase.post = Obj.empty()
251 }
252 return i
253}
254
255/**
256 * Try to parse a test case out
257 * @param {Case} testCase
258 * @param {Object[]} els
259 * @param {number} i
260 * @returns {number}
261 * @throws if the syntax is invalid
262 */
263function parseCaseOut(testCase, els, i) {
264 if (checkHeader(els[i], 3, /^Out( \d{3})?$/)) {
265 if (!(els[i + 1] instanceof Obj)) {
266 throw new ParseError('Expected an {obj}', els[i + 1])
267 }
268 testCase.out = els[i + 1].parse()
269 testCase.statusCode = els[i].value === 'Out' ? 200 : Number(els[i].value.substr(4))
270 i += 2
271 } else {
272 testCase.out = Obj.empty()
273 testCase.statusCode = 200
274 }
275 return i
276}
277
278/**
279 * Try to parse a test case finds
280 * @param {Object<Header>} collections Cleared collections
281 * @param {Case} testCase
282 * @param {Object[]} els
283 * @param {number} i
284 * @returns {number}
285 * @throws if the syntax is invalid
286 */
287function parseCaseFinds(collections, testCase, els, i) {
288 var coll
289 while (i < els.length && !checkHeader(els[i], 2)) {
290 if (!checkHeader(els[i], 3) || els[i].value.indexOf('Find in ') !== 0) {
291 throw new ParseError('Expected "### Find in _collection_"', els[i])
292 } else if (!(els[i + 1] instanceof Obj)) {
293 throw new ParseError('Expected an {obj}', els[i + 1])
294 }
295 coll = els[i].value.substr(8).trim()
296 if (!(coll in collections)) {
297 throw new ParseError('You can\'t do a find in a collection that wasn\'t cleared in the setup', els[i])
298 }
299 testCase.finds.push(new Find(coll, els[i + 1].parse()))
300 i += 2
301 }
302 return i
303}
304
305/**
306 * Check if the given value is a Header with the given level and value
307 * @param {*} x
308 * @param {number} level
309 * @param {(string|RegExp)} [value] default: no value checking
310 * @returns {boolean}
311 */
312function checkHeader(x, level, value) {
313 if (!(x instanceof Header) || x.level !== level) {
314 return false
315 }
316 if (value) {
317 if (value instanceof RegExp) {
318 return value.test(x.value)
319 } else {
320 return value === x.value
321 }
322 }
323 return true
324}
\No newline at end of file