/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 #include #include #include #include using namespace tvm; using namespace tvm::ir; namespace refl = tvm::ffi::reflection; // Basic int/bool/string option resolution TEST(ConfigSchema, BasicTypes) { ConfigSchema schema; schema.def_option("num_threads"); schema.def_option("verbose"); schema.def_option("name"); ffi::Map config = { {"num_threads", int64_t(8)}, {"verbose", true}, {"name", ffi::String("test")}, }; auto resolved = schema.Resolve(config); ASSERT_EQ(resolved.at("num_threads").cast(), 8); ASSERT_EQ(resolved.at("verbose").cast(), true); ASSERT_EQ(resolved.at("name").cast(), "test"); } // Type coercion: correct type passes TEST(ConfigSchema, TypeCoercionPasses) { ConfigSchema schema; schema.def_option("count"); ffi::Map config = {{"count", int64_t(42)}}; auto resolved = schema.Resolve(config); ASSERT_EQ(resolved.at("count").cast(), 42); } // Type mismatch rejection (TypeError) TEST(ConfigSchema, TypeMismatchRejection) { ConfigSchema schema; schema.def_option("count"); // Pass a string where int64_t is expected ffi::Map config = {{"count", ffi::String("not_a_number")}}; try { schema.Resolve(config); FAIL() << "Expected TypeError"; } catch (const ffi::Error& e) { EXPECT_EQ(e.kind(), "TypeError"); EXPECT_NE(std::string(e.message()).find("count"), std::string::npos); } } TEST(ConfigSchema, TypeMismatchBoolForInt) { ConfigSchema schema; schema.def_option("flag"); // Pass a string where bool is expected ffi::Map config = {{"flag", ffi::String("true")}}; try { schema.Resolve(config); FAIL() << "Expected TypeError"; } catch (const ffi::Error& e) { EXPECT_EQ(e.kind(), "TypeError"); EXPECT_NE(std::string(e.message()).find("flag"), std::string::npos); } } // Default value materialization via refl::DefaultValue TEST(ConfigSchema, DefaultValueMaterialization) { ConfigSchema schema; schema.def_option("max_threads", refl::DefaultValue(int64_t(256))); schema.def_option("debug", refl::DefaultValue(false)); // Empty config -> defaults are applied ffi::Map config = {}; auto resolved = schema.Resolve(config); ASSERT_EQ(resolved.at("max_threads").cast(), 256); ASSERT_EQ(resolved.at("debug").cast(), false); } // Default value materialization via refl::DefaultValue TEST(ConfigSchema, DefaultValueViaTrait) { ConfigSchema schema; schema.def_option("max_threads", refl::DefaultValue(int64_t(128))); ffi::Map config = {}; auto resolved = schema.Resolve(config); ASSERT_EQ(resolved.at("max_threads").cast(), 128); } // Explicit value overrides default TEST(ConfigSchema, ExplicitOverridesDefault) { ConfigSchema schema; schema.def_option("max_threads", refl::DefaultValue(int64_t(256))); ffi::Map config = {{"max_threads", int64_t(512)}}; auto resolved = schema.Resolve(config); ASSERT_EQ(resolved.at("max_threads").cast(), 512); } // Missing non-required option stays absent TEST(ConfigSchema, MissingNonRequired) { ConfigSchema schema; schema.def_option("optional_name"); // no default ffi::Map config = {}; auto resolved = schema.Resolve(config); ASSERT_EQ(resolved.find("optional_name"), resolved.end()); } // Unknown key rejection (ValueError) TEST(ConfigSchema, UnknownKeyRejection) { ConfigSchema schema; schema.def_option("known_key"); ffi::Map config = { {"known_key", int64_t(1)}, {"unknown_key", ffi::String("surprise")}, }; try { schema.Resolve(config); FAIL() << "Expected ValueError"; } catch (const ffi::Error& e) { EXPECT_EQ(e.kind(), "ValueError"); EXPECT_NE(std::string(e.message()).find("unknown_key"), std::string::npos); } } // Unknown key allowed when error_on_unknown is false TEST(ConfigSchema, UnknownKeyAllowed) { ConfigSchema schema; schema.def_option("known_key"); schema.set_error_on_unknown(false); ffi::Map config = { {"known_key", int64_t(1)}, {"unknown_key", ffi::String("allowed")}, }; auto resolved = schema.Resolve(config); // Unknown keys are silently dropped (not validated, not forwarded) ASSERT_EQ(resolved.at("known_key").cast(), 1); } // Canonicalizer runs as final step TEST(ConfigSchema, CanonicalizerRunsLast) { ConfigSchema schema; schema.def_option("mcpu"); auto canonicalizer = [](ffi::Map config) { ffi::String mcpu = config.at("mcpu").cast(); config.Set("mcpu", ffi::String("canonical_") + mcpu); // Canonicalizer can add new keys config.Set("feature.is_fast", true); return config; }; schema.set_canonicalizer(canonicalizer); ffi::Map config = {{"mcpu", ffi::String("arm")}}; auto resolved = schema.Resolve(config); ASSERT_EQ(resolved.at("mcpu").cast(), "canonical_arm"); ASSERT_EQ(resolved.at("feature.is_fast").cast(), true); } // Option declaration order is preserved in output TEST(ConfigSchema, DeclarationOrderPreserved) { ConfigSchema schema; schema.def_option("zebra"); schema.def_option("alpha"); schema.def_option("middle"); auto& options = schema.ListOptions(); ASSERT_EQ(options.size(), 3); ASSERT_EQ(std::string(options[0].key), "zebra"); ASSERT_EQ(std::string(options[1].key), "alpha"); ASSERT_EQ(std::string(options[2].key), "middle"); } // Doc string trait accepted without affecting behavior TEST(ConfigSchema, DocStringTrait) { ConfigSchema schema; schema.def_option("count", "Number of items to process"); ffi::Map config = {{"count", int64_t(5)}}; auto resolved = schema.Resolve(config); ASSERT_EQ(resolved.at("count").cast(), 5); } // Array option type TEST(ConfigSchema, ArrayOptionType) { ConfigSchema schema; schema.def_option>("keys"); ffi::Map config = { {"keys", ffi::Array{"cpu", "gpu"}}, }; auto resolved = schema.Resolve(config); auto keys = resolved.at("keys").cast>(); ASSERT_EQ(keys.size(), 2); ASSERT_EQ(keys[0], "cpu"); ASSERT_EQ(keys[1], "gpu"); } // Map option type TEST(ConfigSchema, MapOptionType) { ConfigSchema schema; schema.def_option>("params"); ffi::Map params = {{"a", 1}, {"b", 2}}; ffi::Map config = {{"params", params}}; auto resolved = schema.Resolve(config); auto result = resolved.at("params").cast>(); ASSERT_EQ(result.size(), 2); ASSERT_EQ(result["a"], 1); ASSERT_EQ(result["b"], 2); } // Duplicate option key rejected TEST(ConfigSchema, DuplicateKeyRejected) { ConfigSchema schema; schema.def_option("key1"); try { schema.def_option("key1"); FAIL() << "Expected ValueError"; } catch (const ffi::Error& e) { EXPECT_EQ(e.kind(), "ValueError"); EXPECT_NE(std::string(e.message()).find("key1"), std::string::npos); } } // ListOptions returns option entries in declaration order TEST(ConfigSchema, ListOptionsNames) { ConfigSchema schema; schema.def_option("no_default"); schema.def_option("with_default", refl::DefaultValue(true)); auto& options = schema.ListOptions(); ASSERT_EQ(options.size(), 2); ASSERT_EQ(std::string(options[0].key), "no_default"); ASSERT_EQ(std::string(options[1].key), "with_default"); ASSERT_EQ(std::string(options[0].type_str), "int"); ASSERT_EQ(std::string(options[1].type_str), "bool"); // Check default_value ASSERT_FALSE(options[0].has_default); ASSERT_TRUE(options[1].has_default); ASSERT_EQ(options[1].default_value.cast(), true); } // OptionEntry stores correct type strings TEST(ConfigSchema, OptionEntryTypeStr) { ConfigSchema schema; schema.def_option("count"); schema.def_option("flag"); auto& options = schema.ListOptions(); ASSERT_EQ(std::string(options[0].type_str), "int"); ASSERT_EQ(std::string(options[1].type_str), "bool"); } // HasOption query TEST(ConfigSchema, HasOption) { ConfigSchema schema; schema.def_option("exists"); ASSERT_TRUE(schema.HasOption("exists")); ASSERT_FALSE(schema.HasOption("does_not_exist")); } // Empty config with no options TEST(ConfigSchema, EmptySchemaEmptyConfig) { ConfigSchema schema; ffi::Map config = {}; auto resolved = schema.Resolve(config); ASSERT_EQ(resolved.size(), 0); } // Canonicalizer with defaults TEST(ConfigSchema, CanonicalizerWithDefaults) { ConfigSchema schema; schema.def_option("threads", refl::DefaultValue(int64_t(4))); auto canonicalizer = [](ffi::Map config) { int64_t threads = config.at("threads").cast(); config.Set("threads", threads * 2); return config; }; schema.set_canonicalizer(canonicalizer); // Empty config: default 4 applied, then canonicalizer doubles it ffi::Map config = {}; auto resolved = schema.Resolve(config); ASSERT_EQ(resolved.at("threads").cast(), 8); } // Combined doc + default traits TEST(ConfigSchema, DocAndDefaultTraits) { ConfigSchema schema; schema.def_option("num_cores", "CPU core count", refl::DefaultValue(int64_t(4))); ffi::Map config = {}; auto resolved = schema.Resolve(config); ASSERT_EQ(resolved.at("num_cores").cast(), 4); }