324 lines
10 KiB
C++
324 lines
10 KiB
C++
/*
|
|
* 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 <gtest/gtest.h>
|
|
#include <tvm/ffi/container/array.h>
|
|
#include <tvm/ffi/container/map.h>
|
|
#include <tvm/ffi/string.h>
|
|
#include <tvm/ir/config_schema.h>
|
|
|
|
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<int64_t>("num_threads");
|
|
schema.def_option<bool>("verbose");
|
|
schema.def_option<ffi::String>("name");
|
|
|
|
ffi::Map<ffi::String, ffi::Any> config = {
|
|
{"num_threads", int64_t(8)},
|
|
{"verbose", true},
|
|
{"name", ffi::String("test")},
|
|
};
|
|
|
|
auto resolved = schema.Resolve(config);
|
|
ASSERT_EQ(resolved.at("num_threads").cast<int64_t>(), 8);
|
|
ASSERT_EQ(resolved.at("verbose").cast<bool>(), true);
|
|
ASSERT_EQ(resolved.at("name").cast<ffi::String>(), "test");
|
|
}
|
|
|
|
// Type coercion: correct type passes
|
|
TEST(ConfigSchema, TypeCoercionPasses) {
|
|
ConfigSchema schema;
|
|
schema.def_option<int64_t>("count");
|
|
|
|
ffi::Map<ffi::String, ffi::Any> config = {{"count", int64_t(42)}};
|
|
auto resolved = schema.Resolve(config);
|
|
ASSERT_EQ(resolved.at("count").cast<int64_t>(), 42);
|
|
}
|
|
|
|
// Type mismatch rejection (TypeError)
|
|
TEST(ConfigSchema, TypeMismatchRejection) {
|
|
ConfigSchema schema;
|
|
schema.def_option<int64_t>("count");
|
|
|
|
// Pass a string where int64_t is expected
|
|
ffi::Map<ffi::String, ffi::Any> 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<bool>("flag");
|
|
|
|
// Pass a string where bool is expected
|
|
ffi::Map<ffi::String, ffi::Any> 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<int64_t>("max_threads", refl::DefaultValue(int64_t(256)));
|
|
schema.def_option<bool>("debug", refl::DefaultValue(false));
|
|
|
|
// Empty config -> defaults are applied
|
|
ffi::Map<ffi::String, ffi::Any> config = {};
|
|
auto resolved = schema.Resolve(config);
|
|
ASSERT_EQ(resolved.at("max_threads").cast<int64_t>(), 256);
|
|
ASSERT_EQ(resolved.at("debug").cast<bool>(), false);
|
|
}
|
|
|
|
// Default value materialization via refl::DefaultValue
|
|
TEST(ConfigSchema, DefaultValueViaTrait) {
|
|
ConfigSchema schema;
|
|
schema.def_option<int64_t>("max_threads", refl::DefaultValue(int64_t(128)));
|
|
|
|
ffi::Map<ffi::String, ffi::Any> config = {};
|
|
auto resolved = schema.Resolve(config);
|
|
ASSERT_EQ(resolved.at("max_threads").cast<int64_t>(), 128);
|
|
}
|
|
|
|
// Explicit value overrides default
|
|
TEST(ConfigSchema, ExplicitOverridesDefault) {
|
|
ConfigSchema schema;
|
|
schema.def_option<int64_t>("max_threads", refl::DefaultValue(int64_t(256)));
|
|
|
|
ffi::Map<ffi::String, ffi::Any> config = {{"max_threads", int64_t(512)}};
|
|
auto resolved = schema.Resolve(config);
|
|
ASSERT_EQ(resolved.at("max_threads").cast<int64_t>(), 512);
|
|
}
|
|
|
|
// Missing non-required option stays absent
|
|
TEST(ConfigSchema, MissingNonRequired) {
|
|
ConfigSchema schema;
|
|
schema.def_option<ffi::String>("optional_name"); // no default
|
|
|
|
ffi::Map<ffi::String, ffi::Any> 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<int64_t>("known_key");
|
|
|
|
ffi::Map<ffi::String, ffi::Any> 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<int64_t>("known_key");
|
|
schema.set_error_on_unknown(false);
|
|
|
|
ffi::Map<ffi::String, ffi::Any> 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<int64_t>(), 1);
|
|
}
|
|
|
|
// Canonicalizer runs as final step
|
|
TEST(ConfigSchema, CanonicalizerRunsLast) {
|
|
ConfigSchema schema;
|
|
schema.def_option<ffi::String>("mcpu");
|
|
|
|
auto canonicalizer = [](ffi::Map<ffi::String, ffi::Any> config) {
|
|
ffi::String mcpu = config.at("mcpu").cast<ffi::String>();
|
|
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<ffi::String, ffi::Any> config = {{"mcpu", ffi::String("arm")}};
|
|
auto resolved = schema.Resolve(config);
|
|
ASSERT_EQ(resolved.at("mcpu").cast<ffi::String>(), "canonical_arm");
|
|
ASSERT_EQ(resolved.at("feature.is_fast").cast<bool>(), true);
|
|
}
|
|
|
|
// Option declaration order is preserved in output
|
|
TEST(ConfigSchema, DeclarationOrderPreserved) {
|
|
ConfigSchema schema;
|
|
schema.def_option<ffi::String>("zebra");
|
|
schema.def_option<ffi::String>("alpha");
|
|
schema.def_option<ffi::String>("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<int64_t>("count", "Number of items to process");
|
|
|
|
ffi::Map<ffi::String, ffi::Any> config = {{"count", int64_t(5)}};
|
|
auto resolved = schema.Resolve(config);
|
|
ASSERT_EQ(resolved.at("count").cast<int64_t>(), 5);
|
|
}
|
|
|
|
// Array option type
|
|
TEST(ConfigSchema, ArrayOptionType) {
|
|
ConfigSchema schema;
|
|
schema.def_option<ffi::Array<ffi::String>>("keys");
|
|
|
|
ffi::Map<ffi::String, ffi::Any> config = {
|
|
{"keys", ffi::Array<ffi::String>{"cpu", "gpu"}},
|
|
};
|
|
auto resolved = schema.Resolve(config);
|
|
auto keys = resolved.at("keys").cast<ffi::Array<ffi::String>>();
|
|
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<ffi::Map<ffi::String, int64_t>>("params");
|
|
|
|
ffi::Map<ffi::String, int64_t> params = {{"a", 1}, {"b", 2}};
|
|
ffi::Map<ffi::String, ffi::Any> config = {{"params", params}};
|
|
auto resolved = schema.Resolve(config);
|
|
auto result = resolved.at("params").cast<ffi::Map<ffi::String, int64_t>>();
|
|
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<int64_t>("key1");
|
|
try {
|
|
schema.def_option<int64_t>("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<int64_t>("no_default");
|
|
schema.def_option<bool>("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<bool>(), true);
|
|
}
|
|
|
|
// OptionEntry stores correct type strings
|
|
TEST(ConfigSchema, OptionEntryTypeStr) {
|
|
ConfigSchema schema;
|
|
schema.def_option<int64_t>("count");
|
|
schema.def_option<bool>("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<int64_t>("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<ffi::String, ffi::Any> config = {};
|
|
auto resolved = schema.Resolve(config);
|
|
ASSERT_EQ(resolved.size(), 0);
|
|
}
|
|
|
|
// Canonicalizer with defaults
|
|
TEST(ConfigSchema, CanonicalizerWithDefaults) {
|
|
ConfigSchema schema;
|
|
schema.def_option<int64_t>("threads", refl::DefaultValue(int64_t(4)));
|
|
|
|
auto canonicalizer = [](ffi::Map<ffi::String, ffi::Any> config) {
|
|
int64_t threads = config.at("threads").cast<int64_t>();
|
|
config.Set("threads", threads * 2);
|
|
return config;
|
|
};
|
|
schema.set_canonicalizer(canonicalizer);
|
|
|
|
// Empty config: default 4 applied, then canonicalizer doubles it
|
|
ffi::Map<ffi::String, ffi::Any> config = {};
|
|
auto resolved = schema.Resolve(config);
|
|
ASSERT_EQ(resolved.at("threads").cast<int64_t>(), 8);
|
|
}
|
|
|
|
// Combined doc + default traits
|
|
TEST(ConfigSchema, DocAndDefaultTraits) {
|
|
ConfigSchema schema;
|
|
schema.def_option<int64_t>("num_cores", "CPU core count", refl::DefaultValue(int64_t(4)));
|
|
|
|
ffi::Map<ffi::String, ffi::Any> config = {};
|
|
auto resolved = schema.Resolve(config);
|
|
ASSERT_EQ(resolved.at("num_cores").cast<int64_t>(), 4);
|
|
}
|