1
// @@ begin test lint list maintained by maint/add_warning @@
2
#![allow(clippy::bool_assert_comparison)]
3
#![allow(clippy::clone_on_copy)]
4
#![allow(clippy::dbg_macro)]
5
#![allow(clippy::mixed_attributes_style)]
6
#![allow(clippy::print_stderr)]
7
#![allow(clippy::print_stdout)]
8
#![allow(clippy::single_char_pattern)]
9
#![allow(clippy::unwrap_used)]
10
#![allow(clippy::unchecked_time_subtraction)]
11
#![allow(clippy::useless_vec)]
12
#![allow(clippy::needless_pass_by_value)]
13
#![allow(clippy::string_slice)] // See arti#2571
14
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
15
#![allow(clippy::needless_borrows_for_generic_args)] // TODO add to maint/add_warning
16

            
17
use std::fmt::{self, Debug};
18
use std::mem;
19
use std::slice;
20

            
21
use anyhow::Context as _;
22
use derive_deftly::Deftly;
23
use itertools::izip;
24
use testresult::TestResult;
25
use tor_error::{Bug, ErrorReport as _};
26

            
27
use crate::encode::{ItemEncoder, ItemObjectEncodable, NetdocEncodable, NetdocEncoder};
28
use crate::parse2::{
29
    ArgumentError as P2AE, ArgumentStream, ErrorProblem as P2EP, ItemObjectParseable,
30
    NetdocParseable, ParseError, ParseInput, UnparsedItem, parse_netdoc, parse_netdoc_multiple,
31
    parse_netdoc_multiple_sophisticated, parse_netdoc_multiple_with_offsets,
32
};
33
use crate::types::{Ignored, NotPresent};
34

            
35
38
fn default<T: Default>() -> T {
36
38
    Default::default()
37
38
}
38

            
39
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
40
#[derive_deftly(NetdocEncodable, NetdocParseable)]
41
struct Top {
42
    top_intro: (),
43
    needed: (String,),
44
    optional: Option<(String,)>,
45
    several: Vec<(String,)>,
46
    not_present: NotPresent,
47
    #[deftly(netdoc(default))]
48
    defaulted: (i32,),
49
    #[deftly(netdoc(keyword = "renamed"))]
50
    t4_renamed: Option<(String,)>,
51
    #[deftly(netdoc(subdoc))]
52
    sub1: Sub1,
53
    #[deftly(netdoc(subdoc))]
54
    sub2: Option<Sub2>,
55
    #[deftly(netdoc(subdoc))]
56
    sub3: Vec<Sub3>,
57
    #[deftly(netdoc(subdoc, default))]
58
    sub4: Sub4,
59
}
60

            
61
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
62
#[derive_deftly(NetdocEncodable, NetdocParseable)]
63
struct Sub1 {
64
    sub1_intro: (),
65
    sub1_field: Option<(String,)>,
66
    #[deftly(netdoc(flatten))]
67
    flatten: Flat1,
68
}
69
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
70
#[derive_deftly(NetdocEncodableFields, NetdocParseableFields)]
71
struct Flat1 {
72
    flat_needed: (String,),
73
    flat_optional: Option<(String,)>,
74
    flat_several: Vec<(String,)>,
75
    flat_defaulted: Option<(String,)>,
76
    #[deftly(netdoc(single_arg))]
77
    flat_arg_needed: String,
78
    #[deftly(netdoc(single_arg))]
79
    flat_arg_optional: Option<String>,
80
    #[deftly(netdoc(single_arg))]
81
    flat_arg_several: Vec<String>,
82
    #[deftly(netdoc(single_arg, default))]
83
    flat_arg_defaulted: i32,
84
    #[deftly(netdoc(with = needs_with_parse))]
85
    flat_with_needed: NeedsWith,
86
    #[deftly(netdoc(with = needs_with_parse))]
87
    flat_with_optional: Option<NeedsWith>,
88
    #[deftly(netdoc(with = needs_with_parse))]
89
    flat_with_several: Vec<NeedsWith>,
90
    #[deftly(netdoc(flatten))]
91
    flat_flat: FlatInner,
92
}
93
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
94
#[derive_deftly(NetdocEncodableFields, NetdocParseableFields)]
95
struct FlatInner {
96
    flat_inner_optional: Option<(String,)>,
97
}
98
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
99
#[derive_deftly(NetdocEncodable, NetdocParseable)]
100
struct Sub2 {
101
    #[deftly(netdoc(with = needs_with_intro))]
102
    sub2_intro: NeedsWith,
103
    sub2_field: Option<(String,)>,
104
    #[deftly(netdoc(single_arg))]
105
    arg_needed: String,
106
    #[deftly(netdoc(single_arg))]
107
    arg_optional: Option<String>,
108
    #[deftly(netdoc(single_arg))]
109
    arg_several: Vec<String>,
110
    #[deftly(netdoc(single_arg, default))]
111
    arg_defaulted: i32,
112
    #[deftly(netdoc(with = needs_with_parse))]
113
    with_needed: NeedsWith,
114
    #[deftly(netdoc(with = needs_with_parse))]
115
    with_optional: Option<NeedsWith>,
116
    #[deftly(netdoc(with = "needs_with_parse"))] // leave one with = "..."
117
    with_several: Vec<NeedsWith>,
118
    #[deftly(netdoc(subdoc))]
119
    subsub: SubSub,
120
}
121
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq, Ord, PartialOrd)]
122
#[derive_deftly(NetdocEncodable, NetdocParseable)]
123
struct Sub3 {
124
    sub3_intro: (),
125
    sub3_field: Option<(String,)>,
126
}
127
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
128
#[derive_deftly(NetdocEncodable, NetdocParseable)]
129
struct Sub4 {
130
    sub4_intro: (),
131
    sub4_field: Option<(String,)>,
132
}
133
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
134
#[derive_deftly(NetdocEncodable, NetdocParseable)]
135
struct SubSub {
136
    #[deftly(netdoc(single_arg))]
137
    subsub_intro: String,
138
    subsub_field: Option<(String,)>,
139
}
140

            
141
#[derive(Debug, Default, Clone, Eq, PartialEq, Ord, PartialOrd)]
142
struct NeedsWith;
143

            
144
impl NeedsWith {
145
112
    fn parse_expecting(exp: &str, args: &mut ArgumentStream<'_>) -> Result<NeedsWith, P2AE> {
146
112
        let got = args.next().ok_or(P2AE::Missing)?;
147
110
        (got == exp).then_some(NeedsWith).ok_or(P2AE::Invalid)
148
112
    }
149
}
150

            
151
mod needs_with_parse {
152
    use super::*;
153
82
    pub(super) fn from_unparsed(mut item: UnparsedItem<'_>) -> Result<NeedsWith, P2EP> {
154
82
        NeedsWith::parse_expecting("normal", item.args_mut())
155
82
            .map_err(item.args().error_handler("in needs with"))
156
82
    }
157
    #[allow(clippy::unnecessary_wraps)]
158
24
    pub(super) fn write_item_value_onto(_: &NeedsWith, out: ItemEncoder) -> Result<(), Bug> {
159
24
        out.arg(&"normal");
160
24
        Ok(())
161
24
    }
162
}
163
mod needs_with_intro {
164
    use super::*;
165
18
    pub(super) fn from_unparsed(mut item: UnparsedItem<'_>) -> Result<NeedsWith, P2EP> {
166
18
        NeedsWith::parse_expecting("intro", item.args_mut())
167
18
            .map_err(item.args().error_handler("in needs with"))
168
18
    }
169
    #[allow(clippy::unnecessary_wraps)]
170
4
    pub(super) fn write_item_value_onto(_: &NeedsWith, out: ItemEncoder) -> Result<(), Bug> {
171
4
        out.arg(&"intro");
172
4
        Ok(())
173
4
    }
174
}
175
mod needs_with_arg {
176
    use super::*;
177
12
    pub(super) fn from_args(args: &mut ArgumentStream) -> Result<NeedsWith, P2AE> {
178
12
        NeedsWith::parse_expecting("arg", args)
179
12
    }
180
    #[allow(clippy::unnecessary_wraps)]
181
4
    pub(super) fn write_arg_onto(_self: &NeedsWith, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
182
4
        out.args_raw_string(&"arg");
183
4
        Ok(())
184
4
    }
185
6
    pub(super) fn from_args_rest(s: &str) -> Result<NeedsWith, ()> {
186
6
        (s == "rest of line").then_some(NeedsWith).ok_or(())
187
6
    }
188
    #[allow(clippy::unnecessary_wraps)]
189
2
    pub(super) fn fmt_args_rest(_self: &NeedsWith, f: &mut fmt::Formatter) -> fmt::Result {
190
2
        write!(f, "rest of line")
191
2
    }
192
}
193

            
194
/// Test parsing and encoding of a single-document file
195
///
196
/// `doc_spec` is the document to parse.
197
/// `exp` is what it should parse as.
198
///
199
/// `doc_spec` can have magic instructions at end of each line.
200
/// These allow the re-encoding to be not quite identical to the input document.
201
///
202
///  * **`@ re-encoded:`:
203
///    This line is re-encoded differently.  The *next* source line is the encoding.
204
///
205
///  * **`@ not re-encoded`:
206
///    This line is omitted from the re-encoding.
207
///
208
///  * **`@ re-encoded later N`:
209
///    This line is reordered, to later in the re-encoding, by `N` lines.
210
///
211
///  * **`@ re-encoded only`:
212
///    This line eppears only in the re-encoding.
213
///    Prefer `re-encoded later` or `re-encoded:` if possible as they're clearer.
214
18
fn t_ok<D>(doc_spec: &str, exp: &D) -> TestResult<()>
215
18
where
216
18
    D: NetdocEncodable + NetdocParseable + Debug + PartialEq,
217
{
218
18
    t_ok_multi::<D>(&[], doc_spec, slice::from_ref(exp))
219
18
}
220

            
221
/// Test parsing and encoding of a multi-document file
222
///
223
/// The de/re-encoding syntax is as above.
224
//
225
// It would perhaps be better if `doc_boundaries` were obtained from magic instructions,
226
// but there's only one test case with a fragile hardcoded byte offset ATM.
227
20
fn t_ok_multi<D>(doc_boundaries: &[usize], doc_spec: &str, exp: &[D]) -> TestResult<()>
228
20
where
229
20
    D: NetdocEncodable + NetdocParseable + Debug + PartialEq,
230
{
231
20
    eprintln!("#####");
232
20
    eprint!("====== doc_spec ======\n{doc_spec}");
233
20
    eprintln!("====== exp ======\n{exp:#?}");
234

            
235
20
    let mut lines = doc_spec.split_inclusive('\n');
236
20
    let mut doc = String::new();
237
20
    let mut enc = String::new();
238

            
239
    // indices are line numbers but starting at 0
240
20
    let mut moved = Vec::<String>::new();
241
230
    let process_moved = |enc: &mut String, moved: &mut Vec<String>| {
242
230
        if moved.is_empty() {
243
140
            return;
244
90
        }
245
        loop {
246
112
            let lno = enc.lines().count();
247
112
            let Some(m) = moved.get_mut(lno) else {
248
36
                eprintln!("PN {lno:2} nothing");
249
36
                break;
250
            };
251
76
            if m.is_empty() {
252
54
                eprintln!("PN {lno:2} empty");
253
54
                break;
254
22
            }
255
22
            eprintln!("PN {lno:2} adding {m:?}");
256
22
            *enc += &mem::take(m);
257
        }
258
230
    };
259

            
260
230
    while let Some(l) = lines.next() {
261
210
        if let Some((l, insn)) = l.split_once('@') {
262
56
            eprintln!("LL    insn  {l:?}");
263
56
            let insn = insn.trim();
264
56
            let l = &format!("{}\n", l.trim_end());
265
56
            let insn = insn.trim_end();
266
56
            if insn == "re-encoded:" {
267
6
                doc += l;
268
6
                enc += lines.next().expect(r#""re-encoded:" needs re-encoded"#);
269
50
            } else if insn == "not re-encoded" {
270
10
                doc += l;
271
40
            } else if insn == "re-encoded only" {
272
12
                enc += l;
273
28
            } else if let Some(later) = insn.strip_prefix("re-encoded later ") {
274
28
                doc += l;
275
28
                let later: usize = later.parse().expect(later);
276
28
                let lno = later + enc.lines().count();
277
                loop {
278
120
                    if let Some(m) = moved.get_mut(lno) {
279
28
                        *m += l;
280
28
                        break;
281
92
                    }
282
92
                    moved.push("".into());
283
                }
284
            } else {
285
                panic!("unknown insn {insn:?} in {doc_spec:?}");
286
            }
287
154
        } else {
288
154
            eprintln!("LL    line  {l:?}");
289
154
            doc += l;
290
154
            enc += l;
291
154
        }
292
210
        process_moved(&mut enc, &mut moved);
293
    }
294
20
    process_moved(&mut enc, &mut moved);
295
92
    for (i, l) in moved.iter().enumerate() {
296
92
        assert_eq!(l, "", "line too late! {}: {l:?}", i + 1);
297
    }
298

            
299
20
    eprint!("====== doc ======\n{doc}");
300
20
    eprint!("====== enc exp ======\n{enc}");
301
20
    eprintln!("======");
302

            
303
20
    let input = ParseInput::new(&doc, "<literal>");
304

            
305
20
    if exp.len() == 1 {
306
18
        let got = parse_netdoc::<D>(&input).context(doc.clone())?;
307
18
        assert_eq!(got, exp[0], "parse 1 mismatch");
308
2
    }
309

            
310
20
    let got = parse_netdoc_multiple::<D>(&input)?;
311
20
    assert_eq!(got, exp, "parse_multiple mismatch");
312

            
313
20
    let got_with_offsets = parse_netdoc_multiple_with_offsets::<D>(&input)?;
314
22
    for (i, (got, start, end)) in got_with_offsets.iter().enumerate() {
315
22
        assert_eq!(got, &exp[i], "parse_multiple_with_offsets mismatch");
316
22
        assert_eq!(*start, if i == 0 { 0 } else { doc_boundaries[i - 1] });
317
22
        assert_eq!(*end, doc_boundaries.get(i).copied().unwrap_or(doc.len()));
318
    }
319

            
320
20
    let reenc = {
321
20
        let mut encoder = NetdocEncoder::default();
322
22
        for d in exp {
323
22
            d.encode_unsigned(&mut encoder)?;
324
        }
325
20
        encoder.finish()?
326
    };
327

            
328
20
    eprintln!("====== enc got ======\n{reenc}====== end ======");
329

            
330
20
    assert_eq_or_diff!(&enc, &reenc,);
331

            
332
20
    Ok(())
333
20
}
334

            
335
#[allow(clippy::unnecessary_wraps)] // Result for consistency
336
38
fn t_err_raw<D>(
337
38
    exp_lno: usize,
338
38
    exp_col: Option<usize>,
339
38
    exp_err: &str,
340
38
    doc: &str,
341
38
) -> TestResult<ParseError>
342
38
where
343
38
    D: NetdocParseable + Debug,
344
{
345
38
    let input = ParseInput::new(doc, "<massaged>");
346
38
    let got = parse_netdoc::<D>(&input).expect_err("unexpectedly parsed ok");
347
38
    let got_err = got.problem.to_string();
348
38
    assert_eq!(
349
38
        (got.lno, got.column),
350
38
        (exp_lno, exp_col),
351
        "doc\n====\n{doc}====\n got={}\n exp={exp_err}",
352
        got_err
353
    );
354
38
    assert_eq!(
355
        got_err, exp_err,
356
        "doc\n====\n{doc}====\n got={}\n exp={exp_err}",
357
        got_err
358
    );
359
38
    Ok(got)
360
38
}
361

            
362
/// Test an error case with embedded error message
363
///
364
/// `case` should be the input document, but exactly one line should
365
/// contain `" # "`, with the expected error message as a "comment".
366
///
367
/// Iff the expected message is supposed to have a column number,
368
/// the comment part should end with ` @<column>`.
369
///
370
/// `t_err` will check that that error is reported, at that line.
371
36
fn t_err<D>(mut case: &str) -> TestResult<ParseError>
372
36
where
373
36
    D: NetdocParseable + Debug,
374
{
375
36
    let mut exp = None;
376
36
    let mut doc = String::new();
377
36
    let mut lno = 0;
378
192
    while let Some((l, r)) = case.split_once('\n') {
379
156
        lno += 1;
380
156
        case = r;
381
156
        if let Some((l, r)) = l.split_once(" # ") {
382
36
            assert!(exp.is_none());
383
36
            exp = Some((lno, r.trim()));
384
36
            let l = l.trim_end();
385
36
            doc += l;
386
120
        } else {
387
120
            doc += l;
388
120
        }
389
156
        doc += "\n";
390
    }
391
36
    if !case.is_empty() {
392
        panic!("missing final newline");
393
36
    }
394
36
    let (exp_lno, exp_err) = exp.expect("missing # error indication in test case");
395
36
    let (exp_err, exp_col) = if let Some((l, r)) = exp_err.rsplit_once(" @") {
396
4
        (l, Some(r.parse().unwrap()))
397
    } else {
398
32
        (exp_err, None)
399
    };
400
36
    println!("==== 8<- ====\n{doc}==== ->8 ====");
401
36
    t_err_raw::<D>(exp_lno, exp_col, exp_err, &doc)
402
36
}
403

            
404
/// Test an error case with embedded error message
405
///
406
/// `case` should be the input document, but exactly one line should
407
/// contain `" # "`, with the expected error message as a "comment".
408
///
409
/// Iff the expected message is supposed to have a column number,
410
/// the comment part should end with ` @<column>`.
411
///
412
/// `t_err` will check that that error is reported, at that column.
413
4
fn t_err_chk_msg<D>(case: &str, msg: &str) -> TestResult
414
4
where
415
4
    D: NetdocParseable + Debug,
416
{
417
4
    let err = t_err::<D>(case)?;
418
4
    assert_eq!(err.report().to_string(), msg);
419
4
    Ok(())
420
4
}
421

            
422
#[test]
423
2
fn various_docs() -> TestResult<()> {
424
41
    let val = |s: &str| (s.to_owned(),);
425
23
    let sval = |s: &str| Some(val(s));
426

            
427
2
    let sub1_minimal = Sub1 {
428
2
        flatten: Flat1 {
429
2
            flat_needed: val("FN"),
430
2
            flat_arg_needed: "FAN".into(),
431
2
            ..default()
432
2
        },
433
2
        ..default()
434
2
    };
435
2
    let subsub_minimal = SubSub {
436
2
        subsub_intro: "SSI".into(),
437
2
        ..default()
438
2
    };
439
2
    let sub2_minimal = Sub2 {
440
2
        arg_needed: "AN".into(),
441
2
        subsub: subsub_minimal.clone(),
442
2
        ..default()
443
2
    };
444

            
445
2
    t_ok(
446
2
        r#"top-intro
447
2
needed N
448
2
defaulted 0                             @ re-encoded only
449
2
sub1-intro
450
2
flat-needed FN
451
2
flat-arg-needed FAN
452
2
flat-arg-defaulted 0                    @ re-encoded only
453
2
flat-with-needed normal
454
2
sub4-intro                              @ re-encoded only
455
2
"#,
456
2
        &Top {
457
2
            needed: val("N"),
458
2
            sub1: sub1_minimal.clone(),
459
2
            ..default()
460
2
        },
461
    )?;
462

            
463
2
    t_ok(
464
2
        r#"top-intro
465
2
needed N
466
2
defaulted 0                             @ re-encoded only
467
2
sub1-intro
468
2
flat-needed FN
469
2
flat-arg-needed FAN
470
2
flat-arg-defaulted 0                    @ re-encoded only
471
2
flat-with-needed normal
472
2
sub2-intro intro
473
2
with-needed normal                      @ re-encoded later 2
474
2
arg-needed AN
475
2
arg-defaulted 0                         @ re-encoded only
476
2
subsub-intro SSI
477
2
sub3-intro
478
2
sub3-intro
479
2
sub4-intro
480
2
"#,
481
2
        &Top {
482
2
            needed: val("N"),
483
2
            sub1: sub1_minimal.clone(),
484
2
            sub2: Some(sub2_minimal.clone()),
485
2
            sub3: vec![default(); 2],
486
2
            ..default()
487
2
        },
488
    )?;
489

            
490
2
    t_ok(
491
2
        r#"top-intro
492
2
needed N
493
2
optional O
494
2
several 1
495
2
not-present oh yes it is                @ not re-encoded
496
2
not-present but it is ignored           @ not re-encoded
497
2
several 2
498
2
defaulted -1
499
2
renamed R
500
2
sub1-intro
501
2
flat-several FS1                        @ re-encoded later 3
502
2
flat-needed FN                          @ re-encoded later 1
503
2
flat-with-needed normal                 @ re-encoded later 11
504
2
flat-inner-optional nested              @ re-encoded later 15
505
2
sub1-field A
506
2
flat-with-several normal                @ re-encoded later 11
507
2
flat-with-several normal                @ re-encoded later 11
508
2
flat-optional FO
509
2
flat-arg-needed FAN                     @ re-encoded later 2
510
2
flat-with-optional normal               @ re-encoded later 8
511
2
flat-several FS2
512
2
flat-defaulted FD
513
2
flat-arg-optional FAO
514
2
flat-arg-several FAS1 ignored           @ re-encoded:
515
2
flat-arg-several FAS1
516
2
flat-arg-several FAS2
517
2
flat-arg-defaulted 31
518
2
sub2-intro intro
519
2
with-several normal                     @ re-encoded later 8
520
2
with-several normal                     @ re-encoded later 8
521
2
with-several normal                     @ re-encoded later 8
522
2
sub2-field B
523
2
arg-needed AN
524
2
arg-optional AO
525
2
with-optional normal                    @ re-encoded later 4
526
2
arg-defaulted 4                         @ re-encoded later 2
527
2
arg-several A1
528
2
arg-several A2
529
2
with-needed normal
530
2
subsub-intro SSI
531
2
subsub-field BS
532
2
sub3-intro
533
2
sub3-field C1
534
2
sub3-intro
535
2
sub3-field C2
536
2
sub4-intro
537
2
sub4-field D
538
2
"#,
539
        &Top {
540
2
            needed: val("N"),
541
2
            optional: sval("O"),
542
2
            several: ["1", "2"].map(val).into(),
543
2
            defaulted: (-1,),
544
2
            t4_renamed: sval("R"),
545
2
            sub1: Sub1 {
546
2
                sub1_field: sval("A"),
547
2
                flatten: Flat1 {
548
2
                    flat_needed: val("FN"),
549
2
                    flat_optional: sval("FO"),
550
2
                    flat_several: ["FS1", "FS2"].map(val).into(),
551
2
                    flat_defaulted: sval("FD"),
552
2
                    flat_arg_needed: "FAN".into(),
553
2
                    flat_arg_several: ["FAS1", "FAS2"].map(Into::into).into(),
554
2
                    flat_arg_optional: Some("FAO".into()),
555
2
                    flat_arg_defaulted: 31,
556
2
                    flat_with_optional: Some(NeedsWith),
557
2
                    flat_with_several: vec![NeedsWith; 2],
558
2
                    flat_flat: FlatInner {
559
2
                        flat_inner_optional: sval("nested"),
560
2
                    },
561
2
                    ..Flat1::default()
562
2
                },
563
2
                ..default()
564
2
            },
565
2
            sub2: Some(Sub2 {
566
2
                sub2_field: sval("B"),
567
2
                arg_optional: Some("AO".into()),
568
2
                arg_defaulted: 4,
569
2
                arg_several: ["A1", "A2"].map(Into::into).into(),
570
2
                with_optional: Some(NeedsWith),
571
2
                with_several: vec![NeedsWith; 3],
572
2
                subsub: SubSub {
573
2
                    subsub_field: sval("BS"),
574
2
                    ..subsub_minimal.clone()
575
2
                },
576
2
                ..sub2_minimal.clone()
577
2
            }),
578
2
            sub3: ["C1", "C2"]
579
2
                .map(|s| Sub3 {
580
4
                    sub3_field: sval(s),
581
4
                    ..default()
582
4
                })
583
2
                .into(),
584
2
            sub4: Sub4 {
585
2
                sub4_field: sval("D"),
586
2
                ..default()
587
2
            },
588
2
            ..default()
589
        },
590
    )?;
591

            
592
2
    t_err_raw::<Top>(0, None, "empty document", r#""#)?;
593

            
594
2
    let wrong_document = r#"wrong-keyword # wrong document type
595
2
"#;
596
2
    t_err_chk_msg::<Top>(
597
2
        wrong_document,
598
2
        "error: failed to parse network document, type top-intro: <massaged>:1: wrong document type",
599
    )?;
600

            
601
2
    t_err::<Top>(
602
2
        r#"top-intro
603
2
sub4-intro # missing item needed
604
2
"#,
605
    )?;
606

            
607
2
    t_err::<Top>(
608
2
        r#"top-intro
609
2
sub1-intro
610
2
flat-arg-needed arg
611
2
flat-with-needed normal
612
2
sub4-intro # missing item flat-needed
613
2
"#,
614
    )?;
615

            
616
2
    t_err::<Top>(
617
2
        r#"top-intro
618
2
sub1-intro
619
2
flat-needed flat
620
2
flat-with-needed normal
621
2
sub4-intro # missing item flat-arg-needed
622
2
"#,
623
    )?;
624

            
625
2
    t_err::<Top>(
626
2
        r#"top-intro
627
2
sub1-intro
628
2
flat-arg-needed FAN
629
2
flat-with-needed normal
630
2
flat-needed FN
631
2
sub4-intro # missing item needed
632
2
"#,
633
    )?;
634

            
635
2
    t_err::<Top>(
636
2
        r#"top-intro
637
2
needed N
638
2
sub3-intro
639
2
sub4-intro # missing item sub1-intro
640
2
"#,
641
    )?;
642

            
643
2
    t_err::<Top>(
644
2
        r#"top-intro
645
2
needed N
646
2
sub1-intro
647
2
flat-needed FN1
648
2
flat-arg-needed FAN
649
2
flat-with-needed normal
650
2
sub1-intro # item repeated when not allowed
651
2
flat-needed FN2
652
2
"#,
653
    )?;
654

            
655
2
    t_err::<Top>(
656
2
        r#"top-intro
657
2
needed N
658
2
sub2-intro # missing argument in needs with
659
2
"#,
660
    )?;
661

            
662
2
    let wrong_value = r#"top-intro
663
2
needed N
664
2
sub2-intro wrong-value # invalid value for argument in needs with @12
665
2
"#;
666
2
    t_err_chk_msg::<Top>(
667
2
        wrong_value,
668
2
        "error: failed to parse network document, type top-intro: <massaged>:3.12: invalid value for argument in needs with",
669
    )?;
670

            
671
2
    t_err::<Top>(
672
2
        r#"top-intro
673
2
sub1-intro
674
2
flat-needed FN
675
2
flat-arg-needed arg
676
2
sub4-intro # missing item flat-with-needed
677
2
"#,
678
    )?;
679

            
680
2
    t_err::<Top>(
681
2
        r#"top-intro
682
2
sub1-intro
683
2
flat-needed FN
684
2
flat-arg-needed arg
685
2
flat-with-needed normal
686
2
sub2-intro intro
687
2
arg-needed AN
688
2
flat-arg-needed arg
689
2
sub3-intro # missing item with-needed
690
2
"#,
691
    )?;
692

            
693
2
    Ok(())
694
2
}
695

            
696
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
697
#[derive_deftly(NetdocEncodable, NetdocParseable)]
698
struct TopMinimal {
699
    test_item0: TestItem0,
700
    test_item: Option<TestItem>,
701
    test_item_rest: Option<TestItemRest>,
702
    test_item_rest_with: Option<TestItemRestWith>,
703
    test_item_object_not_present: Option<TestItemObjectNotPresent>,
704
    test_item_object_ignored: Option<TestItemObjectIgnored>,
705
    #[deftly(netdoc(skip))]
706
    __test_skip: (),
707
}
708

            
709
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
710
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
711
#[deftly(netdoc(no_extra_args))]
712
struct TestItem0 {
713
    #[deftly(netdoc(object(label = "UTF-8 STRING"), with = string_data_object))]
714
    object: Option<String>,
715
}
716

            
717
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
718
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
719
#[deftly(netdoc(debug))] // For testing our debugging arrangements
720
struct TestItem {
721
    #[deftly(netdoc(skip))]
722
    skip: u32,
723
    needed: String,
724
    #[deftly(netdoc(with = needs_with_arg))]
725
    optional: Option<NeedsWith>,
726
    rest: Vec<String>,
727
    #[deftly(netdoc(object))]
728
    object: TestObject,
729
}
730

            
731
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
732
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
733
struct TestItemRest {
734
    optional: Option<String>,
735
    #[deftly(netdoc(rest))]
736
    rest: String,
737
}
738

            
739
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
740
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
741
struct TestItemRestWith {
742
    #[deftly(netdoc(rest, with = needs_with_arg))]
743
    rest: NeedsWith,
744
}
745

            
746
#[derive(Debug, Default, Clone, Eq, PartialEq)]
747
struct TestObject(String);
748

            
749
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
750
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
751
struct TestItemObjectNotPresent {
752
    #[deftly(netdoc(object))]
753
    object: NotPresent,
754
}
755

            
756
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
757
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
758
struct TestItemObjectIgnored {
759
    #[deftly(netdoc(object))]
760
    object: Ignored,
761
}
762

            
763
/// Conversion module for `String` as Object with [`ItemValueParseable`]
764
mod string_data_object {
765
    /// Parse the data
766
8
    pub(super) fn try_from(data: Vec<u8>) -> Result<String, std::string::FromUtf8Error> {
767
8
        String::from_utf8(data)
768
8
    }
769

            
770
    /// Encode the data
771
    #[allow(clippy::unnecessary_wraps)] // signature must match the derive's expectation
772
2
    pub(super) fn write_object_onto<B>(self_: &String, b: &mut B) -> tor_bytes::EncodeResult<()>
773
2
    where
774
2
        B: tor_bytes::Writer + ?Sized,
775
    {
776
2
        b.write_all(self_.as_bytes());
777
2
        Ok(())
778
2
    }
779
}
780

            
781
impl ItemObjectParseable for TestObject {
782
18
    fn check_label(label: &str) -> Result<(), P2EP> {
783
18
        if label != "TEST OBJECT" {
784
            return Err(P2EP::ObjectIncorrectLabel);
785
18
        }
786
18
        Ok(())
787
18
    }
788
18
    fn from_bytes(data: &[u8]) -> Result<Self, P2EP> {
789
        Ok(TestObject(
790
18
            String::from_utf8(data.to_owned()).map_err(|_| P2EP::ObjectInvalidData)?,
791
        ))
792
18
    }
793
}
794
impl ItemObjectEncodable for TestObject {
795
6
    fn label(&self) -> &'static str {
796
6
        "TEST OBJECT"
797
6
    }
798
6
    fn write_object_onto(&self, b: &mut Vec<u8>) -> Result<(), Bug> {
799
6
        b.extend(self.0.as_bytes());
800
6
        Ok(())
801
6
    }
802
}
803

            
804
#[test]
805
2
fn various_items() -> TestResult<()> {
806
2
    let test_item_minimal = TestItem {
807
2
        needed: "N".into(),
808
2
        object: TestObject("hello".into()),
809
2
        ..default()
810
2
    };
811

            
812
2
    t_ok(
813
2
        r#"test-item0
814
2
"#,
815
2
        &TopMinimal { ..default() },
816
    )?;
817

            
818
2
    t_ok(
819
2
        r#"test-item0
820
2
test-item N
821
2
-----BEGIN TEST OBJECT-----
822
2
aGVsbG8=
823
2
-----END TEST OBJECT-----
824
2
"#,
825
2
        &TopMinimal {
826
2
            test_item: Some(test_item_minimal.clone()),
827
2
            ..default()
828
2
        },
829
    )?;
830

            
831
2
    t_ok(
832
2
        r#"test-item0
833
2
test-item N arg
834
2
-----BEGIN TEST OBJECT-----
835
2
aGVsbG8=
836
2
-----END TEST OBJECT-----
837
2
"#,
838
2
        &TopMinimal {
839
2
            test_item: Some(TestItem {
840
2
                optional: Some(NeedsWith),
841
2
                ..test_item_minimal.clone()
842
2
            }),
843
2
            ..default()
844
2
        },
845
    )?;
846

            
847
2
    t_ok(
848
2
        r#"test-item0
849
2
-----BEGIN UTF-8 STRING-----
850
2
aGVsbG8=
851
2
-----END UTF-8 STRING-----
852
2
test-item N arg R1 R2
853
2
-----BEGIN TEST OBJECT-----
854
2
aGVsbG8=
855
2
-----END TEST OBJECT-----
856
2
test-item-rest O  and  the rest                 @ re-encoded:
857
2
test-item-rest O and  the rest
858
2
test-item-rest-with   rest of line              @ re-encoded:
859
2
test-item-rest-with rest of line
860
2
test-item-object-not-present
861
2
test-item-object-ignored
862
2
-----BEGIN TEST OBJECT-----                     @ not re-encoded
863
2
aGVsbG8=         @ not re-encoded
864
2
-----END TEST OBJECT-----                       @ not re-encoded
865
2
"#,
866
2
        &TopMinimal {
867
2
            test_item0: TestItem0 {
868
2
                object: Some("hello".into()),
869
2
            },
870
2
            test_item: Some(TestItem {
871
2
                optional: Some(NeedsWith),
872
2
                rest: ["R1", "R2"].map(Into::into).into(),
873
2
                ..test_item_minimal.clone()
874
2
            }),
875
2
            test_item_rest: Some(TestItemRest {
876
2
                optional: Some("O".into()),
877
2
                rest: "and  the rest".into(),
878
2
            }),
879
2
            test_item_rest_with: Some(TestItemRestWith { rest: NeedsWith }),
880
2
            test_item_object_not_present: Some(TestItemObjectNotPresent { object: NotPresent }),
881
2
            test_item_object_ignored: Some(TestItemObjectIgnored { object: Ignored }),
882
2
            __test_skip: (),
883
2
        },
884
    )?;
885

            
886
2
    t_ok_multi(
887
2
        &[11],
888
2
        r#"test-item0
889
2
test-item0
890
2
test-item-rest optional resty rest
891
2
"#,
892
2
        &[
893
2
            TopMinimal::default(),
894
2
            TopMinimal {
895
2
                test_item_rest: Some(TestItemRest {
896
2
                    optional: Some("optional".into()),
897
2
                    rest: "resty rest".into(),
898
2
                }),
899
2
                ..default()
900
2
            },
901
2
        ],
902
    )?;
903

            
904
2
    t_err::<TopMinimal>(
905
2
        r#"test-item0 wrong # too many arguments @12
906
2
"#,
907
    )?;
908
2
    t_err::<TopMinimal>(
909
2
        r#"test-item0 # base64-encoded Object label is not as expected
910
2
-----BEGIN WRONG LABEL-----
911
2
aGVsbG8=
912
2
-----END WRONG LABEL-----
913
2
"#,
914
    )?;
915
2
    t_err::<TopMinimal>(
916
2
        r#"test-item0 # base64-encoded Object END label does not match BEGIN
917
2
-----BEGIN UTF-8 STRING-----
918
2
aGVsbG8=
919
2
-----END WRONG LABEL-----
920
2
"#,
921
    )?;
922
2
    t_err::<TopMinimal>(
923
2
        r#"test-item0
924
2
test-item-object-not-present # base64-encoded Object found where none expected
925
2
-----BEGIN TEST OBJECT-----
926
2
aGVsbG8=
927
2
-----END TEST OBJECT-----
928
2
"#,
929
    )?;
930
2
    t_err::<TopMinimal>(
931
2
        r#"test-item0 # base64-encoded Object has incorrectly formatted delimiter lines
932
2
-----BEGIN UTF-8 STRING-----
933
2
aGVsbG8=
934
2
-----END UTF-8 STRING
935
2
"#,
936
    )?;
937
2
    t_err::<TopMinimal>(
938
2
        r#"test-item0 # base64-encoded Object contains invalid base64
939
2
-----BEGIN UTF-8 STRING-----
940
2
bad b64 !
941
2
-----END UTF-8 STRING-----
942
2

            
943
2
"#,
944
    )?;
945
2
    t_err::<TopMinimal>(
946
2
        r#"test-item0 # base64-encoded Object contains invalid data
947
2
-----BEGIN UTF-8 STRING-----
948
2
hU6Qo2fW7+9PXkcrEyiB62ZDne/gwKPHXBo8lMeV8JCOfVBF5vT4BtKRLP+Jw66x
949
2
-----END UTF-8 STRING-----
950
2
"#,
951
    )?;
952

            
953
2
    Ok(())
954
2
}
955

            
956
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
957
#[derive_deftly(NetdocEncodable, NetdocParseable)]
958
struct TopSkips {
959
    top_skip_intro: (),
960
    #[deftly(netdoc(default(skip)))]
961
    item: TestItemRest,
962
    #[deftly(netdoc(default(skip), subdoc))]
963
    subdoc: Sub3,
964
}
965

            
966
#[test]
967
2
fn default_skip() -> TestResult<()> {
968
2
    t_ok(
969
2
        r#"top-skip-intro
970
2
"#,
971
2
        &TopSkips::default(),
972
    )?;
973

            
974
2
    t_ok(
975
2
        r#"top-skip-intro
976
2
item arg rest
977
2
sub3-intro
978
2
sub3-field s3f
979
2
"#,
980
2
        &TopSkips {
981
2
            item: TestItemRest {
982
2
                optional: Some("arg".into()),
983
2
                rest: "rest".into(),
984
2
            },
985
2
            subdoc: Sub3 {
986
2
                sub3_field: Some(("s3f".into(),)),
987
2
                ..default()
988
2
            },
989
2
            ..default()
990
2
        },
991
    )?;
992

            
993
2
    Ok(())
994
2
}
995

            
996
#[test]
997
2
fn multi_sophisticated() {
998
    #[derive(Debug, PartialEq, Deftly)]
999
    #[derive_deftly(NetdocParseable)]
    struct Doc {
        intro: (i32,),
        body: (i32,),
    }
2
    let text = r#"intro 0
2
body 0
2
intro 10
2
intro 20
2
body garbage
2
intro 30
2
body 30
2
ignored
2
intro 40
2
body garbage
2
ignored
2
intro 50
2
ignored-item-with-object
2
-----BEGIN THING-----
2
base64
2
-----END THING-----
2
body 50
2
intro 60
2
body 60
2
--BROKEN--
2
intro 999
2
body 999
2
"#;
2
    let expecteds = [
2
        Ok(0),
2
        Err("missing item body"),            // 10
2
        Err("invalid value for argument 0"), // 20
2
        Ok(30),
2
        Err("invalid value for argument 0"), // 40
2
        Ok(50),
2
        Err("incorrectly formatted delimiter lines"), // 60
2
                                                      // 999 is not read at all
2
    ];
2
    let input = ParseInput::new(text, "<test data>");
14
    for ((got, _, _), exp) in izip!(
2
        parse_netdoc_multiple_sophisticated::<Doc>(&input).unwrap(),
2
        expecteds,
    ) {
14
        match exp {
6
            Ok(n) => {
6
                let exp_doc = Doc {
6
                    intro: (n,),
6
                    body: (n,),
6
                };
6
                assert_eq!(got.unwrap(), exp_doc, "exp={exp:?}");
            }
8
            Err(e) => {
8
                let m = got.unwrap_err().report().to_string();
8
                assert!(m.contains(e), "exp={exp:?} got=Err({m:?})");
            }
        }
    }
2
}