diff --git a/Cargo.lock b/Cargo.lock index 286b7520..52308692 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1049,9 +1049,11 @@ name = "whippit" version = "0.6.0" dependencies = [ "chumsky", + "env_logger", "format", "indexmap", "insta", + "log", "serde", ] diff --git a/Cargo.toml b/Cargo.toml index 6ce54008..b31ff331 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ resolver = "2" [workspace.dependencies] +env_logger = "0.10.0" format = "0.2.4" indexmap = "2.0.0" insta = "1.38.0" diff --git a/moz-webgpu-cts/Cargo.toml b/moz-webgpu-cts/Cargo.toml index c6c82698..baac885d 100644 --- a/moz-webgpu-cts/Cargo.toml +++ b/moz-webgpu-cts/Cargo.toml @@ -13,7 +13,7 @@ dist = true [dependencies] camino = { version = "1.1.6", features = ["serde1"] } clap = { version = "4.4.2", features = ["derive"] } -env_logger = "0.10.0" +env_logger = { workspace = true } enumset = { version = "1.1.3", features = ["serde"] } format = { workspace = true } indexmap = { workspace = true } diff --git a/moz-webgpu-cts/src/metadata.rs b/moz-webgpu-cts/src/metadata.rs index c7b3b2e3..795d77d0 100644 --- a/moz-webgpu-cts/src/metadata.rs +++ b/moz-webgpu-cts/src/metadata.rs @@ -70,12 +70,12 @@ impl<'a> Properties<'a> for FileProps { fn property_parser( helper: &mut PropertiesParseHelper<'a>, - ) -> Boxed<'a, 'a, &'a str, Self::ParsedProperty, ParseError<'a>> { + ) -> Boxed<'a, 'a, &'a str, Option, ParseError<'a>> { let conditional_term = Expr::parser(Value::parser().map(|expr| expr.to_static())); let prefs = helper .parser( - just("prefs").to(()), + just("prefs").to(()).labelled("`prefs` property"), conditional_term.clone(), group(( ascii::ident() @@ -104,20 +104,23 @@ impl<'a> Properties<'a> for FileProps { just(']').padded_by(inline_whitespace()), ), ) - .map(|((), prefs)| FileProp::Prefs(prefs)); + .map(|opt| opt.map(|((), prefs)| FileProp::Prefs(prefs))); let tags = helper .parser( - keyword("tags").to(()), + keyword("tags").to(()).labelled("`tags` property`"), conditional_term.clone(), ascii::ident() + .labelled("tag name") .map(|i: &str| i.to_owned()) .separated_by(just(',').padded_by(inline_whitespace())) .collect() + .labelled("tag list") .delimited_by( just('[').padded_by(inline_whitespace()), just(']').padded_by(inline_whitespace()), ) + .labelled("tag list") .validate(|idents: Vec<_>, e, emitter| { if idents.is_empty() { emitter.emit(Rich::custom(e.span(), "no tags specified")); @@ -125,11 +128,13 @@ impl<'a> Properties<'a> for FileProps { idents }), ) - .map(|((), tags)| FileProp::Tags(tags)); + .map(|opt| opt.map(|((), tags)| FileProp::Tags(tags))); let disabled = helper .parser( - keyword(DISABLED_IDENT).to(()), + keyword(DISABLED_IDENT) + .to(()) + .labelled("`disabled` property"), conditional_term.clone(), any() .and_is(newline().or(end()).not()) @@ -138,7 +143,7 @@ impl<'a> Properties<'a> for FileProps { .to_slice() .map(|s: &str| s.to_owned()), ) - .map(|((), is_disabled)| FileProp::Disabled(is_disabled)); + .map(|opt| opt.map(|((), is_disabled)| FileProp::Disabled(is_disabled))); let implementation_status = helper .parser( @@ -146,12 +151,15 @@ impl<'a> Properties<'a> for FileProps { conditional_term, ImplementationStatus::property_value_parser(), ) - .map(|((), implementation_status)| { - FileProp::ImplementationStatus(implementation_status) + .map(|opt| { + opt.map(|((), implementation_status)| { + FileProp::ImplementationStatus(implementation_status) + }) }); - choice((prefs, tags, disabled, implementation_status)) - .map_with(|prop, e| (e.span(), prop)) + helper + .complete(choice((prefs, tags, disabled, implementation_status))) + .map_with(|prop, e| prop.map(|prop| (e.span(), prop))) .boxed() } @@ -200,11 +208,13 @@ fn file_props() { @r###" ParseResult { output: Some( - ( - 0..9, - Prefs( - Unconditional( - [], + Some( + ( + 0..9, + Prefs( + Unconditional( + [], + ), ), ), ), @@ -219,16 +229,18 @@ fn file_props() { @r###" ParseResult { output: Some( - ( - 0..32, - Prefs( - Unconditional( - [ - ( - "dom.webgpu.enabled", - "true", - ), - ], + Some( + ( + 0..32, + Prefs( + Unconditional( + [ + ( + "dom.webgpu.enabled", + "true", + ), + ], + ), ), ), ), @@ -242,7 +254,9 @@ fn file_props() { parser.parse("prefs: [dom.webgpu.enabled:[notvalidyet]]"), @r###" ParseResult { - output: None, + output: Some( + None, + ), errs: [ found ''d'' at 8..9 expected "property value", ], @@ -255,24 +269,26 @@ fn file_props() { @r###" ParseResult { output: Some( - ( - 0..114, - Prefs( - Unconditional( - [ - ( - "dom.webgpu.enabled", - "true", - ), - ( - "dom.webgpu.workers.enabled", - "true", - ), - ( - "dom.webgpu.testing.assert-hardware-adapter", - "true", - ), - ], + Some( + ( + 0..114, + Prefs( + Unconditional( + [ + ( + "dom.webgpu.enabled", + "true", + ), + ( + "dom.webgpu.workers.enabled", + "true", + ), + ( + "dom.webgpu.testing.assert-hardware-adapter", + "true", + ), + ], + ), ), ), ), @@ -287,11 +303,13 @@ fn file_props() { @r###" ParseResult { output: Some( - ( - 0..8, - Tags( - Unconditional( - [], + Some( + ( + 0..8, + Tags( + Unconditional( + [], + ), ), ), ), @@ -308,13 +326,15 @@ fn file_props() { @r###" ParseResult { output: Some( - ( - 0..14, - Tags( - Unconditional( - [ - "webgpu", - ], + Some( + ( + 0..14, + Tags( + Unconditional( + [ + "webgpu", + ], + ), ), ), ), @@ -328,7 +348,9 @@ fn file_props() { parser.parse("tags: [INVAL!D]"), @r###" ParseResult { - output: None, + output: Some( + None, + ), errs: [ found ''!'' at 12..13 expected '']'', ], @@ -340,9 +362,11 @@ fn file_props() { parser.parse("implementation-status: default"), @r###" ParseResult { - output: None, + output: Some( + None, + ), errs: [ - found end of input at 23..24 expected "property value", + found ''d'' at 23..24 expected ''b'', ''i'', or ''n'', ], } "### @@ -353,11 +377,13 @@ fn file_props() { @r###" ParseResult { output: Some( - ( - 0..35, - ImplementationStatus( - Unconditional( - Implementing, + Some( + ( + 0..35, + ImplementationStatus( + Unconditional( + Implementing, + ), ), ), ), @@ -372,11 +398,13 @@ fn file_props() { @r###" ParseResult { output: Some( - ( - 0..39, - ImplementationStatus( - Unconditional( - NotImplementing, + Some( + ( + 0..39, + ImplementationStatus( + Unconditional( + NotImplementing, + ), ), ), ), @@ -391,11 +419,13 @@ fn file_props() { @r###" ParseResult { output: Some( - ( - 0..30, - ImplementationStatus( - Unconditional( - Backlog, + Some( + ( + 0..30, + ImplementationStatus( + Unconditional( + Backlog, + ), ), ), ), @@ -409,15 +439,17 @@ fn file_props() { parser.parse("implementation-status: derp"), @r###" ParseResult { - output: None, + output: Some( + None, + ), errs: [ - found end of input at 23..24 expected "property value", + found ''d'' at 23..24 expected ''b'', ''i'', or ''n'', ], } "### ); - let parser = parser.padded(); + let parser = newline().ignore_then(parser.repeated().collect::>()); insta::assert_debug_snapshot!( parser.parse( @@ -433,10 +465,130 @@ disabled: ), @r###" ParseResult { - output: None, - errs: [ - found ''\n'' at 324..325 expected end of input, - ], + output: Some( + [ + Some( + ( + 1..325, + Prefs( + Conditional( + ConditionalValue { + conditions: [ + ( + Eq( + Value( + Variable( + "os", + ), + ), + Value( + Literal( + String( + "mac", + ), + ), + ), + ), + [ + ( + "dom.webgpu.enabled", + "true", + ), + ( + "dom.webgpu.workers.enabled", + "true", + ), + ( + "dom.webgpu.testing.assert-hardware-adapter", + "true", + ), + ], + ), + ( + Eq( + Value( + Variable( + "os", + ), + ), + Value( + Literal( + String( + "windows", + ), + ), + ), + ), + [ + ( + "dom.webgpu.enabled", + "true", + ), + ( + "dom.webgpu.workers.enabled", + "true", + ), + ( + "dom.webgpu.testing.assert-hardware-adapter", + "true", + ), + ], + ), + ], + fallback: Some( + [ + ( + "dom.webgpu.enabled", + "true", + ), + ( + "dom.webgpu.workers.enabled", + "true", + ), + ], + ), + }, + ), + ), + ), + ), + Some( + ( + 325..340, + Tags( + Unconditional( + [ + "webgpu", + ], + ), + ), + ), + ), + Some( + ( + 340..422, + Disabled( + Conditional( + ConditionalValue { + conditions: [ + ( + Value( + Variable( + "release_or_beta", + ), + ), + "https://mozilla-hub.atlassian.net/browse/FFXP-223", + ), + ], + fallback: None, + }, + ), + ), + ), + ), + ], + ), + errs: [], } "### ); @@ -596,7 +748,7 @@ impl ImplementationStatus { const BACKLOG: &'static str = "backlog"; const NOT_IMPLEMENTING: &'static str = "not-implementing"; - fn property_ident_parser<'a>() -> impl Parser<'a, &'a str, (), ParseError<'a>> { + fn property_ident_parser<'a>() -> impl Clone + Parser<'a, &'a str, (), ParseError<'a>> { just(Self::IDENT).to(()) } @@ -639,7 +791,7 @@ pub struct Test { #[cfg(test)] impl Test { - fn parser<'a>() -> impl Parser<'a, &'a str, (SectionHeader, Test), ParseError<'a>> { + fn parser<'a>() -> impl Parser<'a, &'a str, (Option, Test), ParseError<'a>> { metadata::test_parser() } } @@ -990,7 +1142,7 @@ where fn property_parser<'a, P>( helper: &mut PropertiesParseHelper<'a>, outcome_parser: P, - ) -> impl Parser<'a, &'a str, TestProp, ParseError<'a>> + ) -> impl Parser<'a, &'a str, Option>, ParseError<'a>> where Out: Eq + Hash + PartialEq, P: Clone + Parser<'a, &'a str, Out, ParseError<'a>>, @@ -1130,9 +1282,11 @@ where )) .padded_by(inline_whitespace()), ) - .map_with(|((), val), e| TestProp { - span: e.span(), - kind: TestPropKind::Expected(val), + .map_with(|opt, e| { + opt.map(|((), val)| TestProp { + span: e.span(), + kind: TestPropKind::Expected(val), + }) }), helper .parser( @@ -1140,20 +1294,22 @@ where conditional_term.clone(), just("true").to(()), ) - .validate(|((), val), e, emitter| { - match val { - PropertyValue::Unconditional(()) => (), - PropertyValue::Conditional { .. } => { - emitter.emit(Rich::custom( - e.span(), - "conditional rules for `disabled` aren't supported yet", - )); + .validate(|opt, e, emitter| { + opt.map(|((), val)| { + match val { + PropertyValue::Unconditional(()) => (), + PropertyValue::Conditional { .. } => { + emitter.emit(Rich::custom( + e.span(), + "conditional rules for `disabled` aren't supported yet", + )); + } } - } - TestProp { - span: e.span(), - kind: TestPropKind::Disabled, - } + TestProp { + span: e.span(), + kind: TestPropKind::Disabled, + } + }) }), helper .parser( @@ -1161,9 +1317,11 @@ where conditional_term, ImplementationStatus::property_value_parser(), ) - .map_with(|((), val), e| TestProp { - span: e.span(), - kind: TestPropKind::ImplementationStatus(val), + .map_with(|opt, e| { + opt.map(|((), val)| TestProp { + span: e.span(), + kind: TestPropKind::ImplementationStatus(val), + }) }), )) } @@ -1217,7 +1375,7 @@ impl<'a> Properties<'a> for TestProps { type ParsedProperty = TestProp; fn property_parser( helper: &mut PropertiesParseHelper<'a>, - ) -> Boxed<'a, 'a, &'a str, Self::ParsedProperty, ParseError<'a>> { + ) -> Boxed<'a, 'a, &'a str, Option, ParseError<'a>> { TestProp::property_parser( helper, choice(( @@ -1274,7 +1432,7 @@ impl<'a> Properties<'a> for TestProps { type ParsedProperty = TestProp; fn property_parser( helper: &mut PropertiesParseHelper<'a>, - ) -> Boxed<'a, 'a, &'a str, Self::ParsedProperty, ParseError<'a>> { + ) -> Boxed<'a, 'a, &'a str, Option, ParseError<'a>> { TestProp::property_parser( helper, choice(( @@ -1464,7 +1622,20 @@ r#" ), @r###" ParseResult { - output: None, + output: Some( + ( + Some( + "asdf", + ), + Test { + properties: TestProps { + is_disabled: false, + expectations: None, + }, + subtests: {}, + }, + ), + ), errs: [ found end of input at 108..112 expected something else, ], @@ -1484,7 +1655,9 @@ r#" ParseResult { output: Some( ( - "asdf", + Some( + "asdf", + ), Test { properties: TestProps { is_disabled: false, @@ -1556,7 +1729,9 @@ r#" ParseResult { output: Some( ( - "asdf", + Some( + "asdf", + ), Test { properties: TestProps { is_disabled: false, @@ -1651,7 +1826,9 @@ r#" ParseResult { output: Some( ( - "asdf", + Some( + "asdf", + ), Test { properties: TestProps { is_disabled: false, @@ -1718,7 +1895,9 @@ r#" ParseResult { output: Some( ( - "asdf", + Some( + "asdf", + ), Test { properties: TestProps { is_disabled: false, @@ -1783,7 +1962,9 @@ r#" ParseResult { output: Some( ( - "cts.https.html?q=webgpu:api,validation,buffer,destroy:twice:*", + Some( + "cts.https.html?q=webgpu:api,validation,buffer,destroy:twice:*", + ), Test { properties: TestProps { is_disabled: false, @@ -1836,3 +2017,211 @@ r#" "### ); } + +#[test] +fn outta_left_field() { + let file_parser = newline().then(File::parser()); + insta::assert_debug_snapshot!(file_parser.parse( + r#" +[good] + [still good] + [LEFT FIELD FTW] +"#, + ), @r###" + ParseResult { + output: Some( + ( + (), + File { + properties: FileProps { + is_disabled: None, + prefs: None, + tags: None, + implementation_status: None, + }, + tests: { + "good": Test { + properties: TestProps { + is_disabled: false, + expectations: None, + }, + subtests: { + "still good": Subtest { + properties: TestProps { + is_disabled: false, + expectations: None, + }, + }, + }, + }, + }, + }, + ), + ), + errs: [ + found ''['' at 27..28 expected ''e'', or ''d'', + ], + } + "###); +} + +#[test] +fn recover_gud_plz() { + use whippit::metadata::subtest_parser; + + env_logger::init(); + + let file_parser = newline().ignore_then( + subtest_parser::() + .repeated() + .collect::>(), + ); + insta::assert_debug_snapshot!(file_parser.parse( + r#" + [:powerPreference="_undef_";forceFallbackAdapter="_undef_"] + blarg: flarg + expected: + if os == "win" and debug: [PASS, FAIL] + FAIL + + [:powerPreference="_undef_";forceFallbackAdapter=false] + ofrick: lezduit + expected: + if os == "win" and debug: [PASS, FAIL] +"#, + ), @r###" + ParseResult { + output: Some( + [ + ( + Some( + ":powerPreference=\"_undef_\";forceFallbackAdapter=\"_undef_\"", + ), + Subtest { + properties: TestProps { + is_disabled: false, + expectations: Some( + NormalizedExpectationPropertyValue( + Expanded( + { + Windows: Expanded( + { + Debug: [ + Pass, + Fail, + ], + Optimized: [ + Fail, + ], + }, + ), + Linux: Collapsed( + [ + Fail, + ], + ), + MacOs: Collapsed( + [ + Fail, + ], + ), + }, + ), + ), + ), + }, + }, + ), + ( + Some( + ":powerPreference=\"_undef_\";forceFallbackAdapter=false", + ), + Subtest { + properties: TestProps { + is_disabled: false, + expectations: Some( + NormalizedExpectationPropertyValue( + Expanded( + { + Windows: Expanded( + { + Debug: [ + Pass, + Fail, + ], + }, + ), + }, + ), + ), + ), + }, + }, + ), + ], + ), + errs: [ + found ''b'' at 67..68 expected ''e'', or ''d'', + found ''o'' at 213..214 expected ''e'', or ''d'', + ], + } + "###); +} + +#[test] +fn plzwork() { + let file_parser = newline().then(File::parser()); + insta::assert_debug_snapshot!(file_parser.parse( + r#" +[thisfine] + [disgud] + expected: FAIL + ofrick: lol +"#, + ), @r###" + ParseResult { + output: Some( + ( + (), + File { + properties: FileProps { + is_disabled: None, + prefs: None, + tags: None, + implementation_status: None, + }, + tests: { + "thisfine": Test { + properties: TestProps { + is_disabled: false, + expectations: None, + }, + subtests: { + "disgud": Subtest { + properties: TestProps { + is_disabled: false, + expectations: Some( + NormalizedExpectationPropertyValue( + Collapsed( + Collapsed( + [ + Fail, + ], + ), + ), + ), + ), + }, + }, + }, + }, + }, + }, + ), + ), + errs: [ + found ''o'' at 46..47 expected ''e'', or ''d'', + ], + } + "###); +} diff --git a/whippit/Cargo.toml b/whippit/Cargo.toml index 9cc9ac9b..cec90652 100644 --- a/whippit/Cargo.toml +++ b/whippit/Cargo.toml @@ -16,7 +16,9 @@ serde1 = ["dep:serde"] chumsky = { version = "1.0.0-alpha.6", features = ["label", "pratt"] } format = { workspace = true } indexmap = { workspace = true, optional = true } +log = { workspace = true } serde = { workspace = true, features = ["derive"], optional = true } [dev-dependencies] +env_logger = { workspace = true } insta = { workspace = true } diff --git a/whippit/src/metadata.rs b/whippit/src/metadata.rs index 1d28947d..a84a0457 100644 --- a/whippit/src/metadata.rs +++ b/whippit/src/metadata.rs @@ -10,7 +10,9 @@ #[cfg(test)] use { - crate::metadata::properties::unstructured::{UnstructuredFile, UnstructuredSubtest}, + crate::metadata::properties::unstructured::{ + UnstructuredFile, UnstructuredSubtest, UnstructuredTest, + }, insta::assert_debug_snapshot, }; @@ -21,8 +23,9 @@ use chumsky::{ input::Emitter, prelude::Rich, primitive::{any, choice, custom, end, group, just}, + recovery::via_parser, span::SimpleSpan, - text::newline, + text::{inline_whitespace, newline}, IterParser, Parser, }; use format::lazy_format; @@ -68,26 +71,39 @@ where Property(P), Test(T), } - filler() - .ignore_then(choice(( - test_parser().map(Item::Test), - F::Properties::property_parser(&mut PropertiesParseHelper::new(0)).map(Item::Property), - ))) - .then_ignore(filler()) - .map_with(|test, e| (e.span(), test)) - .repeated() - .collect::>() - .validate(|parsed_tests, _e, emitter| { - let mut properties = F::Properties::default(); - let mut tests = F::Tests::default(); - for (span, item) in parsed_tests { - match item { - Item::Test((name, test)) => tests.add_test(name, test, span, emitter), - Item::Property(prop) => properties.add_property(prop, emitter), + choice(( + test_parser().map(Item::Test).labelled("test section"), + F::Properties::property_parser(&mut PropertiesParseHelper::new(0)) + .map(Item::Property) + .labelled("file property"), + )) + .padded_by(filler()) + .map_with(|test, e| (e.span(), test)) + .repeated() + .collect::>() + .validate(|parsed_tests, _e, emitter| { + let mut properties = F::Properties::default(); + let mut tests = F::Tests::default(); + for (span, item) in parsed_tests { + match item { + Item::Test((name, test)) => { + if let Some(name) = name { + tests.add_test(name, test, span, emitter) + } else { + // Presumably we applied recovery and emitted an error, so skip it. + } + } + Item::Property(prop) => { + if let Some(prop) = prop { + properties.add_property(prop, emitter) + } else { + // Presumably we applied recovery and emitted an error, so skip it. + } } } - F::new(properties, tests) - }) + } + F::new(properties, tests) + }) } fn filler<'a>() -> impl Parser<'a, &'a str, (), ParseError<'a>> { @@ -204,7 +220,7 @@ fn smoke_parser() { "blarg": UnstructuredTest { properties: {}, subtests: {}, - span: 1..10, + span: 1..9, }, "stuff": UnstructuredTest { properties: {}, @@ -290,7 +306,7 @@ fn smoke_parser() { ParseResult { output: None, errs: [ - found '' '' at 66..67 expected "test section header", or "indentation at the proper level", + found '' '' at 66..67 expected "test section", or "file property", ], } "###); @@ -488,26 +504,30 @@ pub trait Test<'a> { fn new(span: SimpleSpan, properties: Self::Properties, subtests: Self::Subtests) -> Self; } -pub fn test_parser<'a, T>() -> impl Parser<'a, &'a str, (SectionHeader, T), ParseError<'a>> +pub fn test_parser<'a, T>() -> impl Parser<'a, &'a str, (Option, T), ParseError<'a>> where T: Test<'a>, { #[derive(Debug)] enum Item { - Subtest { name: SectionHeader, subtest: S }, + Subtest { + name: Option, + subtest: S, + }, Property(Tp), - Newline, Comment, } let items = choice(( - subtest_parser().map(|(name, subtest)| Item::Subtest { name, subtest }), + comment(1).map(|_comment| Item::Comment).labelled("comment"), + subtest_parser() + .map(|(name, subtest)| Item::Subtest { name, subtest }) + .labelled("subtest section"), T::Properties::property_parser(&mut PropertiesParseHelper::new(1)) - .labelled("test property") - .map(Item::Property), - newline().labelled("empty line").map(|()| Item::Newline), - comment(1).map(|_comment| Item::Comment), + .map(Item::Property) + .labelled("test property"), )) + .padded_by(filler()) .map_with(|item, e| (e.span(), item)) .repeated() .collect::>(); @@ -523,11 +543,21 @@ where let mut subtests = T::Subtests::default(); for (span, item) in items { match item { - Item::Property(prop) => properties.add_property(prop, emitter), + Item::Property(prop) => { + if let Some(prop) = prop { + properties.add_property(prop, emitter) + } else { + // Presumably we applied recovery and emitted an error, so skip it. + } + } Item::Subtest { name, subtest } => { - subtests.add_subtest(name, subtest, span, emitter) + if let Some(name) = name { + subtests.add_subtest(name, subtest, span, emitter) + } else { + // Presumably we applied recovery and emitted an error, so skip it. + } } - Item::Newline | Item::Comment => (), + Item::Comment => (), } } let test = T::new(e.span(), properties, subtests); @@ -878,7 +908,7 @@ fn smoke_test() { }, ), }, - span: 101..142, + span: 101..144, }, }, span: 1..144, @@ -892,7 +922,8 @@ fn smoke_test() { ); } -fn subtest_parser<'a, S>() -> impl Parser<'a, &'a str, (SectionHeader, S), ParseError<'a>> +pub fn subtest_parser<'a, S>( +) -> impl Parser<'a, &'a str, (Option, S), ParseError<'a>> where S: Subtest<'a>, { @@ -901,13 +932,18 @@ where .labelled("subtest section header") .then( S::Properties::property_parser(&mut PropertiesParseHelper::new(2)) + .padded_by(filler()) .labelled("subtest property") .repeated() .collect::>() .validate(|props, e, emitter| { let mut properties = S::Properties::default(); for prop in props { - properties.add_property(prop, emitter); + if let Some(prop) = prop { + properties.add_property(prop, emitter); + } else { + // An error was presumably emitted during recovery, so skip it. + } } S::new(e.span(), properties) }), @@ -927,7 +963,9 @@ fn smoke_subtest() { ParseResult { output: Some( ( - "stuff and things", + Some( + "stuff and things", + ), UnstructuredSubtest { properties: {}, span: 22..22, @@ -948,7 +986,9 @@ fn smoke_subtest() { ParseResult { output: Some( ( - "stuff and things", + Some( + "stuff and things", + ), UnstructuredSubtest { properties: { "some_prop": Unconditional( @@ -975,7 +1015,9 @@ fn smoke_subtest() { ParseResult { output: Some( ( - "stuff and things", + Some( + "stuff and things", + ), UnstructuredSubtest { properties: { "expected": Conditional( @@ -1015,6 +1057,106 @@ fn indent<'a>(level: u8) -> impl Parser<'a, &'a str, (), ParseError<'a>> { .labelled("indentation at the proper level") } +fn anything_at_indent_or_greater<'a>( + level: u8, +) -> impl Clone + Parser<'a, &'a str, (), ParseError<'a>> { + let level_as_space_count = usize::from(level) * 2; + let indent = just(' ').repeated().exactly(level_as_space_count); + // TODO: Figure out why this doesn't work for line-oriented content: + // + // ``` + // any() + // .and_is(newline().not()) + // .repeated() + // .then(newline().or(end())) + // .repeated() + // .at_least(1) + // ``` + custom(move |input| { + if input.peek().is_some() { + loop { + let start = input.save(); + + // OPT: We _might_ be able to save some cycles by looping on individual characters and + // a finer-grained state machine. + match input.parse(indent.ignored()) { + Ok(()) => { + input.parse(rest_of_line().ignored())?; + let eol = input.save(); + match input.parse(newline()) { + Ok(()) => (), + Err(_e) => { + input.rewind(eol); + if input.next().is_none() { + break; + } + } + } + } + Err(_e) => { + input.rewind(start); + let offset = input.offset(); + match input.parse(inline_whitespace().then(newline().or(end())).ignored()) { + Ok(()) => { + let made_progress = input.offset() != offset; + if !made_progress { + break; + } + } + Err(_e) => { + input.rewind(start); + break; + } + } + } + } + } + } + Ok(()) + }) + .labelled("any content at specified indent or greater") +} + +#[test] +fn anything_at_indent_or_greater_works() { + insta::assert_debug_snapshot!(anything_at_indent_or_greater(0).parse("").into_result(), @r###" + Ok( + (), + ) + "###); + insta::assert_debug_snapshot!(anything_at_indent_or_greater(0).parse("asdf\nblarg").into_result(), @r###" + Ok( + (), + ) + "###); + insta::assert_debug_snapshot!(anything_at_indent_or_greater(0).parse("\n\n\n").into_result(), @r###" + Ok( + (), + ) + "###); + insta::assert_debug_snapshot!(anything_at_indent_or_greater(1).parse("").into_result(), @r###" + Ok( + (), + ) + "###); + insta::assert_debug_snapshot!(anything_at_indent_or_greater(1).parse("asdf\nblarg").into_result(), @r###" + Err( + [ + found ''a'' at 0..1 expected end of input, + ], + ) + "###); + insta::assert_debug_snapshot!(anything_at_indent_or_greater(1).parse("\n\n\n").into_result(), @r###" + Ok( + (), + ) + "###); +} + +fn rest_of_line<'a>() -> impl Clone + Parser<'a, &'a str, &'a str, ParseError<'a>> { + any().and_is(newline().not()).repeated().to_slice() +} + #[test] fn test_indent() { assert_debug_snapshot!(indent(0).parse(""), @r###" @@ -1151,36 +1293,34 @@ impl Debug for SectionHeader { } impl SectionHeader { - fn parser<'a>(indentation: u8) -> impl Parser<'a, &'a str, Self, ParseError<'a>> { + fn parser<'a>(indentation: u8) -> impl Parser<'a, &'a str, Option, ParseError<'a>> { let name = custom::<_, &str, _, _>(|input| { let mut escaped_name = String::new(); loop { - match input.peek() { - None => { - let start = input.offset(); - input.skip(); - let span = input.span_since(start); - return Err(Rich::custom( - span, - "reached end of input before ending section header", - )); - } - Some(']') => break, - Some('\\') => { + if input.parse(newline().or(end()).rewind()).is_ok() { + return Ok(None); + } + match input.peek().unwrap() { + ']' => break, + '\\' => { // NOTE: keep in sync. with the escaping in `Self::escaped`! let c = input.parse(just("\\]").to(']'))?; escaped_name.push(c); } - Some(other) => { + other => { escaped_name.push(other); input.skip(); } } } - Ok(escaped_name) + Ok(Some(escaped_name)) }) .validate(|escaped_name, e, emitter| { - for (idx, c) in escaped_name.char_indices() { + for (idx, c) in escaped_name + .as_ref() + .iter() + .flat_map(|cs| cs.char_indices()) + { if c.is_control() { let span_idx = e.span().start.checked_add(idx).unwrap(); emitter.emit(Rich::custom( @@ -1191,9 +1331,13 @@ impl SectionHeader { } escaped_name }); - indent(indentation) - .ignore_then(name.delimited_by(just('['), just(']'))) - .map(Self) + indent(indentation).then(just('[')).ignore_then( + name.then_ignore(just(']')) + .recover_with(via_parser( + rest_of_line().map(ToOwned::to_owned).map(|_| None), + )) + .map(|opt| opt.map(Self)), + ) } pub fn unescaped(&self) -> impl Display + '_ { @@ -1234,7 +1378,9 @@ fn smoke_section_name() { assert_debug_snapshot!(section_name(0).parse("[hoot]"), @r###" ParseResult { output: Some( - "hoot", + Some( + "hoot", + ), ), errs: [], } @@ -1242,7 +1388,9 @@ fn smoke_section_name() { assert_debug_snapshot!(section_name(0).parse("[asdf\\]blarg]"), @r###" ParseResult { output: Some( - "asdf]blarg", + Some( + "asdf]blarg", + ), ), errs: [], } @@ -1256,3 +1404,219 @@ fn smoke_section_name() { } "###); } + +#[test] +fn test_recover_gud_plzthx() { + let test = newline().then(test_parser::()); + insta::assert_debug_snapshot!(test.parse( + r#" +[cts.https.html?q=webgpu:api,operation,adapter,requestAdapter:requestAdapter:*] + [:powerPreference="_undef_";forceFallbackAdapter="_undef_"] + asdf: blarg + expected: + if os == "win" and debug: [PASS, FAIL] + [:powerPreference="_undef_";forceFallbackAdapter=false] +"#), @r###" + ParseResult { + output: Some( + ( + (), + ( + Some( + "cts.https.html?q=webgpu:api,operation,adapter,requestAdapter:requestAdapter:*", + ), + UnstructuredTest { + properties: {}, + subtests: { + ":powerPreference=\"_undef_\";forceFallbackAdapter=\"_undef_\"": UnstructuredSubtest { + properties: { + "asdf": Unconditional( + "blarg", + ), + "expected": Conditional( + ConditionalValue { + conditions: [ + ( + And( + Eq( + Value( + Variable( + "os", + ), + ), + Value( + Literal( + String( + "win", + ), + ), + ), + ), + Value( + Variable( + "debug", + ), + ), + ), + "[PASS, FAIL]", + ), + ], + fallback: None, + }, + ), + }, + span: 143..218, + }, + ":powerPreference=\"_undef_\";forceFallbackAdapter=false": UnstructuredSubtest { + properties: {}, + span: 276..276, + }, + }, + span: 1..276, + }, + ), + ), + ), + errs: [], + } + "###); +} + +#[test] +fn recover_gud_plz() { + let file_parser = newline().ignore_then(UnstructuredFile::parser()); + insta::assert_debug_snapshot!(file_parser.parse( + r#" +readysetgameover: true +[cts.https.html?q=webgpu:api,operation,adapter,requestAdapter:requestAdapter:*] + [:powerPreference="_undef_";forceFallbackAdapter="_undef_"] + expected: STUPID + wat: meh + + [:powerPreference="_undef_";forceFallbackAdapter=false] + expected: + if os == "win" and debug: [PASS, FAIL] + + [:powerPreference="_undef_";forceFallbackAdapter=true] + expected: + if os == "win" and debug: [PASS, FAIL] + +[whataboutme] + [shrug] +"#, + ), @r###" + ParseResult { + output: Some( + UnstructuredFile { + properties: { + "readysetgameover": Unconditional( + "true", + ), + }, + tests: { + "cts.https.html?q=webgpu:api,operation,adapter,requestAdapter:requestAdapter:*": UnstructuredTest { + properties: {}, + subtests: { + ":powerPreference=\"_undef_\";forceFallbackAdapter=\"_undef_\"": UnstructuredSubtest { + properties: { + "expected": Unconditional( + "STUPID", + ), + "wat": Unconditional( + "meh", + ), + }, + span: 166..201, + }, + ":powerPreference=\"_undef_\";forceFallbackAdapter=false": UnstructuredSubtest { + properties: { + "expected": Conditional( + ConditionalValue { + conditions: [ + ( + And( + Eq( + Value( + Variable( + "os", + ), + ), + Value( + Literal( + String( + "win", + ), + ), + ), + ), + Value( + Variable( + "debug", + ), + ), + ), + "[PASS, FAIL]", + ), + ], + fallback: None, + }, + ), + }, + span: 259..319, + }, + ":powerPreference=\"_undef_\";forceFallbackAdapter=true": UnstructuredSubtest { + properties: { + "expected": Conditional( + ConditionalValue { + conditions: [ + ( + And( + Eq( + Value( + Variable( + "os", + ), + ), + Value( + Literal( + String( + "win", + ), + ), + ), + ), + Value( + Variable( + "debug", + ), + ), + ), + "[PASS, FAIL]", + ), + ], + fallback: None, + }, + ), + }, + span: 376..436, + }, + }, + span: 24..436, + }, + "whataboutme": UnstructuredTest { + properties: {}, + subtests: { + "shrug": UnstructuredSubtest { + properties: {}, + span: 460..460, + }, + }, + span: 436..460, + }, + }, + }, + ), + errs: [], + } + "###); +} diff --git a/whippit/src/metadata/properties.rs b/whippit/src/metadata/properties.rs index e5e30f0a..b6763972 100644 --- a/whippit/src/metadata/properties.rs +++ b/whippit/src/metadata/properties.rs @@ -6,18 +6,22 @@ pub use unstructured::UnstructuredProperties; use std::{fmt::Debug, marker::PhantomData}; +use self::conditional::unstructured_value; pub use self::conditional::{ConditionalValue, Expr, Literal, Value}; use chumsky::{ input::Emitter, prelude::Rich, - primitive::{choice, end, group, just}, + primitive::{choice, custom, end, group, just}, + recovery::via_parser, text::{inline_whitespace, newline}, Boxed, Parser, }; use crate::metadata::{indent, ParseError}; +use super::{anything_at_indent_or_greater, rest_of_line}; + /// A right-hand-side property value in a [`File`], [`Test`], or [`Subtest`]. Can be /// "unconditional" or "conditional" (viz., runtime-evaluated). The `C` type parameter represents /// conditional clauses. The `V` type parameter represents right-hand values that this property can @@ -70,7 +74,7 @@ where /// Retrieve a parser for a single property that [`Self::add_property`] can accept. fn property_parser( helper: &mut PropertiesParseHelper<'a>, - ) -> Boxed<'a, 'a, &'a str, Self::ParsedProperty, ParseError<'a>>; + ) -> Boxed<'a, 'a, &'a str, Option, ParseError<'a>>; /// Accumulate a parsed property into this data structure. /// @@ -107,20 +111,14 @@ impl<'a> PropertiesParseHelper<'a> { /// ConditionalValue, Expr, Literal, PropertiesParseHelper, PropertyValue, Value /// }, /// reexport::chumsky::{ + /// error::Error, /// prelude::*, /// text::{ascii::keyword, newline}, + /// util::MaybeRef, /// } /// }; /// - /// # macro_rules! todo { - /// # ($expr:expr) => { - /// # PropertiesParseHelper::new(0) - /// # }; - /// # } - /// let mut helper: PropertiesParseHelper<'_> = todo!(concat!( - /// "unfortunately, you will need to use your imagination for this part; ", - /// "assume zero indentation for now!" - /// )); + /// let mut helper = PropertiesParseHelper::new(0); /// /// // Use `Expr` and `Value` parsing logic for conditionals. /// let my_condition_parser = Expr::parser(Value::parser()); @@ -132,7 +130,7 @@ impl<'a> PropertiesParseHelper<'a> { /// // If these were truly the full set of accepted values, one might want to use an `enum` and /// // `()` instead of retaining the `&str` values for key and value, respectively. For the /// // purposes of this example, though, we'll just keep things simple. - /// let my_property_parser = choice(( + /// let my_property_parser = helper.complete(choice(( /// helper.parser( /// keyword("disabled"), /// my_condition_parser.clone(), @@ -143,7 +141,7 @@ impl<'a> PropertiesParseHelper<'a> { /// my_condition_parser, /// keyword("FAIL").padded(), /// ), - /// )); + /// ))); /// /// // Make multiline string examples parseable by skipping the first newline /// let my_property_parser = newline().ignore_then(my_property_parser); @@ -152,10 +150,10 @@ impl<'a> PropertiesParseHelper<'a> { /// my_property_parser.parse(r#" /// disabled: true /// "#).into_result().unwrap(), - /// ( + /// Some(( /// "disabled", /// PropertyValue::Unconditional("true"), - /// ), + /// )), /// ); /// /// assert_eq!( @@ -163,67 +161,279 @@ impl<'a> PropertiesParseHelper<'a> { /// expected: /// if os == "win": FAIL /// "#).into_result().unwrap(), - /// ( + /// Some(( /// "expected", /// PropertyValue::Conditional(ConditionalValue { /// conditions: vec![ /// ( /// Expr::Eq( - /// Box::new(Expr::Value(Value::Variable("os".into()))), - /// Box::new(Expr::Value(Value::Literal(Literal::String("win".into())))), + /// Box::new(Expr::Value( + /// Value::Variable("os".into()) + /// )), + /// Box::new(Expr::Value( + /// Value::Literal(Literal::String("win".into())) + /// )), /// ), /// "FAIL", /// ), /// ], /// fallback: None, /// }) - /// ), + /// )), + /// ); + /// + /// assert_eq!( + /// // Incorrect, because `BLARG` isn't the expected `FAIL`. + /// my_property_parser.parse(r#" + /// expected: + /// if os == "win": BLARG + /// "#).into_output_errors(), + /// ( + /// Some(None), + /// vec![ + /// as Error<'_, &'_ str>>::expected_found( + /// [], + /// None, + /// SimpleSpan::from(29..34) + /// ) + /// ] + /// ) /// ); /// ``` pub fn parser( - &mut self, + &self, key_ident_parser: Pk, condition_parser: Pc, value_parser: Pv, - ) -> impl Parser<'a, &'a str, (K, PropertyValue), ParseError<'a>> + ) -> impl Parser<'a, &'a str, Option<(K, PropertyValue)>, ParseError<'a>> where - Pk: Parser<'a, &'a str, K, ParseError<'a>>, - Pc: Parser<'a, &'a str, C, ParseError<'a>>, + Pk: Clone + Parser<'a, &'a str, K, ParseError<'a>>, + Pc: Clone + Parser<'a, &'a str, C, ParseError<'a>>, Pv: Clone + Parser<'a, &'a str, V, ParseError<'a>>, + K: Clone, + C: Clone, + V: Clone, { - let indentation = self.indentation; - - let key_ident_parser = key_ident_parser.labelled("property key"); + let key_ident = key_ident_parser.labelled("property key"); let condition_parser = condition_parser.labelled("conditional term for property value"); let value_parser = value_parser.labelled("property value"); - let conditional_indent_level = indentation + let conditional_indent_level = self + .indentation + .checked_add(1) + .expect("unexpectedly high indentation level"); + + let property_value = choice(( + newline().ignore_then( + ConditionalValue::parser( + conditional_indent_level, + condition_parser, + value_parser.clone(), + ) + .recover_with(via_parser( + anything_at_indent_or_greater(conditional_indent_level).map(|_| None), + )) + .map(|opt| opt.map(PropertyValue::Conditional)) + .labelled("conditional value"), + ), + value_parser + .map(Some) + .recover_with(via_parser(unstructured_value().map(|_| None))) + .then_ignore(newline().or(end())) + .map(|opt| opt.map(PropertyValue::Unconditional)) + .labelled("unconditional value"), + )) + .labelled("property value"); + + key_ident + .then( + Self::colon() + .ignore_then(property_value) + .recover_with(via_parser(self.recovery().map(|_| None))), + ) + .map(|(key, value)| value.map(|value| (key, value))) + .labelled("property") + } + + pub fn complete

( + &self, + prop_parser: impl Parser<'a, &'a str, Option

, ParseError<'a>>, + ) -> impl Parser<'a, &'a str, Option

, ParseError<'a>> { + indent(self.indentation) + .ignore_then(prop_parser.recover_with(via_parser(self.recovery().map(|_| None)))) + } + + fn colon() -> impl Clone + Parser<'a, &'a str, (), ParseError<'a>> { + group((just(':'), inline_whitespace())) + .ignored() + .labelled("`: `") + } + + fn recovery(&self) -> impl Clone + Parser<'a, &'a str, (), ParseError<'a>> { + let conditional_indent_level = self + .indentation .checked_add(1) .expect("unexpectedly high indentation level"); - let property_value = || { - choice(( - value_parser - .clone() - .then_ignore(newline().or(end())) - .map(PropertyValue::Unconditional), - newline().ignore_then( - ConditionalValue::parser( - conditional_indent_level, - condition_parser, - value_parser, - ) - .map(PropertyValue::Conditional), + custom(move |input| { + let res = input.parse(rest_of_line()); + let rest_of_line = res?; + let eol = input.parse(newline().or(end()).to_slice())?; + + if input.peek().is_none() { + if !rest_of_line.is_empty() || !eol.is_empty() { + Ok(()) + } else { + Err(chumsky::error::Error::<'_, &str>::expected_found( + None, + None, + input.span_since(input.offset()), + )) + } + } else { + input.parse(anything_at_indent_or_greater(conditional_indent_level)) + } + }) + .ignored() + } +} + +#[test] +fn wat() { + use chumsky::{text::ascii::keyword, IterParser}; + + env_logger::init(); + + let my_condition_parser = Expr::parser(Value::parser()); + let helper = PropertiesParseHelper::new(0); + let my_property_parser = helper.complete(choice(( + helper.parser( + keyword("disabled"), + my_condition_parser.clone(), + keyword("true").labelled("`true` keyword"), + ), + helper.parser( + keyword("expected"), + my_condition_parser, + unstructured_value().labelled("test outcome"), + ), + ))); + insta::assert_debug_snapshot!( + newline() + .ignore_then( + my_property_parser + .repeated() + .collect::>() + ) + // TODO: test empty lines, empty values, empty everything + .parse( + r#" +disabled: true +expected: BLARG +"# + ), + @r###" + ParseResult { + output: Some( + [ + Some( + ( + "disabled", + Unconditional( + "true", + ), + ), + ), + Some( + ( + "expected", + Unconditional( + "BLARG", + ), + ), + ), + ], + ), + errs: [], + } + "### + ); +} + +#[test] +fn hmm() { + use chumsky::text; + + insta::assert_debug_snapshot!( + text::ascii::ident::<&str, _, ParseError<'_>>() + .try_map(|ident, span| if ident == "asdf" { + Ok( + ident + ) + } else { + Err(Rich::custom(span, format!("`{ident}` is not a valid thing-thang"))) + }) + .parse("blarg\n") + .into_result(), + + @r###" + Err( + [ + `blarg` is not a valid thing-thang at 0..5, + ], + ) + "### + + ); +} + +#[test] +fn plzwork() { + use chumsky::{prelude::*, text::keyword}; + + let helper = PropertiesParseHelper::new(2); + let my_condition_parser = Expr::parser(Value::parser()); + + let my_property_parser = helper.complete(choice(( + helper.parser( + keyword("disabled"), + my_condition_parser.clone(), + keyword("true").padded_by(inline_whitespace()), + ), + helper.parser( + keyword("expected"), + my_condition_parser, + keyword("FAIL").padded_by(inline_whitespace()), + ), + ))); + + let my_property_parser = + newline().ignore_then(my_property_parser.repeated().collect::>()); + + insta::assert_debug_snapshot!(my_property_parser.parse( + + r#" + expected: FAIL + ofrick: lol +"#, + ), @r###" + ParseResult { + output: Some( + [ + Some( + ( + "expected", + Unconditional( + "FAIL", + ), + ), ), - )) - .labelled("property value") - }; - - indent(indentation).ignore_then( - key_ident_parser - .labelled("property key") - .then_ignore(group((just(':'), inline_whitespace()))) - .then(property_value()), - ) + None, + ], + ), + errs: [ + found end of input at 24..30 expected something else, + ], } + "###); } diff --git a/whippit/src/metadata/properties/conditional.rs b/whippit/src/metadata/properties/conditional.rs index cea32018..2e81cf67 100644 --- a/whippit/src/metadata/properties/conditional.rs +++ b/whippit/src/metadata/properties/conditional.rs @@ -10,38 +10,53 @@ use { use chumsky::{ prelude::Rich, - primitive::{any, end, group, just}, + primitive::{any, custom, end, group, just}, + recovery::via_parser, text::{ascii::keyword, newline}, IterParser, Parser, }; -use crate::metadata::{indent, ParseError}; +use crate::metadata::{indent, rest_of_line, ParseError}; pub use self::expr::{Expr, Literal, Value}; +/// TODO: document recovery and errors fn conditional_rule<'a, C, V, Pc, Pv>( indentation: u8, condition_parser: Pc, value_parser: Pv, -) -> impl Parser<'a, &'a str, (C, V), ParseError<'a>> +) -> impl Parser<'a, &'a str, Option<(C, V)>, ParseError<'a>> where Pc: Parser<'a, &'a str, C, ParseError<'a>>, Pv: Parser<'a, &'a str, V, ParseError<'a>>, { - group((indent(indentation), keyword("if"), just(' '))) - .ignore_then( - condition_parser.nested_in( - any() - .and_is(newline().or(just(':').to(())).not()) - .repeated() - .at_least(1) - .to_slice(), - ), - ) - .then_ignore(group((just(':').to(()), just(' ').or_not().to(())))) - .then(value_parser.nested_in(unstructured_value())) - .then_ignore(newline().or(end())) - .labelled("conditional value rule") + group(( + indent(indentation), + keyword("if").labelled("`if` keyword"), + just(' ').labelled("space"), + )) + .ignore_then( + condition_parser + .then_ignore(group((just(':').to(()), just(' ').or_not().to(())))) + .then(value_parser) + .then_ignore(newline().or(end())) + .map(Some) + .recover_with(via_parser( + custom(|input| { + if input.peek().is_none() { + Err(chumsky::error::Error::<'_, &str>::expected_found( + None, + None, + input.span_since(input.offset()), + )) + } else { + Ok(()) + } + }) + .ignore_then(rest_of_line().then(newline().or(end())).map(|_| None)), + )), + ) + .labelled("conditional value rule") } #[test] @@ -57,22 +72,24 @@ fn test_conditional_rule() { assert_debug_snapshot!(conditional_rule(0).parse("if os == \"sux\": woot"), @r###" ParseResult { output: Some( - ( - Eq( - Value( - Variable( - "os", + Some( + ( + Eq( + Value( + Variable( + "os", + ), ), - ), - Value( - Literal( - String( - "sux", + Value( + Literal( + String( + "sux", + ), ), ), ), + "woot", ), - "woot", ), ), errs: [], @@ -82,22 +99,24 @@ fn test_conditional_rule() { assert_debug_snapshot!(conditional_rule(1).parse(" if os == \"sux\": woot"), @r###" ParseResult { output: Some( - ( - Eq( - Value( - Variable( - "os", + Some( + ( + Eq( + Value( + Variable( + "os", + ), ), - ), - Value( - Literal( - String( - "sux", + Value( + Literal( + String( + "sux", + ), ), ), ), + "woot", ), - "woot", ), ), errs: [], @@ -113,22 +132,24 @@ fn test_conditional_rule() { @r###" ParseResult { output: Some( - ( - Eq( - Value( - Variable( - "os", + Some( + ( + Eq( + Value( + Variable( + "os", + ), ), - ), - Value( - Literal( - String( - "sux", + Value( + Literal( + String( + "sux", + ), ), ), ), + "woot", ), - "woot", ), ), errs: [], @@ -142,13 +163,15 @@ fn test_conditional_rule() { @r###" ParseResult { output: Some( - ( - Value( - Variable( - "debug", + Some( + ( + Value( + Variable( + "debug", + ), ), + "ohnoes", ), - "ohnoes", ), ), errs: [], @@ -159,12 +182,16 @@ fn test_conditional_rule() { fn conditional_fallback<'a, V, Pv>( indentation: u8, value_parser: Pv, -) -> impl Parser<'a, &'a str, V, ParseError<'a>> +) -> impl Parser<'a, &'a str, Option, ParseError<'a>> where Pv: Parser<'a, &'a str, V, ParseError<'a>>, { indent(indentation) - .ignore_then(value_parser.nested_in(unstructured_value())) + .ignore_then( + value_parser + .map(Some) + .recover_with(via_parser(unstructured_value().map(|_| None))), + ) .then_ignore(newline().or(end())) .labelled("conditional value fallback") } @@ -176,7 +203,9 @@ fn test_conditional_fallback() { assert_debug_snapshot!(conditional_fallback(0).parse("[PASS, FAIL]"), @r###" ParseResult { output: Some( - "[PASS, FAIL]", + Some( + "[PASS, FAIL]", + ), ), errs: [], } @@ -184,7 +213,9 @@ fn test_conditional_fallback() { assert_debug_snapshot!(conditional_fallback(0).parse(r#""okgo""#), @r###" ParseResult { output: Some( - "\"okgo\"", + Some( + "\"okgo\"", + ), ), errs: [], } @@ -216,7 +247,9 @@ fn test_conditional_fallback() { assert_debug_snapshot!(conditional_fallback(1).parse(" @False"), @r###" ParseResult { output: Some( - "@False", + Some( + "@False", + ), ), errs: [], } @@ -248,7 +281,9 @@ fn test_conditional_fallback() { assert_debug_snapshot!(conditional_fallback(3).parse(" @True"), @r###" ParseResult { output: Some( - "@True", + Some( + "@True", + ), ), errs: [], } @@ -287,7 +322,7 @@ impl ConditionalValue { indentation: u8, condition_parser: Pc, value_parser: Pv, - ) -> impl Parser<'a, &'a str, ConditionalValue, ParseError<'a>> + ) -> impl Parser<'a, &'a str, Option>, ParseError<'a>> where Pc: Parser<'a, &'a str, C, ParseError<'a>>, Pv: Clone + Parser<'a, &'a str, V, ParseError<'a>>, @@ -307,9 +342,14 @@ impl ConditionalValue { ), )); } - ConditionalValue { - conditions, - fallback, + let conditions = conditions.into_iter().flatten().collect::>(); + if conditions.is_empty() && fallback.is_none() { + None + } else { + Some(ConditionalValue { + conditions, + fallback: fallback.flatten(), + }) } }) .labelled("conditional value") @@ -353,28 +393,30 @@ fn test_conditional_value() { @r###" ParseResult { output: Some( - ConditionalValue { - conditions: [ - ( - Eq( - Value( - Variable( - "os", + Some( + ConditionalValue { + conditions: [ + ( + Eq( + Value( + Variable( + "os", + ), ), - ), - Value( - Literal( - String( - "awesome", + Value( + Literal( + String( + "awesome", + ), ), ), ), + "great", ), - "great", - ), - ], - fallback: None, - }, + ], + fallback: None, + }, + ), ), errs: [], } @@ -392,47 +434,49 @@ TIMEOUT @r###" ParseResult { output: Some( - ConditionalValue { - conditions: [ - ( - Eq( - Value( - Variable( - "os", + Some( + ConditionalValue { + conditions: [ + ( + Eq( + Value( + Variable( + "os", + ), ), - ), - Value( - Literal( - String( - "mac", + Value( + Literal( + String( + "mac", + ), ), ), ), + "PASS", ), - "PASS", - ), - ( - Eq( - Value( - Variable( - "os", + ( + Eq( + Value( + Variable( + "os", + ), ), - ), - Value( - Literal( - String( - "linux", + Value( + Literal( + String( + "linux", + ), ), ), ), + "FAIL", ), - "FAIL", + ], + fallback: Some( + "TIMEOUT", ), - ], - fallback: Some( - "TIMEOUT", - ), - }, + }, + ), ), errs: [], } @@ -446,28 +490,30 @@ if os == "mac": PASS @r###" ParseResult { output: Some( - ConditionalValue { - conditions: [ - ( - Eq( - Value( - Variable( - "os", + Some( + ConditionalValue { + conditions: [ + ( + Eq( + Value( + Variable( + "os", + ), ), - ), - Value( - Literal( - String( - "mac", + Value( + Literal( + String( + "mac", + ), ), ), ), + "PASS", ), - "PASS", - ), - ], - fallback: None, - }, + ], + fallback: None, + }, + ), ), errs: [], } @@ -482,45 +528,47 @@ if os == "linux": FAIL @r###" ParseResult { output: Some( - ConditionalValue { - conditions: [ - ( - Eq( - Value( - Variable( - "os", + Some( + ConditionalValue { + conditions: [ + ( + Eq( + Value( + Variable( + "os", + ), ), - ), - Value( - Literal( - String( - "mac", + Value( + Literal( + String( + "mac", + ), ), ), ), + "PASS", ), - "PASS", - ), - ( - Eq( - Value( - Variable( - "os", + ( + Eq( + Value( + Variable( + "os", + ), ), - ), - Value( - Literal( - String( - "linux", + Value( + Literal( + String( + "linux", + ), ), ), ), + "FAIL", ), - "FAIL", - ), - ], - fallback: None, - }, + ], + fallback: None, + }, + ), ), errs: [], } @@ -535,45 +583,47 @@ if os == "linux": FAIL @r###" ParseResult { output: Some( - ConditionalValue { - conditions: [ - ( - Eq( - Value( - Variable( - "os", + Some( + ConditionalValue { + conditions: [ + ( + Eq( + Value( + Variable( + "os", + ), ), - ), - Value( - Literal( - String( - "mac", + Value( + Literal( + String( + "mac", + ), ), ), ), + "PASS", ), - "PASS", - ), - ( - Eq( - Value( - Variable( - "os", + ( + Eq( + Value( + Variable( + "os", + ), ), - ), - Value( - Literal( - String( - "linux", + Value( + Literal( + String( + "linux", + ), ), ), ), + "FAIL", ), - "FAIL", - ), - ], - fallback: None, - }, + ], + fallback: None, + }, + ), ), errs: [], } @@ -589,47 +639,49 @@ if os == "linux": FAIL @r###" ParseResult { output: Some( - ConditionalValue { - conditions: [ - ( - Eq( - Value( - Variable( - "os", + Some( + ConditionalValue { + conditions: [ + ( + Eq( + Value( + Variable( + "os", + ), ), - ), - Value( - Literal( - String( - "mac", + Value( + Literal( + String( + "mac", + ), ), ), ), + "PASS", ), - "PASS", - ), - ( - Eq( - Value( - Variable( - "os", + ( + Eq( + Value( + Variable( + "os", + ), ), - ), - Value( - Literal( - String( - "linux", + Value( + Literal( + String( + "linux", + ), ), ), ), + "FAIL", ), - "FAIL", + ], + fallback: Some( + "TIMEOUT", ), - ], - fallback: Some( - "TIMEOUT", - ), - }, + }, + ), ), errs: [], } @@ -645,47 +697,49 @@ if os == "linux": FAIL @r###" ParseResult { output: Some( - ConditionalValue { - conditions: [ - ( - Eq( - Value( - Variable( - "os", + Some( + ConditionalValue { + conditions: [ + ( + Eq( + Value( + Variable( + "os", + ), ), - ), - Value( - Literal( - String( - "mac", + Value( + Literal( + String( + "mac", + ), ), ), ), + "[dom.webgpu.enabled:true, dom.webgpu.workers.enabled:true, dom.webgpu.testing.assert-hardware-adapter:true]", ), - "[dom.webgpu.enabled:true, dom.webgpu.workers.enabled:true, dom.webgpu.testing.assert-hardware-adapter:true]", - ), - ( - Eq( - Value( - Variable( - "os", + ( + Eq( + Value( + Variable( + "os", + ), ), - ), - Value( - Literal( - String( - "windows", + Value( + Literal( + String( + "windows", + ), ), ), ), + "[dom.webgpu.enabled:true, dom.webgpu.workers.enabled:true, dom.webgpu.testing.assert-hardware-adapter:true]", ), - "[dom.webgpu.enabled:true, dom.webgpu.workers.enabled:true, dom.webgpu.testing.assert-hardware-adapter:true]", + ], + fallback: Some( + "[dom.webgpu.enabled:true, dom.webgpu.workers.enabled:true]", ), - ], - fallback: Some( - "[dom.webgpu.enabled:true, dom.webgpu.workers.enabled:true]", - ), - }, + }, + ), ), errs: [], } diff --git a/whippit/src/metadata/properties/unstructured.rs b/whippit/src/metadata/properties/unstructured.rs index 9612b6cb..fd9bd7dd 100644 --- a/whippit/src/metadata/properties/unstructured.rs +++ b/whippit/src/metadata/properties/unstructured.rs @@ -123,13 +123,13 @@ impl<'a> Properties<'a> for UnstructuredProperties<'a> { ); fn property_parser( helper: &mut PropertiesParseHelper<'a>, - ) -> Boxed<'a, 'a, &'a str, Self::ParsedProperty, ParseError<'a>> { + ) -> Boxed<'a, 'a, &'a str, Option, ParseError<'a>> { helper - .parser( + .complete(helper.parser( ident().map_with(|key, e| (e.span(), key)), unstructured_conditional_term(), unstructured_value(), - ) + )) .boxed() } @@ -147,4 +147,5 @@ impl<'a> Properties<'a> for UnstructuredProperties<'a> { pub(crate) fn unstructured_conditional_term<'a>( ) -> impl Clone + Parser<'a, &'a str, conditional::Expr>, ParseError<'a>> { conditional::Expr::parser(conditional::Value::parser()) + .labelled("unstructured conditional term") }