-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathConfig.lua
More file actions
273 lines (264 loc) · 10.3 KB
/
Copy pathConfig.lua
File metadata and controls
273 lines (264 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
local EnsureDictionary = require(script.Parent.Parent.Utils.EnsureDictionary)
local Config = {}
-- __index defined below
function Config.new(config)
return setmetatable(config or {}, Config)
end
local ConfigType = {}
ConfigType.__index = ConfigType
function ConfigType.new(valueType, desc)
return setmetatable({
Validate = function(value)
-- Returns success, msg or value
-- msg is a format string that can receive the name of the thing this is validating
if not value or typeof(value) == valueType then
return true, value
else
return false, ("'%%s' must be a %s or nil"):format(desc or valueType)
end
end,
}, ConfigType)
end
function ConfigType.ValueToString(value)
return tostring(value)
end
local List = ConfigType.new("table", "list")
local base = List.Validate
function List.Validate(value)
if type(value) == "string" then return true, {[value] = true} end
local goodSoFar, value = base(value)
if not goodSoFar then return goodSoFar, value end
return true, EnsureDictionary(value)
end
function List.ValueToString(value)
if value and #value > 0 then error("Not supported") end
return "{}"
end
local Number = ConfigType.new("number")
local Bool = ConfigType.new("boolean")
local function new(name, configType, default, doc)
return {
Name = name,
Type = configType,
Default = default,
Doc = doc,
}
end
local function Function(defaultFunc, defaultToString)
local Function = ConfigType.new("function")
function Function.ValueToString(value)
return value == defaultFunc and defaultToString or error("Not supported")
end
return Function
end
local function newFunc(name, doc)
local module = script.Parent["Default" .. name]
local default = require(module)
return {
Name = name,
Type = Function(default, module.Source:sub(8)), -- :sub(8) skips "return "
Default = default,
Doc = doc,
}
end
local ModuleList = ConfigType.new("table", "list/set of modules")
local base = ModuleList.Validate
function ModuleList.Validate(value)
if typeof(value) == "Instance" then return true, {value} end
local goodSoFar, value = base(value)
if not goodSoFar then return goodSoFar, value end
if next(value) and #value == 0 then -- was given a set
-- convert to list
local list = {}
for v in value do
table.insert(list, v)
end
value = list
end
-- Make sure that the values of the list are correct
for _, v in value do
if typeof(v) ~= "Instance" or not v:IsA("ModuleScript") then
return false, "%s must contain only ModuleScripts"
end
end
return true, value
end
ModuleList.ValueToString = List.ValueToString
local commonServiceNames = {
"Workspace",
"ReplicatedFirst",
"ReplicatedStorage",
"ServerScriptService",
"ServerStorage",
"StarterGui",
"StarterPack",
"StarterPlayer",
"TestService",
}
local defaultListenServiceNames = {
"TestService",
}
local GetSearchArea = newFunc("GetSearchArea", "(For TestService.TestConfig only) If provided, must return the list of service names to search through for tests. It is provided as argument a list of the service names that scripts are usually stored in. The default is to only search in TestService.")
local base = GetSearchArea.Type.Validate
function GetSearchArea.Type.Validate(value)
local success, problem = base(value)
if not success then return problem end
local problem = Config.ProblemsWithUserSearchArea(value(commonServiceNames))
if problem then
return false, "GetSearchArea(): " .. problem
end
return true, value
end
local configOptions = {
new("inherits", Bool, true, "If false, config options will not be inherited from parent TestConfigs. Recommended to be false for the top-level TestConfig in an independent module."),
new("requireTimeout", Number, 0.5, "A module times out if it hasn't returned from its require after this many seconds"),
--"Seconds for a test module to return from its initial require before timing out"),
new("initTimeout", Number, 0.5, "A module times out if it hasn't returned from its tests setup after this many seconds"),
--"Seconds for a test module to register all its tests before timing out"),
new("timeout", Number, 2, "Seconds for a test to complete before timing out"),
new("skip", List, nil, "The list of test module names to skip over. You can specify the module path (up to but *not* including TestService) as well."),
new("focus", List, nil, "If any test module names (or paths) are in this list, only they are run, regardless of what Skip contains."),
new("alwaysRefresh", ModuleList, {}, "The list of modules that must always be refreshed (due to containing mutable state) whenever it's a dependency of a new test run."),
new("expectedFirst", Bool, nil, "If true, it's t.equals(expected, actual) instead of t.equals(actual, expected)"),
GetSearchArea,
newFunc("SearchShouldRecurse", "(For TestService.TestConfig only) If provided, must return the list of service names to search through for tests. It is provided as argument a list of the service names that scripts are usually stored in (which you can simply return if you want). Note: returning false indicates that the object is not a test."),
newFunc("MayBeTest", "(For TestService.TestConfig only) Given a module script, return true if it could be a test script. Use this to filter scripts based on their name."),
newFunc("GetSetupFunc", "(For TestService.TestConfig only) Given a module script and its required value, return either the setup function or a falsy value if it is not a test.")
}
local default = {}
for _, o in configOptions do
default[o.Name] = o.Default
end
Config.__index = function(self, key)
local v = Config[key] or default[key]
if v == nil then error(("'%s' is not a valid config option"):format(tostring(key)), 2) end
return v
end
Config.Default = default
Config.IsDefault = function(config, key)
return config[key] == default[key]
end
-- local runAllList = {"requireTimeout", "initTimeout", "timeout"}
-- function Config.OnConfigChange(testConfig, old, new, testConfigTree, actions)
-- -- (In future, *could* go through runAllList, then analyze skip/focus to help determine what to rerun)
-- for _, name in runAllList do
-- local option =
-- if old[option.Name] ~= new[option.Name] then
-- end
-- -- etc
-- end
function Config.GetSearchAreaFromModule(moduleScript)
-- moduleScript can be nil
if moduleScript and moduleScript:IsA("ModuleScript") then
local success, config = pcall(require, moduleScript:Clone())
if success then
return Config.GetSearchArea(config, moduleScript)
else
warn(moduleScript:GetFullName(), "errored with:", config)
end
end
return defaultListenServiceNames
end
function Config.GetSearchArea(config, moduleScript)
-- moduleScript: for warning purposes. Provide nil to silence the warning.
if type(config) ~= "table" then
if moduleScript then warn(moduleScript:GetFullName(), "did not return a config table") end
else
local func = config.GetSearchArea
if type(func) ~= "function" then
if func ~= nil and moduleScript then warn(moduleScript:GetFullName() .. ".GetSearchArea is not a function") end
else
local success, value = pcall(config.GetSearchArea, commonServiceNames)
if success then
local problem = Config.ProblemsWithUserSearchArea(value)
if problem then
if moduleScript then warn(moduleScript:GetFullName() .. ".GetSearchArea(commonServiceNames) returned '" .. tostring(value) .. "', but", problem) end
else
return value
end
else
if moduleScript then warn(moduleScript:GetFullName() .. ".GetSearchArea(commonServiceNames) failed:", value) end
end
end
end
return defaultListenServiceNames
end
function Config.ProblemsWithUserSearchArea(searchArea)
-- Returns a problem string if there's something wrong, else nil
if type(searchArea) ~= "table" then
return "it must return a table"
elseif #searchArea == 0 then
return "it is empty"
else
for i, v in searchArea do
if type(v) ~= "string" then
return ("[%d] = %s instead of a string"):format(i, v)
end
if not pcall(game.GetService, game, v) then
return v .. " is not a valid service"
end
end
end
end
function Config.GetDocs(header)
-- Get the documentation for the configuration options
-- The returned value is a string containing a Lua table
local configDocs = {
header("Configuration"), [=[
You can configure this system using ModuleScripts named "TestConfig".
A TestConfig applies to all tests found in its siblings and their descendants (but another TestConfig can override it on a per-property basis).
If a TestConfig's parent is the TestService, it becomes the default TestConfig for the entire game, not just TestService.
These configuration scripts must return a table with any of the following fields (all optional; the values below are the defaults):
return {]=]
}
for _, details in configOptions do
local name, Type, default, doc = details.Name, details.Type, details.Default, details.Doc
configDocs[#configDocs + 1] = ("\t%s = %s,%s"):format(name, Type.ValueToString(default), doc and (" -- %s"):format(doc) or "")
end
configDocs[#configDocs + 1] = "}"
return table.concat(configDocs, "\n")
end
function Config.Validate(config)
-- Returns issues:string (potentially multiline) or nil, newConfig
if type(config) ~= "table" then
return "Config must be a table", nil
end
local problems = {} --{"Config failed to validate:"} -- not necessary (see TestConfigTree's setConfig)
local newConfig = Config.new()
local keysAnalyzed = {}
for _, details in configOptions do
local name, Type = details.Name, details.Type
if config[name] ~= nil then
local success, msg = Type.Validate(config[name])
if success then
newConfig[name] = msg
else
problems[#problems + 1] = msg:format(name)
end
end
keysAnalyzed[name] = true
end
for k, v in config do
if not keysAnalyzed[k] then
problems[#problems + 1] = ("Unrecognized key '%s'"):format(k)
end
end
return #problems > 1 and table.concat(problems, "\n - ") or nil, newConfig
end
function Config.GetConfigFromModule(moduleScript)
-- moduleScript can be nil
if moduleScript and moduleScript:IsA("ModuleScript") then
local success, config = pcall(require, moduleScript:Clone())
if success then
local problems, config = Config.Validate(config)
if problems then
warn(moduleScript:GetFullName(), "is invalid:", problems)
end
return config
-- else -- As of Nov 2020, the error is being displayed despite being in a pcall
-- warn(moduleScript:GetFullName(), "errored with:", config)
end
end
return Config.new()
end
return Config