cj-tauri 0.8.0 → 1.1.1
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 +53 -7
- package/cjpm.toml +52 -25
- package/cli/cjpm.toml +1 -1
- package/cli/src/doctor.cj +304 -0
- package/cli/src/main.cj +33 -7
- package/cli/src/project.cj +59 -1
- package/cli/src/resolve.cj +348 -304
- package/cli/src/scaffold.cj +139 -22
- package/cli/templates/ohos-pc/AppScope/app.json5 +13 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/element/string.json +8 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/media/background.png +0 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/media/foreground.png +0 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/media/layered_image.json +6 -0
- package/cli/templates/ohos-pc/README.md +79 -0
- package/cli/templates/ohos-pc/build-hap.sh +190 -0
- package/cli/templates/ohos-pc/build-profile.json5 +46 -0
- package/cli/templates/ohos-pc/entry/build-profile.json5 +26 -0
- package/cli/templates/ohos-pc/entry/cjpm.toml +43 -0
- package/cli/templates/ohos-pc/entry/hvigorfile.ts +6 -0
- package/cli/templates/ohos-pc/entry/oh-package.json5 +9 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/ability_stage.cj +17 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/app_host.cj +94 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/index.cj +167 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/main_ability.cj +139 -0
- package/cli/templates/ohos-pc/entry/src/main/module.json5 +49 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/element/color.json +8 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/element/string.json +16 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/background.png +0 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/foreground.png +0 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/layered_image.json +6 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/startIcon.png +0 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/profile/main_pages.json +2 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/capabilities.json +6 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/index.html +92 -0
- package/cli/templates/ohos-pc/hvigor/hvigor-config.json5 +5 -0
- package/cli/templates/ohos-pc/hvigorfile.ts +6 -0
- package/cli/templates/ohos-pc/oh-package.json5 +10 -0
- package/native/build_linux.sh +30 -2
- package/native/build_win.bat +62 -61
- package/native/core/bridge_core.c +1626 -0
- package/native/{bridge_core.h → core/bridge_core.h} +246 -6
- package/native/platforms/harmonyos/README.md +106 -0
- package/native/platforms/linux/bridge_linux.c +2485 -0
- package/native/platforms/windows/bridge_win.c +3248 -0
- package/native/tests/test_bridge_core.c +528 -1
- package/package.json +12 -3
- package/prebuilt/win32-x64/cj-tauri.exe +0 -0
- package/src/api_system.cj +10 -2
- package/src/app.cj +248 -85
- package/src/arkweb_proxy.cj +56 -0
- package/src/arkweb_script.cj +154 -0
- package/src/capability.cj +140 -0
- package/src/clipboard_bridge.cj +55 -0
- package/src/dragdrop.cj +26 -0
- package/src/embedded_assets.cj +98 -0
- package/src/hexutil.cj +53 -0
- package/src/host.cj +197 -26
- package/src/host_arkweb.cj +740 -0
- package/src/host_factory.cj +48 -0
- package/src/host_webkit.cj +202 -26
- package/src/host_webview2.cj +176 -3
- package/src/hotkey.cj +66 -0
- package/src/ipc_hub.cj +49 -0
- package/src/ipc_message.cj +2 -1
- package/src/jsutil.cj +33 -0
- package/src/log_bridge.cj +107 -0
- package/src/logging.cj +127 -0
- package/src/menu.cj +197 -196
- package/src/plugin.cj +40 -0
- package/src/plugins/clipboard/plugin_clipboard.cj +144 -0
- package/src/{plugin_dialog.cj → plugins/dialog/plugin_dialog.cj} +74 -17
- package/src/plugins/fs/plugin_fs.cj +194 -0
- package/src/plugins/http/plugin_http.cj +459 -0
- package/src/plugins/log/plugin_log.cj +105 -0
- package/src/{plugin_menu.cj → plugins/menu/plugin_menu.cj} +4 -2
- package/src/plugins/mqtt/mqtt_codec.cj +661 -0
- package/src/plugins/mqtt/plugin_mqtt.cj +1446 -0
- package/src/plugins/notification/plugin_notification.cj +146 -0
- package/src/plugins/plugins.cj +5 -0
- package/src/plugins/redis/plugin_redis.cj +2409 -0
- package/src/plugins/redis/redis_codec.cj +556 -0
- package/src/{plugin_serial.cj → plugins/serial/plugin_serial.cj} +442 -489
- package/src/{serial_bridge.cj → plugins/serial/serial_bridge.cj} +185 -184
- package/src/{plugin_shell.cj → plugins/shell/plugin_shell.cj} +230 -228
- package/src/plugins/sqlite/plugin_sqlite.cj +697 -0
- package/src/plugins/sqlite/sqlite_ffi.cj +206 -0
- package/src/plugins/store/plugin_store.cj +287 -0
- package/src/plugins/ws/plugin_ws.cj +470 -0
- package/src/process_hidden.cj +174 -0
- package/src/scope.cj +391 -0
- package/src/shell_event.cj +69 -68
- package/src/tests/capability_test.cj +53 -0
- package/src/tests/fake_host.cj +85 -20
- package/src/tests/ipc_hub_test.cj +13 -0
- package/src/tests/jsutil_test.cj +62 -0
- package/src/tests/logging_test.cj +62 -0
- package/src/tests/mqtt_codec_test.cj +405 -0
- package/src/tests/plugin_clipboard_test.cj +222 -0
- package/src/tests/plugin_dialog_test.cj +98 -4
- package/src/tests/plugin_http_test.cj +124 -0
- package/src/tests/plugin_menu_test.cj +1 -0
- package/src/tests/plugin_notification_test.cj +205 -0
- package/src/tests/plugin_serial_test.cj +269 -267
- package/src/tests/plugin_shell_test.cj +189 -188
- package/src/tests/plugin_sqlite_test.cj +230 -0
- package/src/tests/plugin_store_test.cj +260 -0
- package/src/tests/plugin_test.cj +542 -524
- package/src/tests/plugin_ws_test.cj +40 -0
- package/src/tests/process_hidden_test.cj +129 -0
- package/src/tests/redis_codec_test.cj +396 -0
- package/src/tests/scope_test.cj +350 -0
- package/src/tests/shell_event_test.cj +0 -1
- package/src/tests/system_host_test.cj +81 -79
- package/src/tests/urlutil_test.cj +53 -0
- package/src/tests/version_test.cj +75 -65
- package/src/tray.cj +90 -0
- package/src/urlutil.cj +159 -0
- package/src/version.cj +23 -2
- package/src/window.cj +51 -42
- package/native/bridge_core.c +0 -797
- package/native/bridge_linux.c +0 -1268
- package/native/bridge_win.c +0 -1633
- package/src/plugin_fs.cj +0 -118
- /package/native/{bridge_js.h → core/bridge_js.h} +0 -0
|
@@ -1,188 +1,189 @@
|
|
|
1
|
-
package cjTauri.tests
|
|
2
|
-
|
|
3
|
-
import cjTauri.*
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import std.unittest
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
var
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@Expect(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@Expect(js.contains("
|
|
40
|
-
@Expect(js.contains("'shell:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
@Expect(openOnly
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
let
|
|
73
|
-
reg
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@Expect(firstError.contains("
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
@Expect(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
let
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
let
|
|
110
|
-
let
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
let
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
let
|
|
142
|
-
let
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
let
|
|
163
|
-
let
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
@Expect(r.get("
|
|
168
|
-
@Expect(r.get("
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
let
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
let
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
1
|
+
package cjTauri.tests
|
|
2
|
+
|
|
3
|
+
import cjTauri.*
|
|
4
|
+
import cjTauri.plugins.shell.*
|
|
5
|
+
import std.collection.*
|
|
6
|
+
import stdx.encoding.json.*
|
|
7
|
+
import std.unittest.*
|
|
8
|
+
import std.unittest.testmacro.*
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* shell 插件单元测试:命令面、命名权限集(allow-open 不含 exec)、参数校验、可读错误、输出捕获与解码。
|
|
12
|
+
* 真正「交给系统默认程序打开」要桌面环境,归实机验证(examples/plugin-shell);这里只跑不起窗口的部分。
|
|
13
|
+
* 唯一会真的起子进程的是 testShellExecCapturesOutputAndExitCode:只跑 `sh`,且仅在 Linux 上执行。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
@Test
|
|
17
|
+
func testShellPluginSurface(): Unit {
|
|
18
|
+
let p = ShellPlugin()
|
|
19
|
+
@Expect(p.name(), "shell")
|
|
20
|
+
let names = pluginCommandNames(p)
|
|
21
|
+
@Expect(names.size, 2)
|
|
22
|
+
var hasOpen = false
|
|
23
|
+
var hasExec = false
|
|
24
|
+
for (n in names) {
|
|
25
|
+
if (n == "shell:open") {
|
|
26
|
+
hasOpen = true
|
|
27
|
+
}
|
|
28
|
+
if (n == "shell:exec") {
|
|
29
|
+
hasExec = true
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
@Expect(hasOpen, true)
|
|
33
|
+
@Expect(hasExec, true)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Test
|
|
37
|
+
func testShellPluginShimTargetsItsOwnNamespace(): Unit {
|
|
38
|
+
let js = ShellPlugin().jsShim()
|
|
39
|
+
@Expect(js.contains("t.shell"), true)
|
|
40
|
+
@Expect(js.contains("'shell:open'"), true)
|
|
41
|
+
@Expect(js.contains("'shell:exec'"), true)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test
|
|
45
|
+
func testShellPluginDeclaresPermissionSets(): Unit {
|
|
46
|
+
let sets = pluginPermissionSets(ShellPlugin())
|
|
47
|
+
@Expect(sets.size, 2)
|
|
48
|
+
let openOnly = sets.get("shell:allow-open").getOrThrow()
|
|
49
|
+
@Expect(openOnly.size, 1)
|
|
50
|
+
@Expect(openOnly[0], "shell:open")
|
|
51
|
+
let all = sets.get("shell:default").getOrThrow()
|
|
52
|
+
@Expect(all.size, 2)
|
|
53
|
+
var allHasExec = false
|
|
54
|
+
for (m in all) {
|
|
55
|
+
if (m == "shell:exec") {
|
|
56
|
+
allHasExec = true
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
@Expect(allHasExec, true)
|
|
60
|
+
// 只想「打开链接 / 文件」的应用不该顺带拿到执行能力
|
|
61
|
+
var openOnlyHasExec = false
|
|
62
|
+
for (m in openOnly) {
|
|
63
|
+
if (m == "shell:exec") {
|
|
64
|
+
openOnlyHasExec = true
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
@Expect(openOnlyHasExec, false)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@Test
|
|
71
|
+
func testShellPermissionSetGatesCommands(): Unit {
|
|
72
|
+
let out = ArrayList<String>()
|
|
73
|
+
let reg = CapabilityRegistry()
|
|
74
|
+
reg.addFromJson(JsonValue.fromStr("{\"identifier\":\"s\",\"permissions\":[\"shell:allow-open\"]}"))
|
|
75
|
+
let hub = hubWithRegistry(reg, out)
|
|
76
|
+
registerPluginCommands(hub, ShellPlugin())
|
|
77
|
+
registerPluginPermissionSets(reg, ShellPlugin())
|
|
78
|
+
|
|
79
|
+
// 集内命令放行:故意不传 target,报的应是业务错误(参数校验),而不是「未授权」
|
|
80
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":31,\"cmd\":\"shell:open\"}")
|
|
81
|
+
waitSink(out, 1)
|
|
82
|
+
@Expect(out.size, 1)
|
|
83
|
+
let firstError = strField(out[0], "error")
|
|
84
|
+
@Expect(firstError.contains("command not allowed"), false)
|
|
85
|
+
@Expect(firstError.contains("missing argument \"target\""), true)
|
|
86
|
+
|
|
87
|
+
// 集外命令仍拒:allow-open 放行的是「打开」,不是整个插件
|
|
88
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":32,\"cmd\":\"shell:exec\"}")
|
|
89
|
+
@Expect(out.size, 2)
|
|
90
|
+
@Expect(strField(out[1], "error"), "command not allowed: shell:exec")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Test
|
|
94
|
+
func testShellExecRequiresProgram(): Unit {
|
|
95
|
+
let cmds = ShellPlugin().commands()
|
|
96
|
+
let ctx = IpcContext("main", IpcHub())
|
|
97
|
+
var threw = false
|
|
98
|
+
try {
|
|
99
|
+
cmds.get("exec").getOrThrow().handle("shell:exec", JsonObject(), ctx)
|
|
100
|
+
} catch (e: CommandException) {
|
|
101
|
+
threw = true
|
|
102
|
+
@Expect(e.message.contains("missing argument \"program\""), true)
|
|
103
|
+
}
|
|
104
|
+
@Expect(threw, true)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@Test
|
|
108
|
+
func testShellOpenRejectsEmptyTarget(): Unit {
|
|
109
|
+
let cmds = ShellPlugin().commands()
|
|
110
|
+
let ctx = IpcContext("main", IpcHub())
|
|
111
|
+
let args = JsonValue.fromStr("{\"target\":\"\"}").asObject()
|
|
112
|
+
var threw = false
|
|
113
|
+
try {
|
|
114
|
+
cmds.get("open").getOrThrow().handle("shell:open", args, ctx)
|
|
115
|
+
} catch (e: CommandException) {
|
|
116
|
+
threw = true
|
|
117
|
+
@Expect(e.message.contains("must not be empty"), true)
|
|
118
|
+
}
|
|
119
|
+
@Expect(threw, true)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@Test
|
|
123
|
+
func testShellExecRejectsNonStringArgs(): Unit {
|
|
124
|
+
let cmds = ShellPlugin().commands()
|
|
125
|
+
let ctx = IpcContext("main", IpcHub())
|
|
126
|
+
// args 里混进数字:宁可报错也不要悄悄丢掉——否则页面以为传了参数,实际没传
|
|
127
|
+
let args = JsonValue.fromStr("{\"program\":\"echo\",\"args\":[1]}").asObject()
|
|
128
|
+
var threw = false
|
|
129
|
+
try {
|
|
130
|
+
cmds.get("exec").getOrThrow().handle("shell:exec", args, ctx)
|
|
131
|
+
} catch (e: CommandException) {
|
|
132
|
+
threw = true
|
|
133
|
+
@Expect(e.message.contains("must contain only strings"), true)
|
|
134
|
+
}
|
|
135
|
+
@Expect(threw, true)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@Test
|
|
139
|
+
func testShellExecMissingProgramIsReadableError(): Unit {
|
|
140
|
+
// 程序不存在时给的是可读的 CommandException(含系统原文),而不是让异常穿透到 IPC 层
|
|
141
|
+
let cmds = ShellPlugin().commands()
|
|
142
|
+
let ctx = IpcContext("main", IpcHub())
|
|
143
|
+
let args = JsonValue.fromStr("{\"program\":\"cj-tauri-no-such-program-xyz\"}").asObject()
|
|
144
|
+
var threw = false
|
|
145
|
+
try {
|
|
146
|
+
cmds.get("exec").getOrThrow().handle("shell:exec", args, ctx)
|
|
147
|
+
} catch (e: CommandException) {
|
|
148
|
+
threw = true
|
|
149
|
+
@Expect(e.message.contains("shell:exec failed"), true)
|
|
150
|
+
}
|
|
151
|
+
@Expect(threw, true)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@Test
|
|
155
|
+
func testShellExecCapturesOutputAndExitCode(): Unit {
|
|
156
|
+
// 真起一个子进程取回 stdout / stderr / 退出码。用 sh 保证两件事同时可测:分流与退出码。
|
|
157
|
+
// 只在 Linux 跑:Windows 上 sh 不保证存在(`echo` 是 cmd 内建、不是可执行文件),
|
|
158
|
+
// 这台机器的实测环境是 Linux,不在没验证过的平台上假装通过。
|
|
159
|
+
if (currentPlatform() != "linux") {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
let cmds = ShellPlugin().commands()
|
|
163
|
+
let ctx = IpcContext("main", IpcHub())
|
|
164
|
+
let args = JsonValue.fromStr(
|
|
165
|
+
"{\"program\":\"sh\",\"args\":[\"-c\",\"echo out; echo err 1>&2; exit 3\"]}").asObject()
|
|
166
|
+
let r = cmds.get("exec").getOrThrow().handle("shell:exec", args, ctx).asObject()
|
|
167
|
+
@Expect(r.get("code").getOrThrow().asInt().getValue(), 3)
|
|
168
|
+
@Expect(r.get("stdout").getOrThrow().asString().getValue().contains("out"), true)
|
|
169
|
+
@Expect(r.get("stderr").getOrThrow().asString().getValue().contains("err"), true)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@Test
|
|
173
|
+
func testDecodeCapturedFallsBackForNonUtf8(): Unit {
|
|
174
|
+
// 子进程往 stdout 写二进制是常事:解码失败要退化成摘要,不能让整条命令崩掉
|
|
175
|
+
let bad = Array<Byte>(3, repeat: 255)
|
|
176
|
+
let text = decodeCaptured(bad)
|
|
177
|
+
@Expect(text.contains("不是合法 UTF-8"), true)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@Test
|
|
181
|
+
func testDecodeCapturedTruncatesLongOutput(): Unit {
|
|
182
|
+
// 超过上限的输出截断并标注,避免一条命令把几十 MB 灌进 IPC 消息
|
|
183
|
+
let big = Array<Byte>(SHELL_MAX_CAPTURE_BYTES + 10, repeat: 65)
|
|
184
|
+
let text = decodeCaptured(big)
|
|
185
|
+
@Expect(text.contains("已截断"), true)
|
|
186
|
+
// 没超上限时不该出现标注
|
|
187
|
+
let small = Array<Byte>(4, repeat: 65)
|
|
188
|
+
@Expect(decodeCaptured(small).contains("已截断"), false)
|
|
189
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
package cjTauri.tests
|
|
2
|
+
|
|
3
|
+
import cjTauri.*
|
|
4
|
+
import cjTauri.plugins.sqlite.*
|
|
5
|
+
import std.collection.*
|
|
6
|
+
import std.fs.*
|
|
7
|
+
import stdx.encoding.json.*
|
|
8
|
+
import std.unittest.*
|
|
9
|
+
import std.unittest.testmacro.*
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* sqlite 插件单元测试:插件面(名字 / 命令 / 权限集 / shim)+ **真库往返**(FFI 直连系统 sqlite3)。
|
|
13
|
+
*
|
|
14
|
+
* 为什么不是「只测插件面」:本插件是仓库里第一处 `foreign func` 直连第三方系统库(D-018),
|
|
15
|
+
* 出参指针、授权回调、列类型映射这些都是链接期与运行期才会露的坑——只断言 HashMap 里有没有键,
|
|
16
|
+
* 等于没测。所以这里用 /tmp 下的临时库真跑一遍:建表 / 插入 / 分页 / 类型 / 截断 / 护栏拒绝。
|
|
17
|
+
*
|
|
18
|
+
* 不创建窗口,可在无 GUI 环境跑;库文件固定放 /tmp/cj-tauri-sqlite-test.db,用例自己先清干净。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
let SQLITE_TEST_DB: String = "/tmp/cj-tauri-sqlite-test.db"
|
|
22
|
+
|
|
23
|
+
func sqliteArgs(args: Array<(String, JsonValue)>): JsonObject {
|
|
24
|
+
let o = JsonObject()
|
|
25
|
+
for (kv in args) {
|
|
26
|
+
o.put(kv[0], kv[1])
|
|
27
|
+
}
|
|
28
|
+
return o
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func sqlitePathArg(path: String): JsonObject {
|
|
32
|
+
return sqliteArgs([("path", JsonString(path))])
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 从命令结果里取字段(结果都是 JsonObject,取不到直接失败——测试里不兜底) */
|
|
36
|
+
func sqliteField(v: JsonValue, key: String): JsonValue {
|
|
37
|
+
return v.asObject().get(key).getOrThrow()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func sqliteStr(v: JsonValue, key: String): String {
|
|
41
|
+
return sqliteField(v, key).asString().getValue()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** JsonArray.get 给的是 Option:测试里取不到就该当场炸(所以直接送进上面的取值函数),不兜底 */
|
|
45
|
+
func sqliteStr(v: Option<JsonValue>, key: String): String {
|
|
46
|
+
return sqliteStr(v.getOrThrow(), key)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func sqliteIntOf(v: JsonValue, key: String): Int64 {
|
|
50
|
+
return sqliteField(v, key).asInt().getValue()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func sqliteIntOf(v: Option<JsonValue>, key: String): Int64 {
|
|
54
|
+
return sqliteIntOf(v.getOrThrow(), key)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func sqliteBoolOf(v: JsonValue, key: String): Bool {
|
|
58
|
+
return sqliteField(v, key).asBool().getValue()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func sqliteBoolOf(v: Option<JsonValue>, key: String): Bool {
|
|
62
|
+
return sqliteBoolOf(v.getOrThrow(), key)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 单元格是 {type, value, …} 形状:取类型名 */
|
|
66
|
+
func sqliteCellType(cell: JsonValue): String {
|
|
67
|
+
return sqliteStr(cell, "type")
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 取第 row 行第 col 格 */
|
|
71
|
+
func sqliteCell(rows: JsonValue, row: Int64, col: Int64): JsonValue {
|
|
72
|
+
return rows.asArray().get(row).getOrThrow().asArray().get(col).getOrThrow()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 清掉上次用例留下的库文件(固定路径,用例之间互不干扰) */
|
|
76
|
+
func sqliteCleanDb(): Unit {
|
|
77
|
+
if (exists(SQLITE_TEST_DB)) {
|
|
78
|
+
remove(SQLITE_TEST_DB)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 跑一条 exec(建表 / 插入都走它),失败就让用例炸在当场 */
|
|
83
|
+
func sqliteExec(handler: CommandHandler, sql: String): JsonValue {
|
|
84
|
+
return handler.handle("sqlite:exec", sqliteArgs([("path", JsonString(SQLITE_TEST_DB)),
|
|
85
|
+
("sql", JsonString(sql))]), IpcContext("main", IpcHub()))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func sqliteErrOf(handler: CommandHandler, cmd: String, args: JsonObject): String {
|
|
89
|
+
try {
|
|
90
|
+
handler.handle(cmd, args, IpcContext("main", IpcHub()))
|
|
91
|
+
return ""
|
|
92
|
+
} catch (e: CommandException) {
|
|
93
|
+
return e.message
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@Test
|
|
98
|
+
func testSqlitePluginSurface(): Unit {
|
|
99
|
+
let plugin = SqlitePlugin()
|
|
100
|
+
@Expect(plugin.name(), "sqlite")
|
|
101
|
+
// 库文件是页面可控路径:必须吃 scope 校验(D-018 第 3 条)
|
|
102
|
+
@Expect(plugin.scopeAware(), true)
|
|
103
|
+
@Expect(plugin.commands().size, 7)
|
|
104
|
+
for (short in SQLITE_PLUGIN_COMMANDS) {
|
|
105
|
+
@Expect(plugin.commands().contains(short), true)
|
|
106
|
+
}
|
|
107
|
+
// 前端 shim 只挂一个命名空间,且七个命令都能被调到(漏一个页面就有一块功能静默不可用)
|
|
108
|
+
let shim = plugin.jsShim()
|
|
109
|
+
@Expect(shim.contains("t.sqlite ="), true)
|
|
110
|
+
for (short in SQLITE_PLUGIN_COMMANDS) {
|
|
111
|
+
@Expect(shim.contains("invoke('sqlite:${short}'"), true)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@Test
|
|
116
|
+
func testSqlitePermissionSetsKeepWriteSeparate(): Unit {
|
|
117
|
+
let sets = SqlitePlugin().permissions()
|
|
118
|
+
let ro = sets.get("readonly").getOrThrow()
|
|
119
|
+
let all = sets.get("default").getOrThrow()
|
|
120
|
+
@Expect(ro.size, 6)
|
|
121
|
+
@Expect(all.size, 7)
|
|
122
|
+
// readonly 里**不能**有 exec:写能力不该被「只想看数据」的应用顺带打开
|
|
123
|
+
@Expect(ro.contains("exec"), false)
|
|
124
|
+
@Expect(all.contains("exec"), true)
|
|
125
|
+
for (short in ro) {
|
|
126
|
+
@Expect(all.contains(short), true)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@Test
|
|
131
|
+
func testSqlitePluginDoesNotAutoGrant(): Unit {
|
|
132
|
+
// 插件声明了权限集 ≠ 放行:能力清单不引用,命令照样被拒(与 fs / redis 同一条铁律)
|
|
133
|
+
let out = ArrayList<String>()
|
|
134
|
+
let hub = hubWith([], [], out)
|
|
135
|
+
registerPluginCommands(hub, SqlitePlugin())
|
|
136
|
+
hub.handleRawMessage("{\"type\":\"invoke\",\"id\":1,\"cmd\":\"sqlite:info\"}")
|
|
137
|
+
waitSink(out, 1)
|
|
138
|
+
@Expect(strField(out[0], "error"), "command not allowed: sqlite:info")
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@Test
|
|
142
|
+
func testSqliteMissingPathIsBusinessError(): Unit {
|
|
143
|
+
let cmds = SqlitePlugin().commands()
|
|
144
|
+
let msg = sqliteErrOf(cmds.get("info").getOrThrow(), "sqlite:info", JsonObject())
|
|
145
|
+
@Expect(msg.contains("missing argument \"path\""), true)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@Test
|
|
149
|
+
func testSqliteUnknownCommandRejected(): Unit {
|
|
150
|
+
let cmds = SqlitePlugin().commands()
|
|
151
|
+
let msg = sqliteErrOf(cmds.get("info").getOrThrow(), "sqlite:nope", sqlitePathArg(SQLITE_TEST_DB))
|
|
152
|
+
@Expect(msg.contains("unknown sqlite command"), true)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Test
|
|
156
|
+
func testSqliteBrowsingAMissingFileDoesNotCreateIt(): Unit {
|
|
157
|
+
sqliteCleanDb()
|
|
158
|
+
let cmds = SqlitePlugin().commands()
|
|
159
|
+
let msg = sqliteErrOf(cmds.get("info").getOrThrow(), "sqlite:info", sqlitePathArg(SQLITE_TEST_DB))
|
|
160
|
+
@Expect(msg.contains("no such database file"), true)
|
|
161
|
+
// 关键:浏览类命令**不能**顺手造一个空库出来(只读打开,文件不存在就是失败)
|
|
162
|
+
@Expect(exists(SQLITE_TEST_DB), false)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@Test
|
|
166
|
+
func testSqliteExecCreateInsertThenBrowse(): Unit {
|
|
167
|
+
sqliteCleanDb()
|
|
168
|
+
let cmds = SqlitePlugin().commands()
|
|
169
|
+
let exec = cmds.get("exec").getOrThrow()
|
|
170
|
+
let created = sqliteExec(exec, "CREATE TABLE items(id INTEGER PRIMARY KEY, name TEXT, price REAL)")
|
|
171
|
+
// 建表本身不需要改行数,但 lastInsertRowid 应可读(同一次 exec 的返回值)
|
|
172
|
+
@Expect(sqliteIntOf(created, "changes") >= 0, true)
|
|
173
|
+
let inserted = sqliteExec(exec, "INSERT INTO items(name, price) VALUES('甲', 1.5), ('乙', 2.5)")
|
|
174
|
+
@Expect(sqliteIntOf(inserted, "changes"), 2)
|
|
175
|
+
@Expect(sqliteIntOf(inserted, "lastInsertRowid"), 2)
|
|
176
|
+
|
|
177
|
+
// info:库文件存在、表数 1、版本非空
|
|
178
|
+
let info = cmds.get("info").getOrThrow().handle("sqlite:info", sqlitePathArg(SQLITE_TEST_DB),
|
|
179
|
+
IpcContext("main", IpcHub()))
|
|
180
|
+
@Expect(sqliteIntOf(info, "tableCount"), 1)
|
|
181
|
+
@Expect(sqliteStr(info, "sqliteVersion").size > 0, true)
|
|
182
|
+
@Expect(sqliteBoolOf(info, "attachBlocked"), true)
|
|
183
|
+
|
|
184
|
+
// tables:一张表、两行、三列
|
|
185
|
+
let tables = cmds.get("tables").getOrThrow().handle("sqlite:tables",
|
|
186
|
+
sqlitePathArg(SQLITE_TEST_DB), IpcContext("main", IpcHub()))
|
|
187
|
+
let list = sqliteField(tables, "tables").asArray()
|
|
188
|
+
@Expect(list.size(), 1)
|
|
189
|
+
@Expect(sqliteStr(list.get(0), "name"), "items")
|
|
190
|
+
@Expect(sqliteStr(list.get(0), "kind"), "table")
|
|
191
|
+
@Expect(sqliteIntOf(list.get(0), "rows"), 2)
|
|
192
|
+
@Expect(sqliteIntOf(list.get(0), "columns"), 3)
|
|
193
|
+
|
|
194
|
+
// schema:列名与建表原文都在
|
|
195
|
+
let schema = cmds.get("schema").getOrThrow().handle("sqlite:schema",
|
|
196
|
+
sqliteArgs([("path", JsonString(SQLITE_TEST_DB)), ("table", JsonString("items"))]),
|
|
197
|
+
IpcContext("main", IpcHub()))
|
|
198
|
+
let cols = sqliteField(schema, "columns").asArray()
|
|
199
|
+
@Expect(cols.size(), 3)
|
|
200
|
+
@Expect(sqliteStr(cols.get(0), "name"), "id")
|
|
201
|
+
@Expect(sqliteBoolOf(cols.get(0), "pk"), true)
|
|
202
|
+
@Expect(sqliteStr(cols.get(1), "name"), "name")
|
|
203
|
+
@Expect(sqliteStr(schema, "sql").contains("CREATE TABLE items"), true)
|
|
204
|
+
|
|
205
|
+
// page:分页取数带 total,单元格按类型给(id 整数 / name 文本 / price 浮点)
|
|
206
|
+
let page = cmds.get("page").getOrThrow().handle("sqlite:page",
|
|
207
|
+
sqliteArgs([("path", JsonString(SQLITE_TEST_DB)), ("table", JsonString("items")),
|
|
208
|
+
("limit", JsonInt(10))]), IpcContext("main", IpcHub()))
|
|
209
|
+
@Expect(sqliteIntOf(page, "total"), 2)
|
|
210
|
+
@Expect(sqliteIntOf(page, "limit"), 10)
|
|
211
|
+
@Expect(sqliteBoolOf(page, "limitClamped"), false)
|
|
212
|
+
let rows = sqliteField(page, "rows")
|
|
213
|
+
@Expect(rows.asArray().size(), 2)
|
|
214
|
+
@Expect(sqliteCellType(sqliteCell(rows, 0, 0)), "integer")
|
|
215
|
+
@Expect(sqliteCellType(sqliteCell(rows, 0, 1)), "text")
|
|
216
|
+
@Expect(sqliteCellType(sqliteCell(rows, 0, 2)), "float")
|
|
217
|
+
@Expect(sqliteStr(sqliteCell(rows, 0, 1), "value"), "甲")
|
|
218
|
+
|
|
219
|
+
// query:只读连接上跑 SELECT,NULL 与 BLOB 各自的形状都要对
|
|
220
|
+
sqliteExec(exec, "INSERT INTO items(name, price) VALUES(NULL, NULL)")
|
|
221
|
+
let qres = cmds.get("query").getOrThrow().handle("sqlite:query",
|
|
222
|
+
sqliteArgs([("path", JsonString(SQLITE_TEST_DB)),
|
|
223
|
+
("sql", JsonString("SELECT name FROM items WHERE id = 3"))]), IpcContext("main", IpcHub()))
|
|
224
|
+
@Expect(sqliteIntOf(qres, "rowCount"), 1)
|
|
225
|
+
@Expect(sqliteCellType(sqliteCell(sqliteField(qres, "rows"), 0, 0)), "null")
|
|
226
|
+
// 没给 value 的 NULL 不该被当成空串:value 键就不该存在
|
|
227
|
+
let nullCell = sqliteCell(sqliteField(qres, "rows"), 0, 0).asObject()
|
|
228
|
+
@Expect(nullCell.get("value").isNone(), true)
|
|
229
|
+
sqliteCleanDb()
|
|
230
|
+
}
|