use std::{
collections::{BTreeMap, BTreeSet, HashSet},
fmt,
};
use base64::{
DecodeError as Base64DecodeError, Engine as _, engine::general_purpose::STANDARD as BASE64,
};
use serde::{Deserialize, Serialize};
use solana_address::Address;
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub enum BacktestRequest {
CreateBacktestSession(CreateBacktestSessionRequest),
Continue(ContinueParams),
ContinueTo(ContinueToParams),
ContinueSessionV1(ContinueSessionRequestV1),
ContinueToSessionV1(ContinueToSessionRequestV1),
CloseBacktestSession,
CloseSessionV1(CloseSessionRequestV1),
AttachBacktestSession {
session_id: String,
last_sequence: Option<u64>,
},
ResumeAttachedSession,
AttachParallelControlSessionV2 {
control_session_id: String,
#[serde(default)]
last_sequences: BTreeMap<String, u64>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(untagged)]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub enum CreateBacktestSessionRequest {
V1(CreateBacktestSessionRequestV1),
V0(CreateSessionParams),
}
impl CreateBacktestSessionRequest {
pub fn into_request_options(self) -> CreateBacktestSessionRequestOptions {
match self {
Self::V0(request) => CreateBacktestSessionRequestOptions {
request,
parallel: false,
},
Self::V1(CreateBacktestSessionRequestV1 { request, parallel }) => {
CreateBacktestSessionRequestOptions { request, parallel }
}
}
}
pub fn into_request_and_parallel(self) -> (CreateSessionParams, bool) {
let options = self.into_request_options();
(options.request, options.parallel)
}
}
impl From<CreateSessionParams> for CreateBacktestSessionRequest {
fn from(value: CreateSessionParams) -> Self {
Self::V0(value)
}
}
impl From<CreateBacktestSessionRequestV1> for CreateBacktestSessionRequest {
fn from(value: CreateBacktestSessionRequestV1) -> Self {
Self::V1(value)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub struct CreateBacktestSessionRequestV1 {
#[serde(flatten)]
pub request: CreateSessionParams,
pub parallel: bool,
}
#[derive(Debug, Clone)]
pub struct CreateBacktestSessionRequestOptions {
pub request: CreateSessionParams,
pub parallel: bool,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub struct ContinueSessionRequestV1 {
pub session_id: String,
pub request: ContinueParams,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub struct ContinueToSessionRequestV1 {
pub session_id: String,
pub request: ContinueToParams,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub struct CloseSessionRequestV1 {
pub session_id: String,
}
#[serde_with::serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export))]
#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
pub enum DiscoveryFilter {
ProgramExecuted(
#[serde_as(as = "serde_with::DisplayFromStr")]
#[cfg_attr(feature = "ts-rs", ts(as = "String"))]
Address,
),
}
pub struct TxMatchContext<'a> {
pub invoked_programs: &'a HashSet<Address>,
}
impl DiscoveryFilter {
pub fn matches(&self, ctx: &TxMatchContext<'_>) -> bool {
match self {
Self::ProgramExecuted(target) => ctx.invoked_programs.contains(target),
}
}
}
#[serde_with::serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
pub struct CreateSessionParams {
pub start_slot: u64,
pub end_slot: u64,
#[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
#[serde(default)]
#[cfg_attr(feature = "ts-rs", ts(as = "Vec<String>"))]
pub signer_filter: BTreeSet<Address>,
#[serde(default)]
pub send_summary: bool,
#[serde(default)]
#[cfg_attr(feature = "ts-rs", ts(optional))]
pub capacity_wait_timeout_secs: Option<u16>,
#[serde(default)]
pub disconnect_timeout_secs: Option<u16>,
#[serde(default)]
pub extra_compute_units: Option<u32>,
#[serde(default)]
pub agents: Vec<AgentParams>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub discoveries: Vec<DiscoveryFilter>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub enum AgentType {
Arb,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub struct ArbRouteParams {
pub base_mint: String,
pub temp_mint: String,
#[serde(default)]
pub buy_dexes: Vec<String>,
#[serde(default)]
pub sell_dexes: Vec<String>,
pub min_input: u64,
pub max_input: u64,
#[serde(default)]
pub min_profit: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub struct AgentParams {
pub agent_type: AgentType,
pub wallet: Option<String>,
pub keypair: Option<String>,
pub seed_sol_lamports: Option<u64>,
#[serde(default)]
pub seed_token_accounts: BTreeMap<String, u64>,
#[serde(default)]
pub arb_routes: Vec<ArbRouteParams>,
}
#[serde_with::serde_as]
#[derive(Debug, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
pub struct AccountModifications(
#[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
#[serde(default)]
#[cfg_attr(feature = "ts-rs", ts(as = "BTreeMap<String, AccountData>"))]
pub BTreeMap<Address, AccountData>,
);
#[serde_with::serde_as]
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
pub struct ContinueParams {
#[serde(default = "ContinueParams::default_advance_count")]
pub advance_count: u64,
#[serde(default)]
pub transactions: Vec<String>,
#[serde(default)]
pub modify_account_states: AccountModifications,
}
impl Default for ContinueParams {
fn default() -> Self {
Self {
advance_count: Self::default_advance_count(),
transactions: Vec::new(),
modify_account_states: AccountModifications(BTreeMap::new()),
}
}
}
impl ContinueParams {
pub fn default_advance_count() -> u64 {
1
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export))]
#[serde(rename_all = "camelCase")]
pub struct PausedEvent {
pub slot: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub batch_index: Option<u32>,
}
#[serde_with::serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export))]
#[serde(rename_all = "camelCase")]
pub struct DiscoveryBatchEvent {
pub slot: u64,
pub batch_index: u32,
pub matched: Vec<DiscoveryFilter>,
pub transactions: Vec<EncodedBinary>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export))]
#[serde(rename_all = "camelCase")]
pub struct ContinueToParams {
pub slot: u64,
#[serde(default)]
pub batch_index: Option<u32>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "lowercase")]
pub enum BinaryEncoding {
Base64,
}
impl BinaryEncoding {
pub fn encode(self, bytes: &[u8]) -> String {
match self {
Self::Base64 => BASE64.encode(bytes),
}
}
pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
match self {
Self::Base64 => BASE64.decode(data),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
pub struct EncodedBinary {
pub data: String,
pub encoding: BinaryEncoding,
}
impl EncodedBinary {
pub fn new(data: String, encoding: BinaryEncoding) -> Self {
Self { data, encoding }
}
pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
Self {
data: encoding.encode(bytes),
encoding,
}
}
pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
self.encoding.decode(&self.data)
}
}
#[serde_with::serde_as]
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
pub struct AccountData {
pub data: EncodedBinary,
pub executable: bool,
pub lamports: u64,
#[serde_as(as = "serde_with::DisplayFromStr")]
#[cfg_attr(feature = "ts-rs", ts(as = "String"))]
pub owner: Address,
pub space: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub enum BacktestResponse {
SessionCreated {
session_id: String,
rpc_endpoint: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
task_id: Option<String>,
},
SessionAttached {
session_id: String,
rpc_endpoint: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
task_id: Option<String>,
},
SessionsCreated {
session_ids: Vec<String>,
},
SessionsCreatedV2 {
control_session_id: String,
session_ids: Vec<String>,
#[serde(default)]
task_ids: Vec<Option<String>>,
},
ParallelSessionAttachedV2 {
control_session_id: String,
session_ids: Vec<String>,
#[serde(default)]
task_ids: Vec<Option<String>>,
},
ReadyForContinue,
SlotNotification(u64),
Paused(PausedEvent),
DiscoveryBatch(DiscoveryBatchEvent),
Error(BacktestError),
Success,
Completed {
#[serde(skip_serializing_if = "Option::is_none")]
summary: Option<SessionSummary>,
#[serde(default, skip_serializing_if = "Option::is_none")]
agent_stats: Option<Vec<AgentStatsReport>>,
},
Status {
status: BacktestStatus,
},
SessionEventV1 {
session_id: String,
event: SessionEventV1,
},
SessionEventV2 {
session_id: String,
seq_id: u64,
event: SessionEventKind,
},
}
impl BacktestResponse {
pub fn is_completed(&self) -> bool {
matches!(self, BacktestResponse::Completed { .. })
}
pub fn is_terminal(&self) -> bool {
match self {
BacktestResponse::Completed { .. } => true,
BacktestResponse::Error(e) => matches!(
e,
BacktestError::NoMoreBlocks
| BacktestError::AdvanceSlotFailed { .. }
| BacktestError::Internal { .. }
),
_ => false,
}
}
}
impl From<BacktestStatus> for BacktestResponse {
fn from(status: BacktestStatus) -> Self {
Self::Status { status }
}
}
impl From<String> for BacktestResponse {
fn from(message: String) -> Self {
BacktestError::Internal { error: message }.into()
}
}
impl From<&str> for BacktestResponse {
fn from(message: &str) -> Self {
BacktestError::Internal {
error: message.to_string(),
}
.into()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub enum SessionEventV1 {
ReadyForContinue,
SlotNotification(u64),
Paused(PausedEvent),
DiscoveryBatch(DiscoveryBatchEvent),
Error(BacktestError),
Success,
Completed {
#[serde(skip_serializing_if = "Option::is_none")]
summary: Option<SessionSummary>,
#[serde(default, skip_serializing_if = "Option::is_none")]
agent_stats: Option<Vec<AgentStatsReport>>,
},
Status {
status: BacktestStatus,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub enum SessionEventKind {
ReadyForContinue,
SlotNotification(u64),
Paused(PausedEvent),
DiscoveryBatch(DiscoveryBatchEvent),
Error(BacktestError),
Success,
Completed {
#[serde(skip_serializing_if = "Option::is_none")]
summary: Option<SessionSummary>,
},
Status {
status: BacktestStatus,
},
}
impl SessionEventKind {
pub fn is_terminal(&self) -> bool {
match self {
Self::Completed { .. } => true,
Self::Error(e) => matches!(
e,
BacktestError::NoMoreBlocks
| BacktestError::AdvanceSlotFailed { .. }
| BacktestError::Internal { .. }
),
_ => false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub struct SequencedResponse {
pub seq_id: u64,
#[serde(flatten)]
pub response: BacktestResponse,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
pub enum BacktestStatus {
StartingRuntime,
DecodedTransactions,
AppliedAccountModifications,
ReadyToExecuteUserTransactions,
ExecutedUserTransactions,
ExecutingBlockTransactions,
ExecutedBlockTransactions,
ProgramAccountsLoaded,
}
impl std::fmt::Display for BacktestStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::StartingRuntime => "starting runtime",
Self::DecodedTransactions => "decoded transactions",
Self::AppliedAccountModifications => "applied account modifications",
Self::ReadyToExecuteUserTransactions => "ready to execute user transactions",
Self::ExecutedUserTransactions => "executed user transactions",
Self::ExecutingBlockTransactions => "executing block transactions",
Self::ExecutedBlockTransactions => "executed block transactions",
Self::ProgramAccountsLoaded => "program accounts loaded",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub struct AgentStatsReport {
pub name: String,
pub slots_processed: u64,
pub opportunities_found: u64,
pub opportunities_skipped: u64,
pub no_routes: u64,
pub txs_produced: u64,
pub expected_gain_by_mint: BTreeMap<String, i64>,
#[serde(default)]
pub txs_submitted: u64,
#[serde(default)]
pub txs_failed: u64,
#[serde(default)]
pub txs_simulation_rejected: u64,
#[serde(default)]
pub txs_simulation_failed: u64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
pub struct SessionSummary {
pub correct_simulation: usize,
pub incorrect_simulation: usize,
pub execution_errors: usize,
pub balance_diff: usize,
pub log_diff: usize,
}
impl SessionSummary {
pub fn has_deviations(&self) -> bool {
self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
}
pub fn total_transactions(&self) -> usize {
self.correct_simulation
+ self.incorrect_simulation
+ self.execution_errors
+ self.balance_diff
+ self.log_diff
}
}
impl std::fmt::Display for SessionSummary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let total = self.total_transactions();
write!(
f,
"Session summary: {total} transactions\n\
\x20 - {} correct simulation\n\
\x20 - {} incorrect simulation\n\
\x20 - {} execution errors\n\
\x20 - {} balance diffs\n\
\x20 - {} log diffs",
self.correct_simulation,
self.incorrect_simulation,
self.execution_errors,
self.balance_diff,
self.log_diff,
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
pub enum BacktestError {
InvalidTransactionEncoding {
index: usize,
error: String,
},
InvalidTransactionFormat {
index: usize,
error: String,
},
InvalidAccountEncoding {
address: String,
encoding: BinaryEncoding,
error: String,
},
InvalidAccountOwner {
address: String,
error: String,
},
InvalidAccountPubkey {
address: String,
error: String,
},
NoMoreBlocks,
AdvanceSlotFailed {
slot: u64,
error: String,
},
InvalidRequest {
error: String,
},
Internal {
error: String,
},
InvalidBlockhashFormat {
slot: u64,
error: String,
},
InitializingSysvarsFailed {
slot: u64,
error: String,
},
ClerkError {
error: String,
},
SimulationError {
error: String,
},
SessionNotFound {
session_id: String,
},
SessionOwnerMismatch,
SessionOwnershipBusy {
reason: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AvailableRange {
pub bundle_start_slot: u64,
pub bundle_start_slot_utc: Option<String>,
pub max_bundle_end_slot: Option<u64>,
pub max_bundle_end_slot_utc: Option<String>,
pub max_bundle_size: Option<u64>,
}
pub fn split_range(
ranges: &[AvailableRange],
requested_start: u64,
requested_end: u64,
) -> Result<Vec<(u64, u64)>, String> {
if requested_end < requested_start {
return Err(format!(
"invalid range: start_slot {requested_start} > end_slot {requested_end}"
));
}
let mut candidates: Vec<(u64, u64)> = ranges
.iter()
.filter_map(|r| Some((r.bundle_start_slot, r.max_bundle_end_slot?)))
.filter(|(start, end)| {
end > start
&& *start >= requested_start
&& *start < requested_end
&& *end > requested_start
})
.collect();
candidates.sort_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1)));
candidates.dedup_by_key(|(start, _)| *start);
if candidates.is_empty() || candidates.first().unwrap().0 != requested_start {
return Err(format!(
"start_slot {requested_start} is not covered by any available bundle range"
));
}
let mut result: Vec<(u64, u64)> = Vec::new();
let mut current_slot = requested_start;
let mut i = 0;
let mut best_end = 0u64;
while current_slot <= requested_end {
while i < candidates.len() && candidates[i].0 <= current_slot {
best_end = best_end.max(candidates[i].1);
i += 1;
}
if best_end < current_slot {
return Err(format!("gap in coverage at slot {current_slot}"));
}
let range_end = best_end.min(requested_end);
result.push((current_slot, range_end));
current_slot = range_end + 1;
}
Ok(result)
}
impl From<BacktestError> for BacktestResponse {
fn from(error: BacktestError) -> Self {
Self::Error(error)
}
}
impl std::error::Error for BacktestError {}
impl fmt::Display for BacktestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BacktestError::InvalidTransactionEncoding { index, error } => {
write!(f, "invalid transaction encoding at index {index}: {error}")
}
BacktestError::InvalidTransactionFormat { index, error } => {
write!(f, "invalid transaction format at index {index}: {error}")
}
BacktestError::InvalidAccountEncoding {
address,
encoding,
error,
} => write!(
f,
"invalid encoding for account {address} ({encoding:?}): {error}"
),
BacktestError::InvalidAccountOwner { address, error } => {
write!(f, "invalid owner for account {address}: {error}")
}
BacktestError::InvalidAccountPubkey { address, error } => {
write!(f, "invalid account pubkey {address}: {error}")
}
BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
BacktestError::AdvanceSlotFailed { slot, error } => {
write!(f, "failed to advance to slot {slot}: {error}")
}
BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
BacktestError::Internal { error } => write!(f, "internal error: {error}"),
BacktestError::InvalidBlockhashFormat { slot, error } => {
write!(f, "invalid blockhash at slot {slot}: {error}")
}
BacktestError::InitializingSysvarsFailed { slot, error } => {
write!(f, "failed to initialize sysvars at slot {slot}: {error}")
}
BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
BacktestError::SimulationError { error } => {
write!(f, "simulation error: {error}")
}
BacktestError::SessionNotFound { session_id } => {
write!(f, "session not found: {session_id}")
}
BacktestError::SessionOwnerMismatch => {
write!(f, "session owner mismatch")
}
BacktestError::SessionOwnershipBusy { reason } => {
write!(f, "session ownership busy: {reason}")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn range(start: u64, end: u64) -> AvailableRange {
AvailableRange {
bundle_start_slot: start,
bundle_start_slot_utc: None,
max_bundle_end_slot: Some(end),
max_bundle_end_slot_utc: None,
max_bundle_size: None,
}
}
#[test]
fn split_range_valid() {
let ranges = vec![range(100, 300)];
assert_eq!(split_range(&ranges, 100, 300).unwrap(), vec![(100, 300)]);
let ranges = vec![range(100, 200), range(201, 300), range(301, 400)];
assert_eq!(
split_range(&ranges, 100, 300).unwrap(),
vec![(100, 200), (201, 300)]
);
let ranges = vec![
range(100, 500),
range(110, 150),
range(150, 190),
range(501, 900),
];
assert_eq!(
split_range(&ranges, 100, 900).unwrap(),
vec![(100, 500), (501, 900)]
);
}
#[test]
fn split_range_err() {
let ranges = vec![range(200, 400)];
assert!(split_range(&ranges, 100, 400).is_err());
let ranges = vec![range(200, 400)];
assert!(split_range(&ranges, 300, 400).is_err());
let ranges = vec![range(100, 200)];
assert!(split_range(&ranges, 100, 300).is_err());
let ranges = vec![range(100, 200), range(210, 300)];
assert!(split_range(&ranges, 100, 300).is_err());
let ranges = vec![range(100, 300)];
assert!(split_range(&ranges, 300, 100).is_err());
}
}