///|
pub(all) enum DifferentialPolicy {
CompareDisposition
CompareResultDigest
CompareDispositionAndDigest
} derive(Debug, Eq)
///|
fn disposition_class(value : ExecutionDisposition) -> String {
match value {
Accepted => "accepted"
Rejected(reason) => "rejected:" + reason
Failed(kind, detail) => "failed:" + kind + ":" + detail
}
}
///|
fn differential_mismatch(
left : TargetObservation,
right : TargetObservation,
policy : DifferentialPolicy,
) -> String? {
let disposition_differs = disposition_class(left.disposition) !=
disposition_class(right.disposition)
let digest_differs = left.result_digest != right.result_digest
match policy {
CompareDisposition =>
if disposition_differs {
Some("disposition")
} else {
None
}
CompareResultDigest =>
if digest_differs {
Some("result-digest")
} else {
None
}
CompareDispositionAndDigest =>
if disposition_differs {
Some("disposition")
} else if digest_differs {
Some("result-digest")
} else {
None
}
}
}
///|
fn merged_features(
left : TargetObservation,
right : TargetObservation,
) -> Array[Feature] {
let result : Array[Feature] = []
for item in left.canonical_features() {
result.push(feature("left/" + item.domain, item.value))
}
for item in right.canonical_features() {
result.push(feature("right/" + item.domain, item.value))
}
result
}
///|
pub fn differential_target(
name : String,
left : Target,
right : Target,
policy? : DifferentialPolicy = CompareDispositionAndDigest,
) -> Target {
Target::new(name, input => {
let left_result = left.run(input)
let right_result = right.run(input)
let features = merged_features(left_result, right_result)
match differential_mismatch(left_result, right_result, policy) {
None =>
accepted(
features~,
result_digest=left_result.result_digest.unwrap_or(""),
cost_class=left_result.cost_class.max(right_result.cost_class),
)
Some(reason) =>
failed(
"differential-mismatch",
reason + " between " + left.name() + " and " + right.name(),
features~,
)
}
})
}
///|
pub struct MetamorphicTransform {
name : String
apply : (Bytes) -> Bytes
relation : (TargetObservation, TargetObservation) -> Bool
}
///|
pub fn MetamorphicTransform::new(
name : String,
apply : (Bytes) -> Bytes,
relation : (TargetObservation, TargetObservation) -> Bool,
) -> MetamorphicTransform {
{ name, apply, relation, }
}
///|
pub fn MetamorphicTransform::name(self : MetamorphicTransform) -> String {
self.name
}
///|
pub fn same_digest_transform(
name : String,
apply : (Bytes) -> Bytes,
) -> MetamorphicTransform {
MetamorphicTransform::new(name, apply, (before, after) => {
before.result_digest == after.result_digest
})
}
///|
pub fn same_disposition_transform(
name : String,
apply : (Bytes) -> Bytes,
) -> MetamorphicTransform {
MetamorphicTransform::new(name, apply, (before, after) => {
disposition_class(before.disposition) ==
disposition_class(after.disposition)
})
}
///|
pub fn metamorphic_target(
name : String,
base : Target,
transforms : Array[MetamorphicTransform],
) -> Target {
Target::new(name, input => {
let baseline = base.run(input)
let features : Array[Feature] = []
for item in baseline.canonical_features() {
features.push(feature("base/" + item.domain, item.value))
}
for transform in transforms {
let transformed_input = (transform.apply)(input)
let transformed = base.run(transformed_input)
for item in transformed.canonical_features() {
features.push(
feature("transform/" + transform.name + "/" + item.domain, item.value),
)
}
if !(transform.relation)(baseline, transformed) {
return failed("metamorphic-violation", transform.name, features~)
}
}
accepted(
features~,
result_digest=baseline.result_digest.unwrap_or(""),
cost_class=baseline.cost_class,
)
})
}
///|
pub fn agreement_target(
name : String,
targets : Array[Target],
minimum_agreement? : Int = 2,
) -> Target {
Target::new(name, input => {
if targets.is_empty() {
return failed("agreement-configuration", "no targets")
}
let counts : @hashmap.HashMap[String, Int] = @hashmap.HashMap([])
let features : Array[Feature] = []
let mut best_key = ""
let mut best_count = 0
for target in targets {
let result = target.run(input)
let key = disposition_class(result.disposition) +
"|" +
result.result_digest.unwrap_or("")
let count = counts.get(key).unwrap_or(0) + 1
counts.set(key, count)
if count > best_count {
best_count = count
best_key = key
}
features.push(feature("engine", target.name() + ":" + key))
}
if best_count < minimum_agreement.max(1) {
failed(
"agreement-failure",
"best=" + best_count.to_string() + "/" + targets.length().to_string(),
features~,
)
} else {
accepted(features~, result_digest=best_key)
}
})
}