Spaces:
Running
Running
| """ | |
| Day 1 verification tests β checks our lye calculator against HSCG/SoapCalc. | |
| Run: python test_chemistry.py | |
| All tests should pass before any AI layer is added. | |
| """ | |
| import sys | |
| from chemistry import ( | |
| calculate_lye, score_recipe, find_substitutes, | |
| cure_timeline, profile_summary, list_oils, OILS | |
| ) | |
| PASS = "β " | |
| FAIL = "β" | |
| results = [] | |
| def check(label: str, condition: bool, detail: str = ""): | |
| status = PASS if condition else FAIL | |
| results.append((status, label, detail)) | |
| print(f"{status} {label}" + (f" β {detail}" if detail else "")) | |
| # --------------------------------------------------------------------------- | |
| # 1. SAP database sanity checks | |
| # --------------------------------------------------------------------------- | |
| print("\nββ Oil database ββ") | |
| check("Oil count >= 40", len(OILS) >= 40, f"got {len(OILS)}") | |
| check("All oils have SAP range", all( | |
| o.naoh_sap_low > 0 and o.naoh_sap_high >= o.naoh_sap_low | |
| for o in OILS.values() | |
| ), "") | |
| check("All SAP averages in realistic range (0.06β0.21)", all( | |
| 0.06 <= o.naoh_sap <= 0.21 for o in OILS.values() | |
| ), "") | |
| check("Vegan flags set correctly", ( | |
| not OILS["lard"].vegan and | |
| not OILS["tallow (beef)"].vegan and | |
| OILS["coconut oil"].vegan and | |
| OILS["olive oil"].vegan | |
| ), "") | |
| check("KOH SAP = NaOH * 1.403 (Β±0.002)", all( | |
| abs(o.koh_sap - round(o.naoh_sap * 1.403, 4)) <= 0.002 | |
| for o in OILS.values() | |
| ), "") | |
| # --------------------------------------------------------------------------- | |
| # 2. Lye calculator β test against manually verified recipes | |
| # --------------------------------------------------------------------------- | |
| print("\nββ Lye calculator ββ") | |
| # Recipe 1: Classic beginner β 500g, 30% coconut / 70% olive, 5% superfat | |
| # Expected NaOH: coconut 150g * 0.1915 = 28.73 + olive 350g * 0.135 = 47.25 β 75.98 * 0.95 = ~72.2g | |
| recipe1 = {"coconut oil": 150, "olive oil": 350} | |
| r1 = calculate_lye(recipe1, lye_type="naoh", superfat=5, water_ratio=0.38) | |
| check("Recipe 1: no error", "error" not in r1, "") | |
| check("Recipe 1: total oil = 500g", r1["total_oil_weight_g"] == 500, f"got {r1.get('total_oil_weight_g')}") | |
| check("Recipe 1: NaOH in expected range (70β76g)", 70 <= r1.get("naoh_g", 0) <= 76, | |
| f"got {r1.get('naoh_g')}g") | |
| check("Recipe 1: water = 190g", r1["water_g"] == 190, f"got {r1.get('water_g')}g") | |
| # Recipe 2: Tallow-based β 400g tallow / 100g coconut, 0% superfat | |
| # Tallow SAP ~0.1405, coconut ~0.1815 | |
| # Expected: 400*0.1405 + 100*0.1815 = 56.2 + 18.15 = 74.35g NaOH (no superfat discount) | |
| recipe2 = {"tallow (beef)": 400, "coconut oil": 100} | |
| r2 = calculate_lye(recipe2, lye_type="naoh", superfat=0, water_ratio=0.38) | |
| check("Recipe 2 (tallow): NaOH in range (72β77g)", 72 <= r2.get("naoh_g", 0) <= 77, | |
| f"got {r2.get('naoh_g')}g") | |
| # Recipe 3: Lard-based β 500g lard, 5% superfat | |
| # Lard SAP ~0.1375, * 500 * 0.95 = ~65.3g | |
| recipe3 = {"lard": 500} | |
| r3 = calculate_lye(recipe3, lye_type="naoh", superfat=5) | |
| check("Recipe 3 (lard): NaOH in range (62β68g)", 62 <= r3.get("naoh_g", 0) <= 68, | |
| f"got {r3.get('naoh_g')}g") | |
| # Recipe 4: KOH for liquid soap β 500g olive oil | |
| recipe4 = {"olive oil": 500} | |
| r4 = calculate_lye(recipe4, lye_type="koh", superfat=0, koh_purity=90) | |
| check("Recipe 4 (KOH): KOH > NaOH equivalent", r4.get("koh_g", 0) > 0, f"got {r4.get('koh_g')}g") | |
| check("Recipe 4 (KOH): in expected range (100β110g at 90% purity)", 100 <= r4.get("koh_g", 0) <= 110, | |
| f"got {r4.get('koh_g')}g") | |
| # Recipe 5: Unknown oil β should return error | |
| r5 = calculate_lye({"dragon oil": 500}) | |
| check("Recipe 5: unknown oil returns error", "error" in r5, r5.get("error", "")) | |
| # --------------------------------------------------------------------------- | |
| # 3. Soap quality scorer | |
| # --------------------------------------------------------------------------- | |
| print("\nββ Quality scorer ββ") | |
| # Standard coconut/olive is a known recipe β coconut pushes hardness + cleansing up | |
| scores1 = score_recipe({"coconut oil": 300, "olive oil": 200}) | |
| check("Scorer: coconut/olive β hardness score returned", "hardness" in scores1, "") | |
| check("Scorer: coconut/olive β cleansing > 12 (coconut should dominate)", | |
| scores1.get("cleansing", {}).get("value", 0) > 12, | |
| f"got {scores1.get('cleansing', {}).get('value')}") | |
| # All-olive β known to be low cleansing, high conditioning, very high iodine | |
| scores2 = score_recipe({"olive oil": 500}) | |
| check("Scorer: all-olive β conditioning high (>70)", | |
| scores2.get("conditioning", {}).get("value", 0) > 70, | |
| f"got {scores2.get('conditioning', {}).get('value')}") | |
| check("Scorer: all-olive β cleansing low (<12, status=low)", | |
| scores2.get("cleansing", {}).get("status") == "low", | |
| f"got {scores2.get('cleansing', {}).get('status')}") | |
| check("Scorer: all-olive β iodine high (>70, status=high)", | |
| scores2.get("iodine_value", {}).get("status") == "high", | |
| f"got {scores2.get('iodine_value', {}).get('value')}") | |
| # --------------------------------------------------------------------------- | |
| # 4. Substitution finder | |
| # --------------------------------------------------------------------------- | |
| print("\nββ Substitution finder ββ") | |
| # Palm oil substitute β should find tallow, lard (similar palmitic/stearic ratio) | |
| subs = find_substitutes("palm oil", n=3) | |
| check("Substitution: palm oil returns 3 results", len(subs) == 3, "") | |
| sub_names = [s["name"].lower() for s in subs] | |
| check("Substitution: palm oil β tallow or lard in top 3", | |
| any("tallow" in n or "lard" in n for n in sub_names), | |
| f"got: {[s['name'] for s in subs]}") | |
| # Coconut oil vegan substitute | |
| subs_vegan = find_substitutes("coconut oil", n=3, vegan_only=True) | |
| check("Substitution: vegan=True excludes non-vegan oils", | |
| all(s["vegan"] for s in subs_vegan), | |
| f"got: {[s['name'] for s in subs_vegan]}") | |
| # Palm kernel should be closest to coconut | |
| subs_coco = find_substitutes("coconut oil", n=5) | |
| top_names = [s["name"].lower() for s in subs_coco[:2]] | |
| check("Substitution: coconut β palm kernel or babassu in top 2", | |
| any("palm kernel" in n or "babassu" in n for n in top_names), | |
| f"got: {[s['name'] for s in subs_coco[:2]]}") | |
| # --------------------------------------------------------------------------- | |
| # 5. Cure timeline | |
| # --------------------------------------------------------------------------- | |
| print("\nββ Cure timeline ββ") | |
| # All-olive β should recommend long cure (8 weeks) | |
| cure_olive = cure_timeline({"olive oil": 500}) | |
| check("Cure: all-olive recommends 8 weeks", cure_olive.get("recommended_cure_weeks") == 8, | |
| f"got {cure_olive.get('recommended_cure_weeks')}") | |
| check("Cure: all-olive has 8 weekly notes", len(cure_olive.get("weekly_notes", {})) == 8, "") | |
| # Coconut-heavy β should recommend shorter cure (4 weeks) | |
| cure_coco = cure_timeline({"coconut oil": 400, "olive oil": 100}) | |
| check("Cure: coconut-heavy recommends β€5 weeks", | |
| cure_coco.get("recommended_cure_weeks", 99) <= 5, | |
| f"got {cure_coco.get('recommended_cure_weeks')}") | |
| # --------------------------------------------------------------------------- | |
| # 6. Profile summary | |
| # --------------------------------------------------------------------------- | |
| print("\nββ Profile summary ββ") | |
| prof = profile_summary({"lard": 300, "coconut oil": 150, "olive oil": 50}) | |
| check("Profile: is_vegan=False when lard present", prof.get("is_vegan") == False, "") | |
| check("Profile: lard in non_vegan_oils", "Lard" in prof.get("non_vegan_oils", []), "") | |
| check("Profile: total_weight correct", prof.get("total_weight_g") == 500, | |
| f"got {prof.get('total_weight_g')}") | |
| # --------------------------------------------------------------------------- | |
| # 7. Helper functions | |
| # --------------------------------------------------------------------------- | |
| print("\nββ Helper functions ββ") | |
| all_oils = list_oils() | |
| check("list_oils: returns >= 40 keys", len(all_oils) >= 40, f"got {len(all_oils)}") | |
| vegan_oils = list_oils(vegan_only=True) | |
| check("list_oils vegan_only: excludes lard", "lard" not in vegan_oils, "") | |
| check("list_oils vegan_only: excludes tallow", "tallow (beef)" not in vegan_oils, "") | |
| check("list_oils vegan_only: includes coconut", "coconut oil" in vegan_oils, "") | |
| # --------------------------------------------------------------------------- | |
| # Summary | |
| # --------------------------------------------------------------------------- | |
| print("\n" + "="*50) | |
| passed = sum(1 for s, _, _ in results if s == PASS) | |
| failed = sum(1 for s, _, _ in results if s == FAIL) | |
| print(f"Results: {passed} passed, {failed} failed out of {len(results)} tests") | |
| if failed > 0: | |
| print("\nFailed tests:") | |
| for s, label, detail in results: | |
| if s == FAIL: | |
| print(f" {FAIL} {label}" + (f" β {detail}" if detail else "")) | |
| sys.exit(1) | |
| else: | |
| print("All tests passed. Chemistry engine is ready. β ") | |