#![allow(clippy::too_many_lines, clippy::semicolon_if_nothing_returned)]
use composer_semver::Stability;
use composer_semver::constraint::Constraint;
use composer_semver::version::{CmpOp, Version};
use serde::Deserialize;
const FIXTURE: &str = include_str!("data/conformance.json");
#[derive(Debug, Deserialize)]
struct Fixture {
source: Source,
suites: Vec<Suite>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Source {
repo: String,
commit: String,
}
#[derive(Debug, Deserialize)]
struct Suite {
class: String,
method: String,
cases: Vec<Vec<serde_json::Value>>,
}
#[test]
fn fixture_is_well_formed() {
let fx: Fixture = serde_json::from_str(FIXTURE).expect("conformance.json is valid JSON");
assert!(
!fx.source.commit.is_empty(),
"fixture must record upstream commit"
);
assert!(!fx.suites.is_empty(), "fixture must have suites");
for s in &fx.suites {
assert!(
!s.cases.is_empty(),
"suite {}::{} is empty — port script regression?",
s.class,
s.method
);
}
}
#[test]
fn upstream_conformance() {
let fx: Fixture = serde_json::from_str(FIXTURE).unwrap();
let mut covered = 0usize;
let mut skipped = 0usize;
let mut failures: Vec<String> = vec![];
for suite in &fx.suites {
let dispatched = dispatch(suite, &mut covered, &mut failures);
if !dispatched {
skipped += suite.cases.len();
}
}
if !failures.is_empty() {
let show: Vec<String> = failures.iter().take(40).cloned().collect();
let extra = failures.len().saturating_sub(show.len());
panic!(
"{} conformance failures (covered {covered}, skipped {skipped}){}:\n {}",
failures.len(),
if extra > 0 {
format!(", showing first {}", show.len())
} else {
String::new()
},
show.join("\n ")
);
}
eprintln!("conformance: covered={covered} skipped={skipped}");
}
fn dispatch(suite: &Suite, covered: &mut usize, failures: &mut Vec<String>) -> bool {
let id = format!("{}::{}", suite.class, suite.method);
match suite.method.as_str() {
"isValidVersions" => {
for case in &suite.cases {
let input = case[0].as_str().unwrap();
let expected = case[1].as_bool().unwrap();
let actual = Version::parse(input).is_ok();
if actual != expected {
failures.push(format!(
"{id}: input={input:?} expected={expected} actual={actual}"
));
}
*covered += 1;
}
true
}
"successfulNormalizedVersions" => {
for case in &suite.cases {
let input = case[0].as_str().unwrap();
let expected = case[1].as_str().unwrap();
match Version::parse(input) {
Ok(v) if v.normalized == expected => {}
Ok(v) => failures.push(format!(
"{id}: input={input:?} expected={expected:?} actual={:?}",
v.normalized
)),
Err(e) => failures.push(format!(
"{id}: input={input:?} expected={expected:?} parse_err={e}"
)),
}
*covered += 1;
}
true
}
"failingNormalizedVersions" => {
for case in &suite.cases {
let input = case[0].as_str().unwrap();
if let Ok(v) = Version::parse(input) {
failures.push(format!(
"{id}: input={input:?} should fail but parsed as {:?}",
v.normalized
));
}
*covered += 1;
}
true
}
"successfulNormalizedBranches" => {
for case in &suite.cases {
let input = case[0].as_str().unwrap();
let expected = case[1].as_str().unwrap();
let actual = Version::normalize_branch(input);
if actual != expected {
failures.push(format!(
"{id}: input={input:?} expected={expected:?} actual={actual:?}"
));
}
*covered += 1;
}
true
}
"numericAliasVersions" => {
for case in &suite.cases {
let input = case[0].as_str().unwrap();
let actual = Version::parse_numeric_alias_prefix(input);
let expected: Option<String> = match &case[1] {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Bool(false) => None,
other => panic!("{id}: unexpected expected value {other:?}"),
};
if actual != expected {
failures.push(format!(
"{id}: input={input:?} expected={expected:?} actual={actual:?}"
));
}
*covered += 1;
}
true
}
"stabilityProvider" => {
for case in &suite.cases {
let expected = case[0].as_str().unwrap();
let input = case[1].as_str().unwrap();
let actual = Version::parse_stability(input).as_str();
let actual_lower = actual.to_ascii_lowercase();
let expected_lower = expected.to_ascii_lowercase();
if actual_lower != expected_lower {
failures.push(format!(
"{id}: input={input:?} expected={expected:?} actual={actual:?}"
));
}
*covered += 1;
}
true
}
"compareProvider" => {
for case in &suite.cases {
let a = case[0].as_str().unwrap();
let op = case[1].as_str().unwrap();
let b = case[2].as_str().unwrap();
let expected = case[3].as_bool().unwrap();
let actual = compare_op(a, op, b);
if actual != expected {
failures.push(format!(
"{id}: {a:?} {op} {b:?} expected={expected} actual={actual}"
));
}
*covered += 1;
}
true
}
"greaterThanProvider" => binary_op(suite, ">", covered, failures),
"greaterThanOrEqualToProvider" => binary_op(suite, ">=", covered, failures),
"lessThanProvider" => binary_op(suite, "<", covered, failures),
"lessThanOrEqualToProvider" => binary_op(suite, "<=", covered, failures),
"equalToProvider" => binary_op(suite, "==", covered, failures),
"notEqualToProvider" => binary_op(suite, "!=", covered, failures),
"sortProvider" => {
for case in &suite.cases {
let input: Vec<String> = case[0]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_owned())
.collect();
let want_asc: Vec<String> = case[1]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_owned())
.collect();
let want_desc: Vec<String> = case[2]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_owned())
.collect();
if let Some(got) = sort_versions(&input, true)
&& got != want_asc
{
failures.push(format!(
"{id}: sort_asc({input:?}) expected={want_asc:?} actual={got:?}"
));
}
if let Some(got) = sort_versions(&input, false)
&& got != want_desc
{
failures.push(format!(
"{id}: sort_desc({input:?}) expected={want_desc:?} actual={got:?}"
));
}
*covered += 2;
}
true
}
"satisfiesProviderPositive" => {
for case in &suite.cases {
let v = case[0].as_str().unwrap();
let c = case[1].as_str().unwrap();
match (Version::parse(v), Constraint::parse(c)) {
(Ok(version), Ok(constraint)) => {
if !constraint.matches(&version) {
failures.push(format!("{id}: {v:?} should satisfy {c:?} but doesn't"));
}
}
(Err(e), _) => {
failures.push(format!("{id}: {v:?} {c:?} version parse err: {e}"))
}
(_, Err(e)) => {
failures.push(format!("{id}: {v:?} {c:?} constraint parse err: {e}"))
}
}
*covered += 1;
}
true
}
"satisfiesProviderNegative" => {
for case in &suite.cases {
let v = case[0].as_str().unwrap();
let c = case[1].as_str().unwrap();
match (Version::parse(v), Constraint::parse(c)) {
(Ok(version), Ok(constraint)) => {
if constraint.matches(&version) {
failures.push(format!("{id}: {v:?} should NOT satisfy {c:?} but does"));
}
}
(Err(e), _) => {
failures.push(format!("{id}: {v:?} {c:?} version parse err: {e}"))
}
(_, Err(e)) => {
failures.push(format!("{id}: {v:?} {c:?} constraint parse err: {e}"))
}
}
*covered += 1;
}
true
}
_ => false,
}
}
fn binary_op(suite: &Suite, op: &str, covered: &mut usize, failures: &mut Vec<String>) -> bool {
let id = format!("{}::{}", suite.class, suite.method);
for case in &suite.cases {
let a = case[0].as_str().unwrap();
let b = case[1].as_str().unwrap();
let expected = case[2].as_bool().unwrap();
let actual = compare_op(a, op, b);
if actual != expected {
failures.push(format!(
"{id}: {a:?} {op} {b:?} expected={expected} actual={actual}"
));
}
*covered += 1;
}
true
}
fn compare_op(a: &str, op: &str, b: &str) -> bool {
let (Ok(va), Ok(vb)) = (Version::parse(a), Version::parse(b)) else {
return false;
};
let cmp_op = match op {
">" => CmpOp::Gt,
">=" => CmpOp::Ge,
"<" => CmpOp::Lt,
"<=" => CmpOp::Le,
"==" | "=" => CmpOp::Eq,
"!=" | "<>" => CmpOp::Ne,
_ => panic!("unknown op {op}"),
};
va.compare(cmp_op, &vb)
}
fn sort_versions(input: &[String], ascending: bool) -> Option<Vec<String>> {
let mut parsed: Vec<(String, Version)> = Vec::with_capacity(input.len());
for s in input {
let v = Version::parse(s).ok()?;
let n = Version::normalize_default_branch(&v.normalized);
let v2 = Version::parse(&n).ok()?;
parsed.push((s.clone(), v2));
}
parsed.sort_by(|a, b| {
if ascending {
a.1.cmp(&b.1)
} else {
b.1.cmp(&a.1)
}
});
Some(parsed.into_iter().map(|(s, _)| s).collect())
}
#[allow(dead_code)]
const _STABILITY_KEEP_IMPORT: Option<Stability> = None;