CoolProp 8.0.0
An open-source fluid property and humid air property database
CoolProp-Tests-FactoryOptions.cpp
Go to the documentation of this file.
1// Catch2 tests for parse_factory_options() — the factory-string `?<options>`
2// suffix parser. Pure-parser tests; no backend wiring, no JSON validation
3// (that lives one layer up). See
4// docs/superpowers/specs/2026-05-16-backend-options-string-design.md for
5// the design.
6
7#if defined(ENABLE_CATCH)
8
9# include <catch2/catch_all.hpp>
10
11# include <cstdio>
12# include <filesystem>
13# include <fstream>
14# include <string>
15
17# include "CoolProp/Exceptions.h"
18# include "TestUtils.h"
19
20namespace {
21
22// RAII temp file for the @path tests. Writes the given contents to
23// a unique path under the system temp dir; removes on destruction.
24class TempJSONFile
25{
26 public:
27 explicit TempJSONFile(const std::string& contents) {
28 path_ = std::filesystem::temp_directory_path()
29 / ("cp_factopts_test_" + std::to_string(CoolProp::tests::test_pid()) + "_" + std::to_string(counter_++) + ".json");
30 std::ofstream(path_) << contents;
31 }
32 ~TempJSONFile() {
33 std::error_code ec;
34 std::filesystem::remove(path_, ec);
35 }
36 TempJSONFile(const TempJSONFile&) = delete;
37 TempJSONFile& operator=(const TempJSONFile&) = delete;
38 TempJSONFile(TempJSONFile&&) = delete;
39 TempJSONFile& operator=(TempJSONFile&&) = delete;
40
41 [[nodiscard]] std::string path() const {
42 return path_.string();
43 }
44
45 private:
46 std::filesystem::path path_;
47 static inline int counter_ = 0;
48};
49
50} // namespace
51
52TEST_CASE("parse_factory_options: no '?' suffix returns whole string + empty options", "[FactoryOptions]") {
53 auto r = CoolProp::parse_factory_options("HEOS::Water");
54 REQUIRE(r.clean_string == "HEOS::Water");
55 REQUIRE(r.options_json.empty());
56}
57
58TEST_CASE("parse_factory_options: bare '?' yields empty options", "[FactoryOptions]") {
59 auto r = CoolProp::parse_factory_options("HEOS::Water?");
60 REQUIRE(r.clean_string == "HEOS::Water");
61 REQUIRE(r.options_json.empty());
62}
63
64TEST_CASE("parse_factory_options: whitespace-only tail is treated as empty", "[FactoryOptions]") {
65 auto r = CoolProp::parse_factory_options("HEOS::Water? \t ");
66 REQUIRE(r.clean_string == "HEOS::Water");
67 REQUIRE(r.options_json.empty());
68}
69
70TEST_CASE("parse_factory_options: inline JSON tail is returned verbatim", "[FactoryOptions]") {
71 auto r = CoolProp::parse_factory_options(R"(SVDSBTL&HEOS::Water?{"critical_patch":"off"})");
72 REQUIRE(r.clean_string == "SVDSBTL&HEOS::Water");
73 REQUIRE(r.options_json == R"({"critical_patch":"off"})");
74}
75
76TEST_CASE("parse_factory_options: split is on the FIRST '?' only", "[FactoryOptions]") {
77 SECTION("'?' inside a JSON string value") {
78 auto r = CoolProp::parse_factory_options(R"(HEOS::Water?{"hint":"what?"})");
79 REQUIRE(r.clean_string == "HEOS::Water");
80 REQUIRE(r.options_json == R"({"hint":"what?"})");
81 }
82 SECTION("'?' is the entire string value") {
83 auto r = CoolProp::parse_factory_options(R"(HEOS::Water?{"q":"?"})");
84 REQUIRE(r.clean_string == "HEOS::Water");
85 REQUIRE(r.options_json == R"({"q":"?"})");
86 }
87 SECTION("URL-style with both '?' and '&' inside the value") {
88 auto r = CoolProp::parse_factory_options(R"(HEOS::Water?{"meta":{"url":"http://x.com/path?id=1&q=2"}})");
89 REQUIRE(r.clean_string == "HEOS::Water");
90 REQUIRE(r.options_json == R"({"meta":{"url":"http://x.com/path?id=1&q=2"}})");
91 }
92 SECTION("regex string with escaped '?'") {
93 auto r = CoolProp::parse_factory_options(R"(HEOS::Water?{"regex":"^[A-Z]+\\?$"})");
94 REQUIRE(r.clean_string == "HEOS::Water");
95 REQUIRE(r.options_json == R"({"regex":"^[A-Z]+\\?$"})");
96 }
97 SECTION("multiple '?' chained inside one string value") {
98 auto r = CoolProp::parse_factory_options(R"(HEOS::Water?{"chain":"first?second?third"})");
99 REQUIRE(r.clean_string == "HEOS::Water");
100 REQUIRE(r.options_json == R"({"chain":"first?second?third"})");
101 }
102}
103
104TEST_CASE("parse_factory_options: '@path' reads file contents verbatim", "[FactoryOptions]") {
105 SECTION("simple file path") {
106 TempJSONFile f(R"({"critical_patch":"auto","grid":{"NT":200}})");
107 const std::string s = "SVDSBTL&HEOS::Water?@" + f.path();
109 REQUIRE(r.clean_string == "SVDSBTL&HEOS::Water");
110 REQUIRE(r.options_json == R"({"critical_patch":"auto","grid":{"NT":200}})");
111 }
112 SECTION("file path with internal characters ('-', '_', '.', digits)") {
113 TempJSONFile f(R"({"x":1})");
114 const std::string s = "HEOS::Water?@" + f.path();
116 REQUIRE(r.options_json == R"({"x":1})");
117 }
118 SECTION("missing '@path' file throws") {
119 REQUIRE_THROWS(CoolProp::parse_factory_options("HEOS::Water?@/this/path/definitely/does/not/exist.json"));
120 }
121 SECTION("bare '@' with no path throws ValueError") {
122 REQUIRE_THROWS_AS(CoolProp::parse_factory_options("HEOS::Water?@"), CoolProp::ValueError);
123 }
124}
125
126TEST_CASE("parse_factory_options: clean_string preserves the full backend+fluid grammar", "[FactoryOptions]") {
127 SECTION("simple backend") {
128 auto r = CoolProp::parse_factory_options("HEOS::Water?{}");
129 REQUIRE(r.clean_string == "HEOS::Water");
130 }
131 SECTION("source-backend split (& inside the cleaned half)") {
132 auto r = CoolProp::parse_factory_options("SVDSBTL&HEOS::Water?{}");
133 REQUIRE(r.clean_string == "SVDSBTL&HEOS::Water");
134 }
135 SECTION("no fluid name") {
136 auto r = CoolProp::parse_factory_options("HEOS?{}");
137 REQUIRE(r.clean_string == "HEOS");
138 REQUIRE(r.options_json == "{}");
139 }
140}
141
142// ---------------------------------------------------------------------------
143// Factory entry-point integration: options are stripped from the backend
144// string before dispatch, and the default generator overload rejects
145// non-empty options with NotImplementedError for backends that haven't
146// opted in.
147// ---------------------------------------------------------------------------
148
149# include "CoolProp/AbstractState.h"
150
151TEST_CASE("AbstractState::factory: '?<options>' is stripped before backend dispatch", "[FactoryOptions]") {
152 SECTION("no options — existing behaviour unchanged") {
153 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "Water"));
154 REQUIRE(AS != nullptr);
155 // HEOS factory returns the pure-fluid wrapper, which reports
156 // "HelmholtzEOSBackend" rather than the mixture variant.
157 REQUIRE(AS->backend_name() == "HelmholtzEOSBackend");
158 }
159 SECTION("empty options ('?{}') accepted by default overload") {
160 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS?{}", "Water"));
161 REQUIRE(AS != nullptr);
162 }
163 SECTION("bare '?' accepted by default overload") {
164 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS?", "Water"));
165 REQUIRE(AS != nullptr);
166 }
167}
168
169TEST_CASE("AbstractState::factory: non-empty options on an opted-out backend throw", "[FactoryOptions]") {
170 REQUIRE_THROWS(std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory(R"(HEOS?{"some_key":1})", "Water")));
171}
172
173TEST_CASE("AbstractState::build_options_json: default returns empty string", "[FactoryOptions]") {
174 auto AS = std::shared_ptr<CoolProp::AbstractState>(CoolProp::AbstractState::factory("HEOS", "Water"));
175 REQUIRE(AS->build_options_json().empty());
176}
177
178#endif // ENABLE_CATCH