Quantcast

Overhaul of WoWAPI unit testing module.

Johnny C. Lam [07-13-14 - 11:30]
Overhaul of WoWAPI unit testing module.

The WoWAPI module tries to provide enough of the WoW environment to be
able to load addon files and do unit testing or integration testing of
addon components.

WoWAPI contains fake implementations of some libraries that are used
by Ovale.

Example usage:

    local state = {
        class = "DRUID",
        level = 90,
    }
    dofile("WoWAPI.lua")
    WoWAPI:Initialize("Ovale", state)
    WoWAPI:ExportSymbols()
    WoWAPI:LoadTOC("Ovale.toc")

This example loads all of the Ovale addon's files in the order listed in
its TOC file.

git-svn-id: svn://svn.curseforge.net/wow/ovale/mainline/trunk@1532 d5049fe3-3747-40f7-a4b5-f36d6801af5f
Filename
WoWAPI.lua
diff --git a/WoWAPI.lua b/WoWAPI.lua
index 934cc93..64498ed 100644
--- a/WoWAPI.lua
+++ b/WoWAPI.lua
@@ -7,47 +7,378 @@
     file accompanying this program.
 --]]--------------------------------------------------------------------

---[[
-	This file implements parts of the WoW API for development and unit-testing.
-
-	This file is not meant to be loaded into the addon.  It should be used only
-	outside of the WoW environment, such as when loaded by a standalone Lua 5.1
-	interpreter.
-
-	It provides global symbols.
---]]
-
--- RAID_CLASS_COLORS is useful mostly as a table to loop through the keys, which are
--- the class tokens for all playable classes.
-RAID_CLASS_COLORS = RAID_CLASS_COLORS or {
-	DEATHKNIGHT = true,
-	DRUID = true,
-	HUNTER = true,
-	MAGE = true,
-	MONK = true,
-	PALADIN = true,
-	PRIEST = true,
-	ROGUE = true,
-	SHAMAN = true,
-	WARLOCK = true,
-	WARRIOR = true,
+--[[--------------------------------------------------------------------
+	This file implements parts of the WoW API for development and
+	unit-testing.
+
+	This file is not meant to be loaded into the addon.  It should be
+	used only outside of the WoW environment, such as when loaded by a
+	standalone Lua 5.1 interpreter.
+--]]--------------------------------------------------------------------
+
+-- Globally-accessible module table.
+WoWAPI = {}
+
+--<private-static-properties>
+local self_state = {}
+local self_privateSymbol = {
+	["ExportSymbols"] = true,
+	["Initialize"] = true,
 }

--- wipe() is a non-standard Lua function that clears the contents of a table
--- and leaves the table pointer intact.
-wipe = wipe or function(t)
-	for k in pairs(t) do
-		t[k] = nil
+-- Metatable to provide __index method to tables so that if the requested key
+-- is missing from the table, then a new key is inserted with the value being
+-- the same as the missing key.
+local KeysAreMissingValuesMetatable = {
+	__index = function(t, k)
+		rawset(t, k, k)
+		return k
+	end,
+}
+--</private-static-properties>
+
+--<private-static-methods>
+function DeepCopy(orig)
+    local orig_type = type(orig)
+    local copy
+    if orig_type == 'table' then
+        copy = {}
+        for orig_key, orig_value in next, orig, nil do
+            copy[DeepCopy(orig_key)] = DeepCopy(orig_value)
+        end
+        setmetatable(copy, DeepCopy(getmetatable(orig)))
+    else -- number, string, boolean, etc
+        copy = orig
+    end
+    return copy
+end
+--</private-static-methods>
+
+--<private-static-properties>
+--[[--------------------------------
+	Fake library implementations.
+--]]--------------------------------
+
+-- AceAddon-3.0
+local AceAddon = nil
+do
+	local lib = {}
+	AceAddon = lib
+
+	lib.initializationQueue = {}
+
+	local prototype = {}
+
+	prototype.GetModule = function(addon, name)
+		addon.modules = addon.modules or {}
+		return addon.modules[name]
+	end
+
+	prototype.GetName = function(addon)
+		return addon.moduleName or addon.name
+	end
+
+	prototype.IterateModules = function(addon)
+		return pairs(addon.modules)
+	end
+
+	prototype.NewModule = function(addon, name, ...)
+		local args = { ... }
+		local mod = lib:NewAddon(string.format("%s_%s", addon.name, name))
+		mod.moduleName = name
+		-- Embed methods from named libraries.
+		for _, libName in ipairs(args) do
+			local lib = LibStub(libName)
+			if lib then
+				for k, v in pairs(lib) do
+					mod[k] = v
+				end
+			end
+		end
+		addon.modules = addon.modules or {}
+		addon.modules[name] = mod
+		return mod
+	end
+
+	lib.GetAddon = function(lib, name)
+		lib.addons = lib.addons or {}
+		return lib.addons[name]
+	end
+
+	lib.ADDON_LOADED = function(lib, event)
+		for _, addon in ipairs(lib.initializationQueue) do
+			if addon.OnInitialize then
+				addon:OnInitialize()
+			end
+		end
+	end
+
+	lib.IterateAddons = function(lib)
+		return pairs(lib.addons)
+	end
+
+	lib.NewAddon = function(lib, name, ...)
+		local addon
+		local args
+		if type(name) == "nil" then
+			addon = {}
+			name = ...
+			args = { select(2, ...) }
+		elseif type(name) == "table" then
+			addon = name
+			name = ...
+			args = { select(2, ...) }
+		else
+			addon = {}
+			args = { ... }
+		end
+		-- Copy addon prototype.
+		for k, v in pairs(prototype) do
+			addon[k] = v
+		end
+		-- Embed methods from named libraries.
+		for _, libName in ipairs(args) do
+			local lib = LibStub(libName)
+			if lib then
+				for k, v in pairs(lib) do
+					addon[k] = v
+				end
+			end
+		end
+		addon.name = name
+		lib.addons = lib.addons or {}
+		lib.addons[name] = addon
+		lib.initializationQueue[#lib.initializationQueue + 1] = addon
+		return addon
 	end
 end
-table.wipe = table.wipe or wipe

--- strsplit() is a non-standard Lua function that splits a string and returns
--- multiple return values for each substring delimited by the named delimiter
--- character.
---
--- This implementaiton is taken verbatim from http://lua-users.org/wiki/SplitJoin
-strsplit = strsplit or function(delim, str, maxNb)
+-- AceConfig-3.0
+local AceConfig = nil
+do
+	local lib = {}
+	AceConfig = lib
+	lib.RegisterOptionsTable = function(lib, ...) end
+end
+
+-- AceConfigDialog-3.0
+local AceConfigDialog = nil
+do
+	local lib = {}
+	AceConfigDialog = lib
+	lib.AddToBlizOptions = function(lib, ...) end
+end
+
+-- AceConsole-3.0
+local AceConsole = nil
+do
+	local lib = {}
+	AceConsole = lib
+
+	lib.Print = function(lib, ...)
+		print(...)
+	end
+
+	lib.Printf = function(lib, ...)
+		print(string.format(...))
+	end
+end
+
+-- AceDB-3.0
+local AceDB = nil
+do
+	local lib = {}
+	AceDB = lib
+
+	lib.New = function(lib, name, template)
+		template = template or {}
+		local db = DeepCopy(template)
+		db.RegisterCallback = function(...) end
+		return db
+	end
+end
+
+-- AceDBOptions-3.0
+local AceDBOptions = nil
+do
+	local lib = {}
+	AceDBOptions = lib
+	lib.GetOptionsTable = function(db) end
+end
+
+-- AceEvent-3.0
+local AceEvent = nil
+do
+	local lib = {}
+	AceEvent = lib
+	lib.SendMessage = function(lib, message, ...) end
+end
+
+-- AceGUI-3.0
+local AceGUI = nil
+do
+	local lib = {}
+	AceGUI = lib
+	lib.RegisterWidgetType = function(...) end
+end
+
+-- AceLocale-3.0
+local AceLocale = nil
+do
+	local lib = {}
+	AceLocale = lib
+
+	lib.GetLocale = function(lib, name)
+		local L
+		if lib.locale and lib.locale[name] then
+			L = lib.locale[name]
+		else
+			L = lib:NewLocale(name, nil)
+		end
+		return L
+	end
+
+	lib.NewLocale = function(lib, name, locale)
+		local L = setmetatable({}, KeysAreMissingValuesMetatable)
+		lib.locale = lib.locale or {}
+		lib.locale[name] = L
+		return L
+	end
+end
+
+-- LibBabble-CreatureType-3.0
+local LibBabbleCreatureType = nil
+do
+	local lib = {}
+	LibBabbleCreatureType = lib
+
+	lib.GetLookupTable = function(lib)
+		local tbl = lib.lookupTable or setmetatable({}, KeysAreMissingValuesMetatable)
+		lib.lookupTable = tbl
+		return tbl
+	end
+end
+
+-- LibStub
+local LibStub = nil
+do
+	local lib = {}
+	LibStub = lib
+
+	lib.library = {
+		["AceAddon-3.0"] = AceAddon,
+		["AceConfig-3.0"] = AceConfig,
+		["AceConfigDialog-3.0"] = AceConfigDialog,
+		["AceConsole-3.0"] = AceConsole,
+		["AceDB-3.0"] = AceDB,
+		["AceDBOptions-3.0"] = AceDBOptions,
+		["AceEvent-3.0"] = AceEvent,
+		["AceGUI-3.0"] = AceGUI,
+		["AceLocale-3.0"] = AceLocale,
+		["LibBabble-CreatureType-3.0"] = LibBabbleCreatureType,
+	}
+
+	local mt = {
+		__call = function(lib, name, flag)
+			return lib:GetLibrary(name, flag)
+		end,
+	}
+
+	lib.GetLibrary = function(lib, name, flag)
+		return lib.library[name]
+	end
+
+	setmetatable(lib, mt)
+end
+--</private-static-properties>
+
+--<public-static-properties>
+--[[----------------------
+	FrameXML/Constants
+--]]----------------------
+
+-- Inventory slots
+WoWAPI.INVSLOT_AMMO		= 0
+WoWAPI.INVSLOT_HEAD		= 1
+WoWAPI.INVSLOT_NECK		= 2
+WoWAPI.INVSLOT_SHOULDER	= 3
+WoWAPI.INVSLOT_BODY		= 4
+WoWAPI.INVSLOT_CHEST	= 5
+WoWAPI.INVSLOT_WAIST	= 6
+WoWAPI.INVSLOT_LEGS		= 7
+WoWAPI.INVSLOT_FEET		= 8
+WoWAPI.INVSLOT_WRIST	= 9
+WoWAPI.INVSLOT_HAND		= 10
+WoWAPI.INVSLOT_FINGER1	= 11
+WoWAPI.INVSLOT_FINGER2	= 12
+WoWAPI.INVSLOT_TRINKET1	= 13
+WoWAPI.INVSLOT_TRINKET2	= 14
+WoWAPI.INVSLOT_BACK		= 15
+WoWAPI.INVSLOT_MAINHAND	= 16
+WoWAPI.INVSLOT_OFFHAND	= 17
+WoWAPI.INVSLOT_RANGED	= 18
+WoWAPI.INVSLOT_TABARD	= 19
+WoWAPI.INVSLOT_FIRST_EQUIPPED = WoWAPI.INVSLOT_HEAD
+WoWAPI.INVSLOT_LAST_EQUIPPED = WoWAPI.INVSLOT_TABARD
+
+-- Power Types
+WoWAPI.SPELL_POWER_MANA				= 0
+WoWAPI.SPELL_POWER_RAGE				= 1
+WoWAPI.SPELL_POWER_FOCUS			= 2
+WoWAPI.SPELL_POWER_ENERGY			= 3
+--WoWAPI.SPELL_POWER_CHI			= 4		-- This is obsolete now.
+WoWAPI.SPELL_POWER_RUNES			= 5
+WoWAPI.SPELL_POWER_RUNIC_POWER		= 6
+WoWAPI.SPELL_POWER_SOUL_SHARDS		= 7
+WoWAPI.SPELL_POWER_ECLIPSE			= 8
+WoWAPI.SPELL_POWER_HOLY_POWER		= 9
+WoWAPI.SPELL_POWER_ALTERNATE_POWER	= 10
+WoWAPI.SPELL_POWER_DARK_FORCE		= 11
+WoWAPI.SPELL_POWER_CHI				= 12
+WoWAPI.SPELL_POWER_SHADOW_ORBS		= 13
+WoWAPI.SPELL_POWER_BURNING_EMBERS	= 14
+WoWAPI.SPELL_POWER_DEMONIC_FURY		= 15
+
+WoWAPI.RAID_CLASS_COLORS = {
+	["HUNTER"] = { r = 0.67, g = 0.83, b = 0.45, colorStr = "ffabd473" },
+	["WARLOCK"] = { r = 0.58, g = 0.51, b = 0.79, colorStr = "ff9482c9" },
+	["PRIEST"] = { r = 1.0, g = 1.0, b = 1.0, colorStr = "ffffffff" },
+	["PALADIN"] = { r = 0.96, g = 0.55, b = 0.73, colorStr = "fff58cba" },
+	["MAGE"] = { r = 0.41, g = 0.8, b = 0.94, colorStr = "ff69ccf0" },
+	["ROGUE"] = { r = 1.0, g = 0.96, b = 0.41, colorStr = "fffff569" },
+	["DRUID"] = { r = 1.0, g = 0.49, b = 0.04, colorStr = "ffff7d0a" },
+	["SHAMAN"] = { r = 0.0, g = 0.44, b = 0.87, colorStr = "ff0070de" },
+	["WARRIOR"] = { r = 0.78, g = 0.61, b = 0.43, colorStr = "ffc79c6e" },
+	["DEATHKNIGHT"] = { r = 0.77, g = 0.12 , b = 0.23, colorStr = "ffc41f3b" },
+	["MONK"] = { r = 0.0, g = 1.00 , b = 0.59, colorStr = "ff00ff96" },
+}
+
+--[[--------------------------
+	FrameXML/GlobalStrings
+--]]--------------------------
+
+WoWAPI.ITEM_LEVEL = "Item Level %d"
+
+--[[--------------------------------------------------------------------
+	debugprofilestop() is a non-standard Lua function that returns the
+	current time in milliseconds.
+
+	This is a trivial implementation to just get the Profiler module
+	working.
+--]]--------------------------------------------------------------------
+WoWAPI.debugprofilestop = function()
+	return 0
+end
+
+--[[--------------------------------------------------------------------
+	strsplit() is a non-standard Lua function that splits a string and
+	returns multiple return values for each substring delimited by the
+	named delimiter character.
+
+	This implementation is taken verbatim from:
+		http://lua-users.org/wiki/SplitJoin
+--]]--------------------------------------------------------------------
+WoWAPI.strsplit = function(delim, str, maxNb)
 	-- Eliminate bad cases...
 	if string.find(str, delim) == nil then
 		return str
@@ -72,16 +403,262 @@ strsplit = strsplit or function(delim, str, maxNb)
 	return unpack(result)
 end

--- LoadAddonFile() does the equivalent of dofile(), but strips out the WoW addon
--- file line that uses ... to get the file arguments.
-LoadAddonFile = LoadAddonFile or function(filename)
-	local lineList = {}
-	for line in io.lines(filename) do
-		if not string.match(line, "^%s*local%s+[%w%s_,]*%s*=%s*[.][.][.]%s*$") then
-			table.insert(lineList, line)
+--[[--------------------------------------------------------------------
+	wipe() is a non-standard Lua function that clears the contents of a
+	table and leaves the table pointer intact.
+--]]--------------------------------------------------------------------
+WoWAPI.wipe = function(t)
+	for k in pairs(t) do
+		t[k] = nil
+	end
+end
+
+--[[-------------------------------------------------
+	Fake Blizzard API functions for unit testing.
+--]]-------------------------------------------------
+
+WoWAPI.CreateFrame = function(...)
+	return {
+		SetOwner = function(...) end,
+	}
+end
+
+WoWAPI.GetAuctionItemSubClasses = function(classIndex)
+	return
+		"One-Handed Axes",
+		"Two-Handed Axes",
+		"Bows",
+		"Guns",
+		"One-Handed Maces",
+		"Two-Handed Maces",
+		"Polearms",
+		"One-Handed Swords",
+		"Two-Handed Swords",
+		"Staves",
+		"Fist Weapons",
+		"Miscellaneous",
+		"Daggers",
+		"Thrown",
+		"Crossbows",
+		"Wands",
+		"Fishing Poles"
+end
+
+WoWAPI.GetItemInfo = function(item)
+	if type(item) == "number" then
+		item = string.format("Item Name Of %d", item)
+	end
+	return item
+end
+
+WoWAPI.GetSpellInfo = function(spell)
+	if type(spell) == "number" then
+		spell = string.format("Spell Name Of %d", spell)
+	end
+	return spell
+end
+
+WoWAPI.RegisterAddonMessagePrefix = function(prefixString) end
+
+WoWAPI.UnitClass = function()
+	local class = self_state.class
+	return class, class
+end
+
+WoWAPI.UnitLevel = function()
+	return self_state.level
+end
+
+WoWAPI.bit = {
+	band = function(...) end,
+	bor = function(...) end,
+}
+
+WoWAPI.LibStub = LibStub
+--</public-static-properties>
+
+--<private-static-methods>
+local function FileExists(filename, directory, verbose)
+	if directory then
+		filename = directory .. filename
+	end
+	local fh = io.open(filename, "r")
+	if fh then
+		fh:close()
+		return true
+	else
+		if verbose then
+			print(string.format("Warning: '%s' not found.", filename))
+		end
+		return false
+	end
+end
+--</private-static-methods>
+
+--<public-static-methods>
+function WoWAPI:Initialize(addonName, state)
+	state = state or {}
+	for k, v in pairs(state) do
+		self_state[k] = v
+	end
+	self_state.addonName = addonName
+end
+
+-- Export symbols to the given namespace, taking care not to overwrite existing symbols.
+function WoWAPI:ExportSymbols(namespace)
+	-- Default to adding symbols to the global namespace.
+	namespace = namespace or _G
+	for k, v in pairs(self) do
+		if not self_privateSymbol[k] then
+			namespace[k] = namespace[k] or v
+		end
+	end
+	-- Special handling for wipe() to add to "table" module.
+	table.wipe = table.wipe or WoWAPI.wipe
+end
+
+--[[--------------------------------------------------------------------
+	LoadAddOnFile() dispatches to the proper method to load the file
+	based on the file extension.
+--]]--------------------------------------------------------------------
+function WoWAPI:LoadAddonFile(filename, directory, verbose)
+	local s = directory and (directory .. filename) or filename
+	directory, filename = string.match(s, "^(.+/)([^/]+[.][%w]+)$")
+	if not directory then
+		filename = s
+	end
+	if string.find(filename, "[.]lua$") then
+		return self:LoadLua(filename, directory, verbose)
+	elseif string.find(filename, "[.]toc$") then
+		return self:LoadTOC(filename, directory, verbose)
+	elseif string.find(filename, "[.]xml$") then
+		return self:LoadXML(filename, directory, verbose)
+	end
+end
+
+--[[--------------------------------------------------------------------
+	LoadAddonFile() does the equivalent of dofile(), but munges the WoW
+	addon file line that uses ... to get the file arguments.
+--]]--------------------------------------------------------------------
+function WoWAPI:LoadLua(filename, directory, verbose)
+	if directory then
+		filename = directory .. filename
+	end
+	if verbose then
+		print(string.format("Loading Lua: %s", filename))
+	end
+
+	local ok = FileExists(filename, nil, verbose)
+	if ok then
+		local list = {}
+		for line in io.lines(filename) do
+			local varName = string.match(line, "^local%s+([%w_]+)%s*,[%w%s_,]*=%s*[.][.][.]%s*$")
+			if varName then
+				line = string.format("local %s = %q", varName, self_state.addonName)
+			end
+			table.insert(list, line)
+		end
+
+		local fileString = table.concat(list, "\n")
+		local func = loadstring(fileString)
+		if func then
+			func()
+		else
+			print(string.format("Error loading '%s'.", filename))
+			ok = false
+		end
+	end
+	return ok
+end
+
+--[[--------------------------------------------------------------------
+	LoadTOC() loads all of the addon's files listed in the TOC file.
+--]]--------------------------------------------------------------------
+function WoWAPI:LoadTOC(filename, directory, verbose)
+	if directory then
+		filename = directory .. filename
+	end
+	if verbose then
+		print(string.format("Loading TOC: %s", filename))
+	end
+
+	local ok = FileExists(filename, nil, verbose)
+	if ok then
+		local list = {}
+		for line in io.lines(filename) do
+			line = string.gsub(line, "\\", "/")
+			local t = {}
+			t.directory, t.file = string.match(line, "^([^#]+/)([^/]+[.][%w]+)$")
+			if t.directory then
+				if directory then
+					t.directory = directory .. t.directory
+				end
+			else
+				t.directory = directory
+				t.file = string.match(line, "^[%w_]+[.][%w]+$")
+			end
+			if t.file then
+				table.insert(list, t)
+			end
+		end
+		for _, t in ipairs(list) do
+			if string.find(t.file, "[.]lua$") then
+				ok = ok and self:LoadLua(t.file, t.directory, verbose)
+			elseif string.find(t.file, "[.]xml$") then
+				ok = ok and self:LoadXML(t.file, t.directory, verbose)
+			end
+			if not ok then
+				break
+			end
+		end
+	end
+	return ok
+end
+
+--[[--------------------------------------------------------------------
+	LoadXML() loads all of the addon's Lua files listed in the XML file.
+--]]--------------------------------------------------------------------
+function WoWAPI:LoadXML(filename, directory, verbose)
+	if directory then
+		filename = directory .. filename
+	end
+	if verbose then
+		print(string.format("Loading XML: %s", filename))
+	end
+
+	local ok = FileExists(filename, nil, verbose)
+	if ok then
+		local list = {}
+		for line in io.lines(filename) do
+			local s = string.match(line, '<Script file="([^"]+)"')
+			if s then
+				s = string.gsub(s, "\\", "/")
+				local t = {}
+				t.directory, t.file = string.match(s, "^(.+/)([^/]+[.][%w]+)$")
+				if t.directory then
+					if directory then
+						t.directory = directory .. t.directory
+					end
+				else
+					t.directory = directory
+					t.file = s
+				end
+				if t.file then
+					table.insert(list, t)
+				end
+			end
+		end
+		for _, t in ipairs(list) do
+			if FileExists(t.file, t.directory, verbose) then
+				if string.find(t.file, "[.]lua$") then
+					ok = ok and self:LoadLua(t.file, t.directory, verbose)
+					if not ok then
+						break
+					end
+				end
+			end
 		end
 	end
-	local fileString = table.concat(lineList, "\n")
-	local func = loadstring(fileString)
-	func()
+	return ok
 end
+--</public-static-methods>