1 | define("dojox/testing/DocTest", ["dojo/string"], function() { |
---|
2 | |
---|
3 | return dojo.declare( |
---|
4 | "dojox.testing.DocTest", |
---|
5 | null, |
---|
6 | { |
---|
7 | // summary: |
---|
8 | // This class executes doctests. |
---|
9 | // description: |
---|
10 | // DocTests are tests that are defined inside the comment. |
---|
11 | // A doctest looks as if it was copied from the shell (which it mostly is). |
---|
12 | // A doctest is executed when the following conditions match: |
---|
13 | // 1) all lines are comments |
---|
14 | // 2) the line always starts with spaces/tabs followed by "//" |
---|
15 | // and at least one space |
---|
16 | // 3) the line(s) of the test to execute starts with ">>>" |
---|
17 | // preceeded by what is described in 2) |
---|
18 | // 4) the first line after 3) starting without ">>>" is the exptected result. |
---|
19 | // preceeded by what is described in 2) |
---|
20 | // 5) the test sequence is terminated by an empty line, or the next |
---|
21 | // test in the following line, or a new line that does not start as described in 2) |
---|
22 | // (simple said: is not a comment) |
---|
23 | // preceeded by what is described in 2) |
---|
24 | // |
---|
25 | // I.e. the following is a simple doctest, that will actually also be run |
---|
26 | // if you run this class against this file here: |
---|
27 | // >>> 1+1 // A simple test case. Terminated by an empty line |
---|
28 | // 2 |
---|
29 | // |
---|
30 | // >>> 1==2 |
---|
31 | // false |
---|
32 | // >>> "a"+"b" // Also without the empty line before, this is a new test. |
---|
33 | // "ab" |
---|
34 | // |
---|
35 | // >>> var anything = "anything" // Multiple commands for one test. |
---|
36 | // >>> "something"==anything |
---|
37 | // false |
---|
38 | // |
---|
39 | // DocTests are great for inline documenting a class or method, they also |
---|
40 | // are very helpful in understanding what the class/method actually does. |
---|
41 | // They don't make sense everywhere, but sometimes they are really handy. |
---|
42 | |
---|
43 | |
---|
44 | // TODO: |
---|
45 | // - using console.log() in a test prints something on the |
---|
46 | // console (if you do it on the console) but its not accepted |
---|
47 | // yet to be the test result, may be override console.log!? |
---|
48 | // i.e. here i wanted to: dojo.forEach(["one", 2], |
---|
49 | // function(el, index) {console.log(el, index)}) that works on |
---|
50 | // the console, but not as a docTest :-( |
---|
51 | // - surround the eval for each test case singlely with a |
---|
52 | // try-catch, to to catch syntax errors etc (though the |
---|
53 | // shouldn't happen if you copy the test from the shell :-)) |
---|
54 | |
---|
55 | |
---|
56 | errors: [], |
---|
57 | |
---|
58 | getTests:function(/*String*/moduleName){ |
---|
59 | // summary: |
---|
60 | // Extract the tests from the given module or string. |
---|
61 | // examples: |
---|
62 | // >>> dojo.isArray(new dojox.testing.DocTest().getTests("dojox.testing.DocTest")) // Use the module name to extract the tests from. |
---|
63 | // true |
---|
64 | var path = dojo.moduleUrl(moduleName).path; |
---|
65 | // TODO: |
---|
66 | // this needs to be done better, this is pretty simple and |
---|
67 | // surely not dummy proof |
---|
68 | var file = path.substring(0, path.length-1)+".js"; |
---|
69 | var xhr = dojo.xhrGet({url:file, handleAs:"text"}); |
---|
70 | // Make loading synchronously, mainly so we can use it in doh. |
---|
71 | var data = dojo._getText(file); |
---|
72 | return this._getTestsFromString(data, true); |
---|
73 | }, |
---|
74 | |
---|
75 | getTestsFromString:function(/*String*/data){ |
---|
76 | // examples: |
---|
77 | // >>> (new dojox.testing.DocTest().getTestsFromString(">>> 1+1\n2\n>>> 2+2\n4")).length // Do tests from strings get detected properly? |
---|
78 | // 2 |
---|
79 | return this._getTestsFromString(data, false); |
---|
80 | }, |
---|
81 | |
---|
82 | _getTestsFromString:function(/*String*/data, /*Boolean*/insideComments){ |
---|
83 | // summary: |
---|
84 | // Parse the given string for tests. |
---|
85 | // insideComments: Boolean |
---|
86 | // if false "data" contains only the pure tests, comments already stripped. |
---|
87 | var trim = dojo.hitch(dojo.string, "trim"); |
---|
88 | var lines = data.split("\n"); |
---|
89 | var len = lines.length; |
---|
90 | var tests = []; |
---|
91 | var test = { |
---|
92 | commands: [], |
---|
93 | expectedResult: [], |
---|
94 | line: null |
---|
95 | }; |
---|
96 | for(var i=0; i<len+1; i++){ |
---|
97 | // Trim the line, so we don't have to worry about leading |
---|
98 | // spaces or tabs, bla bla ... |
---|
99 | var l = trim(lines[i] || ""); // The '|| ""' makes sure tests that have no preceeding \n are taken into account too. |
---|
100 | // TODO: |
---|
101 | // detect tests that dont match the condition: commands, |
---|
102 | // result, empty line. esp the empty line might be missing |
---|
103 | // or be tolerant and accept a new test starting on the |
---|
104 | // next line, which would allow to omit the empty line!? |
---|
105 | if((insideComments && l.match(/^\/\/\s+>>>\s.*/)) || l.match(/^\s*>>>\s.*/)){ |
---|
106 | if(!test.line){ |
---|
107 | test.line = i+1; |
---|
108 | } |
---|
109 | // Find the test commands. |
---|
110 | if(test.expectedResult.length>0){ |
---|
111 | // Start a new test right after the expected result, |
---|
112 | // without an empty line. |
---|
113 | tests.push({ |
---|
114 | commands: test.commands, |
---|
115 | expectedResult: test.expectedResult.join("\n"), |
---|
116 | line: test.line |
---|
117 | }); |
---|
118 | test = {commands:[], expectedResult:[], line:i+1}; |
---|
119 | } |
---|
120 | l = insideComments ? trim(l).substring(2, l.length) : l; // Remove the leading slashes. |
---|
121 | l = trim(l).substring(3, l.length); // Remove the ">>>". |
---|
122 | test.commands.push(trim(l)); |
---|
123 | }else if((!insideComments || l.match(/^\/\/\s+.*/)) && test.commands.length && test.expectedResult.length==0){ |
---|
124 | // Detect the lines after the ">>>"-lines, the exptected result. |
---|
125 | l = insideComments ? trim(l).substring(3, l.length) : l; // Remove the leading slashes. |
---|
126 | test.expectedResult.push(trim(l)); |
---|
127 | }else if(test.commands.length>0 && test.expectedResult.length){ |
---|
128 | if(!insideComments || l.match(/^\/\/\s*$/)){ |
---|
129 | // Detect the empty line. |
---|
130 | tests.push({ |
---|
131 | commands: test.commands, |
---|
132 | expectedResult: test.expectedResult.join("\n"), |
---|
133 | line: test.line |
---|
134 | }); |
---|
135 | } |
---|
136 | if(insideComments && !l.match(/^\/\//)){ |
---|
137 | // If the next line is not a comment at all (doesn't start with "//"). |
---|
138 | tests.push({ |
---|
139 | commands: test.commands, |
---|
140 | expectedResult: test.expectedResult.join("\n"), |
---|
141 | line:test.line |
---|
142 | }); |
---|
143 | } |
---|
144 | test = { |
---|
145 | commands: [], |
---|
146 | expectedResult: [], |
---|
147 | line:0 |
---|
148 | }; |
---|
149 | } |
---|
150 | } |
---|
151 | return tests; |
---|
152 | }, |
---|
153 | |
---|
154 | run: function(moduleName){ |
---|
155 | // summary: |
---|
156 | // Run the doctests in the module given. |
---|
157 | // example: |
---|
158 | // doctest = new dojox.testing.DocTest(); |
---|
159 | // doctest.run("dojox.testing.DocTest"); |
---|
160 | // doctest.errors should finally be an empty array. |
---|
161 | // // The above is not a doctest, because it just would |
---|
162 | // // execute itself in a never ending loop. |
---|
163 | // |
---|
164 | // >>> true==true // Test a new line terminating the test. |
---|
165 | // true |
---|
166 | // |
---|
167 | // >>> true==true // Test a new test terminating the test. |
---|
168 | // true |
---|
169 | // >>> true==true // Test a "not a comment"-line, especially an empty line terminating the test. |
---|
170 | // true |
---|
171 | |
---|
172 | // Make sure the result as printed on the console is the same as what |
---|
173 | // is returned by the test. An array is printed as follows on the console. |
---|
174 | // >>> [1,2,3,4] |
---|
175 | // [1,2,3,4] |
---|
176 | // |
---|
177 | // Test a "not a comment"-line, with some real code(!) terminating the test. |
---|
178 | // This used to be a bug, so make sure the line below the test is real |
---|
179 | // from this method! Don't write a test after it, always above! |
---|
180 | // >>> true==true // Test code on new line terminating the test. |
---|
181 | // true |
---|
182 | |
---|
183 | this.errors = []; |
---|
184 | |
---|
185 | var tests = this.getTests(moduleName); |
---|
186 | if(tests){ |
---|
187 | this._run(tests); |
---|
188 | } |
---|
189 | }, |
---|
190 | |
---|
191 | _run: function(/*Array*/tests){ |
---|
192 | // summary: |
---|
193 | // Each element in the array contains the test in the first element, |
---|
194 | // and the expected result in the second element. |
---|
195 | // tests: |
---|
196 | // Make sure that the types are compared properly. There used to be |
---|
197 | // the bug that a return value false was compared to "false" which |
---|
198 | // made the test fail. This is fixed and should be verified by the |
---|
199 | // following tests. |
---|
200 | // >>> false |
---|
201 | // false |
---|
202 | // |
---|
203 | // >>> "false" |
---|
204 | // "false" |
---|
205 | // |
---|
206 | // >>> true |
---|
207 | // true |
---|
208 | // |
---|
209 | // >>> 1 |
---|
210 | // 1 |
---|
211 | // |
---|
212 | // >>> "s" |
---|
213 | // "s" |
---|
214 | // |
---|
215 | // >>> dojo.toJson({one:1}) |
---|
216 | // "{"one":1}" |
---|
217 | // |
---|
218 | var len = tests.length; |
---|
219 | this.tests = len; |
---|
220 | var oks = 0; |
---|
221 | for(var i=0; i<len; i++){ |
---|
222 | var t = tests[i]; |
---|
223 | var res = this.runTest(t.commands, t.expectedResult); |
---|
224 | var msg = "Test "+(i+1)+": "; |
---|
225 | var viewCommands = t.commands.join(" "); |
---|
226 | // Show the first part of the test command. |
---|
227 | viewCommands = (viewCommands.length > 50 ? |
---|
228 | viewCommands.substr(0,50) + "..." : |
---|
229 | viewCommands |
---|
230 | ); |
---|
231 | if(res.success){ |
---|
232 | // the last if-condition, dojo.toJson() adds a quote sign " |
---|
233 | // before and after the result, may be we remove it and |
---|
234 | // test the result again |
---|
235 | console.info(msg+"OK: "+viewCommands); |
---|
236 | oks += 1; |
---|
237 | }else{ |
---|
238 | this.errors.push({ |
---|
239 | commands: t.commands, |
---|
240 | actual: res.actualResult, |
---|
241 | expected: t.expectedResult |
---|
242 | }); |
---|
243 | console.error(msg+"Failed: "+viewCommands, { |
---|
244 | commands: t.commands, |
---|
245 | actualResult: res.actualResult, |
---|
246 | expectedResult: t.expectedResult |
---|
247 | }); |
---|
248 | } |
---|
249 | } |
---|
250 | console.info(len+" tests ran.", oks+" Success,", this.errors.length+" Errors"); |
---|
251 | }, |
---|
252 | |
---|
253 | runTest: function(commands, expected){ |
---|
254 | var ret = { |
---|
255 | success: false, |
---|
256 | actualResult: null |
---|
257 | }; |
---|
258 | // Concat multiple commands with new lines, so "//" comments at |
---|
259 | // the end of a line don't deactivate the next line (which it |
---|
260 | // would if we only concatenated with ";"). |
---|
261 | var cmds = commands.join("\n"); |
---|
262 | ret.actualResult = eval(cmds); |
---|
263 | if( (String(ret.actualResult)==expected) || |
---|
264 | (dojo.toJson(ret.actualResult)==expected) || |
---|
265 | ( |
---|
266 | (expected.charAt(0)=='"')&& |
---|
267 | (expected.charAt(expected.length-1)=='"')&& |
---|
268 | (String(ret.actualResult)==expected.substring(1, expected.length-1)) |
---|
269 | ) |
---|
270 | ){ |
---|
271 | // the last if-condition, dojo.toJson() adds a quote sign " |
---|
272 | // before and after the result, may be we remove it and test |
---|
273 | // the result again |
---|
274 | ret.success = true; |
---|
275 | } |
---|
276 | return ret; |
---|
277 | } |
---|
278 | } |
---|
279 | ); |
---|
280 | |
---|
281 | }); |
---|