-
Notifications
You must be signed in to change notification settings - Fork 80
Expand file tree
/
Copy pathlog_unit.cpp
More file actions
197 lines (169 loc) · 7.31 KB
/
Copy pathlog_unit.cpp
File metadata and controls
197 lines (169 loc) · 7.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
// log_unit.cpp - white-box tests for the process-global log sink.
//
// Pins the three-state contract of transcribe_log_set:
//
// never configured -> log_msg falls back to stderr (not directly
// assertable here; covered by the callback states)
// callback installed -> library messages AND ggml diagnostics (via the
// bridge transcribe_log_set installs with
// ggml_log_set) reach the callback
// explicitly NULL -> everything is dropped, including ggml messages
//
// and the ggml bridge specifics: level MAPPING (ggml's DEBUG/INFO/WARN/
// ERROR numbering differs from transcribe_log_level — a raw reinterpret
// would scramble severities) and trailing-newline normalization (ggml
// messages embed "\n"; transcribe messages do not).
//
// The log sink is process-global state; this binary owns it exclusively,
// and each test installs the state it needs.
#include "ggml.h"
#include "transcribe-log.h"
#include "transcribe.h"
#include <cstdio>
#include <cstdlib>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
namespace {
int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
// Recording sink. userdata carries the vector so the tests also pin that
// userdata is delivered alongside the callback.
using Record = std::vector<std::pair<transcribe_log_level, std::string>>;
void recording_cb(transcribe_log_level level, const char * msg, void * userdata) {
auto * rec = static_cast<Record *>(userdata);
rec->emplace_back(level, msg != nullptr ? msg : "<null>");
}
void test_callback_receives_log_msg() {
Record rec;
transcribe_log_set(recording_cb, &rec);
transcribe::log_msg(TRANSCRIBE_LOG_LEVEL_WARN, "answer=%d", 42);
CHECK(rec.size() == 1);
CHECK(rec[0].first == TRANSCRIBE_LOG_LEVEL_WARN);
CHECK(rec[0].second == "answer=42");
}
void throwing_cb(transcribe_log_level, const char *, void * userdata) {
// Count the invocation before throwing so the test can tell "contained"
// from "never called".
++(*static_cast<int *>(userdata));
throw std::runtime_error("host log callback bug");
}
void test_throwing_callback_is_contained() {
// If containment is broken, the throw escapes log_msg and the test dies.
int calls = 0;
transcribe_log_set(throwing_cb, &calls);
transcribe::log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "emitted into a throwing callback");
CHECK(calls == 1);
// The sink must remain functional afterwards for a well-behaved
// callback.
Record rec;
transcribe_log_set(recording_cb, &rec);
transcribe::log_msg(TRANSCRIBE_LOG_LEVEL_INFO, "recovered");
CHECK(rec.size() == 1);
CHECK(rec[0].second == "recovered");
}
void test_null_disables_then_reinstall_restores() {
Record rec;
transcribe_log_set(recording_cb, &rec);
transcribe::log_msg(TRANSCRIBE_LOG_LEVEL_INFO, "before");
CHECK(rec.size() == 1);
// Explicit NULL = silence: the message must not reach the previous
// callback (and per the public contract it goes nowhere at all).
transcribe_log_set(nullptr, nullptr);
transcribe::log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "dropped");
CHECK(rec.size() == 1);
// Reinstalling brings messages back.
transcribe_log_set(recording_cb, &rec);
transcribe::log_msg(TRANSCRIBE_LOG_LEVEL_INFO, "after");
CHECK(rec.size() == 2);
CHECK(rec[1].second == "after");
}
void test_ggml_bridge_installed_and_maps_levels() {
Record rec;
transcribe_log_set(recording_cb, &rec);
// transcribe_log_set must have routed ggml's logger to its bridge.
ggml_log_callback bridge = nullptr;
void * bridge_userdata = nullptr;
ggml_log_get(&bridge, &bridge_userdata);
CHECK(bridge != nullptr);
if (bridge == nullptr) {
return; // cannot exercise the mapping without the bridge
}
// Drive the bridge exactly as ggml would. The numeric values of the
// two enums DIFFER (ggml DEBUG=1 INFO=2 WARN=3 ERROR=4 vs transcribe
// INFO=1 WARN=2 ERROR=3 DEBUG=4): each ggml level must arrive as the
// SAME-MEANING transcribe level, not the same number.
bridge(GGML_LOG_LEVEL_DEBUG, "d\n", bridge_userdata);
bridge(GGML_LOG_LEVEL_INFO, "i\n", bridge_userdata);
bridge(GGML_LOG_LEVEL_WARN, "w\n", bridge_userdata);
bridge(GGML_LOG_LEVEL_ERROR, "e\n", bridge_userdata);
bridge(GGML_LOG_LEVEL_CONT, ".", bridge_userdata);
CHECK(rec.size() == 5);
if (rec.size() == 5) {
CHECK(rec[0].first == TRANSCRIBE_LOG_LEVEL_DEBUG);
CHECK(rec[1].first == TRANSCRIBE_LOG_LEVEL_INFO);
CHECK(rec[2].first == TRANSCRIBE_LOG_LEVEL_WARN);
CHECK(rec[3].first == TRANSCRIBE_LOG_LEVEL_ERROR);
CHECK(rec[4].first == TRANSCRIBE_LOG_LEVEL_CONT);
// Exactly one trailing newline is stripped (transcribe messages
// carry none); CONT fragments pass through unmodified.
CHECK(rec[0].second == "d");
CHECK(rec[1].second == "i");
CHECK(rec[4].second == ".");
}
}
void test_init_backends_emits_device_summary() {
// Release ggml silences its per-module load diagnostics, so the
// library's own post-scan summary is the one reliable "which backends
// made it" signal a host callback gets. It must arrive on a FRESH scan
// (idempotent repeats stay quiet) and name at least one device in this
// static build.
Record rec;
transcribe_log_set(recording_cb, &rec);
CHECK(transcribe_init_backends(".") == TRANSCRIBE_OK);
bool saw_summary = false;
for (const auto & entry : rec) {
if (entry.first == TRANSCRIBE_LOG_LEVEL_INFO &&
entry.second.find("compute device(s) registered") != std::string::npos) {
saw_summary = true;
}
}
CHECK(saw_summary);
// Idempotent repeat: same directory, no second summary.
const size_t n_before = rec.size();
CHECK(transcribe_init_backends(".") == TRANSCRIBE_OK);
CHECK(rec.size() == n_before);
}
void test_ggml_bridge_honors_disable() {
Record rec;
transcribe_log_set(recording_cb, &rec);
ggml_log_callback bridge = nullptr;
void * bridge_userdata = nullptr;
ggml_log_get(&bridge, &bridge_userdata);
CHECK(bridge != nullptr);
if (bridge == nullptr) {
return;
}
// Disabling the transcribe sink must also silence ggml's diagnostics:
// the bridge stays installed but drops on the disabled state.
transcribe_log_set(nullptr, nullptr);
bridge(GGML_LOG_LEVEL_ERROR, "dropped\n", bridge_userdata);
CHECK(rec.empty());
}
} // namespace
int main() {
test_callback_receives_log_msg();
test_throwing_callback_is_contained();
test_null_disables_then_reinstall_restores();
test_ggml_bridge_installed_and_maps_levels();
test_init_backends_emits_device_summary();
test_ggml_bridge_honors_disable();
return g_failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}