use std::{fs, path::PathBuf};
use chrono::{DateTime, Utc};
use serde::Serialize;
use super::WorkflowListFilter;
use crate::{RunId, WorkflowId, WorkflowStatus, WorkflowSummary};
const OPS_CONSOLE_CASES_PATH: &str =
"../../apps/aion-ops-console/src/features/workflow-list/lib/text-match.cases.json";
const UPDATE_VAR: &str = "AION_UPDATE_BINDINGS";
const REGENERATE_COMMAND: &str =
"AION_UPDATE_BINDINGS=1 cargo test -p aion-core export_console_text_match_cases";
const FIXTURE_WORKFLOW_ID: u128 = 0xabc0_0000_0000_0000_0000_0000_0000_0001;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct TextMatchCase {
name: &'static str,
needle: Option<&'static str>,
display_name: Option<&'static str>,
workflow_id: String,
matches: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct TextMatchCaseDocument {
generated_by: &'static str,
rule: &'static str,
cases: Vec<TextMatchCase>,
}
fn summary(display_name: Option<&str>) -> WorkflowSummary {
WorkflowSummary {
workflow_id: WorkflowId::new(uuid::Uuid::from_u128(FIXTURE_WORKFLOW_ID)),
run_id: RunId::new_v4(),
workflow_type: String::from("checkout"),
status: WorkflowStatus::Running,
started_at: DateTime::<Utc>::default(),
updated_at: DateTime::<Utc>::default() + chrono::Duration::seconds(5),
ended_at: None,
parent: None,
failed_step: None,
failure_reason: None,
display_name: display_name.map(str::to_owned),
kind: None,
current_worker: None,
package_version: None,
}
}
const CASES: &[(&str, Option<&str>, Option<&str>, bool)] = &[
(
"a needle is trimmed, folded, and matched as a substring of the display name",
Some(" NIGHTLY "),
Some("the nightly build"),
true,
),
(
"a display name that does not contain it does not match",
Some(" NIGHTLY "),
Some("weekly build"),
false,
),
(
"a run with no display name cannot match by name",
Some(" NIGHTLY "),
None,
false,
),
(
"the id matches as a case-insensitive prefix",
Some("ABC00000"),
None,
true,
),
(
"the id is a PREFIX, not a substring",
Some("bc00000"),
None,
false,
),
(
"a whitespace-only needle is no predicate at all",
Some(" "),
None,
true,
),
("an absent needle matches everything", None, None, true),
(
"a needle with an embedded space is one literal substring, not two terms",
Some("run C"),
Some("Run c"),
true,
),
(
"and that literal is not two terms: the words apart do not match",
Some("run C"),
Some("Run of c"),
false,
),
(
"the display name is matched raw, so a name that is only whitespace matches nothing",
Some("nightly"),
Some(" "),
false,
),
];
fn filter_for(needle: Option<&str>) -> WorkflowListFilter {
WorkflowListFilter {
text: needle.map(str::to_owned),
..WorkflowListFilter::default()
}
}
fn rendered_cases() -> Result<String, Box<dyn std::error::Error>> {
let document = TextMatchCaseDocument {
generated_by: REGENERATE_COMMAND,
rule: "crates/aion-core/src/listing.rs — WorkflowListFilter::matches_text",
cases: CASES
.iter()
.map(|&(name, needle, display_name, matches)| TextMatchCase {
name,
needle,
display_name,
workflow_id: WorkflowId::new(uuid::Uuid::from_u128(FIXTURE_WORKFLOW_ID))
.to_string(),
matches,
})
.collect(),
};
let mut rendered = serde_json::to_string_pretty(&document)?;
rendered.push('\n');
Ok(rendered)
}
fn cases_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(OPS_CONSOLE_CASES_PATH)
}
#[test]
fn every_emitted_case_is_what_the_rule_answers() {
for &(name, needle, display_name, expected) in CASES {
let filter = filter_for(needle);
assert_eq!(
filter.matches_text(&summary(display_name)),
expected,
"the emitted case `{name}` does not match what `matches_text` answers"
);
}
}
#[test]
fn export_console_text_match_cases() -> Result<(), Box<dyn std::error::Error>> {
let generated = rendered_cases()?;
let output_path = cases_path();
if std::env::var_os(UPDATE_VAR).is_some() {
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&output_path, &generated)?;
return Ok(());
}
let committed = fs::read_to_string(&output_path).unwrap_or_default();
if committed == generated {
return Ok(());
}
Err(format!(
"the ops console's text-match cases are STALE: `{}` does not match what the list text \
rule produces.\nNothing was written — regenerate and commit the result:\n \
{REGENERATE_COMMAND}\n(committed {} bytes, the rule produces {} bytes)",
output_path.display(),
committed.len(),
generated.len(),
)
.into())
}