Skip to content
This repository has been archived by the owner on Jul 12, 2020. It is now read-only.

Create API coverage tool #147

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/Habitat.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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,
Expand Down
141 changes: 141 additions & 0 deletions lib/apicoverage.lua
Original file line number Diff line number Diff line change
@@ -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))
4 changes: 2 additions & 2 deletions lib/instances/BaseInstance_spec.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
local Game = import("./Game")
local DataModel = import("./DataModel")
local Folder = import("./Folder")
local typeof = import("../functions/typeof")

Expand Down Expand Up @@ -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
Expand Down
24 changes: 12 additions & 12 deletions lib/instances/Game.lua → lib/instances/DataModel.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
return DataModel
12 changes: 6 additions & 6 deletions lib/instances/Game_spec.lua → lib/instances/DataModel_spec.lua
Original file line number Diff line number Diff line change
@@ -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")

Expand All @@ -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")
Expand All @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion lib/instances/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ local names = {
"ContentProvider",
"CoreGui",
"CorePackages",
"DataModel",
"Folder",
"Frame",
"Game",
"GuiButton",
"GuiObject",
"GuiService",
Expand Down