Skip to main content

max / alloy

10.9 KB · 301 lines History Blame Raw
1 //! Tests for [`super`].
2
3 use super::super::config::config_file;
4 use super::super::fixtures::{FW12, external, fw12};
5 use super::*;
6
7 #[test]
8 fn parses_the_real_capture() {
9 let outputs = parse(FW12).unwrap();
10 assert_eq!(outputs.len(), 1);
11 let panel = &outputs[0];
12 assert_eq!(panel.name, "eDP-1");
13 assert_eq!(panel.make, "BOE");
14 assert_eq!(panel.model, "NV122WUM-N42");
15 assert!(panel.active && panel.dpms && panel.focused);
16 assert_eq!(panel.transform, "normal");
17 }
18
19 // sway substitutes the literal string "Unknown" for a field the panel does
20 // not report. A parser expecting null or an absent key mis-handles this
21 // panel, and the identifier rule below depends on noticing it.
22 #[test]
23 fn an_unknown_serial_is_a_string_not_a_null() {
24 assert_eq!(fw12().serial, "Unknown");
25 }
26
27 // Millihertz. 60002, not 60 and not 60.0.
28 #[test]
29 fn refresh_is_millihertz() {
30 let mode = fw12().current_mode.expect("the panel has a current mode");
31 assert_eq!(mode.refresh, 60002);
32 assert_eq!(mode.spelled(), "1920x1200@60.002Hz");
33 }
34
35 // `rect` is the logical size and `current_mode` the physical one, and at
36 // scale 1.25 they disagree by design. A view that showed one of them would
37 // make the scale look inert.
38 #[test]
39 fn the_logical_and_physical_sizes_both_survive() {
40 let panel = fw12();
41 assert_eq!((panel.rect.width, panel.rect.height), (1536, 960));
42 let mode = panel.current_mode.unwrap();
43 assert_eq!((mode.width, mode.height), (1920, 1200));
44 assert!((panel.scale - 1.25).abs() < f64::EPSILON);
45 }
46
47 // The panel advertises exactly one mode, which is why there is no mode
48 // picker. Pinned so that the day a capture with more than one arrives, the
49 // reason for the omission is visible in a diff.
50 #[test]
51 fn the_panel_advertises_exactly_one_mode() {
52 assert_eq!(fw12().modes.len(), 1);
53 }
54
55 #[test]
56 fn malformed_json_is_an_error() {
57 assert!(parse("not json").is_err());
58 assert!(parse("{}").is_err(), "an object is not a list of outputs");
59 }
60
61 // sway sends the fields this parser does not read, and it will send more
62 // next release. Ignoring them is the point of the derive.
63 #[test]
64 fn unknown_fields_are_ignored() {
65 let raw = r#"[{"name":"HDMI-A-1","something_new":{"nested":true}}]"#;
66 assert_eq!(parse(raw).unwrap()[0].name, "HDMI-A-1");
67 }
68
69 // A field sway stops sending must not read as "asleep" or "scale 0".
70 #[test]
71 fn absent_fields_take_the_safe_default() {
72 let output = parse(r#"[{"name":"DP-1"}]"#).unwrap().remove(0);
73 assert!(output.dpms, "an output sway does not describe as asleep");
74 assert!((output.scale - 1.0).abs() < f64::EPSILON);
75 assert!(output.modes.is_empty());
76 assert_eq!(output.current_mode, None);
77 }
78
79 // The identifier rule since 2026-08-06: the connector, always, for every
80 // output. The triple it replaced was vendor text in a file sway parses and
81 // was not unique across identical serial-less monitors; `monitors.rs` holds
82 // the reasoning and the replacement.
83 #[test]
84 fn every_output_is_matched_by_its_connector() {
85 let monitor = external("DP-3", "Example Co", "PA279CV", "K8LMQS032990");
86 assert_eq!(monitor.identifier(), "DP-3");
87 assert_eq!(fw12().identifier(), "eDP-1");
88 }
89
90 #[test]
91 fn every_laptop_panel_connector_reads_as_built_in() {
92 for name in ["eDP-1", "eDP-2", "LVDS-1", "DSI-1"] {
93 let mut panel = external(name, "BOE", "NV122WUM-N42", UNKNOWN);
94 panel.name = name.into();
95 assert!(panel.built_in(), "{name}");
96 assert_eq!(panel.identifier(), name);
97 }
98 assert!(!external("DP-3", "Example Co", "PA279CV", "S1").built_in());
99 }
100
101 /// The injection hazard, closed structurally rather than escaped: none of
102 /// these can reach a stanza by any route, because no EDID text is written
103 /// at all.
104 #[test]
105 fn no_edid_text_reaches_a_stanza_however_hostile() {
106 for make in [
107 "Ex\"Co",
108 "Ex\\Co",
109 "Ex\noutput * scale 3",
110 "Ex\rCo",
111 "Ex\tCo",
112 ] {
113 let output = external("DP-1", make, "PA279CV", "S1");
114 assert_eq!(output.identifier(), "DP-1", "{make:?}");
115 let line = Directive::new(&output, "scale", "2").line();
116 assert_eq!(line, "output DP-1 scale 2", "{make:?}");
117 }
118 }
119
120 /// The portability a triple would buy is not lost, it moved: two monitors
121 /// that differ only in punctuation are still two identities, and the
122 /// identity is a hash rather than something a config file has to hold.
123 #[test]
124 fn punctuation_still_distinguishes_two_monitors() {
125 let one = external("DP-1", "Example Co.", "PA279CV", "S1");
126 let two = external("DP-1", "Example Co", "PA279CV", "S1");
127 assert_ne!(one.fingerprint(), two.fingerprint());
128 assert!(one.fingerprint().is_some());
129 }
130
131 /// An output with nothing to identify it is remembered by nothing. There is
132 /// no fact about it that would survive a replug, so a table row would be a
133 /// row that cannot be right.
134 #[test]
135 fn an_anonymous_output_has_no_fingerprint() {
136 assert_eq!(
137 external("HDMI-A-1", UNKNOWN, UNKNOWN, UNKNOWN).fingerprint(),
138 None
139 );
140 assert_eq!(external("HDMI-A-1", "", "", "").fingerprint(), None);
141 // Serial alone is not identity: it is the field most often Unknown, and
142 // what is left is the two fields that were missing.
143 assert_eq!(
144 external("HDMI-A-1", UNKNOWN, UNKNOWN, "S1").fingerprint(),
145 None
146 );
147 }
148
149 /// The built-in panel cannot move, so it is not in the table.
150 #[test]
151 fn the_built_in_panel_has_no_fingerprint() {
152 assert_eq!(fw12().fingerprint(), None);
153 }
154
155 /// The tripwire that replaced the quoting function. Connector names pass;
156 /// anything carrying the old hazards does not, which is what would fire if
157 /// vendor text ever found its way back into a stanza.
158 #[test]
159 fn a_stanza_identifier_is_one_word() {
160 assert!(is_one_word("eDP-1"));
161 assert!(is_one_word("HDMI-A-1"));
162 assert!(!is_one_word("BOE NV122WUM-N42 Unknown"));
163 assert!(!is_one_word("Ex\"Co"));
164 assert!(!is_one_word("Ex\\Co"));
165 assert!(!is_one_word("Ex\nCo"));
166 assert!(!is_one_word(""));
167 }
168
169 // The load-bearing property of the whole design: the words that apply the
170 // change and the words that persist it are the same words.
171 #[test]
172 fn the_runtime_command_and_the_config_line_are_the_same_words() {
173 let directive = Directive::new(&fw12(), "scale", "1.25");
174 assert_eq!(directive.line(), "output eDP-1 scale 1.25");
175 assert_eq!(
176 directive.invocation().display(),
177 "swaymsg output eDP-1 scale 1.25",
178 );
179 assert_eq!(
180 directive.invocation().display(),
181 format!("swaymsg {}", directive.line()),
182 );
183 }
184
185 // swaymsg joins its argv with spaces and hands the result to the same
186 // parser that reads a config file, and its `join_args` adds no quoting of
187 // its own. That used to mean a three-word triple had to carry quotes in the
188 // string, in both consumers; with connector names there is nothing to
189 // quote, and the property to hold is that neither consumer adds any.
190 #[test]
191 fn neither_consumer_quotes_a_connector_name() {
192 let monitor = external("DP-3", "Example Co", "PA279CV", "K8LMQS032990");
193 let directive = Directive::new(&monitor, "scale", "2");
194 assert_eq!(directive.line(), "output DP-3 scale 2");
195 assert_eq!(
196 directive.invocation().display(),
197 "swaymsg output DP-3 scale 2",
198 "the log pane's single quotes appear only around an argument with \
199 whitespace in it, and there is none left",
200 );
201 }
202
203 // `scale 1.250000` is the same instruction spelled to look machine-written,
204 // and the shared string is only worth having if a person can paste it.
205 #[test]
206 fn scales_are_spelled_the_way_a_person_would_type_them() {
207 assert_eq!(spell_scale(1.0), "1");
208 assert_eq!(spell_scale(1.25), "1.25");
209 assert_eq!(spell_scale(1.5), "1.5");
210 assert_eq!(spell_scale(2.0), "2");
211 }
212
213 #[test]
214 fn the_scale_key_always_moves() {
215 let mut panel = fw12();
216 assert!((panel.next_scale() - 1.5).abs() < f64::EPSILON);
217 panel.scale = 2.0;
218 assert!(
219 (panel.next_scale() - 1.0).abs() < f64::EPSILON,
220 "the top rung wraps rather than dead-ending",
221 );
222 // A scale set outside the console lands on the next rung above it.
223 panel.scale = 1.1;
224 assert!((panel.next_scale() - 1.25).abs() < f64::EPSILON);
225 }
226
227 // `transform normal` is sway's default, so writing it says nothing and reads
228 // as though the console had an opinion about rotation.
229 #[test]
230 fn only_a_rotation_that_is_not_the_default_is_written() {
231 let mut panel = fw12();
232 assert!(!config_file(&[panel.clone()]).contains("transform"));
233 panel.transform = "90".into();
234 assert!(config_file(&[panel]).contains("output eDP-1 transform 90"));
235 }
236
237 // An output the user turned off has to stay off across a reboot, or the key
238 // did not do what it said.
239 #[test]
240 fn a_disabled_output_is_persisted_as_disabled() {
241 let mut monitor = external("DP-3", "Example Co", "PA279CV", "S1");
242 monitor.active = false;
243 let file = config_file(&[fw12(), monitor]);
244 assert!(file.contains("scale 1"), "{file}");
245 assert!(file.contains("output DP-3 enable false"), "{file}");
246 }
247
248 // The one edit that costs the session with no way back, and in a per-user
249 // config file it survives the reboot that would otherwise recover it.
250 #[test]
251 fn the_last_usable_output_cannot_be_switched_off() {
252 let panel = fw12();
253 let reason = refuse(
254 std::slice::from_ref(&panel),
255 &panel,
256 &Change::Enabled(false),
257 )
258 .expect("refused with a reason");
259 assert!(reason.contains("eDP-1"), "{reason}");
260 }
261
262 #[test]
263 fn switching_one_off_is_allowed_while_another_is_usable() {
264 let panel = fw12();
265 let monitor = external("DP-3", "Example Co", "PA279CV", "S1");
266 let outputs = vec![panel.clone(), monitor];
267 assert!(refuse(&outputs, &panel, &Change::Enabled(false)).is_none());
268 }
269
270 // A second output that is present but asleep or disabled is not a screen the
271 // user can read, so it does not license switching off the one that is.
272 #[test]
273 fn an_asleep_second_output_does_not_count_as_a_screen() {
274 let panel = fw12();
275 let mut dark = external("DP-3", "Example Co", "PA279CV", "S1");
276 dark.dpms = false;
277 let outputs = vec![panel.clone(), dark];
278 assert!(refuse(&outputs, &panel, &Change::Enabled(false)).is_some());
279 }
280
281 // The indirect way to lose the session: a scale that leaves no readable
282 // geometry. 1920 physical pixels at scale 4 is 480 logical, which is a dark
283 // screen with extra steps.
284 #[test]
285 fn a_scale_that_leaves_no_usable_geometry_is_refused() {
286 let panel = fw12();
287 assert!(refuse(std::slice::from_ref(&panel), &panel, &Change::Scale(4.0)).is_some());
288 assert!(refuse(std::slice::from_ref(&panel), &panel, &Change::Scale(2.0)).is_none());
289 }
290
291 // An output sway has not described has no geometry to check. Refusing on
292 // that would block the key on every such output, which is worse than a
293 // scale that has to be pressed twice.
294 #[test]
295 fn a_scale_on_an_output_with_no_mode_is_allowed() {
296 let mut unknown = external("DP-9", "", "", "");
297 unknown.current_mode = None;
298 unknown.modes = Vec::new();
299 assert!(refuse(&[unknown.clone()], &unknown, &Change::Scale(3.0)).is_none());
300 }
301