Skip to main content

rucc_headers/
merge.rs

1//! One header, merged across every release that has it.
2//!
3//! Design: `spec/cross-compile/08-sysroots.md` section 8.3. The shape of the answer is Zig's
4//! `generic-glibc`, which is one tree with the per-version differences written inside the files as
5//! conditionals on `__GLIBC_MINOR__`, and the technique for deriving one rather than maintaining it
6//! by hand is `ziglang/universal-headers`, which document 01.2 records as the state of the art and
7//! as unfinished.
8//!
9//! # What a merge is allowed to do
10//!
11//! Write text that every release shipped, and conditionals around the text that only some of them
12//! shipped. That is all. It never edits a declaration, never reflows anything and never invents a
13//! line, with one exception that is not an exception so much as a requirement: glibc's own
14//! definition of `__GLIBC_MINOR__` has to go, because the compiler is what defines it in a merged
15//! tree, and what replaces it is a check that somebody did.
16//!
17//! # The two ways cutting a file up can break it
18//!
19//! A conditional put in the wrong place turns a working header into one that does not compile, or
20//! worse into one that compiles differently, so both ways are checked rather than avoided by being
21//! careful.
22//!
23//! The first is a piece that cannot be split, which is a macro or a directive continued with a
24//! backslash, or a line whose trailing comment closes further down. `norm::pieces` is what makes
25//! that impossible: the smallest thing the merge can put a conditional between is a logical line.
26//!
27//! The second is a piece that is part of the file's own conditional. A region that contains an
28//! `#endif` whose `#if` is above it, or an `#else` belonging to an `#if` above it, cannot be wrapped
29//! in an `#if` of ours: our `#endif` would close theirs, or their `#else` would become ours.
30//!
31//! The answer to the second one is to make the region bigger until it holds the whole of whatever it
32//! was reaching into, and the region says which way to grow rather than being searched for. A branch
33//! that closes an `#if` from above needs the text above it, a branch that leaves an `#if` of its own
34//! open needs the text below it, and growing stops as soon as no branch reaches out. Growing
35//! upward takes back a region already decided, which is the one place a decision here is
36//! reconsidered. A file where the region grows to the whole file is one copy per release, which is
37//! how this started and is still the answer for a file whose releases disagree about where their own
38//! conditionals are.
39//!
40//! # Why it reads its own output back
41//!
42//! Because the only statement worth making about a merged tree is that it is the releases it was
43//! merged from, and the way to say that is to take it apart again. Every file, for every release
44//! that has it, is evaluated at that release's version and held against what that release shipped.
45//! A merge that cannot reproduce its inputs is a bug in this file, and the check runs before the
46//! tree is written rather than in a test over a fixture, because the fixture that matters is
47//! glibc.
48
49use crate::cond::{self, Releases};
50use crate::diff;
51use crate::norm;
52
53/// What the merge had to do to one file.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum Kind {
56    /// Every release agrees about the code, so the newest release's text is the answer and there is
57    /// no conditional in it at all.
58    Same,
59    /// Conditionals inside the file, around the parts that differ.
60    Conditional,
61    /// One branch holding the whole file per release, because every smaller region grew until it
62    /// was the file: the releases disagree about where the file's own conditionals begin or end.
63    PerRelease,
64}
65
66/// One merged header.
67#[derive(Debug, Clone)]
68pub struct Merged {
69    /// The text to write.
70    pub text: String,
71    /// What had to be done to get it.
72    pub kind: Kind,
73    /// Whether at least one release does not have this header at all.
74    pub guarded: bool,
75    /// How many conditionals of ours are in it, not counting the guard.
76    pub branches: usize,
77    /// Whether glibc's own definition of `__GLIBC_MINOR__` was replaced in it.
78    pub defined_the_macro: bool,
79    /// What is wrong with it, which is empty for every file in a tree worth shipping.
80    pub problems: Vec<String>,
81}
82
83/// What replaces glibc's own definition of the version macro.
84///
85/// A merged tree cannot define the minor version, because the whole point is that one tree serves
86/// several, so the definition has to go. What goes in its place is the question this answers: an
87/// empty space would mean a compiler that forgot to define it gets the oldest release's
88/// declarations and no warning, and every `__GLIBC_PREREQ` in the program would quietly answer for
89/// 2.28. So the definition is replaced by a check that there is one.
90pub const GUARD: &str = "#ifndef __GLIBC_MINOR__\n# error \"this is rucc's merged glibc header \
91                         tree, in which the compiler defines __GLIBC_MINOR__ from the target; see \
92                         spec/cross-compile/08-sysroots.md section 8.3\"\n#endif\n";
93
94/// Merges one header. `texts` has one entry per release, `None` where that release does not have it.
95///
96/// # Panics
97///
98/// When `texts` is not one entry per release, which is a mistake in the caller rather than
99/// something a tree can be shaped like.
100pub fn one(releases: &Releases, path: &str, texts: &[Option<&str>]) -> Result<Merged, String> {
101    assert_eq!(texts.len(), releases.count(), "one text per release, present or not");
102    for (n, text) in texts.iter().enumerate() {
103        if text.is_some_and(cond::carries_mark) {
104            return Err(format!(
105                "{path}: the {} copy contains {}, so it has been through a merge already and \
106                 merging it again would read its conditionals as ours",
107                releases.spelled(n),
108                cond::MARK
109            ));
110        }
111    }
112
113    let mut problems = Vec::new();
114    let patched: Vec<Option<(String, bool)>> = texts.iter().map(|t| t.map(patch)).collect();
115    let want: Vec<Option<&str>> =
116        patched.iter().map(|p| p.as_ref().map(|(text, _)| text.as_str())).collect();
117    let present: Vec<bool> = want.iter().map(Option::is_some).collect();
118    let have: Vec<usize> = (0..releases.count()).filter(|&n| present[n]).collect();
119    let (Some(&first), Some(&newest)) = (have.first(), have.last()) else {
120        return Err(format!("{path}: no release has it"));
121    };
122    let cut: Vec<Option<norm::Pieces>> = want.iter().map(|t| t.map(norm::pieces)).collect();
123    let pieces = |which: usize| cut[which].as_ref().expect("a release that has the file");
124
125    // The lines every release has, and where each release has them. One release at a time against
126    // what the ones before it agreed about, which is why what comes out is common to all of them.
127    let mut spine: Vec<String> = pieces(first).keys().iter().map(|&k| k.to_owned()).collect();
128    let mut at: Vec<Vec<usize>> = vec![(0..spine.len()).collect()];
129    for &r in &have[1..] {
130        let theirs = pieces(r).keys();
131        let mine: Vec<&str> = spine.iter().map(String::as_str).collect();
132        let pairs = diff::aligned(&mine, &theirs);
133        let kept: Vec<String> = pairs.iter().map(|&(x, _)| spine[x].clone()).collect();
134        for row in &mut at {
135            *row = pairs.iter().map(|&(x, _)| row[x]).collect();
136        }
137        at.push(pairs.iter().map(|&(_, y)| y).collect());
138        spine = kept;
139    }
140
141    // The file as slots. An even slot is the text between two spine lines, which is where a
142    // difference lives, and an odd slot is a spine line, which every release has.
143    let slots = 2 * spine.len() + 1;
144    let by_slot: Vec<Vec<String>> = have
145        .iter()
146        .enumerate()
147        .map(|(j, &r)| {
148            let items = &pieces(r).items;
149            (0..slots)
150                .map(|slot| {
151                    if slot % 2 == 1 {
152                        return items[at[j][slot / 2]].text.clone();
153                    }
154                    let gap = slot / 2;
155                    let from = if gap == 0 { 0 } else { at[j][gap - 1] + 1 };
156                    let to = if gap == spine.len() { items.len() } else { at[j][gap] };
157                    let mut text: String =
158                        items[from..to].iter().map(|i| i.text.as_str()).collect();
159                    if gap == spine.len() {
160                        // The comments after the last line of code belong to the last gap.
161                        text.push_str(&pieces(r).tail);
162                    }
163                    text
164                })
165                .collect()
166        })
167        .collect();
168    let region = |lo: usize, hi: usize| grouped(&have, |j, _| by_slot[j][lo..=hi].concat());
169    // Which slots have anything in them at all, so that a region holding all of them can be
170    // reported as what it is, one copy of the file per release, however many empty slots the
171    // alignment left around it.
172    let content: Vec<bool> =
173        (0..slots).map(|slot| by_slot.iter().any(|row| !row[slot].is_empty())).collect();
174    let whole_file =
175        |lo: usize, hi: usize| !content[..lo].contains(&true) && !content[hi + 1..].contains(&true);
176
177    // What gets a conditional around it. A slot every release agrees about is written as it stands,
178    // and a slot they do not agree about is wrapped together with as few of its neighbours as it
179    // takes for every branch to stand on its own. Growing is not a search: a branch that reaches an
180    // `#endif` whose `#if` is above it needs the text above it, and a branch that leaves an `#if`
181    // open needs the text below it, so the branch says which way to grow and the region stops as
182    // soon as nothing is reaching out of it.
183    let mut regions: Vec<(usize, usize)> = Vec::new();
184    let mut slot = 0;
185    while slot < slots {
186        let (mut lo, mut hi) = (slot, slot);
187        loop {
188            let groups = region(lo, hi);
189            let reach =
190                groups.iter().fold(Reach::default(), |all, (_, _, text)| all.with(&needs(text)));
191            if groups.len() == 1 || !reach.out_of_it() {
192                break;
193            }
194            // Growing below takes a slot this loop had not reached yet; growing above takes back a
195            // region already decided, which is the one case where a decision is reconsidered.
196            let below = reach.below && hi + 1 < slots;
197            let above = reach.above && lo > 0;
198            if below {
199                hi += 1;
200            } else if above {
201                lo = regions.pop().expect("a region above to take back").0;
202            } else if hi + 1 < slots {
203                hi += 1;
204            } else if lo > 0 {
205                lo = regions.pop().expect("a region above to take back").0;
206            } else {
207                // The whole file, and its own conditionals do not balance. The branch is written
208                // anyway and the check below is what says so.
209                break;
210            }
211        }
212        regions.push((lo, hi));
213        slot = hi + 1;
214    }
215
216    let mut body = String::new();
217    let mut branches = 0;
218    let mut kind = Kind::Same;
219    for &(lo, hi) in &regions {
220        let groups = region(lo, hi);
221        if let [(_, _, only)] = &groups[..] {
222            body.push_str(only);
223            continue;
224        }
225        // A group with nothing in it gets no branch. The releases in it are the ones that have
226        // nothing here, and an empty `#if` would say that in three lines instead of none.
227        let said: Vec<&(String, Vec<usize>, String)> =
228            groups.iter().filter(|(_, _, text)| !text.is_empty()).collect();
229        branches += 1;
230        kind = if whole_file(lo, hi) { Kind::PerRelease } else { Kind::Conditional };
231        for (n, (_, members, text)) in said.iter().enumerate() {
232            line_end(&mut body);
233            body.push_str(&cond::directive(
234                if n == 0 { "if" } else { "elif" },
235                Some(&condition(releases, members)),
236            ));
237            body.push_str(text);
238            if !stands_alone(text) {
239                problems.push(format!(
240                    "{path}: the copies for {} do not have balanced conditionals, so no branch \
241                     around them is right",
242                    spelled(releases, members)
243                ));
244            }
245        }
246        line_end(&mut body);
247        body.push_str(&cond::directive("endif", None));
248    }
249
250    // A header that arrives or goes away is still one file in the tree, and what it says for a
251    // release that does not have it is what a missing header would have said.
252    let guarded = have.len() != releases.count();
253    let text = if have.len() == releases.count() {
254        body
255    } else {
256        let mut text = cond::directive("if", Some(&condition(releases, &have)));
257        text.push_str(&body);
258        line_end(&mut text);
259        text.push_str(&cond::directive("else", None));
260        text.push_str(&format!(
261            "#error \"rucc: {path} is not a header of this glibc release; it is in {}\"\n",
262            spelled(releases, &have)
263        ));
264        text.push_str(&cond::directive("endif", None));
265        text
266    };
267
268    // The check, which is the reason to believe any of the above.
269    for (n, each) in want.iter().enumerate() {
270        let Some(each) = each else { continue };
271        match cond::evaluate(&text, releases.minors()[n]) {
272            Ok(got) if norm::code(&got) == norm::code(each) => {}
273            Ok(got) => problems.push(format!(
274                "{path}: what this writes does not give the {} copy back, {}",
275                releases.spelled(n),
276                first_difference(&norm::code(&got), &norm::code(each))
277            )),
278            Err(why) => problems.push(format!(
279                "{path}: reading back what this writes for {} failed: {why}",
280                releases.spelled(n)
281            )),
282        }
283    }
284    if kind == Kind::Same && !guarded {
285        // Nothing was written around anything, so this is a copy and the bytes say so.
286        let same = want[newest].unwrap_or_default();
287        if text.trim_end_matches('\n') != same.trim_end_matches('\n') {
288            problems.push(format!(
289                "{path}: no conditional was needed and the text still is not the {} copy",
290                releases.spelled(newest)
291            ));
292        }
293    }
294
295    Ok(Merged {
296        text,
297        kind,
298        guarded,
299        branches,
300        defined_the_macro: patched.iter().flatten().any(|(_, did)| *did),
301        problems,
302    })
303}
304
305/// The releases grouped by the code of what `text` gives for each of them, in the order their
306/// oldest member comes in, with the newest member's real text kept for each group.
307///
308/// Grouping by the code rather than by the bytes is what keeps a copyright year from becoming a
309/// conditional, and keeping the newest member's text is what keeps the tree reading like the newest
310/// release rather than like a patchwork.
311fn grouped(
312    have: &[usize],
313    mut text: impl FnMut(usize, usize) -> String,
314) -> Vec<(String, Vec<usize>, String)> {
315    let mut groups: Vec<(String, Vec<usize>, String)> = Vec::new();
316    for (j, &r) in have.iter().enumerate() {
317        let text = text(j, r);
318        let code = norm::code(&text);
319        match groups.iter_mut().find(|group| group.0 == code) {
320            Some(group) => {
321                group.1.push(r);
322                group.2 = text;
323            }
324            None => groups.push((code, vec![r], text)),
325        }
326    }
327    groups
328}
329
330/// The condition for a set of releases named by index.
331fn condition(releases: &Releases, members: &[usize]) -> String {
332    let mut flags = vec![false; releases.count()];
333    for &m in members {
334        flags[m] = true;
335    }
336    // Every release is a condition of its own only when something else in the file distinguishes
337    // them, so this is never asked about the whole set.
338    releases.condition(&flags).unwrap_or_else(|| "1".to_owned())
339}
340
341/// A set of releases, for a message.
342fn spelled(releases: &Releases, members: &[usize]) -> String {
343    members.iter().map(|&m| releases.spelled(m)).collect::<Vec<_>>().join(" ")
344}
345
346/// Whether this text can have a conditional wrapped around it.
347///
348/// It can when its own conditional directives balance and none of them continues one from outside
349/// it. An `#endif` with nothing above it would close ours, and so would an `#else`, which is the
350/// second of the hazards in the module documentation.
351fn stands_alone(text: &str) -> bool {
352    !needs(text).out_of_it()
353}
354
355/// Which way a text reaches out of itself, which is which way a region around it has to grow.
356#[derive(Debug, Default, Clone, Copy)]
357struct Reach {
358    /// It closes or continues a conditional opened above it, so the region has to start higher up.
359    above: bool,
360    /// It leaves a conditional of its own open, so the region has to end further down.
361    below: bool,
362}
363
364impl Reach {
365    /// Both of them, because a region is as big as its neediest branch.
366    fn with(self, other: &Reach) -> Self {
367        Self { above: self.above || other.above, below: self.below || other.below }
368    }
369
370    /// Whether it reaches out at all, which is the same question `stands_alone` asks.
371    fn out_of_it(self) -> bool {
372        self.above || self.below
373    }
374}
375
376/// What this text would need around it before a conditional of ours could wrap it.
377///
378/// The walk is over the code, so a directive inside a comment is not one. An `#endif` that takes the
379/// depth below zero is closing somebody else's `#if` and so is an `#else` at depth zero, and both
380/// are answered by starting the region higher up. Depth left above zero at the end is an `#if` of
381/// the file's own that nothing here closes, and that is answered by ending the region further down.
382fn needs(text: &str) -> Reach {
383    let mut depth = 0i32;
384    let mut reach = Reach::default();
385    for line in norm::code(text).lines() {
386        let Some(rest) = line.trim_start().strip_prefix('#') else { continue };
387        let rest = rest.trim_start();
388        if rest.starts_with("if") {
389            depth += 1;
390        } else if rest.starts_with("endif") {
391            depth -= 1;
392            if depth < 0 {
393                reach.above = true;
394                depth = 0;
395            }
396        } else if (rest.starts_with("else") || rest.starts_with("elif")) && depth == 0 {
397            reach.above = true;
398        }
399    }
400    if depth > 0 {
401        reach.below = true;
402    }
403    reach
404}
405
406/// glibc's own definition of the version macro, replaced by the check that there is one.
407fn patch(text: &str) -> (String, bool) {
408    let cut = norm::pieces(text);
409    if !cut.items.iter().any(|item| defines_the_macro(&item.key)) {
410        return (text.to_owned(), false);
411    }
412    let mut out = String::with_capacity(text.len());
413    for item in &cut.items {
414        if defines_the_macro(&item.key) {
415            // The comment above it stays, because it is glibc's comment about the version macros
416            // and it is still true.
417            out.push_str(&item.text[..item.code_at]);
418            out.push_str(GUARD);
419        } else {
420            out.push_str(&item.text);
421        }
422    }
423    out.push_str(&cut.tail);
424    (out, true)
425}
426
427/// Whether this line is what defines the version macro.
428fn defines_the_macro(key: &str) -> bool {
429    let Some(rest) = key.strip_prefix('#') else { return false };
430    let Some(rest) = rest.trim_start().strip_prefix("define") else { return false };
431    let Some(rest) = rest.trim_start().strip_prefix(cond::MACRO) else { return false };
432    let value = rest.trim();
433    !value.is_empty() && value.bytes().all(|b| b.is_ascii_digit())
434}
435
436/// A newline, when the text does not already end in one, so a directive starts its own line.
437fn line_end(out: &mut String) {
438    if !out.is_empty() && !out.ends_with('\n') {
439        out.push('\n');
440    }
441}
442
443/// Where two texts first differ, for a message somebody has to act on.
444fn first_difference(got: &str, want: &str) -> String {
445    for (n, (left, right)) in got.lines().zip(want.lines()).enumerate() {
446        if left != right {
447            return format!("at line {} of the code: {} against {}", n + 1, cut(left), cut(right));
448        }
449    }
450    format!("{} lines of code against {}", got.lines().count(), want.lines().count())
451}
452
453/// Enough of a line to recognize it, and not a screen of it.
454fn cut(line: &str) -> String {
455    let line = line.trim();
456    if line.chars().count() <= 60 {
457        return format!("`{line}`");
458    }
459    format!("`{}...`", line.chars().take(57).collect::<String>())
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    fn releases() -> Releases {
467        Releases::new(vec![28, 31, 34]).expect("ascending")
468    }
469
470    /// The merge, and then the check on it that matters: what it wrote gives every input back.
471    fn merged(texts: &[Option<&str>]) -> Merged {
472        let all = releases();
473        let out = one(&all, "sys/thing.h", texts).expect("a tree nobody merged before");
474        assert_eq!(out.problems, Vec::<String>::new());
475        for (n, want) in texts.iter().enumerate() {
476            if let Some(want) = want {
477                let got = cond::evaluate(&out.text, all.minors()[n]).expect("our own conditionals");
478                assert_eq!(norm::code(&got), norm::code(want), "the 2.{} copy", all.minors()[n]);
479            }
480        }
481        out
482    }
483
484    #[test]
485    fn three_copies_of_one_file_are_that_file() {
486        let text = "#ifndef _THING_H\n#define _THING_H 1\nint f (void);\n#endif\n";
487        let out = merged(&[Some(text), Some(text), Some(text)]);
488        assert_eq!(out.kind, Kind::Same);
489        assert_eq!(out.text, text);
490        assert_eq!(out.branches, 0);
491        assert!(!out.guarded);
492    }
493
494    #[test]
495    fn a_year_in_a_comment_is_not_worth_a_conditional() {
496        let old = "/* Copyright (C) 2018 FSF. */\nint f (void);\n";
497        let new = "/* Copyright (C) 2024 FSF. */\nint f (void);\n";
498        let out = merged(&[Some(old), Some(old), Some(new)]);
499        assert_eq!(out.kind, Kind::Same);
500        // The newest release's text, which is how the tree ends up reading like one release.
501        assert_eq!(out.text, new);
502    }
503
504    #[test]
505    fn a_declaration_added_in_the_newest_release_is_behind_a_conditional() {
506        let old = "int f (void);\n";
507        let new = "int f (void);\nint g (void);\n";
508        let out = merged(&[Some(old), Some(old), Some(new)]);
509        assert_eq!(out.kind, Kind::Conditional);
510        assert_eq!(out.branches, 1);
511        // One branch and not two, because the releases without the line have nothing to put in one.
512        assert_eq!(
513            out.text,
514            "int f (void);\n#if __GLIBC_MINOR__ >= 34 /* rucc */\nint g (void);\n#endif /* rucc */\n"
515        );
516    }
517
518    #[test]
519    fn a_declaration_removed_in_the_newest_release_is_behind_one_too() {
520        let old = "int f (void);\nint gone (void);\n";
521        let new = "int f (void);\n";
522        let out = merged(&[Some(old), Some(old), Some(new)]);
523        assert_eq!(out.kind, Kind::Conditional);
524        assert!(out.text.contains("#if __GLIBC_MINOR__ < 34 /* rucc */"), "{}", out.text);
525    }
526
527    #[test]
528    fn a_constant_that_changed_value_is_one_conditional_with_two_branches() {
529        let out = merged(&[
530            Some("#define _STAT_VER 1\n"),
531            Some("#define _STAT_VER 1\n"),
532            Some("#define _STAT_VER 3\n"),
533        ]);
534        assert_eq!(out.branches, 1);
535        assert_eq!(out.text.matches("#elif").count(), 1);
536    }
537
538    #[test]
539    fn a_header_that_arrives_later_says_so_for_the_releases_without_it() {
540        let out = merged(&[None, None, Some("int f (void);\n")]);
541        assert!(out.guarded);
542        assert!(out.text.starts_with("#if __GLIBC_MINOR__ >= 34 /* rucc */"), "{}", out.text);
543        assert!(
544            out.text.contains(
545                "#error \"rucc: sys/thing.h is not a header of this glibc \
546                                   release; it is in 2.34\""
547            ),
548            "{}",
549            out.text
550        );
551        // A release that does not have it gets the error and nothing else.
552        let gone = cond::evaluate(&out.text, 28).expect("ours");
553        assert!(gone.contains("#error"), "{gone}");
554        assert!(!gone.contains("int f (void);"), "{gone}");
555    }
556
557    #[test]
558    fn a_header_that_went_away_is_the_same_the_other_way_round() {
559        let out = merged(&[Some("int f (void);\n"), Some("int f (void);\n"), None]);
560        assert!(out.guarded);
561        assert!(out.text.starts_with("#if __GLIBC_MINOR__ < 34 /* rucc */"), "{}", out.text);
562    }
563
564    /// A region that differs and sits inside one of the file's own conditionals is not the hazard:
565    /// a branch there closes nothing it did not open.
566    #[test]
567    fn a_region_inside_the_files_own_conditional_is_left_where_it_is() {
568        let old = "#ifdef __USE_GNU\nint f (void);\n#endif\n";
569        let new = "#ifdef __USE_GNU\nint f (void);\nint g (void);\n#endif\nint h (void);\n";
570        let out = merged(&[Some(old), Some(old), Some(new)]);
571        assert_eq!(out.kind, Kind::Conditional);
572        // One for the line added inside the file's own conditional and one for the line added
573        // after it, which is two places rather than one thing in two places.
574        assert_eq!(out.branches, 2);
575        assert!(out.text.contains("#ifdef __USE_GNU\n"), "{}", out.text);
576        // The file's own conditional is still one `#ifdef` and one `#endif` in both releases.
577        for release in [28, 34] {
578            let got = cond::evaluate(&out.text, release).expect("ours");
579            assert_eq!(got.matches("#ifdef __USE_GNU").count(), 1, "{got}");
580            assert_eq!(got.matches("#endif").count(), 1, "{got}");
581        }
582    }
583
584    /// A condition the releases changed cannot be wrapped on its own, because the branch holding
585    /// the old `#if` leaves it open. The region grows until it holds the block and stops there,
586    /// which is the whole point of growing rather than escalating.
587    #[test]
588    fn a_changed_condition_takes_its_block_with_it_and_not_the_file() {
589        let old = "int before (void);\n#ifdef A\nint f (void);\n#endif\nint after (void);\n";
590        let new = "int before (void);\n#if defined A || defined B\nint f (void);\n#endif\n\
591                   int after (void);\n";
592        let out = merged(&[Some(old), Some(old), Some(new)]);
593        assert_eq!(out.kind, Kind::Conditional);
594        assert_eq!(out.branches, 1);
595        // The lines either side of the block are written once, so the region was the block.
596        assert_eq!(out.text.matches("int before (void);").count(), 1, "{}", out.text);
597        assert_eq!(out.text.matches("int after (void);").count(), 1, "{}", out.text);
598        assert_eq!(out.text.matches("int f (void);").count(), 2, "{}", out.text);
599    }
600
601    /// And when growing cannot stop short of the file, it does not: these two releases disagree
602    /// about which of their own conditionals contains the other, so no region inside the file has
603    /// branches that stand on their own.
604    #[test]
605    fn a_file_whose_conditionals_nest_differently_is_one_copy_per_release() {
606        let old = "#if A\nint f (void);\n#endif\n#if B\nint g (void);\n#endif\n";
607        let new = "#if A\nint f (void);\n#if B\nint g (void);\n#endif\n#endif\n";
608        let out = merged(&[Some(old), Some(old), Some(new)]);
609        assert_eq!(out.kind, Kind::PerRelease, "{}", out.text);
610        assert_eq!(out.branches, 1);
611        assert!(out.text.starts_with("#if __GLIBC_MINOR__ < 34 /* rucc */"), "{}", out.text);
612        assert_eq!(out.text.matches("int f (void);").count(), 2, "{}", out.text);
613        assert!(out.problems.is_empty(), "{:?}", out.problems);
614    }
615
616    #[test]
617    fn the_definition_of_the_version_macro_is_replaced_by_the_check_for_one() {
618        let all = releases();
619        let texts: Vec<String> = all
620            .minors()
621            .iter()
622            .map(|m| format!("#define __GLIBC__ 2\n#define\t__GLIBC_MINOR__\t{m}\nint f (void);\n"))
623            .collect();
624        let given: Vec<Option<&str>> = texts.iter().map(|t| Some(t.as_str())).collect();
625        let out = one(&all, "features.h", &given).expect("not merged before");
626        assert_eq!(out.problems, Vec::<String>::new());
627        assert!(out.defined_the_macro);
628        assert_eq!(out.kind, Kind::Same, "{}", out.text);
629        assert!(!out.text.contains("#define\t__GLIBC_MINOR__"), "{}", out.text);
630        assert!(out.text.contains("#define __GLIBC__ 2"), "{}", out.text);
631        assert!(out.text.contains("#ifndef __GLIBC_MINOR__"), "{}", out.text);
632        assert!(out.text.contains("# error"), "{}", out.text);
633    }
634
635    /// A mention of the macro in a comment is not a definition of it.
636    #[test]
637    fn a_comment_about_the_version_macro_is_left_alone() {
638        let text = "/* #define __GLIBC_MINOR__ 44 is what glibc does. */\nint f (void);\n";
639        let out = merged(&[Some(text), Some(text), Some(text)]);
640        assert!(!out.defined_the_macro);
641        assert_eq!(out.text, text);
642    }
643
644    #[test]
645    fn a_tree_that_has_been_merged_once_is_refused() {
646        let all = releases();
647        let text = format!("int f (void);\n{}", cond::directive("endif", None));
648        let why = one(&all, "sys/thing.h", &[Some(&text), Some(&text), Some(&text)])
649            .expect_err("it carries the marker");
650        assert!(why.contains("through a merge already"), "{why}");
651    }
652
653    #[test]
654    fn a_file_no_release_has_is_an_error_rather_than_an_empty_file() {
655        assert!(one(&releases(), "sys/thing.h", &[None, None, None]).is_err());
656    }
657
658    /// The merge can cut between logical lines only, so a macro whose body changed is one piece and
659    /// the conditional lands around the whole definition.
660    #[test]
661    fn a_continued_macro_that_changed_is_replaced_whole() {
662        let old = "#define F(a) \\\n  ((a) + 1)\nint f (void);\n";
663        let new = "#define F(a) \\\n  ((a) + 2)\nint f (void);\n";
664        let out = merged(&[Some(old), Some(old), Some(new)]);
665        assert_eq!(out.kind, Kind::Conditional);
666        // Neither branch is half a definition.
667        for release in [28, 34] {
668            let got = cond::evaluate(&out.text, release).expect("ours");
669            assert_eq!(got.matches("#define F(a)").count(), 1, "{got}");
670        }
671    }
672
673    #[test]
674    fn a_file_with_no_trailing_newline_still_gets_whole_directives() {
675        let out = merged(&[Some("int f (void);"), Some("int f (void);"), Some("int g (void);")]);
676        for line in out.text.lines() {
677            assert!(!line.contains("#endif") || line.trim_start().starts_with('#'), "{line}");
678        }
679    }
680
681    #[test]
682    fn which_way_a_branch_reaches_out_of_itself() {
683        assert!(needs("#endif\n").above);
684        assert!(needs("#else\nint f (void);\n").above);
685        assert!(needs("#ifdef A\nint f (void);\n").below);
686        assert!(!needs("#ifdef A\nint f (void);\n#endif\n").out_of_it());
687        // An `#endif` that closes somebody else's and then an `#if` of its own reaches both ways.
688        let both = needs("#endif\n#ifdef A\nint f (void);\n");
689        assert!(both.above && both.below);
690    }
691
692    #[test]
693    fn what_stands_alone_and_what_does_not() {
694        assert!(stands_alone("int f (void);\n"));
695        assert!(stands_alone("#ifdef A\nint f (void);\n#endif\n"));
696        assert!(!stands_alone("#endif\n"));
697        assert!(!stands_alone("#else\nint f (void);\n"));
698        assert!(!stands_alone("#ifdef A\nint f (void);\n"));
699        // A directive inside a comment is not a directive.
700        assert!(stands_alone("/* #endif */\nint f (void);\n"));
701    }
702}