CoolProp 8.0.0
An open-source fluid property and humid air property database
CoolProp-Tests-PropsSIOptions.cpp
Go to the documentation of this file.
1// Catch2 tests for the `?<options>` suffix flowing through the
2// high-level CoolProp API (PropsSI, Props1SI, PhaseSI). No backend
3// opts in to options in PR A; these tests therefore verify that:
4//
5// * empty / no-options strings work identically with or without the
6// `?` suffix (the suffix is parsed and dropped without affecting
7// the call);
8// * `?{...}` options on a backend that hasn't opted in throw a
9// clear NotImplementedError instead of being silently dropped.
10//
11// When PR B (SVDSBTL opt-in) lands, this file gains positive tests
12// asserting that the SVDSBTL backend respects the supplied options.
13// CoolProp-iqz.
14
15#if defined(ENABLE_CATCH)
16
17# include <catch2/catch_all.hpp>
18
19# include <cmath>
20# include <filesystem>
21# include <fstream>
22# include <string>
23
25# include "CoolProp/CoolProp.h"
26# include "TestUtils.h"
27
28namespace {
29
30class TempJSONFile
31{
32 public:
33 explicit TempJSONFile(const std::string& contents) {
34 path_ = std::filesystem::temp_directory_path()
35 / ("cp_propssi_opts_test_" + std::to_string(CoolProp::tests::test_pid()) + "_" + std::to_string(counter_++) + ".json");
36 std::ofstream(path_) << contents;
37 }
38 ~TempJSONFile() {
39 std::error_code ec;
40 std::filesystem::remove(path_, ec);
41 }
42 TempJSONFile(const TempJSONFile&) = delete;
43 TempJSONFile& operator=(const TempJSONFile&) = delete;
44 TempJSONFile(TempJSONFile&&) = delete;
45 TempJSONFile& operator=(TempJSONFile&&) = delete;
46
47 [[nodiscard]] std::string path() const {
48 return path_.string();
49 }
50
51 private:
52 std::filesystem::path path_;
53 static inline int counter_ = 0;
54};
55
56} // namespace
57
58TEST_CASE("PropsSI: '?<options>' suffix on fluid string parses + dispatches", "[FactoryOptions][PropsSI]") {
59 const double T = 300.0;
60 const double p = 101325.0;
61 const double rho_ref = CoolProp::PropsSI("D", "T", T, "P", p, "HEOS::Water");
62 REQUIRE(std::isfinite(rho_ref));
63
64 SECTION("bare '?' is a no-op (empty options)") {
65 const double rho = CoolProp::PropsSI("D", "T", T, "P", p, "HEOS::Water?");
66 REQUIRE(rho == Catch::Approx(rho_ref));
67 }
68 SECTION("empty JSON object is a no-op") {
69 const double rho = CoolProp::PropsSI("D", "T", T, "P", p, "HEOS::Water?{}");
70 REQUIRE(rho == Catch::Approx(rho_ref));
71 }
72 // Trailing-whitespace tail is exercised by parse_factory_options
73 // directly; PropsSI's string path normalises whitespace separately,
74 // so don't double-test that integration here.
75}
76
77TEST_CASE("PropsSI: non-empty options on opted-out backend errors gracefully", "[FactoryOptions][PropsSI]") {
78 // HEOS does not (yet) opt in to options; non-empty payload must
79 // surface as a non-finite result, not a silent success at the
80 // default settings. PropsSI catches the exception and returns
81 // HUGE_VAL; the errstring will carry the original message.
82 const double rho = CoolProp::PropsSI("D", "T", 300.0, "P", 101325.0, R"(HEOS::Water?{"key":1})");
83 REQUIRE_FALSE(std::isfinite(rho));
84 const std::string err = CoolProp::get_global_param_string("errstring");
85 REQUIRE_FALSE(err.empty());
86}
87
88TEST_CASE("PropsSI: '@path' indirection reads file on fluid string", "[FactoryOptions][PropsSI]") {
89 TempJSONFile f("{}");
90 const std::string fluid_arg = "HEOS::Water?@" + f.path();
91 const double rho = CoolProp::PropsSI("D", "T", 300.0, "P", 101325.0, fluid_arg);
92 REQUIRE(std::isfinite(rho));
93 // Same as the no-options call.
94 const double rho_ref = CoolProp::PropsSI("D", "T", 300.0, "P", 101325.0, "HEOS::Water");
95 REQUIRE(rho == Catch::Approx(rho_ref));
96}
97
98TEST_CASE("Props1SI / PhaseSI: '?<options>' suffix accepted through high-level API", "[FactoryOptions][PropsSI]") {
99 SECTION("Props1SI empty options") {
100 const double tcrit = CoolProp::Props1SI("HEOS::Water?{}", "Tcrit");
101 REQUIRE(tcrit == Catch::Approx(647.096).margin(0.5));
102 }
103 SECTION("PhaseSI empty options") {
104 const std::string phase = CoolProp::PhaseSI("T", 300.0, "P", 101325.0, "HEOS::Water?{}");
105 REQUIRE(phase == "liquid");
106 }
107}
108
109TEST_CASE("AbstractState::factory: ambiguous '?<options>' on both sides throws", "[FactoryOptions]") {
110 // If options are on the backend AND the fluid, that's almost
111 // certainly a typo — fail loud with a clear message.
112 REQUIRE_THROWS(std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory(R"(HEOS?{})", R"(Water?{})")));
113}
114
115TEST_CASE("AbstractState::factory: '?<options>' on the fluid side is hoisted", "[FactoryOptions]") {
116 // PropsSI's extract_backend() puts the `?<options>` suffix on the
117 // fluid side after the "::" split. factory() must hoist it back
118 // into the dispatch layer rather than dropping it silently.
119 SECTION("empty options on fluid side accepted") {
120 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "Water?{}"));
121 REQUIRE(AS != nullptr);
122 }
123 SECTION("non-empty options on fluid side reach the default overload (throws)") {
124 REQUIRE_THROWS(std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", R"(Water?{"x":1})")));
125 }
126}
127
128TEST_CASE("AbstractState::factory: '?<options>' on the LAST mixture token is hoisted", "[FactoryOptions]") {
129 // For mixtures, factory(string, string) splits on '&' before the
130 // vector-overload runs — so e.g. "HEOS::R32&R125?{}" arrives as
131 // fluid_names = ["R32", "R125?{}"] and the suffix lands on the
132 // last token, not the first. Make sure the parser scans the
133 // whole vector and strips correctly.
134 SECTION("empty options on last mixture token accepted") {
135 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "R32&R125?{}"));
136 REQUIRE(AS != nullptr);
137 REQUIRE(AS->fluid_names().size() == 2);
138 REQUIRE(AS->fluid_names()[0] == "R32");
139 REQUIRE(AS->fluid_names()[1] == "R125"); // trailing '?' stripped
140 }
141 SECTION("empty options on first mixture token accepted") {
142 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "R32?{}&R125"));
143 REQUIRE(AS != nullptr);
144 REQUIRE(AS->fluid_names()[0] == "R32");
145 REQUIRE(AS->fluid_names()[1] == "R125");
146 }
147 SECTION("non-empty options on last mixture token reach the default overload (throws)") {
148 REQUIRE_THROWS(std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", R"(R32&R125?{"k":1})")));
149 }
150 SECTION("options on multiple mixture tokens is rejected") {
151 REQUIRE_THROWS(std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "R32?{}&R125?{}")));
152 }
153}
154
155#endif // ENABLE_CATCH