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/apicoverage.lua b/lib/apicoverage.lua new file mode 100644 index 0000000..f08fa9f --- /dev/null +++ b/lib/apicoverage.lua @@ -0,0 +1,141 @@ +require("lib.baste").global() + +local httpExists, http = pcall(require, "socket.http") +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", -- luacheck: ignore +} + +-- 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" + +-- 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) + 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] + return property ~= nil +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 + if validMember(member) 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 + if validMember(member) then + classCoverage.Results[member.Name] = { + Success = false, + } + end + 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 ipairs(output) do + print(line) + end +end + +print(("Total API coverage: %.02f%%"):format((averageInfoPassed / averageInfoTotal) * 100)) 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",