use serde::{Deserialize, Serialize};
use crate::actions::Via;
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum RunMode {
#[default]
Ask,
Autopilot,
}
impl RunMode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Ask => "ask",
Self::Autopilot => "autopilot",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum OracleOp {
Write,
Reject,
Again,
Edit,
Reading,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub enum Reading {
A,
B,
}
impl Reading {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::A => "A",
Self::B => "B",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct Expectation {
pub scenario: String,
pub given: String,
pub expect: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct Untestable {
pub scenario: String,
pub why: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ReadingText {
pub given: String,
pub expect: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct Resolution {
pub reading: Reading,
pub by: String,
pub at: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct Ambiguity {
pub scenario: String,
pub a: ReadingText,
pub b: ReadingText,
pub resolution: Option<Resolution>,
}
impl Ambiguity {
#[must_use]
pub const fn open(&self) -> bool {
self.resolution.is_none()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AuthoredBy {
Model,
Person,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct OracleTest {
pub path: String,
pub content: String,
pub scenarios: Vec<String>,
pub red_class: String,
pub red: bool,
pub authored_by: AuthoredBy,
pub edited_by: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct OracleDiscarded {
pub path: String,
pub why: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct OracleRejected {
pub path: String,
pub reason: String,
pub by: String,
pub at: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct HeldOutSummary {
pub state: String,
pub count: usize,
pub manifest_sha256: Option<String>,
pub reason: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum OracleState {
None,
Written,
NotRed,
Failed,
Ambiguous,
Frozen,
Lapsed,
FrozenElsewhere,
}
impl OracleState {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::Written => "written",
Self::NotRed => "not_red",
Self::Failed => "failed",
Self::Ambiguous => "ambiguous",
Self::Frozen => "frozen",
Self::Lapsed => "lapsed",
Self::FrozenElsewhere => "frozen_elsewhere",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct OracleView {
pub req: String,
pub title: String,
pub scenarios: Vec<crate::spec::ScenarioRef>,
pub state: OracleState,
pub frozen_on: Option<String>,
pub frozen_by: Option<String>,
pub frozen_at: Option<u64>,
pub tests: Vec<OracleTest>,
pub discarded: Vec<OracleDiscarded>,
pub rejected: Vec<OracleRejected>,
pub uncovered: Vec<String>,
pub untestable: Vec<Untestable>,
pub ambiguities: Vec<Ambiguity>,
pub held_out: HeldOutSummary,
pub strength_target: Option<u8>,
pub calls: u8,
pub ambiguity_check: bool,
pub manifest_sha256: Option<String>,
pub error: Option<String>,
}
impl OracleView {
pub fn open_ambiguities(&self) -> impl Iterator<Item = &Ambiguity> {
self.ambiguities.iter().filter(|a| a.open())
}
#[must_use]
pub fn freezes(&self) -> String {
let red = self.tests.iter().filter(|t| t.red).count();
if let Some(a) = self.open_ambiguities().next() {
return format!("blocked: two readings of {}", a.scenario);
}
match self.state {
OracleState::Frozen => {
format!("{red} {} frozen", if red == 1 { "test" } else { "tests" })
}
_ if red == 0 => "tests written at run time".to_owned(),
_ => {
let mut parts = vec![format!("{red} {}", if red == 1 { "test" } else { "tests" })];
if self.held_out.state == "kept" {
parts.push(format!("{} held out", self.held_out.count));
}
if let Some(t) = self.strength_target {
parts.push(format!("target {t}%"));
}
parts.join(" · ")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ReviewApproval {
pub sha256: String,
pub mode: RunMode,
pub by: String,
pub via: Via,
pub at: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct OfferedGrant {
pub id: String,
pub class: String,
pub object: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ApproveSheet {
#[serde(default)]
pub sha256: Option<String>,
pub approves: Vec<String>,
pub blocked: Vec<String>,
pub autopilot_blocked: Option<String>,
pub sandbox: Option<String>,
pub grants: Vec<OfferedGrant>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct AutopilotDenial {
pub task: String,
pub turn: u32,
pub class: String,
pub object: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum CommentAnchor {
Passage {
path: String,
start_line: usize,
end_line: usize,
quote: String,
#[serde(default)]
quote_sha256: String,
#[serde(default)]
section: String,
},
DiffLine {
path: String,
side: String,
line: usize,
quote: String,
},
TestLine {
req: String,
path: String,
line: usize,
quote: String,
},
Finding {
source: String,
id: String,
text: String,
},
}
impl CommentAnchor {
#[must_use]
pub fn quote(&self) -> &str {
match self {
Self::Passage { quote, .. }
| Self::DiffLine { quote, .. }
| Self::TestLine { quote, .. } => quote,
Self::Finding { text, .. } => text,
}
}
#[must_use]
pub fn place(&self) -> String {
match self {
Self::Passage {
path,
start_line,
end_line,
..
} if start_line == end_line => format!("{path}:{start_line}"),
Self::Passage {
path,
start_line,
end_line,
..
} => format!("{path}:{start_line}-{end_line}"),
Self::DiffLine {
path, side, line, ..
} => format!("{path}:{line} ({side})"),
Self::TestLine {
req, path, line, ..
} => format!("{req} {path}:{line}"),
Self::Finding { source, id, .. } => format!("{source} {id}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DraftState {
Draft,
Outdated,
Sent,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CommentDraft {
pub id: String,
pub anchor: CommentAnchor,
pub text: String,
pub by: String,
pub at: u64,
pub state: DraftState,
#[serde(default)]
pub kept: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct RevisionView {
pub id: String,
pub comments: Vec<String>,
pub sections: Vec<String>,
pub skill: String,
pub changed: Vec<String>,
pub outside: Vec<String>,
pub outcome: String,
pub files: Vec<String>,
#[serde(default)]
pub asked_again: Vec<String>,
#[serde(default)]
pub beside: Vec<String>,
#[serde(default)]
pub diffs: Vec<SectionDiff>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct BugTests {
pub task: String,
pub title: String,
pub confirmation: crate::bug::Confirmation,
pub reproductions: Vec<BugTest>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct BugTest {
pub statement: String,
pub file: String,
pub test: String,
pub red_class: String,
pub content: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct SectionDiff {
pub path: String,
pub heading: String,
pub before: String,
pub after: String,
pub outside: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CommentsView {
pub feature: String,
pub drafts: Vec<CommentDraft>,
pub turn: Option<RevisionView>,
pub revise_with: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CodeComment {
pub path: String,
pub line: usize,
pub quote: String,
pub text: String,
}
impl CodeComment {
#[must_use]
pub fn nudge_line(&self) -> String {
let quote: String = self.quote.trim().chars().take(80).collect();
format!(
"{}:{} \"{}\" — {}",
self.path,
self.line,
quote,
self.text.trim()
)
}
}
#[must_use]
pub fn normalise(text: &str) -> String {
text.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.to_lowercase()
}
#[must_use]
pub fn compare(
a: &[Expectation],
a_untestable: &[Untestable],
b: &[Expectation],
b_untestable: &[Untestable],
) -> Vec<Ambiguity> {
let mut out: Vec<Ambiguity> = Vec::new();
let mut push = |scenario: &str, ra: ReadingText, rb: ReadingText| {
if !out.iter().any(|x| x.scenario == scenario) {
out.push(Ambiguity {
scenario: scenario.to_owned(),
a: ra,
b: rb,
resolution: None,
});
}
};
let reading = |e: &Expectation| ReadingText {
given: e.given.clone(),
expect: e.expect.clone(),
};
let untestable = |u: &Untestable| ReadingText {
given: String::new(),
expect: format!("untestable: {}", u.why),
};
for ea in a {
for eb in b.iter().filter(|eb| eb.scenario == ea.scenario) {
if normalise(&ea.given) == normalise(&eb.given)
&& normalise(&ea.expect) != normalise(&eb.expect)
{
push(&ea.scenario, reading(ea), reading(eb));
}
}
if let Some(u) = b_untestable.iter().find(|u| u.scenario == ea.scenario) {
push(&ea.scenario, reading(ea), untestable(u));
}
}
for eb in b {
if let Some(u) = a_untestable.iter().find(|u| u.scenario == eb.scenario) {
push(&eb.scenario, untestable(u), reading(eb));
}
}
out
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
fn e(s: &str, given: &str, expect: &str) -> Expectation {
Expectation {
scenario: s.to_owned(),
given: given.to_owned(),
expect: expect.to_owned(),
}
}
#[test]
fn the_same_given_with_another_outcome_is_ambiguous() {
let a = [
e("US1-1", "name Ada", "Hello, Ada!"),
e("US1-2", "\"\"", "Hello!"),
];
let b = [
e("US1-1", " Name ada ", "hello, ada!"),
e("US1-2", "\"\"", "an error"),
];
let found = compare(&a, &[], &b, &[]);
assert_eq!(found.len(), 1);
assert_eq!(found[0].scenario, "US1-2");
assert_eq!(found[0].a.expect, "Hello!");
assert_eq!(found[0].b.expect, "an error");
assert!(found[0].open());
}
#[test]
fn different_givens_are_different_examples() {
let a = [e("US1-1", "name Ada", "Hello, Ada!")];
let b = [e("US1-1", "name Bob", "Hello, Bob!")];
assert!(compare(&a, &[], &b, &[]).is_empty());
}
#[test]
fn covered_by_one_and_untestable_for_the_other_is_ambiguous() {
let a = [e("US1-3", "offline", "an error")];
let u = [Untestable {
scenario: "US1-3".to_owned(),
why: "needs a network".to_owned(),
}];
let found = compare(&a, &[], &[], &u);
assert_eq!(found.len(), 1);
assert!(found[0].b.expect.starts_with("untestable"));
let back = compare(&[], &u, &a, &[]);
assert_eq!(back.len(), 1);
assert!(back[0].a.expect.starts_with("untestable"));
}
proptest! {
#[test]
fn agreeing_authors_raise_nothing(given in "[a-zA-Z ]{1,20}", expect in "[a-zA-Z ]{1,20}", pad in " {0,3}") {
let a = [e("S", &given, &expect)];
let b = [e("S", &format!("{pad}{}{pad}", given.to_uppercase()), &expect.to_lowercase())];
prop_assert!(compare(&a, &[], &b, &[]).is_empty());
}
#[test]
fn the_rule_is_symmetric_in_whether_it_fires(g in "[a-c]{1,3}", x in "[a-c]{1,3}", y in "[a-c]{1,3}") {
let a = [e("S", &g, &x)];
let b = [e("S", &g, &y)];
prop_assert_eq!(compare(&a, &[], &b, &[]).len(), compare(&b, &[], &a, &[]).len());
prop_assert_eq!(compare(&a, &[], &b, &[]).is_empty(), normalise(&x) == normalise(&y));
}
}
#[test]
fn the_sheet_says_what_approval_freezes() {
let mut v = OracleView {
req: "US1".to_owned(),
title: "Greet".to_owned(),
scenarios: Vec::new(),
state: OracleState::Written,
frozen_on: None,
frozen_by: None,
frozen_at: None,
tests: vec![OracleTest {
path: "tests/a.rs".to_owned(),
content: String::new(),
scenarios: vec!["US1-1".to_owned()],
red_class: "assertion".to_owned(),
red: true,
authored_by: AuthoredBy::Model,
edited_by: None,
}],
discarded: Vec::new(),
rejected: Vec::new(),
uncovered: Vec::new(),
untestable: Vec::new(),
ambiguities: Vec::new(),
held_out: HeldOutSummary {
state: "kept".to_owned(),
count: 2,
manifest_sha256: None,
reason: None,
},
strength_target: Some(80),
calls: 2,
ambiguity_check: true,
manifest_sha256: None,
error: None,
};
assert_eq!(v.freezes(), "1 test · 2 held out · target 80%");
v.tests[0].red = false;
assert_eq!(v.freezes(), "tests written at run time");
let line = CodeComment {
path: "src/a.rs".to_owned(),
line: 12,
quote: " let x = 1;".to_owned(),
text: " use a constant ".to_owned(),
};
assert_eq!(
line.nudge_line(),
"src/a.rs:12 \"let x = 1;\" — use a constant"
);
}
}