diff options
author | Bernardo <[email protected]> | 2018-12-12 17:51:43 +0000 |
---|---|---|
committer | Bernardo <[email protected]> | 2018-12-25 18:45:21 +0000 |
commit | 3d98744c2a929c35852d20b5724ef5b6be1e3312 (patch) | |
tree | 2125060d997f327d6ebeccd12833be0cf8ffce8b /crates | |
parent | 5fb426cb9eeefa69a53d7c8c3367f7c6b714b9b8 (diff) |
proptest strategies for TextUnit and AtomTextEdit
Diffstat (limited to 'crates')
-rw-r--r-- | crates/ra_text_edit/Cargo.toml | 1 | ||||
-rw-r--r-- | crates/ra_text_edit/src/lib.rs | 2 | ||||
-rw-r--r-- | crates/ra_text_edit/src/test_utils.rs | 114 |
3 files changed, 117 insertions, 0 deletions
diff --git a/crates/ra_text_edit/Cargo.toml b/crates/ra_text_edit/Cargo.toml index 3c4157a4e..67dfd0924 100644 --- a/crates/ra_text_edit/Cargo.toml +++ b/crates/ra_text_edit/Cargo.toml | |||
@@ -10,3 +10,4 @@ text_unit = "0.1.5" | |||
10 | 10 | ||
11 | [dev-dependencies] | 11 | [dev-dependencies] |
12 | test_utils = { path = "../test_utils" } | 12 | test_utils = { path = "../test_utils" } |
13 | proptest = "0.8.7" | ||
diff --git a/crates/ra_text_edit/src/lib.rs b/crates/ra_text_edit/src/lib.rs index 13e20f6fb..0a407795e 100644 --- a/crates/ra_text_edit/src/lib.rs +++ b/crates/ra_text_edit/src/lib.rs | |||
@@ -1,5 +1,7 @@ | |||
1 | mod text_edit; | 1 | mod text_edit; |
2 | pub mod text_utils; | 2 | pub mod text_utils; |
3 | #[cfg(test)] | ||
4 | pub mod test_utils; | ||
3 | 5 | ||
4 | pub use crate::text_edit::{TextEdit, TextEditBuilder}; | 6 | pub use crate::text_edit::{TextEdit, TextEditBuilder}; |
5 | 7 | ||
diff --git a/crates/ra_text_edit/src/test_utils.rs b/crates/ra_text_edit/src/test_utils.rs new file mode 100644 index 000000000..92b79ab59 --- /dev/null +++ b/crates/ra_text_edit/src/test_utils.rs | |||
@@ -0,0 +1,114 @@ | |||
1 | use proptest::{prelude::*, proptest, proptest_helper}; | ||
2 | use text_unit::{TextUnit, TextRange}; | ||
3 | use crate::AtomTextEdit; | ||
4 | |||
5 | pub fn arb_text() -> proptest::string::RegexGeneratorStrategy<String> { | ||
6 | // generate multiple newlines | ||
7 | proptest::string::string_regex("(.*\n?)*").unwrap() | ||
8 | } | ||
9 | |||
10 | fn text_offsets(text: &str) -> Vec<TextUnit> { | ||
11 | text.char_indices() | ||
12 | .map(|(i, _)| TextUnit::from_usize(i)) | ||
13 | .collect() | ||
14 | } | ||
15 | |||
16 | pub fn arb_offset(text: &str) -> BoxedStrategy<TextUnit> { | ||
17 | let offsets = text_offsets(text); | ||
18 | // this is necessary to avoid "Uniform::new called with `low >= high`" panic | ||
19 | if offsets.is_empty() { | ||
20 | Just(TextUnit::from(0)).boxed() | ||
21 | } else { | ||
22 | prop::sample::select(offsets).boxed() | ||
23 | } | ||
24 | } | ||
25 | |||
26 | pub fn arb_edits(text: &str) -> BoxedStrategy<Vec<AtomTextEdit>> { | ||
27 | let offsets = text_offsets(text); | ||
28 | let offsets_len = offsets.len(); | ||
29 | |||
30 | if offsets_len == 0 { | ||
31 | return proptest::bool::ANY | ||
32 | .prop_flat_map(|b| { | ||
33 | // only valid edits | ||
34 | if b { | ||
35 | arb_text() | ||
36 | .prop_map(|text| vec![AtomTextEdit::insert(TextUnit::from(0), text)]) | ||
37 | .boxed() | ||
38 | } else { | ||
39 | Just(vec![]).boxed() | ||
40 | } | ||
41 | }) | ||
42 | .boxed(); | ||
43 | } | ||
44 | |||
45 | proptest::sample::subsequence(offsets, 0..offsets_len) | ||
46 | .prop_flat_map(|xs| { | ||
47 | let strategies: Vec<_> = xs | ||
48 | .chunks(2) | ||
49 | .map(|chunk| match chunk { | ||
50 | &[from, to] => { | ||
51 | let range = TextRange::from_to(from, to); | ||
52 | (proptest::bool::ANY) | ||
53 | .prop_flat_map(move |b| { | ||
54 | if b { | ||
55 | Just(AtomTextEdit::delete(range)).boxed() | ||
56 | } else { | ||
57 | arb_text() | ||
58 | .prop_map(move |text| AtomTextEdit::replace(range, text)) | ||
59 | .boxed() | ||
60 | } | ||
61 | }) | ||
62 | .boxed() | ||
63 | } | ||
64 | &[x] => arb_text() | ||
65 | .prop_map(move |text| AtomTextEdit::insert(x, text)) | ||
66 | .boxed(), | ||
67 | _ => unreachable!(), | ||
68 | }) | ||
69 | .collect(); | ||
70 | strategies | ||
71 | }) | ||
72 | .boxed() | ||
73 | } | ||
74 | |||
75 | fn arb_text_with_edits() -> BoxedStrategy<(String, Vec<AtomTextEdit>)> { | ||
76 | let text = arb_text(); | ||
77 | text.prop_flat_map(|s| { | ||
78 | let edits = arb_edits(&s); | ||
79 | (Just(s), edits) | ||
80 | }) | ||
81 | .boxed() | ||
82 | } | ||
83 | |||
84 | fn intersect(r1: TextRange, r2: TextRange) -> Option<TextRange> { | ||
85 | let start = r1.start().max(r2.start()); | ||
86 | let end = r1.end().min(r2.end()); | ||
87 | if start <= end { | ||
88 | Some(TextRange::from_to(start, end)) | ||
89 | } else { | ||
90 | None | ||
91 | } | ||
92 | } | ||
93 | |||
94 | proptest! { | ||
95 | #[test] | ||
96 | fn atom_text_edits_are_valid((text, edits) in arb_text_with_edits()) { | ||
97 | proptest_atom_text_edits_are_valid(text, edits) | ||
98 | } | ||
99 | } | ||
100 | |||
101 | fn proptest_atom_text_edits_are_valid(text: String, edits: Vec<AtomTextEdit>) { | ||
102 | // slicing doesn't panic | ||
103 | for e in &edits { | ||
104 | let _ = &text[e.delete]; | ||
105 | } | ||
106 | // ranges do not overlap | ||
107 | for (i1, e1) in edits.iter().skip(1).enumerate() { | ||
108 | for e2 in &edits[0..i1] { | ||
109 | if intersect(e1.delete, e2.delete).is_some() { | ||
110 | assert!(false, "Overlapping ranges {} {}", e1.delete, e2.delete); | ||
111 | } | ||
112 | } | ||
113 | } | ||
114 | } | ||