use crate::capsule::common::{CapsuleTag, PolicyDecision, SpanTag};
use crate::session::SessionError;
use crate::opawasm::{DefaultContext, Policy, Runtime};
use async_trait::async_trait;
use itertools::Itertools;
use lru::LruCache;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::num::NonZeroUsize;
use wasmtime::{AsContextMut, Config, Engine, Module, Store};
const ANTIMATTER_ENTRYPOINT: &str = "antimatter/bundle/result";
const CACHE_SIZE: usize = 2000;
pub type PolicyRequest = HashMap<String, HashMap<String, String>>;
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
pub struct Span {
pub start: usize,
pub end: usize,
pub whole_tags: Vec<CapsuleTag>,
pub policy_request: PolicyRequest,
}
pub struct PolicyEngine {
policy: Policy<DefaultContext>,
store: Store<()>,
cache: LruCache<Vec<u8>, PolicyDecision>,
}
#[derive(Clone, PartialEq)]
struct TagEvent {
index: usize,
tags_to_add: Vec<CapsuleTag>,
tags_to_remove: Vec<CapsuleTag>,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
struct RawPolicyResults {
result: Vec<Vec<Vec<String>>>,
}
#[async_trait]
pub trait PolicyEngineEvaluator {
async fn evaluate(
&mut self,
spans: &[Span],
redact_tags: &[CapsuleTag],
) -> Result<Vec<PolicyDecision>, SessionError>;
}
impl PartialOrd for PolicyDecision {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.as_int().partial_cmp(&other.as_int())
}
}
impl PolicyDecision {
fn as_int(&self) -> u16 {
match self {
PolicyDecision::NoMatch => 0,
PolicyDecision::Allow => 1,
PolicyDecision::Tokenize => 2,
PolicyDecision::Redact => 3,
PolicyDecision::DenyRecord => 4,
PolicyDecision::DenyCapsule => 5,
}
}
fn from_str(s: &str) -> Result<PolicyDecision, SessionError> {
match s {
"Allow" => Ok(PolicyDecision::Allow),
"Redact" => Ok(PolicyDecision::Redact),
"Tokenize" => Ok(PolicyDecision::Tokenize),
"DenyRecord" => Ok(PolicyDecision::DenyRecord),
"DenyCapsule" => Ok(PolicyDecision::DenyCapsule),
"NoMatch" => Ok(PolicyDecision::NoMatch),
_ => Err(SessionError::OPAError(format!("unknown policy type {}", s))),
}
}
pub fn from_str_vec(input_strings: &Vec<String>) -> Result<PolicyDecision, SessionError> {
for input in input_strings {
let decision = PolicyDecision::from_str(input)?;
if decision != PolicyDecision::NoMatch {
return Ok(decision);
}
}
Ok(PolicyDecision::NoMatch)
}
}
impl Span {
fn new(start: usize) -> Self {
Span {
start,
end: 0, whole_tags: Vec::new(),
policy_request: HashMap::from([
("tags".to_string(), HashMap::new()),
("readParameters".to_string(), HashMap::new()),
("domainIdentity".to_string(), HashMap::new()),
]),
}
}
#[cfg(test)]
fn new_end(start: usize, end: usize) -> Self {
let mut span = Span::new(start);
span.end = end;
span
}
}
impl TagEvent {
fn new(index: usize) -> Self {
TagEvent {
index,
tags_to_add: Vec::new(),
tags_to_remove: Vec::new(),
}
}
}
impl PolicyEngine {
pub async fn new(policy_bundle: &Vec<u8>) -> Result<Self, SessionError> {
let mut config = Config::new();
config.async_support(true);
let engine =
Engine::new(&config).map_err(|e| SessionError::OPAError(format!("engine: {}", e)))?;
let module = Module::new(&engine, policy_bundle)
.map_err(|e| SessionError::OPAError(format!("failed to load bundle: {}", e)))?;
let mut store = Store::new(&engine, ());
let runtime = Runtime::new(&mut store, &module)
.await
.map_err(|e| SessionError::OPAError(format!("runtime: {}", e)))?;
let policy = runtime
.without_data(&mut store)
.await
.map_err(|e| SessionError::OPAError(format!("policy: {}", e)))?;
Ok(Self {
policy,
store,
cache: LruCache::new(NonZeroUsize::new(CACHE_SIZE).unwrap()),
})
}
async fn evaluate_span(
&mut self,
span: &Span,
redact_tags: &[CapsuleTag],
) -> Result<PolicyDecision, SessionError> {
let cache_key = serde_json::to_vec(&(&span.policy_request, redact_tags)).unwrap();
let lookup = self.cache.get(&cache_key);
if let Some(result) = lookup {
return Ok(*result);
}
for tag in &span.whole_tags {
if redact_tags.contains(tag) {
self.cache.put(cache_key, PolicyDecision::Redact);
return Ok(PolicyDecision::Redact);
}
}
let raw_evaluation_result: Value = self
.policy
.evaluate(
self.store.as_context_mut(),
ANTIMATTER_ENTRYPOINT,
&vec![&span.policy_request],
)
.await
.map_err(|e| {
SessionError::OPAError(format!("failed to evaluate policy with data: {}", e))
})?;
let converted: Vec<RawPolicyResults> =
serde_json::from_value(raw_evaluation_result.clone()).unwrap();
if converted.len() != 1 {
return Err(SessionError::OPAError(format!(
"malformed evaluation output, expected 1 response, got {}",
converted.len()
)));
}
let raw_result = converted.first().unwrap();
let mut output: PolicyDecision = PolicyDecision::NoMatch;
for input in raw_result.result.iter() {
for policy in input.iter() {
for rule in policy.iter() {
let parts = rule.split(';').collect::<Vec<_>>();
let decision_str = match parts.get(2) {
Some(s) => s,
None => {
return Err(SessionError::OPAError(format!(
"malformed policy decision {}",
rule,
)))
}
};
let decision = PolicyDecision::from_str(decision_str)?;
if decision != PolicyDecision::NoMatch {
if decision > output {
output = decision;
}
break;
}
}
}
}
Ok(output)
}
}
#[async_trait]
impl PolicyEngineEvaluator for PolicyEngine {
async fn evaluate(
&mut self,
spans: &[Span],
redact_tags: &[CapsuleTag],
) -> Result<Vec<PolicyDecision>, SessionError> {
let mut result: Vec<PolicyDecision> = Vec::new();
for span in spans.iter() {
result.push(self.evaluate_span(span, redact_tags).await?)
}
Ok(result)
}
}
pub fn generate_spans(
span_tags: &[SpanTag],
column_tags: &[CapsuleTag],
read_parameters: &HashMap<String, String>,
capsule_tags: &[CapsuleTag],
domain_identity: &HashMap<String, String>,
data_length: usize,
) -> Vec<Span> {
let mut tag_events: HashMap<usize, TagEvent> = HashMap::new();
for span_tag in span_tags {
tag_events
.entry(span_tag.start)
.or_insert_with(|| TagEvent::new(span_tag.start))
.tags_to_add
.push(span_tag.tag.clone());
tag_events
.entry(span_tag.end)
.or_insert_with(|| TagEvent::new(span_tag.end))
.tags_to_remove
.push(span_tag.tag.clone());
}
let mut spans: Vec<Span> = Vec::new();
let mut init_tags: Vec<CapsuleTag> = capsule_tags.to_owned();
init_tags.extend(column_tags.to_owned());
let mut current_span: Span = Span::new(0);
let mut tail_boundary = 0;
tag_events
.entry(0)
.or_insert_with(|| TagEvent::new(0))
.tags_to_add
.extend(init_tags.clone());
let mut tags: Vec<CapsuleTag> = Vec::new();
let mut tag_count: HashMap<CapsuleTag, usize> = HashMap::new(); for key in tag_events.keys().sorted() {
current_span.end = *key;
if current_span.start < current_span.end {
current_span
.policy_request
.insert("readParameters".to_string(), read_parameters.clone());
current_span
.policy_request
.insert("domainIdentity".to_string(), domain_identity.clone());
spans.push(current_span.clone());
}
let event = &tag_events[key];
for tag in &event.tags_to_add {
*tag_count.entry(tag.clone()).or_insert(0) += 1;
if tag_count[tag] == 1 {
tags.push(tag.clone());
}
}
for tag in &event.tags_to_remove {
if let Some(count) = tag_count.get_mut(tag) {
*count -= 1;
if *count == 0 {
tags.retain(|t| t != tag);
}
}
}
current_span = Span::new(*key);
for tag in tags.iter_mut() {
let current_span_tags = current_span.policy_request.get_mut("tags").unwrap();
current_span_tags.insert(tag.name.clone().to_string(), tag.value.clone().to_string());
current_span.whole_tags.push(tag.clone());
}
tail_boundary = *key;
}
if tail_boundary < data_length {
let mut tail = Span::new(tail_boundary);
tail.end = data_length;
tail.policy_request
.insert("readParameters".to_string(), read_parameters.clone());
tail.policy_request
.insert("domainIdentity".to_string(), domain_identity.clone());
for tag in tags.iter_mut() {
let tail_tags = tail.policy_request.get_mut("tags").unwrap();
tail_tags.insert(tag.name.clone().to_string(), tag.value.clone().to_string());
tail.whole_tags.push(tag.clone());
}
spans.push(tail);
}
spans
}
pub async fn enforce_policies<T>(
data: &[u8],
spans: Vec<Span>,
redact_tags: &[CapsuleTag],
engine: &mut T,
) -> Result<(Vec<u8>, Vec<(PolicyDecision, SpanTag)>, PolicyDecision), SessionError>
where
T: PolicyEngineEvaluator,
{
let mut adjusted_span_tags: Vec<(PolicyDecision, SpanTag)> = Vec::new();
let policy_decisions = engine.evaluate(&spans, redact_tags).await?;
if spans.len() != policy_decisions.len() {
return Err(SessionError::OPAError(format!(
"length mismatch between spans ({}) and decisions ({})",
spans.len(),
policy_decisions.len()
)));
}
let mut result: Vec<u8> = vec![];
let mut previously_redacted = false;
for (span, decision) in spans.iter().zip(policy_decisions.iter()) {
if *decision == PolicyDecision::DenyRecord {
return Ok((
data.to_owned(),
adjusted_span_tags,
PolicyDecision::DenyRecord,
));
}
if *decision == PolicyDecision::DenyCapsule {
return Ok((
data.to_owned(),
adjusted_span_tags,
PolicyDecision::DenyCapsule,
));
}
let span_result: Vec<u8> = match decision {
PolicyDecision::NoMatch | PolicyDecision::Tokenize | PolicyDecision::Allow => {
previously_redacted = false;
if span.start > span.end || span.end > data.len() {
return Err(SessionError::OPAError(format!(
"Span is out of bounds. Data element: [{}:{}], span: [{}:{}]",
0,
data.len(),
span.start,
span.end
)));
}
Ok(data[span.start..span.end].to_vec())
}
PolicyDecision::Redact => {
if previously_redacted {
Ok("".as_bytes().to_vec())
} else {
previously_redacted = true;
Ok("{redacted}".as_bytes().to_vec())
}
}
PolicyDecision::DenyRecord => Err(SessionError::OPAError(
"attempted to match a DenyRecord decision".to_string(),
)), PolicyDecision::DenyCapsule => Err(SessionError::OPAError(
"attempted to match a DenyCapsule decision".to_string(),
)), }?;
let before = result.len();
result = [result.as_slice(), span_result.as_slice()].concat();
for whole_tag in span.whole_tags.iter() {
adjusted_span_tags.push((
*decision,
SpanTag {
start: before,
end: result.len(),
tag: whole_tag.clone(),
},
));
}
}
Ok((result, adjusted_span_tags, PolicyDecision::NoMatch))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::capsule::common::TagType;
#[test]
fn test_policy_decision_from_str() {
assert_eq!(
PolicyDecision::from_str("Allow").unwrap(),
PolicyDecision::Allow
);
assert_eq!(
PolicyDecision::from_str("Redact").unwrap(),
PolicyDecision::Redact
);
assert_eq!(
PolicyDecision::from_str("Tokenize").unwrap(),
PolicyDecision::Tokenize
);
assert_eq!(
PolicyDecision::from_str("DenyRecord").unwrap(),
PolicyDecision::DenyRecord
);
assert_eq!(
PolicyDecision::from_str("DenyCapsule").unwrap(),
PolicyDecision::DenyCapsule
);
assert_eq!(
PolicyDecision::from_str("NoMatch").unwrap(),
PolicyDecision::NoMatch
);
assert!(matches!(
PolicyDecision::from_str("InvalidDecision"),
Err(SessionError::OPAError(_))
));
}
#[test]
fn test_policy_decision_from_str_vec() {
let no_match_decisions = vec![
"NoMatch".to_string(),
"NoMatch".to_string(),
"NoMatch".to_string(),
];
assert_eq!(
PolicyDecision::from_str_vec(&no_match_decisions).unwrap(),
PolicyDecision::NoMatch
);
let empty_decisions: Vec<String> = Vec::new();
assert_eq!(
PolicyDecision::from_str_vec(&empty_decisions).unwrap(),
PolicyDecision::NoMatch
);
let mixed_decisions = vec![
"NoMatch".to_string(),
"NoMatch".to_string(),
"Redact".to_string(),
"NoMatch".to_string(),
"Tokenize".to_string(),
];
assert_eq!(
PolicyDecision::from_str_vec(&mixed_decisions).unwrap(),
PolicyDecision::Redact
);
}
fn tag_a() -> CapsuleTag {
CapsuleTag {
name: "test1".to_string(),
tag_type: TagType::Unary,
value: "none".to_string(),
source: "src".to_string(),
hook_version: (0, 0, 0),
}
}
fn tag_b() -> CapsuleTag {
CapsuleTag {
name: "test2".to_string(),
tag_type: TagType::Unary,
value: "none".to_string(),
source: "src".to_string(),
hook_version: (0, 0, 0),
}
}
#[test]
fn test_non_overlapping_tags() {
let span_tags = vec![
SpanTag {
start: 0,
end: 11,
tag: tag_a(),
},
SpanTag {
start: 11,
end: 21,
tag: tag_b(),
},
];
let read_parameters = HashMap::new();
let domain_identity = HashMap::new();
let data_length = 21;
let spans = generate_spans(
&span_tags,
&vec![],
&read_parameters,
&vec![],
&domain_identity,
data_length,
);
assert_eq!(spans.len(), 2);
assert_eq!(
spans.get(0).unwrap().policy_request,
HashMap::from([
(
"tags".to_string(),
HashMap::from([("test1".to_string(), "none".to_string())])
),
("readParameters".to_string(), HashMap::new()),
("domainIdentity".to_string(), HashMap::new())
])
);
assert_eq!(
spans.get(1).unwrap().policy_request,
HashMap::from([
(
"tags".to_string(),
HashMap::from([("test2".to_string(), "none".to_string())])
),
("readParameters".to_string(), HashMap::new()),
("domainIdentity".to_string(), HashMap::new())
])
);
}
#[test]
fn test_overlapping_tags() {
let span_tags = vec![
SpanTag {
start: 0,
end: 15,
tag: tag_a(),
},
SpanTag {
start: 10,
end: 21,
tag: tag_b(),
},
];
let read_parameters = HashMap::new();
let domain_identity = HashMap::new();
let data_length = 21;
let spans = generate_spans(
&span_tags,
&vec![],
&read_parameters,
&vec![],
&domain_identity,
data_length,
);
assert_eq!(spans.len(), 3);
assert_eq!(
spans.get(0).unwrap().policy_request,
HashMap::from([
(
"tags".to_string(),
HashMap::from([("test1".to_string(), "none".to_string())])
),
("readParameters".to_string(), HashMap::new()),
("domainIdentity".to_string(), HashMap::new())
])
);
assert_eq!(
spans.get(1).unwrap().policy_request,
HashMap::from([
(
"tags".to_string(),
HashMap::from([
("test1".to_string(), "none".to_string()),
("test2".to_string(), "none".to_string())
])
),
("readParameters".to_string(), HashMap::new()),
("domainIdentity".to_string(), HashMap::new())
])
);
assert_eq!(
spans.get(2).unwrap().policy_request,
HashMap::from([
(
"tags".to_string(),
HashMap::from([("test2".to_string(), "none".to_string())])
),
("readParameters".to_string(), HashMap::new()),
("domainIdentity".to_string(), HashMap::new())
])
);
assert_eq!(spans.get(2).unwrap().end, 21);
}
struct MockPolicyEngine {
responses: Vec<PolicyDecision>,
}
#[async_trait::async_trait]
impl PolicyEngineEvaluator for MockPolicyEngine {
async fn evaluate(
&mut self,
pr: &[Span],
_redact_tags: &[CapsuleTag],
) -> Result<Vec<PolicyDecision>, SessionError> {
Ok(pr
.iter()
.enumerate()
.map(|(idx, &_)| self.responses.get(idx).unwrap().clone())
.collect())
}
}
#[tokio::test]
async fn test_enforce_policies_allow() {
let data = "This is some data".as_bytes().to_vec();
let spans = vec![
Span::new_end(0, 5),
Span::new_end(5, 10),
Span::new_end(10, data.len()),
];
let mut engine = MockPolicyEngine {
responses: vec![
PolicyDecision::Allow,
PolicyDecision::Allow,
PolicyDecision::Allow,
],
};
let (result, span_tags, decision) =
enforce_policies(&data, spans, &Vec::new(), &mut engine)
.await
.unwrap();
assert_eq!(result, data);
assert_eq!(span_tags, vec![]);
assert_eq!(decision, PolicyDecision::NoMatch);
}
#[tokio::test]
async fn test_enforce_policies_redact() {
let data = "This is some data".as_bytes().to_vec();
let spans = vec![
Span::new_end(0, 5),
Span::new_end(5, 7),
Span::new_end(7, data.len()),
];
let mut engine = MockPolicyEngine {
responses: vec![
PolicyDecision::Allow,
PolicyDecision::Redact,
PolicyDecision::Allow,
],
};
let (result, span_tags, decision) =
enforce_policies(&data, spans, &Vec::new(), &mut engine)
.await
.unwrap();
assert_eq!(result, "This {redacted} some data".as_bytes().to_vec());
assert_eq!(span_tags, vec![]);
assert_eq!(decision, PolicyDecision::NoMatch);
}
#[tokio::test]
async fn test_enforce_policies_deny_row() {
let data = "This is some data".as_bytes().to_vec();
let spans = vec![
Span::new_end(0, 5),
Span::new_end(5, 7),
Span::new_end(7, data.len()),
];
let mut engine = MockPolicyEngine {
responses: vec![
PolicyDecision::Allow,
PolicyDecision::Redact,
PolicyDecision::DenyRecord,
],
};
let (result, span_tags, decision) =
enforce_policies(&data, spans, &Vec::new(), &mut engine)
.await
.unwrap();
assert_eq!(result, data);
assert_eq!(span_tags, vec![]);
assert_eq!(decision, PolicyDecision::DenyRecord);
}
#[tokio::test]
async fn test_enforce_policies_deny_capsule() {
let data = "This is some data".as_bytes().to_vec();
let spans = vec![
Span::new_end(0, 5),
Span::new_end(5, 7),
Span::new_end(7, data.len()),
];
let mut engine = MockPolicyEngine {
responses: vec![
PolicyDecision::Allow,
PolicyDecision::Redact,
PolicyDecision::DenyCapsule,
],
};
let (result, span_tags, decision) =
enforce_policies(&data, spans, &Vec::new(), &mut engine)
.await
.unwrap();
assert_eq!(result, data);
assert_eq!(span_tags, vec![]);
assert_eq!(decision, PolicyDecision::DenyCapsule);
}
#[tokio::test]
async fn test_enforce_policies_out_of_bounds() {
let data = "This is some data".as_bytes().to_vec();
let spans = vec![Span::new_end(2, data.len() + 1)];
let mut engine = MockPolicyEngine {
responses: vec![PolicyDecision::Allow],
};
let outcome = enforce_policies(&data, spans, &Vec::new(), &mut engine).await;
assert_eq!(outcome.is_err(), true);
assert_eq!(
outcome.err().unwrap(),
SessionError::OPAError(
"Span is out of bounds. Data element: [0:17], span: [2:18]".to_string()
)
);
}
#[test]
fn test_overlapping_same_tags() {
let span_tags = vec![
SpanTag {
start: 1434,
end: 1436,
tag: CapsuleTag {
name: "tag.antimatter.io/pii/financial_terms".to_string(),
tag_type: TagType::Unary,
value: "".to_string(),
source: "custom".to_string(),
hook_version: (1, 0, 0),
},
},
SpanTag {
start: 1434,
end: 1442,
tag: CapsuleTag {
name: "tag.antimatter.io/pii/financial_terms".to_string(),
tag_type: TagType::Unary,
value: "".to_string(),
source: "custom".to_string(),
hook_version: (1, 0, 0),
},
},
];
let read_parameters = HashMap::new();
let domain_identity = HashMap::new();
let data_length = 1500;
let capsule_tags = vec![CapsuleTag {
name: "tag.antimatter.io/pii/cap/financial_terms".to_string(),
tag_type: TagType::Unary,
value: "".to_string(),
source: "custom".to_string(),
hook_version: (1, 0, 0),
}];
let spans = generate_spans(
&span_tags,
&vec![],
&read_parameters,
&capsule_tags,
&domain_identity,
data_length,
);
assert_eq!(spans.len(), 4);
assert_eq!(spans.get(0).unwrap().whole_tags.len(), 1);
assert_eq!(
spans.get(0).unwrap().whole_tags.get(0),
Some(&CapsuleTag {
name: "tag.antimatter.io/pii/cap/financial_terms".to_string(),
tag_type: TagType::Unary,
value: "".to_string(),
source: "custom".to_string(),
hook_version: (1, 0, 0)
})
);
assert_eq!(spans.get(1).unwrap().whole_tags.len(), 2);
assert_eq!(
spans.get(1).unwrap().whole_tags.get(0),
Some(&CapsuleTag {
name: "tag.antimatter.io/pii/cap/financial_terms".to_string(),
tag_type: TagType::Unary,
value: "".to_string(),
source: "custom".to_string(),
hook_version: (1, 0, 0)
})
);
assert_eq!(
spans.get(1).unwrap().whole_tags.get(1),
Some(&CapsuleTag {
name: "tag.antimatter.io/pii/financial_terms".to_string(),
tag_type: TagType::Unary,
value: "".to_string(),
source: "custom".to_string(),
hook_version: (1, 0, 0)
})
);
assert_eq!(spans.get(2).unwrap().whole_tags.len(), 2);
assert_eq!(
spans.get(2).unwrap().whole_tags.get(0),
Some(&CapsuleTag {
name: "tag.antimatter.io/pii/cap/financial_terms".to_string(),
tag_type: TagType::Unary,
value: "".to_string(),
source: "custom".to_string(),
hook_version: (1, 0, 0)
})
);
assert_eq!(
spans.get(2).unwrap().whole_tags.get(1),
Some(&CapsuleTag {
name: "tag.antimatter.io/pii/financial_terms".to_string(),
tag_type: TagType::Unary,
value: "".to_string(),
source: "custom".to_string(),
hook_version: (1, 0, 0)
})
);
assert_eq!(spans.get(3).unwrap().whole_tags.len(), 1);
assert_eq!(
spans.get(3).unwrap().whole_tags.get(0),
Some(&CapsuleTag {
name: "tag.antimatter.io/pii/cap/financial_terms".to_string(),
tag_type: TagType::Unary,
value: "".to_string(),
source: "custom".to_string(),
hook_version: (1, 0, 0)
})
);
}
fn create_capsule_tag(name: &str) -> CapsuleTag {
CapsuleTag {
name: name.to_string(),
tag_type: TagType::Unary,
value: "".to_string(),
source: "custom".to_string(),
hook_version: (1, 0, 0),
}
}
fn create_span_tag(start: usize, end: usize, name: &str) -> SpanTag {
SpanTag {
start,
end,
tag: create_capsule_tag(name),
}
}
#[test]
fn test_multiple_overlapping_spans() {
let span_tags = vec![
create_span_tag(100, 200, "tag1"),
create_span_tag(150, 250, "tag1"),
create_span_tag(180, 300, "tag1"),
];
let capsule_tags = vec![create_capsule_tag("capsule")];
let spans = generate_spans(
&span_tags,
&[],
&HashMap::new(),
&capsule_tags,
&HashMap::new(),
400,
);
assert_eq!(spans.len(), 7);
assert_eq!(spans[0].whole_tags.len(), 1); assert_eq!(spans[1].whole_tags.len(), 2); assert_eq!(spans[2].whole_tags.len(), 2); assert_eq!(spans[3].whole_tags.len(), 2); assert_eq!(spans[4].whole_tags.len(), 2); assert_eq!(spans[5].whole_tags.len(), 2); assert_eq!(spans[6].whole_tags.len(), 1); }
#[test]
fn test_spans_starting_and_ending_together() {
let span_tags = vec![
create_span_tag(100, 200, "tag1"),
create_span_tag(100, 200, "tag2"),
create_span_tag(200, 300, "tag3"),
];
let capsule_tags = vec![create_capsule_tag("capsule")];
let spans = generate_spans(
&span_tags,
&[],
&HashMap::new(),
&capsule_tags,
&HashMap::new(),
400,
);
assert_eq!(spans.len(), 4);
assert_eq!(spans[0].whole_tags.len(), 1); assert_eq!(spans[1].whole_tags.len(), 3); assert_eq!(spans[2].whole_tags.len(), 2); assert_eq!(spans[3].whole_tags.len(), 1); }
#[test]
fn test_span_covering_entire_range() {
let span_tags = vec![create_span_tag(0, 400, "tag1")];
let capsule_tags = vec![create_capsule_tag("capsule")];
let spans = generate_spans(
&span_tags,
&[],
&HashMap::new(),
&capsule_tags,
&HashMap::new(),
400,
);
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].whole_tags.len(), 2); }
#[test]
fn test_no_spans() {
let capsule_tags = vec![create_capsule_tag("capsule")];
let spans = generate_spans(
&[],
&[],
&HashMap::new(),
&capsule_tags,
&HashMap::new(),
400,
);
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].whole_tags.len(), 1); }
#[test]
fn test_overlapping_different_tags() {
let span_tags = vec![
create_span_tag(100, 300, "tag1"),
create_span_tag(200, 400, "tag2"),
];
let capsule_tags = vec![create_capsule_tag("capsule")];
let spans = generate_spans(
&span_tags,
&[],
&HashMap::new(),
&capsule_tags,
&HashMap::new(),
500,
);
assert_eq!(spans.len(), 5);
assert_eq!(spans[0].whole_tags.len(), 1); assert_eq!(spans[1].whole_tags.len(), 2); assert_eq!(spans[2].whole_tags.len(), 3); assert_eq!(spans[3].whole_tags.len(), 2); assert_eq!(spans[4].whole_tags.len(), 1); }
#[test]
fn test_zero_length_spans() {
let span_tags = vec![
create_span_tag(100, 100, "tag1"),
create_span_tag(200, 200, "tag2"),
];
let capsule_tags = vec![create_capsule_tag("capsule")];
let spans = generate_spans(
&span_tags,
&[],
&HashMap::new(),
&capsule_tags,
&HashMap::new(),
300,
);
assert_eq!(spans.len(), 3);
assert_eq!(spans[0].whole_tags.len(), 1); assert_eq!(spans[1].whole_tags.len(), 1); assert_eq!(spans[2].whole_tags.len(), 1); }
}