@quenty/studio-bridge 0.22.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +36 -8
- package/dist/src/bridge/bridge-connection.d.ts.map +1 -1
- package/dist/src/bridge/bridge-connection.js +1 -0
- package/dist/src/bridge/bridge-connection.js.map +1 -1
- package/dist/src/bridge/bridge-session.d.ts +11 -16
- package/dist/src/bridge/bridge-session.d.ts.map +1 -1
- package/dist/src/bridge/bridge-session.js +37 -29
- package/dist/src/bridge/bridge-session.js.map +1 -1
- package/dist/src/bridge/bridge-session.test.js +35 -0
- package/dist/src/bridge/bridge-session.test.js.map +1 -1
- package/dist/src/bridge/internal/bridge-host.d.ts +2 -0
- package/dist/src/bridge/internal/bridge-host.d.ts.map +1 -1
- package/dist/src/bridge/internal/bridge-host.js +1 -0
- package/dist/src/bridge/internal/bridge-host.js.map +1 -1
- package/dist/src/bridge/internal/host-protocol.d.ts.map +1 -1
- package/dist/src/bridge/internal/host-protocol.js +23 -10
- package/dist/src/bridge/internal/host-protocol.js.map +1 -1
- package/dist/src/bridge/types.d.ts +5 -2
- package/dist/src/bridge/types.d.ts.map +1 -1
- package/dist/src/bridge/types.js.map +1 -1
- package/dist/src/commands/console/exec/exec.d.ts +13 -3
- package/dist/src/commands/console/exec/exec.d.ts.map +1 -1
- package/dist/src/commands/console/exec/exec.js +82 -7
- package/dist/src/commands/console/exec/exec.js.map +1 -1
- package/dist/src/commands/console/exec/exec.test.js +63 -1
- package/dist/src/commands/console/exec/exec.test.js.map +1 -1
- package/dist/src/commands/framework/action-loader.test.js +1 -1
- package/dist/src/commands/framework/action-loader.test.js.map +1 -1
- package/dist/src/commands/playtest/player/playtest-players.d.ts.map +1 -1
- package/dist/src/commands/playtest/player/playtest-players.js +15 -93
- package/dist/src/commands/playtest/player/playtest-players.js.map +1 -1
- package/dist/src/commands/playtest/player/playtest-players.test.js +34 -21
- package/dist/src/commands/playtest/player/playtest-players.test.js.map +1 -1
- package/dist/src/commands/playtest/playtest-sessions.d.ts +19 -3
- package/dist/src/commands/playtest/playtest-sessions.d.ts.map +1 -1
- package/dist/src/commands/playtest/playtest-sessions.js +54 -74
- package/dist/src/commands/playtest/playtest-sessions.js.map +1 -1
- package/dist/src/commands/playtest/playtest-sessions.test.js +74 -8
- package/dist/src/commands/playtest/playtest-sessions.test.js.map +1 -1
- package/dist/src/commands/playtest/run/run.d.ts +9 -7
- package/dist/src/commands/playtest/run/run.d.ts.map +1 -1
- package/dist/src/commands/playtest/run/run.js +33 -38
- package/dist/src/commands/playtest/run/run.js.map +1 -1
- package/dist/src/commands/process/close/close.d.ts +4 -0
- package/dist/src/commands/process/close/close.d.ts.map +1 -1
- package/dist/src/commands/process/close/close.js +54 -4
- package/dist/src/commands/process/close/close.js.map +1 -1
- package/dist/src/commands/process/close/close.test.js +36 -3
- package/dist/src/commands/process/close/close.test.js.map +1 -1
- package/dist/src/commands/process/info/info.d.ts +15 -2
- package/dist/src/commands/process/info/info.d.ts.map +1 -1
- package/dist/src/commands/process/info/info.js +44 -4
- package/dist/src/commands/process/info/info.js.map +1 -1
- package/dist/src/commands/process/info/info.test.js +48 -0
- package/dist/src/commands/process/info/info.test.js.map +1 -1
- package/dist/src/commands/process/launch/launch.d.ts.map +1 -1
- package/dist/src/commands/process/launch/launch.js +22 -3
- package/dist/src/commands/process/launch/launch.js.map +1 -1
- package/dist/src/commands/process/launch/launch.test.js +27 -5
- package/dist/src/commands/process/launch/launch.test.js.map +1 -1
- package/dist/src/commands/process/list/list.d.ts +49 -6
- package/dist/src/commands/process/list/list.d.ts.map +1 -1
- package/dist/src/commands/process/list/list.js +185 -18
- package/dist/src/commands/process/list/list.js.map +1 -1
- package/dist/src/commands/process/list/list.test.js +92 -1
- package/dist/src/commands/process/list/list.test.js.map +1 -1
- package/dist/src/commands/process/run/run.d.ts +1 -1
- package/dist/src/commands/process/run/run.d.ts.map +1 -1
- package/dist/src/commands/process/run/run.js +3 -3
- package/dist/src/commands/process/run/run.js.map +1 -1
- package/dist/src/docker/docker-delegator.js +1 -1
- package/dist/src/docker/docker-delegator.js.map +1 -1
- package/dist/src/docker/docker-delegator.test.js +1 -1
- package/dist/src/docker/docker-delegator.test.js.map +1 -1
- package/dist/src/plugin/playtest-bridge.d.ts +36 -0
- package/dist/src/plugin/playtest-bridge.d.ts.map +1 -0
- package/dist/src/plugin/playtest-bridge.js +65 -0
- package/dist/src/plugin/playtest-bridge.js.map +1 -0
- package/dist/src/plugin/playtest-bridge.test.d.ts +2 -0
- package/dist/src/plugin/playtest-bridge.test.d.ts.map +1 -0
- package/dist/src/plugin/playtest-bridge.test.js +35 -0
- package/dist/src/plugin/playtest-bridge.test.js.map +1 -0
- package/dist/src/process/studio-launch-registry.d.ts +14 -0
- package/dist/src/process/studio-launch-registry.d.ts.map +1 -0
- package/dist/src/process/studio-launch-registry.js +37 -0
- package/dist/src/process/studio-launch-registry.js.map +1 -0
- package/dist/src/process/studio-launch-registry.test.d.ts +2 -0
- package/dist/src/process/studio-launch-registry.test.d.ts.map +1 -0
- package/dist/src/process/studio-launch-registry.test.js +38 -0
- package/dist/src/process/studio-launch-registry.test.js.map +1 -0
- package/dist/src/process/studio-process-closer.d.ts +3 -1
- package/dist/src/process/studio-process-closer.d.ts.map +1 -1
- package/dist/src/process/studio-process-closer.js +29 -11
- package/dist/src/process/studio-process-closer.js.map +1 -1
- package/dist/src/process/studio-process-identity.d.ts +10 -0
- package/dist/src/process/studio-process-identity.d.ts.map +1 -0
- package/dist/src/process/studio-process-identity.js +63 -0
- package/dist/src/process/studio-process-identity.js.map +1 -0
- package/dist/src/process/studio-process-identity.test.d.ts +2 -0
- package/dist/src/process/studio-process-identity.test.d.ts.map +1 -0
- package/dist/src/process/studio-process-identity.test.js +30 -0
- package/dist/src/process/studio-process-identity.test.js.map +1 -0
- package/dist/src/roblox-mcp/index.d.ts +3 -0
- package/dist/src/roblox-mcp/index.d.ts.map +1 -0
- package/dist/src/roblox-mcp/index.js +3 -0
- package/dist/src/roblox-mcp/index.js.map +1 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.d.ts +60 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.d.ts.map +1 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.js +171 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.js.map +1 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.test.d.ts +2 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.test.js +104 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.test.js.map +1 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.d.ts +2 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.d.ts.map +1 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.js +22 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.js.map +1 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.test.d.ts +2 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.test.js +42 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.test.js.map +1 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.d.ts +34 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.d.ts.map +1 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.js +190 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.js.map +1 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.test.d.ts +2 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.test.js +123 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.test.js.map +1 -0
- package/dist/src/roblox-mcp/studio/studio-session.d.ts +39 -0
- package/dist/src/roblox-mcp/studio/studio-session.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio/studio-session.js +175 -0
- package/dist/src/roblox-mcp/studio/studio-session.js.map +1 -0
- package/dist/src/roblox-mcp/studio/studio-session.test.d.ts +2 -0
- package/dist/src/roblox-mcp/studio/studio-session.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio/studio-session.test.js +213 -0
- package/dist/src/roblox-mcp/studio/studio-session.test.js.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.d.ts +19 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.js +121 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.js.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.test.d.ts +2 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.test.js +107 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.test.js.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.d.ts +23 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.js +65 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.js.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.test.d.ts +2 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.test.js +81 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.test.js.map +1 -0
- package/dist/src/roblox-mcp/test/fake-mcp-server.d.ts +27 -0
- package/dist/src/roblox-mcp/test/fake-mcp-server.d.ts.map +1 -0
- package/dist/src/roblox-mcp/test/fake-mcp-server.js +55 -0
- package/dist/src/roblox-mcp/test/fake-mcp-server.js.map +1 -0
- package/dist/src/server/web-socket-protocol.d.ts +50 -15
- package/dist/src/server/web-socket-protocol.d.ts.map +1 -1
- package/dist/src/server/web-socket-protocol.js +63 -13
- package/dist/src/server/web-socket-protocol.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/docs/playtest-bridge.md +21 -0
- package/package.json +5 -5
- package/src/bridge/bridge-connection.ts +1 -0
- package/src/bridge/bridge-session.test.ts +51 -0
- package/src/bridge/bridge-session.ts +66 -47
- package/src/bridge/internal/bridge-host.ts +3 -0
- package/src/bridge/internal/host-protocol.ts +22 -9
- package/src/bridge/types.ts +5 -0
- package/src/commands/console/exec/exec.test.ts +94 -0
- package/src/commands/console/exec/exec.ts +130 -16
- package/src/commands/console/exec/execute.lua +147 -5
- package/src/commands/console/exec/execute.spec.lua +22 -2
- package/src/commands/framework/action-loader.test.ts +1 -1
- package/src/commands/playtest/player/playtest-players.lua +127 -0
- package/src/commands/playtest/player/playtest-players.spec.lua +48 -0
- package/src/commands/playtest/player/playtest-players.test.ts +40 -21
- package/src/commands/playtest/player/playtest-players.ts +30 -110
- package/src/commands/playtest/playtest-sessions.test.ts +116 -14
- package/src/commands/playtest/playtest-sessions.ts +77 -88
- package/src/commands/playtest/run/run.ts +44 -58
- package/src/commands/playtest/start/start-playtest.lua +105 -6
- package/src/commands/process/close/close.test.ts +52 -3
- package/src/commands/process/close/close.ts +78 -4
- package/src/commands/process/info/info.test.ts +76 -0
- package/src/commands/process/info/info.ts +74 -6
- package/src/commands/process/launch/launch.test.ts +33 -5
- package/src/commands/process/launch/launch.ts +41 -3
- package/src/commands/process/list/list.test.ts +130 -1
- package/src/commands/process/list/list.ts +290 -22
- package/src/commands/process/run/run.ts +4 -4
- package/src/docker/docker-delegator.test.ts +1 -1
- package/src/docker/docker-delegator.ts +1 -1
- package/src/plugin/playtest-bridge.test.ts +53 -0
- package/src/plugin/playtest-bridge.ts +98 -0
- package/src/process/studio-launch-registry.test.ts +56 -0
- package/src/process/studio-launch-registry.ts +63 -0
- package/src/process/studio-process-closer.ts +36 -11
- package/src/process/studio-process-identity.test.ts +45 -0
- package/src/process/studio-process-identity.ts +90 -0
- package/src/roblox-mcp/index.ts +17 -0
- package/src/roblox-mcp/mcp/mcp-stdio-client.test.ts +148 -0
- package/src/roblox-mcp/mcp/mcp-stdio-client.ts +270 -0
- package/src/roblox-mcp/mcp/repair-list-result.test.ts +52 -0
- package/src/roblox-mcp/mcp/repair-list-result.ts +23 -0
- package/src/roblox-mcp/roblox-studio-mcp.test.ts +168 -0
- package/src/roblox-mcp/roblox-studio-mcp.ts +330 -0
- package/src/roblox-mcp/studio/studio-session.test.ts +284 -0
- package/src/roblox-mcp/studio/studio-session.ts +279 -0
- package/src/roblox-mcp/studio-mcp-locator.test.ts +186 -0
- package/src/roblox-mcp/studio-mcp-locator.ts +180 -0
- package/src/roblox-mcp/studio-mcp-snapshot.test.ts +113 -0
- package/src/roblox-mcp/studio-mcp-snapshot.ts +104 -0
- package/src/roblox-mcp/test/fake-mcp-server.ts +74 -0
- package/src/server/web-socket-protocol.ts +119 -29
- package/templates/studio-bridge-plugin/playtest/StudioBridgePlaytest.client.lua +32 -0
- package/templates/studio-bridge-plugin/playtest/StudioBridgePlaytest.server.lua +40 -0
- package/templates/studio-bridge-plugin/src/Shared/ActionRouter.lua +25 -3
- package/templates/studio-bridge-plugin/src/Shared/PrecompiledSources.lua +47 -0
- package/templates/studio-bridge-plugin/src/Shared/PrecompiledSources.spec.lua +57 -0
- package/templates/studio-bridge-plugin/src/Shared/StudioBridgeClient.lua +357 -0
- package/templates/studio-bridge-plugin/src/Shared/StudioBridgeSessionUtils.lua +29 -3
- package/templates/studio-bridge-plugin/src/Shared/StudioBridgeSessionUtils.spec.lua +35 -2
- package/templates/studio-bridge-plugin/src/StudioBridgePlugin.server.lua +16 -292
- package/src/commands/playtest/run/playtest-runner.luau +0 -264
- package/src/commands/playtest/run/playtest-runner.spec.lua +0 -71
- package/src/commands/playtest/run/run-playtest-script.lua +0 -139
- package/src/commands/playtest/run/run-playtest-script.spec.lua +0 -43
|
@@ -1,44 +1,26 @@
|
|
|
1
1
|
--!strict
|
|
2
2
|
--[[
|
|
3
|
-
Entry point for the studio-bridge plugin.
|
|
4
|
-
|
|
3
|
+
Entry point for the studio-bridge plugin. Starts StudioBridgeClient in one of
|
|
4
|
+
two modes:
|
|
5
5
|
|
|
6
6
|
- Ephemeral: the CLI substituted the build constants below, so it connects
|
|
7
7
|
straight to that port and session.
|
|
8
8
|
- Persistent: the constants are left empty, so it scans for a
|
|
9
|
-
running bridge
|
|
9
|
+
running bridge and reconnects whenever it drops.
|
|
10
10
|
|
|
11
11
|
The plugin cannot use the Nevermore loader: rojo builds this template on its
|
|
12
12
|
own from the published CLI package, without any node_modules.
|
|
13
13
|
]]
|
|
14
14
|
|
|
15
|
-
local HttpService = game:GetService("HttpService")
|
|
16
|
-
local LogService = game:GetService("LogService")
|
|
17
|
-
local Players = game:GetService("Players")
|
|
18
15
|
local RunService = game:GetService("RunService")
|
|
19
16
|
|
|
20
|
-
local
|
|
21
|
-
local BuiltInActions = require(script.Parent.Shared.BuiltInActions)
|
|
22
|
-
local DiscoveryStateMachine = require(script.Parent.Shared.DiscoveryStateMachine)
|
|
23
|
-
local MessageBuffer = require(script.Parent.Shared.MessageBuffer)
|
|
24
|
-
local StudioBridgeConnection = require(script.Parent.Shared.StudioBridgeConnection)
|
|
25
|
-
local StudioBridgeLogUtils = require(script.Parent.Shared.StudioBridgeLogUtils)
|
|
17
|
+
local StudioBridgeClient = require(script.Parent.Shared.StudioBridgeClient)
|
|
26
18
|
local StudioBridgeSessionUtils = require(script.Parent.Shared.StudioBridgeSessionUtils)
|
|
27
19
|
|
|
28
20
|
local PORT = "{{PORT}}"
|
|
29
21
|
local SESSION_ID = "{{SESSION_ID}}"
|
|
30
22
|
local IS_EPHEMERAL = ("{{EPHEMERAL}}" :: string) == "true"
|
|
31
23
|
|
|
32
|
-
local LOG_BUFFER_CAPACITY = 1000
|
|
33
|
-
local POLL_INTERVAL_SEC = 2
|
|
34
|
-
|
|
35
|
-
local LOG_LEVELS: { [Enum.MessageType]: string } = {
|
|
36
|
-
[Enum.MessageType.MessageOutput] = "Print",
|
|
37
|
-
[Enum.MessageType.MessageInfo] = "Info",
|
|
38
|
-
[Enum.MessageType.MessageWarning] = "Warning",
|
|
39
|
-
[Enum.MessageType.MessageError] = "Error",
|
|
40
|
-
}
|
|
41
|
-
|
|
42
24
|
-- Playtests load a plugin instance into their server and client, but only when
|
|
43
25
|
-- Studio's "load user plugins on playtest" setting is on. Those instances cannot
|
|
44
26
|
-- make the HTTP requests port discovery needs, so they only connect in a
|
|
@@ -55,283 +37,25 @@ if not RunService:IsStudio() or (RunService:IsRunning() and not playtestArgs) th
|
|
|
55
37
|
return
|
|
56
38
|
end
|
|
57
39
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if not nonceOk then
|
|
65
|
-
nonce = createNonce()
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
-- Read on every connect: Studio can load plugins before it names the DataModel after the place
|
|
69
|
-
local function getInstanceId(): string
|
|
70
|
-
if playtestArgs then
|
|
71
|
-
return playtestArgs.instanceId
|
|
40
|
+
-- A plugin that loads into the playtest's server replaces the bridge planted for it, which can only run code compiled before the playtest
|
|
41
|
+
if playtestArgs and RunService:IsServer() then
|
|
42
|
+
local planted =
|
|
43
|
+
StudioBridgeSessionUtils.findPlantedBridge(game:GetService("ServerScriptService"), playtestArgs.playtestId)
|
|
44
|
+
if planted then
|
|
45
|
+
planted:Destroy()
|
|
72
46
|
end
|
|
73
|
-
return StudioBridgeSessionUtils.getInstanceId(game.Name, nonce)
|
|
74
|
-
end
|
|
75
|
-
local playtestPort = if playtestArgs then playtestArgs.port else nil
|
|
76
|
-
|
|
77
|
-
local function getPlayerName(): string?
|
|
78
|
-
local player = if context == "client" then Players.LocalPlayer else nil
|
|
79
|
-
return if player then player.Name else nil
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
-- The clients of a multiplayer test share an instance id, so their session ids name their player
|
|
83
|
-
local function getSessionId(): string
|
|
84
|
-
local isMultiplayerClient = playtestArgs and playtestArgs.multiplayer and context == "client"
|
|
85
|
-
return StudioBridgeSessionUtils.getSessionId(
|
|
86
|
-
getInstanceId(),
|
|
87
|
-
context,
|
|
88
|
-
if isMultiplayerClient then getPlayerName() else nil
|
|
89
|
-
)
|
|
90
47
|
end
|
|
91
48
|
|
|
92
|
-
local router = ActionRouter.new()
|
|
93
|
-
local logBuffer = MessageBuffer.new(LOG_BUFFER_CAPACITY)
|
|
94
|
-
local currentConnection: StudioBridgeConnection.StudioBridgeConnection? = nil
|
|
95
|
-
local currentWebSocket: any = nil
|
|
96
|
-
local discovery: DiscoveryStateMachine.DiscoveryStateMachine? = nil
|
|
97
|
-
local pollThread: thread? = nil
|
|
98
|
-
local nameConnection: RBXScriptConnection? = nil
|
|
99
|
-
local isUnloaded = false
|
|
100
|
-
|
|
101
49
|
local pngOk, png = pcall(function()
|
|
102
50
|
return (require :: any)(script.Parent.Vendor.png)
|
|
103
51
|
end)
|
|
104
|
-
if pngOk then
|
|
105
|
-
router:SetVendorPng(png)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
BuiltInActions.register(router, function(message)
|
|
109
|
-
if currentConnection then
|
|
110
|
-
currentConnection:Send(message)
|
|
111
|
-
end
|
|
112
|
-
end, logBuffer)
|
|
113
|
-
|
|
114
|
-
-- Hybrid clock: the wall clock at load, then os.clock() deltas, which are cheaper per entry
|
|
115
|
-
local clockBase = os.clock()
|
|
116
|
-
local timeBase = DateTime.now().UnixTimestampMillis / 1000
|
|
117
|
-
|
|
118
|
-
local logConnection = LogService.MessageOut:Connect(function(message, messageType)
|
|
119
|
-
if string.sub(message, 1, #StudioBridgeLogUtils.PREFIX) == StudioBridgeLogUtils.PREFIX then
|
|
120
|
-
return
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
logBuffer:Push({
|
|
124
|
-
level = LOG_LEVELS[messageType] or "Print",
|
|
125
|
-
body = message,
|
|
126
|
-
timestamp = timeBase + (os.clock() - clockBase),
|
|
127
|
-
})
|
|
128
|
-
end)
|
|
129
52
|
|
|
130
|
-
local
|
|
131
|
-
if
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if currentConnection then
|
|
136
|
-
currentConnection:Destroy()
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
currentConnection = StudioBridgeConnection.new({
|
|
140
|
-
webSocket = webSocket,
|
|
141
|
-
router = router,
|
|
142
|
-
sessionId = sessionId,
|
|
143
|
-
registerPayload = StudioBridgeSessionUtils.createRegisterPayload(
|
|
144
|
-
getInstanceId(),
|
|
145
|
-
context,
|
|
146
|
-
StudioBridgeSessionUtils.getLaunchId(workspace),
|
|
147
|
-
getPlayerName()
|
|
148
|
-
),
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
StudioBridgeLogUtils.logVerbose(`Connected to {label} as {sessionId}`)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
local function createWebSocket(url: string): (boolean, any)
|
|
155
|
-
local ok, webSocket = pcall(function()
|
|
156
|
-
return HttpService:CreateWebStreamClient(Enum.WebStreamClientType.WebSocket, { Url = url })
|
|
157
|
-
end)
|
|
158
|
-
if ok then
|
|
159
|
-
currentWebSocket = webSocket
|
|
160
|
-
end
|
|
161
|
-
return ok, webSocket
|
|
162
|
-
end
|
|
53
|
+
local stop = StudioBridgeClient.start({
|
|
54
|
+
ephemeral = if IS_EPHEMERAL then { port = PORT, sessionId = SESSION_ID } else nil,
|
|
55
|
+
playtestArgs = playtestArgs,
|
|
56
|
+
vendorPng = if pngOk then png else nil,
|
|
57
|
+
})
|
|
163
58
|
|
|
164
59
|
-- Reloading the plugin destroys this script, but the socket callbacks and the
|
|
165
60
|
-- poll loop outlive it, leaving the old instance connected beside the new one
|
|
166
|
-
plugin.Unloading:Connect(
|
|
167
|
-
isUnloaded = true
|
|
168
|
-
logConnection:Disconnect()
|
|
169
|
-
if nameConnection then
|
|
170
|
-
nameConnection:Disconnect()
|
|
171
|
-
nameConnection = nil
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
if pollThread then
|
|
175
|
-
task.cancel(pollThread)
|
|
176
|
-
pollThread = nil
|
|
177
|
-
end
|
|
178
|
-
if discovery then
|
|
179
|
-
discovery:Stop()
|
|
180
|
-
end
|
|
181
|
-
if currentConnection then
|
|
182
|
-
currentConnection:Destroy()
|
|
183
|
-
currentConnection = nil
|
|
184
|
-
end
|
|
185
|
-
if currentWebSocket then
|
|
186
|
-
pcall(function()
|
|
187
|
-
currentWebSocket:Close()
|
|
188
|
-
end)
|
|
189
|
-
currentWebSocket = nil
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
router:Destroy()
|
|
193
|
-
end)
|
|
194
|
-
|
|
195
|
-
local function warnOnError(webSocket: any)
|
|
196
|
-
webSocket.Error:Connect(function(status, err)
|
|
197
|
-
StudioBridgeLogUtils.logVerbose(`WebSocket error ({status}): {err}`)
|
|
198
|
-
end)
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
--[[
|
|
202
|
-
Probes every port's /health endpoint in parallel and returns the first port
|
|
203
|
-
to answer, or nil once all fail or the timeout passes. The caller is resumed
|
|
204
|
-
with task.defer so it has reached coroutine.yield() first.
|
|
205
|
-
]]
|
|
206
|
-
local function scanPortsAsync(ports: { number }, timeoutSec: number): (number?, string?)
|
|
207
|
-
local callerThread = coroutine.running()
|
|
208
|
-
local foundPort: number? = nil
|
|
209
|
-
local foundBody: string? = nil
|
|
210
|
-
local remaining = #ports
|
|
211
|
-
local settled = false
|
|
212
|
-
|
|
213
|
-
local function settle()
|
|
214
|
-
if not settled then
|
|
215
|
-
settled = true
|
|
216
|
-
task.defer(callerThread)
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
local threads: { thread } = {}
|
|
221
|
-
for _, port in ports do
|
|
222
|
-
table.insert(
|
|
223
|
-
threads,
|
|
224
|
-
task.spawn(function()
|
|
225
|
-
local ok, body = pcall(HttpService.GetAsync, HttpService, `http://localhost:{port}/health`)
|
|
226
|
-
if ok and not settled then
|
|
227
|
-
foundPort = port
|
|
228
|
-
foundBody = body
|
|
229
|
-
settle()
|
|
230
|
-
return
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
remaining -= 1
|
|
234
|
-
if remaining <= 0 then
|
|
235
|
-
settle()
|
|
236
|
-
end
|
|
237
|
-
end)
|
|
238
|
-
)
|
|
239
|
-
end
|
|
240
|
-
table.insert(threads, task.delay(timeoutSec, settle))
|
|
241
|
-
|
|
242
|
-
coroutine.yield()
|
|
243
|
-
|
|
244
|
-
for _, thread in threads do
|
|
245
|
-
pcall(task.cancel, thread)
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
return foundPort, foundBody
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
if IS_EPHEMERAL then
|
|
252
|
-
-- Every open Studio loads this plugin, but only the one launched for this run
|
|
253
|
-
-- has the session id stamped on its place, which may load after the plugin
|
|
254
|
-
local function connectEphemeral()
|
|
255
|
-
if StudioBridgeSessionUtils.getLaunchId(workspace) ~= SESSION_ID or currentWebSocket then
|
|
256
|
-
return
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
local ok, webSocket = createWebSocket(`ws://localhost:{PORT}/{SESSION_ID}`)
|
|
260
|
-
if not (ok and webSocket) then
|
|
261
|
-
StudioBridgeLogUtils.warn("Failed to create WebSocket client")
|
|
262
|
-
return
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
webSocket.Opened:Connect(function()
|
|
266
|
-
openConnection(webSocket, SESSION_ID, `localhost:{PORT} (ephemeral)`)
|
|
267
|
-
end)
|
|
268
|
-
warnOnError(webSocket)
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
workspace:GetAttributeChangedSignal("StudioBridgeSessionId"):Connect(connectEphemeral)
|
|
272
|
-
connectEphemeral()
|
|
273
|
-
return
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
-- A multiplayer client's session id names its player, so wait for one
|
|
277
|
-
if playtestArgs and playtestArgs.multiplayer and context == "client" then
|
|
278
|
-
while not Players.LocalPlayer do
|
|
279
|
-
Players:GetPropertyChangedSignal("LocalPlayer"):Wait()
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
|
|
283
|
-
local stateMachine: DiscoveryStateMachine.DiscoveryStateMachine
|
|
284
|
-
stateMachine = DiscoveryStateMachine.new(nil, {
|
|
285
|
-
scanPortsAsync = if playtestPort
|
|
286
|
-
then function()
|
|
287
|
-
return playtestPort, nil
|
|
288
|
-
end
|
|
289
|
-
else scanPortsAsync,
|
|
290
|
-
connectWebSocket = createWebSocket,
|
|
291
|
-
onStateChange = function(oldState, newState)
|
|
292
|
-
if oldState == "idle" and newState == "searching" then
|
|
293
|
-
StudioBridgeLogUtils.logVerbose("Searching for host on ports 38741-38744...")
|
|
294
|
-
end
|
|
295
|
-
end,
|
|
296
|
-
onConnected = function(webSocket, port)
|
|
297
|
-
webSocket.Opened:Connect(function()
|
|
298
|
-
openConnection(webSocket, getSessionId(), `localhost:{port}`)
|
|
299
|
-
end)
|
|
300
|
-
webSocket.Closed:Connect(function()
|
|
301
|
-
stateMachine:HandleDisconnect("closed")
|
|
302
|
-
end)
|
|
303
|
-
warnOnError(webSocket)
|
|
304
|
-
end,
|
|
305
|
-
onDisconnected = function(reason)
|
|
306
|
-
StudioBridgeLogUtils.logVerbose(`Disconnected ({reason}), searching...`)
|
|
307
|
-
end,
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
discovery = stateMachine
|
|
311
|
-
|
|
312
|
-
stateMachine:Start()
|
|
313
|
-
|
|
314
|
-
-- Reconnecting re-registers under the session id for the new name. A
|
|
315
|
-
-- playtest's id comes from its test args, so a rename leaves it unchanged.
|
|
316
|
-
nameConnection = game:GetPropertyChangedSignal("Name"):Connect(function()
|
|
317
|
-
if currentWebSocket and not playtestArgs then
|
|
318
|
-
pcall(function()
|
|
319
|
-
currentWebSocket:Close()
|
|
320
|
-
end)
|
|
321
|
-
end
|
|
322
|
-
end)
|
|
323
|
-
|
|
324
|
-
-- Each cycle gets a fixed time budget, which PollAsync threads through to the port scan
|
|
325
|
-
pollThread = task.spawn(function()
|
|
326
|
-
while true do
|
|
327
|
-
local startTime = os.clock()
|
|
328
|
-
stateMachine:PollAsync(POLL_INTERVAL_SEC)
|
|
329
|
-
|
|
330
|
-
local remaining = POLL_INTERVAL_SEC - (os.clock() - startTime)
|
|
331
|
-
if remaining > 0 then
|
|
332
|
-
task.wait(remaining)
|
|
333
|
-
else
|
|
334
|
-
task.wait()
|
|
335
|
-
end
|
|
336
|
-
end
|
|
337
|
-
end)
|
|
61
|
+
plugin.Unloading:Connect(stop)
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
--[=[
|
|
3
|
-
Runs a `playtest run` script inside the playtest's server and ends the
|
|
4
|
-
playtest with the result, so running a script does not depend on Studio
|
|
5
|
-
loading user plugins into playtests.
|
|
6
|
-
|
|
7
|
-
The `run-playtest-script` action plants this source as a Script in the edit
|
|
8
|
-
DataModel, with the code as a ModuleScript named `Code` under it, and starts a
|
|
9
|
-
playtest. The playtest copies the edit DataModel, so the runner starts in its
|
|
10
|
-
server. It only acts in the playtest it was planted for: anywhere else, such
|
|
11
|
-
as a place saved while it was planted, it does nothing.
|
|
12
|
-
|
|
13
|
-
The result crosses back through `StudioTestService:EndTest` in the same shape
|
|
14
|
-
as an `execute` reply. Instances cannot cross DataModels, so values are
|
|
15
|
-
marshalled here. Output capture, tracebacks and the marshaller are copies of
|
|
16
|
-
execute.lua's, since the runner cannot require it.
|
|
17
|
-
|
|
18
|
-
@class PlaytestRunner
|
|
19
|
-
]=]
|
|
20
|
-
|
|
21
|
-
local MAX_DEPTH = 64
|
|
22
|
-
local PLUGIN_PREFIX = "[StudioBridge]"
|
|
23
|
-
-- Tracebacks name this module's frames the way its first line does
|
|
24
|
-
local OWN_FRAME_PREFIX = string.match(debug.traceback(), "^([^\n]-):%d+")
|
|
25
|
-
|
|
26
|
-
local PlaytestRunner = {}
|
|
27
|
-
|
|
28
|
-
type LogEntry = {
|
|
29
|
-
level: string,
|
|
30
|
-
body: string,
|
|
31
|
-
timestamp: number,
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
local LOG_LEVELS: { [Enum.MessageType]: string } = {
|
|
35
|
-
[Enum.MessageType.MessageOutput] = "Print",
|
|
36
|
-
[Enum.MessageType.MessageInfo] = "Info",
|
|
37
|
-
[Enum.MessageType.MessageWarning] = "Warning",
|
|
38
|
-
[Enum.MessageType.MessageError] = "Error",
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
--[=[
|
|
42
|
-
Runs the `Code` module under `runner` and ends the playtest with the result,
|
|
43
|
-
when this playtest is the one `runner` was planted for.
|
|
44
|
-
]=]
|
|
45
|
-
function PlaytestRunner.runAsync(runner: Instance, studioTestService: any, players: Players)
|
|
46
|
-
local argsOk, testArgs = pcall(function()
|
|
47
|
-
return studioTestService:GetTestArgs()
|
|
48
|
-
end)
|
|
49
|
-
if not argsOk or type(testArgs) ~= "table" or testArgs.studioBridgeRunnerId ~= runner:GetAttribute("RunnerId") then
|
|
50
|
-
return
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
local hasEnded = false
|
|
54
|
-
local captured: { LogEntry } = {}
|
|
55
|
-
local function endTest(result: { [string]: any })
|
|
56
|
-
if hasEnded then
|
|
57
|
-
return
|
|
58
|
-
end
|
|
59
|
-
hasEnded = true
|
|
60
|
-
studioTestService:EndTest(result)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
local timeoutSec = testArgs.studioBridgeTimeoutSec
|
|
64
|
-
if type(timeoutSec) == "number" then
|
|
65
|
-
task.delay(timeoutSec, function()
|
|
66
|
-
endTest({
|
|
67
|
-
success = false,
|
|
68
|
-
error = `Script timed out after {timeoutSec}s`,
|
|
69
|
-
code = "SCRIPT_TIMEOUT",
|
|
70
|
-
output = captured,
|
|
71
|
-
})
|
|
72
|
-
end)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
-- The plugin path waits for the client to connect, so match it in play mode
|
|
76
|
-
if testArgs.studioBridgeWaitForPlayer and #players:GetPlayers() == 0 then
|
|
77
|
-
players.PlayerAdded:Wait()
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
local scriptArgs = if type(testArgs.studioBridgeScriptArgs) == "table" then testArgs.studioBridgeScriptArgs else {}
|
|
81
|
-
endTest(PlaytestRunner.run(runner:FindFirstChild("Code"), captured, scriptArgs))
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
--[=[
|
|
85
|
-
Runs a code module with `args`, collecting everything logged while it runs
|
|
86
|
-
into `captured`, and returns the result in the shape of an `execute` reply.
|
|
87
|
-
]=]
|
|
88
|
-
function PlaytestRunner.run(codeModule: Instance?, captured: { LogEntry }, args: { any }?): { [string]: any }
|
|
89
|
-
if not (codeModule and codeModule:IsA("ModuleScript")) then
|
|
90
|
-
return { success = false, error = "Runner has no Code module", code = "SCRIPT_LOAD_ERROR", output = captured }
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
local requireOk, fn = pcall(require, codeModule)
|
|
94
|
-
if not requireOk or type(fn) ~= "function" then
|
|
95
|
-
return {
|
|
96
|
-
success = false,
|
|
97
|
-
error = if requireOk then "Code module did not return a function" else tostring(fn),
|
|
98
|
-
code = "SCRIPT_LOAD_ERROR",
|
|
99
|
-
output = captured,
|
|
100
|
-
}
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
local logConnection = PlaytestRunner._captureOutput(captured)
|
|
104
|
-
local returned = table.pack(xpcall(fn, function(err)
|
|
105
|
-
return {
|
|
106
|
-
message = tostring(err),
|
|
107
|
-
traceback = PlaytestRunner._dropOwnFrames(debug.traceback(nil, 2)),
|
|
108
|
-
}
|
|
109
|
-
end, table.unpack(args or {})))
|
|
110
|
-
|
|
111
|
-
-- MessageOut can fire a frame late, after the code's last print
|
|
112
|
-
task.wait()
|
|
113
|
-
logConnection:Disconnect()
|
|
114
|
-
|
|
115
|
-
if not returned[1] then
|
|
116
|
-
return {
|
|
117
|
-
success = false,
|
|
118
|
-
error = returned[2].message,
|
|
119
|
-
traceback = returned[2].traceback,
|
|
120
|
-
code = "SCRIPT_RUNTIME_ERROR",
|
|
121
|
-
output = captured,
|
|
122
|
-
}
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
success = true,
|
|
127
|
-
output = captured,
|
|
128
|
-
returnValues = PlaytestRunner._serializeReturnValues(returned),
|
|
129
|
-
}
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
--[[
|
|
133
|
-
Drops the frames of this module's own wrapper from a traceback, leaving only
|
|
134
|
-
the script's.
|
|
135
|
-
]]
|
|
136
|
-
function PlaytestRunner._dropOwnFrames(traceback: string): string
|
|
137
|
-
if not OWN_FRAME_PREFIX then
|
|
138
|
-
return traceback
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
local lines = {}
|
|
142
|
-
for _, line in string.split(traceback, "\n") do
|
|
143
|
-
if string.sub(line, 1, #OWN_FRAME_PREFIX + 1) ~= `{OWN_FRAME_PREFIX}:` then
|
|
144
|
-
table.insert(lines, line)
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
return table.concat(lines, "\n")
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
--[[
|
|
151
|
-
Collects every message logged until the connection is disconnected, other
|
|
152
|
-
than the plugin's own.
|
|
153
|
-
]]
|
|
154
|
-
function PlaytestRunner._captureOutput(captured: { LogEntry }): RBXScriptConnection
|
|
155
|
-
return game:GetService("LogService").MessageOut:Connect(function(message: string, messageType: Enum.MessageType)
|
|
156
|
-
if string.sub(message, 1, #PLUGIN_PREFIX) == PLUGIN_PREFIX then
|
|
157
|
-
return
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
table.insert(captured, {
|
|
161
|
-
level = LOG_LEVELS[messageType] or "Print",
|
|
162
|
-
body = message,
|
|
163
|
-
timestamp = os.clock(),
|
|
164
|
-
})
|
|
165
|
-
end)
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
--[[
|
|
169
|
-
Marshals what a script returned, in order, skipping the pcall status. A nil
|
|
170
|
-
becomes an explicit marker, since an array cannot hold a hole.
|
|
171
|
-
]]
|
|
172
|
-
function PlaytestRunner._serializeReturnValues(packed: { n: number, [number]: any }): { any }
|
|
173
|
-
local returnValues = {}
|
|
174
|
-
for index = 2, packed.n do
|
|
175
|
-
local serialized = PlaytestRunner._serializeValue(packed[index], 1, {})
|
|
176
|
-
if serialized == nil then
|
|
177
|
-
serialized = { type = "Nil" }
|
|
178
|
-
end
|
|
179
|
-
returnValues[index - 1] = serialized
|
|
180
|
-
end
|
|
181
|
-
return returnValues
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
--[[
|
|
185
|
-
Produces the SerializedReturnValue shapes declared in web-socket-protocol.ts.
|
|
186
|
-
Copied from execute.lua; keep the two in step.
|
|
187
|
-
]]
|
|
188
|
-
function PlaytestRunner._serializeValue(value: any, depth: number, seen: { [any]: boolean }): any
|
|
189
|
-
local valueType = typeof(value)
|
|
190
|
-
|
|
191
|
-
if valueType == "nil" or valueType == "string" or valueType == "boolean" then
|
|
192
|
-
return value
|
|
193
|
-
elseif valueType == "number" then
|
|
194
|
-
if value ~= value or value == math.huge or value == -math.huge then
|
|
195
|
-
return PlaytestRunner._unsupported("number", tostring(value))
|
|
196
|
-
end
|
|
197
|
-
return value
|
|
198
|
-
elseif valueType == "table" then
|
|
199
|
-
if depth > MAX_DEPTH then
|
|
200
|
-
return PlaytestRunner._unsupported("table", "<max depth exceeded>")
|
|
201
|
-
end
|
|
202
|
-
if seen[value] then
|
|
203
|
-
return PlaytestRunner._unsupported("table", "<cycle>")
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
seen[value] = true
|
|
207
|
-
local serialized = PlaytestRunner._serializeTable(value, depth, seen)
|
|
208
|
-
seen[value] = nil
|
|
209
|
-
return serialized
|
|
210
|
-
elseif valueType == "Vector3" then
|
|
211
|
-
return { type = "Vector3", value = { value.X, value.Y, value.Z } }
|
|
212
|
-
elseif valueType == "Vector2" then
|
|
213
|
-
return { type = "Vector2", value = { value.X, value.Y } }
|
|
214
|
-
elseif valueType == "CFrame" then
|
|
215
|
-
return { type = "CFrame", value = { value:GetComponents() } }
|
|
216
|
-
elseif valueType == "Color3" then
|
|
217
|
-
return { type = "Color3", value = { value.R, value.G, value.B } }
|
|
218
|
-
elseif valueType == "UDim2" then
|
|
219
|
-
return { type = "UDim2", value = { value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset } }
|
|
220
|
-
elseif valueType == "UDim" then
|
|
221
|
-
return { type = "UDim", value = { value.Scale, value.Offset } }
|
|
222
|
-
elseif valueType == "BrickColor" then
|
|
223
|
-
return { type = "BrickColor", name = value.Name, value = value.Number }
|
|
224
|
-
elseif valueType == "EnumItem" then
|
|
225
|
-
return { type = "EnumItem", enum = tostring(value.EnumType), name = value.Name, value = value.Value }
|
|
226
|
-
elseif valueType == "Instance" then
|
|
227
|
-
return { type = "Instance", className = value.ClassName, path = value:GetFullName() }
|
|
228
|
-
else
|
|
229
|
-
return PlaytestRunner._unsupported(valueType, tostring(value))
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
function PlaytestRunner._serializeTable(value: { [any]: any }, depth: number, seen: { [any]: boolean }): any
|
|
234
|
-
local count = 0
|
|
235
|
-
for _ in value do
|
|
236
|
-
count += 1
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
if count == #value then
|
|
240
|
-
local array = {}
|
|
241
|
-
for index = 1, count do
|
|
242
|
-
array[index] = PlaytestRunner._serializeValue(value[index], depth + 1, seen)
|
|
243
|
-
end
|
|
244
|
-
return array
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
local map: { [string]: any } = {}
|
|
248
|
-
for key, item in value do
|
|
249
|
-
map[if type(key) == "string" then key else tostring(key)] =
|
|
250
|
-
PlaytestRunner._serializeValue(item, depth + 1, seen)
|
|
251
|
-
end
|
|
252
|
-
return map
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
function PlaytestRunner._unsupported(typeName: string, text: string): { [string]: any }
|
|
256
|
-
return { type = "Unsupported", typeName = typeName, toString = text }
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
-- Planted as a Script it runs; in the test place it is a ModuleScript specs require
|
|
260
|
-
if script:IsA("Script") then
|
|
261
|
-
task.spawn(PlaytestRunner.runAsync, script, game:GetService("StudioTestService"), game:GetService("Players"))
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
return PlaytestRunner
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
--[[
|
|
3
|
-
@class playtest-runner.spec.lua
|
|
4
|
-
]]
|
|
5
|
-
|
|
6
|
-
local require = require(script.Parent.loader).load(script)
|
|
7
|
-
|
|
8
|
-
local Jest = require("Jest")
|
|
9
|
-
local PlaytestRunner = require("playtest-runner")
|
|
10
|
-
|
|
11
|
-
local describe = Jest.Globals.describe
|
|
12
|
-
local expect = Jest.Globals.expect
|
|
13
|
-
local it = Jest.Globals.it
|
|
14
|
-
|
|
15
|
-
local function setup(testArgs: any)
|
|
16
|
-
local ended = {}
|
|
17
|
-
local runner = Instance.new("Folder")
|
|
18
|
-
runner:SetAttribute("RunnerId", "runner-1")
|
|
19
|
-
|
|
20
|
-
local studioTestService = {
|
|
21
|
-
GetTestArgs = function()
|
|
22
|
-
return testArgs
|
|
23
|
-
end,
|
|
24
|
-
EndTest = function(_self, value)
|
|
25
|
-
table.insert(ended, value)
|
|
26
|
-
end,
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
runner = runner,
|
|
31
|
-
studioTestService = studioTestService,
|
|
32
|
-
ended = ended,
|
|
33
|
-
Destroy = function()
|
|
34
|
-
runner:Destroy()
|
|
35
|
-
end,
|
|
36
|
-
}
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
describe("PlaytestRunner.runAsync()", function()
|
|
40
|
-
it("should do nothing in a playtest it was not planted for", function()
|
|
41
|
-
local controller = setup({ studioBridgeRunnerId = "someone-else" })
|
|
42
|
-
|
|
43
|
-
PlaytestRunner.runAsync(controller.runner, controller.studioTestService, game:GetService("Players"))
|
|
44
|
-
|
|
45
|
-
expect(controller.ended).toEqual({})
|
|
46
|
-
controller:Destroy()
|
|
47
|
-
end)
|
|
48
|
-
|
|
49
|
-
it("should end the playtest with a load error when the code module is missing", function()
|
|
50
|
-
local controller = setup({ studioBridgeRunnerId = "runner-1" })
|
|
51
|
-
|
|
52
|
-
PlaytestRunner.runAsync(controller.runner, controller.studioTestService, game:GetService("Players"))
|
|
53
|
-
|
|
54
|
-
expect(#controller.ended).toEqual(1)
|
|
55
|
-
expect(controller.ended[1].success).toEqual(false)
|
|
56
|
-
expect(controller.ended[1].code).toEqual("SCRIPT_LOAD_ERROR")
|
|
57
|
-
controller:Destroy()
|
|
58
|
-
end)
|
|
59
|
-
end)
|
|
60
|
-
|
|
61
|
-
describe("PlaytestRunner._serializeReturnValues()", function()
|
|
62
|
-
it("should keep a nil in its position", function()
|
|
63
|
-
local packed = table.pack(true, 1, nil, Vector3.new(1, 2, 3))
|
|
64
|
-
|
|
65
|
-
expect(PlaytestRunner._serializeReturnValues(packed)).toEqual({
|
|
66
|
-
1,
|
|
67
|
-
{ type = "Nil" },
|
|
68
|
-
{ type = "Vector3", value = { 1, 2, 3 } },
|
|
69
|
-
})
|
|
70
|
-
end)
|
|
71
|
-
end)
|