From a37e4703a43b41c145516e6e4a7c9d2977480273 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 6 Nov 2024 01:50:40 +0400 Subject: [PATCH] feat(ast): derive `PartialEq` and `Eq` for testing (#259) - only applies when `cfg_attr(test)` --- brush-parser/src/ast.rs | 41 +++++++++++++++++++++ brush-parser/src/parser.rs | 73 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/brush-parser/src/ast.rs b/brush-parser/src/ast.rs index 9b667a2b..cdc40c02 100644 --- a/brush-parser/src/ast.rs +++ b/brush-parser/src/ast.rs @@ -10,6 +10,7 @@ const DISPLAY_INDENT: &str = " "; /// Represents a complete shell program. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct Program { /// A sequence of complete shell commands. pub complete_commands: Vec, @@ -33,6 +34,7 @@ pub type CompleteCommandItem = CompoundListItem; /// Indicates whether the preceding command is executed synchronously or asynchronously. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub enum SeparatorOperator { /// The preceding command is executed asynchronously. Async, @@ -52,6 +54,7 @@ impl Display for SeparatorOperator { /// Represents a sequence of command pipelines connected by boolean operators. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct AndOrList { /// The first command pipeline. pub first: Pipeline, @@ -74,6 +77,7 @@ impl Display for AndOrList { /// succeeding pipeline. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub enum AndOr { /// Boolean AND operator; the embedded pipeline is only to be executed if the /// preceding command has succeeded. @@ -96,6 +100,7 @@ impl Display for AndOr { /// to the command that follows it. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct Pipeline { /// Indicates whether the result of the overall pipeline should be the logical /// negation of the result of the pipeline. @@ -123,6 +128,7 @@ impl Display for Pipeline { /// Represents a shell command. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub enum Command { /// A simple command, directly invoking an external command, a built-in command, /// a shell function, or similar. @@ -157,6 +163,7 @@ impl Display for Command { /// Represents a compound command, potentially made up of multiple nested commands. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub enum CompoundCommand { /// An arithmetic command, evaluating an arithmetic expression. Arithmetic(ArithmeticCommand), @@ -208,6 +215,7 @@ impl Display for CompoundCommand { /// An arithmetic command, evaluating an arithmetic expression. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct ArithmeticCommand { /// The raw, unparsed and unexpanded arithmetic expression. pub expr: UnexpandedArithmeticExpr, @@ -222,6 +230,7 @@ impl Display for ArithmeticCommand { /// A subshell, which executes commands in a subshell. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct SubshellCommand(pub CompoundList); impl Display for SubshellCommand { @@ -235,6 +244,7 @@ impl Display for SubshellCommand { /// A for clause, which loops over a set of values. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct ForClauseCommand { /// The name of the iterator variable. pub variable_name: String, @@ -267,6 +277,7 @@ impl Display for ForClauseCommand { /// An arithmetic for clause, which loops until an arithmetic condition is reached. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct ArithmeticForClauseCommand { /// Optionally, the initializer expression evaluated before the first iteration of the loop. pub initializer: Option, @@ -308,6 +319,7 @@ impl Display for ArithmeticForClauseCommand { /// pattern-based filters. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct CaseClauseCommand { /// The value being matched on. pub value: Word, @@ -329,6 +341,7 @@ impl Display for CaseClauseCommand { /// A sequence of commands. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct CompoundList(pub Vec); impl Display for CompoundList { @@ -356,6 +369,7 @@ impl Display for CompoundList { /// An element of a compound command list. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct CompoundListItem(pub AndOrList, pub SeparatorOperator); impl Display for CompoundListItem { @@ -369,6 +383,7 @@ impl Display for CompoundListItem { /// An if clause, which conditionally executes a command. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct IfClauseCommand { /// The command whose execution result is inspected. pub condition: CompoundList, @@ -402,6 +417,7 @@ impl Display for IfClauseCommand { /// Represents the `else` clause of a conditional command. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct ElseClause { /// If present, the condition that must be met for this `else` clause to be executed. pub condition: Option, @@ -429,6 +445,7 @@ impl Display for ElseClause { /// An individual matching case item in a case clause. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct CaseItem { /// The patterns that select this case branch. pub patterns: Vec, @@ -460,6 +477,7 @@ impl Display for CaseItem { /// Describes the action to take after executing the body command of a case clause. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub enum CaseItemPostAction { /// The containing case should be exited. ExitCase, @@ -484,6 +502,7 @@ impl Display for CaseItemPostAction { /// A while or until clause, whose looping is controlled by a condition. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct WhileOrUntilClauseCommand(pub CompoundList, pub DoGroupCommand); impl Display for WhileOrUntilClauseCommand { @@ -495,6 +514,7 @@ impl Display for WhileOrUntilClauseCommand { /// Encapsulates the definition of a shell function. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct FunctionDefinition { /// The name of the function. pub fname: String, @@ -515,6 +535,7 @@ impl Display for FunctionDefinition { /// Encapsulates the body of a function definition. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct FunctionBody(pub CompoundCommand, pub Option); impl Display for FunctionBody { @@ -531,6 +552,7 @@ impl Display for FunctionBody { /// A brace group, which groups commands together. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct BraceGroupCommand(pub CompoundList); impl Display for BraceGroupCommand { @@ -547,6 +569,7 @@ impl Display for BraceGroupCommand { /// A do group, which groups commands together. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct DoGroupCommand(pub CompoundList); impl Display for DoGroupCommand { @@ -561,6 +584,7 @@ impl Display for DoGroupCommand { /// Represents the invocation of a simple command. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct SimpleCommand { /// Optionally, a prefix to the command. pub prefix: Option, @@ -607,6 +631,7 @@ impl Display for SimpleCommand { /// Represents a prefix to a simple command. #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct CommandPrefix(pub Vec); impl Display for CommandPrefix { @@ -625,6 +650,7 @@ impl Display for CommandPrefix { /// Represents a suffix to a simple command; a word argument, declaration, or I/O redirection. #[derive(Clone, Default, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct CommandSuffix(pub Vec); impl Display for CommandSuffix { @@ -643,6 +669,7 @@ impl Display for CommandSuffix { /// Represents the I/O direction of a process substitution. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub enum ProcessSubstitutionKind { /// The process is read from. Read, @@ -662,6 +689,7 @@ impl Display for ProcessSubstitutionKind { /// A prefix or suffix for a simple command. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub enum CommandPrefixOrSuffixItem { /// An I/O redirection. IoRedirect(IoRedirect), @@ -689,6 +717,7 @@ impl Display for CommandPrefixOrSuffixItem { /// Encapsulates an assignment declaration. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct Assignment { /// Name being assigned to. pub name: AssignmentName, @@ -711,6 +740,7 @@ impl Display for Assignment { /// The target of an assignment. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub enum AssignmentName { /// A named variable. VariableName(String), @@ -732,6 +762,7 @@ impl Display for AssignmentName { /// A value being assigned to a variable. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub enum AssignmentValue { /// A scalar (word) value. Scalar(Word), @@ -763,6 +794,7 @@ impl Display for AssignmentValue { /// A list of I/O redirections to be applied to a command. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct RedirectList(pub Vec); impl Display for RedirectList { @@ -777,6 +809,7 @@ impl Display for RedirectList { /// An I/O redirection. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub enum IoRedirect { /// Redirection to a file. File(Option, IoFileRedirectKind, IoFileRedirectTarget), @@ -844,6 +877,7 @@ impl Display for IoRedirect { /// Kind of file I/O redirection. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub enum IoFileRedirectKind { /// Read (`<`). Read, @@ -878,6 +912,7 @@ impl Display for IoFileRedirectKind { /// Target for an I/O file redirection. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub enum IoFileRedirectTarget { /// Path to a file. Filename(Word), @@ -903,6 +938,7 @@ impl Display for IoFileRedirectTarget { /// Represents an I/O here document. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct IoHereDocument { /// Whether to remove leading tabs from the here document. pub remove_tabs: bool, @@ -953,6 +989,7 @@ impl Display for TestExpr { /// An extended test expression. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub enum ExtendedTestExpr { /// Logical AND operation on two nested expressions. And(Box, Box), @@ -996,6 +1033,7 @@ impl Display for ExtendedTestExpr { /// A unary predicate usable in an extended test expression. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub enum UnaryPredicate { /// Computes if the operand is a path to an existing file. FileExists, @@ -1084,6 +1122,7 @@ impl Display for UnaryPredicate { /// A binary predicate usable in an extended test expression. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub enum BinaryPredicate { /// Computes if two files refer to the same device and inode numbers. FilesReferToSameDeviceAndInodeNumbers, @@ -1142,6 +1181,7 @@ impl Display for BinaryPredicate { /// Represents a shell word. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct Word { /// Raw text of the word. pub value: String, @@ -1189,6 +1229,7 @@ impl Word { /// Encapsulates an unparsed arithmetic expression. #[derive(Clone, Debug)] #[cfg_attr(feature = "fuzz-testing", derive(arbitrary::Arbitrary))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct UnexpandedArithmeticExpr { /// The raw text of the expression. pub value: String, diff --git a/brush-parser/src/parser.rs b/brush-parser/src/parser.rs index 7529905d..74f79023 100644 --- a/brush-parser/src/parser.rs +++ b/brush-parser/src/parser.rs @@ -1008,4 +1008,77 @@ esac\ } Ok(()) } + + #[test] + fn test_parse_program() -> Result<()> { + let input = r#" + +#!/usr/bin/env bash + +for f in A B C; do + + # sdfsdf + echo "${f@L}" >&2 + + done + +"#; + use ast::*; + let expected = Program { + complete_commands: vec![CompoundList(vec![CompoundListItem( + AndOrList { + first: Pipeline { + bang: false, + seq: vec![Command::Compound( + CompoundCommand::ForClause(ForClauseCommand { + variable_name: "f".into(), + values: Some(vec![Word::new("A"), Word::new("B"), Word::new("C")]), + body: DoGroupCommand(CompoundList(vec![CompoundListItem( + AndOrList { + first: Pipeline { + bang: false, + seq: vec![Command::Simple(SimpleCommand { + prefix: None, + word_or_name: Some(Word::new("echo")), + suffix: Some(CommandSuffix(vec![ + CommandPrefixOrSuffixItem::Word(Word::new( + r#""${f@L}""#, + )), + CommandPrefixOrSuffixItem::IoRedirect( + IoRedirect::File( + None, + IoFileRedirectKind::DuplicateOutput, + IoFileRedirectTarget::Fd(2), + ), + ), + ])), + })], + }, + additional: vec![], + }, + SeparatorOperator::Sequence, + )])), + }), + None, + )], + }, + additional: vec![], + }, + SeparatorOperator::Sequence, + )])], + }; + + let tokens = tokenize_str(input)?; + let result = super::token_parser::program( + &Tokens { + tokens: tokens.as_slice(), + }, + &ParserOptions::default(), + &SourceInfo::default(), + )?; + + assert_eq!(result, expected); + + Ok(()) + } }