2019-01-07 11:33:07 +01:00
|
|
|
// Copyright 2015-2019 Parity Technologies (UK) Ltd.
|
|
|
|
// This file is part of Parity Ethereum.
|
2016-04-30 17:41:24 +02:00
|
|
|
|
2019-01-07 11:33:07 +01:00
|
|
|
// Parity Ethereum is free software: you can redistribute it and/or modify
|
2016-04-30 17:41:24 +02:00
|
|
|
// it under the terms of the GNU General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
|
2019-01-07 11:33:07 +01:00
|
|
|
// Parity Ethereum is distributed in the hope that it will be useful,
|
2016-04-30 17:41:24 +02:00
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU General Public License for more details.
|
|
|
|
|
|
|
|
// You should have received a copy of the GNU General Public License
|
2019-01-07 11:33:07 +01:00
|
|
|
// along with Parity Ethereum. If not, see <http://www.gnu.org/licenses/>.
|
2016-04-30 17:41:24 +02:00
|
|
|
|
2016-05-16 18:33:32 +02:00
|
|
|
//! Trace filters type definitions
|
|
|
|
|
2016-04-30 17:41:24 +02:00
|
|
|
use std::ops::Range;
|
2018-01-14 22:43:28 +01:00
|
|
|
use ethereum_types::{Address, Bloom, BloomInput};
|
2016-05-16 18:33:32 +02:00
|
|
|
use trace::flat::FlatTrace;
|
2017-07-12 13:09:17 +02:00
|
|
|
use super::trace::{Action, Res};
|
2016-04-30 17:41:24 +02:00
|
|
|
|
|
|
|
/// Addresses filter.
|
|
|
|
///
|
|
|
|
/// Used to create bloom possibilities and match filters.
|
2016-12-21 15:09:35 +01:00
|
|
|
#[derive(Debug)]
|
2016-05-16 18:33:32 +02:00
|
|
|
pub struct AddressesFilter {
|
|
|
|
list: Vec<Address>
|
|
|
|
}
|
2016-04-30 17:41:24 +02:00
|
|
|
|
|
|
|
impl From<Vec<Address>> for AddressesFilter {
|
|
|
|
fn from(addresses: Vec<Address>) -> Self {
|
2016-05-16 18:33:32 +02:00
|
|
|
AddressesFilter { list: addresses }
|
2016-04-30 17:41:24 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl AddressesFilter {
|
|
|
|
/// Returns true if address matches one of the searched addresses.
|
|
|
|
pub fn matches(&self, address: &Address) -> bool {
|
2016-05-16 18:33:32 +02:00
|
|
|
self.matches_all() || self.list.contains(address)
|
2016-04-30 17:41:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns true if this address filter matches everything.
|
|
|
|
pub fn matches_all(&self) -> bool {
|
2016-05-16 18:33:32 +02:00
|
|
|
self.list.is_empty()
|
2016-04-30 17:41:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns blooms of this addresses filter.
|
2018-01-14 22:43:28 +01:00
|
|
|
pub fn blooms(&self) -> Vec<Bloom> {
|
2016-05-16 18:33:32 +02:00
|
|
|
match self.list.is_empty() {
|
2018-01-14 22:43:28 +01:00
|
|
|
true => vec![Bloom::default()],
|
2016-05-16 18:33:32 +02:00
|
|
|
false => self.list.iter()
|
2018-01-14 22:43:28 +01:00
|
|
|
.map(|address| Bloom::from(BloomInput::Raw(address)))
|
2016-07-23 18:50:20 +02:00
|
|
|
.collect(),
|
2016-04-30 17:41:24 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns vector of blooms zipped with blooms of this addresses filter.
|
2018-01-14 22:43:28 +01:00
|
|
|
pub fn with_blooms(&self, blooms: Vec<Bloom>) -> Vec<Bloom> {
|
2016-05-16 18:33:32 +02:00
|
|
|
match self.list.is_empty() {
|
2016-04-30 17:41:24 +02:00
|
|
|
true => blooms,
|
|
|
|
false => blooms
|
|
|
|
.into_iter()
|
2016-05-16 18:33:32 +02:00
|
|
|
.flat_map(|bloom| self.list.iter()
|
2018-01-14 22:43:28 +01:00
|
|
|
.map(|address| {
|
2018-02-19 14:08:33 +01:00
|
|
|
let mut bloom = bloom.clone();
|
2018-01-14 22:43:28 +01:00
|
|
|
bloom.accrue(BloomInput::Raw(address));
|
|
|
|
bloom
|
|
|
|
})
|
2016-04-30 17:41:24 +02:00
|
|
|
.collect::<Vec<_>>())
|
2016-07-23 18:50:20 +02:00
|
|
|
.collect(),
|
2016-04-30 17:41:24 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-21 15:09:35 +01:00
|
|
|
#[derive(Debug)]
|
2016-04-30 17:41:24 +02:00
|
|
|
/// Traces filter.
|
|
|
|
pub struct Filter {
|
|
|
|
/// Block range.
|
|
|
|
pub range: Range<usize>,
|
|
|
|
|
|
|
|
/// From address filter.
|
|
|
|
pub from_address: AddressesFilter,
|
|
|
|
|
|
|
|
/// To address filter.
|
|
|
|
pub to_address: AddressesFilter,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Filter {
|
|
|
|
/// Returns combinations of each address.
|
2018-06-20 15:13:07 +02:00
|
|
|
pub fn bloom_possibilities(&self) -> Vec<Bloom> {
|
2016-04-30 17:41:24 +02:00
|
|
|
self.to_address.with_blooms(self.from_address.blooms())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns true if given trace matches the filter.
|
|
|
|
pub fn matches(&self, trace: &FlatTrace) -> bool {
|
2016-10-20 23:35:56 +02:00
|
|
|
match trace.action {
|
2016-04-30 17:41:24 +02:00
|
|
|
Action::Call(ref call) => {
|
|
|
|
let from_matches = self.from_address.matches(&call.from);
|
|
|
|
let to_matches = self.to_address.matches(&call.to);
|
|
|
|
from_matches && to_matches
|
2017-08-29 15:12:58 +02:00
|
|
|
},
|
2016-04-30 17:41:24 +02:00
|
|
|
Action::Create(ref create) => {
|
|
|
|
let from_matches = self.from_address.matches(&create.from);
|
2016-10-20 23:35:56 +02:00
|
|
|
|
|
|
|
let to_matches = match trace.result {
|
|
|
|
Res::Create(ref create_result) => self.to_address.matches(&create_result.address),
|
2019-01-11 18:08:58 +01:00
|
|
|
_ => self.to_address.matches_all(),
|
2016-10-20 23:35:56 +02:00
|
|
|
};
|
|
|
|
|
2016-04-30 17:41:24 +02:00
|
|
|
from_matches && to_matches
|
2016-07-24 00:20:21 +02:00
|
|
|
},
|
2016-07-22 14:47:23 +02:00
|
|
|
Action::Suicide(ref suicide) => {
|
|
|
|
let from_matches = self.from_address.matches(&suicide.address);
|
|
|
|
let to_matches = self.to_address.matches(&suicide.refund_address);
|
|
|
|
from_matches && to_matches
|
2017-07-18 12:14:06 +02:00
|
|
|
},
|
|
|
|
Action::Reward(ref reward) => {
|
2018-03-14 20:27:57 +01:00
|
|
|
self.from_address.matches_all() && self.to_address.matches(&reward.author)
|
2017-08-29 15:12:58 +02:00
|
|
|
},
|
2016-04-30 17:41:24 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2018-01-14 22:43:28 +01:00
|
|
|
use ethereum_types::{Address, Bloom, BloomInput};
|
2017-07-27 17:54:17 +02:00
|
|
|
use trace::trace::{Action, Call, Res, Create, CreateResult, Suicide, Reward};
|
2016-04-30 17:41:24 +02:00
|
|
|
use trace::flat::FlatTrace;
|
2017-07-27 19:15:25 +02:00
|
|
|
use trace::{Filter, AddressesFilter, TraceError, RewardType};
|
2017-07-12 13:09:17 +02:00
|
|
|
use evm::CallType;
|
2016-04-30 17:41:24 +02:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn empty_trace_filter_bloom_possibilities() {
|
|
|
|
let filter = Filter {
|
|
|
|
range: (0..0),
|
|
|
|
from_address: AddressesFilter::from(vec![]),
|
|
|
|
to_address: AddressesFilter::from(vec![]),
|
|
|
|
};
|
|
|
|
|
|
|
|
let blooms = filter.bloom_possibilities();
|
2018-01-14 22:43:28 +01:00
|
|
|
assert_eq!(blooms, vec![Bloom::default()]);
|
2016-04-30 17:41:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn single_trace_filter_bloom_possibility() {
|
|
|
|
let filter = Filter {
|
|
|
|
range: (0..0),
|
|
|
|
from_address: AddressesFilter::from(vec![Address::from(1)]),
|
|
|
|
to_address: AddressesFilter::from(vec![Address::from(2)]),
|
|
|
|
};
|
|
|
|
|
|
|
|
let blooms = filter.bloom_possibilities();
|
|
|
|
assert_eq!(blooms.len(), 1);
|
|
|
|
|
2018-01-14 22:43:28 +01:00
|
|
|
assert!(blooms[0].contains_input(BloomInput::Raw(&Address::from(1))));
|
|
|
|
assert!(blooms[0].contains_input(BloomInput::Raw(&Address::from(2))));
|
|
|
|
assert!(!blooms[0].contains_input(BloomInput::Raw(&Address::from(3))));
|
2016-04-30 17:41:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn only_from_trace_filter_bloom_possibility() {
|
|
|
|
let filter = Filter {
|
|
|
|
range: (0..0),
|
|
|
|
from_address: AddressesFilter::from(vec![Address::from(1)]),
|
|
|
|
to_address: AddressesFilter::from(vec![]),
|
|
|
|
};
|
|
|
|
|
|
|
|
let blooms = filter.bloom_possibilities();
|
|
|
|
assert_eq!(blooms.len(), 1);
|
|
|
|
|
2018-01-14 22:43:28 +01:00
|
|
|
assert!(blooms[0].contains_input(BloomInput::Raw(&Address::from(1))));
|
|
|
|
assert!(!blooms[0].contains_input(BloomInput::Raw(&Address::from(2))));
|
2016-04-30 17:41:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn only_to_trace_filter_bloom_possibility() {
|
|
|
|
let filter = Filter {
|
|
|
|
range: (0..0),
|
|
|
|
from_address: AddressesFilter::from(vec![]),
|
|
|
|
to_address: AddressesFilter::from(vec![Address::from(1)]),
|
|
|
|
};
|
|
|
|
|
|
|
|
let blooms = filter.bloom_possibilities();
|
|
|
|
assert_eq!(blooms.len(), 1);
|
|
|
|
|
2018-01-14 22:43:28 +01:00
|
|
|
assert!(blooms[0].contains_input(BloomInput::Raw(&Address::from(1))));
|
|
|
|
assert!(!blooms[0].contains_input(BloomInput::Raw(&Address::from(2))));
|
2016-04-30 17:41:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn multiple_trace_filter_bloom_possibility() {
|
|
|
|
let filter = Filter {
|
|
|
|
range: (0..0),
|
|
|
|
from_address: AddressesFilter::from(vec![Address::from(1), Address::from(3)]),
|
|
|
|
to_address: AddressesFilter::from(vec![Address::from(2), Address::from(4)]),
|
|
|
|
};
|
|
|
|
|
|
|
|
let blooms = filter.bloom_possibilities();
|
|
|
|
assert_eq!(blooms.len(), 4);
|
|
|
|
|
2018-01-14 22:43:28 +01:00
|
|
|
assert!(blooms[0].contains_input(BloomInput::Raw(&Address::from(1))));
|
|
|
|
assert!(blooms[0].contains_input(BloomInput::Raw(&Address::from(2))));
|
|
|
|
assert!(!blooms[0].contains_input(BloomInput::Raw(&Address::from(3))));
|
|
|
|
assert!(!blooms[0].contains_input(BloomInput::Raw(&Address::from(4))));
|
|
|
|
|
|
|
|
assert!(blooms[1].contains_input(BloomInput::Raw(&Address::from(1))));
|
|
|
|
assert!(blooms[1].contains_input(BloomInput::Raw(&Address::from(4))));
|
|
|
|
assert!(!blooms[1].contains_input(BloomInput::Raw(&Address::from(2))));
|
|
|
|
assert!(!blooms[1].contains_input(BloomInput::Raw(&Address::from(3))));
|
|
|
|
|
|
|
|
assert!(blooms[2].contains_input(BloomInput::Raw(&Address::from(2))));
|
|
|
|
assert!(blooms[2].contains_input(BloomInput::Raw(&Address::from(3))));
|
|
|
|
assert!(!blooms[2].contains_input(BloomInput::Raw(&Address::from(1))));
|
|
|
|
assert!(!blooms[2].contains_input(BloomInput::Raw(&Address::from(4))));
|
|
|
|
|
|
|
|
assert!(blooms[3].contains_input(BloomInput::Raw(&Address::from(3))));
|
|
|
|
assert!(blooms[3].contains_input(BloomInput::Raw(&Address::from(4))));
|
|
|
|
assert!(!blooms[3].contains_input(BloomInput::Raw(&Address::from(1))));
|
|
|
|
assert!(!blooms[3].contains_input(BloomInput::Raw(&Address::from(2))));
|
2016-04-30 17:41:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn filter_matches() {
|
|
|
|
let f0 = Filter {
|
|
|
|
range: (0..0),
|
|
|
|
from_address: AddressesFilter::from(vec![Address::from(1)]),
|
|
|
|
to_address: AddressesFilter::from(vec![]),
|
|
|
|
};
|
|
|
|
|
|
|
|
let f1 = Filter {
|
|
|
|
range: (0..0),
|
|
|
|
from_address: AddressesFilter::from(vec![Address::from(3), Address::from(1)]),
|
|
|
|
to_address: AddressesFilter::from(vec![]),
|
|
|
|
};
|
|
|
|
|
|
|
|
let f2 = Filter {
|
|
|
|
range: (0..0),
|
|
|
|
from_address: AddressesFilter::from(vec![]),
|
|
|
|
to_address: AddressesFilter::from(vec![]),
|
|
|
|
};
|
|
|
|
|
|
|
|
let f3 = Filter {
|
|
|
|
range: (0..0),
|
|
|
|
from_address: AddressesFilter::from(vec![]),
|
|
|
|
to_address: AddressesFilter::from(vec![Address::from(2)]),
|
|
|
|
};
|
|
|
|
|
|
|
|
let f4 = Filter {
|
|
|
|
range: (0..0),
|
|
|
|
from_address: AddressesFilter::from(vec![]),
|
|
|
|
to_address: AddressesFilter::from(vec![Address::from(2), Address::from(3)]),
|
|
|
|
};
|
|
|
|
|
|
|
|
let f5 = Filter {
|
|
|
|
range: (0..0),
|
|
|
|
from_address: AddressesFilter::from(vec![Address::from(1)]),
|
|
|
|
to_address: AddressesFilter::from(vec![Address::from(2), Address::from(3)]),
|
|
|
|
};
|
|
|
|
|
|
|
|
let f6 = Filter {
|
|
|
|
range: (0..0),
|
|
|
|
from_address: AddressesFilter::from(vec![Address::from(1)]),
|
|
|
|
to_address: AddressesFilter::from(vec![Address::from(4)]),
|
|
|
|
};
|
|
|
|
|
|
|
|
let trace = FlatTrace {
|
|
|
|
action: Action::Call(Call {
|
2016-07-22 14:47:23 +02:00
|
|
|
from: 1.into(),
|
|
|
|
to: 2.into(),
|
|
|
|
value: 3.into(),
|
|
|
|
gas: 4.into(),
|
2016-04-30 17:41:24 +02:00
|
|
|
input: vec![0x5],
|
2016-07-27 17:41:21 +02:00
|
|
|
call_type: CallType::Call,
|
2016-04-30 17:41:24 +02:00
|
|
|
}),
|
2016-09-05 11:56:44 +02:00
|
|
|
result: Res::FailedCall(TraceError::OutOfGas),
|
2016-07-28 20:31:29 +02:00
|
|
|
trace_address: vec![0].into_iter().collect(),
|
2016-04-30 17:41:24 +02:00
|
|
|
subtraces: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
assert!(f0.matches(&trace));
|
|
|
|
assert!(f1.matches(&trace));
|
|
|
|
assert!(f2.matches(&trace));
|
|
|
|
assert!(f3.matches(&trace));
|
|
|
|
assert!(f4.matches(&trace));
|
|
|
|
assert!(f5.matches(&trace));
|
|
|
|
assert!(!f6.matches(&trace));
|
2016-07-22 14:47:23 +02:00
|
|
|
|
2016-07-23 18:50:20 +02:00
|
|
|
let trace = FlatTrace {
|
|
|
|
action: Action::Create(Create {
|
|
|
|
from: 1.into(),
|
|
|
|
value: 3.into(),
|
|
|
|
gas: 4.into(),
|
|
|
|
init: vec![0x5],
|
|
|
|
}),
|
|
|
|
result: Res::Create(CreateResult {
|
|
|
|
gas_used: 10.into(),
|
|
|
|
code: vec![],
|
|
|
|
address: 2.into(),
|
|
|
|
}),
|
2016-07-28 20:31:29 +02:00
|
|
|
trace_address: vec![0].into_iter().collect(),
|
2016-07-23 18:50:20 +02:00
|
|
|
subtraces: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
assert!(f0.matches(&trace));
|
|
|
|
assert!(f1.matches(&trace));
|
|
|
|
assert!(f2.matches(&trace));
|
|
|
|
assert!(f3.matches(&trace));
|
|
|
|
assert!(f4.matches(&trace));
|
|
|
|
assert!(f5.matches(&trace));
|
|
|
|
assert!(!f6.matches(&trace));
|
|
|
|
|
2016-07-22 14:47:23 +02:00
|
|
|
let trace = FlatTrace {
|
|
|
|
action: Action::Suicide(Suicide {
|
|
|
|
address: 1.into(),
|
|
|
|
refund_address: 2.into(),
|
|
|
|
balance: 3.into(),
|
|
|
|
}),
|
|
|
|
result: Res::None,
|
2016-07-28 20:31:29 +02:00
|
|
|
trace_address: vec![].into_iter().collect(),
|
2016-07-22 14:47:23 +02:00
|
|
|
subtraces: 0
|
|
|
|
};
|
|
|
|
|
|
|
|
assert!(f0.matches(&trace));
|
|
|
|
assert!(f1.matches(&trace));
|
|
|
|
assert!(f2.matches(&trace));
|
|
|
|
assert!(f3.matches(&trace));
|
|
|
|
assert!(f4.matches(&trace));
|
|
|
|
assert!(f5.matches(&trace));
|
|
|
|
assert!(!f6.matches(&trace));
|
2017-07-27 17:54:17 +02:00
|
|
|
|
|
|
|
let trace = FlatTrace {
|
|
|
|
action: Action::Reward(Reward {
|
2017-07-31 12:06:38 +02:00
|
|
|
author: 2.into(),
|
2017-07-27 17:54:17 +02:00
|
|
|
value: 100.into(),
|
|
|
|
reward_type: RewardType::Block,
|
|
|
|
}),
|
|
|
|
result: Res::None,
|
|
|
|
trace_address: vec![].into_iter().collect(),
|
|
|
|
subtraces: 0
|
|
|
|
};
|
|
|
|
|
2018-03-14 20:27:57 +01:00
|
|
|
assert!(!f0.matches(&trace));
|
|
|
|
assert!(!f1.matches(&trace));
|
2017-07-27 17:54:17 +02:00
|
|
|
assert!(f2.matches(&trace));
|
|
|
|
assert!(f3.matches(&trace));
|
|
|
|
assert!(f4.matches(&trace));
|
2018-03-14 20:27:57 +01:00
|
|
|
assert!(!f5.matches(&trace));
|
2017-07-27 17:54:17 +02:00
|
|
|
assert!(!f6.matches(&trace));
|
2016-04-30 17:41:24 +02:00
|
|
|
}
|
2018-03-14 20:27:57 +01:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn filter_match_block_reward_fix_8070() {
|
|
|
|
let f0 = Filter {
|
|
|
|
range: (0..0),
|
|
|
|
from_address: vec![1.into()].into(),
|
|
|
|
to_address: vec![].into(),
|
|
|
|
};
|
|
|
|
|
|
|
|
let f1 = Filter {
|
|
|
|
range: (0..0),
|
|
|
|
from_address: vec![].into(),
|
|
|
|
to_address: vec![].into(),
|
|
|
|
};
|
|
|
|
|
|
|
|
let f2 = Filter {
|
|
|
|
range: (0..0),
|
|
|
|
from_address: vec![].into(),
|
|
|
|
to_address: vec![2.into()].into(),
|
|
|
|
};
|
|
|
|
|
|
|
|
let trace = FlatTrace {
|
|
|
|
action: Action::Reward(Reward {
|
|
|
|
author: 2.into(),
|
|
|
|
value: 10.into(),
|
|
|
|
reward_type: RewardType::Block,
|
|
|
|
}),
|
|
|
|
result: Res::None,
|
|
|
|
trace_address: vec![0].into_iter().collect(),
|
|
|
|
subtraces: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
assert!(!f0.matches(&trace));
|
|
|
|
assert!(f1.matches(&trace));
|
|
|
|
assert!(f2.matches(&trace));
|
|
|
|
}
|
2019-01-11 18:08:58 +01:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn filter_match_failed_contract_creation_fix_9822() {
|
|
|
|
|
|
|
|
let f0 = Filter {
|
|
|
|
range: (0..0),
|
|
|
|
from_address: vec![1.into()].into(),
|
|
|
|
to_address: vec![].into(),
|
|
|
|
};
|
|
|
|
|
|
|
|
let f1 = Filter {
|
|
|
|
range: (0..0),
|
|
|
|
from_address: vec![].into(),
|
|
|
|
to_address: vec![].into(),
|
|
|
|
};
|
|
|
|
|
|
|
|
let f2 = Filter {
|
|
|
|
range: (0..0),
|
|
|
|
from_address: vec![].into(),
|
|
|
|
to_address: vec![2.into()].into(),
|
|
|
|
};
|
|
|
|
|
|
|
|
let trace = FlatTrace {
|
|
|
|
action: Action::Create(Create {
|
|
|
|
from: 1.into(),
|
|
|
|
gas: 4.into(),
|
|
|
|
init: vec![0x5],
|
|
|
|
value: 3.into(),
|
|
|
|
}),
|
|
|
|
result: Res::FailedCall(TraceError::BadInstruction),
|
|
|
|
trace_address: vec![].into_iter().collect(),
|
|
|
|
subtraces: 0
|
|
|
|
};
|
|
|
|
|
|
|
|
assert!(f0.matches(&trace));
|
|
|
|
assert!(f1.matches(&trace));
|
|
|
|
assert!(!f2.matches(&trace));
|
|
|
|
}
|
|
|
|
|
2016-04-30 17:41:24 +02:00
|
|
|
}
|
2019-01-11 18:08:58 +01:00
|
|
|
|