lzunit.lzx
<library>
<script when="immediate">
//---
// The TestFailure class is used to record a failed test and the
// reason for its failure.
//
// @keywords private
//
// @param failedTest: the test that failed
// @param String reasonForFailure: the reason the test failed
//---
public final class TestFailure {
public var test:String;
public var reason:String;
public function TestFailure (failedTest:String, reasonForFailure:String) {
this.test = failedTest;
this.reason = reasonForFailure;
}
public function toString() :String {
return "TestFailure: " + this.test + " failed: " + this.reason;
}
}
//---
// The TestError class is used to record a test that casued a runtime
// error and the reason for the error. This is different from
// TestFailure in that sometimes we want to differentiate the two
//
// @keywords private
//
// @param erroredTest: the test that failed
// @param String reasonForError: the reason the test failed
//---
public final class TestError {
public var test:String;
public var reason:String;
public function TestError (erroredTest:String, reasonForError:String) {
this.test = erroredTest;
this.reason = reasonForError;
}
public function toString() :String {
return "TestError: " + this.test + " failed: " + this.reason;
}
}
</script>
<script>
// Features that can be disabled
var catchErrors = true;
var asynchronousTests = true;
canvas.runTests = 0;
</script>
<class name="DebugObject">
<attribute name="debugWrite" value="false
" type="boolean"/>
// compare two XML objects for lisp-style EQUAL
// this takes two string
<method name="xmlstringequals" args="str1, str2">
var xml1 = LzDataElement.stringToLzData(str1)
var xml2 = LzDataElement.stringToLzData(str2)
return xmlequals(xml1, xml2);
</method>
<method name="xmlequals" args="x1, x2">
if (x1.nodeType != x2.nodeType) return false;
// text node
if (x1.nodeType == 3) {
if (x1.data != x2.data) return false;
} else if (x1.nodeType == 1){
// shouldn't ever happen, childNodes should always be non-null
if ( ((x1.childNodes == null) && (x2.childNodes != null)) ||
((x1.childNodes != null) && (x2.childNodes == null))) return false;
if (x1.childNodes.length != x2.childNodes.length) return false;
// compare attributes
var x1attrs = x1.attributes;
var x1keys = [];
var x2attrs = x2.attributes;
var x2keys = [];
for (var attr in x1attrs) {
x1keys.push(attr);
}
for (var attr in x2attrs) {
x2keys.push(attr);
}
var limit = x1keys.length;
if (limit != x2keys.length) { return false; }
x1keys.sort();
x2keys.sort();
for (var i = 0; i < limit; i++) {
var key = x1keys[i];
if (key != x2keys[i]) { return false; }
if (x1attrs[key] != x2attrs[key]) { return false; }
}
// recurse
for (var i = 0; i < x1.childNodes.length; i++) {
if (!xmlequals(x1.childNodes[i], x2.childNodes[i])) {
return false;
}
}
} else {
return false;
}
return true;
</method>
// TODO: [2002-11-09 ptw] (ActionScript condition incompatible JavaScript)
// ActionScript does not obey Javascript semantics for testing whether
// an expression is true in a conditional
<method name="jsTrue" args="condition">
var t = typeof(condition);
if (t == "string") {
return condition.length > 0;
} else if (t == "object") {
return true;
// Safe test for undefined
} else if (t == "undefined") {
return false;
} else {
return !!condition;
}
</method>
<method name="construct" args="parent, args">
this.debugWrite = jsTrue(args["debugWrite"]);
super.construct(parent, args);
dw("DebugObject.construct(", args, ")");
</method>
<method name="dw" args="...args">
if (debugWrite) {
var s = "";
for (var i = 0; i < args.length; i++) {
var e = args[i];
if (typeof(e) == "string") {
s += e;
} else {
s += Debug.__String(e);
}
}
Debug.debug(s);
}
</method>
</class>
<class name="Test" extends="DebugObject" with="formatter">
<attribute name="result"/>
<attribute name="semantics" value="'actionscript'
"/>
<attribute name="debugLZUnit" value="false
"/>
<method name="construct" args="parent, args">
this.result = null;
super.construct(parent, args);
dw("Test.construct(", args, ")");
</method>
<method name="setResult" args="theTestResult">
if (typeof(theTestResult) == "undefined") {
theTestResult = new lz.TestResult();
}
this.result = theTestResult;
</method>
<method name="semanticsTrue" args="condition">
// Safe test for undefined
if (typeof(condition) == "undefined") {
return false;
} else if (semantics == "javascript") {
return jsTrue(condition);
} else if (semantics == "actionscript") {
return (!! condition);
} else {
error("Unknown semantics: " + semantics);
}
</method>
<method name="fail" args="message">
var suite = this.parent;
suite.ontestfail.sendEvent(message);
if (this.result) {
this.result.addFailure(message.toString());
} else if ($debug) {
Debug.debug('result is null on fail call: "' + message + '"');
}
if ($debug) {
var file = null, line = null;
if ($as3) {
} else {
// Find the failing test, which is four frames up
var bt = Debug.backtrace(4);
if (bt != null) {
var sf = bt[bt.length - 1];
file = sf.filename();
line = sf.lineno();
}
}
Debug.freshLine();
// create an error, which will include a backtrace, if applicable
Debug.__write(new LzError(file, line, message));
}
</method>
<method name="error" args="message=''">
var suite = this.parent;
suite.ontestfail.sendEvent(message);
if (this.result) {
this.result.addError(message);
} else if ($debug){
Debug.debug('result is null on error call: "' + message + '"');
}
if ($debug) {
Debug.freshLine();
// create an error, which will include a backtrace, if applicable
Debug.__write(new LzError(null, null, message));
}
</method>
<method name="tformat" args="message, expected, actual">
return this.formatToString(
'%s expected %#w got %#w',
(jsTrue(message) ? message + ": " : ""), expected, actual);
</method>
<method name="displayMessage" args="...args">
var message = this.formatToString.apply(this, args);
if (this.result) {
this.result.addMessage(message);
}
if ($debug) {
Debug.freshLine();
// create an error, which will include a backtrace, if applicable
Debug.__write(new LzInfo(null, null, message));
}
</method>
<method name="assertTrue" args="condition, assertion='True'">
if (! semanticsTrue(condition)) {
this.fail(tformat(assertion, true, condition));
}
canvas.setAttribute('runTests', canvas.runTests + 1)
</method>
<method name="assertFalse" args="condition, assertion='False'">
if (!! semanticsTrue(condition)) {
this.fail(tformat(assertion, false, condition));
}
canvas.setAttribute('runTests', canvas.runTests + 1)
</method>
<method name="assertEquals" args="expected, actual, message='Equals'">
if (! (expected == actual)) {
this.fail(tformat(message, expected, actual));
}
canvas.setAttribute('runTests', canvas.runTests + 1)
</method>
<method name="assertWithin" args="expected, actual, delta, message='Within'">
// handle infinite expected
if (expected == actual) return;
var error = (actual <= expected) ? (expected - actual) : (actual - expected);
// note NaN compares are always false
if (! (error <= delta)) {
this.fail(tformat(message, "" + expected + "\u00B1" + delta , actual));
}
canvas.setAttribute('runTests', canvas.runTests + 1)
</method>
<method name="assertNull" args="object, message='Null'">
if (object !== null) {
this.fail(tformat(message, null, object));
}
canvas.setAttribute('runTests', canvas.runTests + 1)
</method>
<method name="assertNotNull" args="object, message='NotNull'">
if (object === null) {
this.fail(tformat(message, "non-null value", object));
}
canvas.setAttribute('runTests', canvas.runTests + 1)
</method>
<method name="assertUndefined" args="object, message='Undefined'">
if (typeof(object) != "undefined") {
this.fail(tformat(message, "undefined value", object));
}
canvas.setAttribute('runTests', canvas.runTests + 1)
</method>
<method name="assertNotUndefined" args="object, message='NotUndefined'">
if (typeof(object) == "undefined") {
this.fail(tformat(message, "defined value", object));
}
canvas.setAttribute('runTests', canvas.runTests + 1)
</method>
<method name="assertSame" args="expected, actual, message='Same'">
// Use typeof to compare undefined without warnings
if (typeof(expected) == "undefined" &&
typeof(actual) == "undefined") {
return;
}
if (expected !== actual) {
this.fail(tformat(message, expected, actual));
}
canvas.setAttribute('runTests', canvas.runTests + 1)
</method>
<method name="assertNotSame" args="expected, actual, message='NotSame'">
if (expected === actual) {
// In-line Test.tformat so we can invert the sense
var msg = this.formatToString(
'%s expected anything but %#w got %#w',
message + ": ", expected, actual);
this.fail(msg);
}
canvas.setAttribute('runTests', canvas.runTests + 1)
</method>
</class>
<class name="TestCase" extends="Test">
<attribute name="name"/>
<attribute name="testnames" value="null
"/>
<method name="addTests">
if ($debug){
Debug.warn("You should override the TestCase.addTests method of",this, "to add your tests to the test case.", this);
}
</method>
<method name="addTest" args="testname">
if (this.testnames == null) {
this.testnames = [];
}
this.testnames.push(testname);
</method>
<method name="construct" args="parent, args">
if (jsTrue(args["testName"])) { this.name = args.testName; delete args.testName; }
super.construct(parent, args);
dw("TestCase.construct(", args, ")");
</method>
<method name="init">
super.init();
this.addTests();
</method>
<method name="run" args="theTestResult, theTestName">
dw("TestCase.run(", theTestName, ")");
setResult(theTestResult);
this.result.startTest(this.formatToString("%w/%s", this, theTestName));
setUp();
// NOTE: [2008-10-26 ptw] Intercepting source warnings is only
// available in debug mode
if ($debug) {
var inrsw = false;
var rsw = $reportSourceWarning;
var testcase = this;
var wrapper = function (filename, lineNumber, msg, fatal) {
if (! inrsw) {
try {
inrsw = true;
rsw.call(this, filename, lineNumber, msg, fatal);
testcase.error(msg);
} finally {
inrsw = false;
}
}
}
}
try {
if ($debug){
var savedrsw = $reportSourceWarning;
if (catchErrors) {
$reportSourceWarning = wrapper;
}
}
runTest(theTestName);
} catch(e) {
if ($debug) {
Debug.error("%s", e);
}
this.error('' + e);
} finally {
if ($debug){
if (catchErrors) {
$reportSourceWarning = savedrsw;
}
}
}
tearDown();
return this.result;
</method>
<method name="setUp">
</method>
<method name="runTest" args="theTestName">
if (typeof(theTestName) == "undefined") theTestName = name;
dw("TestCase.runTest(", theTestName, ")");
// Invoke the test method
var m = this[theTestName];
if (typeof(m) != "function") {
error("method '" + theTestName + "' not found");
}
else {
m.call(this);
}
</method>
<method name="tearDown">
</method>
</class>
<class name="SyncTester" extends="TestCase">
<attribute name="tested_object" type="expression"/>
<attribute name="current_method" value="${cur_meth.xpathQuery('@name')}"/>
<attribute name="del"/>
<method name="inspect" args="res"/>
<datapointer name="cur_meth" xpath="$once{parent.name + '_methods:/*/call[1]'}"/>
<method name="callNext">
var p = cur_meth.xpathQuery('@args')
var e = cur_meth.xpathQuery('@event')
if (!this['del'])
{
this.del = (typeof(e) != "undefined" ? new LzDelegate(this, '_handler',
tested_object, e) : new LzDelegate(this, '_handler'))
}
else if (typeof(e) != "undefined")
this.del.register(tested_object, e);
if (typeof(p) != "undefined")
tested_object[current_method](p, del)
else
tested_object[current_method](del)
</method>
<method name="testBegin">
callNext()
</method>
<method name="_handler" args="res">
var o = cur_meth.xpathQuery('@tester')
if (typeof(o) != "undefined")
this[o](res);
else
inspect(res);
if (cur_meth.selectNext())
callNext()
</method>
<doc>
<tag name="shortdesc">
<text>An extension of TestCase for testing asynchronous objects safely.</text>
</tag>
<text>
<p>SyncTester is an extension of TestCase that is useful for testing objects whose method are to be called sequentially, in effect synchronizing methods with potentially asynchronous behavior.</p>
<p>To take advantage of this helper class, you must declare a dataset named "<instance name>_methods", with a root node whose children are the method names to be called synchronously. The method nodes must be named "call", and have at least the "name" attribute defined. If the method needs to be called with arguments, specify them as value of the optional "args" attribute (only one argument is currently supported).</p>
<p>Your specific tests will only run once a method returns. It is possible to provide an inspector method for each of the asynchronous methods declared; you reference it with the "tester" attribute of a node in the dataset. These inspector methods must be defined on the SyncTester object. If you dont specify a tester for a method, the default handler named <attribute>inspect</attribute> will be called with the result of the method call as an argument. You should override this method if you want to have a generic inspector for most or all of your methods.</p>
<p>Generally speaking, you would expect that an event is sent when a method is done. This framework allows you to specify what event indicates the end of method execution by declaring the "event" attribute. It is assumed that the sender of the event is the object referenced by the <class>tested_object</class> attribute, or that the following method accepts a delegate to call on completion, as the last argument. If neither of these assumptions is correct, the flow of method execution will break.</p>
<p>For example, if you have an instance of this class named "userinfo", then your list of methods might be declared like this:</p>
<programlisting>
<dataset name="userinfo_methods">
<suite>
<call name="isAuthenticated" args="admin"/>
<call name="getExpiration" event="ondata"/>
<call name="createAccount" event="onload" tester="checkAcct"/>
</suite>
</dataset>
</programlisting>
</text>
</doc>
</class>
<class name="TestResult" extends="DebugObject" opacity="0.9" bgcolor="0xCCCCCC">
<attribute name="totalTests" value="${canvas.runTests}"/>
<attribute name="failedTests"/>
<attribute name="erroredTests"/>
<attribute name="currentTest"/>
<attribute name="failures"/>
<attribute name="errors"/>
<attribute name="messages"/>
<method name="construct" args="parent, args">
this.failedTests = 0;
this.erroredTests = 0;
this.currentTest = null;
this.failures = [];
this.errors = [];
this.messages = [];
super.construct(parent, args);
dw("TestResult.construct(", args, ");");
</method>
<method name="startTest" args="test">
this.currentTest = test;
update();
</method>
<handler name="ontotalTests">
update()
</handler>
<method name="addFailure" args="reason">
var f = new TestFailure(currentTest, reason);
dw("TestResult.AddFailure(", f, ");");
this.failedTests++;
this.failures.push(f);
this.update();
</method>
<method name="addError" args="reason">
var f = new TestError(currentTest, reason);
dw("TestResult.AddError(", f, ");");
this.erroredTests++;
errors.push(f);
update();
</method>
<method name="addMessage" args="...args">
messages.push(readout.formatToString.apply(readout, args));
update();
</method>
<method name="toString">
var s = "Tests: " + this.totalTests +
" Failures: " + failedTests +
" Errors: " + erroredTests;
for (var i = 0, len = failures.length; i < len; ++i)
s += "\n" + failures[i];
for (var i = 0, len = errors.length; i < len; ++i)
s += "\n" + errors[i];
for (var i = 0, len = messages.length; i < len; ++i)
s += "\n" + messages[i];
return s;
</method>
<method name="numFailures">
var totalBad = erroredTests + failedTests;
return totalBad;
</method>
<method name="update">
var totalBad = erroredTests + failedTests;
if (totalTests > 0) {
with (display.progress) {
var bw = background.width;
errorbar.setAttribute("width", bw * erroredTests / totalTests);
failbar.setAttribute("width", bw * totalBad / totalTests);
donebar.setAttribute("width", bw * totalTests / totalTests);
}
}
if (totalBad > 0) {
readout.setAttribute("bgcolor", lz.colors.red);
}
// TODO: [2002-11-10 ptw] setAttribute("text", ...) does not work?
readout.setAttribute("text", lz.Browser.xmlEscape(this.toString()));
</method>
<view name="display" x="0" y="0" width="100%" height="100%">
<simplelayout axis="y" spacing="15"/>
<text>Test Progress</text>
<view name="progress" bgcolor="black" width="${parent.width}" height="10">
<view name="background" bgcolor="white" width="${parent.width-2}" height="8" x="1" y="1"/>
<view name="donebar" bgcolor="green" width="0" height="8" x="1" y="1"/>
<view name="failbar" bgcolor="red" width="0" height="8" x="1" y="1"/>
<view name="errorbar" bgcolor="yellow" width="0" height="8" x="1" y="1"/>
</view>
<text id="readout" selectable="true" width="100%" multiline="true">
Test Results
</text>
</view>
</class>
<view name="lzunitControlPanel" x="0" y="0" height="100%" width="100%" options="ignorelayout">
<handler name="oninited" reference="canvas">
this.bringToFront();
if ($debug) {
Debug.ensureVisible();
}
</handler>
<simplelayout axis="y" spacing="10"/>
<TestResult name="theTestResult" width="100%" height="100%"/>
</view>
<class name="TestSuite" extends="Test" width="100%" height="100%">
<attribute name="logfile" type="string" value="lzunit.log
"/>
<attribute name="resultstring" value="''
"/>
<attribute name="tests"/>
<attribute name="nextCase"/>
<attribute name="nextTest"/>
<event name="onsuitestart"/>
<event name="onsuitefinish"/>
<event name="onteststart"/>
<event name="ontestfinish"/>
<event name="ontestfail"/>
<method name="sendLogData" args="logfile,msg">
var url:LzURL = lz.Browser.getLoadURLAsLzURL();
// compute the base directory of the current app,
// url.path looks like "/trunk/path/to/file.lzx"
var base:String = url.path.substring(0, url.path.indexOf("/", 1));
url.path = base + "/test/lzunit/";
url.file = "Logger.jsp";
url.query = "logfile=" + encodeURIComponent(logfile) + "&msg=" + encodeURIComponent(msg);
var tloader = new LzHTTPLoader(this, false);
tloader.loadSuccess = this.loadComplete;
tloader.open("GET" , url.toString(), /* username */ null, /* password */ null);
tloader.send(/* content */ null);
</method>
<method name="construct" args="parent, args">
// TODO: [2002-11-10 ptw] (uninitialized attribute) should
// not have to set value
this.tests = null;
this.nextCase = 0;
this.nextTest = 0;
super.construct(parent, args);
dw("TestSuite.construct(", args, ")");
</method>
<handler name="onsuitefinish">
//Debug.debug("onsuitefinish");
//this.resultstring += ("failures: "+ this.result.numFailures()+ "\n");
//this.resultstring += ("time: "+ (((new Date)['getTime']()) - this.starttime)+"\n");
this.resultstring += "finish_testsuite: "+this.testpath + " failures: "+ this.result.numFailures()+ "\n";
this.sendLogData(this.logfile, this.resultstring);
</handler>
<method name="loadComplete" args="loader:LzHTTPLoader, data:*">
if (lz.Browser.getInitArg('close_when_finished') == 'true') {
Debug.info('query arg "close_when_finished" is set, closing window.');
var cleanupwindow = function () {
lz.Browser.callJS("window.close()");
}
LzTimeKernel.setTimeout(cleanupwindow, 3000);
}
</method>
<handler name="onteststart" args="tc">
this.testStartTime = ((new Date)['getTime']());
//var testcase = "testcase: "+tc+" "+((((new Date)['getTime']()) - this.testStartTime));
//this.resultstring += (testcase+"\n");
</handler>
<handler name="ontestfail" args="msg">
//this.resultstring += ("failure: "+msg+"\n");
</handler>
<method name="init">
super.init();
this.tests = [];
initSuite()
var lzurl = lz.Browser.getLoadURLAsLzURL();
// strip the deployment name from the path
this.testpath = lzurl.path.substring(lzurl.path.indexOf("/", 1) + 1) + lzurl.file;
this.starttime = ((new Date)['getTime']());
//this.resultstring = "start testsuite: "+this.testpath +"\n";
</method>
<method name="initSuite" args="ignore=null">
if (this.nextCase == subviews.length)
{
this.nextCase = 0
dw("TestSuite.initSuite(", this, ")");
run()
}
else
{
var sv = subviews[this.nextCase];
dw("TestSuite.initSuite: subviews[", this.nextCase, "] = ", sv);
if (sv instanceof lz.TestCase && sv.testnames != null) {
for (var n = 0; n < sv.testnames.length; n++) {
var t = sv.testnames[n];
if (typeof(sv[t]) == "function") {
//--- /^test/.test(n)
if (t.indexOf("test") == 0) {
if (typeof(tests[this.nextCase]) == "undefined")
tests[this.nextCase] = [];
dw("tests[", this.nextCase, "].push(", t, ");");
tests[this.nextCase].push(t);
}
}
}
}
this.nextCase++
var del = new LzDelegate(this, "initSuite")
lz.Idle.callOnIdle(del)
}
</method>
<method name="run">
dw("TestSuite.run()");
// bleah
this.setResult(controlPanel.theTestResult);
dw("TestSuite.result = ", this.result);
dw("tests.length = ", tests.length);
this.nextCase = 0;
this.nextTest = 0;
if (asynchronousTests) {
runNextTest();
} else {
for (var v = 0; v < this.tests.length; ++v) {
var tc = this.tests[v];
if (typeof(tc) != "undefined") {
dw("tc.length = ", tc.length);
for (var i = 0; i < tc.length; ++i) {
dw("subviews[", v, "].run(", this.result, ", ", tc[i], ");");
subviews[v].run(this.result, tc[i]);
}
}
}
}
dw("TestSuite.result = ", this.result);
return this.result;
</method>
<method name="runNextTest" args="ignore=null">
dw("In run next test, nextCase: ", this.nextCase, " nextTest: ", this.nextTest);
var v = this.nextCase;
if (v > this.tests.length) {
this.onsuitefinish.sendEvent(this.result.numFailures() > 0 ? 'fail' : 'pass');
return false;
}
var tc = this.tests[v];
var i = this.nextTest++;
if (typeof(tc) == "undefined" || (i >= tc.length)) {
this.nextCase++;
this.nextTest = 0;
} else {
dw("subviews[", v, "].run(", this.result, ", ", tc[i], ");");
this.onteststart.sendEvent(tc[i]);
subviews[v].run(this.result, tc[i]);
this.ontestfinish.sendEvent([tc[i],this.result.numFailures() > 0 ? 'fail' : 'pass']);
}
var c = new LzDelegate( this , "runNextTest" )
lz.Idle.callOnIdle(c);
return true;
</method>
<method name="addTest" args="theTest">
tests.append(theTest);
</method>
<attribute name="controlPanel" value="${canvas.lzunitControlPanel}"/>
<doc>
<tag name="shortdesc">
<text>A view that comprises a suite of LZUnit tests.</text>
</tag>
<text>
<p>
This is the LZUnit library. LZUnit is an implementation of the
xUnit testing framework for <code>LZX</code> programs (cf., <a href="http://junit.sourceforge.net/doc/cookstour/cookstour.htm" shape="rect">JUnit
A Cook's Tour</a>).
</p>
<p>
Each of the xUnit components is implemented as an LZX tag with the
corresponding name. Tests can be written by defining a subclass of
<code>TestCase</code> and defining <code>test<i>...</i></code>
methods. A test suite can be created by enclosing any number of
<code>TestCases</code> in a <code>TestSuite</code>.
</p>
<p>
The usual helper methods, <code>assertTrue</code>,
<code>assertEquals</code>, <code>assertWithin</code>,
<code>assertSame</code>, etc. are available for implementing the
tests. (See the documentation of <code>Test</code> for a full
list.)
</p>
<p>
An <code>LZX</code> program that consists of a
<code>TestSuite</code> will, when loaded, automatically run all of
its child <code>TestCases</code> and report the number of test cases
run, the number of failures, and the number of errors. If any error
occurs, an obvious error message is presented.
</p>
<p>
Below is a simple example of the use of LZUnit demonstrating a
successful test, a failed test, and a test that causes an error.
</p>
<note><para>You must run LZUnit with debugging on for it to detect errors.</para></note>
<p>For a more in depth discussion, please see the <a href="../guide/lzunit.html" target="laszlo-dguide" shape="rect">Developer's Guide</a>.</p>
<example>
<programlisting>
<canvas debug="true">
<debug y="275"/>
<include href="lzunit"/>
<class name="Tautologies" extends="TestCase">
<method name="addTests" override="true">
addTest("testSuccess");
</method>
<method name="testSuccess">
assertTrue(true);
assertFalse(false);
assertEquals(null, undefined);
assertWithin(0, .001, .01);
assertSame(null, null);
assertNotSame(null, undefined);
assertNull(null);
assertNotNull(undefined);
assertUndefined(undefined);
assertNotUndefined(null);
</method>
</class>
<class name="IntentionalBugs" extends="TestCase">
<method name="addTests" override="true">
addTest("testFailure");
addTest("testError");
</method>
<method name="testFailure">
fail("This is an intentional failure");
</method>
<method name="testError">
error("This is an intentional error");
</method>
</class>
<TestSuite>
<Tautologies/>
<IntentionalBugs/>
</TestSuite>
</canvas>
</programlisting>
</example>
</text>
</doc>
</class>
</library>
Cross References
Classes
Named Instances