///|
pub(all) enum BatchStrategy {
OneMutantPerBatch
FixedBatchSize(Int)
TargetBatchMillis(Int)
} derive(Debug, Eq, ToJson)
///|
pub(all) struct BatchValidation {
valid : Bool
message : String
} derive(Debug, Eq, ToJson)
///|
pub(all) struct ExecutionBatch {
batch_id : Int
start_index : Int
end_index : Int
execution_count : Int
command_count : Int
estimated_timeout_ms : Int
executions : Array[MutationExecution]
} derive(Debug, Eq, ToJson)
///|
pub(all) struct BatchPlan {
strategy : BatchStrategy
selection : SelectionMode
batch_count : Int
execution_count : Int
command_count : Int
estimated_timeout_ms : Int
valid : Bool
message : String
batches : Array[ExecutionBatch]
} derive(Debug, Eq, ToJson)
///|
pub fn validate_batch_strategy(strategy : BatchStrategy) -> BatchValidation {
match strategy {
OneMutantPerBatch => { valid: true, message: "one mutant per batch" }
FixedBatchSize(size) =>
if size > 0 {
{ valid: true, message: "fixed batch size \{size}" }
} else {
{ valid: false, message: "batch size must be positive" }
}
TargetBatchMillis(ms) =>
if ms > 0 {
{ valid: true, message: "target batch budget \{ms} ms" }
} else {
{ valid: false, message: "target batch millis must be positive" }
}
}
}
///|
pub fn build_batch_plan(
plan : ExecutionPlan,
strategy : BatchStrategy,
) -> BatchPlan {
build_selected_batch_plan(plan, AllMutants, strategy)
}
///|
pub fn build_selected_batch_plan(
plan : ExecutionPlan,
selection : SelectionMode,
strategy : BatchStrategy,
) -> BatchPlan {
let executions = select_executions(plan, selection)
let validation = validate_batch_strategy(strategy)
if validation.valid {
let batches = batches_for_strategy(executions, strategy)
summarize_batches(strategy, selection, validation, batches)
} else {
summarize_batches(strategy, selection, validation, [])
}
}
///|
pub fn estimate_execution_timeout_ms(execution : MutationExecution) -> Int {
let mut total = 0
for command in execution.commands {
total += command.timeout_ms
}
total
}
///|
pub fn format_batch_strategy(strategy : BatchStrategy) -> String {
match strategy {
OneMutantPerBatch => "one-mutant"
FixedBatchSize(size) => "fixed-size \{size}"
TargetBatchMillis(ms) => "target-ms \{ms}"
}
}
///|
pub fn format_batch_plan(plan : BatchPlan) -> String {
let lines : Array[String] = [
"Mutation batch plan",
"strategy: \{format_batch_strategy(plan.strategy)}",
"selection: \{format_selection_mode(plan.selection)}",
"valid: \{plan.valid}",
"batches: \{plan.batch_count}",
"executions: \{plan.execution_count}",
"commands: \{plan.command_count}",
"estimated-timeout-ms: \{plan.estimated_timeout_ms}",
"message: \{plan.message}",
]
for batch in plan.batches {
lines.push(
"- batch #\{batch.batch_id}: executions " +
"\{batch.start_index}..\{batch.end_index}, " +
"mutants=\{batch.execution_count}, " +
"commands=\{batch.command_count}, " +
"timeout-ms=\{batch.estimated_timeout_ms}",
)
}
lines.join("\n")
}
///|
fn batches_for_strategy(
executions : ArrayView[MutationExecution],
strategy : BatchStrategy,
) -> Array[ExecutionBatch] {
match strategy {
OneMutantPerBatch => batches_by_size(executions, 1)
FixedBatchSize(size) => batches_by_size(executions, size)
TargetBatchMillis(ms) => batches_by_timeout(executions, ms)
}
}
///|
fn batches_by_size(
executions : ArrayView[MutationExecution],
size : Int,
) -> Array[ExecutionBatch] {
let batches : Array[ExecutionBatch] = []
let mut current : Array[MutationExecution] = []
let mut start_index = 0
for index, execution in executions {
if current.is_empty() {
start_index = index
}
current.push(execution)
if current.length() == size {
batches.push(
make_batch(batches.length(), start_index, index + 1, current),
)
current = []
}
}
if !current.is_empty() {
batches.push(
make_batch(
batches.length(),
start_index,
start_index + current.length(),
current,
),
)
}
batches
}
///|
fn batches_by_timeout(
executions : ArrayView[MutationExecution],
target_ms : Int,
) -> Array[ExecutionBatch] {
let batches : Array[ExecutionBatch] = []
let mut current : Array[MutationExecution] = []
let mut current_ms = 0
let mut start_index = 0
for index, execution in executions {
let next_ms = estimate_execution_timeout_ms(execution)
if current.is_empty() {
start_index = index
current.push(execution)
current_ms = next_ms
} else if current_ms + next_ms > target_ms {
batches.push(make_batch(batches.length(), start_index, index, current))
current = [execution]
current_ms = next_ms
start_index = index
} else {
current.push(execution)
current_ms += next_ms
}
}
if !current.is_empty() {
batches.push(
make_batch(
batches.length(),
start_index,
start_index + current.length(),
current,
),
)
}
batches
}
///|
fn make_batch(
batch_id : Int,
start_index : Int,
end_index : Int,
executions : Array[MutationExecution],
) -> ExecutionBatch {
let mut command_count = 0
let mut estimated_timeout_ms = 0
for execution in executions {
command_count += execution.commands.length()
estimated_timeout_ms += estimate_execution_timeout_ms(execution)
}
{
batch_id,
start_index,
end_index,
execution_count: executions.length(),
command_count,
estimated_timeout_ms,
executions,
}
}
///|
fn summarize_batches(
strategy : BatchStrategy,
selection : SelectionMode,
validation : BatchValidation,
batches : Array[ExecutionBatch],
) -> BatchPlan {
let mut execution_count = 0
let mut command_count = 0
let mut estimated_timeout_ms = 0
for batch in batches {
execution_count += batch.execution_count
command_count += batch.command_count
estimated_timeout_ms += batch.estimated_timeout_ms
}
{
strategy,
selection,
batch_count: batches.length(),
execution_count,
command_count,
estimated_timeout_ms,
valid: validation.valid,
message: validation.message,
batches,
}
}