max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+98 insertions,
-0 deletions
| @@ -1217,6 +1217,104 @@ | |||
| 1217 | 1217 | } | |
| 1218 | 1218 | } | |
| 1219 | 1219 | ||
| 1220 | + | // ── Metamorphic: applying a discount and removing it again ─────────────── | |
| 1221 | + | // | |
| 1222 | + | // Wiki `testing-posture`, phase 2. A metamorphic relation states how two | |
| 1223 | + | // runs relate rather than what either returns, so it needs no table of | |
| 1224 | + | // expected values and nothing has to be recomputed by hand when a price | |
| 1225 | + | // changes. See Chen et al. 1998. | |
| 1226 | + | // | |
| 1227 | + | // The relation asked for was "apply then remove is the identity". It is, | |
| 1228 | + | // for Fixed, and it is not for Percentage, because integer cents rounding | |
| 1229 | + | // is not invertible. What replaces it is not a tolerance: the loss is an | |
| 1230 | + | // exact quantity and the tests below pin it as one. | |
| 1231 | + | ||
| 1232 | + | proptest::proptest! { | |
| 1233 | + | /// Fixed is invertible wherever it does not clamp: the discount is a | |
| 1234 | + | /// subtraction, and adding it back is the inverse of subtracting it. | |
| 1235 | + | #[test] | |
| 1236 | + | fn prop_removing_a_fixed_discount_restores_the_price_exactly( | |
| 1237 | + | price in 0..=1_000_000i32, | |
| 1238 | + | discount in 0..=1_000_000i32, | |
| 1239 | + | ) { | |
| 1240 | + | proptest::prop_assume!(discount <= price); | |
| 1241 | + | let discounted = apply_discount(price, DiscountType::Fixed, discount); | |
| 1242 | + | proptest::prop_assert_eq!(discounted + discount, price); | |
| 1243 | + | } | |
| 1244 | + | ||
| 1245 | + | /// Above the price it is not invertible, and that is the intended | |
| 1246 | + | /// behaviour rather than a gap: the clamp to zero is what stops a | |
| 1247 | + | /// generous coupon paying the buyer. Every price at or below the | |
| 1248 | + | /// discount collapses to the same 0, so no inverse can tell them apart. | |
| 1249 | + | #[test] | |
| 1250 | + | fn prop_a_fixed_discount_over_the_price_destroys_it( | |
| 1251 | + | price in 0..=1_000_000i32, | |
| 1252 | + | excess in 0..=1_000_000i32, | |
| 1253 | + | ) { | |
| 1254 | + | let discount = price.saturating_add(excess); | |
| 1255 | + | proptest::prop_assert_eq!(apply_discount(price, DiscountType::Fixed, discount), 0); | |
| 1256 | + | } | |
| 1257 | + | ||
| 1258 | + | /// What a percentage discount loses, stated exactly. | |
| 1259 | + | /// | |
| 1260 | + | /// `apply_discount` computes `price - (price * pct) / 100` with integer | |
| 1261 | + | /// division, so writing `price * pct = 100q + r` with `0 <= r < 100` | |
| 1262 | + | /// gives `discounted * 100 = price * (100 - pct) + r`. The remainder `r` | |
| 1263 | + | /// is the whole of the round-trip loss and it is bounded by 100 | |
| 1264 | + | /// regardless of how large the price is. | |
| 1265 | + | /// | |
| 1266 | + | /// That identity is the tolerance the task asked to have pinned, and it | |
| 1267 | + | /// is worth having as an equation rather than an epsilon: the error does | |
| 1268 | + | /// not grow with the price, so a $10,000 sale is no less recoverable | |
| 1269 | + | /// than a $1 one. | |
| 1270 | + | #[test] | |
| 1271 | + | fn prop_a_percentage_discount_loses_exactly_the_rounding_remainder( | |
| 1272 | + | price in 0..=1_000_000i32, | |
| 1273 | + | pct in 0..=100i32, | |
| 1274 | + | ) { | |
| 1275 | + | let discounted = apply_discount(price, DiscountType::Percentage, pct); | |
| 1276 | + | let remainder = i64::from(discounted) * 100 - i64::from(price) * i64::from(100 - pct); | |
| 1277 | + | proptest::prop_assert!( | |
| 1278 | + | (0..100).contains(&remainder), | |
| 1279 | + | "price={} pct={} discounted={} left remainder {}, outside [0, 100)", | |
| 1280 | + | price, pct, discounted, remainder, | |
| 1281 | + | ); | |
| 1282 | + | proptest::prop_assert_eq!( | |
| 1283 | + | remainder, | |
| 1284 | + | (i64::from(price) * i64::from(pct)) % 100, | |
| 1285 | + | "the remainder is not the one integer division dropped", | |
| 1286 | + | ); | |
| 1287 | + | } | |
| 1288 | + | ||
| 1289 | + | /// So removal is exact exactly when nothing was dropped, which is when | |
| 1290 | + | /// 100 divides `price * pct`. Constructing such a price is the point: | |
| 1291 | + | /// this is the half of the original relation that does survive. | |
| 1292 | + | #[test] | |
| 1293 | + | fn prop_removing_a_percentage_discount_is_exact_when_it_divides_evenly( | |
| 1294 | + | hundreds in 0..=10_000i32, | |
| 1295 | + | pct in 0..=99i32, | |
| 1296 | + | ) { | |
| 1297 | + | let price = hundreds * 100; | |
| 1298 | + | let discounted = apply_discount(price, DiscountType::Percentage, pct); | |
| 1299 | + | // No remainder, so the inverse is the plain rational one. | |
| 1300 | + | proptest::prop_assert_eq!( | |
| 1301 | + | i64::from(discounted) * 100 / i64::from(100 - pct), | |
| 1302 | + | i64::from(price), | |
| 1303 | + | ); | |
| 1304 | + | } | |
| 1305 | + | } | |
| 1306 | + | ||
| 1307 | + | /// The one case where the relation cannot hold however the price is chosen. | |
| 1308 | + | /// A full discount maps every price to 0, so removal has nothing to work | |
| 1309 | + | /// from. Worth a named test rather than an `prop_assume!` that quietly skips | |
| 1310 | + | /// it, since "free" is a real configuration and not an edge. | |
| 1311 | + | #[test] | |
| 1312 | + | fn removing_a_full_discount_is_impossible_by_construction() { | |
| 1313 | + | for price in [0, 1, 99, 100, 101, 999, 1_000_000] { | |
| 1314 | + | assert_eq!(apply_discount(price, DiscountType::Percentage, 100), 0); | |
| 1315 | + | } | |
| 1316 | + | } | |
| 1317 | + | ||
| 1220 | 1318 | // Cart promo semantics: one redemption = one use (ultra-fuzz Run 10 Pay S1) | |
| 1221 | 1319 | ||
| 1222 | 1320 | /// Build a percentage-discount promo with no scope/min-price gating. |