diff --git a/nselib/unittest.lua b/nselib/unittest.lua
new file mode 100644
index 000000000..6898657f8
--- /dev/null
+++ b/nselib/unittest.lua
@@ -0,0 +1,264 @@
+---
+-- Unit testing support for NSE libraries.
+--
+-- This library will import all NSE libraries looking for a global variable
+-- test_suite. This must be a callable that returns true or false
+-- and the number of tests that failed. For convenience, the
+-- unittest.TestSuite class has this property, and tests can be
+-- added with add_test. Example:
+--
+-- local data = {"foo", "bar", "baz"}
+-- test_suite = unittest.TestSuite:new()
+-- test_suite:add_test(equal(data[2], "bar"), "data[2] should equal 'bar'")
+--
+-- The library is driven by the unittest NSE script.
+--
+-- @copyright Same as Nmap--See http://nmap.org/book/man-legal.html
+
+local stdnse = require "stdnse"
+local nsedebug = require "nsedebug"
+local string = require "string"
+local debug = require "debug"
+_ENV = stdnse.module("unittest", stdnse.seeall)
+
+local libs = {
+"afp", "ajp", "amqp", "asn1", "base32", "base64", "bin", "bitcoin", "bit",
+"bittorrent", "bjnp", "brute", "cassandra", "citrixxml", "comm", "creds",
+"cvs", "datafiles", "dhcp6", "dhcp", "dnsbl", "dns", "dnssd", "drda", "eap",
+"eigrp", "formulas", "ftp", "giop", "gps", "http", "httpspider", "iax2", "ike",
+"imap", "informix", "ipOps", "ipp", "iscsi", "isns", "jdwp", "json", "ldap",
+"lfs", "listop", "match", "membase", "mobileme", "mongodb", "msrpc",
+"msrpcperformance", "msrpctypes", "mssql", "mysql", "natpmp", "ncp", "ndmp",
+"netbios", "nmap", "nrpc", "nsedebug", "omp2", "openssl", "ospf", "packet",
+"pcre", "pgsql", "pop3", "pppoe", "proxy", "rdp", "redis", "rmi", "rpcap",
+"rpc", "rsync", "rtsp", "sasl", "shortport", "sip", "smbauth", "smb", "smtp",
+"snmp", "socks", "srvloc", "ssh1", "ssh2", "sslcert", "stdnse", "strbuf",
+"stun", "tab", "target", "tftp", "tns", "unittest", "unpwdb", "upnp", "url",
+"versant", "vnc", "vulns", "vuzedht", "wsdd", "xdmcp", "xmpp",
+}
+
+---
+-- Run tests provided by NSE libraries
+-- @param to_test A list (table) of libraries to test. If none is provided, all
+-- libraries are tested.
+run_tests = function(to_test)
+ if to_test == nil then
+ to_test = libs
+ end
+ local fails = stdnse.output_table()
+ for _,lib in ipairs(to_test) do
+ stdnse.print_debug(1, "Testing %s", lib)
+ local thelib = require(lib)
+ local failed = 0
+ if rawget(thelib,"test_suite") ~= nil then
+ failed = thelib.test_suite()
+ end
+ if failed ~= 0 then
+ fails[lib] = failed
+ end
+ end
+ return fails
+end
+
+--- The TestSuite class
+--
+-- Holds and runs tests.
+TestSuite = {
+
+ --- Creates a new TestSuite object
+ --
+ -- @return TestSuite object
+ new = function(self)
+ local o = {}
+ setmetatable(o, self)
+ self.__index = self
+ o.tests = {}
+ return o
+ end,
+
+ --- Set up test environment. Override this.
+ setup = function(self)
+ return true
+ end,
+ --- Tear down test environment. Override this.
+ teardown = function(self)
+ return true
+ end,
+ --- Add a test.
+ -- @param test Function that will be called with the TestSuite object as its only parameter.
+ -- @param description A description of the test being run
+ add_test = function(self, test, description)
+ self.tests[#self.tests+1] = {test, description}
+ end,
+
+ --- Run tests.
+ -- Runs all tests in the TestSuite, and returns the number of failures.
+ -- @return failures The number of tests that failed
+ -- @return tests The number of tests run
+ __call = function(self)
+ local failures = 0
+ local passes = 0
+ self:setup()
+ for _,test in ipairs(self.tests) do
+ stdnse.print_debug(2, "| Test: %s...", test[2])
+ local status, note = test[1](self)
+ local result
+ local lvl = 2
+ if status then
+ result = "Pass"
+ passes = passes + 1
+ else
+ result = "Fail"
+ lvl = 1
+ if nmap.debugging() < 2 then
+ stdnse.print_debug(1, "| Test: %s...", test[2])
+ end
+ failures = failures + 1
+ end
+ if note then
+ stdnse.print_debug(lvl, "| \\_result: %s (%s)", result, note)
+ else
+ stdnse.print_debug(lvl, "| \\_result: %s", result)
+ end
+ end
+ stdnse.print_debug(1, "|_%d of %d tests passed", passes, #self.tests)
+ self:teardown()
+ return failures, #self.tests
+ end,
+}
+
+--- Test creation helper function.
+-- Turns a simple function into a test factory.
+-- @param test A function that returns true or false depending on test
+-- @param nargs The number of arguments to the test function
+-- @param fmt A format string describing the failure condition using the
+-- arguments to the test function
+-- @return function that generates tests suitable for use in add_test
+make_test = function(test, blah, fmt)
+ return function(...)
+ local args={"dummy", ...}
+ local nargs = select("#", ...) + 1
+ return function(suite)
+ if not test(table.unpack(args,2,nargs)) then
+ return false, string.format(fmt, table.unpack(args,2,nargs))
+ end
+ return true
+ end
+ end
+end
+
+--- Test for nil
+-- @param value The value to test
+-- @return bool True if the value is nil, false otherwise.
+is_nil = make_test( function(value)
+ return value == nil
+end, 1,
+"Expected not nil, got %s"
+)
+
+--- Test for not nil
+-- @param value The value to test
+-- @return bool True if the value is not nil, false otherwise.
+not_nil = make_test( function(value)
+ return value ~= nil
+end, 1,
+"Expected not nil, got %s"
+)
+
+--- Test for equality
+-- @param a The first value to test
+-- @param b The second value to test
+-- @return bool True if a == b, false otherwise.
+equal = make_test( function(a, b)
+ return a == b
+end, 2,
+"%s not equal to %s"
+)
+
+--- Test for inequality
+-- @param a The first value to test
+-- @param b The second value to test
+-- @return bool True if a != b, false otherwise.
+not_equal = make_test( function(a, b)
+ return a ~= b
+end, 2,
+"%s unexpectedly equal to %s"
+)
+
+--- Test for truth
+-- @param value The value to test
+-- @return bool True if value is a boolean and true
+is_true = make_test( function(value)
+ return value == true
+end, 1,
+"Expected true, got %s"
+)
+
+--- Test for falsehood
+-- @param value The value to test
+-- @return bool True if value is a boolean and false
+is_false = make_test( function(value)
+ return value == false
+end, 1,
+"Expected false, got %s"
+)
+
+--- Test less than
+-- @param a The first value to test
+-- @param b The second value to test
+-- @return bool True if a < b, false otherwise.
+lt = make_test( function(a, b)
+ return a < b
+end, 2,
+"%s not less than %s"
+)
+
+--- Test less than or equal to
+-- @param a The first value to test
+-- @param b The second value to test
+-- @return bool True if a <= b, false otherwise.
+lte = make_test( function(a, b)
+ return a <= b
+end, 2,
+"%s not less than %s"
+)
+
+--- Test length
+-- @param t The table to test
+-- @param l The length to test
+-- @return bool True if the length of t is l
+length_is = make_test( function(t, l)
+ return #t == l
+end, 2,
+"Length of %s is not %s"
+)
+
+--- Expected failure test
+-- @param test The test to run
+-- @return function A test for expected failure of the test
+expected_failure = function(test)
+ return function(suite)
+ if test(suite) then
+ return true, "Test unexpectedly passed"
+ else
+ return true, "Test failed as expected"
+ end
+ return true
+ end
+end
+
+-- Self test
+test_suite = TestSuite:new()
+
+test_suite:add_test(is_nil(test_suite["asdfdoesnotexist"]), "Nonexistent key does not exist")
+test_suite:add_test(equal(1+1336, 7 * 191), "Arithmetically equal expressions are equal")
+test_suite:add_test(not_equal( true, "true" ), "Boolean true not equal to string \"true\"")
+test_suite:add_test(is_true("test" == "test"), "Boolean expression evaluates to true")
+test_suite:add_test(is_false(1.9999 == 2.0), "Boolean expression evaluates to false")
+test_suite:add_test(lt(1, 999), "1 < 999")
+test_suite:add_test(lte(8, 8), "8 <= 8")
+test_suite:add_test(expected_failure(not_nil(nil)), "Test expected to fail fails")
+test_suite:add_test(expected_failure(is_nil(nil)), "Test expected to fail succeeds")
+test_suite:add_test(length_is(test_suite.tests, 10), "Number of tests is 10")
+
+return _ENV;
diff --git a/scripts/unittest.nse b/scripts/unittest.nse
new file mode 100644
index 000000000..029ce995f
--- /dev/null
+++ b/scripts/unittest.nse
@@ -0,0 +1,42 @@
+local stdnse = require "stdnse"
+local unittest = require "unittest"
+
+description = [[
+Runs unit tests on all NSE libraries.
+]]
+
+---
+-- @args unittest.run Run tests
+-- @args unittest.tests Run tests from only these libraries (defaults to all)
+--
+-- @usage
+-- nmap --script unittest --script-args unittest.run
+--
+-- @output
+-- Pre-scan script results:
+-- | unittest:
+-- |_ All tests passed
+
+author = "Daniel Miller"
+
+license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
+
+categories = {}
+
+
+prerule = function() return stdnse.get_script_args("unittest.run") end
+
+action = function()
+ local libs = stdnse.get_script_args("unittest.tests")
+ local result
+ if libs then
+ result = unittest.run_tests(libs)
+ else
+ result = unittest.run_tests()
+ end
+ if #result == 0 then
+ return "All tests passed"
+ else
+ return result
+ end
+end