Created
December 7, 2009 19:46
-
-
Save mckoss/251044 to your computer and use it in GitHub Desktop.
Enigma Machine Simulator with Unit Tests
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<title>Engima Test Page</title> | |
<script src="namespace.js"></script> | |
<script src="enigma.js"></script> | |
<script src="unit.js"></script> | |
</head> | |
<body> | |
<h1><script>document.write(document.title);</script></h1> | |
<SCRIPT> | |
ts = new UT.TestSuite(); | |
ts.DWOutputDiv(); | |
</script> | |
<script> | |
var Enigma = global_namespace.Import('startpad.enigma'); | |
Enigma.fnTrace = console.log; | |
ts.AddTest("Basic Reciprocal Encoding", function(ut) | |
{ | |
var machine = new Enigma.Enigma(); | |
var cipher = machine.Encode("plain text"); | |
machine.Init(); | |
var plain = machine.Encode(cipher); | |
ut.AssertEq(plain, "PLAIN TEXT"); | |
}); | |
ts.AddTest("Sample Config", function(ut) | |
{ | |
var aTests = [ | |
["ENIGMA REVEALED", "QMJIDO MZWZJFJR"], | |
["QMJIDO PQDPSRJLCV", "ENIGMA AUFGEDECKT"] | |
]; | |
var enigma = new Enigma.Enigma(); | |
for (var i in aTests) | |
{ | |
ut.AssertEq(enigma.Encode(aTests[i][0]), aTests[i][1]); | |
enigma.Init(); | |
ut.AssertEq(enigma.Encode(aTests[i][1]), aTests[i][0]); | |
enigma.Init(); | |
} | |
}); | |
ts.AddTest("Rotor Motion", function(ut) | |
{ | |
var enigma = new Enigma.Enigma(); | |
enigma.Init({position:['A', 'D', 'V']}); | |
ut.AssertEq(enigma.toString(), "Enigma Rotors: I-II-III Position: ADV"); | |
ut.AssertEq(enigma.Encode("A"), "Q"); | |
ut.AssertEq(enigma.toString(), "Enigma Rotors: I-II-III Position: AEW"); | |
ut.AssertEq(enigma.Encode("A"), "I"); | |
ut.AssertEq(enigma.toString(), "Enigma Rotors: I-II-III Position: BFX"); | |
ut.AssertEq(enigma.Encode("A"), "B"); | |
ut.AssertEq(enigma.toString(), "Enigma Rotors: I-II-III Position: BFY"); | |
enigma.Init(); | |
ut.AssertEq(enigma.Encode("AA"), "QI"); | |
ut.AssertEq(enigma.toString(), "Enigma Rotors: I-II-III Position: BFX"); | |
}); | |
ts.AddTest("Ring Settings", function(ut) | |
{ | |
var enigma = new Enigma.Enigma(); | |
enigma.Init({position:['A', 'A', 'A']}); | |
ut.AssertEq(enigma.toString(), "Enigma Rotors: I-II-III Position: AAA"); | |
ut.AssertEq(enigma.Encode("AAA"), "BDZ"); | |
enigma.Init({rings: ['A', 'A', 'B']}); | |
ut.AssertEq(enigma.toString(), "Enigma Rotors: I-II-III Position: AAA Rings: AAB"); | |
ut.AssertEq(enigma.Encode("AAA"), "UBD"); | |
enigma.Init({position: ['A', 'A', 'U']}); | |
ut.AssertEq(enigma.toString(), "Enigma Rotors: I-II-III Position: AAU Rings: AAB"); | |
ut.AssertEq(enigma.Encode("AAA"), "BTU"); | |
}); | |
ts.AddTest("Plugboard Settings", function(ut) | |
{ | |
var enigma = new Enigma.Enigma(); | |
enigma.Init({plugs:"AB CD E-F"}); | |
ut.AssertEq(enigma.toString(), "Enigma Rotors: I-II-III Position: MCK Plugboard: AB CD EF"); | |
var cipher = enigma.Encode("ABCDEF"); | |
enigma.Init(); | |
ut.AssertEq(enigma.Encode(cipher), "ABCDEF"); | |
}); | |
ts.Run(); | |
ts.Report(); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Enigma.js | |
Enigma machine simulation. | |
Copyright (c) 2009, Mike Koss | |
See Paper Enigma at: | |
http://mckoss.com/Crypto/Enigma.htm | |
Usage: | |
var enigma = global_namespace.Import('startpad.enigma'); | |
var machine = new enigma.Enigma(); | |
var cipher = machine.Encode("plain text"); | |
machine.Init(); | |
var plain = machine.Encode(cipher); -> "PLAIN TEXT" | |
Rotor settings from: | |
http://homepages.tesco.net/~andycarlson/enigma/simulating_enigma.html | |
*/ | |
global_namespace.Define('startpad.enigma', function (NS) | |
{ | |
NS.mRotors = { | |
I: {wires: "EKMFLGDQVZNTOWYHXUSPAIBRCJ", notch: 'Q'}, | |
II: {wires: "AJDKSIRUXBLHWTMCQGZNPYFVOE", notch: 'E'}, | |
III: {wires: "BDFHJLCPRTXVZNYEIWGAKMUSQO", notch: 'V'}, | |
IV: {wires: "ESOVPZJAYQUIRHXLNFTGKDCMWB", notch: 'J'}, | |
V: {wires: "VZBRGITYUPSDNHLXAWMJQOFECK", notch: 'Z'} | |
}; | |
NS.mReflectors = { | |
B: {wires: "YRUHQSLDPXNGOKMIEBFZCWVJAT"}, | |
C: {wires: "FVPJIAOYEDRZXWGCTKUQSBNMHL"}, | |
}; | |
NS.fnTrace = undefined; | |
var codeA = 'A'.charCodeAt(0); | |
function IFromCh(ch) | |
{ | |
ch = ch.toUpperCase(); | |
return ch.charCodeAt(0) - codeA; | |
} | |
function ChFromI(i) | |
{ | |
return String.fromCharCode(i + codeA); | |
} | |
function MapRotor(rotor) | |
{ | |
// Determine the relative offset (mod 26) of encoding and decoding each letter | |
// (wire position) | |
rotor.map = {}; | |
rotor.mapRev = {}; | |
for (var iFrom = 0; iFrom < 26; iFrom++) | |
{ | |
var iTo = IFromCh(rotor.wires.charAt(iFrom)); | |
rotor.map[iFrom] = (26 + iTo - iFrom) % 26; | |
rotor.mapRev[iTo] = (26 + iFrom - iTo) % 26; | |
} | |
} | |
// Compute forward and reverse mappings for rotors and reflectors | |
for (var sRotor in NS.mRotors) | |
MapRotor(NS.mRotors[sRotor]); | |
for (var sReflector in NS.mReflectors) | |
MapRotor(NS.mReflectors[sReflector]); | |
NS.Enigma = function(settings) | |
{ | |
this.fnTrace = NS.fnTrace; | |
this.settings = { | |
rotors: ['I', 'II', 'III'], | |
reflector: 'B', | |
position: ['M', 'C', 'K'], | |
rings: ['A', 'A', 'A'], | |
plugs: "" | |
}; | |
NS.Extend(this.settings, settings); | |
this.Init(); | |
}; | |
NS.Extend(NS.Enigma.prototype, { | |
Init: function(settings) | |
{ | |
NS.Extend(this.settings, settings); | |
this.rotors = []; | |
for (var i in this.settings.rotors) | |
this.rotors[i] = NS.mRotors[this.settings.rotors[i]]; | |
this.reflector = NS.mReflectors[this.settings.reflector]; | |
// Position is for the position of the out Rings (i.e. the visible | |
// marking on the code wheel. | |
this.position = []; | |
for (var i in this.settings.rotors) | |
{ | |
this.position[i] = (IFromCh(this.settings.position[i])); | |
} | |
this.rings = []; | |
for (var i in this.settings.rings) | |
{ | |
this.rings[i] = IFromCh(this.settings.rings[i]); | |
} | |
this.settings.plugs = this.settings.plugs.toUpperCase(); | |
this.settings.plugs = this.settings.plugs.replace(/[^A-Z]/g, ''); | |
if (this.settings.plugs.length % 2 == 1) | |
console.warn("Invalid plugboard settings - must have an even number of characters."); | |
this.mPlugs = {}; | |
for (var i = 0; i < 26; i++) | |
this.mPlugs[i] = i; | |
for (var i = 0; i < this.settings.plugs.length; i += 2) | |
{ | |
var iFrom = IFromCh(this.settings.plugs[i]); | |
var iTo = IFromCh(this.settings.plugs[i+1]); | |
if (this.mPlugs[iFrom] != iFrom) | |
console.warn("Redefinition of plug setting for " + ChFromI(iFrom)); | |
if (this.mPlugs[iTo] != iTo) | |
console.warn("Redefinition of plug setting for " + ChFromI(iTo)); | |
this.mPlugs[iFrom] = iTo; | |
this.mPlugs[iTo] = iFrom; | |
} | |
if (this.fnTrace) | |
this.fnTrace("Init: " + this.toString()) | |
}, | |
/* Return machine state as a string | |
* | |
* Format: "Enigma Rotors: I-II-III Position: ABC <Rings:AAA> <Plugboard: AB CD>" | |
* | |
* Rings settings of AAA not displayed. | |
* Null plugboard not displayed. | |
*/ | |
toString: function() | |
{ | |
var s = ""; | |
s += "Enigma Rotors: "; | |
s += this.settings.rotors.join("-"); | |
s += " Position: "; | |
for (var i in this.position) | |
s += ChFromI(this.position[i]); | |
var sT = "Rings: "; | |
for (var i in this.rings) | |
sT += ChFromI(this.rings[i]); | |
if (sT != "Rings: AAA") | |
s += " " + sT; | |
var sT = "Plugboard: " | |
var chSep = ""; | |
for (var i = 0; i < 26; i++) | |
{ | |
if (i < this.mPlugs[i]) | |
{ | |
sT += chSep + ChFromI(i) + ChFromI(this.mPlugs[i]); | |
chSep = " "; | |
} | |
} | |
if (sT != "Plugboard: ") | |
s += " " + sT; | |
return s; | |
}, | |
IncrementRotors: function() | |
{ | |
/* Note that notches are components of the outer rings. So wheel | |
* motion is tied to the visible rotor position (letter or number) | |
* NOT the wiring position - which is dictated by the rings settings | |
* (or offset from the 'A' position). | |
*/ | |
// Middle notch - all rotors rotate | |
if (this.position[1] == IFromCh(this.rotors[1].notch)) | |
{ | |
this.position[0] += 1; | |
this.position[1] += 1; | |
} | |
// Right notch - right two rotors rotate | |
else if (this.position[2] == IFromCh(this.rotors[2].notch)) | |
this.position[1] += 1; | |
this.position[2] += 1; | |
for (var i in this.rotors) | |
this.position[i] = this.position[i] % 26; | |
}, | |
EncodeCh: function(ch) | |
{ | |
var aTrace = []; | |
var i; | |
ch = ch.toUpperCase(); | |
// Short circuit non alphabetics | |
if (ch < 'A' || ch > 'Z') | |
return ch; | |
this.IncrementRotors(); | |
i = IFromCh(ch); | |
aTrace.push(i); | |
i = this.mPlugs[i]; | |
aTrace.push(i); | |
for (var r = 2; r >= 0; r--) | |
{ | |
var d = this.rotors[r].map[(26 + i + this.position[r] - this.rings[r]) % 26]; | |
i = (i + d) % 26; | |
aTrace.push(i); | |
} | |
i = (i + this.reflector.map[i]) % 26; | |
aTrace.push(i); | |
for (var r = 0; r < 3; r++) | |
{ | |
var d = this.rotors[r].mapRev[(26 + i + this.position[r] - this.rings[r]) % 26]; | |
i = (i + d) % 26; | |
aTrace.push(i); | |
} | |
i = this.mPlugs[i]; | |
aTrace.push(i); | |
var chOut = ChFromI(i); | |
if (this.fnTrace) | |
{ | |
var s = ""; | |
var chSep = ""; | |
for (var i in aTrace) | |
{ | |
s += chSep + ChFromI(aTrace[i]); | |
chSep = "->"; | |
} | |
this.fnTrace(s + " " + this.toString()); | |
} | |
return chOut; | |
}, | |
Encode: function(s) | |
{ | |
var sOut = ""; | |
for (var i = 0; i < s.length; i++) | |
sOut += this.EncodeCh(s[i]); | |
return sOut; | |
} | |
}); | |
}); // startpad.enigma |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Namespace.js | |
Version 1.0, June 2009 | |
by Mike Koss - released into the public domain. | |
Support for building modular namespaces in javascript. | |
Globals: | |
window.global_namespace (Namespace) - The top of the namespace heirarchy. Child namespaces | |
are stored as properties in each namespace object. | |
*** Class Namespace *** | |
Methods: | |
ns.Define(sPath, fnCallback(ns)) - Define a new Namespace object and call | |
the provided function with the new namespace as a parameter. | |
sPath - Path of the form ('unique.module.sub_module'). Pat | |
Returns the newly defined namespace. | |
ns.Extend(oDest, oSource) - Copy the (own) properties of the source object | |
into the destination object. Returns oDest. Note: This method is a convenience | |
function - it has no effect on the Namespace object itself. | |
ns.Import(sPath) - Return the namespace object with the given (absolute) path. | |
Usage example: | |
global_namespace.Define('startpad.base', function(ns) { | |
var Other = ns.Import('startpad.other'); | |
ns.Extend(ns, { | |
var1: value1, | |
var2: value2, | |
MyFunc: function(args) | |
{ | |
....Other.AFunction(args)... | |
} | |
}); | |
ns.ClassName = function(args) | |
{ | |
}; | |
ns.ClassName.prototype = { | |
constructor: ns.ClassName, | |
var1: value1, | |
Method1: function(args) | |
{ | |
} | |
}; | |
}); | |
*/ | |
// Define stubs for FireBug objects if not present | |
// This is here because this will often be the very first javascript file loaded | |
if (!window.console) | |
{ | |
(function () | |
{ | |
var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", | |
"group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"]; | |
window.console = {}; | |
for (var i = 0; i < names.length; ++i) | |
{ | |
window.console[names[i]] = function() {}; | |
} | |
})(); | |
} | |
(function() | |
{ | |
var sGlobal = 'global_namespace'; | |
// Don't run this function more than once. | |
if (window[sGlobal]) | |
return; | |
/** @constructor **/ | |
function Namespace(nsParent, sName) | |
{ | |
if (sName) | |
sName = sName.replace(/-/g, '_'); | |
this._nsParent = nsParent; | |
if (this._nsParent) | |
{ | |
this._nsParent[sName] = this; | |
this._sPath = this._nsParent._sPath; | |
if (this._sPath != '') | |
this._sPath += '.'; | |
this._sPath += sName; | |
} | |
else | |
this._sPath = ''; | |
}; | |
Namespace.prototype['Extend'] = function(oDest, var_args) | |
{ | |
if (oDest == undefined) | |
oDest = {}; | |
for (var i = 1; i < arguments.length; i++) | |
{ | |
var oSource = arguments[i]; | |
for (var prop in oSource) | |
{ | |
if (oSource.hasOwnProperty(prop)) | |
oDest[prop] = oSource[prop]; | |
} | |
} | |
return oDest; | |
}; | |
var ns = window[sGlobal] = new Namespace(null); | |
ns['Extend'](Namespace.prototype, { | |
'Define': function (sPath, fnCallback) | |
{ | |
sPath = sPath.replace(/-/g, '_'); | |
var aPath = sPath.split('.'); | |
var nsCur = this; | |
for (var i = 0; i < aPath.length; i++) | |
{ | |
var sName = aPath[i]; | |
if (nsCur[sName] == undefined) | |
new Namespace(nsCur, sName); | |
nsCur = nsCur[sName]; | |
} | |
// In case a namespace is multiply loaded - we ignore the definition function | |
// for all but the first call. | |
if (fnCallback) | |
{ | |
if (!nsCur._fDefined) | |
{ | |
nsCur._fDefined = true; | |
fnCallback(nsCur); | |
console.info("Namespace '" + nsCur._sPath + "' defined."); | |
} | |
else | |
console.warn("WARNING: Namespace '" + nsCur._sPath + "' redefinition."); | |
} | |
else if (!nsCur._fDefined) | |
console.warn("Namespace '" + nsCur._sPath + "' forward reference."); | |
return nsCur; | |
}, | |
'Import': function(sPath) | |
{ | |
return window[sGlobal]['Define'](sPath); | |
}, | |
'SGlobalName': function(sInNamespace) | |
{ | |
sInNamespace = sInNamespace.replace(/-/g, '_'); | |
return sGlobal + '.' + this._sPath + '.' + sInNamespace; | |
} | |
}); | |
})(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// unit.js - Unit testing framework | |
// Copyright (c) 2007-2009, Mike Koss ([email protected]) | |
// | |
// Usage: | |
// ts = new UT.TestSuite("Suite Name"); | |
// ts.DWOutputDiv(); | |
// ts.AddTest("Test Name", function(ut) { ... ut.Assert() ... }); | |
// ... | |
// ts.Run(); | |
// ts.Report(); | |
// | |
// Requires: base.js, timer.js | |
// UnitTest - Each unit test calls a function which in turn calls | |
// back Assert's on the unit test object. | |
Function.prototype.FnMethod = function(obj) | |
{ | |
var _fn = this; | |
return function () { return _fn.apply(obj, arguments); }; | |
}; | |
Function.prototype.FnArgs = function() | |
{ | |
var _fn = this; | |
var _args = []; | |
for (var i = 0; i < arguments.length; i++) | |
_args.push(arguments[i]); | |
return function () { | |
var args = []; | |
// In case this is a method call, preserve the "this" variable | |
var self = this; | |
for (var i = 0; i < arguments.length; i++) | |
args.push(arguments[i]); | |
for (i = 0; i < _args.length; i++) | |
args.push(_args[i]); | |
return _fn.apply(self, args); | |
}; | |
}; | |
var UT = { | |
// Extend(dest, src1, src2, ... ) | |
// Shallow copy properties in turn into dest object | |
Extend: function(dest) | |
{ | |
for (var i = 1; i < arguments.length; i++) | |
{ | |
var src = arguments[i]; | |
for (var prop in src) | |
{ | |
if (src.hasOwnProperty(prop)) | |
dest[prop] = src[prop]; | |
} | |
} | |
}, | |
DW: function(st) {document.write(st);}, | |
Browser: | |
{ | |
version: parseInt(navigator.appVersion), | |
fIE: navigator.appName.indexOf("Microsoft") != -1 | |
}, | |
// Convert all top-level object properties into a URL query string. | |
// {a:1, b:"hello, world"} -> "?a=1&b=hello%2C%20world" | |
StParams: function(obj) | |
{ | |
if (obj == undefined || obj == null) | |
return ""; | |
var stDelim = "?"; | |
var stParams = ""; | |
for (var prop in obj) | |
{ | |
if (!obj.hasOwnProperty(prop) || prop == "_anchor") | |
continue; | |
stParams += stDelim; | |
stParams += encodeURIComponent(prop); | |
// BUG: This is a bit bogus to encode a query param in JSON | |
if (typeof obj[prop] == "object") | |
stParams += "=" + encodeURIComponent(PF.EncodeJSON(obj[prop], true)); | |
else if (obj[prop] != null) | |
stParams += "=" + encodeURIComponent(obj[prop]); | |
stDelim = "&"; | |
} | |
if (obj._anchor) | |
stParams += "#" + encodeURIComponent(obj._anchor); | |
return stParams; | |
} | |
}; // UT | |
UT.Timer = function(fnCallback, ms) | |
{ | |
this.ms = ms; | |
this.fnCallback = fnCallback; | |
return this; | |
}; | |
UT.Timer.prototype = { | |
constructor: UT.Timer, | |
fActive: false, | |
fRepeat: false, | |
fInCallback: false, | |
fReschedule: false, | |
Repeat: function(f) | |
{ | |
if (f == undefined) | |
f = true; | |
this.fRepeat = f; | |
return this; | |
}, | |
Ping: function() | |
{ | |
// In case of race condition - don't call function if deactivated | |
if (!this.fActive) | |
return; | |
// Eliminate re-entrancy - is this possible? | |
if (this.fInCallback) | |
{ | |
this.fReschedule = true; | |
return; | |
} | |
this.fInCallback = true; | |
this.fnCallback(); | |
this.fInCallback = false; | |
if (this.fActive && (this.fRepeat || this.fReschedule)) | |
this.Active(true); | |
}, | |
// Calling Active resets the timer so that next call to Ping will be in this.ms milliseconds from NOW | |
Active: function(fActive) | |
{ | |
if (fActive == undefined) | |
fActive = true; | |
this.fActive = fActive; | |
this.fReschedule = false; | |
if (this.iTimer) | |
{ | |
clearTimeout(this.iTimer); | |
this.iTimer = undefined; | |
} | |
if (fActive) | |
this.iTimer = setTimeout(this.Ping.FnMethod(this), this.ms); | |
return this; | |
} | |
}; // UT.Timer | |
UT.UnitTest = function (stName, fn) | |
{ | |
this.stName = stName; | |
this.fn = fn; | |
this.rgres = []; | |
}; | |
UT.UnitTest.states = { | |
created: 0, | |
running: 1, | |
completed: 2 | |
}; | |
UT.UnitTest.prototype = { | |
constructor: UT.UnitTest, | |
state: UT.UnitTest.states.created, | |
cErrors: 0, | |
cErrorsExpected: 0, | |
cAsserts: 0, | |
fEnable: true, | |
cAsync: 0, | |
fThrows: false, | |
cThrows: 0, | |
msTimeout: 10000, | |
stTrace: "", | |
cBreakOn: 0, | |
fStopFail: false, | |
Run: function(ts) | |
{ | |
var fCaught = false; | |
if (!this.fEnable) | |
return; | |
this.state = UT.UnitTest.states.running; | |
console.log("=== Running test: " + this.stName + " ==="); | |
if (this.cAsync) | |
this.tm = new UT.Timer(this.Timeout.FnMethod(this), this.msTimeout).Active(); | |
try | |
{ | |
this.fn(this); | |
} | |
catch (e) | |
{ | |
fCaught = true; | |
this.fStopFail = false; // Avoid recursive asserts | |
this.e = e; | |
this.Async(0) | |
this.AssertException(this.e, this.stThrows, this.fThrows); | |
} | |
if (this.fThrows && !fCaught) | |
this.Assert(false, "Missing expected Exception: " + this.stThrows); | |
if (!this.cAsync) | |
this.state = UT.UnitTest.states.completed; | |
}, | |
IsComplete: function() | |
{ | |
return !this.fEnable || this.state == UT.UnitTest.states.completed; | |
}, | |
AssertThrown: function() | |
{ | |
this.AssertGT(this.cThrows, 0, "Expected exceptions not thrown."); | |
this.cThrows = 0; | |
}, | |
Enable: function(f) | |
{ | |
this.fEnable = f; | |
return this; | |
}, | |
StopFail: function(f) | |
{ | |
this.fStopFail = f; | |
return this; | |
}, | |
// Change expected number async events running - test is finishd at 0. | |
// Async(false) -> -1 | |
// Async(true) -> +1 | |
// Async(0) -> set cAsync to zero | |
// Async(c) -> +c | |
// TODO: Could use Expected number of calls to Assert to terminate an async test | |
// instead of relying on the cAsync count going to zero. | |
Async: function(dc, msTimeout) | |
{ | |
if (dc == undefined || dc === true) | |
dc = 1; | |
if (dc === false) | |
dc = -1; | |
if (msTimeout) | |
{ | |
// Don't call assert unless we have a failure - would mess up user counts for numbers | |
// of Asserts expected. | |
if (this.cAsync != 0 || this.state == UT.UnitTest.states.running) | |
this.Assert(false, "Test error: Async timeout only allowed at test initialization."); | |
this.msTimeout = msTimeout; | |
} | |
if (dc == 0) | |
this.cAsync = 0; | |
else | |
this.cAsync += dc; | |
if (this.cAsync < 0) | |
{ | |
this.Assert(false, "Test error: unbalanced calls to Async") | |
this.cAsync = 0; | |
} | |
// When cAsync goes to zero, the aynchronous test is complete | |
if (this.cAsync == 0 && this.state == UT.UnitTest.states.running) | |
this.state = UT.UnitTest.states.completed; | |
this.CheckValid(); | |
return this; | |
}, | |
Timeout: function() | |
{ | |
if (this.cAsync > 0) | |
{ | |
this.fStopFail = false; | |
this.Async(0); | |
this.Assert(false, "Async test timed out."); | |
} | |
}, | |
Throws: function(stThrows) | |
{ | |
this.fThrows = true; | |
this.stThrows = stThrows; | |
this.CheckValid(); | |
return this; | |
}, | |
Expect: function(cErrors, cTests) | |
{ | |
this.cErrorsExpected = cErrors; | |
this.cTestsExpected = cTests; | |
return this; | |
}, | |
CheckValid: function() | |
{ | |
if (this.cAsync > 0 && this.fThrows) | |
this.Assert(false, "Test error: can't test async thrown exceptions."); | |
}, | |
Reference: function(url) | |
{ | |
this.urlRef = url; | |
return this; | |
}, | |
// All asserts bottleneck to this function | |
// Eror line pattern "N. [Trace] Note (Note2)" | |
Assert: function(f, stNote, stNote2) | |
{ | |
// TODO: is there a way to get line numbers out of the callers? | |
// A backtrace (outside of unit.js) would be the best way to designate | |
// the location of failing asserts. | |
if (this.stTrace) | |
stNote = (this.cAsserts+1) + ". [" + this.stTrace + "] " + stNote; | |
else | |
stNote = (this.cAsserts+1) + ". " + stNote; | |
if (stNote2) | |
stNote += " (" + stNote2 + ")"; | |
// Allow the user to set a breakpoint when we hit a particular failing test | |
if (!f && (this.cBreakOn == -1 || this.cBreakOn == this.cAsserts+1)) | |
this.Breakpoint(stNote); | |
var res = new UT.TestResult(f, this, stNote); | |
this.rgres.push(res); | |
if (!res.f) | |
this.cErrors++; | |
this.cAsserts++; | |
// We don't throw an exception on StopFail if we already have thrown one! | |
if (this.fStopFail && this.cErrors > this.cErrorsExpected && !this.e) | |
{ | |
this.fStopFail = false; | |
this.Async(0); | |
throw new Error("StopFail - test terminates on first (unexpected) failure."); | |
} | |
}, | |
Trace: function(stTrace) | |
{ | |
this.stTrace = stTrace; | |
}, | |
BreakOn: function(cBreakOn) | |
{ | |
this.cBreakOn = cBreakOn; | |
}, | |
// Set Firebug breakpoint in this function | |
Breakpoint: function(stNote) | |
{ | |
console.log("unit.js Breakpoint: [" + this.stName + "] " + stNote); | |
// Set Firebug breakpoint on this line: | |
var x = 1; | |
}, | |
AssertEval: function(stEval) | |
{ | |
this.Assert(eval(stEval), stEval); | |
}, | |
// v1 is the quantity to be tested against the "known" quantity, v2. | |
AssertEq: function(v1, v2, stNote) | |
{ | |
if (typeof v1 != typeof v2) | |
{ | |
this.Assert(false, "Comparing values of different type: " + typeof v1 + ", " + typeof v2, stNote); | |
return; | |
} | |
switch (typeof v1) | |
{ | |
case "string": | |
pos = ""; | |
if (v1 != v2) | |
for (var i = 0; i < v2.length; i++) | |
{ | |
if (v1[i] != v2[i]) | |
{ | |
pos += "@" + i + " x" + v1.charCodeAt(i).toString(16) + " != x" + v2.charCodeAt(i).toString(16) + ", " | |
break; | |
} | |
} | |
this.Assert(v1 == v2, "\"" + v1 + "\" == \"" + v2 + "\" (" + pos + "len: " + v1.length + ", " + v2.length + ")", stNote); | |
break; | |
case "object": | |
this.AssertContains(v1, v2); | |
var cProp1 = this.PropCount(v1); | |
var cProp2 = this.PropCount(v2); | |
this.Assert(cProp1 == cProp2, "Objects have different property counts (" + cProp1 + " != " + cProp2 + ")"); | |
// Make sure Dates match | |
if (v1.constructor == Date) | |
{ | |
this.AssertEq(v2.constructor, Date); | |
if (v2.constructor == Date) | |
this.AssertEq(v1.toString(), v2.toString()); | |
} | |
break; | |
default: | |
this.Assert(v1 == v2, v1 + " == " + v2 + " (type: " + typeof v1 + ")", stNote); | |
break; | |
} | |
}, | |
PropCount: function(obj) | |
{ | |
var cProp = 0; | |
for (var prop in obj) | |
{ | |
if (obj.hasOwnProperty(prop)) | |
cProp++; | |
} | |
return cProp; | |
}, | |
AssertType: function(v1, type, stNote) | |
{ | |
if (type == "array") | |
type = Array; | |
// Check if object is an instance of type | |
if (typeof type == "function") | |
{ | |
this.AssertEq(typeof v1, "object", stNote); | |
this.Assert(v1 instanceof type, stNote, "not a " + type); | |
return; | |
} | |
this.AssertEq(typeof v1, type, stNote); | |
}, | |
AssertTypes: function(obj, mTypes) | |
{ | |
for (var prop in mTypes) | |
{ | |
if (!mTypes.hasOwnProperty(prop)) | |
continue; | |
this.AssertType(obj[prop], mTypes[prop], prop + " should be type " + mTypes[prop]) | |
} | |
}, | |
// Assert that objAll contains all the (top level) properties of objSome | |
AssertContains: function(objAll, objSome) | |
{ | |
if (typeof objAll != "object" || typeof objSome != "object") | |
{ | |
this.Assert(false, "AssertContains expects objects: " + typeof objAll + " ~ " + typeof objSome); | |
return; | |
} | |
// For arrays, just confirm that the elements of the 2nd array are included as members of the first | |
if (objSome instanceof Array) | |
{ | |
if (!(objAll instanceof Array)) | |
{ | |
this.Assert(false, "AssertContains unmatched Array: " + objAll.constructor); | |
return; | |
} | |
var map1 = {} | |
for (var prop in objAll) | |
{ | |
if (typeof(objAll[prop]) != 'object') | |
map1[objAll[prop]] = true; | |
} | |
for (var prop in objSome) | |
{ | |
if (typeof(objSome[prop]) != 'object') | |
{ | |
this.Assert(map1[objSome[prop]], "Missing array value: " + objSome[prop] + | |
" (type: " + typeof(objSome[prop]) + ")"); | |
} | |
else | |
{ | |
this.Assert(false, "AssertContains does shallow compare only"); | |
} | |
} | |
return; | |
} | |
for (var prop in objSome) | |
{ | |
if (!objSome.hasOwnProperty(prop)) | |
continue; | |
this.AssertEq(objAll[prop], objSome[prop], "prop: " + prop); | |
} | |
}, | |
AssertIdent: function(v1, v2) | |
{ | |
this.Assert(v1 === v2, v1 + " === " + v2); | |
}, | |
AssertNEq: function(v1, v2) | |
{ | |
this.Assert(v1 != v2, v1 + " != " + v2); | |
}, | |
AssertGT: function(v1, v2) | |
{ | |
this.Assert(v1 > v2, v1 + " > " + v2); | |
}, | |
AssertLT: function(v1, v2) | |
{ | |
this.Assert(v1 < v2, v1 + " < " + v2); | |
}, | |
AssertFn: function(fn) | |
{ | |
var stFn = fn.toString(); | |
stFn = stFn.substring(stFn.indexOf("{")+1, stFn.lastIndexOf("}")-1); | |
this.Assert(fn(), stFn); | |
}, | |
// Useage: ut.AssertThrows(<type>, function(ut) {...}); | |
AssertThrows: function(stExpected, fn) | |
{ | |
try | |
{ | |
fn(this); | |
} | |
catch (e) | |
{ | |
this.AssertException(e, stExpected); | |
return; | |
} | |
this.Assert(false, "Missing expected Exception: " + stExpected); | |
}, | |
// Assert expected and caught exceptions | |
// If stExpected != undefined, e.name or e.message must contain it | |
AssertException: function(e, stExpected, fExpected) | |
{ | |
if (fExpected == undefined) fExpected = true; | |
if (fExpected) | |
{ | |
if (e.name) e.name = e.name.toLowerCase(); | |
if (e.message) e.message = e.message.toLowerCase(); | |
if (stExpected) stExpected = stExpected.toLowerCase(); | |
this.Assert(!stExpected || e.name.indexOf(stExpected) != -1 || | |
e.message.indexOf(stExpected) != -1, | |
"Exception: " + e.name + " (" + e.message + ")" + | |
(stExpected ? " Expecting: " + stExpected : "")); | |
this.cThrows++; | |
} | |
else | |
{ | |
var stMsg = "Exception: " + e.name + " (" + e.message; | |
if (e.number != undefined) | |
stMsg += ", Error No:" + (e.number & 0xFFFF); | |
stMsg += ")"; | |
if (e.lineNumber != undefined) | |
stMsg += " @ line " + e.lineNumber; | |
this.Assert(false, stMsg); | |
} | |
}, | |
// AsyncSequence - Run a sequence of asynchronous function calls | |
// Each fn(ut) must call ut.NextFn() to advance | |
// Last call to NextFn calls Async(false) | |
AsyncSequence: function(rgfn) | |
{ | |
this.rgfn = rgfn; | |
this.ifn = 0; | |
this.NextFn(); | |
}, | |
NextFn: function() | |
{ | |
if (this.ifn >= this.rgfn.length) | |
{ | |
this.Async(false); | |
return; | |
} | |
this.Trace("AsyncSeq: " + (this.ifn+1)); | |
try | |
{ | |
this.rgfn[this.ifn++](this); | |
} | |
catch (e) | |
{ | |
this.AssertException(e, "", false) | |
} | |
}, | |
// Wrap asynchronous function calls so we can catch are report exception errors | |
FnWrap: function(fn) | |
{ | |
var ut = this; | |
return ( | |
function () { | |
try | |
{ | |
fn.apply(undefined, arguments); | |
} | |
catch (e) | |
{ | |
ut.AssertException(e, "", false); | |
// Advance to next function in sequence | |
ut.NextFn(); | |
} | |
}); | |
} | |
}; // UT.UnitTest | |
// TestResult - a single result from the test | |
UT.TestResult = function (f, ut, stNote) | |
{ | |
this.f = f; | |
this.ut = ut; | |
this.stNote = stNote; | |
}; | |
// ------------------------------------------------------------------------ | |
// Test Suite - Holds, executes, and reports on a collection of unit tests. | |
// ------------------------------------------------------------------------ | |
UT.TestSuite = function (stName) | |
{ | |
this.stName = stName; | |
this.rgut = []; | |
this.stOut = ""; | |
}; | |
UT.TestSuite.prototype = { | |
constructor: UT.TestSuite, | |
cFailures: 0, | |
iReport: -1, | |
fStopFail: false, | |
fTerminateAll: false, | |
iutNext: 0, // Will auto-disable any unit test less than iutNext (see SkipTo) | |
AddTest: function(stName, fn) | |
{ | |
var ut = new UT.UnitTest(stName, fn); | |
this.rgut.push(ut); | |
// Global setting - stop all unit tests on first failure. | |
if (this.fStopFail) | |
ut.StopFail(true) | |
return ut; | |
}, | |
StopFail: function(f) | |
{ | |
this.fStopFail = f; | |
return this; | |
}, | |
SkipTo: function(iut) | |
{ | |
// Tests displayed as one-based | |
this.iutNext = iut-1; | |
return this; | |
}, | |
// We support asynchronous tests - so we use a timer to kick off tests when the current one | |
// is complete. | |
Run: function() | |
{ | |
// BUG: should this be Active(false) - since we do first iteration immediately? | |
this.tmRun = new UT.Timer(this.RunNext.FnMethod(this), 100).Repeat().Active(true); | |
this.iCur = 0; | |
// Don't wait for timer - start right away. | |
this.RunNext(); | |
}, | |
RunNext: function() | |
{ | |
if (this.iCur == this.rgut.length) | |
return; | |
this.tmRun.Active(false); | |
loop: | |
while (this.iCur < this.rgut.length) | |
{ | |
var ut = this.rgut[this.iCur]; | |
var state = ut.state; | |
if (!ut.fEnable || this.fTerminateAll || this.iCur < this.iutNext) | |
state = UT.UnitTest.states.completed; | |
switch(state) | |
{ | |
case UT.UnitTest.states.created: | |
ut.Run(); | |
break; | |
case UT.UnitTest.states.running: | |
break loop; | |
case UT.UnitTest.states.completed: | |
this.iCur++; | |
this.ReportWhenReady(); | |
// Skip all remaining tests on failure if StopFail | |
if (this.fStopFail && ut.cErrors != ut.cErrorsExpected) | |
this.fTerminateAll = true; | |
break; | |
} | |
} | |
this.tmRun.Active(true); | |
}, | |
AllComplete: function() | |
{ | |
return (this.iCur == this.rgut.length); | |
}, | |
DWOutputDiv: function() | |
{ | |
UT.DW("<DIV style=\"font-family: Courier;border:1px solid red;\" id=\"divUnit\">Unit Test Output</DIV>"); | |
}, | |
Out: function(st) | |
{ | |
this.stOut += st; | |
return this; | |
}, | |
OutRef: function(st, url) | |
{ | |
if (!url) | |
{ | |
this.Out(st); | |
return; | |
} | |
if (this.divOut) | |
this.Out("<A target=\"_blank\" href=\"" + url + "\">" + st + "</A>"); | |
else | |
{ | |
if (st != url) | |
this.Out(st + " (" + url + ")"); | |
else | |
this.Out(st); | |
} | |
}, | |
NL: function() | |
{ | |
if (this.divOut) | |
{ | |
this.divOut.appendChild(document.createElement("BR")); | |
var txt = document.createElement("span"); | |
txt.innerHTML = this.stOut; | |
this.divOut.appendChild(txt); | |
} | |
else if (typeof console != "undefined") | |
console.log(this.stOut); | |
else | |
alert(this.stOut); | |
this.stOut = ""; | |
return this; | |
}, | |
Report: function() | |
{ | |
this.divOut = this.divOut || document.getElementById("divUnit"); | |
this.cFailures = 0; | |
this.iReport = 0; | |
this.ReportWhenReady(); | |
}, | |
ReportWhenReady: function() | |
{ | |
// Reporting not enabled | |
if (this.iReport == -1) | |
return; | |
while (this.iReport < this.iCur) | |
this.ReportOne(this.iReport++); | |
if (!this.AllComplete()) | |
return; | |
this.ReportSummary(); | |
this.ReportOut(); | |
}, | |
ReportOne: function(i) | |
{ | |
var ut = this.rgut[i]; | |
this.Out((i+1) + ". "); | |
switch (ut.state) | |
{ | |
case UT.UnitTest.states.created: | |
this.Out("N/A"); | |
break; | |
case UT.UnitTest.states.running: | |
if (ut.cAsync > 0) | |
this.Out("RUNNING"); | |
else | |
{ | |
this.Out("INCOMPLETE"); | |
} | |
this.cFailures++; | |
break; | |
case UT.UnitTest.states.completed: | |
if (ut.cErrors == ut.cErrorsExpected && | |
(ut.cTestsExpected == undefined || ut.cTestsExpected == ut.cAsserts)) | |
this.Out("PASS"); | |
else | |
{ | |
this.Out("FAIL"); | |
this.cFailures++; | |
} | |
break; | |
} | |
this.Out(" ["); | |
this.OutRef(ut.stName, ut.urlRef); | |
this.Out("] "); | |
if (ut.state != UT.UnitTest.states.created) | |
{ | |
this.Out(ut.cErrors + " errors " + "out of " + ut.cAsserts + " tests"); | |
if (ut.cTestsExpected && ut.cTestsExpected != ut.cAsserts) | |
this.Out(" (" + ut.cTestsExpected + " expected)"); | |
} | |
this.NL(); | |
for (var j = 0; j < ut.rgres.length; j++) | |
{ | |
var res = ut.rgres[j]; | |
if (!res.f) | |
this.Out("Failed: " + res.stNote).NL(); | |
} | |
}, | |
ReportSummary: function() | |
{ | |
if (this.cFailures == 0) | |
this.Out("Summary: All (" + this.rgut.length + ") tests pass.").NL(); | |
else | |
this.Out("Summary: " + this.cFailures + " failures out of " + this.rgut.length + " tests.").NL(); | |
}, | |
// Report results to master unit test, if any. | |
ReportOut: function() | |
{ | |
if (!this.AllComplete()) | |
return; | |
if (window.opener && window.opener.MasterTest) | |
{ | |
var iUnit = parseInt(window.name.replace(/^Unit_/, "")); | |
window.opener.MasterTest(iUnit, this.cFailures, this.rgut.length); | |
} | |
}, | |
AddSubTest: function(stPath) | |
{ | |
var ut = this.AddTest(stPath, this.RunSubTest.FnMethod(this)).Async(true).Reference(stPath); | |
ut.stPath = stPath; | |
ut.iUnit = this.rgut.length-1; | |
return ut; | |
}, | |
RunSubTest: function(ut) | |
{ | |
var stName = "Unit_" + ut.iUnit; | |
// Ensure unique name even if multi-level of master-child tests. | |
if (window.name) | |
stName += " from " + window.name; | |
ut.win = window.open(ut.stPath, "Unit_" + ut.iUnit); | |
if (window.MasterTest == undefined) | |
window.MasterTest = this.MasterTest.FnMethod(this); | |
}, | |
MasterTest: function(iUnit, cErrors, cTests) | |
{ | |
var ut = this.rgut[iUnit]; | |
ut.cErrors = cErrors; | |
ut.cAsserts = cTests; | |
ut.Async(false); | |
if (ut.cErrors == ut.cErrorsExpected) | |
ut.win.close(); | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment