///|
pub(all) struct ValidationIssue {
code : String
message : String
severity : String
}
///|
pub(all) struct ValidationReport {
subject : String
issues : Array[ValidationIssue]
}
///|
pub fn validation_issue(
code : String,
message : String,
severity? : String = "error",
) -> ValidationIssue {
{ code, message, severity }
}
///|
pub fn ValidationReport::passed(self : ValidationReport) -> Bool {
self.issues.length() == 0
}
///|
pub fn ValidationReport::summary(self : ValidationReport) -> String {
if self.passed() {
"PASS " + self.subject
} else {
"FAIL " + self.subject + " issues=" + self.issues.length().to_string()
}
}
///|
pub fn ValidationReport::text(self : ValidationReport) -> String {
let buf = StringBuilder::new()
buf.write_string(self.summary())
for issue in self.issues {
buf.write_string("\n")
buf.write_string(
"- " + issue.severity + " " + issue.code + ": " + issue.message,
)
}
buf.to_string()
}
///|
pub fn validate_sim(sim : Sim) -> ValidationReport {
let issues : Array[ValidationIssue] = []
if sim.now < 0 {
issues.push(
validation_issue("negative-time", "simulation time must not be negative"),
)
}
if sim.next_id < 1 {
issues.push(
validation_issue("bad-next-id", "next event id must be positive"),
)
}
validate_event_ids(sim.events, issues)
validate_event_ticks(sim.events, sim.now, issues)
validate_event_heap(sim.events, issues)
{ subject: "sim", issues }
}
///|
fn validate_event_ids(
events : Array[ScheduledEvent],
issues : Array[ValidationIssue],
) -> Unit {
let mut i = 0
while i < events.length() {
if events[i].id < 1 {
issues.push(validation_issue("bad-event-id", "event id must be positive"))
}
let mut j = i + 1
while j < events.length() {
if events[i].id == events[j].id &&
!events[i].cancelled &&
!events[j].cancelled {
issues.push(
validation_issue(
"duplicate-event-id", "pending event ids must be unique",
),
)
}
j += 1
}
i += 1
}
}
///|
fn validate_event_ticks(
events : Array[ScheduledEvent],
now : Int,
issues : Array[ValidationIssue],
) -> Unit {
for event in events {
if event.tick < now && !event.cancelled {
issues.push(
validation_issue(
"past-pending-event", "pending event cannot be before current time",
),
)
}
if event.repeat_every < 0 {
issues.push(
validation_issue(
"negative-repeat", "repeat interval must not be negative",
),
)
}
}
}
///|
fn validate_event_heap(
events : Array[ScheduledEvent],
issues : Array[ValidationIssue],
) -> Unit {
let mut index = 1
while index < events.length() {
let parent = (index - 1) / 2
if compare_event(events[index], events[parent]) < 0 {
issues.push(
validation_issue(
"invalid-event-heap", "pending events must preserve stable heap ordering",
),
)
return
}
index += 1
}
}
///|
pub fn validate_trace(entries : Array[TraceEntry]) -> ValidationReport {
let issues : Array[ValidationIssue] = []
let mut last_tick = 0
let mut seen = false
for entry in entries {
if entry.tick < 0 {
issues.push(
validation_issue(
"negative-trace-tick", "trace tick must be non-negative",
),
)
}
if seen && entry.tick < last_tick && entry.kind == "execute" {
issues.push(
validation_issue(
"trace-time-regression", "execute trace moved backwards",
),
)
}
last_tick = entry.tick
seen = true
if entry.kind == "" {
issues.push(
validation_issue("empty-trace-kind", "trace kind should be non-empty"),
)
}
}
{ subject: "trace", issues }
}
///|
pub fn validate_metrics(metrics : Metrics) -> ValidationReport {
let issues : Array[ValidationIssue] = []
for counter in metrics.snapshot() {
if counter.name == "" {
issues.push(
validation_issue(
"empty-counter-name", "counter name should be non-empty",
),
)
}
}
for gauge in metrics.gauge_snapshot() {
if gauge.name == "" {
issues.push(
validation_issue("empty-gauge-name", "gauge name should be non-empty"),
)
}
}
for sample in metrics.sample_snapshot() {
if sample.name == "" {
issues.push(
validation_issue("empty-sample-name", "sample name should be non-empty"),
)
}
}
{ subject: "metrics", issues }
}
///|
pub fn Sim::validate(self : Sim) -> ValidationReport {
validate_sim(self)
}