[483] | 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 | }); |
---|