mirror of
https://github.com/Helldez/BigMoeOnEdge.git
synced 2026-10-19 15:33:46 +00:00
* docs: a security policy, the forms a contributor expects, and indexes that list every page
SECURITY.md says how to report a problem privately and what is in scope. A
bug-report form and a pull-request checklist join the benchmark form. The docs
index lists the framework, screens and design pages; the bench-data index lists
all eighteen sessions; the agent guide names session.cpp as the composition
root; and the public pages call the test phone by its class, not its model.
* refactor(params): RunConfig holds tunables only, and the table names every bound
--progress and --compute-trace-layers wrote RunConfig fields the parameter table had no row for.
Neither is a tunable of a run: the line protocol is the CLI's output mode and reaches the engine as
RunOutputs::render_text (run() takes its callback, sinks and that switch as one RunOutputs), and
the compute trace's granularity belongs to the trace (IComputeTraceSink::layers_only()).
BMOE_PROGRESS goes through the CLI's env-override table, --ppl-skip through the table's strict
integer parser (now public as parse_int_strict), the last literal bounds are named constants
validate() checks too, and the sink factories return std::unique_ptr.
* fix(server): a malformed request is a 400, an answer fetches nothing, a network client opens only the models folder
A field of the wrong type threw out of the JSON library as a 500; reads are now type-checked, a
body must be an object, and a JSON exception a handler still throws is a 400. Model output keeps
an image only when its bytes are inline, and every response carries a Content-Security-Policy with
nosniff and no-referrer. Sec-Fetch-Site same-site is refused on the API like cross-site.
--api-key-file keeps the key off the command line. A client whose peer address is not loopback
names models inside the models directory only, after canonicalisation; --models-only confines
local clients too. The default host, port, context and answer length are named in server.h.
* ci: a typed tag or a signing secret reaches the shell as data, never as script
inputs.tag and the release tag name were interpolated into run: lines, and the keystore passwords
into an unquoted heredoc. All of them now travel through env: and quoted expansions (printf '%s'
for the properties file), a tag that is not a plain ref name stops the run, and ci.yml declares
contents: read at the top, the APK job keeping its own write permission.
* fix(io): direct reads are aligned to what the file's filesystem states, not to 4096
Every reader cut its O_DIRECT windows at a literal 4 KiB and the storage probe
reported that literal as a measurement. The reader now asks the descriptor it
opened (pio::direct_alignment: statx STATX_DIOALIGN on Linux/Android where the
kernel and sandbox allow it, the filesystem block size otherwise, the volume
sector sizes on Windows, the block size on Apple) with one named fallback, and
destination buffers are aligned to that granule and never less than a page.
The refault price is read at the VM page, row-stream slabs are whole pages,
the readiness flags are padded to the widest cache line, the swap probe finds
any zram device, and the spin pause and the decide probe's file reads moved
into core/src/io so no OS branch is left in core/src/moe.
* test(gates): the RPC device listens on a port the kernel picks, not on 50599
Two gate runs on one machine (two build trees, two CI jobs on one runner) met
on the fixed port: the second failed to bind, or registered the first's server.
* refactor: one probe-and-plan sequence in core, and every tensor name read in one place
bmoe-cli --auto and the server's /api/plan each carried the planner's probe
calls in the same order, the cache-floor arithmetic and the device-probe
timeout. Both now call probe_and_plan (bmoe/auto_plan.h) with what differs as
options, and the isolated probe's child is probe_device_child; neither
front-end includes llama.h to plan.
plan_run is a list of named stages over one planning state and allocate a
list of named steps; every plan the planner tests make is identical before and
after. Rationale that tells a person what to type spells the flag through the
parameter table (flag_spelling). The smallest suggested context and the
fitter's device margin and minimum context are named constants.
Tensor, graph-node and fitter override-pattern names are parsed by
core/src/moe/tensor_names, replacing the sscanf, strncmp and std::regex
copies in the router hook, decide probe, model probe, prefill path, session
and placement probe. Unit-tested.
* refactor(server,ui): the UIs read the facts the server reports, not engine parameters
The engine reports the wall-additive flash term already chosen for the run's
mode (flash_wait_ms per token, flash_wait_s_tok in the summary), and the loaded
model in /api/info carries the threads a CPU figure is read against
(compute_threads, busy_threads). The web UI's live line, metrics card and
streaming badge read those facts and name no engine parameter.
--scan-dir PATH lists models outside the models directory, read in place;
/api/models adds scan_dirs and each model's dir and files. Split sets are
recognised by an explicit, tested parser (server/model_files.h) instead of
std::regex. The Hexagon backend's environment moves into server/platform.cpp.
The web UI checks the shape of every answer and event it reads, asks for the
config again on reconnection, and keeps a failed message in the transcript with
Send again, as docs/screens.md specifies; the chat store's failure path is
tested against a stubbed server.
* refactor(android): the app reads the server's facts and labels, and keeps no model logic
The live line and the metrics card read the flash term and the threads the
server reports instead of the threads, io-threads and overlap settings with
invented defaults. A recorded run's settings are labelled, typed and explained
by the parameter table; the hand-kept preamble labels are gone, and a run's
identity names every setting that changes the reply and is off its default.
GgufHeader.kt and the shard-name regexes are deleted: the service passes the
phone's other model folders as --scan-dir, the Models screen lists what the
server lists, and a delete removes the files the server named. A summary's rate
is read by field name, a chat error by its code, a closed port by
ConnectException. A failed message can be sent again, and a failed decision
leaves no question behind.
* fix(android): the engine always takes a key, the app proves it is its own, and stops off the main thread
Loopback on a phone is reachable by every installed app, and the server ran
there without a key unless shared: any app could read the conversations off
/api/events. The server now always starts with a key, random per launch or the
shown one when shared, handed over in a 0600 file in the app's private storage
(--api-key-file), and every request carries it. Before trusting the server on
the port the app checks it refuses a random key and accepts its own; a port
held by another program is reported and sent nothing.
Starts and stops run one at a time on a background thread, a restart starts
the next server only once the previous one has ended, and a stopped server's
late exit no longer overwrites the state of its replacement. Every call has a
read timeout fitted to it, the event stream a watchdog of two missed pings, a
cancelled coroutine is not read as a failed call, and the port is one constant.
* refactor(session): open and generate read as their steps, and diagnostics go through a log port
Session::open() (~690 lines) and Session::generate() (~770) become short
sequences of named steps on the Impl: an Opening context for the load
(load_model ... warn_about_load) and a Turn for one generation
(render_prompt, tokenize_prompt, reuse_kv_prefix, prefill, decode with
draft / verify_decode / accept / emit, trim_speculative_kv, summarize,
finish). Same order of operations, error kinds, messages and metrics;
the byte-identity gates pass. RouterHook::on_eval dispatches to
ctrace_observe, capture_sources, arena_eval, serve_rows and on_topk,
each reached only when its feature is on.
bmoe/log.h: set_log_sink / set_log_level and log_printf. Core's 70
diagnostic fprintf(stderr) calls go through it; the default sink writes
the same 'bmoe: <line>' bytes. Protocol lines never pass through it.
* test(server): the engine host's state machine, driven with a scripted session
EngineHost opens sessions through a SessionOpener over the narrow
HostedSession view of a Session (server/hosted_session.h), the real
Session by default. tests/engine_host_test.cpp drives loads, turns while
loading, cancel and unload mid-turn, a failed decode closing the session
and a recoverable kind keeping it, and decide's fatal rule.
The host also installs a log sink: the same stderr line, plus a 'log'
event (level, message) on /api/events.
* ci: the gates under ASan and UBSan, on macOS too, and the web UI type-checked
A Linux job runs ctest with -fsanitize=address,undefined (RelWithDebInfo,
llama.cpp instrumented too); upstream findings this project does not
patch are listed in tests/sanitizers/ubsan.supp. The macOS job now builds
the tests and runs the gates. The ui job adds npm run check and the
build. Windows stays compile-only, with the reason in the workflow.
* docs(architecture): the layer map names every header and source the engine has
The map left out the session, chat, decide, n-gram draft, trace and version
headers, the stall union and the chat-message adapter. The roadmap names the
I/O bench by its source file.
* docs(changelog): this work is unreleased, not part of 0.30.0
0.30.0 was cut while these changes were in review, so their entries moved from
its section to a new [Unreleased] one, grouped as added, changed and fixed.
* fix(prefill): the dense layer slots are one table, indexed as they were sized
Choosing between two member arrays with a conditional and indexing the result
was read as the wrong parity for the matrix slot by GCC 13 under ASan+UBSan, so
nemotron_h_moe's recurrent layer landed in the smaller slot and ggml aborted
(gate G17, found by the new sanitizer job). One [data][parity] table, indexed
the way init_dense sizes it.
* ci: llama.cpp's process-lifetime RPC objects are listed for LeakSanitizer
The RPC registry, its device and buffer type, and the loopback rpc-server the
prefill-device gate starts live as long as the process upstream; they are named
one by one in tests/sanitizers/lsan.supp, as the UBSan findings are.
* fix: the probed read granule keeps its floor, the flag padding follows the target, and the slot rewrite is not called a fix
Three findings of the review, each a change this branch made without the measurement or the
case that would justify it, taken back to what is known:
- The direct-read granule statx reports is the legality floor, and on ext4 over a 512-byte-sector
NVMe it says 512. Reading at 512 where 4096 was read before is a throughput change on the SSDs
this project has measured punishing small reads, and it was not measured, so the granule never
goes under direct_align_fallback. The kernel's answer still raises it (16 KiB f2fs).
- The padding between the streamer's ready flags went from 64 to 128 bytes everywhere because
Apple's cores have 128-byte lines. It sizes the hottest poll in the engine, one flag per expert
and projection, so it now follows the target: 128 on Apple arm64, 64 elsewhere.
- The dense-slot rewrite in the prefill arena selects the same buffer as the conditional it
replaced and cannot have fixed the G17 abort seen under ASan+UBSan. The comment and the
changelog say so, and record that fault as open rather than as a compiler bug nobody reproduced.
Also: the Android picker now lists dense ggufs too, labelled, which the changelog did not say;
the agent guide notes the server's two env writes; the gate's port helper describes the window it
actually has.
* fix(server): a turn sent the moment a load is announced waits for the load
load() reads "loading" before its thread has taken the generation lock,
and a request in that gap took the lock first, found no session and
answered 503 for a model seconds from ready. The engine host test caught
it on Windows ("a turn waits for a load that will fail", intermittent).
A turn now takes the lock through take_turn(): while the state reads
loading it gives the lock back and waits on the state, then takes it once
the load has ended either way. The state change notifies through
publish_state(), which every transition already calls. The test flushes
each check, so a check that hangs the next one is still on record.
353 lines
14 KiB
C++
353 lines
14 KiB
C++
// The engine host's state machine (server/engine_host.h), driven with a scripted session in place
|
|
// of a model: what a load, a turn, a cancel and an unload do to one another across threads, and
|
|
// which failures close the session. No model, no llama.cpp graph — the session is a fake behind
|
|
// the host's one seam (server/hosted_session.h).
|
|
//
|
|
// * a load runs in the background and ends ready, or in error with the opener's reason;
|
|
// * a turn with nothing loaded is a 503 `no_model`; a turn sent while a load runs waits for it,
|
|
// then runs on the new session or gets the 503 if the load failed;
|
|
// * cancel() and unload() during a turn stop it: the turn answers cancelled, and unload leaves the
|
|
// host empty with the session gone;
|
|
// * a failed decode closes the session (500, state error), a recoverable kind keeps it (400);
|
|
// * decide() follows the same rule on `fatal`;
|
|
// * the engine's diagnostics reach the bus as `log` events while a host lives.
|
|
//
|
|
// Checks are explicit (not <cassert>): the Release build defines NDEBUG, which compiles assert out.
|
|
|
|
#include "engine_host.h"
|
|
|
|
#include "bmoe/log.h"
|
|
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <cstdio>
|
|
#include <functional>
|
|
#include <future>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <thread>
|
|
|
|
using namespace bmoe;
|
|
using namespace bmoe::server;
|
|
using namespace std::chrono_literals;
|
|
|
|
static int failures = 0;
|
|
|
|
static void check(bool ok, const std::string & what) {
|
|
std::printf("[%s] %s\n", ok ? "PASS" : "FAIL", what.c_str());
|
|
std::fflush(stdout); // a check that hangs the next one is still on record
|
|
if (!ok) ++failures;
|
|
}
|
|
|
|
// A one-shot gate a test opens from its own thread.
|
|
struct Gate {
|
|
std::mutex m;
|
|
std::condition_variable cv;
|
|
bool open = false;
|
|
void release() {
|
|
std::lock_guard<std::mutex> lk(m);
|
|
open = true;
|
|
cv.notify_all();
|
|
}
|
|
bool wait(std::chrono::milliseconds limit = 5s) {
|
|
std::unique_lock<std::mutex> lk(m);
|
|
return cv.wait_for(lk, limit, [&] { return open; });
|
|
}
|
|
};
|
|
|
|
// What the fake sessions of one test share with it: how each turn ends, and what happened.
|
|
struct Script {
|
|
// How generate() ends. Blocking turns signal `started` and run until cancel().
|
|
enum class Turn { Ok, Block, Fail, ContextFull } turn = Turn::Ok;
|
|
DecideResult decide_result;
|
|
|
|
Gate started;
|
|
std::atomic<int> opened{0};
|
|
std::atomic<int> alive{0};
|
|
std::atomic<int> turns{0};
|
|
};
|
|
|
|
class FakeSession final : public HostedSession {
|
|
public:
|
|
explicit FakeSession(Script & s) : s_(s) { ++s_.alive; }
|
|
~FakeSession() override { --s_.alive; }
|
|
|
|
RunResult generate(const GenerateRequest &, const std::function<void(const TokenMetrics &)> & on_token) override {
|
|
++s_.turns;
|
|
RunResult r;
|
|
switch (s_.turn) {
|
|
case Script::Turn::Ok: {
|
|
TokenMetrics m;
|
|
m.step = 1;
|
|
m.steps = 1;
|
|
if (on_token) on_token(m);
|
|
r.ok = true;
|
|
r.generated_text = "hi";
|
|
r.summary.n_generated = 1;
|
|
return r;
|
|
}
|
|
case Script::Turn::Block:
|
|
s_.started.release();
|
|
// As the abort callback does: a cancel ends the decode, and the turn answers cancelled.
|
|
for (int i = 0; i < 5000 && !cancelled_.load(); ++i)
|
|
std::this_thread::sleep_for(1ms);
|
|
r.ok = true;
|
|
r.cancelled = cancelled_.load();
|
|
return r;
|
|
case Script::Turn::Fail:
|
|
r.error = "decode failed during generation";
|
|
r.error_kind = RunError::Failed;
|
|
return r;
|
|
case Script::Turn::ContextFull:
|
|
r.error = "the conversation exceeds the session n_ctx";
|
|
r.error_kind = RunError::ContextFull;
|
|
return r;
|
|
}
|
|
return r;
|
|
}
|
|
DecideResult decide(const DecideRequest &) override { return s_.decide_result; }
|
|
void cancel() override { cancelled_ = true; }
|
|
|
|
const std::string & arch() const override { return arch_; }
|
|
int n_ctx() const override { return 4096; }
|
|
int n_expert_used() const override { return 8; }
|
|
ThinkControl think_control() const override { return ThinkControl::Template; }
|
|
double load_seconds() const override { return 0.25; }
|
|
|
|
private:
|
|
Script & s_;
|
|
std::atomic<bool> cancelled_{false};
|
|
std::string arch_ = "fakemoe";
|
|
};
|
|
|
|
// An opener over `script`. A non-null `hold` keeps the load open until the test releases it; a
|
|
// non-empty `fail` makes the load fail with that reason.
|
|
static SessionOpener opener(Script & script, Gate * hold = nullptr, std::string fail = "") {
|
|
return [&script, hold, fail](const SessionConfig &, std::string & error) -> std::unique_ptr<HostedSession> {
|
|
if (hold) hold->wait();
|
|
++script.opened;
|
|
if (!fail.empty()) {
|
|
error = fail;
|
|
return nullptr;
|
|
}
|
|
return std::make_unique<FakeSession>(script);
|
|
};
|
|
}
|
|
|
|
static RunConfig config() {
|
|
RunConfig c;
|
|
c.model_path = "models/fake.gguf"; // never opened: the fake opener stands in for the load
|
|
return c;
|
|
}
|
|
|
|
static std::string state(const EngineHost & h) {
|
|
return h.info().value("state", "");
|
|
}
|
|
|
|
// Wait until the host's state reads `want`, or give up.
|
|
static bool wait_state(const EngineHost & h, const std::string & want, std::chrono::milliseconds limit = 5s) {
|
|
const auto end = std::chrono::steady_clock::now() + limit;
|
|
while (std::chrono::steady_clock::now() < end) {
|
|
if (state(h) == want) return true;
|
|
std::this_thread::sleep_for(2ms);
|
|
}
|
|
return state(h) == want;
|
|
}
|
|
|
|
static ChatRequest turn() {
|
|
ChatRequest r;
|
|
ChatMessage m;
|
|
m.role = "user";
|
|
m.content = "hello";
|
|
r.messages.push_back(m);
|
|
r.n_predict = 4;
|
|
return r;
|
|
}
|
|
|
|
static void load_tests() {
|
|
{
|
|
EventBus bus;
|
|
Script script;
|
|
EngineHost host(bus, opener(script));
|
|
check(state(host) == "empty" && !host.ready(), "a new host holds nothing");
|
|
const ChatOutcome none = host.chat(turn(), nullptr);
|
|
check(none.status == 503 && none.code == error_code::no_model, "a turn with nothing loaded is a 503 no_model");
|
|
|
|
check(host.load(config(), config()).empty(), "a valid config starts a load");
|
|
check(wait_state(host, "ready") && host.ready(), "the load ends ready");
|
|
const json info = host.info();
|
|
check(info["model"].value("arch", "") == "fakemoe" && info["model"].value("n_ctx", 0) == 4096 &&
|
|
info["model"].value("name", "") == "fake.gguf",
|
|
"the ready state describes the session the opener made");
|
|
check(host.model_name() == "fake.gguf" && script.opened == 1, "one load opened one session");
|
|
|
|
RunConfig bad = config();
|
|
bad.model_path.clear();
|
|
check(!host.load(bad, bad).empty() && host.ready(),
|
|
"a config that cannot start is refused, and changes nothing");
|
|
|
|
check(host.load(config(), config()).empty() && wait_state(host, "ready"), "a second load replaces the first");
|
|
check(script.opened == 2 && script.alive == 1, "the replaced session is gone before the new one runs");
|
|
}
|
|
{
|
|
EventBus bus;
|
|
Script script;
|
|
EngineHost host(bus, opener(script, nullptr, "failed to load model: models/fake.gguf"));
|
|
host.load(config(), config());
|
|
check(wait_state(host, "error") && !host.ready(), "a failed load ends in error");
|
|
check(host.info().value("error", "") == "failed to load model: models/fake.gguf",
|
|
"the error is the opener's reason");
|
|
}
|
|
}
|
|
|
|
static void loading_tests() {
|
|
{
|
|
EventBus bus;
|
|
Script script;
|
|
Gate hold;
|
|
EngineHost host(bus, opener(script, &hold));
|
|
host.load(config(), config());
|
|
check(wait_state(host, "loading") && host.loaded_or_loading() && !host.ready(), "a held load reads loading");
|
|
check(host.load(config(), config()) == "a model is already loading", "a load while loading is refused");
|
|
|
|
// The turn takes its place in line and waits out the load, which holds the generation lock.
|
|
auto pending = std::async(std::launch::async, [&] { return host.chat(turn(), nullptr); });
|
|
check(pending.wait_for(50ms) == std::future_status::timeout, "a turn sent while loading waits for the load");
|
|
hold.release();
|
|
const ChatOutcome out = pending.get();
|
|
check(out.status == 200 && out.result.generated_text == "hi", "then runs on the session the load made");
|
|
}
|
|
{
|
|
EventBus bus;
|
|
Script script;
|
|
Gate hold;
|
|
EngineHost host(bus, opener(script, &hold, "no MoE recipe"));
|
|
host.load(config(), config());
|
|
wait_state(host, "loading");
|
|
auto pending = std::async(std::launch::async, [&] { return host.chat(turn(), nullptr); });
|
|
check(pending.wait_for(50ms) == std::future_status::timeout, "a turn waits for a load that will fail");
|
|
hold.release();
|
|
const ChatOutcome out = pending.get();
|
|
check(out.status == 503 && out.code == error_code::no_model, "and gets the 503 once it has");
|
|
}
|
|
}
|
|
|
|
static void cancel_tests() {
|
|
{
|
|
EventBus bus;
|
|
Script script;
|
|
script.turn = Script::Turn::Block;
|
|
EngineHost host(bus, opener(script));
|
|
host.load(config(), config());
|
|
wait_state(host, "ready");
|
|
auto pending = std::async(std::launch::async, [&] { return host.chat(turn(), nullptr); });
|
|
check(script.started.wait(), "the turn is generating");
|
|
check(host.generating(), "the host says so");
|
|
host.cancel();
|
|
const ChatOutcome out = pending.get();
|
|
check(out.status == 200 && out.result.cancelled, "cancel() during a turn answers it cancelled");
|
|
check(host.ready() && !host.generating() && script.alive == 1, "and keeps the session");
|
|
}
|
|
{
|
|
EventBus bus;
|
|
Script script;
|
|
script.turn = Script::Turn::Block;
|
|
EngineHost host(bus, opener(script));
|
|
host.load(config(), config());
|
|
wait_state(host, "ready");
|
|
auto pending = std::async(std::launch::async, [&] { return host.chat(turn(), nullptr); });
|
|
script.started.wait();
|
|
host.unload(); // cancels the turn, waits for it, then drops the session
|
|
const ChatOutcome out = pending.get();
|
|
check(out.status == 200 && out.result.cancelled, "unload() during a turn cancels it");
|
|
check(state(host) == "empty" && !host.loaded_or_loading() && script.alive == 0,
|
|
"and leaves the host empty, the session closed");
|
|
const ChatOutcome after = host.chat(turn(), nullptr);
|
|
check(after.status == 503, "the next turn finds no model");
|
|
}
|
|
}
|
|
|
|
static void failure_tests() {
|
|
{
|
|
EventBus bus;
|
|
Script script;
|
|
script.turn = Script::Turn::ContextFull;
|
|
EngineHost host(bus, opener(script));
|
|
host.load(config(), config());
|
|
wait_state(host, "ready");
|
|
const ChatOutcome out = host.chat(turn(), nullptr);
|
|
check(out.status == 400 && out.code == "context_full", "a recoverable kind is a 400 with its code");
|
|
check(host.ready() && script.alive == 1, "and keeps the session");
|
|
}
|
|
{
|
|
EventBus bus;
|
|
Script script;
|
|
script.turn = Script::Turn::Fail;
|
|
EngineHost host(bus, opener(script));
|
|
host.load(config(), config());
|
|
wait_state(host, "ready");
|
|
const ChatOutcome out = host.chat(turn(), nullptr);
|
|
check(out.status == 500 && out.code == "failed", "a failed decode is a 500");
|
|
check(state(host) == "error" && script.alive == 0, "and closes the session");
|
|
check(host.info().value("error", "").find("the session failed and was closed") == 0,
|
|
"the error says the session was closed");
|
|
check(host.chat(turn(), nullptr).status == 503, "the next turn finds no model");
|
|
}
|
|
{
|
|
EventBus bus;
|
|
Script script;
|
|
script.decide_result.error = "no choices";
|
|
script.decide_result.error_kind = RunError::InvalidRequest;
|
|
EngineHost host(bus, opener(script));
|
|
host.load(config(), config());
|
|
wait_state(host, "ready");
|
|
const DecideOutcome refused = host.decide(DecideRequest{});
|
|
check(refused.status == 400 && refused.code == "invalid_request" && host.ready(),
|
|
"a refused decision is a 400 and keeps the session");
|
|
script.decide_result.fatal = true;
|
|
script.decide_result.error_kind = RunError::Failed;
|
|
const DecideOutcome failed = host.decide(DecideRequest{});
|
|
check(failed.status == 500 && state(host) == "error" && script.alive == 0,
|
|
"a fatal decision is a 500 and closes the session");
|
|
}
|
|
}
|
|
|
|
static void log_tests() {
|
|
EventBus bus;
|
|
Script script;
|
|
auto q = bus.subscribe();
|
|
{
|
|
EngineHost host(bus, opener(script));
|
|
log_printf(LogLevel::Warn, "prefill stays on the %s", "CPU");
|
|
std::string frame;
|
|
bool seen = false;
|
|
while (EventBus::next(*q, frame, 100ms) && !frame.empty())
|
|
if (frame.find("event: log\n") == 0)
|
|
seen = frame.find("\"level\":\"warn\"") != std::string::npos &&
|
|
frame.find("\"message\":\"prefill stays on the CPU\"") != std::string::npos;
|
|
check(seen, "an engine diagnostic reaches the bus as a log event");
|
|
}
|
|
log_printf(LogLevel::Info, "after the host");
|
|
std::string frame;
|
|
bool seen = false;
|
|
while (EventBus::next(*q, frame, 50ms) && !frame.empty())
|
|
seen = seen || frame.find("after the host") != std::string::npos;
|
|
check(!seen, "a host gone hands the log back to stderr");
|
|
bus.unsubscribe(q);
|
|
}
|
|
|
|
int main() {
|
|
load_tests();
|
|
loading_tests();
|
|
cancel_tests();
|
|
failure_tests();
|
|
log_tests();
|
|
if (failures) {
|
|
std::printf("\n%d check(s) FAILED\n", failures);
|
|
return 1;
|
|
}
|
|
std::printf("\nall engine host checks passed\n");
|
|
return 0;
|
|
}
|