Evan's Blog
EvanZhu0721 / 公开仓库

SBMS

镜像已同步

SBMS complete the calculation rules for the default logical desktop topology relationship and the physical dimensions of the displays in the Windows multi-screen collaboration system.

上游 GitHub
READ-ONLY MIRROR

README、源码与 Release 已保存到本站;源码视图固定到 commit a37d6c4b。

main/ root / tests
a37d6c4b· 固定源码快照
tests/cli_contract.rs9.9 KB
1use std::fs;
2use std::path::{Path, PathBuf};
3use std::process::{Command, Output};
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use serde_json::{Value, json};
7
8static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0);
9
10struct TestRoot(PathBuf);
11
12impl TestRoot {
13 fn new(name: &str) -> Self {
14 let path = std::env::temp_dir().join(format!(
15 "sbms-cli-contract-{}-{}-{name}",
16 std::process::id(),
17 TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed)
18 ));
19 fs::create_dir_all(&path).unwrap();
20 Self(path)
21 }
22
23 fn path(&self) -> &Path {
24 &self.0
25 }
26
27 fn config_path(&self) -> PathBuf {
28 self.0.join("SBMS").join("config-v2.json")
29 }
30
31 fn profiles_path(&self) -> PathBuf {
32 self.0.join("SBMS").join("config-profiles-v1.json")
33 }
34}
35
36impl Drop for TestRoot {
37 fn drop(&mut self) {
38 let _ = fs::remove_dir_all(&self.0);
39 }
40}
41
42fn run(root: &TestRoot, arguments: &[&str]) -> Output {
43 Command::new(env!("CARGO_BIN_EXE_sbms"))
44 .args(arguments)
45 .env("LOCALAPPDATA", root.path())
46 .output()
47 .unwrap()
48}
49
50fn text(bytes: &[u8]) -> String {
51 String::from_utf8_lossy(bytes).replace("\r\n", "\n")
52}
53
54fn usage() -> &'static str {
55 "usage:
56 sbms --version
57 sbms list
58 sbms map [--target <monitor-device-path>] [--hold-ms <milliseconds>]
59 sbms plan validate <plan.json>
60 sbms plan run <plan.json> [--hold-ms <milliseconds>]
61 sbms config path|list|show [<profile>]
62 sbms config save <profile>
63 sbms config import <profile> <file.json> [--replace] [--activate]
64 sbms config export <profile> <file.json> [--force]
65 sbms config activate|delete <profile>
66 sbms config reload
67 sbms config set-target <monitor-device-path>|clear-target|reset
68 sbms shutdown
69 sbms ui
70"
71}
72
73#[test]
74fn version_is_machine_readable() {
75 let root = TestRoot::new("version");
76 let output = run(&root, &["--version"]);
77
78 assert!(output.status.success());
79 assert_eq!(
80 text(&output.stdout),
81 format!("sbms {}\n", env!("CARGO_PKG_VERSION"))
82 );
83 assert_eq!(text(&output.stderr), "");
84}
85
86#[test]
87fn invalid_invocations_keep_the_usage_contract() {
88 for arguments in [&[][..], &["unknown"], &["--version", "extra"]] {
89 let root = TestRoot::new("usage");
90 let output = run(&root, arguments);
91
92 assert_eq!(output.status.code(), Some(2));
93 assert_eq!(text(&output.stdout), "");
94 assert_eq!(text(&output.stderr), usage());
95 }
96}
97
98#[test]
99fn config_path_and_missing_config_show_initializes_default_profile() {
100 let root = TestRoot::new("config-defaults");
101 let path_output = run(&root, &["config", "path"]);
102
103 assert!(path_output.status.success());
104 assert_eq!(
105 text(&path_output.stdout).trim_end(),
106 root.profiles_path().display().to_string()
107 );
108
109 let show_output = run(&root, &["config", "show"]);
110 assert!(show_output.status.success());
111 let shown: Value = serde_json::from_slice(&show_output.stdout).unwrap();
112 assert_eq!(shown["version"], 2);
113 assert_eq!(shown["groups"].as_array().unwrap().len(), 1);
114 assert_eq!(shown["groups"][0]["id"], 0);
115 assert_eq!(shown["selected_group_id"], 0);
116 assert!(root.profiles_path().exists());
117 assert!(!root.config_path().exists());
118}
119
120#[test]
121fn clear_target_only_changes_output_one() {
122 let root = TestRoot::new("clear-target");
123 let config_path = root.config_path();
124 fs::create_dir_all(config_path.parent().unwrap()).unwrap();
125 let original = json!({
126 "version": 2,
127 "groups": [
128 {
129 "id": 3,
130 "route": {
131 "kind": "stream_only",
132 "screen": {
133 "width": 1920,
134 "height": 1080,
135 "refresh_millihz": 60000,
136 "aspect_ratio": {"width": 16, "height": 9},
137 "rotation": "deg0"
138 }
139 }
140 },
141 {
142 "id": 0,
143 "route": {
144 "kind": "mirror",
145 "target_id": "stable-display-id"
146 }
147 },
148 {
149 "id": 7,
150 "route": {
151 "kind": "mirror"
152 }
153 }
154 ],
155 "selected_group_id": 3
156 });
157 fs::write(&config_path, serde_json::to_vec_pretty(&original).unwrap()).unwrap();
158
159 let output = run(&root, &["config", "clear-target"]);
160
161 assert!(output.status.success(), "{}", text(&output.stderr));
162 assert!(text(&output.stdout).starts_with("saved=default revision=2 reload="));
163 let profiles: Value = serde_json::from_slice(&fs::read(root.profiles_path()).unwrap()).unwrap();
164 let saved = &profiles["profiles"][0]["config"];
165 assert_eq!(saved["groups"][0], original["groups"][0]);
166 assert_eq!(saved["groups"][0]["id"], 3);
167 assert_eq!(saved["groups"][1]["id"], 0);
168 assert_eq!(saved["groups"][1]["route"], json!({"kind": "mirror"}));
169 assert_eq!(saved["groups"][2], original["groups"][2]);
170 assert_eq!(saved["selected_group_id"], 3);
171}
172
173#[test]
174fn clear_target_preserves_malformed_config_bytes() {
175 let root = TestRoot::new("bad-config");
176 let config_path = root.config_path();
177 fs::create_dir_all(config_path.parent().unwrap()).unwrap();
178 let original = b"{not json";
179 fs::write(&config_path, original).unwrap();
180
181 let output = run(&root, &["config", "clear-target"]);
182
183 assert_eq!(output.status.code(), Some(1));
184 assert!(text(&output.stderr).contains("cannot initialize config profiles"));
185 assert_eq!(fs::read(config_path).unwrap(), original);
186
187 let reset = run(&root, &["config", "reset"]);
188 assert!(reset.status.success(), "{}", text(&reset.stderr));
189 assert!(text(&reset.stdout).starts_with("reset=default revision=2 reload="));
190 assert_eq!(fs::read(root.config_path()).unwrap(), original);
191 let shown = run(&root, &["config", "show"]);
192 let config: Value = serde_json::from_slice(&shown.stdout).unwrap();
193 assert_eq!(config["version"], 2);
194 assert_eq!(config["groups"].as_array().unwrap().len(), 1);
195}
196
197#[test]
198fn config_profiles_import_activate_export_and_enforce_limit() {
199 let root = TestRoot::new("profiles");
200 let imported_path = root.path().join("Gaming 配置.json");
201 let imported = json!({
202 "version": 2,
203 "groups": [{
204 "id": 0,
205 "route": {"kind": "mirror", "target_id": "gaming-display"}
206 }],
207 "selected_group_id": 0
208 });
209 fs::write(
210 &imported_path,
211 serde_json::to_vec_pretty(&imported).unwrap(),
212 )
213 .unwrap();
214
215 let imported_output = run(
216 &root,
217 &[
218 "config",
219 "import",
220 "gaming",
221 imported_path.to_str().unwrap(),
222 "--activate",
223 ],
224 );
225 assert!(
226 imported_output.status.success(),
227 "{}",
228 text(&imported_output.stderr)
229 );
230 assert!(text(&imported_output.stdout).contains("imported=gaming"));
231
232 let shown = run(&root, &["config", "show"]);
233 assert_eq!(
234 serde_json::from_slice::<Value>(&shown.stdout).unwrap(),
235 imported
236 );
237
238 let exported_path = root.path().join("exported.json");
239 let exported = run(
240 &root,
241 &[
242 "config",
243 "export",
244 "gaming",
245 exported_path.to_str().unwrap(),
246 ],
247 );
248 assert!(exported.status.success(), "{}", text(&exported.stderr));
249 assert_eq!(
250 serde_json::from_slice::<Value>(&fs::read(exported_path).unwrap()).unwrap(),
251 imported
252 );
253
254 assert!(run(&root, &["config", "save", "third"]).status.success());
255 let fourth = run(&root, &["config", "save", "fourth"]);
256 assert_eq!(fourth.status.code(), Some(1));
257 assert!(text(&fourth.stderr).contains("3-profile limit"));
258
259 let listed = run(&root, &["config", "list"]);
260 let profiles: Value = serde_json::from_slice(&listed.stdout).unwrap();
261 assert_eq!(profiles.as_array().unwrap().len(), 3);
262 assert!(
263 profiles
264 .as_array()
265 .unwrap()
266 .iter()
267 .any(|profile| { profile["id"] == "gaming" && profile["active"] == true })
268 );
269}
270
271#[test]
272fn config_export_never_overwrites_store_or_existing_destination_without_force() {
273 let root = TestRoot::new("safe-export");
274 let initialized = run(&root, &["config", "show"]);
275 assert!(initialized.status.success());
276 let store_bytes = fs::read(root.profiles_path()).unwrap();
277
278 let self_export = run(
279 &root,
280 &[
281 "config",
282 "export",
283 "default",
284 root.profiles_path().to_str().unwrap(),
285 "--force",
286 ],
287 );
288 assert_eq!(self_export.status.code(), Some(1));
289 assert!(text(&self_export.stderr).contains("config profile store"));
290 assert_eq!(fs::read(root.profiles_path()).unwrap(), store_bytes);
291
292 let destination = root.path().join("existing.json");
293 fs::write(&destination, b"keep-me").unwrap();
294 let refused = run(
295 &root,
296 &["config", "export", "default", destination.to_str().unwrap()],
297 );
298 assert_eq!(refused.status.code(), Some(1));
299 assert_eq!(fs::read(destination).unwrap(), b"keep-me");
300}
301
302#[test]
303fn plan_validation_reports_valid_and_empty_plans() {
304 let root = TestRoot::new("plan");
305 let valid_path = Path::new(env!("CARGO_MANIFEST_DIR"))
306 .join("examples")
307 .join("portrait-stream.json");
308 let valid = run(&root, &["plan", "validate", valid_path.to_str().unwrap()]);
309
310 assert!(valid.status.success(), "{}", text(&valid.stderr));
311 assert_eq!(text(&valid.stdout), "valid_groups=1\n");
312
313 let empty_path = root.path().join("empty-plan.json");
314 fs::write(&empty_path, br#"{"groups":[]}"#).unwrap();
315 let empty = run(&root, &["plan", "validate", empty_path.to_str().unwrap()]);
316
317 assert_eq!(empty.status.code(), Some(1));
318 assert!(text(&empty.stderr).contains("at least one mapping group is required"));
319}
320