You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
281 lines
9.5 KiB
281 lines
9.5 KiB
define("dojox/testing/DocTest", ["dojo/string"], function() { |
|
|
|
return dojo.declare( |
|
"dojox.testing.DocTest", |
|
null, |
|
{ |
|
// summary: |
|
// This class executes doctests. |
|
// description: |
|
// DocTests are tests that are defined inside the comment. |
|
// A doctest looks as if it was copied from the shell (which it mostly is). |
|
// A doctest is executed when the following conditions match: |
|
// 1) all lines are comments |
|
// 2) the line always starts with spaces/tabs followed by "//" |
|
// and at least one space |
|
// 3) the line(s) of the test to execute starts with ">>>" |
|
// preceded by what is described in 2) |
|
// 4) the first line after 3) starting without ">>>" is the exptected result. |
|
// preceded by what is described in 2) |
|
// 5) the test sequence is terminated by an empty line, or the next |
|
// test in the following line, or a new line that does not start as described in 2) |
|
// (simple said: is not a comment) |
|
// preceded by what is described in 2) |
|
// |
|
// I.e. the following is a simple doctest, that will actually also be run |
|
// if you run this class against this file here: |
|
// >>> 1+1 // A simple test case. Terminated by an empty line |
|
// 2 |
|
// |
|
// >>> 1==2 |
|
// false |
|
// >>> "a"+"b" // Also without the empty line before, this is a new test. |
|
// "ab" |
|
// |
|
// >>> var anything = "anything" // Multiple commands for one test. |
|
// >>> "something"==anything |
|
// false |
|
// |
|
// DocTests are great for inline documenting a class or method, they also |
|
// are very helpful in understanding what the class/method actually does. |
|
// They don't make sense everywhere, but sometimes they are really handy. |
|
|
|
|
|
// TODO: |
|
// - using console.log() in a test prints something on the |
|
// console (if you do it on the console) but its not accepted |
|
// yet to be the test result, may be override console.log!? |
|
// i.e. here i wanted to: dojo.forEach(["one", 2], |
|
// function(el, index) {console.log(el, index)}) that works on |
|
// the console, but not as a docTest :-( |
|
// - surround the eval for each test case singlely with a |
|
// try-catch, to to catch syntax errors etc (though the |
|
// shouldn't happen if you copy the test from the shell :-)) |
|
|
|
|
|
errors: [], |
|
|
|
getTests:function(/*String*/moduleName){ |
|
// summary: |
|
// Extract the tests from the given module or string. |
|
// examples: |
|
// >>> dojo.isArray(new dojox.testing.DocTest().getTests("dojox.testing.DocTest")) // Use the module name to extract the tests from. |
|
// true |
|
var path = dojo.moduleUrl(moduleName).path; |
|
// TODO: |
|
// this needs to be done better, this is pretty simple and |
|
// surely not dummy proof |
|
var file = path.substring(0, path.length-1)+".js"; |
|
var xhr = dojo.xhrGet({url:file, handleAs:"text"}); |
|
// Make loading synchronously, mainly so we can use it in doh. |
|
var data = dojo._getText(file); |
|
return this._getTestsFromString(data, true); |
|
}, |
|
|
|
getTestsFromString:function(/*String*/data){ |
|
// examples: |
|
// >>> (new dojox.testing.DocTest().getTestsFromString(">>> 1+1\n2\n>>> 2+2\n4")).length // Do tests from strings get detected properly? |
|
// 2 |
|
return this._getTestsFromString(data, false); |
|
}, |
|
|
|
_getTestsFromString:function(/*String*/data, /*Boolean*/insideComments){ |
|
// summary: |
|
// Parse the given string for tests. |
|
// insideComments: Boolean |
|
// if false "data" contains only the pure tests, comments already stripped. |
|
var trim = dojo.hitch(dojo.string, "trim"); |
|
var lines = data.split("\n"); |
|
var len = lines.length; |
|
var tests = []; |
|
var test = { |
|
commands: [], |
|
expectedResult: [], |
|
line: null |
|
}; |
|
for(var i=0; i<len+1; i++){ |
|
// Trim the line, so we don't have to worry about leading |
|
// spaces or tabs, bla bla ... |
|
var l = trim(lines[i] || ""); // The '|| ""' makes sure tests that have no preceding \n are taken into account too. |
|
// TODO: |
|
// detect tests that dont match the condition: commands, |
|
// result, empty line. esp the empty line might be missing |
|
// or be tolerant and accept a new test starting on the |
|
// next line, which would allow to omit the empty line!? |
|
if((insideComments && l.match(/^\/\/\s+>>>\s.*/)) || l.match(/^\s*>>>\s.*/)){ |
|
if(!test.line){ |
|
test.line = i+1; |
|
} |
|
// Find the test commands. |
|
if(test.expectedResult.length>0){ |
|
// Start a new test right after the expected result, |
|
// without an empty line. |
|
tests.push({ |
|
commands: test.commands, |
|
expectedResult: test.expectedResult.join("\n"), |
|
line: test.line |
|
}); |
|
test = {commands:[], expectedResult:[], line:i+1}; |
|
} |
|
l = insideComments ? trim(l).substring(2, l.length) : l; // Remove the leading slashes. |
|
l = trim(l).substring(3, l.length); // Remove the ">>>". |
|
test.commands.push(trim(l)); |
|
}else if((!insideComments || l.match(/^\/\/\s+.*/)) && test.commands.length && test.expectedResult.length==0){ |
|
// Detect the lines after the ">>>"-lines, the exptected result. |
|
l = insideComments ? trim(l).substring(3, l.length) : l; // Remove the leading slashes. |
|
test.expectedResult.push(trim(l)); |
|
}else if(test.commands.length>0 && test.expectedResult.length){ |
|
if(!insideComments || l.match(/^\/\/\s*$/)){ |
|
// Detect the empty line. |
|
tests.push({ |
|
commands: test.commands, |
|
expectedResult: test.expectedResult.join("\n"), |
|
line: test.line |
|
}); |
|
} |
|
if(insideComments && !l.match(/^\/\//)){ |
|
// If the next line is not a comment at all (doesn't start with "//"). |
|
tests.push({ |
|
commands: test.commands, |
|
expectedResult: test.expectedResult.join("\n"), |
|
line:test.line |
|
}); |
|
} |
|
test = { |
|
commands: [], |
|
expectedResult: [], |
|
line:0 |
|
}; |
|
} |
|
} |
|
return tests; |
|
}, |
|
|
|
run: function(moduleName){ |
|
// summary: |
|
// Run the doctests in the module given. |
|
// example: |
|
// doctest = new dojox.testing.DocTest(); |
|
// doctest.run("dojox.testing.DocTest"); |
|
// doctest.errors should finally be an empty array. |
|
// // The above is not a doctest, because it just would |
|
// // execute itself in a never ending loop. |
|
// |
|
// >>> true==true // Test a new line terminating the test. |
|
// true |
|
// |
|
// >>> true==true // Test a new test terminating the test. |
|
// true |
|
// >>> true==true // Test a "not a comment"-line, especially an empty line terminating the test. |
|
// true |
|
|
|
// Make sure the result as printed on the console is the same as what |
|
// is returned by the test. An array is printed as follows on the console. |
|
// >>> [1,2,3,4] |
|
// [1,2,3,4] |
|
// |
|
// Test a "not a comment"-line, with some real code(!) terminating the test. |
|
// This used to be a bug, so make sure the line below the test is real |
|
// from this method! Don't write a test after it, always above! |
|
// >>> true==true // Test code on new line terminating the test. |
|
// true |
|
|
|
this.errors = []; |
|
|
|
var tests = this.getTests(moduleName); |
|
if(tests){ |
|
this._run(tests); |
|
} |
|
}, |
|
|
|
_run: function(/*Array*/tests){ |
|
// summary: |
|
// Each element in the array contains the test in the first element, |
|
// and the expected result in the second element. |
|
// tests: |
|
// Make sure that the types are compared properly. There used to be |
|
// the bug that a return value false was compared to "false" which |
|
// made the test fail. This is fixed and should be verified by the |
|
// following tests. |
|
// >>> false |
|
// false |
|
// |
|
// >>> "false" |
|
// "false" |
|
// |
|
// >>> true |
|
// true |
|
// |
|
// >>> 1 |
|
// 1 |
|
// |
|
// >>> "s" |
|
// "s" |
|
// |
|
// >>> dojo.toJson({one:1}) |
|
// "{"one":1}" |
|
// |
|
var len = tests.length; |
|
this.tests = len; |
|
var oks = 0; |
|
for(var i=0; i<len; i++){ |
|
var t = tests[i]; |
|
var res = this.runTest(t.commands, t.expectedResult); |
|
var msg = "Test "+(i+1)+": "; |
|
var viewCommands = t.commands.join(" "); |
|
// Show the first part of the test command. |
|
viewCommands = (viewCommands.length > 50 ? |
|
viewCommands.substr(0,50) + "..." : |
|
viewCommands |
|
); |
|
if(res.success){ |
|
// the last if-condition, dojo.toJson() adds a quote sign " |
|
// before and after the result, may be we remove it and |
|
// test the result again |
|
console.info(msg+"OK: "+viewCommands); |
|
oks += 1; |
|
}else{ |
|
this.errors.push({ |
|
commands: t.commands, |
|
actual: res.actualResult, |
|
expected: t.expectedResult |
|
}); |
|
console.error(msg+"Failed: "+viewCommands, { |
|
commands: t.commands, |
|
actualResult: res.actualResult, |
|
expectedResult: t.expectedResult |
|
}); |
|
} |
|
} |
|
console.info(len+" tests ran.", oks+" Success,", this.errors.length+" Errors"); |
|
}, |
|
|
|
runTest: function(commands, expected){ |
|
var ret = { |
|
success: false, |
|
actualResult: null |
|
}; |
|
// Concat multiple commands with new lines, so "//" comments at |
|
// the end of a line don't deactivate the next line (which it |
|
// would if we only concatenated with ";"). |
|
var cmds = commands.join("\n"); |
|
ret.actualResult = eval(cmds); |
|
if( (String(ret.actualResult)==expected) || |
|
(dojo.toJson(ret.actualResult)==expected) || |
|
( |
|
(expected.charAt(0)=='"')&& |
|
(expected.charAt(expected.length-1)=='"')&& |
|
(String(ret.actualResult)==expected.substring(1, expected.length-1)) |
|
) |
|
){ |
|
// the last if-condition, dojo.toJson() adds a quote sign " |
|
// before and after the result, may be we remove it and test |
|
// the result again |
|
ret.success = true; |
|
} |
|
return ret; |
|
} |
|
} |
|
); |
|
|
|
}); |