use std::{borrow::Cow, collections::BTreeSet, str::FromStr};
pub use parser::ParseError;
use super::{
hop_pattern::{lexer::HopPatternLexer, parser::HopPatternParser},
types::{HopPredicate, PathPolicyHop},
};
use crate::path::{ScionPath, policy::PathPolicy};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct HopPatternPolicy(Vec<HopPatternExpression>);
impl HopPatternPolicy {
#[inline]
pub fn parse(s: &str) -> Result<Self, ParseError> {
let tokens = HopPatternLexer::new(s).tokenize();
HopPatternParser::new(&tokens).parse()
}
#[inline]
pub fn matches(&self, hops: &[PathPolicyHop]) -> bool {
let mut positions: Vec<usize> = vec![0];
for expr in &self.0 {
let mut next_positions = Vec::new();
for &position in &positions {
next_positions.extend(expr.match_from(hops, position));
}
next_positions.sort_unstable();
next_positions.dedup();
positions = next_positions;
if positions.is_empty() {
return false;
}
}
positions.contains(&hops.len())
}
}
impl FromStr for HopPatternPolicy {
type Err = ParseError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl PathPolicy for HopPatternPolicy {
#[inline]
fn path_allowed(&self, path: &ScionPath) -> Result<bool, std::borrow::Cow<'static, str>> {
let path_hops = PathPolicyHop::hops_from_path(path).map_err(Cow::from)?;
Ok(self.matches(&path_hops))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum HopPatternExpression {
HopPredicate(HopPredicate),
Or(Box<HopPatternExpression>, Box<HopPatternExpression>),
Optional(Box<HopPatternExpression>),
OneOrMore(Box<HopPatternExpression>),
ZeroOrMore(Box<HopPatternExpression>),
}
impl HopPatternExpression {
pub fn match_from(&self, hops: &[PathPolicyHop], pos: usize) -> BTreeSet<usize> {
match self {
HopPatternExpression::HopPredicate(pred) => {
if pos < hops.len() && hops[pos].matches(pred) {
let mut set = BTreeSet::new();
set.insert(pos + 1); set
} else {
BTreeSet::new()
}
}
HopPatternExpression::Or(a, b) => {
let mut left = a.match_from(hops, pos);
let mut right = b.match_from(hops, pos);
left.append(&mut right);
left
}
HopPatternExpression::Optional(inner) => {
let mut res = BTreeSet::new();
res.insert(pos);
res.extend(inner.match_from(hops, pos));
res
}
HopPatternExpression::OneOrMore(inner) => {
Self::all_nested_matches(hops, pos, inner)
}
HopPatternExpression::ZeroOrMore(inner) => {
let mut vec = Self::all_nested_matches(hops, pos, inner);
vec.insert(pos);
vec
}
}
}
#[inline]
fn all_nested_matches(
hops: &[PathPolicyHop],
pos: usize,
inner: &HopPatternExpression,
) -> BTreeSet<usize> {
let mut all = BTreeSet::new();
let mut frontier = inner.match_from(hops, pos);
all.extend(&frontier);
while !frontier.is_empty() {
let mut next = BTreeSet::new();
for p in frontier {
let res = inner.match_from(hops, p);
for n in res {
if !all.contains(&n) {
all.insert(n);
next.insert(n);
}
}
}
frontier = next;
}
all
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identifier::isd_asn::IsdAsn;
fn hops(spec: &[&str]) -> Vec<PathPolicyHop> {
spec.iter()
.map(|s| {
PathPolicyHop {
isd_asn: IsdAsn::from_str(s).expect("valid IsdAsn"),
ingress: 0,
egress: 0,
}
})
.collect()
}
mod happy {
use super::*;
#[test]
fn simple_linear_pattern_matches() {
let seq = HopPatternPolicy::parse("1 2 3").unwrap();
let hv = hops(&["1-1", "2-1", "3-1"]);
assert!(seq.matches(&hv));
}
#[test]
fn pattern_too_short_path_no_match() {
let seq = HopPatternPolicy::parse("1 2 3").unwrap();
let hv = hops(&["1-1", "2-1"]);
assert!(!seq.matches(&hv));
}
#[test]
fn pattern_wrong_order_no_match() {
let seq = HopPatternPolicy::parse("1 2").unwrap();
let hv = hops(&["2-1", "1-1"]);
assert!(!seq.matches(&hv));
}
#[test]
fn optional_absent_matches() {
let seq = HopPatternPolicy::parse("1 2? 3").unwrap();
let hv = hops(&["1-1", "3-1"]);
assert!(seq.matches(&hv));
}
#[test]
fn optional_present_matches() {
let seq = HopPatternPolicy::parse("1 2? 3").unwrap();
let hv = hops(&["1-1", "2-1", "3-1"]);
assert!(seq.matches(&hv));
}
#[test]
fn one_or_more_single_matches() {
let seq = HopPatternPolicy::parse("1+").unwrap();
let hv = hops(&["1-1"]);
assert!(seq.matches(&hv));
}
#[test]
fn one_or_more_multiple_matches() {
let seq = HopPatternPolicy::parse("1+").unwrap();
let hv = hops(&["1-1", "1-1", "1-1"]);
assert!(seq.matches(&hv));
}
#[test]
fn one_or_more_with_final_segment_matches() {
let seq = HopPatternPolicy::parse("1+ 1-4").unwrap();
let hv = hops(&["1-1", "1-1", "1-1", "1-4"]);
assert!(seq.matches(&hv));
}
#[test]
fn one_or_more_missing_final_no_match() {
let seq = HopPatternPolicy::parse("1+ 1-4").unwrap();
let hv = hops(&["1-1", "1-1", "1-1", "1-5"]);
assert!(!seq.matches(&hv));
}
#[test]
fn one_or_more_with_zero_no_match() {
let seq = HopPatternPolicy::parse("1+").unwrap();
let hv = hops(&["2-1"]);
assert!(!seq.matches(&hv));
}
#[test]
fn zero_or_more_zero_case_matches() {
let seq = HopPatternPolicy::parse("1* 2").unwrap();
let hv = hops(&["2-1"]);
assert!(seq.matches(&hv));
}
#[test]
fn zero_or_more_multiple_case_matches() {
let seq = HopPatternPolicy::parse("1* 2").unwrap();
let hv = hops(&["1-1", "1-1", "2-1"]);
assert!(seq.matches(&hv));
}
#[test]
fn zero_or_more_with_final_matches() {
let seq = HopPatternPolicy::parse("1* 1-5").unwrap();
let hv = hops(&["1-1", "1-1", "1-5"]);
assert!(seq.matches(&hv));
}
#[test]
fn zero_or_more_bad_final_no_match() {
let seq = HopPatternPolicy::parse("1* 1-5").unwrap();
let hv = hops(&["1-1", "1-1", "1-4"]);
assert!(!seq.matches(&hv));
}
#[test]
fn or_left_branch_matches() {
let seq = HopPatternPolicy::parse("(1 | 2) 3").unwrap();
let hv = hops(&["1-1", "3-1"]);
assert!(seq.matches(&hv));
}
#[test]
fn or_right_branch_matches() {
let seq = HopPatternPolicy::parse("(1 | 2) 3").unwrap();
let hv = hops(&["2-1", "3-1"]);
assert!(seq.matches(&hv));
}
#[test]
fn chained_or_middle_branch_matches() {
let seq = HopPatternPolicy::parse("1 | 2 | 3").unwrap();
let hv = hops(&["2-1"]);
assert!(seq.matches(&hv));
}
#[test]
fn or_no_branch_matches_no_match() {
let seq = HopPatternPolicy::parse("(1 | 2) 3").unwrap();
let hv = hops(&["4-1", "3-1"]);
assert!(!seq.matches(&hv));
}
#[test]
fn concatenated_alternations_match() {
let seq = HopPatternPolicy::parse("(1 | 2) (3 | 4)").unwrap();
let hv = hops(&["2-1", "4-1"]);
assert!(seq.matches(&hv));
}
#[test]
fn complex_nested_quantifiers_and_or_match() {
let seq = HopPatternPolicy::parse("1 (2+ | 3) 4").unwrap();
let hv = hops(&["1-1", "2-1", "2-1", "4-1"]);
assert!(seq.matches(&hv));
let hv = hops(&["1-1", "3-1", "4-1"]);
assert!(seq.matches(&hv));
let hv = hops(&["1-1", "2-1", "2-1", "3-1", "4-1"]);
assert!(!seq.matches(&hv));
let hv = hops(&["1-1", "4-1"]);
assert!(!seq.matches(&hv));
}
#[test]
fn optional_followed_by_plus_matches() {
let seq = HopPatternPolicy::parse("1? 2+ 3").unwrap();
let hv = hops(&["2-1", "3-1"]);
assert!(seq.matches(&hv));
}
#[test]
fn zero_or_more_then_plus_matches() {
let seq = HopPatternPolicy::parse("1* 2+").unwrap();
let hv = hops(&["1-1", "1-1", "2-1", "2-1"]);
assert!(seq.matches(&hv));
}
#[test]
fn plus_group_missing_required_no_match() {
let seq = HopPatternPolicy::parse("0+ (1 | 2)+ 3+").unwrap();
let hv = hops(&["0-1", "1-1", "2-1"]);
assert!(!seq.matches(&hv));
}
#[test]
fn optional_path_failing_later_no_match() {
let seq = HopPatternPolicy::parse("1? 2 3").unwrap();
let hv = hops(&["1-1", "2-1"]); assert!(!seq.matches(&hv));
}
#[test]
fn star_consumes_all_missing_tail_no_match() {
let seq = HopPatternPolicy::parse("1* 2 3").unwrap();
let hv = hops(&["1-1", "1-1", "2-1"]); assert!(!seq.matches(&hv));
}
#[test]
fn concatenated_alternations_wrong_second_no_match() {
let seq = HopPatternPolicy::parse("(1 | 2) (3 | 4)").unwrap();
let hv = hops(&["2-1", "5-1"]);
assert!(!seq.matches(&hv));
}
}
}
pub mod lexer {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TokenKind {
HopPredicate(String),
Bang,
And,
Or,
LParen,
RParen,
QMark,
Plus,
Star,
EOI,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Token {
pub kind: TokenKind,
pub span: (usize, usize),
}
pub type TokenSplat<'t> = (TokenKind, (usize, usize));
impl Token {
#[inline]
pub fn splat(&self) -> TokenSplat<'_> {
(self.kind.clone(), self.span)
}
#[inline]
const fn single_char(kind: TokenKind, i: usize) -> Self {
Self {
kind,
span: (i, i + 1),
}
}
}
pub struct HopPatternLexer<'a> {
input: std::iter::Peekable<std::str::CharIndices<'a>>,
len: usize,
}
impl<'a> HopPatternLexer<'a> {
const RESERVED_CHARS: &'static str = "!&|()+?*";
#[inline]
pub fn new(s: &'a str) -> Self {
Self {
input: s.char_indices().peekable(),
len: s.len(),
}
}
#[inline]
fn next_token(&mut self) -> Option<Token> {
while let Some((idx, c)) = self.input.next() {
return Some(match c {
'?' => Token::single_char(TokenKind::QMark, idx),
'+' => Token::single_char(TokenKind::Plus, idx),
'*' => Token::single_char(TokenKind::Star, idx),
'!' => Token::single_char(TokenKind::Bang, idx),
'&' => Token::single_char(TokenKind::And, idx),
'|' => Token::single_char(TokenKind::Or, idx),
'(' => Token::single_char(TokenKind::LParen, idx),
')' => Token::single_char(TokenKind::RParen, idx),
' ' | '\t' | '\n' => continue, _ => self.read_hop_predicate(c, idx),
});
}
None
}
#[inline]
fn read_hop_predicate(&mut self, first_char: char, start: usize) -> Token {
let mut ident = String::new();
ident.push(first_char);
while let Some((_, p)) = self.input.peek().copied() {
if p.is_whitespace() || Self::RESERVED_CHARS.contains(p) {
break;
}
self.input.next();
ident.push(p);
}
let end = start + ident.len();
Token {
kind: TokenKind::HopPredicate(ident),
span: (start, end),
}
}
#[inline]
pub fn tokenize(&mut self) -> Vec<Token> {
let mut out = Vec::new();
while let Some(t) = self.next_token() {
out.push(t);
}
out.push(Token {
kind: TokenKind::EOI,
span: (self.len, self.len),
});
out
}
}
#[cfg(test)]
mod tests {
use crate::path::policy::hop_pattern::lexer::{HopPatternLexer, TokenKind};
#[test]
fn lex_single_ident_succeeds() {
let mut lx = HopPatternLexer::new("1-ff00:0:133#1");
let tokens = lx.tokenize();
assert_eq!(tokens.len(), 2);
assert_eq!(
tokens[0].kind,
TokenKind::HopPredicate("1-ff00:0:133#1".into())
);
assert_eq!(tokens[1].kind, TokenKind::EOI);
}
#[test]
fn lex_symbols_succeeds() {
let mut lx = HopPatternLexer::new("! & | ( ) ? + *");
let tokens = lx.tokenize();
let kinds: Vec<_> = tokens.into_iter().map(|t| t.kind).collect();
assert_eq!(
kinds,
vec![
TokenKind::Bang,
TokenKind::And,
TokenKind::Or,
TokenKind::LParen,
TokenKind::RParen,
TokenKind::QMark,
TokenKind::Plus,
TokenKind::Star,
TokenKind::EOI,
]
);
}
#[test]
fn lex_mixed_expression_succeeds() {
let mut lx = HopPatternLexer::new("!foo & (bar | baz)");
let tokens = lx.tokenize();
let kinds: Vec<_> = tokens.into_iter().map(|t| t.kind).collect();
assert_eq!(
kinds,
vec![
TokenKind::Bang,
TokenKind::HopPredicate("foo".into()),
TokenKind::And,
TokenKind::LParen,
TokenKind::HopPredicate("bar".into()),
TokenKind::Or,
TokenKind::HopPredicate("baz".into()),
TokenKind::RParen,
TokenKind::EOI,
]
);
}
#[test]
fn lex_whitespace_handling_succeeds() {
let mut lx = HopPatternLexer::new(" foo\t\n&bar ");
let tokens = lx.tokenize();
let kinds: Vec<_> = tokens.into_iter().map(|t| t.kind).collect();
assert_eq!(
kinds,
vec![
TokenKind::HopPredicate("foo".into()),
TokenKind::And,
TokenKind::HopPredicate("bar".into()),
TokenKind::EOI,
]
);
}
}
}
pub mod parser {
use std::borrow::Cow;
use super::*;
use crate::path::policy::hop_pattern::lexer::{Token, TokenKind, TokenSplat};
const NO_BIND_POWER: u8 = 0;
const OR_BIND_POWER: u8 = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Grouping {
LeftToRight,
#[allow(dead_code)]
RightToLeft,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ParseError {
pub span: (usize, usize),
pub message: Cow<'static, str>,
}
impl ParseError {
#[inline]
pub const fn new(span: (usize, usize), message: Cow<'static, str>) -> Self {
Self { span, message }
}
#[inline]
pub fn report(&self, input: &str) -> String {
let (start, end) = self.span;
let start = start.min(input.len());
let end = end.min(input.len());
let context = 20;
let slice_start = start.saturating_sub(context);
let slice_end = (end + context).min(input.len());
let snippet = &input[slice_start..slice_end];
let marker_offset = start - slice_start;
let marker_len = (end - start).max(1);
let mut marker = String::new();
marker.push_str(&" ".repeat(marker_offset));
marker.push_str(&"^".repeat(marker_len));
format!("{snippet}\n{marker}\n{}", self.message)
}
}
pub struct HopPatternParser<'a> {
tokens: &'a [Token],
pos: usize,
}
impl<'a> HopPatternParser<'a> {
#[inline]
pub const fn new(tokens: &'a [Token]) -> Self {
Self { tokens, pos: 0 }
}
#[inline]
fn peek_kind(&self) -> Option<&TokenKind> {
self.tokens.get(self.pos).map(|t| &t.kind)
}
#[inline]
fn consume(&mut self) -> Option<TokenSplat<'_>> {
if let Some(t) = self.tokens.get(self.pos) {
self.pos += 1;
Some(t.splat())
} else {
None
}
}
fn parse_expr(
&mut self,
left_binding_power: u8,
) -> Result<HopPatternExpression, ParseError> {
let mut expr = match self.consume() {
Some((TokenKind::HopPredicate(s), span)) => {
HopPatternExpression::HopPredicate(s.parse().map_err(|e| {
ParseError::new(span, format!("invalid hop predicate '{s}': {e}").into())
})?)
}
Some((TokenKind::Bang, span)) => {
return Err(ParseError::new(
span,
"Negative lookahead '!' is not supported".into(),
));
}
Some((TokenKind::LParen, span_l)) => {
let nested_expr = self.parse_expr(NO_BIND_POWER)?;
match self.consume() {
Some((TokenKind::RParen, _)) => nested_expr,
Some((_, span)) => {
return Err(ParseError::new(span, "expected ')'".into()));
}
None => {
return Err(ParseError::new(
span_l,
"unexpected end of token stream".into(),
));
}
}
}
Some((kind, span)) => {
return Err(ParseError::new(
span,
format!("unexpected token: {kind:?}, Expected a HopPredicate, '!' or '('")
.into(),
));
}
None => {
let span = self
.tokens
.last()
.map(|t| (t.span.1, t.span.1))
.unwrap_or((0, 0));
return Err(ParseError::new(
span,
"unexpected end of token stream, Expected a HopPredicate, '!' or '('"
.into(),
));
}
};
loop {
match self.peek_kind() {
Some(TokenKind::QMark) => {
self.consume();
expr = HopPatternExpression::Optional(Box::new(expr));
continue;
}
Some(TokenKind::Plus) => {
self.consume();
expr = HopPatternExpression::OneOrMore(Box::new(expr));
continue;
}
Some(TokenKind::Star) => {
self.consume();
expr = HopPatternExpression::ZeroOrMore(Box::new(expr));
continue;
}
_ => {}
}
let (op_binding_power, op_grouping, build_infix): (
u8,
Grouping,
fn(HopPatternExpression, HopPatternExpression) -> HopPatternExpression,
) = match self.peek_kind() {
Some(TokenKind::And) => {
return Err(ParseError::new(
self.tokens[self.pos].span,
"AND operator '&' is not supported".into(),
));
}
Some(TokenKind::Or) => {
(OR_BIND_POWER, Grouping::LeftToRight, |lhse, rhse| {
HopPatternExpression::Or(Box::new(lhse), Box::new(rhse))
})
}
_ => break,
};
if left_binding_power > op_binding_power {
break;
}
self.consume();
let rhs_binding_power = match op_grouping {
Grouping::LeftToRight => op_binding_power + 1,
Grouping::RightToLeft => op_binding_power,
};
let right_expr = self.parse_expr(rhs_binding_power)?;
expr = build_infix(expr, right_expr);
}
Ok(expr)
}
#[inline]
pub fn parse(&mut self) -> Result<HopPatternPolicy, ParseError> {
let mut hop_pattern = Vec::new();
while self.peek_kind() != Some(&TokenKind::EOI) {
let expr = self.parse_expr(NO_BIND_POWER)?;
hop_pattern.push(expr);
}
if self.pos < self.tokens.len() - 1 {
let span = self.tokens[self.pos].span;
return Err(ParseError::new(span, "unexpected trailing tokens".into()));
}
Ok(HopPatternPolicy(hop_pattern))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identifier::isd::Isd;
fn tok(kind: TokenKind) -> Token {
Token { kind, span: (0, 0) }
}
fn parse_single_expression(tokens: Vec<TokenKind>) -> HopPatternExpression {
let tokens: Vec<Token> = tokens
.into_iter()
.map(tok)
.chain([tok(TokenKind::EOI)])
.collect();
let mut parser = HopPatternParser::new(&tokens);
parser.parse().unwrap().0.remove(0)
}
#[test]
fn generate_hop_patterns_from_string() {
let token = HopPatternLexer::new("1-ff00:0:133#1 2-1").tokenize();
let expr = HopPatternParser::new(&token).parse().unwrap();
let (first, second) = (&expr.0[0], &expr.0[1]);
match first {
HopPatternExpression::HopPredicate(s) if s.isd == Isd(1) => {}
other => panic!("Expected HopPredicate(Isd(1)), got: {other:?}"),
}
match second {
HopPatternExpression::HopPredicate(s) if s.isd == Isd(2) => {}
other => panic!("Expected HopPredicate(Isd(2)), got: {other:?}"),
}
}
#[test]
fn parse_single_predicate_succeeds() {
let expr = parse_single_expression(vec![TokenKind::HopPredicate("1".into())]);
match expr {
HopPatternExpression::HopPredicate(ref s) if s.isd == Isd(1) => {}
other => panic!("Expected HopPredicate(Isd(1)), got: {other:?}"),
}
}
#[test]
fn parse_parentheses_succeeds() {
let expr = parse_single_expression(vec![
TokenKind::LParen,
TokenKind::HopPredicate("1".into()),
TokenKind::Or,
TokenKind::HopPredicate("2".into()),
TokenKind::RParen,
TokenKind::Or,
TokenKind::HopPredicate("3".into()),
]);
match expr {
HopPatternExpression::Or(lhs, rhs) => {
match *lhs {
HopPatternExpression::Or(..) => {}
ref other => {
panic!("Expected Or inside parentheses on LHS, got: {other:?}")
}
}
match *rhs {
HopPatternExpression::HopPredicate(_) => {}
ref other => panic!("Expected HopPredicate on RHS, got: {other:?}"),
}
}
other => panic!("Expected And at root, got: {other:?}"),
}
}
#[test]
fn parse_postfix_optional_succeeds() {
let expr = parse_single_expression(vec![
TokenKind::HopPredicate("1".into()),
TokenKind::QMark,
]);
match expr {
HopPatternExpression::Optional(inner) => {
match *inner {
HopPatternExpression::HopPredicate(_) => {}
ref other => {
panic!("Expected HopPredicate inside Optional, got: {other:?}")
}
}
}
other => panic!("Expected Optional, got: {other:?}"),
}
}
#[test]
fn parse_postfix_plus_succeeds() {
let expr =
parse_single_expression(vec![TokenKind::HopPredicate("1".into()), TokenKind::Plus]);
match expr {
HopPatternExpression::OneOrMore(inner) => {
match *inner {
HopPatternExpression::HopPredicate(_) => {}
ref other => {
panic!("Expected HopPredicate inside OneOrMore, got: {other:?}")
}
}
}
other => panic!("Expected OneOrMore, got: {other:?}"),
}
}
#[test]
fn parse_postfix_star_succeeds() {
let expr =
parse_single_expression(vec![TokenKind::HopPredicate("1".into()), TokenKind::Star]);
match expr {
HopPatternExpression::ZeroOrMore(inner) => {
match *inner {
HopPatternExpression::HopPredicate(_) => {}
ref other => {
panic!("Expected HopPredicate inside ZeroOrMore, got: {other:?}")
}
}
}
other => panic!("Expected ZeroOrMore, got: {other:?}"),
}
}
#[test]
fn parse_chained_postfix_succeeds() {
let expr = parse_single_expression(vec![
TokenKind::HopPredicate("1".into()),
TokenKind::QMark,
TokenKind::Plus,
TokenKind::Star,
]);
match expr {
HopPatternExpression::ZeroOrMore(inner1) => {
match *inner1 {
HopPatternExpression::OneOrMore(inner2) => {
match *inner2 {
HopPatternExpression::Optional(inner3) => {
match *inner3 {
HopPatternExpression::HopPredicate(_) => {}
ref other => {
panic!(
"Expected HopPredicate inside Optional, got: {other:?}"
)
}
}
}
ref other => {
panic!("Expected Optional inside OneOrMore, got: {other:?}")
}
}
}
ref other => {
panic!("Expected OneOrMore inside ZeroOrMore, got: {other:?}")
}
}
}
other => panic!("Expected ZeroOrMore at root, got: {other:?}"),
}
}
mod error_tests {
use super::*;
#[test]
fn parse_unexpected_token_returns_error() {
let tokens = vec![tok(TokenKind::And), tok(TokenKind::EOI)];
let mut parser = HopPatternParser::new(&tokens);
let err = parser.parse().unwrap_err();
assert!(
err.message.contains("unexpected token"),
"Expected error message to contain 'unexpected token', got: {:?}",
err.message
);
}
#[test]
fn parse_unexpected_end_returns_error() {
let tokens = vec![tok(TokenKind::LParen)];
let mut parser = HopPatternParser::new(&tokens);
let err = parser.parse().unwrap_err();
assert!(
err.message.contains("unexpected end"),
"Expected error message to contain 'unexpected end', got: {:?}",
err.message
);
}
#[test]
fn parse_unexpected_trailing_tokens_returns_error() {
let tokens = vec![
tok(TokenKind::HopPredicate("1".into())),
tok(TokenKind::HopPredicate("2".into())),
tok(TokenKind::EOI),
tok(TokenKind::Bang),
];
let mut parser = HopPatternParser::new(&tokens);
let err = parser.parse().unwrap_err();
assert!(
err.message.contains("unexpected trailing"),
"Expected error message to contain 'unexpected trailing', got: {:?}",
err.message
);
}
}
}
}