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
19
fn default<T: Default>() -> T {
36
19
    Default::default()
37
19
}
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
56
    fn parse_expecting(exp: &str, args: &mut ArgumentStream<'_>) -> Result<NeedsWith, P2AE> {
146
56
        let got = args.next().ok_or(P2AE::Missing)?;
147
55
        (got == exp).then_some(NeedsWith).ok_or(P2AE::Invalid)
148
56
    }
149
}
150

            
151
mod needs_with_parse {
152
    use super::*;
153
41
    pub(super) fn from_unparsed(mut item: UnparsedItem<'_>) -> Result<NeedsWith, P2EP> {
154
41
        NeedsWith::parse_expecting("normal", item.args_mut())
155
41
            .map_err(item.args().error_handler("in needs with"))
156
41
    }
157
    #[allow(clippy::unnecessary_wraps)]
158
12
    pub(super) fn write_item_value_onto(_: &NeedsWith, out: ItemEncoder) -> Result<(), Bug> {
159
12
        out.arg(&"normal");
160
12
        Ok(())
161
12
    }
162
}
163
mod needs_with_intro {
164
    use super::*;
165
9
    pub(super) fn from_unparsed(mut item: UnparsedItem<'_>) -> Result<NeedsWith, P2EP> {
166
9
        NeedsWith::parse_expecting("intro", item.args_mut())
167
9
            .map_err(item.args().error_handler("in needs with"))
168
9
    }
169
    #[allow(clippy::unnecessary_wraps)]
170
2
    pub(super) fn write_item_value_onto(_: &NeedsWith, out: ItemEncoder) -> Result<(), Bug> {
171
2
        out.arg(&"intro");
172
2
        Ok(())
173
2
    }
174
}
175
mod needs_with_arg {
176
    use super::*;
177
6
    pub(super) fn from_args(args: &mut ArgumentStream) -> Result<NeedsWith, P2AE> {
178
6
        NeedsWith::parse_expecting("arg", args)
179
6
    }
180
    #[allow(clippy::unnecessary_wraps)]
181
2
    pub(super) fn write_arg_onto(_self: &NeedsWith, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
182
2
        out.args_raw_string(&"arg");
183
2
        Ok(())
184
2
    }
185
3
    pub(super) fn from_args_rest(s: &str) -> Result<NeedsWith, ()> {
186
3
        (s == "rest of line").then_some(NeedsWith).ok_or(())
187
3
    }
188
    #[allow(clippy::unnecessary_wraps)]
189
1
    pub(super) fn fmt_args_rest(_self: &NeedsWith, f: &mut fmt::Formatter) -> fmt::Result {
190
1
        write!(f, "rest of line")
191
1
    }
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
9
fn t_ok<D>(doc_spec: &str, exp: &D) -> TestResult<()>
215
9
where
216
9
    D: NetdocEncodable + NetdocParseable + Debug + PartialEq,
217
{
218
9
    t_ok_multi::<D>(&[], doc_spec, slice::from_ref(exp))
219
9
}
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
10
fn t_ok_multi<D>(doc_boundaries: &[usize], doc_spec: &str, exp: &[D]) -> TestResult<()>
228
10
where
229
10
    D: NetdocEncodable + NetdocParseable + Debug + PartialEq,
230
{
231
10
    eprintln!("#####");
232
10
    eprint!("====== doc_spec ======\n{doc_spec}");
233
10
    eprintln!("====== exp ======\n{exp:#?}");
234

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

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

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

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

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

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

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

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

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

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

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

            
332
10
    Ok(())
333
10
}
334

            
335
#[allow(clippy::unnecessary_wraps)] // Result for consistency
336
19
fn t_err_raw<D>(
337
19
    exp_lno: usize,
338
19
    exp_col: Option<usize>,
339
19
    exp_err: &str,
340
19
    doc: &str,
341
19
) -> TestResult<ParseError>
342
19
where
343
19
    D: NetdocParseable + Debug,
344
{
345
19
    let input = ParseInput::new(doc, "<massaged>");
346
19
    let got = parse_netdoc::<D>(&input).expect_err("unexpectedly parsed ok");
347
19
    let got_err = got.problem.to_string();
348
19
    assert_eq!(
349
19
        (got.lno, got.column),
350
19
        (exp_lno, exp_col),
351
        "doc\n====\n{doc}====\n got={}\n exp={exp_err}",
352
        got_err
353
    );
354
19
    assert_eq!(
355
        got_err, exp_err,
356
        "doc\n====\n{doc}====\n got={}\n exp={exp_err}",
357
        got_err
358
    );
359
19
    Ok(got)
360
19
}
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
18
fn t_err<D>(mut case: &str) -> TestResult<ParseError>
372
18
where
373
18
    D: NetdocParseable + Debug,
374
{
375
18
    let mut exp = None;
376
18
    let mut doc = String::new();
377
18
    let mut lno = 0;
378
96
    while let Some((l, r)) = case.split_once('\n') {
379
78
        lno += 1;
380
78
        case = r;
381
78
        if let Some((l, r)) = l.split_once(" # ") {
382
18
            assert!(exp.is_none());
383
18
            exp = Some((lno, r.trim()));
384
18
            let l = l.trim_end();
385
18
            doc += l;
386
60
        } else {
387
60
            doc += l;
388
60
        }
389
78
        doc += "\n";
390
    }
391
18
    if !case.is_empty() {
392
        panic!("missing final newline");
393
18
    }
394
18
    let (exp_lno, exp_err) = exp.expect("missing # error indication in test case");
395
18
    let (exp_err, exp_col) = if let Some((l, r)) = exp_err.rsplit_once(" @") {
396
2
        (l, Some(r.parse().unwrap()))
397
    } else {
398
16
        (exp_err, None)
399
    };
400
18
    println!("==== 8<- ====\n{doc}==== ->8 ====");
401
18
    t_err_raw::<D>(exp_lno, exp_col, exp_err, &doc)
402
18
}
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
2
fn t_err_chk_msg<D>(case: &str, msg: &str) -> TestResult
414
2
where
415
2
    D: NetdocParseable + Debug,
416
{
417
2
    let err = t_err::<D>(case)?;
418
2
    assert_eq!(err.report().to_string(), msg);
419
2
    Ok(())
420
2
}
421

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
693
1
    Ok(())
694
1
}
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
4
    pub(super) fn try_from(data: Vec<u8>) -> Result<String, std::string::FromUtf8Error> {
767
4
        String::from_utf8(data)
768
4
    }
769

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

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

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

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

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

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

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

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

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

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

            
953
1
    Ok(())
954
1
}
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
1
fn default_skip() -> TestResult<()> {
968
1
    t_ok(
969
1
        r#"top-skip-intro
970
1
"#,
971
1
        &TopSkips::default(),
972
    )?;
973

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

            
993
1
    Ok(())
994
1
}
995

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