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