Files
LocalAI/backend/cpp/audio-cpp/model_options_test.cpp
Ettore Di Giacinto aea4d921df backend(audio-cpp): reject out-of-range numeric model options
std::atoi is undefined once the digits exceed long and in practice wraps, so
device:2147483648 was accepted and handed the ggml backend selector a device
index of -2147483648 from a function whose error text promises a non-negative
integer. Parse with strtol and reject on ERANGE, on a value above INT_MAX, and
on any unconsumed trailing input. The error strings are unchanged.

Name the whole entry in the unknown-key error too: an entry like ':value' has
an empty key and left the user nothing to grep for in their YAML.

Tests look keys up through a helper instead of map::at, so a prefix off-by-one
fails one named check rather than aborting the binary and skipping the rest of
the suite, and cover the overflow, negative and non-numeric paths.

Assisted-by: Claude:claude-opus-5 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-07-29 19:03:32 +00:00

152 lines
6.1 KiB
C++

// Unit tests for model_options. Standard library only, so
// backend/cpp/run-unit-tests.sh picks this up with no engine checkout.
//
// The harness compiles this file as a single translation unit with no other
// sources, so the implementation is included directly rather than linked.
//
// Build and run standalone:
// g++ -std=c++17 -I. model_options_test.cpp -o t && ./t
#include "model_options.cpp"
#include <cstdio>
#include <map>
#include <string>
#include <vector>
static int failures = 0;
static void check(bool ok, const std::string &name) {
if (!ok) {
failures++;
fprintf(stderr, "FAIL: %s\n", name.c_str());
} else {
fprintf(stderr, "ok: %s\n", name.c_str());
}
}
// Returns the mapped value, or an empty string when the key is absent. map::at
// would throw on a miss and abort the whole binary, so one prefix off-by-one
// would hide every check that follows it instead of failing a single one.
static std::string lookup(const std::map<std::string, std::string> &values,
const std::string &key) {
const auto found = values.find(key);
return found == values.end() ? std::string() : found->second;
}
using audiocpp_backend::parse_model_options;
static void test_defaults() {
auto r = parse_model_options({});
check(r.error.empty(), "empty option list is not an error");
check(r.options.family.empty(), "family defaults to empty");
check(r.options.task.empty(), "task defaults to empty");
check(r.options.backend == "cpu", "backend defaults to cpu");
check(r.options.device == 0, "device defaults to 0");
check(r.options.threads == 0, "threads defaults to 0");
check(r.options.busy_timeout_ms == 0, "busy_timeout_ms defaults to 0");
check(r.options.load_options.empty(), "load_options defaults empty");
check(r.options.session_options.empty(), "session_options defaults empty");
}
static void test_scalar_options() {
auto r = parse_model_options({
"family:qwen3_tts",
"task:tts",
"backend:cuda",
"device:1",
"threads:8",
"busy_timeout_ms:30000",
});
check(r.error.empty(), "scalar options parse without error");
check(r.options.family == "qwen3_tts", "family parsed");
check(r.options.task == "tts", "task parsed");
check(r.options.backend == "cuda", "backend parsed");
check(r.options.device == 1, "device parsed");
check(r.options.threads == 8, "threads parsed");
check(r.options.busy_timeout_ms == 30000, "busy_timeout_ms parsed");
}
// Values containing colons must survive: split on the FIRST colon only.
static void test_value_containing_colon() {
auto r = parse_model_options({"model_spec_override:/models/a:b/spec.json"});
check(r.error.empty(), "colon-bearing value is not an error");
check(r.options.model_spec_override == "/models/a:b/spec.json",
"value keeps every colon after the first separator");
}
static void test_namespaced_options() {
auto r = parse_model_options({
"load.weight_type:q8_0",
"session.miocodec.weight_type:f16",
"session.graph_capacity:tiered",
});
check(r.error.empty(), "namespaced options parse without error");
check(r.options.load_options.size() == 1, "one load option");
check(lookup(r.options.load_options, "weight_type") == "q8_0", "load prefix stripped");
check(r.options.session_options.size() == 2, "two session options");
check(lookup(r.options.session_options, "miocodec.weight_type") == "f16",
"session prefix stripped, inner dots kept");
check(lookup(r.options.session_options, "graph_capacity") == "tiered",
"second session option parsed");
}
static void test_errors() {
check(!parse_model_options({"family"}).error.empty(),
"entry without a colon is rejected");
check(!parse_model_options({"nonsense:1"}).error.empty(),
"unknown key is rejected");
check(!parse_model_options({"device:abc"}).error.empty(),
"non-numeric device is rejected");
check(!parse_model_options({"threads:-1"}).error.empty(),
"negative threads is rejected");
check(!parse_model_options({"load.:x"}).error.empty(),
"empty load key is rejected");
check(!parse_model_options({"session.:x"}).error.empty(),
"empty session key is rejected");
check(!parse_model_options({"busy_timeout_ms:abc"}).error.empty(),
"non-numeric busy_timeout_ms is rejected");
check(!parse_model_options({"device:-1"}).error.empty(),
"negative device is rejected");
check(!parse_model_options({"threads:x"}).error.empty(),
"non-numeric threads is rejected");
// Values too large for int must be rejected, not silently wrapped into a
// negative device index that then reaches the ggml backend selector.
check(!parse_model_options({"device:2147483648"}).error.empty(),
"device above INT_MAX is rejected");
check(!parse_model_options({"threads:99999999999999"}).error.empty(),
"threads above INT_MAX is rejected");
// The error text must name the offending entry so a user can fix their YAML.
const auto r = parse_model_options({"nonsense:1"});
check(r.error.find("nonsense") != std::string::npos,
"error names the offending key");
// An empty key still has to give the user something to grep for.
const auto empty_key = parse_model_options({":value"});
check(empty_key.error.find(":value") != std::string::npos,
"unknown-key error names the entry even when the key is empty");
}
static void test_blank_entries_ignored() {
auto r = parse_model_options({"", " ", "family:supertonic"});
check(r.error.empty(), "blank entries are skipped, not rejected");
check(r.options.family == "supertonic", "real entry still parsed");
}
int main() {
test_defaults();
test_scalar_options();
test_value_containing_colon();
test_namespaced_options();
test_errors();
test_blank_entries_ignored();
if (failures) {
fprintf(stderr, "%d check(s) failed\n", failures);
return 1;
}
fprintf(stderr, "all model_options checks passed\n");
return 0;
}