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