159 lines
4.8 KiB
Python
159 lines
4.8 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import math
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from funkyjuicerecipes.logic.calculations import (
|
||
|
|
compute_breakdown,
|
||
|
|
breakdown_for_view,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_readme_style_example_120ml_nic_in_pg():
|
||
|
|
bd = compute_breakdown(
|
||
|
|
size_ml=120,
|
||
|
|
base_pg_pct=30.0,
|
||
|
|
base_vg_pct=70.0,
|
||
|
|
nic_target_mg_per_ml=3.0,
|
||
|
|
nic_strength_mg_per_ml=100.0,
|
||
|
|
nic_base="PG",
|
||
|
|
flavors=[
|
||
|
|
{"name": "Vanilla", "base": "PG", "pct": 8.0},
|
||
|
|
("Strawberry", "VG", 4.0),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
|
||
|
|
# Percentages
|
||
|
|
assert bd.pg_pct == pytest.approx(19.0)
|
||
|
|
assert bd.vg_pct == pytest.approx(66.0)
|
||
|
|
assert bd.nic_pct == pytest.approx(3.0)
|
||
|
|
|
||
|
|
# mL calculations
|
||
|
|
assert bd.pg_ml == pytest.approx(22.8)
|
||
|
|
assert bd.vg_ml == pytest.approx(79.2)
|
||
|
|
assert bd.nic_ml == pytest.approx(3.6)
|
||
|
|
|
||
|
|
names = [f.name for f in bd.flavors]
|
||
|
|
assert names == ["Vanilla", "Strawberry"]
|
||
|
|
amounts = {f.name: f.ml for f in bd.flavors}
|
||
|
|
assert amounts["Vanilla"] == pytest.approx(9.6)
|
||
|
|
assert amounts["Strawberry"] == pytest.approx(4.8)
|
||
|
|
assert bd.flavor_prep_ml == pytest.approx(14.4)
|
||
|
|
|
||
|
|
# Rounded view (1 decimal) should match obvious roundings
|
||
|
|
v = breakdown_for_view(bd)
|
||
|
|
assert v.pg_ml == pytest.approx(22.8)
|
||
|
|
assert v.vg_ml == pytest.approx(79.2)
|
||
|
|
assert v.nic_ml == pytest.approx(3.6)
|
||
|
|
assert {f.name: f.ml for f in v.flavors}["Vanilla"] == pytest.approx(9.6)
|
||
|
|
assert {f.name: f.ml for f in v.flavors}["Strawberry"] == pytest.approx(4.8)
|
||
|
|
assert v.flavor_prep_ml == pytest.approx(14.4)
|
||
|
|
|
||
|
|
|
||
|
|
def test_nic_in_vg_changes_pg_vg_split():
|
||
|
|
bd = compute_breakdown(
|
||
|
|
size_ml=120,
|
||
|
|
base_pg_pct=30.0,
|
||
|
|
base_vg_pct=70.0,
|
||
|
|
nic_target_mg_per_ml=3.0,
|
||
|
|
nic_strength_mg_per_ml=100.0,
|
||
|
|
nic_base="VG",
|
||
|
|
flavors=[
|
||
|
|
{"name": "Vanilla", "base": "PG", "pct": 8.0},
|
||
|
|
("Strawberry", "VG", 4.0),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
assert bd.pg_pct == pytest.approx(22.0)
|
||
|
|
assert bd.vg_pct == pytest.approx(63.0)
|
||
|
|
assert bd.pg_ml == pytest.approx(26.4)
|
||
|
|
assert bd.vg_ml == pytest.approx(75.6)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize(
|
||
|
|
"base_pg,base_vg",
|
||
|
|
[
|
||
|
|
(70.0, 30.0),
|
||
|
|
(70.0000001, 29.9999999), # tolerance
|
||
|
|
],
|
||
|
|
)
|
||
|
|
def test_base_pg_vg_sum_to_100_with_tolerance(base_pg, base_vg):
|
||
|
|
bd = compute_breakdown(
|
||
|
|
size_ml=60,
|
||
|
|
base_pg_pct=base_pg,
|
||
|
|
base_vg_pct=base_vg,
|
||
|
|
nic_target_mg_per_ml=0.0,
|
||
|
|
nic_strength_mg_per_ml=100.0,
|
||
|
|
nic_base="PG",
|
||
|
|
flavors=[],
|
||
|
|
)
|
||
|
|
assert bd.pg_pct + bd.vg_pct + bd.nic_pct + sum(f.pct for f in bd.flavors) == pytest.approx(100.0)
|
||
|
|
|
||
|
|
|
||
|
|
def test_excessive_flavor_or_negative_pg_raises():
|
||
|
|
# This combination would push PG negative (base PG 10, nic 10 PG, flavor PG 5 => PG becomes -5)
|
||
|
|
with pytest.raises(ValueError):
|
||
|
|
compute_breakdown(
|
||
|
|
size_ml=30,
|
||
|
|
base_pg_pct=10.0,
|
||
|
|
base_vg_pct=90.0,
|
||
|
|
nic_target_mg_per_ml=10.0,
|
||
|
|
nic_strength_mg_per_ml=100.0,
|
||
|
|
nic_base="PG",
|
||
|
|
flavors=[("Whatever", "PG", 5.0)],
|
||
|
|
)
|
||
|
|
|
||
|
|
# Flavors sum > 100 should also fail via negative VG/PG after deduction
|
||
|
|
with pytest.raises(ValueError):
|
||
|
|
compute_breakdown(
|
||
|
|
size_ml=30,
|
||
|
|
base_pg_pct=50.0,
|
||
|
|
base_vg_pct=50.0,
|
||
|
|
nic_target_mg_per_ml=0.0,
|
||
|
|
nic_strength_mg_per_ml=100.0,
|
||
|
|
nic_base="PG",
|
||
|
|
flavors=[("A", "PG", 60.0), ("B", "VG", 50.0)],
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_input_variants_tuple_and_mapping_supported():
|
||
|
|
bd = compute_breakdown(
|
||
|
|
size_ml=10,
|
||
|
|
base_pg_pct=50.0,
|
||
|
|
base_vg_pct=50.0,
|
||
|
|
nic_target_mg_per_ml=0.0,
|
||
|
|
nic_strength_mg_per_ml=100.0,
|
||
|
|
nic_base="PG",
|
||
|
|
flavors=[
|
||
|
|
{"name": "X", "base": "PG", "pct": 5.0},
|
||
|
|
("Y", "VG", 5.0),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_50mg_stock_requires_more_nic_volume_and_changes_pg_vg_split():
|
||
|
|
# With 50 mg/mL stock, to reach 3 mg/mL target we need 6% nic volume.
|
||
|
|
bd = compute_breakdown(
|
||
|
|
size_ml=120,
|
||
|
|
base_pg_pct=30.0,
|
||
|
|
base_vg_pct=70.0,
|
||
|
|
nic_target_mg_per_ml=3.0,
|
||
|
|
nic_strength_mg_per_ml=50.0,
|
||
|
|
nic_base="PG",
|
||
|
|
flavors=[
|
||
|
|
{"name": "Vanilla", "base": "PG", "pct": 8.0},
|
||
|
|
("Strawberry", "VG", 4.0),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
# Now nic_pct (volume) is 6%
|
||
|
|
assert bd.nic_pct == pytest.approx(6.0)
|
||
|
|
# PG is further reduced by extra 3% vs the 100 mg/mL case
|
||
|
|
# Base pg 30 - flavor PG 8 - nic 6 = 16
|
||
|
|
assert bd.pg_pct == pytest.approx(16.0)
|
||
|
|
# VG remains 70 - flavor VG 4 = 66
|
||
|
|
assert bd.vg_pct == pytest.approx(66.0)
|
||
|
|
# mL
|
||
|
|
assert bd.nic_ml == pytest.approx(7.2) # 6% of 120
|
||
|
|
assert bd.pg_ml == pytest.approx(19.2) # 16% of 120
|
||
|
|
assert bd.vg_ml == pytest.approx(79.2) # 66% of 120
|
||
|
|
assert {f.name for f in bd.flavors} == {"Vanilla", "Strawberry"}
|