name = "num-rational"
description = "Rational numbers (Ratio<T>) — ETNA workload."
language = "rust"
strategies = ["proptest", "quickcheck", "crabcheck", "hegel"]
crate = "num-rational"
base_commit = "4d55ad22ac86ebbc4cb45d79a956e4a1f7af57d1"

[[tasks]]
mutations = ["recip_zero_panic_and_sign_norm_8c75506_1"]

[tasks.source]
repo = "https://github.com/rust-num/num-rational"
commits = ["8c75506f2275c215d45dcccf52340da8717c6fa5"]
commit_subjects = ["Ratio::recip now corrects sign and fails for zero"]
summary = "Pre-fix `Ratio::recip` simply swapped numerator and denominator with `Ratio::new_raw(self.denom, self.numer)`. That returns `1/0` (a malformed ratio) when called on zero rather than panicking, and leaves the denominator negative for negative inputs (`(-1/2).recip()` produces `2/-1` instead of `-2/1`)."

[tasks.injection]
kind = "patch"
patch = "patches/recip_zero_panic_and_sign_norm_8c75506_1.patch"
files = ["src/lib.rs"]
locations = [{ file = "src/lib.rs", symbol = "Ratio::into_recip" }]

[tasks.bug]
short_name = "recip_zero_panic_and_sign_norm"
invariant = "`Ratio::recip` must panic when called on a zero ratio (no meaningful reciprocal exists) and must return a reciprocal whose denominator is positive — i.e. `recip` is total over nonzero ratios and produces a normalized result."
how_triggered = "The buggy body is `Ratio::new_raw(self.denom, self.numer)`. For `Ratio::new(0, 1)` this returns `1/0` and never panics, violating the panic-on-zero contract. For `Ratio::new(-1, 2)` this returns `2/-1`, leaving the sign on the denominator."

[[tasks.tasks]]
property = "RecipZeroPanicAndSignNorm"
witnesses = [
  { test_fn = "witness_recip_zero_panic_and_sign_norm_case_zero_panics" },
  { test_fn = "witness_recip_zero_panic_and_sign_norm_case_neg_one_half" },
]

[[tasks]]
mutations = ["is_pos_neg_zero_excluded_c22e3bf_1"]

[tasks.source]
repo = "https://github.com/rust-num/num-rational"
commits = ["c22e3bf9a2733274a2a6d422cbd35b5a135d63ff"]
commit_subjects = ["Correct Ratio::is_negative and Ratio::is_positive"]
summary = "Pre-fix `Signed::is_positive` for `Ratio<T>` was implemented as `!self.is_negative()` and `is_negative` as `self.numer.is_negative() ^ self.denom.is_negative()`. That made zero report as positive (because zero is also not negative), violating the contract that zero is neither positive nor negative."

[tasks.injection]
kind = "patch"
patch = "patches/is_pos_neg_zero_excluded_c22e3bf_1.patch"
files = ["src/lib.rs"]
locations = [{ file = "src/lib.rs", symbol = "Signed::is_positive" }]

[tasks.bug]
short_name = "is_pos_neg_zero_excluded"
invariant = "For `Ratio<i32>`, both `is_positive` and `is_negative` return `false` on zero, agreeing with the integer `Signed` impl. For nonzero values exactly one of the two returns `true`."
how_triggered = "The buggy `is_positive` body is `!self.is_negative()`. On zero, `is_negative` returns `false`, so `is_positive` returns `true` — the property catches this by checking the canonical zero case `Ratio::new(0, 1)`."

[[tasks.tasks]]
property = "IsPosNegZeroExcluded"
witnesses = [
  { test_fn = "witness_is_pos_neg_zero_excluded_case_zero" },
  { test_fn = "witness_is_pos_neg_zero_excluded_case_neg_one_half" },
]

[[tasks]]
mutations = ["cmp_zero_numer_equal_e10ca81_1"]

[tasks.source]
repo = "https://github.com/rust-num/num-rational"
commits = ["e10ca814e7b1f4d5fcc42ed5e7ceb39f8443ef5c"]
commit_subjects = ["Fix #49"]
summary = "Pre-fix `Ord::cmp` had a special case for equal numerators that compared denominators inversely. It did not special-case zero numerators, so two zero ratios with distinct denominators (e.g. `Ratio::new_raw(0, 1)` and `Ratio::new_raw(0, 2)`) were reported as unequal."

[tasks.injection]
kind = "patch"
patch = "patches/cmp_zero_numer_equal_e10ca81_1.patch"
files = ["src/lib.rs"]
locations = [{ file = "src/lib.rs", symbol = "Ratio::cmp" }]

[tasks.bug]
short_name = "cmp_zero_numer_equal"
invariant = "Two `Ratio` values with equal numerators that are zero compare `Equal`, regardless of their denominators."
how_triggered = "The buggy body removes the `if self.numer.is_zero() { return Equal; }` guard. For `Ratio::new_raw(0, 1).cmp(&Ratio::new_raw(0, 2))` the equal-numerators branch falls through to the denominator comparison and reports `Greater` (since for nonneg numerators the denominators are reverse-compared)."

[[tasks.tasks]]
property = "CmpZeroNumerEqual"
witnesses = [
  { test_fn = "witness_cmp_zero_numer_equal_case_one_two" },
  { test_fn = "witness_cmp_zero_numer_equal_case_three_five" },
]

[[tasks]]
mutations = ["cmp_overflow_safe_4e66bbe_1"]

[tasks.source]
repo = "https://github.com/rust-num/num-rational"
commits = ["4e66bbe6a7560c788ec94369e33decc7d3d41648"]
commit_subjects = ["Avoid overflows in Ratio's Ord::cmp"]
summary = "Pre-fix `Ord::cmp`, `PartialOrd`, `PartialEq`, and `Eq` were generated by a `cmp_impl!` macro that compared `a*d` with `b*c`. For fixed-size integer types like `i8` the cross product silently overflows and produces the wrong ordering — issue #7 demonstrated the bug with `128/1` vs `1/128`."

[tasks.injection]
kind = "patch"
files = ["src/lib.rs"]
locations = [{ file = "src/lib.rs", symbol = "Ratio::cmp" }]
patch = "patches/cmp_overflow_safe_4e66bbe_1.patch"

[tasks.bug]
short_name = "cmp_overflow_safe"
invariant = "`Ratio::cmp` on `Ratio<i8>` must agree with the mathematically correct ordering of the rationals for every quadruple `(a, b, c, d): i8` with `b != 0`, `d != 0`, none equal to `i8::MIN`. In particular, comparing `127/1` with `1/127` must return `Greater`."
how_triggered = "The buggy body is `(self.numer * other.denom).cmp(&(self.denom * other.numer))`. For `i8` inputs whose cross product exceeds `i8::MAX` the multiplication overflows, panics in debug mode, and silently wraps in release mode."

[[tasks.tasks]]
property = "CmpOverflowSafe"
witnesses = [
  { test_fn = "witness_cmp_overflow_safe_case_big_vs_small" },
  { test_fn = "witness_cmp_overflow_safe_case_close_pairs" },
]

[[dropped]]
commit = "33bbb54"
subject = "Fix CheckedDiv with zeros"
reason = "surface refactored: 6903b6b's `if self.denom == rhs.denom` short-circuit makes the buggy 0/0 path return None even without the `is_zero` guard, so reverting only the guard does not yield observable failure under `Ratio::new`."
