voice_audio_multi/tests/unittests/test_server_model_installer.cpp
Fabio c36510c21e
Some checks failed
Linux Build / Linux cpu (push) Has been cancelled
Linux Build / Linux cpu-clang (push) Has been cancelled
Linux Build / Linux vulkan (push) Has been cancelled
mac-build / macOS CPU (push) Has been cancelled
Nix Build / Nix (cpu) (push) Has been cancelled
Nix Build / Nix (vulkan) (push) Has been cancelled
windows-build / Windows CPU (push) Has been cancelled
Initial import: audio.cpp with Orange Pi documentation
2026-11-04 19:05:22 +01:00

307 lines
16 KiB
C++

#include "model_installer.h"
#include "engine/framework/io/json.h"
#include "engine/framework/package_manager/manager.h"
#include <chrono>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>
#include <thread>
#include <future>
#include <vector>
namespace {
void require(bool condition, const std::string & message) {
if (!condition) throw std::runtime_error(message);
}
std::filesystem::path make_root() {
const auto suffix = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
const auto root = std::filesystem::temp_directory_path() /
("audiocpp-native-model-manager-test-" + std::to_string(suffix));
std::filesystem::create_directories(root);
return root;
}
void write(const std::filesystem::path & path, const std::string & value) {
std::filesystem::create_directories(path.parent_path());
std::ofstream output(path, std::ios::binary | std::ios::trunc);
output << value;
}
void set_base_url(const std::string & value) {
#ifdef _WIN32
_putenv_s("AUDIOCPP_HF_BASE_URL", value.c_str());
#else
setenv("AUDIOCPP_HF_BASE_URL", value.c_str(), 1);
#endif
}
std::string wait_for(minitts::server::ModelInstaller & installer, const std::string & id,
const std::string & state, int attempts = 200) {
for (int attempt = 0; attempt < attempts; ++attempt) {
const auto status = installer.status(id);
if (status.find("\"state\":\"" + state + "\"") != std::string::npos) return status;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
throw std::runtime_error("timed out waiting for " + id + " to become " + state);
}
void test_native_package_info() {
namespace json = engine::io::json;
const auto root = make_root();
try {
write(root / "model_specs" / "demo.json", R"({"family":"demo",
"package_defaults":{"download":{"kind":"huggingface_snapshot","repo":"org/repo"}},
"packages":[
{"id":"components","display_name":"Components","format":"gguf","precision":"f32",
"target_directory":"Nested/Components","strip_prefix":"Remote/",
"files":["Remote/config.json","Remote/subdir/z.gguf","Remote/a.gguf"]},
{"id":"directory","display_name":"Directory","format":"directory","precision":"f32",
"target_directory":"Directory","strip_prefix":".","files":["./config.json"]},
{"id":"empty","display_name":"Empty","format":"gguf","precision":"f32",
"target_directory":"Empty","files":[]}
]})");
engine::package_manager::PackageManager manager(root, root / "models");
require(!manager.package_info("missing").has_value(), "unknown packages return no package info");
const auto info = manager.package_info("components");
require(info.has_value() && info->family == "demo" && !info->installed &&
info->package.root == root / "models" / "Nested" / "Components" &&
info->package.format == "gguf", "package info exposes raw identity, root, format, and local presence");
const std::vector<std::filesystem::path> expected = {"config.json", "subdir/z.gguf", "a.gguf"};
require(info->package.files == expected,
"package info strips the prefix, preserves file order and subdirectories, and retains every component");
require(!std::filesystem::exists(info->package.root), "package lookup does not install or create a package");
const auto directory = manager.package_info("directory");
require(directory.has_value() && directory->package.format == "directory" &&
directory->package.files == std::vector<std::filesystem::path>{"config.json"},
"non-GGUF packages use the same generic metadata and dot-prefix semantics");
require(!manager.package_info("empty")->installed && manager.package_info("empty")->package.files.empty(),
"packages without files retain the installer's not-installed state");
const auto check_inventory = [&](bool installed) {
const auto response = json::parse(manager.inventory(false, "components"));
const auto & row = response.as_array().at(0);
const auto & metadata = row.require("package");
require(row.require("installed").as_bool() == installed &&
manager.package_info("components")->installed == installed,
"lookup and local inventory agree on complete package presence");
require(metadata.require("family").as_string() == info->family &&
metadata.require("root").as_string() == info->package.root.generic_string() &&
metadata.require("format").as_string() == info->package.format,
"inventory exposes the same raw package identity, root, and format");
const auto & files = metadata.require("files").as_array();
require(files.size() == expected.size(), "inventory retains all package files");
for (size_t i = 0; i < expected.size(); ++i) {
require(files[i].as_string() == expected[i].generic_string(),
"inventory paths are installed-relative and retain source order");
}
require(metadata.as_object().size() == 4 && row.find("load_target") == nullptr &&
row.find("load_options") == nullptr && row.find("session_options") == nullptr,
"native inventory contains raw package data without loading policy or options");
};
check_inventory(false);
write(info->package.root / expected[0], "{}");
check_inventory(false);
write(info->package.root / expected[1], "payload");
write(info->package.root / expected[2], "payload");
check_inventory(true);
std::filesystem::remove(info->package.root / expected[0]);
check_inventory(false);
} catch (...) {
std::filesystem::remove_all(root);
throw;
}
std::filesystem::remove_all(root);
}
void test_native_package_lifecycle() {
const auto root = make_root();
try {
write(root / "model_specs" / "demo.json", R"JSON({
"family":"demo","display_name":"Demo","description":"","category":"tts",
"status":"supported","tasks":["tts"],"modes":["offline"],"languages":[],
"capabilities":{},"runtime":{},
"package_defaults":{"download":{"kind":"huggingface_snapshot","repo":"org/repo","revision":"main"}},
"packages":[
{"id":"demo_q8","display_name":"Demo Q8","default":true,"format":"gguf","precision":"q8_0",
"target_directory":"Demo","files":["Demo/model-q8.gguf","Demo/shared.json"],"strip_prefix":"Demo"},
{"id":"demo_f16","display_name":"Demo F16","format":"gguf","precision":"f16",
"target_directory":"Demo","files":["Demo/model-f16.gguf","Demo/shared.json"],"strip_prefix":"Demo"},
{"id":"demo_slow","display_name":"Demo Slow","format":"gguf","precision":"q8_0",
"target_directory":"Slow","files":["Slow/model.gguf"],"strip_prefix":"Slow"},
{"id":"demo_slow_sibling","display_name":"Demo Slow sibling","format":"gguf","precision":"f16",
"target_directory":"Slow","files":["Slow/sibling.gguf"],"strip_prefix":"Slow"}
],"sources":[]
})JSON");
set_base_url("http://127.0.0.1:18991");
auto fixture = std::async(std::launch::async, [] {
#ifdef _WIN32
return std::system("python \"" AUDIOCPP_NATIVE_MANAGER_FIXTURE "\" --requests 16");
#else
return std::system("python3 \"" AUDIOCPP_NATIVE_MANAGER_FIXTURE "\" --requests 16");
#endif
});
std::this_thread::sleep_for(std::chrono::milliseconds(200));
minitts::server::ModelInstaller installer(root, root / "models");
require(installer.status("missing").find("\"state\":\"idle\"") != std::string::npos,
"unknown jobs are idle");
bool rejected = false;
try { (void) installer.start("bad & package", "", "", "", "", false); }
catch (const std::runtime_error &) { rejected = true; }
require(rejected, "unsafe package ids are rejected");
auto started = installer.start("demo_q8", "", "", "", "", false);
require(started.find("\"state\":\"queued\"") != std::string::npos ||
started.find("\"state\":\"running\"") != std::string::npos,
"native install starts asynchronously");
const auto sibling_started = installer.start("demo_f16", "", "", "", "", false);
require(sibling_started.find("\"state\":\"queued\"") != std::string::npos ||
sibling_started.find("\"state\":\"running\"") != std::string::npos,
"a sibling package can be queued while the first package is installing");
auto complete = wait_for(installer, "demo_q8", "complete");
(void) wait_for(installer, "demo_f16", "complete");
require(complete.find("\"progress_percent\":100") != std::string::npos,
"completed install reports 100 percent");
require(std::filesystem::is_regular_file(root / "models" / "Demo" / "model-q8.gguf"),
"model payload is installed");
require(std::filesystem::is_regular_file(root / "models" / "Demo" / "shared.json"),
"shared sidecar is installed");
require(std::filesystem::is_regular_file(root / "models" / "Demo" / ".audiocpp-package-demo_q8.json"),
"native install writes a version manifest");
// Packages in the same model family share a target directory. The
// package manager serializes them so common sidecars are published once.
require(std::filesystem::is_regular_file(root / "models" / "Demo" / "model-f16.gguf"),
"sibling precision installs beside Q8");
auto removed = installer.remove("demo_q8");
require(removed.find("\"removed\":true") != std::string::npos, "Q8 removal succeeds");
require(!std::filesystem::exists(root / "models" / "Demo" / "model-q8.gguf"),
"Q8-owned file is removed");
require(std::filesystem::exists(root / "models" / "Demo" / "shared.json"),
"sidecar shared by installed F16 is retained");
(void) installer.start("demo_slow", "", "", "", "", false);
(void) wait_for(installer, "demo_slow", "running");
std::this_thread::sleep_for(std::chrono::milliseconds(75));
const auto stopping = installer.stop("demo_slow");
require(stopping.find("\"state\":\"cancelling\"") != std::string::npos,
"native download accepts cancellation");
(void) wait_for(installer, "demo_slow", "cancelled");
require(!std::filesystem::exists(root / "models" / "Slow" / "model.gguf"),
"cancelled package is never published to its final path");
// A sibling queued behind an active install must be cancellable while
// it waits for the shared target-directory lock.
(void) installer.start("demo_slow", "", "", "", "", false);
(void) wait_for(installer, "demo_slow", "running");
(void) installer.start("demo_slow_sibling", "", "", "", "", false);
(void) wait_for(installer, "demo_slow_sibling", "queued");
const auto sibling_stopping = installer.stop("demo_slow_sibling");
require(sibling_stopping.find("\"state\":\"cancelling\"") != std::string::npos,
"queued sibling accepts cancellation while another package is active");
(void) wait_for(installer, "demo_slow_sibling", "cancelled");
require(installer.status("demo_slow").find("\"state\":\"running\"") != std::string::npos,
"cancelling a queued sibling does not stop the active package");
(void) installer.stop("demo_slow");
(void) wait_for(installer, "demo_slow", "cancelled");
require(!std::filesystem::exists(root / "models" / "Slow" / "sibling.gguf"),
"cancelled queued sibling is never published");
const auto partial = root / "models" / ".Demo.abandoned";
write(partial / "model.tmp", "partial");
const auto cleaned = installer.clean_partial("demo_q8");
require(cleaned.find("\"cleaned\":true") != std::string::npos, "partial cleanup succeeds");
require(!std::filesystem::exists(partial), "partial staging directory is removed");
const auto initial = installer.package_sizes();
require(initial.find("\"state\":\"running\"") != std::string::npos,
"remote inventory starts in the background");
bool inventory_complete = false;
for (int attempt = 0; attempt < 200; ++attempt) {
const auto sizes = installer.package_sizes();
if (sizes.find("\"state\":\"complete\"") != std::string::npos) {
require(sizes.find("\"id\":\"demo_f16\"") != std::string::npos,
"inventory includes package ids");
require(sizes.find("\"installed\":true") != std::string::npos,
"inventory reports installed packages");
require(sizes.find("\"version_state\":\"up_to_date\"") != std::string::npos,
"per-file ETag version check reports up to date");
inventory_complete = true;
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
require(inventory_complete, "remote package inventory completes");
bool legacy_missing = false;
try { (void) installer.start("demo_q8", "checkpoint.bin", "", "", "", false); }
catch (const std::runtime_error & error) {
legacy_missing = std::string(error.what()).find("model_manager_deprecated.py") != std::string::npos;
}
require(legacy_missing, "explicit converter inputs retain the legacy converter boundary");
require(fixture.get() == 0, "local HTTP fixture completed normally");
} catch (...) {
std::error_code error;
std::filesystem::remove_all(root, error);
throw;
}
std::error_code error;
std::filesystem::remove_all(root, error);
}
void test_large_inventory() {
const auto root = make_root();
std::string spec = R"({"family":"demo","packages":[)";
for (int i = 0; i < 500; ++i) {
if (i) spec += ',';
spec += "{\"id\":\"demo_" + std::to_string(i) + R"(","display_name":"Demo",
"format":"gguf","precision":"f32","target_directory":"Demo","files":[],
"download":{"kind":"huggingface_snapshot","repo":"org/repo"}})";
}
spec += "]}";
write(root / "model_specs" / "demo.json", spec);
try {
minitts::server::ModelInstaller installer(root, root / "models");
bool complete = false;
for (int i = 0; i < 200; ++i) {
const auto text = installer.package_sizes();
const auto response = engine::io::json::parse(text);
require(response.require("data").as_array().size() == 500,
"inventory larger than 64 KiB must retain every package");
require(text.size() > 65536, "fixture must exceed the old log-tail limit");
if (response.require("state").as_string() == "complete") {
complete = true;
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
require(complete, "large inventory completes");
} catch (...) {
std::filesystem::remove_all(root);
throw;
}
std::filesystem::remove_all(root);
}
} // namespace
int main() {
try {
test_native_package_info();
test_native_package_lifecycle();
test_large_inventory();
}
catch (const std::exception & error) {
std::cerr << error.what() << '\n';
return 1;
}
std::cout << "server_model_installer_test passed\n";
return 0;
}