diff --git a/src/utils/common/configuration.h b/src/utils/common/configuration.h new file mode 100644 index 0000000000..2d03e2250a --- /dev/null +++ b/src/utils/common/configuration.h @@ -0,0 +1,187 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef NIXL_SRC_UTILS_COMMON_CONFIGURATION_H +#define NIXL_SRC_UTILS_COMMON_CONFIGURATION_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "nixl_log.h" +#include "nixl_types.h" + +namespace nixl::config { + +[[nodiscard]] inline std::optional +getenvOptional(const std::string &name) { + if (const char *value = std::getenv(name.c_str())) { + NIXL_DEBUG << "Obtained environment variable " << name << "=" << value; + return std::string(value); + } + NIXL_DEBUG << "Missing environment variable " << name; + return std::nullopt; +} + +[[nodiscard]] inline std::string +getenvDefaulted(const std::string &name, const std::string &fallback) { + if (const char *value = std::getenv(name.c_str())) { + NIXL_DEBUG << "Obtained environment variable " << name << "=" << value; + return std::string(value); + } + NIXL_DEBUG << "Using default '" << fallback << "' for missing environment variable " << name; + return fallback; +} + +template struct convertTraits; + +template<> struct convertTraits { + [[nodiscard]] static bool + convert(const std::string &value) { + static const std::vector positive = {"y", "yes", "on", "1", "true", "enable"}; + + static const std::vector negative = { + "n", "no", "off", "0", "false", "disable"}; + + if (match(value, positive)) { + return true; + } + + if (match(value, negative)) { + return false; + } + + const std::string msg = "Conversion to bool failed for string '" + value + "' known are " + + absl::StrJoin(positive, ", ") + " as positive and " + absl::StrJoin(negative, ", ") + + " as negative (case insensitive)"; + throw std::runtime_error(msg); + } + +private: + [[nodiscard]] static bool + match(const std::string &value, const std::vector &haystack) noexcept { + const auto pred = [&](const std::string &ref) { + return strcasecmp(ref.c_str(), value.c_str()) == 0; + }; + return std::find_if(haystack.begin(), haystack.end(), pred) != haystack.end(); + } +}; + +template<> struct convertTraits { + [[nodiscard]] static std::string + convert(const std::string &value) { + return value; + } +}; + +template struct integralTraits { + [[nodiscard]] static integer + convert(const std::string &value) { + integer result; + const auto status = + std::from_chars(start(value), value.data() + value.size(), result, base(value)); + switch (status.ec) { + case std::errc::invalid_argument: + throw std::runtime_error("Invalid integer string '" + value + "' for type " + + typeid(integer).name()); + case std::errc::result_out_of_range: + throw std::runtime_error("Integer string '" + value + "' out of range for type " + + typeid(integer).name()); + default: + if (status.ptr != value.data() + value.size()) { + throw std::runtime_error("Trailing garbage in integer string '" + value + "'"); + } + break; + } + return result; + } + +private: + [[nodiscard]] static bool + isHex(const std::string &value) noexcept { + return std::is_unsigned_v && (value.size() > 2) && (value[0] == '0') && + ((value[1] == 'x') || (value[1] == 'X')); + } + + [[nodiscard]] static int + base(const std::string &value) noexcept { + return isHex(value) ? 16 : 10; + } + + [[nodiscard]] static const char * + start(const std::string &value) noexcept { + return value.data() + (isHex(value) ? 2 : 0); + } +}; + +template +struct convertTraits>> + : integralTraits {}; + +template class traits = convertTraits> +[[nodiscard]] nixl_status_t +getValueWithStatus(type &result, const std::string &env) { + if (const auto opt = getenvOptional(env)) { + try { + result = traits>::convert(*opt); + return NIXL_SUCCESS; + } + catch (const std::exception &e) { + NIXL_DEBUG << "Unable to convert value '" << *opt << "' from environment variable '" + << env << "' to target type " << typeid(type).name(); + return NIXL_ERR_MISMATCH; + } + } + return NIXL_ERR_NOT_FOUND; +} + +template class traits = convertTraits> +[[nodiscard]] type +getValue(const std::string &env) { + if (const auto opt = getenvOptional(env)) { + return traits::convert(*opt); + } + throw std::runtime_error("Missing environment variable '" + env + "'"); +} + +template class traits = convertTraits> +[[nodiscard]] std::optional +getValueOptional(const std::string &env) { + if (const auto opt = getenvOptional(env)) { + return traits::convert(*opt); + } + return std::nullopt; +} + +template class traits = convertTraits> +[[nodiscard]] type +getValueDefaulted(const std::string &env, const type &fallback) { + return getValueOptional(env).value_or(fallback); +} + +} // namespace nixl::config + +#endif diff --git a/test/gtest/configuration.cpp b/test/gtest/configuration.cpp new file mode 100644 index 0000000000..41b4eff8f6 --- /dev/null +++ b/test/gtest/configuration.cpp @@ -0,0 +1,218 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "common/configuration.h" +#include "gtest/gtest.h" + +#include +#include +#include + +namespace { + +const std::string variable = "NIXL_CONFIG_TEST"; +const std::string undefined = "ASDLFHASLK1298159816"; + +} // namespace + +namespace nixl::config { + +TEST(Config, EnvWrapper) { + const std::string value = "foo"; + ASSERT_EQ(::setenv(variable.c_str(), value.c_str(), 1), 0); + ASSERT_EQ(getenvOptional(variable), value); + const std::string fallback = "bar"; + ASSERT_EQ(getenvDefaulted(variable, fallback), value); + ASSERT_FALSE(getenvOptional(undefined).has_value()); + ASSERT_EQ(getenvDefaulted(undefined, fallback), fallback); +} + +TEST(Config, Undefined) { + ASSERT_EQ(getValueOptional(undefined), std::nullopt); + ASSERT_EQ(getValueOptional(undefined), std::nullopt); + ASSERT_EQ(getValueOptional(undefined), std::nullopt); + + ASSERT_EQ(getValueDefaulted(undefined, true), true); + ASSERT_EQ(getValueDefaulted(undefined, false), false); + ASSERT_EQ(getValueDefaulted(undefined, 42), 42); + const std::string value = "foo"; + ASSERT_EQ(getValueDefaulted(undefined, value), value); + + bool b; + ASSERT_EQ(getValueWithStatus(b, undefined), NIXL_ERR_NOT_FOUND); + char c; + ASSERT_EQ(getValueWithStatus(c, undefined), NIXL_ERR_NOT_FOUND); + std::string s; + ASSERT_EQ(getValueWithStatus(s, undefined), NIXL_ERR_NOT_FOUND); +} + +namespace { + + template + void + testSimpleSuccess(const std::string &input, const T value) { + ASSERT_EQ(::setenv(variable.c_str(), input.c_str(), 1), 0); + EXPECT_EQ(getValue(variable), value); + EXPECT_EQ(getValueOptional(variable), value); + EXPECT_EQ(getValueDefaulted(variable, !value), value); + EXPECT_EQ(getValue(variable), input); + EXPECT_EQ(getValueOptional(variable), input); + EXPECT_EQ(getValueDefaulted(variable, variable), input); + T out; + EXPECT_EQ(getValueWithStatus(out, variable), NIXL_SUCCESS); + EXPECT_EQ(out, value); + std::string str; + EXPECT_EQ(getValueWithStatus(str, variable), NIXL_SUCCESS); + EXPECT_EQ(str, input); + } + + template + void + testSimpleFailure(const std::string &input) { + ASSERT_EQ(::setenv(variable.c_str(), input.c_str(), 1), 0); + EXPECT_ANY_THROW((void)getValue(variable)); + EXPECT_ANY_THROW((void)getValueOptional(variable)); + EXPECT_ANY_THROW((void)getValueDefaulted(variable, T())); + EXPECT_EQ(getValue(variable), input); + EXPECT_EQ(getValueOptional(variable), input); + EXPECT_EQ(getValueDefaulted(variable, variable), input); + T out; + EXPECT_EQ(getValueWithStatus(out, variable), NIXL_ERR_MISMATCH); + } + +} // namespace + +TEST(Config, ConvertBool) { + testSimpleSuccess("1", true); + testSimpleSuccess("0", false); + testSimpleSuccess("yes", true); + testSimpleSuccess("no", false); + testSimpleSuccess("Yes", true); + testSimpleSuccess("No", false); + testSimpleSuccess("YeS", true); + testSimpleSuccess("nO", false); + testSimpleSuccess("enable", true); + testSimpleSuccess("disable", false); + testSimpleSuccess("on", true); + testSimpleSuccess("off", false); + testSimpleSuccess("oN", true); + testSimpleSuccess("oFF", false); + testSimpleSuccess("TRUE", true); + testSimpleSuccess("FALSE", false); + testSimpleSuccess("true", true); + testSimpleSuccess("false", false); + + testSimpleFailure(""); + testSimpleFailure("2"); + testSimpleFailure("enabled"); +} + +namespace { + template + void + testSigned() { + testSimpleSuccess("0", T(0)); + testSimpleSuccess("1", T(1)); + testSimpleSuccess("-1", T(-1)); + testSimpleSuccess("42", T(42)); + testSimpleSuccess("-42", T(-42)); + const T min_value = std::numeric_limits::min(); + const std::string min_string = std::to_string(min_value); + testSimpleSuccess(min_string, min_value); + const T max_value = std::numeric_limits::max(); + const std::string max_string = std::to_string(max_value); + testSimpleSuccess(max_string, max_value); + + testSimpleFailure(""); + testSimpleFailure("-"); + testSimpleFailure("+"); + testSimpleFailure("+0"); + testSimpleFailure("+1"); + testSimpleFailure("r"); + testSimpleFailure("0y"); + testSimpleFailure("0x"); + testSimpleFailure(max_string + '0'); + testSimpleFailure(min_string + '0'); + + testSimpleFailure("0x0"); + testSimpleFailure("0x000"); + testSimpleFailure("0x01"); + testSimpleFailure("0x1f"); + testSimpleFailure("-0x01"); + } + + template + void + testUnsigned() { + testSimpleSuccess("0", T(0)); + testSimpleSuccess("1", T(1)); + testSimpleSuccess("42", T(42)); + const T max_value = std::numeric_limits::max(); + const std::string max_string = std::to_string(max_value); + testSimpleSuccess(max_string, max_value); + + testSimpleFailure(""); + testSimpleFailure(" 0"); + testSimpleFailure("0 "); + testSimpleFailure("-"); + testSimpleFailure("+"); + testSimpleFailure("-0"); + testSimpleFailure("-1"); + testSimpleFailure("-42"); + testSimpleFailure("+1"); + testSimpleFailure("+0"); + testSimpleFailure("+42"); + testSimpleFailure("-m"); + testSimpleFailure("r"); + testSimpleFailure("0y"); + testSimpleFailure("0x"); + testSimpleFailure(max_string + '0'); + + testSimpleSuccess("0x0", T(0)); + testSimpleSuccess("0x000", T(0)); + testSimpleSuccess("0x01", T(1)); + + testSimpleSuccess("0X0", T(0)); + testSimpleSuccess("0X000", T(0)); + testSimpleSuccess("0X01", T(1)); + + testSimpleSuccess("0x1f", T(31)); + testSimpleSuccess("0X1f", T(31)); + testSimpleSuccess("0X1F", T(31)); + testSimpleSuccess("0x1F", T(31)); + + testSimpleFailure("+0x00"); + testSimpleFailure("1x00"); + } + +} // namespace + +TEST(Config, ConvertSigned) { + testSigned(); + testSigned(); + testSigned(); + testSigned(); +} + +TEST(Config, ConvertUnsigned) { + testUnsigned(); + testUnsigned(); + testUnsigned(); + testUnsigned(); +} + +} // namespace nixl::config diff --git a/test/gtest/meson.build b/test/gtest/meson.build index b150f23347..3d8b7bc7f3 100644 --- a/test/gtest/meson.build +++ b/test/gtest/meson.build @@ -66,7 +66,8 @@ gtest_sources = [ 'metadata_exchange.cpp', 'common.cpp', 'query_mem.cpp', - 'telemetry_test.cpp' + 'telemetry_test.cpp', + 'configuration.cpp' ] if ucx_gpu_device_api_available