use std::path::PathBuf;
use std::process::Command;
fn run(args: &[&str]) -> Out {
let out = Command::new(env!("CARGO_BIN_EXE_fsmp"))
.args(args)
.output()
.expect("failed to run fsmp");
Out {
code: out.status.code().unwrap_or(-1),
text: format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
),
}
}
struct Out {
code: i32,
text: String,
}
impl Out {
fn ok(self) -> Out {
assert_eq!(
self.code, 0,
"expected success, got {}:\n{}",
self.code, self.text
);
self
}
fn fail(self) -> Out {
assert_ne!(
self.code, 0,
"expected failure, got success:\n{}",
self.text
);
self
}
fn has(self, needle: &str) -> Out {
assert!(
self.text.contains(needle),
"missing {needle:?} in:\n{}",
self.text
);
self
}
fn lacks(self, needle: &str) -> Out {
assert!(
!self.text.contains(needle),
"unexpected {needle:?} in:\n{}",
self.text
);
self
}
}
fn write_def(name: &str, contents: &str) -> String {
write_def_ext(name, "yaml", contents)
}
fn write_def_ext(name: &str, ext: &str, contents: &str) -> String {
let file = if ext.is_empty() {
format!("fsmp-lint-it-{name}")
} else {
format!("fsmp-lint-it-{name}.{ext}")
};
let path = std::env::temp_dir().join(file);
std::fs::write(&path, contents).unwrap();
path.to_string_lossy().into_owned()
}
fn dev_cycle() -> String {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join(".claude/skills/dev-cycle/fsmp-definition.yaml")
.to_string_lossy()
.into_owned()
}
fn author_workflow() -> String {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join(".claude/skills/author-fsmp-workflow/fsmp-definition.yaml")
.to_string_lossy()
.into_owned()
}
#[test]
fn the_shipped_dev_cycle_definition_lints_clean() {
run(&["lint", "--def", &dev_cycle()]).ok().has("clean");
}
#[test]
fn the_shipped_author_workflow_definition_lints_clean() {
run(&["lint", "--def", &author_workflow()])
.ok()
.has("clean");
}
#[test]
fn a_clean_custom_definition_exits_zero() {
let p = write_def(
"clean",
"\
name: t
initial: a
states:
a:
transitions:
go: { to: b }
b:
terminal: true
",
);
run(&["lint", "--def", &p]).ok().has("clean");
}
#[test]
fn reports_unknown_target_and_unreachable_together_and_exits_nonzero() {
let p = write_def(
"combined",
"\
name: t
initial: a
states:
a:
transitions:
go: { to: ghost }
b:
terminal: true
",
);
run(&["lint", "--def", &p])
.fail()
.has("ghost")
.has("unreachable")
.has("2 problems found");
}
#[test]
fn json_output_reports_findings_and_ok_false() {
let p = write_def(
"json",
"\
name: t
initial: a
states:
a:
transitions:
go: { to: ghost }
b:
terminal: true
",
);
run(&["lint", "--def", &p, "--json"])
.fail()
.has("\"ok\": false")
.has("\"findings\"")
.has("\"unknown_target\"")
.has("\"unreachable_state\"");
}
#[test]
fn json_output_reports_ok_true_for_a_clean_definition() {
let p = write_def(
"jsonclean",
"\
name: t
initial: a
states:
a:
terminal: true
",
);
run(&["lint", "--def", &p, "--json"])
.ok()
.has("\"ok\": true");
}
#[test]
fn a_malformed_definition_is_a_hard_error_not_a_finding() {
let p = write_def("malformed", "name: [unterminated\n");
run(&["lint", "--def", &p]).fail().lacks("clean");
}
#[test]
fn an_unsupported_extension_is_a_hard_error_not_a_finding() {
let p = write_def_ext(
"wrongext",
"txt",
"\
name: t
initial: a
states:
a:
terminal: true
",
);
run(&["lint", "--def", &p])
.fail()
.lacks("clean")
.has(".yaml");
}
#[test]
fn new_also_hard_errors_on_an_unsupported_extension() {
let p = write_def_ext(
"wrongextnew",
"txt",
"\
name: t
initial: a
states:
a:
terminal: true
",
);
run(&["new", "--def", &p, "--id", "ext-reject"])
.fail()
.has(".yaml");
}