1use core::fmt;
50use core::str::FromStr;
51use std::collections::{BTreeMap, BTreeSet};
52#[cfg(feature = "fs")]
53use std::path::Path;
54
55use rowan::{TextRange, TextSize};
56
57pub const FILE_NAME: &str = "interfaces.lock";
59
60pub const HEADER: &str = "# interfaces.lock — written by ridl lock; do not edit by hand.";
63
64#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
70pub enum LockKey {
71 Interface(String),
73 Service(String),
76}
77
78impl fmt::Display for LockKey {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 LockKey::Interface(name) => f.write_str(name),
82 LockKey::Service(name) => write!(f, "service:{name}"),
83 }
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct InvalidLockKey(pub String);
90
91impl fmt::Display for InvalidLockKey {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 f.write_str(&self.0)
94 }
95}
96
97impl std::error::Error for InvalidLockKey {}
98
99impl FromStr for LockKey {
100 type Err = InvalidLockKey;
101
102 fn from_str(text: &str) -> Result<Self, Self::Err> {
110 if let Some(name) = text.strip_prefix("service:") {
111 if !name.is_empty() && name.split('.').all(is_ident) {
112 Ok(LockKey::Service(name.to_string()))
113 } else {
114 Err(InvalidLockKey(format!(
115 "`{text}` is not a lock key: a service key is `service:` followed by a dotted name"
116 )))
117 }
118 } else if text.split('.').all(is_ident) {
119 Ok(LockKey::Interface(text.to_string()))
120 } else {
121 Err(InvalidLockKey(format!(
122 "`{text}` is not a lock key: an interface key is a dotted name of identifiers"
123 )))
124 }
125 }
126}
127
128fn is_ident(text: &str) -> bool {
130 let mut bytes = text.bytes();
131 bytes
132 .next()
133 .is_some_and(|first| first.is_ascii_alphabetic())
134 && bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Hash)]
139pub struct LockEntry {
140 pub key: LockKey,
141 pub number: u32,
143 pub retired: bool,
144 pub range: TextRange,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Hash)]
153pub struct InterfaceLock {
154 pub next: u32,
156 pub entries: Vec<LockEntry>,
159}
160
161impl Default for InterfaceLock {
162 fn default() -> Self {
165 Self {
166 next: 1,
167 entries: Vec::new(),
168 }
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq)]
177pub struct LockError {
178 pub message: String,
179 pub range: TextRange,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
184pub enum LockEditError {
185 NoLiveEntry(LockKey),
188 KeyAlreadyLive(LockKey),
190}
191
192impl fmt::Display for LockEditError {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 match self {
195 LockEditError::NoLiveEntry(key) => {
196 write!(f, "no live entry `{key}` in {FILE_NAME}")
197 }
198 LockEditError::KeyAlreadyLive(key) => {
199 write!(f, "`{key}` is already a live entry in {FILE_NAME}")
200 }
201 }
202 }
203}
204
205impl std::error::Error for LockEditError {}
206
207pub fn parse(text: &str) -> Result<InterfaceLock, LockError> {
221 let mut next: Option<u32> = None;
222 let mut entries: Vec<LockEntry> = Vec::new();
223 let mut offset = 0usize;
224
225 for raw_line in text.split_inclusive('\n') {
226 let start = offset;
227 offset += raw_line.len();
228 let line = raw_line.trim_end();
229 if line.is_empty() || line.trim_start().starts_with('#') {
230 continue;
231 }
232 let range = byte_range(start, start + line.len());
233
234 let Some(bound) = next else {
235 next = Some(parse_next_line(line, range)?);
236 continue;
237 };
238 let entry = parse_entry_line(line, range)?;
239 if entry.number >= bound {
240 return Err(LockError {
241 message: format!(
242 "`next` is {bound}, which is not greater than the number {} of `{}`",
243 entry.number, entry.key
244 ),
245 range,
246 });
247 }
248 if let Some(other) = entries.iter().find(|other| other.number == entry.number) {
249 return Err(LockError {
250 message: format!(
251 "number {} is on two entries: `{}` and `{}`",
252 entry.number, other.key, entry.key
253 ),
254 range,
255 });
256 }
257 if !entry.retired
258 && let Some(other) = entries
259 .iter()
260 .find(|other| !other.retired && other.key == entry.key)
261 {
262 return Err(LockError {
263 message: format!(
264 "live key `{}` is on two entries: {} and {}",
265 entry.key, other.number, entry.number
266 ),
267 range,
268 });
269 }
270 entries.push(entry);
271 }
272
273 match next {
274 Some(next) => Ok(InterfaceLock { next, entries }),
275 None => Err(LockError {
276 message: "no `next` line: the first line after the header must be `next N`".to_string(),
277 range: TextRange::default(),
278 }),
279 }
280}
281
282fn parse_next_line(line: &str, range: TextRange) -> Result<u32, LockError> {
286 let mut fields = line.split(' ');
287 if fields.next() != Some("next") {
288 return Err(LockError {
289 message: "no `next` line: the first line after the header must be `next N`".to_string(),
290 range: TextRange::default(),
291 });
292 }
293 match (fields.next().and_then(parse_number), fields.next()) {
294 (Some(next), None) => Ok(next),
295 _ => Err(LockError {
296 message: "the `next` line does not parse: expected `next N` with N at least 1"
297 .to_string(),
298 range,
299 }),
300 }
301}
302
303fn parse_entry_line(line: &str, range: TextRange) -> Result<LockEntry, LockError> {
306 let does_not_parse = |reason: &str| LockError {
307 message: format!("line does not parse: {reason}"),
308 range,
309 };
310 let mut fields = line.split(' ');
311 let key = fields
312 .next()
313 .unwrap_or_default()
314 .parse::<LockKey>()
315 .map_err(|InvalidLockKey(reason)| does_not_parse(&reason))?;
316 let number = fields
317 .next()
318 .and_then(parse_number)
319 .ok_or_else(|| does_not_parse("expected `Key N` with N at least 1"))?;
320 let retired = match fields.next() {
321 None => false,
322 Some("retired") => true,
323 Some(_) => {
324 return Err(does_not_parse(
325 "expected `retired` or the end of the line after the number",
326 ));
327 }
328 };
329 if fields.next().is_some() {
330 return Err(does_not_parse("unexpected text after `retired`"));
331 }
332 Ok(LockEntry {
333 key,
334 number,
335 retired,
336 range,
337 })
338}
339
340fn parse_number(text: &str) -> Option<u32> {
343 if text.is_empty() || text.starts_with('0') || !text.bytes().all(|b| b.is_ascii_digit()) {
344 return None;
345 }
346 text.parse().ok()
347}
348
349fn byte_range(start: usize, end: usize) -> TextRange {
350 TextRange::new(TextSize::from(start as u32), TextSize::from(end as u32))
351}
352
353impl InterfaceLock {
354 pub fn render(&self) -> String {
357 let mut out = format!("{HEADER}\nnext {}\n", self.next);
358 let mut entries: Vec<&LockEntry> = self.entries.iter().collect();
359 entries.sort_by_key(|entry| entry.number);
360 for entry in entries {
361 push_line(&mut out, entry);
362 }
363 out
364 }
365
366 pub fn live(&self, key: &LockKey) -> Option<&LockEntry> {
368 self.entries
369 .iter()
370 .find(|entry| !entry.retired && entry.key == *key)
371 }
372
373 pub fn max_number(&self) -> u32 {
376 self.entries
377 .iter()
378 .map(|entry| entry.number)
379 .max()
380 .unwrap_or(0)
381 }
382
383 pub fn allocate(&mut self, key: LockKey) -> Result<u32, LockEditError> {
387 if self.live(&key).is_some() {
388 return Err(LockEditError::KeyAlreadyLive(key));
389 }
390 let number = self.next;
391 self.next = number
392 .checked_add(1)
393 .expect("interface numbers stay far below u32::MAX");
394 self.entries.push(LockEntry {
395 key,
396 number,
397 retired: false,
398 range: TextRange::default(),
399 });
400 Ok(number)
401 }
402
403 pub fn rename(&mut self, old: &LockKey, new: LockKey) -> Result<u32, LockEditError> {
406 let index = self
407 .entries
408 .iter()
409 .position(|entry| !entry.retired && entry.key == *old)
410 .ok_or_else(|| LockEditError::NoLiveEntry(old.clone()))?;
411 if self.live(&new).is_some() {
412 return Err(LockEditError::KeyAlreadyLive(new));
413 }
414 let entry = &mut self.entries[index];
415 entry.key = new;
416 Ok(entry.number)
417 }
418
419 pub fn retire(&mut self, key: &LockKey) -> Result<u32, LockEditError> {
422 let entry = self
423 .entries
424 .iter_mut()
425 .find(|entry| !entry.retired && entry.key == *key)
426 .ok_or_else(|| LockEditError::NoLiveEntry(key.clone()))?;
427 entry.retired = true;
428 Ok(entry.number)
429 }
430}
431
432fn push_line(out: &mut String, entry: &LockEntry) {
435 out.push_str(&entry.key.to_string());
436 out.push(' ');
437 out.push_str(&entry.number.to_string());
438 if entry.retired {
439 out.push_str(" retired");
440 }
441 out.push('\n');
442}
443
444#[derive(Debug, Clone, PartialEq, Eq)]
446pub enum MergeOutcome {
447 Clean(InterfaceLock),
450 Conflict { text: String },
454}
455
456#[derive(Clone, Copy, PartialEq, Eq)]
458enum Side {
459 Ours,
460 Theirs,
461 Both,
462}
463
464struct Conflict {
467 at: u32,
468 ours: Vec<LockEntry>,
469 theirs: Vec<LockEntry>,
470}
471
472pub fn merge(
499 base: &InterfaceLock,
500 ours: &InterfaceLock,
501 theirs: &InterfaceLock,
502 marker_size: usize,
503) -> MergeOutcome {
504 let base_at = by_number(base);
505 let ours_at = by_number(ours);
506 let theirs_at = by_number(theirs);
507 let numbers: BTreeSet<u32> = base_at
508 .keys()
509 .chain(ours_at.keys())
510 .chain(theirs_at.keys())
511 .copied()
512 .collect();
513
514 let mut next = base.next.max(ours.next).max(theirs.next);
515 let mut resolved: Vec<(Side, LockEntry)> = Vec::new();
516 let mut conflicts: Vec<Conflict> = Vec::new();
517 for number in numbers {
518 let b = base_at.get(&number).copied();
519 let o = ours_at.get(&number).copied();
520 let t = theirs_at.get(&number).copied();
521 if agree(o, t) {
522 if let Some(entry) = o {
523 resolved.push((Side::Both, fresh(entry)));
524 }
525 } else if agree(o, b) {
526 if let Some(entry) = t {
527 resolved.push((Side::Theirs, fresh(entry)));
528 }
529 } else if agree(t, b) {
530 if let Some(entry) = o {
531 resolved.push((Side::Ours, fresh(entry)));
532 }
533 } else if let (None, Some(ours_entry), Some(theirs_entry)) = (b, o, t) {
534 resolved.push((Side::Ours, fresh(ours_entry)));
535 let mut renumbered = fresh(theirs_entry);
536 renumbered.number = next;
537 next = next
538 .checked_add(1)
539 .expect("interface numbers stay far below u32::MAX");
540 resolved.push((Side::Theirs, renumbered));
541 } else {
542 conflicts.push(Conflict {
543 at: number,
544 ours: o.map(fresh).into_iter().collect(),
545 theirs: t.map(fresh).into_iter().collect(),
546 });
547 }
548 }
549
550 let mut by_key: BTreeMap<&LockKey, Vec<usize>> = BTreeMap::new();
553 for (index, (_, entry)) in resolved.iter().enumerate() {
554 if !entry.retired {
555 by_key.entry(&entry.key).or_default().push(index);
556 }
557 }
558 let duplicated: Vec<Vec<usize>> = by_key
559 .into_values()
560 .filter(|indices| indices.len() > 1)
561 .collect();
562 let mut in_conflict = vec![false; resolved.len()];
563 for indices in duplicated {
564 let mut conflict = Conflict {
565 at: u32::MAX,
566 ours: Vec::new(),
567 theirs: Vec::new(),
568 };
569 for index in indices {
570 in_conflict[index] = true;
571 let (side, entry) = &resolved[index];
572 conflict.at = conflict.at.min(entry.number);
573 if *side != Side::Theirs {
574 conflict.ours.push(entry.clone());
575 }
576 if *side != Side::Ours {
577 conflict.theirs.push(entry.clone());
578 }
579 }
580 conflicts.push(conflict);
581 }
582
583 let mut entries: Vec<LockEntry> = resolved
584 .into_iter()
585 .zip(in_conflict)
586 .filter(|(_, taken)| !taken)
587 .map(|((_, entry), _)| entry)
588 .collect();
589 entries.sort_by_key(|entry| entry.number);
590 if conflicts.is_empty() {
591 return MergeOutcome::Clean(InterfaceLock { next, entries });
592 }
593
594 let mut text = format!("{HEADER}\nnext {next}\n");
596 conflicts.sort_by_key(|conflict| conflict.at);
597 let mut conflicts = conflicts.into_iter().peekable();
598 for entry in &entries {
599 while conflicts
600 .peek()
601 .is_some_and(|conflict| conflict.at < entry.number)
602 {
603 push_conflict(&mut text, &conflicts.next().expect("peeked"), marker_size);
604 }
605 push_line(&mut text, entry);
606 }
607 for conflict in conflicts {
608 push_conflict(&mut text, &conflict, marker_size);
609 }
610 MergeOutcome::Conflict { text }
611}
612
613fn by_number(lock: &InterfaceLock) -> BTreeMap<u32, &LockEntry> {
614 lock.entries
615 .iter()
616 .map(|entry| (entry.number, entry))
617 .collect()
618}
619
620fn agree(a: Option<&LockEntry>, b: Option<&LockEntry>) -> bool {
623 match (a, b) {
624 (None, None) => true,
625 (Some(a), Some(b)) => a.key == b.key && a.retired == b.retired,
626 _ => false,
627 }
628}
629
630fn fresh(entry: &LockEntry) -> LockEntry {
632 LockEntry {
633 range: TextRange::default(),
634 ..entry.clone()
635 }
636}
637
638fn push_conflict(out: &mut String, conflict: &Conflict, marker_size: usize) {
639 out.push_str(&"<".repeat(marker_size));
640 out.push_str(" ours\n");
641 for entry in &conflict.ours {
642 push_line(out, entry);
643 }
644 out.push_str(&"=".repeat(marker_size));
645 out.push('\n');
646 for entry in &conflict.theirs {
647 push_line(out, entry);
648 }
649 out.push_str(&">".repeat(marker_size));
650 out.push_str(" theirs\n");
651}
652
653#[cfg(feature = "fs")]
658pub fn read(dir: &Path) -> std::io::Result<Option<String>> {
659 match std::fs::read_to_string(dir.join(FILE_NAME)) {
660 Ok(text) => Ok(Some(text)),
661 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
662 Err(err) => Err(err),
663 }
664}
665
666#[cfg(feature = "fs")]
668pub fn write(dir: &Path, lock: &InterfaceLock) -> std::io::Result<()> {
669 std::fs::write(dir.join(FILE_NAME), lock.render())
670}
671
672#[cfg(test)]
673mod tests {
674 use super::*;
675
676 const EXAMPLE: &str = "\
678# interfaces.lock — written by ridl lock; do not edit by hand.
679next 6
680CruiseControl 1
681LaneAssist 2 retired
682LaneKeeping 3
683DoorControl 4
684service:veh.hvac.cabin 5
685";
686
687 #[test]
688 fn an_interface_key_is_a_dotted_relative_name() {
689 assert_eq!(
690 key("cluster.SpeedDisplay"),
691 LockKey::Interface("cluster.SpeedDisplay".into())
692 );
693 assert_eq!(key("Session"), LockKey::Interface("Session".into()));
694 for bad in ["cluster..Speed", ".Speed", "cluster.", "a-b.Speed"] {
695 assert!(bad.parse::<LockKey>().is_err(), "{bad}");
696 }
697 }
698
699 #[test]
700 fn a_dotted_key_round_trips_through_parse_and_render() {
701 let lock = parse("next 3\ncluster.Speed 1\nSession 2\n").unwrap();
702 let text = lock.render();
703 assert!(
704 text.ends_with("next 3\ncluster.Speed 1\nSession 2\n"),
705 "{text}"
706 );
707 assert_eq!(parse(&text).unwrap().render(), text);
708 }
709
710 fn key(text: &str) -> LockKey {
711 text.parse().expect("a valid lock key")
712 }
713
714 fn range(start: usize, end: usize) -> TextRange {
715 TextRange::new(TextSize::from(start as u32), TextSize::from(end as u32))
716 }
717
718 fn line_range(text: &str, line: &str) -> TextRange {
720 let start = text.find(line).expect("the line is in the text");
721 range(start, start + line.len())
722 }
723
724 fn malformed(text: &str) -> LockError {
725 parse(text).expect_err("the text is malformed")
726 }
727
728 #[test]
731 fn a_rendered_lock_parses_back_to_the_same_table() {
732 let lock = parse(EXAMPLE).expect("the design's example parses");
733
734 assert_eq!(lock.next, 6);
735 assert_eq!(lock.entries.len(), 5);
736 assert_eq!(
737 lock.entries[0].key,
738 LockKey::Interface("CruiseControl".to_string())
739 );
740 assert_eq!(lock.entries[0].number, 1);
741 assert!(!lock.entries[0].retired);
742 assert!(lock.entries[1].retired, "`LaneAssist 2 retired` is retired");
743 assert_eq!(
744 lock.entries[4].key,
745 LockKey::Service("veh.hvac.cabin".to_string()),
746 "the `service:` prefix keys the inline shape"
747 );
748
749 assert_eq!(
750 lock.render(),
751 EXAMPLE,
752 "the writer reproduces the file byte for byte"
753 );
754 assert_eq!(
755 parse(&lock.render()).expect("the rendered text parses"),
756 lock
757 );
758 }
759
760 #[test]
764 fn an_entry_range_covers_its_line() {
765 let lock = parse(EXAMPLE).expect("the design's example parses");
766 assert_eq!(
767 lock.entries[1].range,
768 line_range(EXAMPLE, "LaneAssist 2 retired")
769 );
770 assert_eq!(
771 lock.entries[4].range,
772 line_range(EXAMPLE, "service:veh.hvac.cabin 5")
773 );
774 }
775
776 #[test]
779 fn header_comment_and_blank_lines_are_skipped() {
780 let text = "# interfaces.lock — written by ridl lock; do not edit by hand.\n\
781 \n\
782 \x20 # an indented comment\n\
783 next 3 \n\
784 A 1\r\n\
785 \t\n\
786 B 2\t";
787 let lock = parse(text).expect("comments, blank lines and trailing whitespace are skipped");
788 assert_eq!(lock.next, 3);
789 assert_eq!(
790 lock.entries
791 .iter()
792 .map(|entry| (entry.key.to_string(), entry.number))
793 .collect::<Vec<_>>(),
794 [("A".to_string(), 1), ("B".to_string(), 2)]
795 );
796 assert_eq!(lock.entries[0].range, line_range(text, "A 1"));
797 assert_eq!(lock.entries[1].range, line_range(text, "B 2"));
798 }
799
800 #[test]
806 fn a_missing_next_line_is_malformed() {
807 for text in ["", "# a header only\n\n", "CruiseControl 1\nnext 2\n"] {
808 let error = malformed(text);
809 assert!(
810 error.message.contains("no `next` line"),
811 "{text:?} must report the missing `next` line, got: {}",
812 error.message
813 );
814 assert_eq!(error.range, range(0, 0), "{text:?} is reported at 0..0");
815 }
816 for text in ["next\n", "next 0\n", "next x\n", "next 3 4\n", "next 03\n"] {
817 let error = malformed(text);
818 assert!(
819 error.message.contains("`next` line does not parse"),
820 "{text:?} must report the bad `next` line, got: {}",
821 error.message
822 );
823 assert_eq!(error.range, line_range(text, text.trim_end()));
824 }
825 }
826
827 #[test]
828 fn a_next_not_above_every_entry_is_malformed() {
829 let text = "next 3\nA 1\nB 3\nC 2\n";
830 let error = malformed(text);
831 assert_eq!(
832 error.message,
833 "`next` is 3, which is not greater than the number 3 of `B`"
834 );
835 assert_eq!(
836 error.range,
837 line_range(text, "B 3"),
838 "the first offending line"
839 );
840
841 let error = malformed("next 2\nA 5 retired\n");
842 assert_eq!(
843 error.message, "`next` is 2, which is not greater than the number 5 of `A`",
844 "a retired entry is bounded too"
845 );
846 }
847
848 #[test]
849 fn one_number_on_two_entries_is_malformed() {
850 let text = "next 3\nA 1\nB 2\nC 1\n";
851 let error = malformed(text);
852 assert_eq!(error.message, "number 1 is on two entries: `A` and `C`");
853 assert_eq!(
854 error.range,
855 line_range(text, "C 1"),
856 "the second entry's line"
857 );
858
859 let error = malformed("next 3\nA 1 retired\nB 1\n");
860 assert_eq!(
861 error.message, "number 1 is on two entries: `A` and `B`",
862 "a retired entry holds its number too"
863 );
864 }
865
866 #[test]
867 fn one_live_key_on_two_entries_is_malformed() {
868 let text = "next 4\nA 1\nB 2\nA 3\n";
869 let error = malformed(text);
870 assert_eq!(error.message, "live key `A` is on two entries: 1 and 3");
871 assert_eq!(
872 error.range,
873 line_range(text, "A 3"),
874 "the second entry's line"
875 );
876
877 let error = malformed("next 3\nservice:x 1\nservice:x 2\n");
878 assert_eq!(
879 error.message,
880 "live key `service:x` is on two entries: 1 and 2"
881 );
882 }
883
884 #[test]
887 fn a_retired_key_may_be_live_again_on_another_number() {
888 let lock = parse("next 3\nA 1 retired\nA 2\n").expect("a retired name may be live again");
889 assert_eq!(lock.live(&key("A")).map(|entry| entry.number), Some(2));
890
891 let lock = parse("next 3\nA 1 retired\nA 2 retired\n")
892 .expect("a name retired twice is two retired entries");
893 assert_eq!(lock.live(&key("A")), None);
894 }
895
896 #[test]
897 fn a_line_that_does_not_parse_is_malformed() {
898 for line in [
899 "A",
900 "A x",
901 "A 0",
902 "A 01",
903 "A 1 dead",
904 "A 1 retired extra",
905 " A 1",
906 "A 1",
907 "A-B 1",
908 "9A 1",
909 "service: 1",
910 "service:a..b 1",
911 "service:a.b. 1",
912 ] {
913 let text = format!("next 3\n{line}\n");
914 let error = malformed(&text);
915 assert!(
916 error.message.contains("does not parse"),
917 "{line:?} must be a line that does not parse, got: {}",
918 error.message
919 );
920 assert_eq!(
921 error.range,
922 line_range(&text, line),
923 "{line:?} is reported on its line"
924 );
925 }
926 }
927
928 #[test]
931 fn git_conflict_markers_are_malformed() {
932 let text = "\
933# interfaces.lock — written by ridl lock; do not edit by hand.
934next 4
935A 1
936<<<<<<< HEAD
937B 2
938=======
939Bee 2
940>>>>>>> feature
941C 3
942";
943 let error = malformed(text);
944 assert!(
945 error.message.contains("does not parse"),
946 "got: {}",
947 error.message
948 );
949 assert_eq!(error.range, line_range(text, "<<<<<<< HEAD"));
950
951 for marker in [
952 "<<<<<<< HEAD",
953 "=======",
954 "||||||| merged common ancestors",
955 ">>>>>>> feature",
956 ] {
957 let text = format!("next 2\n{marker}\nA 1\n");
958 let error = malformed(&text);
959 assert!(
960 error.message.contains("does not parse"),
961 "{marker:?}: {}",
962 error.message
963 );
964 assert_eq!(error.range, line_range(&text, marker));
965 }
966 }
967
968 #[test]
972 fn cabin_and_service_cabin_are_two_keys() {
973 let lock = parse("next 3\ncabin 1\nservice:cabin 2\n").expect("two distinct keys");
974 assert_eq!(key("cabin"), LockKey::Interface("cabin".to_string()));
975 assert_eq!(key("service:cabin"), LockKey::Service("cabin".to_string()));
976 assert_ne!(key("cabin"), key("service:cabin"));
977 assert_eq!(lock.live(&key("cabin")).map(|entry| entry.number), Some(1));
978 assert_eq!(
979 lock.live(&key("service:cabin")).map(|entry| entry.number),
980 Some(2)
981 );
982 }
983
984 #[test]
985 fn lock_key_display_and_from_str_agree() {
986 for text in [
987 "CruiseControl",
988 "service:veh.hvac.cabin",
989 "cabin",
990 "service:cabin",
991 "A_B9",
992 "next",
993 "service:next",
994 ] {
995 assert_eq!(key(text).to_string(), text);
996 }
997 for text in [
998 "",
999 "service:",
1000 "a b",
1001 "service:a..b",
1002 "9x",
1003 "-",
1004 "service:Ab-c",
1005 "Service:x",
1006 ] {
1007 assert!(
1008 text.parse::<LockKey>().is_err(),
1009 "{text:?} is not a lock key"
1010 );
1011 }
1012 }
1013
1014 #[test]
1018 fn an_interface_named_next_round_trips() {
1019 let text = format!("{HEADER}\nnext 4\nA 1\nnext 3\n");
1020 let lock = parse(&text).expect("an entry keyed `next` parses");
1021 assert_eq!(lock.next, 4);
1022 assert_eq!(lock.live(&key("next")).map(|entry| entry.number), Some(3));
1023 assert_eq!(lock.entries[1].range, line_range(&text, "next 3"));
1024 assert_eq!(lock.render(), text, "the entry is written back as `next 3`");
1025
1026 let mut lock = InterfaceLock::default();
1027 assert_eq!(lock.allocate(key("next")), Ok(1));
1028 assert_eq!(lock.render(), format!("{HEADER}\nnext 2\nnext 1\n"));
1029 let reread = parse(&lock.render()).expect("the rendered lock parses");
1030 assert_eq!(reread.next, 2);
1031 assert_eq!(reread.live(&key("next")).map(|entry| entry.number), Some(1));
1032
1033 let error = malformed("next 3\nA 1\nnext 5\n");
1037 assert_eq!(
1038 error.message,
1039 "`next` is 3, which is not greater than the number 5 of `next`"
1040 );
1041 assert_eq!(error.range, line_range("next 3\nA 1\nnext 5\n", "next 5"));
1042 assert!(
1043 malformed("next 3\nA 1\nnext 3\n")
1044 .message
1045 .starts_with("`next` is 3, which is not greater"),
1046 "an equal number is not below `next` either"
1047 );
1048 }
1049
1050 #[test]
1055 fn render_writes_entries_in_number_order_with_one_newline_each() {
1056 let lock = parse("next 4\nC 3\nA 1\nB 2 retired").expect("entries may arrive in any order");
1057 assert_eq!(
1058 lock.render(),
1059 format!("{HEADER}\nnext 4\nA 1\nB 2 retired\nC 3\n")
1060 );
1061 }
1062
1063 #[test]
1066 fn an_empty_lock_is_next_one_with_no_entries() {
1067 let lock = InterfaceLock::default();
1068 assert_eq!(lock.next, 1);
1069 assert_eq!(lock.entries, Vec::new());
1070 assert_eq!(lock.max_number(), 0);
1071 assert_eq!(lock.render(), format!("{HEADER}\nnext 1\n"));
1072 assert_eq!(parse(&lock.render()).expect("the empty lock parses"), lock);
1073 }
1074
1075 #[test]
1076 fn max_number_counts_retired_entries() {
1077 let lock = parse("next 9\nA 1\nB 8 retired\n").expect("parses");
1078 assert_eq!(lock.max_number(), 8);
1079 }
1080
1081 #[test]
1084 fn allocate_takes_next_and_raises_it() {
1085 let mut lock = parse(EXAMPLE).expect("parses");
1086 assert_eq!(lock.allocate(key("Parking")), Ok(6));
1087 assert_eq!(lock.next, 7);
1088 let entry = lock.live(&key("Parking")).expect("the new entry is live");
1089 assert_eq!(entry.number, 6);
1090 assert!(!entry.retired);
1091 assert_eq!(
1092 entry.range,
1093 TextRange::default(),
1094 "an allocated entry has no line yet"
1095 );
1096
1097 assert_eq!(
1098 lock.allocate(key("CruiseControl")),
1099 Err(LockEditError::KeyAlreadyLive(key("CruiseControl")))
1100 );
1101 assert_eq!(
1102 lock.allocate(key("LaneAssist")),
1103 Ok(7),
1104 "a retired name is free for a new interface (§4)"
1105 );
1106 assert_eq!(lock.next, 8);
1107 assert!(lock.render().ends_with("Parking 6\nLaneAssist 7\n"));
1108 }
1109
1110 #[test]
1111 fn rename_keeps_the_number() {
1112 let mut lock = parse(EXAMPLE).expect("parses");
1113 assert_eq!(
1114 lock.rename(&key("LaneKeeping"), key("LaneCentering")),
1115 Ok(3)
1116 );
1117 assert_eq!(
1118 lock.live(&key("LaneCentering")).map(|entry| entry.number),
1119 Some(3)
1120 );
1121 assert_eq!(lock.live(&key("LaneKeeping")), None);
1122 assert_eq!(lock.next, 6, "a rename never allocates");
1123 assert!(lock.render().contains("\nLaneCentering 3\n"));
1124
1125 assert_eq!(
1126 lock.rename(&key("Absent"), key("X")),
1127 Err(LockEditError::NoLiveEntry(key("Absent")))
1128 );
1129 assert_eq!(
1130 lock.rename(&key("LaneAssist"), key("X")),
1131 Err(LockEditError::NoLiveEntry(key("LaneAssist"))),
1132 "a retired entry is not live"
1133 );
1134 assert_eq!(
1135 lock.rename(&key("CruiseControl"), key("DoorControl")),
1136 Err(LockEditError::KeyAlreadyLive(key("DoorControl")))
1137 );
1138 assert_eq!(
1139 lock.rename(&key("CruiseControl"), key("CruiseControl")),
1140 Err(LockEditError::KeyAlreadyLive(key("CruiseControl")))
1141 );
1142 assert_eq!(
1143 lock.rename(&key("service:veh.hvac.cabin"), key("service:veh.hvac.rear")),
1144 Ok(5),
1145 "a service rename is an entry rename (§3)"
1146 );
1147 }
1148
1149 #[test]
1150 fn retire_keeps_the_number_and_next() {
1151 let mut lock = parse(EXAMPLE).expect("parses");
1152 assert_eq!(lock.retire(&key("DoorControl")), Ok(4));
1153 assert_eq!(lock.live(&key("DoorControl")), None);
1154 assert_eq!(lock.next, 6, "a retire never lowers or raises `next`");
1155 assert_eq!(lock.max_number(), 5);
1156 assert!(lock.render().contains("\nDoorControl 4 retired\n"));
1157
1158 assert_eq!(
1159 lock.retire(&key("LaneAssist")),
1160 Err(LockEditError::NoLiveEntry(key("LaneAssist"))),
1161 "already retired"
1162 );
1163 assert_eq!(
1164 lock.retire(&key("Absent")),
1165 Err(LockEditError::NoLiveEntry(key("Absent")))
1166 );
1167 }
1168
1169 #[test]
1170 fn edit_errors_name_the_key() {
1171 assert_eq!(
1172 LockEditError::NoLiveEntry(key("A")).to_string(),
1173 "no live entry `A` in interfaces.lock"
1174 );
1175 assert_eq!(
1176 LockEditError::KeyAlreadyLive(key("service:a.b")).to_string(),
1177 "`service:a.b` is already a live entry in interfaces.lock"
1178 );
1179 }
1180
1181 fn table(next: u32, entries: &str) -> InterfaceLock {
1188 parse(&format!("{HEADER}\nnext {next}\n{entries}")).expect("a valid table")
1189 }
1190
1191 #[test]
1192 fn merge_drops_a_line_deleted_on_one_side_and_unchanged_on_the_other() {
1193 let base = table(3, "A 1\nB 2\n");
1194 let ours = table(3, "A 1\n");
1195 let theirs = table(4, "A 1\nB 2\nC 3\n");
1196 let expected = table(4, "A 1\nC 3\n").render();
1197 match merge(&base, &ours, &theirs, 7) {
1198 MergeOutcome::Clean(lock) => assert_eq!(lock.render(), expected),
1199 MergeOutcome::Conflict { text } => panic!("unexpected conflict:\n{text}"),
1200 }
1201 match merge(&base, &theirs, &ours, 7) {
1202 MergeOutcome::Clean(lock) => assert_eq!(lock.render(), expected),
1203 MergeOutcome::Conflict { text } => panic!("unexpected conflict:\n{text}"),
1204 }
1205 }
1206
1207 #[test]
1208 fn merge_conflicts_when_a_line_deleted_on_one_side_changed_on_the_other() {
1209 let base = table(3, "A 1\nB 2\n");
1210 let ours = table(3, "A 1\n");
1211 let theirs = table(3, "A 1\nB 2 retired\n");
1212 let MergeOutcome::Conflict { text } = merge(&base, &ours, &theirs, 7) else {
1213 panic!("a deletion against a change is a conflict");
1214 };
1215 assert_eq!(
1216 text,
1217 format!("{HEADER}\nnext 3\nA 1\n<<<<<<< ours\n=======\nB 2 retired\n>>>>>>> theirs\n")
1218 );
1219 assert!(parse(&text).is_err(), "a conflict text does not parse");
1220 }
1221
1222 #[cfg(feature = "fs")]
1225 mod fs {
1226 use super::*;
1227 use std::path::PathBuf;
1228 use std::sync::atomic::{AtomicUsize, Ordering};
1229
1230 struct TempDir(PathBuf);
1232
1233 impl TempDir {
1234 fn new(label: &str) -> Self {
1235 static COUNTER: AtomicUsize = AtomicUsize::new(0);
1236 let mut path = std::env::temp_dir();
1237 path.push(format!(
1238 "ridl-core-interface-lock-{label}-{}-{}",
1239 std::process::id(),
1240 COUNTER.fetch_add(1, Ordering::SeqCst),
1241 ));
1242 std::fs::create_dir_all(&path).expect("create the temp dir");
1243 Self(path)
1244 }
1245 }
1246
1247 impl Drop for TempDir {
1248 fn drop(&mut self) {
1249 let _ = std::fs::remove_dir_all(&self.0);
1250 }
1251 }
1252
1253 #[test]
1254 fn read_returns_none_when_the_file_is_absent() {
1255 let dir = TempDir::new("absent");
1256 assert_eq!(read(&dir.0).expect("an absent file is not an error"), None);
1257 }
1258
1259 #[test]
1260 fn write_then_read_round_trips_the_text() {
1261 let dir = TempDir::new("round-trip");
1262 let lock = parse(EXAMPLE).expect("parses");
1263 write(&dir.0, &lock).expect("the file is written");
1264 assert!(dir.0.join(FILE_NAME).is_file());
1265 assert_eq!(
1266 read(&dir.0).expect("the file is read"),
1267 Some(EXAMPLE.to_string())
1268 );
1269 }
1270 }
1271}