1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
//! "Interface" tests for pin store, maybe more later
use crate::repo::DataStore;
use std::path::PathBuf;
use std::sync::Arc;
use tempfile::TempDir;
pub struct DSTestContext<T> {
#[allow(dead_code)]
tempdir: TempDir,
datastore: Arc<T>,
}
impl<T: DataStore> DSTestContext<T> {
/// Create the test context which holds the DataStore inside an Arc and deletes the temporary
/// directory on drop.
pub async fn with<F>(factory: F) -> Self
where
F: FnOnce(PathBuf) -> T,
{
let tempdir = TempDir::new().expect("tempdir creation failed");
let p = tempdir.path().to_owned();
let ds = factory(p);
ds.init().await.unwrap();
DSTestContext {
tempdir,
datastore: Arc::new(ds),
}
}
}
impl<T: DataStore> std::ops::Deref for DSTestContext<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.datastore
}
}
/// Generates the "common interface" tests for PinStore implementations as a given module using a
/// types factory method. When adding tests, it might be easier to write them against the one
/// implementation and only then move them here; the compiler errors seem to point at the
/// `#[tokio::test]` attribute and the error needs to be guessed.
#[macro_export]
macro_rules! pinstore_interface_tests {
($module_name:ident, $factory:expr) => {
#[cfg(test)]
mod $module_name {
use futures::{StreamExt, TryStreamExt};
use ipld_core::cid::Cid;
use std::collections::HashMap;
use std::convert::TryFrom;
use $crate::repo::common_tests::DSTestContext;
use $crate::repo::{PinKind, PinMode, PinStore};
#[tokio::test]
async fn pin_direct_twice_is_good() {
let repo = DSTestContext::with($factory).await;
let empty =
Cid::try_from("QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH").unwrap();
assert_eq!(
repo.is_pinned(&empty).await.unwrap(),
false,
"initially unpinned"
);
repo.insert_direct_pin(&empty).await.unwrap();
assert_eq!(
repo.is_pinned(&empty).await.unwrap(),
true,
"must be pinned following direct pin"
);
repo.insert_direct_pin(&empty)
.await
.expect("rewriting existing direct pin as direct should be noop");
assert_eq!(
repo.is_pinned(&empty).await.unwrap(),
true,
"must be pinned following two direct pins"
);
}
#[tokio::test]
async fn cannot_recursively_unpin_unpinned() {
let repo = DSTestContext::with($factory).await;
// root/nested/deeper: QmX5S2xLu32K6WxWnyLeChQFbDHy79ULV9feJYH2Hy9bgp
let empty =
Cid::try_from("QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH").unwrap();
// the only pin we can try removing without first querying is direct, as shown in
// `cannot_unpin_indirect`.
let e = repo
.remove_recursive_pin(&empty, futures::stream::empty().boxed())
.await
.unwrap_err();
// FIXME: go-ipfs errors on the actual path
assert_eq!(e.to_string(), "not pinned or pinned indirectly");
}
#[tokio::test]
async fn cannot_unpin_indirect() {
let repo = DSTestContext::with($factory).await;
// root/nested/deeper: QmX5S2xLu32K6WxWnyLeChQFbDHy79ULV9feJYH2Hy9bgp
let root = Cid::try_from("QmX5S2xLu32K6WxWnyLeChQFbDHy79ULV9feJYH2Hy9bgp").unwrap();
let empty =
Cid::try_from("QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH").unwrap();
// first refs
repo.insert_recursive_pin(
&root,
futures::stream::iter(vec![Ok(empty.clone())]).boxed(),
)
.await
.unwrap();
// should panic because the caller must not attempt this because:
let (_, kind) = repo
.query(vec![empty.clone()], None)
.await
.unwrap()
.into_iter()
.next()
.unwrap();
// mem based uses "canonicalized" cids and fs uses them raw
match kind {
PinKind::IndirectFrom(v0_or_v1)
if v0_or_v1.hash() == root.hash() && v0_or_v1.codec() == root.codec() => {}
x => unreachable!("{:?}", x),
}
// this makes the "remove direct" invalid, as the direct pin must not be removed while
// recursively pinned
let e = repo.remove_direct_pin(&empty).await.unwrap_err();
// FIXME: go-ipfs errors on the actual path
assert_eq!(e.to_string(), "not pinned or pinned indirectly");
}
#[tokio::test]
async fn can_pin_direct_as_recursive() {
// the other way around doesn't work
let repo = DSTestContext::with($factory).await;
//
// root/nested/deeper: QmX5S2xLu32K6WxWnyLeChQFbDHy79ULV9feJYH2Hy9bgp
let root = Cid::try_from("QmX5S2xLu32K6WxWnyLeChQFbDHy79ULV9feJYH2Hy9bgp").unwrap();
let empty =
Cid::try_from("QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH").unwrap();
repo.insert_direct_pin(&root).await.unwrap();
let pins = repo.list(None).await.try_collect::<Vec<_>>().await.unwrap();
assert_eq!(
pins,
vec![(root.clone(), PinMode::Direct)],
"must find direct pin for root"
);
// first refs
repo.insert_recursive_pin(
&root,
futures::stream::iter(vec![Ok(empty.clone())]).boxed(),
)
.await
.unwrap();
let mut both = repo
.list(None)
.await
.try_collect::<HashMap<Cid, PinMode>>()
.await
.unwrap();
assert_eq!(both.remove(&root), Some(PinMode::Recursive));
assert_eq!(both.remove(&empty), Some(PinMode::Indirect));
assert!(both.is_empty(), "{:?}", both);
}
#[tokio::test]
async fn pin_recursive_pins_all_blocks() {
let repo = DSTestContext::with($factory).await;
// root/nested/deeper: QmX5S2xLu32K6WxWnyLeChQFbDHy79ULV9feJYH2Hy9bgp
let root = Cid::try_from("QmX5S2xLu32K6WxWnyLeChQFbDHy79ULV9feJYH2Hy9bgp").unwrap();
let empty =
Cid::try_from("QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH").unwrap();
// assumed use:
repo.insert_recursive_pin(
&root,
futures::stream::iter(vec![Ok(empty.clone())]).boxed(),
)
.await
.unwrap();
assert!(repo.is_pinned(&root).await.unwrap());
assert!(repo.is_pinned(&empty).await.unwrap());
let mut both = repo
.list(None)
.await
.try_collect::<HashMap<Cid, PinMode>>()
.await
.unwrap();
assert_eq!(both.remove(&root), Some(PinMode::Recursive));
assert_eq!(both.remove(&empty), Some(PinMode::Indirect));
assert!(both.is_empty(), "{:?}", both);
}
#[tokio::test]
async fn indirect_can_be_pinned_directly() {
let repo = DSTestContext::with($factory).await;
// root/nested/deeper: QmX5S2xLu32K6WxWnyLeChQFbDHy79ULV9feJYH2Hy9bgp
let root = Cid::try_from("QmX5S2xLu32K6WxWnyLeChQFbDHy79ULV9feJYH2Hy9bgp").unwrap();
let empty =
Cid::try_from("QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH").unwrap();
repo.insert_direct_pin(&empty).await.unwrap();
repo.insert_recursive_pin(
&root,
futures::stream::iter(vec![Ok(empty.clone())]).boxed(),
)
.await
.unwrap();
let mut both = repo
.list(None)
.await
.try_collect::<HashMap<Cid, PinMode>>()
.await
.unwrap();
assert_eq!(both.remove(&root), Some(PinMode::Recursive));
// when working on the first round of mem based recursive pinning I had understood
// this to be a rule. go-ipfs preferes the priority order of Recursive, Direct,
// Indirect and so does our fs datastore.
let mode = both.remove(&empty).unwrap();
assert!(
mode == PinMode::Indirect || mode == PinMode::Direct,
"{:?}",
mode
);
assert!(both.is_empty(), "{:?}", both);
}
#[tokio::test]
async fn direct_and_indirect_when_parent_unpinned() {
let repo = DSTestContext::with($factory).await;
// root/nested/deeper: QmX5S2xLu32K6WxWnyLeChQFbDHy79ULV9feJYH2Hy9bgp
let root = Cid::try_from("QmX5S2xLu32K6WxWnyLeChQFbDHy79ULV9feJYH2Hy9bgp").unwrap();
let empty =
Cid::try_from("QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH").unwrap();
repo.insert_direct_pin(&empty).await.unwrap();
assert_eq!(
repo.query(vec![empty.clone()], None)
.await
.unwrap()
.into_iter()
.collect::<Vec<_>>(),
vec![(empty.clone(), PinKind::Direct)],
);
// first refs
repo.insert_recursive_pin(
&root,
futures::stream::iter(vec![Ok(empty.clone())]).boxed(),
)
.await
.unwrap();
// second refs
repo.remove_recursive_pin(
&root,
futures::stream::iter(vec![Ok(empty.clone())]).boxed(),
)
.await
.unwrap();
let mut one = repo
.list(None)
.await
.try_collect::<HashMap<Cid, PinMode>>()
.await
.unwrap();
assert_eq!(one.remove(&empty), Some(PinMode::Direct));
assert!(one.is_empty(), "{:?}", one);
}
#[tokio::test]
async fn cannot_pin_recursively_pinned_directly() {
// this is a bit of odd as other ops are additive
let repo = DSTestContext::with($factory).await;
let empty =
Cid::try_from("QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH").unwrap();
repo.insert_recursive_pin(&empty, futures::stream::iter(vec![]).boxed())
.await
.unwrap();
let e = repo.insert_direct_pin(&empty).await.unwrap_err();
// go-ipfs puts the cid in front here, not sure if we want to at this level? though in
// go-ipfs it's different than path resolving
assert_eq!(e.to_string(), "already pinned recursively");
}
}
};
}