Skip to main content

diffr_plugin_test_bodies/
lib.rs

1//! Collapse test bodies and test modules.
2use diffr_plugin_sdk::{
3    anyhow, docstring_of, export, has_tag, is_fold, line_count, walk, Draft, FileEntry, Move,
4    Pairing, Plugin, Source,
5};
6use serde::Deserialize;
7use std::collections::BTreeMap;
8
9/// The plugin's name, and the tags its queries set: a test function's body,
10/// and a test module's body.
11const PLUGIN: &str = "test-bodies";
12const TEST: &str = "test-bodies:test";
13const MODULE: &str = "test-bodies:module";
14
15#[derive(Deserialize)]
16#[serde(deny_unknown_fields)]
17pub struct Options {
18    min_lines: usize,
19}
20
21/// Test bodies of at least `min_lines` start collapsed on both sides, paired
22/// or not, so a diff reads as the code under test first. A whole test
23/// module, such as a Rust `#[cfg(test)] mod tests`, collapses as one fold
24/// labelled "test module"; the header stays visible and the fold expands
25/// like any other. A test body is linked to its docstring, which collapses
26/// with it.
27pub struct TestBodies {
28    options: Options,
29}
30
31impl Plugin for TestBodies {
32    type Options = Options;
33
34    fn new(options: Options) -> anyhow::Result<Self> {
35        Ok(Self { options })
36    }
37
38    fn queries(&self) -> anyhow::Result<Vec<diffr_plugin_sdk::QuerySource>> {
39        Ok(vec![
40            diffr_plugin_sdk::QuerySource {
41                language: "rust".into(),
42                name: "builtin:test-bodies/queries/rust.scm".into(),
43                text: include_str!("../queries/rust.scm").into(),
44            },
45            diffr_plugin_sdk::QuerySource {
46                language: "python".into(),
47                name: "builtin:test-bodies/queries/python.scm".into(),
48                text: include_str!("../queries/python.scm").into(),
49            },
50            diffr_plugin_sdk::QuerySource {
51                language: "go".into(),
52                name: "builtin:test-bodies/queries/go.scm".into(),
53                text: include_str!("../queries/go.scm").into(),
54            },
55            diffr_plugin_sdk::QuerySource {
56                language: "javascript".into(),
57                name: "builtin:test-bodies/queries/javascript.scm".into(),
58                text: include_str!("../queries/javascript.scm").into(),
59            },
60            diffr_plugin_sdk::QuerySource {
61                language: "javascriptjsx".into(),
62                name: "builtin:test-bodies/queries/javascript.scm".into(),
63                text: include_str!("../queries/javascript.scm").into(),
64            },
65            diffr_plugin_sdk::QuerySource {
66                language: "typescript".into(),
67                name: "builtin:test-bodies/queries/javascript.scm".into(),
68                text: include_str!("../queries/javascript.scm").into(),
69            },
70            diffr_plugin_sdk::QuerySource {
71                language: "typescripttsx".into(),
72                name: "builtin:test-bodies/queries/javascript.scm".into(),
73                text: include_str!("../queries/javascript.scm").into(),
74            },
75        ])
76    }
77
78    fn classify(&self, _file: &FileEntry) -> anyhow::Result<Vec<String>> {
79        Ok(Vec::new())
80    }
81
82    fn mutate(&self, _file: &FileEntry, sides: &Pairing<Source>) -> anyhow::Result<Vec<Move>> {
83        let options = &self.options;
84        // Every fold has its own id, so each side's fold is its own target
85        // and takes its own label. A test body's docstring, when it has one,
86        // is linked after it collapses.
87        let mut labels: BTreeMap<u32, (String, Option<u32>)> = BTreeMap::new();
88        for source in sides.sides() {
89            walk(&source.regions, &mut |region| {
90                if !is_fold(region) || line_count(region) < options.min_lines {
91                    return;
92                }
93                if has_tag(region, MODULE) {
94                    labels.insert(
95                        region.id,
96                        (
97                            if region.visibility.label.is_empty() {
98                                "test module".to_owned()
99                            } else {
100                                region.visibility.label.clone()
101                            },
102                            None,
103                        ),
104                    );
105                } else if has_tag(region, TEST) {
106                    labels.entry(region.id).or_insert((
107                        if region.visibility.label.is_empty() {
108                            "test body".to_owned()
109                        } else {
110                            region.visibility.label.clone()
111                        },
112                        docstring_of(source, region, PLUGIN),
113                    ));
114                }
115            });
116        }
117        let mut draft = Draft::new(sides);
118        for (id, (label, docstring)) in labels {
119            draft.collapse(id, label.to_owned())?;
120            if let Some(docstring) = docstring {
121                draft.link(&[id, docstring])?;
122            }
123        }
124        Ok(draft.into_moves())
125    }
126}
127
128export!("test-bodies", TestBodies);