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