cj-tauri 2.0.0 → 2.1.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/README.md +113 -99
- package/bin/cj-tauri.js +287 -287
- package/cjpm.toml +63 -52
- package/cli/cjpm.toml +22 -22
- package/cli/src/doctor.cj +308 -306
- package/cli/src/main.cj +129 -129
- package/cli/src/manifest.cj +632 -558
- package/cli/src/manifest_test.cj +87 -0
- package/cli/src/project.cj +770 -770
- package/cli/src/resolve.cj +348 -348
- package/cli/src/scaffold.cj +369 -369
- package/cli/src/sync.cj +526 -526
- package/cli/src/util.cj +168 -168
- package/cli/templates/app/README.md +51 -48
- package/cli/templates/app/assets/capabilities/default.json +7 -7
- package/cli/templates/app/assets/ui/index.html +120 -118
- package/cli/templates/app/cj-tauri.toml +32 -22
- package/cli/templates/app/cjpm.toml +34 -34
- package/cli/templates/app/src/main.cj +101 -90
- package/cli/templates/app-react/README.md +56 -56
- package/cli/templates/app-react/assets/capabilities/default.json +7 -7
- package/cli/templates/app-react/assets/ui/index.html +25 -25
- package/cli/templates/app-react/assets/ui/package.json +19 -19
- package/cli/templates/app-react/assets/ui/src/App.css +67 -67
- package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
- package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
- package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
- package/cli/templates/app-react/cj-tauri.toml +33 -23
- package/cli/templates/app-react/cjpm.toml +34 -34
- package/cli/templates/app-react/src/main.cj +91 -91
- package/cli/templates/app-vue/README.md +56 -56
- package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
- package/cli/templates/app-vue/assets/ui/index.html +25 -25
- package/cli/templates/app-vue/assets/ui/package.json +18 -18
- package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
- package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
- package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
- package/cli/templates/app-vue/cj-tauri.toml +33 -23
- package/cli/templates/app-vue/cjpm.toml +34 -34
- package/cli/templates/app-vue/src/main.cj +91 -91
- package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
- package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
- package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
- package/cli/templates/ohos-skeleton/README.md +84 -79
- package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
- package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
- package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
- package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
- package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
- package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -167
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
- package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
- package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
- package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
- package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
- package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
- package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
- package/native/build_win.bat +62 -62
- package/native/core/bridge_core.c +1681 -1626
- package/native/core/bridge_core.h +675 -618
- package/native/core/bridge_js.h +108 -108
- package/native/platforms/harmonyos/README.md +115 -6
- package/native/platforms/linux/bridge_linux.c +2787 -2485
- package/native/platforms/windows/bridge_win.c +3740 -3248
- package/native/tests/gtk_threading_probe.c +143 -143
- package/native/tests/test_bridge_core.c +1339 -1228
- package/package.json +53 -53
- package/prebuilt/linux-x64/cj-tauri +0 -0
- package/src/api_system.cj +67 -67
- package/src/app.cj +325 -969
- package/src/app_events.cj +166 -0
- package/src/app_host_ops.cj +287 -0
- package/src/app_run.cj +283 -0
- package/src/app_spec.cj +76 -76
- package/src/arkweb_menu.cj +216 -0
- package/src/arkweb_script.cj +142 -142
- package/src/capability.cj +409 -409
- package/src/clipboard_bridge.cj +178 -0
- package/src/dialog_spec.cj +79 -0
- package/src/dragdrop.cj +26 -26
- package/src/embedded_assets.cj +98 -98
- package/src/host.cj +752 -607
- package/src/host_arkweb.cj +924 -778
- package/src/host_webkit.cj +663 -570
- package/src/host_webview2.cj +663 -570
- package/src/hotkey.cj +66 -66
- package/src/ipc_hub.cj +337 -337
- package/src/ipc_message.cj +286 -286
- package/src/launch.cj +62 -62
- package/src/logging.cj +127 -127
- package/src/menu.cj +197 -197
- package/src/platform/platform_app_context.cj +77 -77
- package/src/platform/platform_assets.cj +33 -33
- package/src/platform/platform_discover.cj +176 -176
- package/src/platform/platform_env.cj +29 -29
- package/src/platform/platform_feature.cj +31 -31
- package/src/platform/platform_page_source.cj +87 -87
- package/src/platform/platform_target_info.cj +32 -32
- package/src/platform_host_provider.cj +87 -87
- package/src/plugin.cj +333 -333
- package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
- package/src/plugins/dialog/plugin_dialog.cj +209 -209
- package/src/plugins/fs/plugin_fs.cj +105 -1
- package/src/plugins/http/http_backend.cj +131 -0
- package/src/plugins/http/http_desktop.cj +195 -0
- package/src/plugins/http/http_ohos.cj +204 -0
- package/src/plugins/http/plugin_http.cj +310 -459
- package/src/plugins/log/plugin_log.cj +105 -105
- package/src/plugins/menu/plugin_menu.cj +158 -158
- package/src/plugins/notification/plugin_notification.cj +154 -154
- package/src/plugins/serial/plugin_serial.cj +458 -454
- package/src/plugins/serial/serial_bridge.cj +194 -194
- package/src/plugins/serial/serial_manager_ohos.cj +92 -0
- package/src/plugins/serial/serial_ohos.cj +529 -0
- package/src/plugins/shell/plugin_shell.cj +258 -258
- package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
- package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
- package/src/plugins/ws/plugin_ws.cj +488 -470
- package/src/process_hidden.cj +174 -174
- package/src/scope.cj +391 -391
- package/src/shell_event.cj +69 -69
- package/src/tests/app_spec_test.cj +126 -127
- package/src/tests/capability_test.cj +243 -243
- package/src/tests/dialog_spec_test.cj +73 -0
- package/src/tests/fake_host.cj +201 -201
- package/src/tests/ipc_hub_test.cj +487 -487
- package/src/tests/logging_test.cj +62 -62
- package/src/tests/platform_contract_test.cj +251 -251
- package/src/tests/plugin_clipboard_test.cj +222 -222
- package/src/tests/plugin_dialog_test.cj +237 -237
- package/src/tests/plugin_fs_test.cj +207 -0
- package/src/tests/plugin_http_test.cj +124 -124
- package/src/tests/plugin_notification_test.cj +205 -205
- package/src/tests/plugin_serial_test.cj +48 -0
- package/src/tests/plugin_test.cj +605 -585
- package/src/tests/plugin_ws_test.cj +40 -40
- package/src/tests/process_hidden_test.cj +129 -129
- package/src/tests/scope_test.cj +350 -350
- package/src/tests/system_host_test.cj +99 -81
- package/src/tests/urlutil_test.cj +53 -53
- package/src/tests/version_test.cj +75 -75
- package/src/tests/window_registry_test.cj +123 -123
- package/src/tray.cj +90 -90
- package/src/urlutil.cj +159 -159
- package/src/version.cj +44 -44
- package/src/window.cj +51 -51
- package/src/window_registry.cj +116 -116
- package/prebuilt/win32-x64/cj-tauri.exe +0 -0
- package/src/plugins/serial/serial_ohos_stub.cj +0 -61
- package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
|
@@ -1,487 +1,487 @@
|
|
|
1
|
-
package cjTauri.tests
|
|
2
|
-
|
|
3
|
-
import cjTauri.*
|
|
4
|
-
import std.collection.*
|
|
5
|
-
import std.sync.*
|
|
6
|
-
import stdx.encoding.json.*
|
|
7
|
-
import std.unittest.*
|
|
8
|
-
import std.unittest.testmacro.*
|
|
9
|
-
|
|
10
|
-
/*
|
|
11
|
-
* IPC 链路单元测试:解析 → 能力校验 → 分发 → 结果/错误回写 → 事件投递。
|
|
12
|
-
* 这些用例不碰宿主层(不创建窗口),纯逻辑,可在无 GUI 的环境跑。
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/** 固定返回 "ok" 的处理器 */
|
|
16
|
-
class OkHandler <: CommandHandler {
|
|
17
|
-
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
18
|
-
let _ = cmd
|
|
19
|
-
let _ = args
|
|
20
|
-
let _ = ipc
|
|
21
|
-
return JsonString("ok")
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** 抛 CommandException 的处理器(预期内的失败) */
|
|
26
|
-
class BoomHandler <: CommandHandler {
|
|
27
|
-
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
28
|
-
let _ = cmd
|
|
29
|
-
let _ = args
|
|
30
|
-
let _ = ipc
|
|
31
|
-
throw CommandException("boom")
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** 抛普通异常的处理器(预期外的失败) */
|
|
36
|
-
class CrashHandler <: CommandHandler {
|
|
37
|
-
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
38
|
-
let _ = cmd
|
|
39
|
-
let _ = args
|
|
40
|
-
let _ = ipc
|
|
41
|
-
throw Exception("crash")
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** 取 JSON 顶层字符串字段(缺失返回空串) */
|
|
46
|
-
func strField(json: String, key: String): String {
|
|
47
|
-
let o = JsonValue.fromStr(json).asObject()
|
|
48
|
-
if (let Some(x) <- o.get(key)) {
|
|
49
|
-
return x.asString().getValue()
|
|
50
|
-
}
|
|
51
|
-
return ""
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** 取 JSON 顶层布尔字段(缺失返回 false) */
|
|
55
|
-
func boolField(json: String, key: String): Bool {
|
|
56
|
-
let o = JsonValue.fromStr(json).asObject()
|
|
57
|
-
if (let Some(x) <- o.get(key)) {
|
|
58
|
-
return x.asBool().getValue()
|
|
59
|
-
}
|
|
60
|
-
return false
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/*
|
|
64
|
-
* 命令执行自「异步分发」改造起跑在 worker 线程上(见 IpcHub.handleInvoke),于是用例里出现两侧并发:
|
|
65
|
-
* worker 线程往 sink 里写,用例线程读。两边都过同一把锁——既避免读写竞态,也借锁的释放/获取建立可见性
|
|
66
|
-
* (没有同步点的话,写完的那侧不保证被读到)。
|
|
67
|
-
*/
|
|
68
|
-
let TEST_SINK_LOCK = Mutex()
|
|
69
|
-
|
|
70
|
-
/** 往用例 sink 追加一条(加锁;hub 的 jsSink 与断言前的等待都走它建立的顺序) */
|
|
71
|
-
func addToTestSink(sink: ArrayList<String>, s: String): Unit {
|
|
72
|
-
synchronized(TEST_SINK_LOCK) {
|
|
73
|
-
sink.add(s)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* 等 sink 至少收到 n 条消息(最多等 timeoutMs 毫秒后放弃,让断言自己报失败而不是挂死)。
|
|
79
|
-
* 「已放行且已注册」的调用是异步的,断言前必须先等;「未授权 / 未注册」是同步拒绝,不必等。
|
|
80
|
-
*/
|
|
81
|
-
func waitSink(sink: ArrayList<String>, n: Int64, timeoutMs!: Int64 = 10000): Unit {
|
|
82
|
-
var waited: Int64 = 0
|
|
83
|
-
while (waited < timeoutMs) {
|
|
84
|
-
synchronized(TEST_SINK_LOCK) {
|
|
85
|
-
if (sink.size >= n) {
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
sleep(Duration.millisecond * 5)
|
|
90
|
-
waited += 5
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** 组装一个「只放行指定命令/事件」的 hub,sink 收集输出 */
|
|
95
|
-
func hubWith(commands: Array<String>, events: Array<String>, sink: ArrayList<String>): IpcHub {
|
|
96
|
-
let hub = IpcHub()
|
|
97
|
-
let cap = Capability("test")
|
|
98
|
-
cap.allowWindow("main")
|
|
99
|
-
for (c in commands) {
|
|
100
|
-
cap.allowCommand(c)
|
|
101
|
-
}
|
|
102
|
-
for (e in events) {
|
|
103
|
-
cap.allowEvent(e)
|
|
104
|
-
}
|
|
105
|
-
let reg = CapabilityRegistry()
|
|
106
|
-
reg.add(cap)
|
|
107
|
-
hub.setCapabilities(reg)
|
|
108
|
-
// 第二个参数是目标窗口 label:单窗口用例只关心「有没有投出去」,路由由 TauriApp 层的用例覆盖
|
|
109
|
-
hub.jsSink = { s, _ => addToTestSink(sink, s) }
|
|
110
|
-
return hub
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
@Test
|
|
114
|
-
func testInvokeRequestParseOk(): Unit {
|
|
115
|
-
let req = InvokeRequest.parse("{\"type\":\"invoke\",\"id\":7,\"cmd\":\"greet\",\"args\":{\"who\":\"cj\"}}")
|
|
116
|
-
@Expect(req.isSome(), true)
|
|
117
|
-
let r = req.getOrThrow()
|
|
118
|
-
@Expect(r.id, 7)
|
|
119
|
-
@Expect(r.cmd, "greet")
|
|
120
|
-
@Expect(r.args.get("who").getOrThrow().asString().getValue(), "cj")
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
@Test
|
|
124
|
-
func testInvokeRequestParseToleratesMissingArgs(): Unit {
|
|
125
|
-
let req = InvokeRequest.parse("{\"type\":\"invoke\",\"id\":1,\"cmd\":\"system:ping\"}")
|
|
126
|
-
@Expect(req.isSome(), true)
|
|
127
|
-
@Expect(req.getOrThrow().args.size(), 0)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
@Test
|
|
131
|
-
func testInvokeRequestParseRejectsMalformed(): Unit {
|
|
132
|
-
// 非 JSON
|
|
133
|
-
@Expect(InvokeRequest.parse("not json").isNone(), true)
|
|
134
|
-
// 顶层不是对象
|
|
135
|
-
@Expect(InvokeRequest.parse("[1,2,3]").isNone(), true)
|
|
136
|
-
// type 不是 invoke:emit 报文由 EmitRequest.parse 接走,这边照样是 None
|
|
137
|
-
@Expect(InvokeRequest.parse("{\"type\":\"emit\",\"id\":1,\"event\":\"x\"}").isNone(), true)
|
|
138
|
-
// 缺 cmd
|
|
139
|
-
@Expect(InvokeRequest.parse("{\"type\":\"invoke\",\"id\":1}").isNone(), true)
|
|
140
|
-
// 缺 id
|
|
141
|
-
@Expect(InvokeRequest.parse("{\"type\":\"invoke\",\"cmd\":\"greet\"}").isNone(), true)
|
|
142
|
-
// 缺 type
|
|
143
|
-
@Expect(InvokeRequest.parse("{\"id\":1,\"cmd\":\"greet\"}").isNone(), true)
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
@Test
|
|
147
|
-
func testHubResolvesAllowedCommand(): Unit {
|
|
148
|
-
let out = ArrayList<String>()
|
|
149
|
-
let hub = hubWith(["greet"], [], out)
|
|
150
|
-
hub.register("greet", OkHandler())
|
|
151
|
-
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":3,\"cmd\":\"greet\"}")
|
|
152
|
-
waitSink(out, 1)
|
|
153
|
-
@Expect(out.size, 1)
|
|
154
|
-
@Expect(strField(out[0], "type"), "resolve")
|
|
155
|
-
@Expect(strField(out[0], "data"), "ok")
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
@Test
|
|
159
|
-
func testHubRejectsCommandNotAllowed(): Unit {
|
|
160
|
-
let out = ArrayList<String>()
|
|
161
|
-
let hub = hubWith(["system:ping"], [], out)
|
|
162
|
-
hub.register("greet", OkHandler())
|
|
163
|
-
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":4,\"cmd\":\"greet\"}")
|
|
164
|
-
// 未授权在**调用线程上同步拒绝**:不必等 worker,调用返回即可见
|
|
165
|
-
@Expect(out.size, 1)
|
|
166
|
-
@Expect(strField(out[0], "error"), "command not allowed: greet")
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
@Test
|
|
170
|
-
func testHubRejectsUnregisteredCommand(): Unit {
|
|
171
|
-
let out = ArrayList<String>()
|
|
172
|
-
// 放行了但没注册:与「未放行」是两条不同的报错,容易混淆
|
|
173
|
-
let hub = hubWith(["ghost"], [], out)
|
|
174
|
-
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":5,\"cmd\":\"ghost\"}")
|
|
175
|
-
// 未注册同样是同步拒绝
|
|
176
|
-
@Expect(out.size, 1)
|
|
177
|
-
@Expect(strField(out[0], "error"), "command not registered: ghost")
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
@Test
|
|
181
|
-
func testHubReportsCommandException(): Unit {
|
|
182
|
-
let out = ArrayList<String>()
|
|
183
|
-
let hub = hubWith(["boom"], [], out)
|
|
184
|
-
hub.register("boom", BoomHandler())
|
|
185
|
-
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":6,\"cmd\":\"boom\"}")
|
|
186
|
-
waitSink(out, 1)
|
|
187
|
-
@Expect(out.size, 1)
|
|
188
|
-
@Expect(strField(out[0], "error"), "boom")
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
@Test
|
|
192
|
-
func testHubReportsUnexpectedException(): Unit {
|
|
193
|
-
let out = ArrayList<String>()
|
|
194
|
-
let hub = hubWith(["crash"], [], out)
|
|
195
|
-
hub.register("crash", CrashHandler())
|
|
196
|
-
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":8,\"cmd\":\"crash\"}")
|
|
197
|
-
waitSink(out, 1)
|
|
198
|
-
@Expect(out.size, 1)
|
|
199
|
-
// 预期外的异常统一加前缀,前端据此区分「业务拒绝」与「崩了」
|
|
200
|
-
@Expect(strField(out[0], "error").startsWith("command failed:"), true)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
@Test
|
|
204
|
-
func testHubIgnoresMessageWithoutId(): Unit {
|
|
205
|
-
let out = ArrayList<String>()
|
|
206
|
-
let hub = hubWith(["greet"], ["tick"], out)
|
|
207
|
-
hub.register("greet", OkHandler())
|
|
208
|
-
// 结构坏掉且**读不出 id**:对应不到任何 promise,只能 stderr(这是设计,不是遗留问题)
|
|
209
|
-
hub.handleRawMessage("garbage")
|
|
210
|
-
hub.handleRawMessage("{\"type\":\"emit\",\"event\":\"tick\"}")
|
|
211
|
-
hub.handleRawMessage("{\"type\":\"invoke\"}")
|
|
212
|
-
@Expect(out.size, 0)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
@Test
|
|
216
|
-
func testEmitRequiresDeclaredEvent(): Unit {
|
|
217
|
-
let denied = ArrayList<String>()
|
|
218
|
-
let hubDenied = hubWith(["greet"], [], denied)
|
|
219
|
-
hubDenied.emitToWindow("tick", JsonObject(), "main")
|
|
220
|
-
@Expect(denied.size, 0)
|
|
221
|
-
|
|
222
|
-
let allowed = ArrayList<String>()
|
|
223
|
-
let hubAllowed = hubWith(["greet"], ["tick"], allowed)
|
|
224
|
-
hubAllowed.emitToWindow("tick", JsonObject(), "main")
|
|
225
|
-
@Expect(allowed.size, 1)
|
|
226
|
-
@Expect(strField(allowed[0], "type"), "event")
|
|
227
|
-
@Expect(strField(allowed[0], "event"), "tick")
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
@Test
|
|
231
|
-
func testIpcMessageResolveKeepsPayload(): Unit {
|
|
232
|
-
var payload = JsonObject()
|
|
233
|
-
payload.put("n", JsonInt(1))
|
|
234
|
-
let msg = IpcMessage.resolve(9, payload)
|
|
235
|
-
@Expect(strField(msg, "type"), "resolve")
|
|
236
|
-
let o = JsonValue.fromStr(msg).asObject()
|
|
237
|
-
@Expect(o.get("data").getOrThrow().asObject().get("n").getOrThrow().asInt().getValue(), 1)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/*
|
|
241
|
-
* 以下是「异步分发」的行为契约(命令执行跑在 worker 线程上,见 IpcHub.handleInvoke):
|
|
242
|
-
* 调用线程只做校验并立刻返回,慢命令不再把宿主 UI 线程钉住。
|
|
243
|
-
*/
|
|
244
|
-
|
|
245
|
-
/** 睡 300ms 再回答:模拟 `shell:exec` 这类慢命令 */
|
|
246
|
-
class SlowHandler <: CommandHandler {
|
|
247
|
-
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
248
|
-
let _ = cmd
|
|
249
|
-
let _ = args
|
|
250
|
-
let _ = ipc
|
|
251
|
-
sleep(Duration.millisecond * 300)
|
|
252
|
-
return JsonString("slow-ok")
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
@Test
|
|
257
|
-
func testDispatchReturnsBeforeSlowCommandFinishes(): Unit {
|
|
258
|
-
let out = ArrayList<String>()
|
|
259
|
-
let hub = hubWith(["slow"], [], out)
|
|
260
|
-
hub.register("slow", SlowHandler())
|
|
261
|
-
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":41,\"cmd\":\"slow\"}")
|
|
262
|
-
// 关键:handler 还在睡,调用就已经返回了 —— 这就是「UI 线程不被命令阻塞」的单元级证据
|
|
263
|
-
// (handler 睡 300ms,这两行断言在微秒级完成,不存在轮到 worker 跑完的可能)
|
|
264
|
-
@Expect(out.size, 0)
|
|
265
|
-
waitSink(out, 1)
|
|
266
|
-
@Expect(strField(out[0], "type"), "resolve")
|
|
267
|
-
@Expect(strField(out[0], "data"), "slow-ok")
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
@Test
|
|
271
|
-
func testRejectsStaySynchronous(): Unit {
|
|
272
|
-
let out = ArrayList<String>()
|
|
273
|
-
let hub = hubWith(["allowed"], [], out)
|
|
274
|
-
// 未授权:调用线程上同步回写,调用一返回就可见,不需要等
|
|
275
|
-
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":42,\"cmd\":\"nope\"}")
|
|
276
|
-
@Expect(out.size, 1)
|
|
277
|
-
@Expect(strField(out[0], "error"), "command not allowed: nope")
|
|
278
|
-
// 未注册:同样同步(放行了但没注册,两条报错别混)
|
|
279
|
-
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":43,\"cmd\":\"allowed\"}")
|
|
280
|
-
@Expect(out.size, 2)
|
|
281
|
-
@Expect(strField(out[1], "error"), "command not registered: allowed")
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
@Test
|
|
285
|
-
func testConcurrentInvokesEachResolveExactlyOnce(): Unit {
|
|
286
|
-
let out = ArrayList<String>()
|
|
287
|
-
let hub = hubWith(["slow"], [], out)
|
|
288
|
-
hub.register("slow", SlowHandler())
|
|
289
|
-
// 同一命令连发 6 次:**不保证执行顺序**(各自独立跑,谁先完谁先回),但每条 id 必须有且只有一条响应
|
|
290
|
-
for (i in 50..56) {
|
|
291
|
-
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":${i},\"cmd\":\"slow\"}")
|
|
292
|
-
}
|
|
293
|
-
waitSink(out, 6, timeoutMs: 20000)
|
|
294
|
-
@Expect(out.size, 6)
|
|
295
|
-
let ids = ArrayList<Int64>()
|
|
296
|
-
for (m in out) {
|
|
297
|
-
@Expect(strField(m, "type"), "resolve")
|
|
298
|
-
ids.add(JsonValue.fromStr(m).asObject().get("id").getOrThrow().asInt().getValue())
|
|
299
|
-
}
|
|
300
|
-
for (i in 50..56) {
|
|
301
|
-
var hits = 0
|
|
302
|
-
for (id in ids) {
|
|
303
|
-
if (id == i) {
|
|
304
|
-
hits += 1
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
// 既不丢(0)也不重复(2):并发下每条 invoke 恰好被回投一次
|
|
308
|
-
@Expect(hits, 1)
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/*
|
|
313
|
-
* 前端 emit(P1-4 缺口):只投后端监听器、不回投页面;未授权与非法报文的失败都要回给前端。
|
|
314
|
-
* 与 Tauri v2 的差异是有意选的(见 EmitRequest 注释 / docs/IPC-通信机制.md §7)。
|
|
315
|
-
*/
|
|
316
|
-
|
|
317
|
-
/** 抛异常的监听器:验证「一个听众崩了不牵连别的听众与发送方」 */
|
|
318
|
-
func boomListener(p: JsonValue): Unit {
|
|
319
|
-
let _ = p
|
|
320
|
-
throw Exception("listener boom")
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
@Test
|
|
324
|
-
func testEmitRequestParseOk(): Unit {
|
|
325
|
-
let req = EmitRequest.parse("{\"type\":\"emit\",\"id\":9,\"event\":\"tick\",\"payload\":{\"n\":1}}")
|
|
326
|
-
@Expect(req.isSome(), true)
|
|
327
|
-
let r = req.getOrThrow()
|
|
328
|
-
@Expect(r.id, 9)
|
|
329
|
-
@Expect(r.event, "tick")
|
|
330
|
-
@Expect(r.payload.asObject().get("n").getOrThrow().asInt().getValue(), 1)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
@Test
|
|
334
|
-
func testEmitRequestParseDefaultsPayloadAndRejectsMalformed(): Unit {
|
|
335
|
-
// 前端 emit('tick') 不带第二个参数:payload 缺省成空对象
|
|
336
|
-
let bare = EmitRequest.parse("{\"type\":\"emit\",\"id\":1,\"event\":\"tick\"}")
|
|
337
|
-
@Expect(bare.isSome(), true)
|
|
338
|
-
@Expect(bare.getOrThrow().payload.asObject().size(), 0)
|
|
339
|
-
// 缺 id / 缺 event / 类型不对,都不是合法 emit 报文
|
|
340
|
-
@Expect(EmitRequest.parse("{\"type\":\"emit\",\"event\":\"tick\"}").isNone(), true)
|
|
341
|
-
@Expect(EmitRequest.parse("{\"type\":\"emit\",\"id\":1}").isNone(), true)
|
|
342
|
-
@Expect(EmitRequest.parse("{\"type\":\"invoke\",\"id\":1,\"cmd\":\"greet\"}").isNone(), true)
|
|
343
|
-
@Expect(EmitRequest.parse("not json").isNone(), true)
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
@Test
|
|
347
|
-
func testFrontendEmitReachesBackendListener(): Unit {
|
|
348
|
-
let out = ArrayList<String>()
|
|
349
|
-
let hub = hubWith([], ["tick"], out)
|
|
350
|
-
let seen = ArrayList<Int64>()
|
|
351
|
-
hub.listenEvent("tick", { p =>
|
|
352
|
-
synchronized(TEST_SINK_LOCK) {
|
|
353
|
-
seen.add(p.asObject().get("n").getOrThrow().asInt().getValue())
|
|
354
|
-
}
|
|
355
|
-
})
|
|
356
|
-
hub.handleRawMessage("{\"type\":\"emit\",\"id\":21,\"event\":\"tick\",\"payload\":{\"n\":7}}")
|
|
357
|
-
waitSink(out, 1)
|
|
358
|
-
@Expect(seen.size, 1)
|
|
359
|
-
@Expect(seen[0], 7)
|
|
360
|
-
@Expect(out.size, 1)
|
|
361
|
-
@Expect(strField(out[0], "type"), "resolve")
|
|
362
|
-
@Expect(boolField(out[0], "ok"), true)
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
@Test
|
|
366
|
-
func testAllListenersOfEventAreCalled(): Unit {
|
|
367
|
-
let out = ArrayList<String>()
|
|
368
|
-
let hub = hubWith([], ["tick"], out)
|
|
369
|
-
let hits = ArrayList<String>()
|
|
370
|
-
hub.listenEvent("tick", { _ => synchronized(TEST_SINK_LOCK) { hits.add("a") } })
|
|
371
|
-
hub.listenEvent("tick", { _ => synchronized(TEST_SINK_LOCK) { hits.add("b") } })
|
|
372
|
-
hub.handleRawMessage("{\"type\":\"emit\",\"id\":23,\"event\":\"tick\",\"payload\":{}}")
|
|
373
|
-
waitSink(out, 1)
|
|
374
|
-
@Expect(hits.size, 2)
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
@Test
|
|
378
|
-
func testFailingListenerDoesNotBreakOthers(): Unit {
|
|
379
|
-
let out = ArrayList<String>()
|
|
380
|
-
let hub = hubWith([], ["tick"], out)
|
|
381
|
-
hub.listenEvent("tick", boomListener)
|
|
382
|
-
let ok = ArrayList<Bool>()
|
|
383
|
-
hub.listenEvent("tick", { _ => synchronized(TEST_SINK_LOCK) { ok.add(true) } })
|
|
384
|
-
hub.handleRawMessage("{\"type\":\"emit\",\"id\":24,\"event\":\"tick\",\"payload\":{}}")
|
|
385
|
-
waitSink(out, 1)
|
|
386
|
-
// 事件是广播语义:一个听众抛错不该牵连其它听众,也不该让发送方收到失败
|
|
387
|
-
@Expect(ok.size, 1)
|
|
388
|
-
@Expect(boolField(out[0], "ok"), true)
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
@Test
|
|
392
|
-
func testFrontendEmitDeniedEventIsRejected(): Unit {
|
|
393
|
-
let out = ArrayList<String>()
|
|
394
|
-
let hub = hubWith([], ["tick"], out)
|
|
395
|
-
// 未授权的事件:与命令一样在调用线程上同步拒绝,不必等 worker
|
|
396
|
-
hub.handleRawMessage("{\"type\":\"emit\",\"id\":22,\"event\":\"nope\",\"payload\":{}}")
|
|
397
|
-
@Expect(out.size, 1)
|
|
398
|
-
@Expect(boolField(out[0], "ok"), false)
|
|
399
|
-
@Expect(strField(out[0], "error"), "event not allowed: nope")
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
@Test
|
|
403
|
-
func testFrontendEmitWithoutListenerStillResolves(): Unit {
|
|
404
|
-
let out = ArrayList<String>()
|
|
405
|
-
let hub = hubWith([], ["tick"], out)
|
|
406
|
-
// 授权了但没人听:不是错误(Tauri 同样不报),发送方照常拿到 resolve
|
|
407
|
-
hub.handleRawMessage("{\"type\":\"emit\",\"id\":25,\"event\":\"tick\",\"payload\":{}}")
|
|
408
|
-
waitSink(out, 1)
|
|
409
|
-
@Expect(out.size, 1)
|
|
410
|
-
@Expect(boolField(out[0], "ok"), true)
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
@Test
|
|
414
|
-
func testMalformedMessageWithIdIsRejected(): Unit {
|
|
415
|
-
let out = ArrayList<String>()
|
|
416
|
-
let hub = hubWith([], ["tick"], out)
|
|
417
|
-
// 未知报文类型但 id 可读:回一条 reject,前端那条 promise 不再永久 pending
|
|
418
|
-
hub.handleRawMessage("{\"type\":\"wat\",\"id\":31}")
|
|
419
|
-
@Expect(out.size, 1)
|
|
420
|
-
@Expect(boolField(out[0], "ok"), false)
|
|
421
|
-
@Expect(strField(out[0], "error"), "invalid IPC message")
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
@Test
|
|
425
|
-
func testEventCarriesWindowLabelOrNothingWhenBroadcast(): Unit {
|
|
426
|
-
let out = ArrayList<String>()
|
|
427
|
-
let hub = hubWith([], ["tick"], out)
|
|
428
|
-
hub.emitToWindow("tick", JsonObject(), "main")
|
|
429
|
-
@Expect(strField(out[0], "window"), "main")
|
|
430
|
-
hub.emitAll("tick", JsonObject())
|
|
431
|
-
// 广播不带 window 字段:页面里由桥放行(多窗口过滤靠它)
|
|
432
|
-
@Expect(JsonValue.fromStr(out[1]).asObject().get("window").isNone(), true)
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/*
|
|
436
|
-
* emitLatest:显式 opt-in 的「同事件同帧只投最新值」。
|
|
437
|
-
* 报文侧只该多一个 coalesce:true,页面侧的合并(一帧一次、只投尾值)由桥 JS 做,
|
|
438
|
-
* 所以单元测试能钉住的是「报文契约」与「能力白名单」两条;合并行为本身靠实机探针取证
|
|
439
|
-
* (见 docs/IPC-通信机制.md §6 第 5 项)。
|
|
440
|
-
*/
|
|
441
|
-
|
|
442
|
-
@Test
|
|
443
|
-
func testEmitLatestMarksCoalesceAndKeepsPayload(): Unit {
|
|
444
|
-
let out = ArrayList<String>()
|
|
445
|
-
let hub = hubWith([], ["progress"], out)
|
|
446
|
-
var payload = JsonObject()
|
|
447
|
-
payload.put("n", JsonInt(3))
|
|
448
|
-
hub.emitLatest("progress", payload, "")
|
|
449
|
-
@Expect(out.size, 1)
|
|
450
|
-
@Expect(strField(out[0], "type"), "event")
|
|
451
|
-
@Expect(strField(out[0], "event"), "progress")
|
|
452
|
-
@Expect(boolField(out[0], "coalesce"), true)
|
|
453
|
-
let n = JsonValue.fromStr(out[0]).asObject().get("payload").getOrThrow().asObject().get("n").getOrThrow().asInt().getValue()
|
|
454
|
-
@Expect(n, 3)
|
|
455
|
-
// 广播不带 window 字段(与 emitAll 一致)
|
|
456
|
-
@Expect(JsonValue.fromStr(out[0]).asObject().get("window").isNone(), true)
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
@Test
|
|
460
|
-
func testEmitLatestCarriesWindowLabelWhenTargeted(): Unit {
|
|
461
|
-
let out = ArrayList<String>()
|
|
462
|
-
let hub = hubWith([], ["progress"], out)
|
|
463
|
-
hub.emitLatest("progress", JsonObject(), "main")
|
|
464
|
-
@Expect(strField(out[0], "window"), "main")
|
|
465
|
-
@Expect(boolField(out[0], "coalesce"), true)
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
@Test
|
|
469
|
-
func testEmitLatestRespectsCapabilityDeny(): Unit {
|
|
470
|
-
let out = ArrayList<String>()
|
|
471
|
-
let hub = hubWith([], ["other"], out)
|
|
472
|
-
// 未授权的事件与 emit 同一套白名单:不投递,也不能因为「只是合并语义」就绕过声明
|
|
473
|
-
hub.emitLatest("progress", JsonObject(), "")
|
|
474
|
-
@Expect(out.size, 0)
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
@Test
|
|
478
|
-
func testPlainEmitHasNoCoalesceField(): Unit {
|
|
479
|
-
let out = ArrayList<String>()
|
|
480
|
-
let hub = hubWith([], ["tick"], out)
|
|
481
|
-
hub.emitAll("tick", JsonObject())
|
|
482
|
-
// 默认路径不做合并:报文里不能出现 coalesce 字段(合并必须显式 opt-in)
|
|
483
|
-
@Expect(JsonValue.fromStr(out[0]).asObject().get("coalesce").isNone(), true)
|
|
484
|
-
hub.emitToWindow("tick", JsonObject(), "main")
|
|
485
|
-
@Expect(JsonValue.fromStr(out[1]).asObject().get("coalesce").isNone(), true)
|
|
486
|
-
@Expect(strField(out[1], "window"), "main")
|
|
487
|
-
}
|
|
1
|
+
package cjTauri.tests
|
|
2
|
+
|
|
3
|
+
import cjTauri.*
|
|
4
|
+
import std.collection.*
|
|
5
|
+
import std.sync.*
|
|
6
|
+
import stdx.encoding.json.*
|
|
7
|
+
import std.unittest.*
|
|
8
|
+
import std.unittest.testmacro.*
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* IPC 链路单元测试:解析 → 能力校验 → 分发 → 结果/错误回写 → 事件投递。
|
|
12
|
+
* 这些用例不碰宿主层(不创建窗口),纯逻辑,可在无 GUI 的环境跑。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** 固定返回 "ok" 的处理器 */
|
|
16
|
+
class OkHandler <: CommandHandler {
|
|
17
|
+
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
18
|
+
let _ = cmd
|
|
19
|
+
let _ = args
|
|
20
|
+
let _ = ipc
|
|
21
|
+
return JsonString("ok")
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 抛 CommandException 的处理器(预期内的失败) */
|
|
26
|
+
class BoomHandler <: CommandHandler {
|
|
27
|
+
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
28
|
+
let _ = cmd
|
|
29
|
+
let _ = args
|
|
30
|
+
let _ = ipc
|
|
31
|
+
throw CommandException("boom")
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 抛普通异常的处理器(预期外的失败) */
|
|
36
|
+
class CrashHandler <: CommandHandler {
|
|
37
|
+
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
38
|
+
let _ = cmd
|
|
39
|
+
let _ = args
|
|
40
|
+
let _ = ipc
|
|
41
|
+
throw Exception("crash")
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 取 JSON 顶层字符串字段(缺失返回空串) */
|
|
46
|
+
func strField(json: String, key: String): String {
|
|
47
|
+
let o = JsonValue.fromStr(json).asObject()
|
|
48
|
+
if (let Some(x) <- o.get(key)) {
|
|
49
|
+
return x.asString().getValue()
|
|
50
|
+
}
|
|
51
|
+
return ""
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 取 JSON 顶层布尔字段(缺失返回 false) */
|
|
55
|
+
func boolField(json: String, key: String): Bool {
|
|
56
|
+
let o = JsonValue.fromStr(json).asObject()
|
|
57
|
+
if (let Some(x) <- o.get(key)) {
|
|
58
|
+
return x.asBool().getValue()
|
|
59
|
+
}
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/*
|
|
64
|
+
* 命令执行自「异步分发」改造起跑在 worker 线程上(见 IpcHub.handleInvoke),于是用例里出现两侧并发:
|
|
65
|
+
* worker 线程往 sink 里写,用例线程读。两边都过同一把锁——既避免读写竞态,也借锁的释放/获取建立可见性
|
|
66
|
+
* (没有同步点的话,写完的那侧不保证被读到)。
|
|
67
|
+
*/
|
|
68
|
+
let TEST_SINK_LOCK = Mutex()
|
|
69
|
+
|
|
70
|
+
/** 往用例 sink 追加一条(加锁;hub 的 jsSink 与断言前的等待都走它建立的顺序) */
|
|
71
|
+
func addToTestSink(sink: ArrayList<String>, s: String): Unit {
|
|
72
|
+
synchronized(TEST_SINK_LOCK) {
|
|
73
|
+
sink.add(s)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 等 sink 至少收到 n 条消息(最多等 timeoutMs 毫秒后放弃,让断言自己报失败而不是挂死)。
|
|
79
|
+
* 「已放行且已注册」的调用是异步的,断言前必须先等;「未授权 / 未注册」是同步拒绝,不必等。
|
|
80
|
+
*/
|
|
81
|
+
func waitSink(sink: ArrayList<String>, n: Int64, timeoutMs!: Int64 = 10000): Unit {
|
|
82
|
+
var waited: Int64 = 0
|
|
83
|
+
while (waited < timeoutMs) {
|
|
84
|
+
synchronized(TEST_SINK_LOCK) {
|
|
85
|
+
if (sink.size >= n) {
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
sleep(Duration.millisecond * 5)
|
|
90
|
+
waited += 5
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 组装一个「只放行指定命令/事件」的 hub,sink 收集输出 */
|
|
95
|
+
func hubWith(commands: Array<String>, events: Array<String>, sink: ArrayList<String>): IpcHub {
|
|
96
|
+
let hub = IpcHub()
|
|
97
|
+
let cap = Capability("test")
|
|
98
|
+
cap.allowWindow("main")
|
|
99
|
+
for (c in commands) {
|
|
100
|
+
cap.allowCommand(c)
|
|
101
|
+
}
|
|
102
|
+
for (e in events) {
|
|
103
|
+
cap.allowEvent(e)
|
|
104
|
+
}
|
|
105
|
+
let reg = CapabilityRegistry()
|
|
106
|
+
reg.add(cap)
|
|
107
|
+
hub.setCapabilities(reg)
|
|
108
|
+
// 第二个参数是目标窗口 label:单窗口用例只关心「有没有投出去」,路由由 TauriApp 层的用例覆盖
|
|
109
|
+
hub.jsSink = { s, _ => addToTestSink(sink, s) }
|
|
110
|
+
return hub
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@Test
|
|
114
|
+
func testInvokeRequestParseOk(): Unit {
|
|
115
|
+
let req = InvokeRequest.parse("{\"type\":\"invoke\",\"id\":7,\"cmd\":\"greet\",\"args\":{\"who\":\"cj\"}}")
|
|
116
|
+
@Expect(req.isSome(), true)
|
|
117
|
+
let r = req.getOrThrow()
|
|
118
|
+
@Expect(r.id, 7)
|
|
119
|
+
@Expect(r.cmd, "greet")
|
|
120
|
+
@Expect(r.args.get("who").getOrThrow().asString().getValue(), "cj")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@Test
|
|
124
|
+
func testInvokeRequestParseToleratesMissingArgs(): Unit {
|
|
125
|
+
let req = InvokeRequest.parse("{\"type\":\"invoke\",\"id\":1,\"cmd\":\"system:ping\"}")
|
|
126
|
+
@Expect(req.isSome(), true)
|
|
127
|
+
@Expect(req.getOrThrow().args.size(), 0)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@Test
|
|
131
|
+
func testInvokeRequestParseRejectsMalformed(): Unit {
|
|
132
|
+
// 非 JSON
|
|
133
|
+
@Expect(InvokeRequest.parse("not json").isNone(), true)
|
|
134
|
+
// 顶层不是对象
|
|
135
|
+
@Expect(InvokeRequest.parse("[1,2,3]").isNone(), true)
|
|
136
|
+
// type 不是 invoke:emit 报文由 EmitRequest.parse 接走,这边照样是 None
|
|
137
|
+
@Expect(InvokeRequest.parse("{\"type\":\"emit\",\"id\":1,\"event\":\"x\"}").isNone(), true)
|
|
138
|
+
// 缺 cmd
|
|
139
|
+
@Expect(InvokeRequest.parse("{\"type\":\"invoke\",\"id\":1}").isNone(), true)
|
|
140
|
+
// 缺 id
|
|
141
|
+
@Expect(InvokeRequest.parse("{\"type\":\"invoke\",\"cmd\":\"greet\"}").isNone(), true)
|
|
142
|
+
// 缺 type
|
|
143
|
+
@Expect(InvokeRequest.parse("{\"id\":1,\"cmd\":\"greet\"}").isNone(), true)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@Test
|
|
147
|
+
func testHubResolvesAllowedCommand(): Unit {
|
|
148
|
+
let out = ArrayList<String>()
|
|
149
|
+
let hub = hubWith(["greet"], [], out)
|
|
150
|
+
hub.register("greet", OkHandler())
|
|
151
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":3,\"cmd\":\"greet\"}")
|
|
152
|
+
waitSink(out, 1)
|
|
153
|
+
@Expect(out.size, 1)
|
|
154
|
+
@Expect(strField(out[0], "type"), "resolve")
|
|
155
|
+
@Expect(strField(out[0], "data"), "ok")
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@Test
|
|
159
|
+
func testHubRejectsCommandNotAllowed(): Unit {
|
|
160
|
+
let out = ArrayList<String>()
|
|
161
|
+
let hub = hubWith(["system:ping"], [], out)
|
|
162
|
+
hub.register("greet", OkHandler())
|
|
163
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":4,\"cmd\":\"greet\"}")
|
|
164
|
+
// 未授权在**调用线程上同步拒绝**:不必等 worker,调用返回即可见
|
|
165
|
+
@Expect(out.size, 1)
|
|
166
|
+
@Expect(strField(out[0], "error"), "command not allowed: greet")
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@Test
|
|
170
|
+
func testHubRejectsUnregisteredCommand(): Unit {
|
|
171
|
+
let out = ArrayList<String>()
|
|
172
|
+
// 放行了但没注册:与「未放行」是两条不同的报错,容易混淆
|
|
173
|
+
let hub = hubWith(["ghost"], [], out)
|
|
174
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":5,\"cmd\":\"ghost\"}")
|
|
175
|
+
// 未注册同样是同步拒绝
|
|
176
|
+
@Expect(out.size, 1)
|
|
177
|
+
@Expect(strField(out[0], "error"), "command not registered: ghost")
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@Test
|
|
181
|
+
func testHubReportsCommandException(): Unit {
|
|
182
|
+
let out = ArrayList<String>()
|
|
183
|
+
let hub = hubWith(["boom"], [], out)
|
|
184
|
+
hub.register("boom", BoomHandler())
|
|
185
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":6,\"cmd\":\"boom\"}")
|
|
186
|
+
waitSink(out, 1)
|
|
187
|
+
@Expect(out.size, 1)
|
|
188
|
+
@Expect(strField(out[0], "error"), "boom")
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@Test
|
|
192
|
+
func testHubReportsUnexpectedException(): Unit {
|
|
193
|
+
let out = ArrayList<String>()
|
|
194
|
+
let hub = hubWith(["crash"], [], out)
|
|
195
|
+
hub.register("crash", CrashHandler())
|
|
196
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":8,\"cmd\":\"crash\"}")
|
|
197
|
+
waitSink(out, 1)
|
|
198
|
+
@Expect(out.size, 1)
|
|
199
|
+
// 预期外的异常统一加前缀,前端据此区分「业务拒绝」与「崩了」
|
|
200
|
+
@Expect(strField(out[0], "error").startsWith("command failed:"), true)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@Test
|
|
204
|
+
func testHubIgnoresMessageWithoutId(): Unit {
|
|
205
|
+
let out = ArrayList<String>()
|
|
206
|
+
let hub = hubWith(["greet"], ["tick"], out)
|
|
207
|
+
hub.register("greet", OkHandler())
|
|
208
|
+
// 结构坏掉且**读不出 id**:对应不到任何 promise,只能 stderr(这是设计,不是遗留问题)
|
|
209
|
+
hub.handleRawMessage("garbage")
|
|
210
|
+
hub.handleRawMessage("{\"type\":\"emit\",\"event\":\"tick\"}")
|
|
211
|
+
hub.handleRawMessage("{\"type\":\"invoke\"}")
|
|
212
|
+
@Expect(out.size, 0)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@Test
|
|
216
|
+
func testEmitRequiresDeclaredEvent(): Unit {
|
|
217
|
+
let denied = ArrayList<String>()
|
|
218
|
+
let hubDenied = hubWith(["greet"], [], denied)
|
|
219
|
+
hubDenied.emitToWindow("tick", JsonObject(), "main")
|
|
220
|
+
@Expect(denied.size, 0)
|
|
221
|
+
|
|
222
|
+
let allowed = ArrayList<String>()
|
|
223
|
+
let hubAllowed = hubWith(["greet"], ["tick"], allowed)
|
|
224
|
+
hubAllowed.emitToWindow("tick", JsonObject(), "main")
|
|
225
|
+
@Expect(allowed.size, 1)
|
|
226
|
+
@Expect(strField(allowed[0], "type"), "event")
|
|
227
|
+
@Expect(strField(allowed[0], "event"), "tick")
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@Test
|
|
231
|
+
func testIpcMessageResolveKeepsPayload(): Unit {
|
|
232
|
+
var payload = JsonObject()
|
|
233
|
+
payload.put("n", JsonInt(1))
|
|
234
|
+
let msg = IpcMessage.resolve(9, payload)
|
|
235
|
+
@Expect(strField(msg, "type"), "resolve")
|
|
236
|
+
let o = JsonValue.fromStr(msg).asObject()
|
|
237
|
+
@Expect(o.get("data").getOrThrow().asObject().get("n").getOrThrow().asInt().getValue(), 1)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/*
|
|
241
|
+
* 以下是「异步分发」的行为契约(命令执行跑在 worker 线程上,见 IpcHub.handleInvoke):
|
|
242
|
+
* 调用线程只做校验并立刻返回,慢命令不再把宿主 UI 线程钉住。
|
|
243
|
+
*/
|
|
244
|
+
|
|
245
|
+
/** 睡 300ms 再回答:模拟 `shell:exec` 这类慢命令 */
|
|
246
|
+
class SlowHandler <: CommandHandler {
|
|
247
|
+
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
248
|
+
let _ = cmd
|
|
249
|
+
let _ = args
|
|
250
|
+
let _ = ipc
|
|
251
|
+
sleep(Duration.millisecond * 300)
|
|
252
|
+
return JsonString("slow-ok")
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@Test
|
|
257
|
+
func testDispatchReturnsBeforeSlowCommandFinishes(): Unit {
|
|
258
|
+
let out = ArrayList<String>()
|
|
259
|
+
let hub = hubWith(["slow"], [], out)
|
|
260
|
+
hub.register("slow", SlowHandler())
|
|
261
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":41,\"cmd\":\"slow\"}")
|
|
262
|
+
// 关键:handler 还在睡,调用就已经返回了 —— 这就是「UI 线程不被命令阻塞」的单元级证据
|
|
263
|
+
// (handler 睡 300ms,这两行断言在微秒级完成,不存在轮到 worker 跑完的可能)
|
|
264
|
+
@Expect(out.size, 0)
|
|
265
|
+
waitSink(out, 1)
|
|
266
|
+
@Expect(strField(out[0], "type"), "resolve")
|
|
267
|
+
@Expect(strField(out[0], "data"), "slow-ok")
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@Test
|
|
271
|
+
func testRejectsStaySynchronous(): Unit {
|
|
272
|
+
let out = ArrayList<String>()
|
|
273
|
+
let hub = hubWith(["allowed"], [], out)
|
|
274
|
+
// 未授权:调用线程上同步回写,调用一返回就可见,不需要等
|
|
275
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":42,\"cmd\":\"nope\"}")
|
|
276
|
+
@Expect(out.size, 1)
|
|
277
|
+
@Expect(strField(out[0], "error"), "command not allowed: nope")
|
|
278
|
+
// 未注册:同样同步(放行了但没注册,两条报错别混)
|
|
279
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":43,\"cmd\":\"allowed\"}")
|
|
280
|
+
@Expect(out.size, 2)
|
|
281
|
+
@Expect(strField(out[1], "error"), "command not registered: allowed")
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@Test
|
|
285
|
+
func testConcurrentInvokesEachResolveExactlyOnce(): Unit {
|
|
286
|
+
let out = ArrayList<String>()
|
|
287
|
+
let hub = hubWith(["slow"], [], out)
|
|
288
|
+
hub.register("slow", SlowHandler())
|
|
289
|
+
// 同一命令连发 6 次:**不保证执行顺序**(各自独立跑,谁先完谁先回),但每条 id 必须有且只有一条响应
|
|
290
|
+
for (i in 50..56) {
|
|
291
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":${i},\"cmd\":\"slow\"}")
|
|
292
|
+
}
|
|
293
|
+
waitSink(out, 6, timeoutMs: 20000)
|
|
294
|
+
@Expect(out.size, 6)
|
|
295
|
+
let ids = ArrayList<Int64>()
|
|
296
|
+
for (m in out) {
|
|
297
|
+
@Expect(strField(m, "type"), "resolve")
|
|
298
|
+
ids.add(JsonValue.fromStr(m).asObject().get("id").getOrThrow().asInt().getValue())
|
|
299
|
+
}
|
|
300
|
+
for (i in 50..56) {
|
|
301
|
+
var hits = 0
|
|
302
|
+
for (id in ids) {
|
|
303
|
+
if (id == i) {
|
|
304
|
+
hits += 1
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// 既不丢(0)也不重复(2):并发下每条 invoke 恰好被回投一次
|
|
308
|
+
@Expect(hits, 1)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/*
|
|
313
|
+
* 前端 emit(P1-4 缺口):只投后端监听器、不回投页面;未授权与非法报文的失败都要回给前端。
|
|
314
|
+
* 与 Tauri v2 的差异是有意选的(见 EmitRequest 注释 / docs/IPC-通信机制.md §7)。
|
|
315
|
+
*/
|
|
316
|
+
|
|
317
|
+
/** 抛异常的监听器:验证「一个听众崩了不牵连别的听众与发送方」 */
|
|
318
|
+
func boomListener(p: JsonValue): Unit {
|
|
319
|
+
let _ = p
|
|
320
|
+
throw Exception("listener boom")
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
@Test
|
|
324
|
+
func testEmitRequestParseOk(): Unit {
|
|
325
|
+
let req = EmitRequest.parse("{\"type\":\"emit\",\"id\":9,\"event\":\"tick\",\"payload\":{\"n\":1}}")
|
|
326
|
+
@Expect(req.isSome(), true)
|
|
327
|
+
let r = req.getOrThrow()
|
|
328
|
+
@Expect(r.id, 9)
|
|
329
|
+
@Expect(r.event, "tick")
|
|
330
|
+
@Expect(r.payload.asObject().get("n").getOrThrow().asInt().getValue(), 1)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
@Test
|
|
334
|
+
func testEmitRequestParseDefaultsPayloadAndRejectsMalformed(): Unit {
|
|
335
|
+
// 前端 emit('tick') 不带第二个参数:payload 缺省成空对象
|
|
336
|
+
let bare = EmitRequest.parse("{\"type\":\"emit\",\"id\":1,\"event\":\"tick\"}")
|
|
337
|
+
@Expect(bare.isSome(), true)
|
|
338
|
+
@Expect(bare.getOrThrow().payload.asObject().size(), 0)
|
|
339
|
+
// 缺 id / 缺 event / 类型不对,都不是合法 emit 报文
|
|
340
|
+
@Expect(EmitRequest.parse("{\"type\":\"emit\",\"event\":\"tick\"}").isNone(), true)
|
|
341
|
+
@Expect(EmitRequest.parse("{\"type\":\"emit\",\"id\":1}").isNone(), true)
|
|
342
|
+
@Expect(EmitRequest.parse("{\"type\":\"invoke\",\"id\":1,\"cmd\":\"greet\"}").isNone(), true)
|
|
343
|
+
@Expect(EmitRequest.parse("not json").isNone(), true)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
@Test
|
|
347
|
+
func testFrontendEmitReachesBackendListener(): Unit {
|
|
348
|
+
let out = ArrayList<String>()
|
|
349
|
+
let hub = hubWith([], ["tick"], out)
|
|
350
|
+
let seen = ArrayList<Int64>()
|
|
351
|
+
hub.listenEvent("tick", { p =>
|
|
352
|
+
synchronized(TEST_SINK_LOCK) {
|
|
353
|
+
seen.add(p.asObject().get("n").getOrThrow().asInt().getValue())
|
|
354
|
+
}
|
|
355
|
+
})
|
|
356
|
+
hub.handleRawMessage("{\"type\":\"emit\",\"id\":21,\"event\":\"tick\",\"payload\":{\"n\":7}}")
|
|
357
|
+
waitSink(out, 1)
|
|
358
|
+
@Expect(seen.size, 1)
|
|
359
|
+
@Expect(seen[0], 7)
|
|
360
|
+
@Expect(out.size, 1)
|
|
361
|
+
@Expect(strField(out[0], "type"), "resolve")
|
|
362
|
+
@Expect(boolField(out[0], "ok"), true)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
@Test
|
|
366
|
+
func testAllListenersOfEventAreCalled(): Unit {
|
|
367
|
+
let out = ArrayList<String>()
|
|
368
|
+
let hub = hubWith([], ["tick"], out)
|
|
369
|
+
let hits = ArrayList<String>()
|
|
370
|
+
hub.listenEvent("tick", { _ => synchronized(TEST_SINK_LOCK) { hits.add("a") } })
|
|
371
|
+
hub.listenEvent("tick", { _ => synchronized(TEST_SINK_LOCK) { hits.add("b") } })
|
|
372
|
+
hub.handleRawMessage("{\"type\":\"emit\",\"id\":23,\"event\":\"tick\",\"payload\":{}}")
|
|
373
|
+
waitSink(out, 1)
|
|
374
|
+
@Expect(hits.size, 2)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
@Test
|
|
378
|
+
func testFailingListenerDoesNotBreakOthers(): Unit {
|
|
379
|
+
let out = ArrayList<String>()
|
|
380
|
+
let hub = hubWith([], ["tick"], out)
|
|
381
|
+
hub.listenEvent("tick", boomListener)
|
|
382
|
+
let ok = ArrayList<Bool>()
|
|
383
|
+
hub.listenEvent("tick", { _ => synchronized(TEST_SINK_LOCK) { ok.add(true) } })
|
|
384
|
+
hub.handleRawMessage("{\"type\":\"emit\",\"id\":24,\"event\":\"tick\",\"payload\":{}}")
|
|
385
|
+
waitSink(out, 1)
|
|
386
|
+
// 事件是广播语义:一个听众抛错不该牵连其它听众,也不该让发送方收到失败
|
|
387
|
+
@Expect(ok.size, 1)
|
|
388
|
+
@Expect(boolField(out[0], "ok"), true)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
@Test
|
|
392
|
+
func testFrontendEmitDeniedEventIsRejected(): Unit {
|
|
393
|
+
let out = ArrayList<String>()
|
|
394
|
+
let hub = hubWith([], ["tick"], out)
|
|
395
|
+
// 未授权的事件:与命令一样在调用线程上同步拒绝,不必等 worker
|
|
396
|
+
hub.handleRawMessage("{\"type\":\"emit\",\"id\":22,\"event\":\"nope\",\"payload\":{}}")
|
|
397
|
+
@Expect(out.size, 1)
|
|
398
|
+
@Expect(boolField(out[0], "ok"), false)
|
|
399
|
+
@Expect(strField(out[0], "error"), "event not allowed: nope")
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
@Test
|
|
403
|
+
func testFrontendEmitWithoutListenerStillResolves(): Unit {
|
|
404
|
+
let out = ArrayList<String>()
|
|
405
|
+
let hub = hubWith([], ["tick"], out)
|
|
406
|
+
// 授权了但没人听:不是错误(Tauri 同样不报),发送方照常拿到 resolve
|
|
407
|
+
hub.handleRawMessage("{\"type\":\"emit\",\"id\":25,\"event\":\"tick\",\"payload\":{}}")
|
|
408
|
+
waitSink(out, 1)
|
|
409
|
+
@Expect(out.size, 1)
|
|
410
|
+
@Expect(boolField(out[0], "ok"), true)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
@Test
|
|
414
|
+
func testMalformedMessageWithIdIsRejected(): Unit {
|
|
415
|
+
let out = ArrayList<String>()
|
|
416
|
+
let hub = hubWith([], ["tick"], out)
|
|
417
|
+
// 未知报文类型但 id 可读:回一条 reject,前端那条 promise 不再永久 pending
|
|
418
|
+
hub.handleRawMessage("{\"type\":\"wat\",\"id\":31}")
|
|
419
|
+
@Expect(out.size, 1)
|
|
420
|
+
@Expect(boolField(out[0], "ok"), false)
|
|
421
|
+
@Expect(strField(out[0], "error"), "invalid IPC message")
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
@Test
|
|
425
|
+
func testEventCarriesWindowLabelOrNothingWhenBroadcast(): Unit {
|
|
426
|
+
let out = ArrayList<String>()
|
|
427
|
+
let hub = hubWith([], ["tick"], out)
|
|
428
|
+
hub.emitToWindow("tick", JsonObject(), "main")
|
|
429
|
+
@Expect(strField(out[0], "window"), "main")
|
|
430
|
+
hub.emitAll("tick", JsonObject())
|
|
431
|
+
// 广播不带 window 字段:页面里由桥放行(多窗口过滤靠它)
|
|
432
|
+
@Expect(JsonValue.fromStr(out[1]).asObject().get("window").isNone(), true)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/*
|
|
436
|
+
* emitLatest:显式 opt-in 的「同事件同帧只投最新值」。
|
|
437
|
+
* 报文侧只该多一个 coalesce:true,页面侧的合并(一帧一次、只投尾值)由桥 JS 做,
|
|
438
|
+
* 所以单元测试能钉住的是「报文契约」与「能力白名单」两条;合并行为本身靠实机探针取证
|
|
439
|
+
* (见 docs/IPC-通信机制.md §6 第 5 项)。
|
|
440
|
+
*/
|
|
441
|
+
|
|
442
|
+
@Test
|
|
443
|
+
func testEmitLatestMarksCoalesceAndKeepsPayload(): Unit {
|
|
444
|
+
let out = ArrayList<String>()
|
|
445
|
+
let hub = hubWith([], ["progress"], out)
|
|
446
|
+
var payload = JsonObject()
|
|
447
|
+
payload.put("n", JsonInt(3))
|
|
448
|
+
hub.emitLatest("progress", payload, "")
|
|
449
|
+
@Expect(out.size, 1)
|
|
450
|
+
@Expect(strField(out[0], "type"), "event")
|
|
451
|
+
@Expect(strField(out[0], "event"), "progress")
|
|
452
|
+
@Expect(boolField(out[0], "coalesce"), true)
|
|
453
|
+
let n = JsonValue.fromStr(out[0]).asObject().get("payload").getOrThrow().asObject().get("n").getOrThrow().asInt().getValue()
|
|
454
|
+
@Expect(n, 3)
|
|
455
|
+
// 广播不带 window 字段(与 emitAll 一致)
|
|
456
|
+
@Expect(JsonValue.fromStr(out[0]).asObject().get("window").isNone(), true)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
@Test
|
|
460
|
+
func testEmitLatestCarriesWindowLabelWhenTargeted(): Unit {
|
|
461
|
+
let out = ArrayList<String>()
|
|
462
|
+
let hub = hubWith([], ["progress"], out)
|
|
463
|
+
hub.emitLatest("progress", JsonObject(), "main")
|
|
464
|
+
@Expect(strField(out[0], "window"), "main")
|
|
465
|
+
@Expect(boolField(out[0], "coalesce"), true)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
@Test
|
|
469
|
+
func testEmitLatestRespectsCapabilityDeny(): Unit {
|
|
470
|
+
let out = ArrayList<String>()
|
|
471
|
+
let hub = hubWith([], ["other"], out)
|
|
472
|
+
// 未授权的事件与 emit 同一套白名单:不投递,也不能因为「只是合并语义」就绕过声明
|
|
473
|
+
hub.emitLatest("progress", JsonObject(), "")
|
|
474
|
+
@Expect(out.size, 0)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
@Test
|
|
478
|
+
func testPlainEmitHasNoCoalesceField(): Unit {
|
|
479
|
+
let out = ArrayList<String>()
|
|
480
|
+
let hub = hubWith([], ["tick"], out)
|
|
481
|
+
hub.emitAll("tick", JsonObject())
|
|
482
|
+
// 默认路径不做合并:报文里不能出现 coalesce 字段(合并必须显式 opt-in)
|
|
483
|
+
@Expect(JsonValue.fromStr(out[0]).asObject().get("coalesce").isNone(), true)
|
|
484
|
+
hub.emitToWindow("tick", JsonObject(), "main")
|
|
485
|
+
@Expect(JsonValue.fromStr(out[1]).asObject().get("coalesce").isNone(), true)
|
|
486
|
+
@Expect(strField(out[1], "window"), "main")
|
|
487
|
+
}
|