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.
tests/cli_contract.rs9.9 KB
1
use std::fs;2
use std::path::{Path, PathBuf};3
use std::process::{Command, Output};4
use std::sync::atomic::{AtomicU64, Ordering};5
6
use serde_json::{Value, json};7
8
static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0);9
10
struct TestRoot(PathBuf);11
12
impl 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.025
}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
36
impl Drop for TestRoot {37
fn drop(&mut self) {38
let _ = fs::remove_dir_all(&self.0);39
}40
}41
42
fn 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
50
fn text(bytes: &[u8]) -> String {51
String::from_utf8_lossy(bytes).replace("\r\n", "\n")52
}53
54
fn usage() -> &'static str {55
"usage:56
sbms --version57
sbms list58
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 reload67
sbms config set-target <monitor-device-path>|clear-target|reset68
sbms shutdown69
sbms ui70
"71
}72
73
#[test]74
fn 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]87
fn 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]99
fn 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]121
fn 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": 3156
});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]174
fn 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]198
fn 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": 0208
});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
imported236
);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
imported252
);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
profiles264
.as_array()265
.unwrap()266
.iter()267
.any(|profile| { profile["id"] == "gaming" && profile["active"] == true })268
);269
}270
271
#[test]272
fn 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]303
fn 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