From 7c2c7dab5d370c12fe44acab283feaed9ffdfe2a Mon Sep 17 00:00:00 2001 From: Kampfkarren Date: Thu, 18 Oct 2018 16:38:12 -0700 Subject: [PATCH 1/6] Create API coverage tool --- lib/apicoverage.lua | 154 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 lib/apicoverage.lua diff --git a/lib/apicoverage.lua b/lib/apicoverage.lua new file mode 100644 index 0000000..4f7e937 --- /dev/null +++ b/lib/apicoverage.lua @@ -0,0 +1,154 @@ +require("lib.baste").global() + +local httpExists, http = pcall(require, "socket.http") +local instances = import("./instances") +local json = import("./json") + +if not httpExists then + error("Please install `luasocket` to use the API coverage.") +end + +local urls = { + APIDump = "https://s3.amazonaws.com/setup.roblox.com/{VERSION_ID}-API-Dump.json", + GetVersion = "http://versioncompatibility.api.roblox.com/GetCurrentClientVersionUpload?binaryType=WindowsStudio&apiKey=76e5a40c-3ae1-4028-9f10-7c62520bd94f", +} + +-- Get current version of Roblox to get the latest JSON dump. The url returns it in quotes, so strip the quotes. +local currentVersion = http.request(urls.GetVersion):match("\"(.+)\"") +local apiDumpUrl = urls.APIDump:gsub("{VERSION_ID}", currentVersion) +local apiDumpBody = http.request(apiDumpUrl) +local apiDump = json.decode(apiDumpBody) + +local COVERAGE_CLASS_OUTPUT_LINE = "[%d%%] %s" +local COVERAGE_MEMBER_OUTPUT_LINE = "\t[%s] %s" +local PRIMITIVE_PROPERTIES = { + -- dumpType = luaType + bool = "boolean", + double = "number", + float = "number", + int = "number", + int64 = "number", + string = "string", +} + +-- Replace Instance with BaseInstance. +apiDump.Classes[1].Name = "BaseInstance" + +local function verifyFunction(instance, member) + -- TODO: Verify return types + local metatable = getmetatable(instance) + local method = metatable.class.prototype[member.Name] + return method ~= nil +end + +local function verifyProperty(instance, member) + local metatable = getmetatable(instance) + local property = metatable.class.properties[member.Name] + if not property then return false end + + if member.ValueType.Category == "Primitive" then + return type(property.getDefault(instance)) == PRIMITIVE_PROPERTIES[member.ValueType.Name] + end + + return false +end + +local coverage = {} + +for _, class in ipairs(apiDump.Classes) do + local skip = false + + for _, tag in ipairs(class.Tags or {}) do + -- Lemur has no obligation to support deprecated instances + if tag == "Deprecated" then + skip = true + break + end + end + + local classCoverage = { + Created = false, + Results = {}, + } + + if not skip then + local instanceExists, instanceReference = pcall(import, "./lib/instances/" .. class.Name) + classCoverage.Created = instanceExists + + if instanceExists then + local instance = instanceReference:new() + + for _, member in ipairs(class.Members) do + local skipMember = false + + for _, tag in ipairs(member.Tags or {}) do + if tag == "Deprecated" then + skipMember = true + break + end + end + + if not skipMember then + local success = false + + if member.MemberType == "Function" then + success = verifyFunction(instance, member) + elseif member.MemberType == "Property" then + success = verifyProperty(instance, member) + end + + classCoverage.Results[member.Name] = { + Created = true, + Success = success, + } + + class.Members[member.Name] = nil + end + end + else + -- Instance doesn't exist, everything fails!!! + for _, member in ipairs(class.Members) do + classCoverage.Results[member.Name] = { + Success = false, + } + end + end + end + + coverage[class.Name] = classCoverage +end + +-- Output +local averageInfoPassed, averageInfoTotal = 0, 0 + +for name, results in pairs(coverage) do + local output = {} + local successSum, successTotal = 0, 0 + + for memberName, result in pairs(results.Results) do + local numberSuccess = (result.Success and 1 or 0) + successSum = successSum + numberSuccess + successTotal = successTotal + 1 + output[#output + 1] = COVERAGE_MEMBER_OUTPUT_LINE:format( + result.Success and "+" or "-", + memberName + ) + + averageInfoPassed = averageInfoPassed + numberSuccess + averageInfoTotal = averageInfoTotal + 1 + end + + local average = successSum == 0 and 0 or (successSum / successTotal) * 100 + + if not results.Created then + name = name .. " [UNIMPLEMENTED]" + end + + print(COVERAGE_CLASS_OUTPUT_LINE:format(average, name)) + + for _, line in pairs(output) do + print(line) + end +end + +print(("Total API coverage: %.02f%%"):format((averageInfoPassed / averageInfoTotal) * 100)) From e59308f8271de8f3dfae7d56c0afda28d4244f32 Mon Sep 17 00:00:00 2001 From: Kampfkarren Date: Thu, 18 Oct 2018 16:39:51 -0700 Subject: [PATCH 2/6] Remove property type checks, too much work for little to no gain FMPOV --- lib/apicoverage.lua | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/apicoverage.lua b/lib/apicoverage.lua index 4f7e937..2e3f1b1 100644 --- a/lib/apicoverage.lua +++ b/lib/apicoverage.lua @@ -44,13 +44,7 @@ end local function verifyProperty(instance, member) local metatable = getmetatable(instance) local property = metatable.class.properties[member.Name] - if not property then return false end - - if member.ValueType.Category == "Primitive" then - return type(property.getDefault(instance)) == PRIMITIVE_PROPERTIES[member.ValueType.Name] - end - - return false + return property ~= nil end local coverage = {} From c020bdcac97cc2520b24ff725af9d0a0af712242 Mon Sep 17 00:00:00 2001 From: Kampfkarren Date: Thu, 18 Oct 2018 16:43:50 -0700 Subject: [PATCH 3/6] Fix luacheck --- lib/apicoverage.lua | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/lib/apicoverage.lua b/lib/apicoverage.lua index 2e3f1b1..2444e7f 100644 --- a/lib/apicoverage.lua +++ b/lib/apicoverage.lua @@ -1,7 +1,6 @@ require("lib.baste").global() local httpExists, http = pcall(require, "socket.http") -local instances = import("./instances") local json = import("./json") if not httpExists then @@ -10,7 +9,7 @@ end local urls = { APIDump = "https://s3.amazonaws.com/setup.roblox.com/{VERSION_ID}-API-Dump.json", - GetVersion = "http://versioncompatibility.api.roblox.com/GetCurrentClientVersionUpload?binaryType=WindowsStudio&apiKey=76e5a40c-3ae1-4028-9f10-7c62520bd94f", + GetVersion = "http://versioncompatibility.api.roblox.com/GetCurrentClientVersionUpload?binaryType=WindowsStudio&apiKey=76e5a40c-3ae1-4028-9f10-7c62520bd94f", -- luacheck: ignore } -- Get current version of Roblox to get the latest JSON dump. The url returns it in quotes, so strip the quotes. @@ -21,15 +20,6 @@ local apiDump = json.decode(apiDumpBody) local COVERAGE_CLASS_OUTPUT_LINE = "[%d%%] %s" local COVERAGE_MEMBER_OUTPUT_LINE = "\t[%s] %s" -local PRIMITIVE_PROPERTIES = { - -- dumpType = luaType - bool = "boolean", - double = "number", - float = "number", - int = "number", - int64 = "number", - string = "string", -} -- Replace Instance with BaseInstance. apiDump.Classes[1].Name = "BaseInstance" From 92af685905a36145d9acc73ba6cfb89df9b9c7e9 Mon Sep 17 00:00:00 2001 From: Kampfkarren Date: Fri, 19 Oct 2018 06:19:44 -0700 Subject: [PATCH 4/6] Fixed unimplemented classes showing deprecated members. --- lib/apicoverage.lua | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/apicoverage.lua b/lib/apicoverage.lua index 2444e7f..f1fe426 100644 --- a/lib/apicoverage.lua +++ b/lib/apicoverage.lua @@ -24,6 +24,16 @@ local COVERAGE_MEMBER_OUTPUT_LINE = "\t[%s] %s" -- Replace Instance with BaseInstance. apiDump.Classes[1].Name = "BaseInstance" +local function validMember(member) + for _, tag in ipairs(member.Tags or {}) do + if tag == "Deprecated" then + return false + end + end + + return true +end + local function verifyFunction(instance, member) -- TODO: Verify return types local metatable = getmetatable(instance) @@ -63,16 +73,7 @@ for _, class in ipairs(apiDump.Classes) do local instance = instanceReference:new() for _, member in ipairs(class.Members) do - local skipMember = false - - for _, tag in ipairs(member.Tags or {}) do - if tag == "Deprecated" then - skipMember = true - break - end - end - - if not skipMember then + if validMember(member) then local success = false if member.MemberType == "Function" then @@ -92,9 +93,11 @@ for _, class in ipairs(apiDump.Classes) do else -- Instance doesn't exist, everything fails!!! for _, member in ipairs(class.Members) do - classCoverage.Results[member.Name] = { - Success = false, - } + if validMember(member) then + classCoverage.Results[member.Name] = { + Success = false, + } + end end end end From b71a0f6fb506ba27ce2024f78b6dafa9b872aadc Mon Sep 17 00:00:00 2001 From: Kampfkarren Date: Fri, 19 Oct 2018 07:06:16 -0700 Subject: [PATCH 5/6] Change pairs to ipairs on list --- lib/apicoverage.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/apicoverage.lua b/lib/apicoverage.lua index f1fe426..f08fa9f 100644 --- a/lib/apicoverage.lua +++ b/lib/apicoverage.lua @@ -133,7 +133,7 @@ for name, results in pairs(coverage) do print(COVERAGE_CLASS_OUTPUT_LINE:format(average, name)) - for _, line in pairs(output) do + for _, line in ipairs(output) do print(line) end end From 9719f832e8765f7659953e327a12f4ad58bfa932 Mon Sep 17 00:00:00 2001 From: Kampfkarren Date: Fri, 19 Oct 2018 09:49:44 -0700 Subject: [PATCH 6/6] Game => DataModel --- lib/Habitat.lua | 4 ++-- lib/instances/BaseInstance_spec.lua | 4 ++-- lib/instances/{Game.lua => DataModel.lua} | 24 +++++++++---------- .../{Game_spec.lua => DataModel_spec.lua} | 12 +++++----- lib/instances/init.lua | 2 +- 5 files changed, 23 insertions(+), 23 deletions(-) rename lib/instances/{Game.lua => DataModel.lua} (81%) rename lib/instances/{Game_spec.lua => DataModel_spec.lua} (82%) diff --git a/lib/Habitat.lua b/lib/Habitat.lua index 6dfb003..b45973e 100644 --- a/lib/Habitat.lua +++ b/lib/Habitat.lua @@ -8,7 +8,7 @@ local Instance = import("./Instance") local TaskScheduler = import("./TaskScheduler") local createEnvironment = import("./createEnvironment") local fs = import("./fs") -local Game = import("./instances/Game") +local DataModel = import("./instances/DataModel") local validateType = import("./validateType") local assign = import("./assign") @@ -21,7 +21,7 @@ Habitat.__index = Habitat function Habitat.new(settings) local habitat = { - game = Game:new(), + game = DataModel:new(), taskScheduler = TaskScheduler.new(), settings = settings or {}, environment = nil, diff --git a/lib/instances/BaseInstance_spec.lua b/lib/instances/BaseInstance_spec.lua index fcbf337..c1e66ac 100644 --- a/lib/instances/BaseInstance_spec.lua +++ b/lib/instances/BaseInstance_spec.lua @@ -1,4 +1,4 @@ -local Game = import("./Game") +local DataModel = import("./DataModel") local Folder = import("./Folder") local typeof = import("../functions/typeof") @@ -367,7 +367,7 @@ describe("instances.BaseInstance", function() it("should exclude game", function() local instance = BaseInstance:new() instance.Name = "Test" - local other = Game:new() + local other = DataModel:new() other.Name = "Parent" instance.Parent = other diff --git a/lib/instances/Game.lua b/lib/instances/DataModel.lua similarity index 81% rename from lib/instances/Game.lua rename to lib/instances/DataModel.lua index 750284e..70c55c8 100644 --- a/lib/instances/Game.lua +++ b/lib/instances/DataModel.lua @@ -27,9 +27,9 @@ local UserInputService = import("./UserInputService") local VirtualInputManager = import("./VirtualInputManager") local Workspace = import("./Workspace") -local Game = BaseInstance:extend("DataModel") +local DataModel = BaseInstance:extend("DataModel") -function Game:init(instance) +function DataModel:init(instance) AnalyticsService:new().Parent = instance ContentProvider:new().Parent = instance CoreGui:new().Parent = instance @@ -56,7 +56,7 @@ function Game:init(instance) Workspace:new().Parent = instance end -function Game.prototype:GetService(serviceName) +function DataModel.prototype:GetService(serviceName) local service = self:FindFirstChildOfClass(serviceName) if service then @@ -68,52 +68,52 @@ function Game.prototype:GetService(serviceName) error(string.format("Cannot get service %q", tostring(serviceName)), 2) end -Game.properties.CreatorId = InstanceProperty.readOnly({ +DataModel.properties.CreatorId = InstanceProperty.readOnly({ getDefault = function() return 0 end, }) -Game.properties.CreatorType = InstanceProperty.readOnly({ +DataModel.properties.CreatorType = InstanceProperty.readOnly({ getDefault = function() return CreatorType.User end, }) -Game.properties.GameId = InstanceProperty.readOnly({ +DataModel.properties.GameId = InstanceProperty.readOnly({ getDefault = function() return 0 end, }) -Game.properties.JobId = InstanceProperty.readOnly({ +DataModel.properties.JobId = InstanceProperty.readOnly({ getDefault = function() return "" end, }) -Game.properties.PlaceId = InstanceProperty.readOnly({ +DataModel.properties.PlaceId = InstanceProperty.readOnly({ getDefault = function() return 0 end, }) -Game.properties.PlaceVersion = InstanceProperty.readOnly({ +DataModel.properties.PlaceVersion = InstanceProperty.readOnly({ getDefault = function() return 0 end, }) -Game.properties.VIPServerId = InstanceProperty.readOnly({ +DataModel.properties.VIPServerId = InstanceProperty.readOnly({ getDefault = function() return "" end, }) -Game.properties.VIPServerOwnerId = InstanceProperty.readOnly({ +DataModel.properties.VIPServerOwnerId = InstanceProperty.readOnly({ getDefault = function() return 0 end, }) -return Game \ No newline at end of file +return DataModel \ No newline at end of file diff --git a/lib/instances/Game_spec.lua b/lib/instances/DataModel_spec.lua similarity index 82% rename from lib/instances/Game_spec.lua rename to lib/instances/DataModel_spec.lua index 1090995..ed10b19 100644 --- a/lib/instances/Game_spec.lua +++ b/lib/instances/DataModel_spec.lua @@ -1,16 +1,16 @@ -local Game = import("./Game") +local DataModel = import("./DataModel") local typeof = import("../functions/typeof") -describe("instances.Game", function() +describe("instances.DataModel", function() it("should instantiate", function() - local instance = Game:new() + local instance = DataModel:new() assert.not_nil(instance) end) describe("GetService", function() it("should have GetService", function() - local instance = Game:new() + local instance = DataModel:new() local ReplicatedStorage = instance:GetService("ReplicatedStorage") @@ -19,7 +19,7 @@ describe("instances.Game", function() end) it("should throw when given invalid service names", function() - local instance = Game:new() + local instance = DataModel:new() assert.has.errors(function() instance:GetService("SOMETHING THAT WILL NEVER EXIST") @@ -28,7 +28,7 @@ describe("instances.Game", function() end) it("should have properties defined", function() - local instance = Game:new() + local instance = DataModel:new() assert.equal(typeof(instance.CreatorId), "number") assert.equal(typeof(instance.CreatorType), "EnumItem") diff --git a/lib/instances/init.lua b/lib/instances/init.lua index 6bb09c9..d70aadd 100644 --- a/lib/instances/init.lua +++ b/lib/instances/init.lua @@ -7,9 +7,9 @@ local names = { "ContentProvider", "CoreGui", "CorePackages", + "DataModel", "Folder", "Frame", - "Game", "GuiButton", "GuiObject", "GuiService",