247 lines
8.1 KiB
Plaintext
247 lines
8.1 KiB
Plaintext
// The logger's pieces on their own: the MPSC queue under contention, the Event Log ring's
|
|
// capacity changes, and the event_log_capacity setting's parsing.
|
|
|
|
#scope_file
|
|
|
|
checks_run := 0;
|
|
checks_failed := 0;
|
|
|
|
MAX_GROUPS :: 8;
|
|
group_names : [MAX_GROUPS] string;
|
|
group_checks : [MAX_GROUPS] int;
|
|
group_fails : [MAX_GROUPS] int;
|
|
group_count := 0;
|
|
|
|
output_builder: *String_Builder;
|
|
|
|
emit :: (format_string: string, args: ..Any) {
|
|
if output_builder print_to_builder(output_builder, format_string, ..args);
|
|
else print(format_string, ..args);
|
|
} @PrintLike
|
|
|
|
group :: (name: string) {
|
|
assert(group_count < MAX_GROUPS, "raise MAX_GROUPS");
|
|
group_names[group_count] = name;
|
|
group_count += 1;
|
|
emit("\n-- % --\n", name);
|
|
}
|
|
|
|
expect :: (condition: bool, format: string, args: ..Any, loc := #caller_location) {
|
|
checks_run += 1;
|
|
if group_count group_checks[group_count-1] += 1;
|
|
if condition return;
|
|
|
|
checks_failed += 1;
|
|
if group_count group_fails[group_count-1] += 1;
|
|
emit(" FAIL [%:%] %\n", group_names[max(group_count-1, 0)], loc.line_number, tprint(format, ..args));
|
|
}
|
|
|
|
report :: () -> bool {
|
|
emit("\n");
|
|
for 0..group_count-1 {
|
|
if group_fails[it] emit(" % -- % checks, % FAILED\n", group_names[it], group_checks[it], group_fails[it]);
|
|
else emit(" % -- % checks\n", group_names[it], group_checks[it]);
|
|
}
|
|
if checks_failed emit("\n% OF % LOGGER CHECKS FAILED\n", checks_failed, checks_run);
|
|
else emit("\nALL LOGGER TESTS PASSED (% checks over % groups)\n", checks_run, group_count);
|
|
return checks_failed == 0;
|
|
}
|
|
|
|
//
|
|
// The queue: producers push numbered nodes while this thread pops; every node must arrive once,
|
|
// and each producer's in the order it pushed them.
|
|
//
|
|
|
|
MPSC_PRODUCERS :: 8;
|
|
MPSC_NODES_PER_PRODUCER :: 50_000;
|
|
MPSC_TIMEOUT_MS :: 30_000;
|
|
|
|
Test_Node :: struct {
|
|
using link: Mpsc_Node;
|
|
producer: s64;
|
|
sequence: s64;
|
|
}
|
|
|
|
Producer :: struct {
|
|
queue: *Mpsc_Queue;
|
|
nodes: [] Test_Node;
|
|
}
|
|
|
|
producer_proc :: (thread: *Thread) -> s64 {
|
|
producer := cast(*Producer) thread.data;
|
|
for * producer.nodes mpsc_push(producer.queue, *it.link);
|
|
return 0;
|
|
}
|
|
|
|
mpsc_tests :: () {
|
|
group("mpsc queue");
|
|
|
|
queue := New(Mpsc_Queue);
|
|
defer free(queue);
|
|
mpsc_init(queue);
|
|
|
|
expect(mpsc_pop(queue) == null, "a new queue popped a node");
|
|
|
|
producers: [MPSC_PRODUCERS] Producer;
|
|
threads: [MPSC_PRODUCERS] Thread;
|
|
started: [MPSC_PRODUCERS] bool;
|
|
for * producers {
|
|
it.queue = queue;
|
|
it.nodes = NewArray(MPSC_NODES_PER_PRODUCER, Test_Node);
|
|
for * node, sequence: it.nodes {
|
|
node.producer = it_index;
|
|
node.sequence = sequence;
|
|
}
|
|
}
|
|
defer for producers array_free(it.nodes);
|
|
|
|
for * threads {
|
|
started[it_index] = thread_init(it, producer_proc, name = tprint("mpsc test %", it_index + 1));
|
|
expect(started[it_index], "could not start producer %", it_index);
|
|
if !started[it_index] continue;
|
|
it.data = *producers[it_index];
|
|
thread_start(it);
|
|
}
|
|
|
|
expected_total := 0;
|
|
for started if it expected_total += MPSC_NODES_PER_PRODUCER;
|
|
|
|
next_sequence: [MPSC_PRODUCERS] s64;
|
|
received := 0;
|
|
out_of_order := 0;
|
|
start := current_time_monotonic();
|
|
while received < expected_total {
|
|
node := cast(*Test_Node) mpsc_pop(queue);
|
|
if !node {
|
|
if to_milliseconds(current_time_monotonic() - start) > MPSC_TIMEOUT_MS break;
|
|
continue;
|
|
}
|
|
if node.sequence != next_sequence[node.producer] out_of_order += 1;
|
|
next_sequence[node.producer] = node.sequence + 1;
|
|
received += 1;
|
|
}
|
|
|
|
for * threads {
|
|
if !started[it_index] continue;
|
|
thread_is_done(it, -1);
|
|
thread_deinit(it);
|
|
}
|
|
|
|
expect(received == expected_total, "received % of % nodes", received, expected_total);
|
|
expect(out_of_order == 0, "% nodes arrived out of their producer's order", out_of_order);
|
|
for next_sequence expect(!started[it_index] || it == MPSC_NODES_PER_PRODUCER, "producer % delivered % of %", it_index, it, MPSC_NODES_PER_PRODUCER);
|
|
expect(mpsc_pop(queue) == null, "the drained queue popped another node");
|
|
|
|
// After a drain the queue is reusable.
|
|
extra: Test_Node;
|
|
mpsc_push(queue, *extra.link);
|
|
expect(cast(*Test_Node) mpsc_pop(queue) == *extra, "a push after the drain did not pop back");
|
|
expect(mpsc_pop(queue) == null, "the queue was not empty after the last pop");
|
|
}
|
|
|
|
//
|
|
// The Event Log ring.
|
|
//
|
|
|
|
new_entry :: (sequence: s64) -> *Log_Message {
|
|
entry := cast(*Log_Message) alloc(size_of(Log_Message),, context.default_allocator);
|
|
memset(entry, 0, size_of(Log_Message));
|
|
entry.thread_index = sequence; // stands in for the line's identity here
|
|
return entry;
|
|
}
|
|
|
|
// The entries' sequence numbers, oldest first, as one string.
|
|
ring_contents :: (ring: *Circle_Buffer(*Log_Message)) -> string {
|
|
builder: String_Builder;
|
|
builder.allocator = temp;
|
|
for 0..count(ring.*)-1 {
|
|
if it append(*builder, " ");
|
|
print_to_builder(*builder, "%", ring.*[it].thread_index);
|
|
}
|
|
return builder_to_string(*builder,, temp);
|
|
}
|
|
|
|
ring_tests :: () {
|
|
group("event log ring");
|
|
|
|
ring: Circle_Buffer(*Log_Message);
|
|
ring.allocator = context.default_allocator;
|
|
defer {
|
|
for ring free(it,, context.default_allocator);
|
|
delete(*ring);
|
|
}
|
|
|
|
freed := event_log_resize(*ring, 4);
|
|
expect(freed == 0 && ring.allocated == 4, "sizing an empty ring freed % and allocated %", freed, ring.allocated);
|
|
|
|
dropped := 0;
|
|
for 1..6 if event_log_insert(*ring, new_entry(it)) dropped += 1;
|
|
expect(dropped == 2, "filling 4 slots with 6 dropped %", dropped);
|
|
expect(ring_contents(*ring) == "3 4 5 6", "after 6 inserts the ring held %", ring_contents(*ring));
|
|
|
|
freed = event_log_resize(*ring, 2);
|
|
expect(freed == 2, "shrinking 4 to 2 freed %", freed);
|
|
expect(ring_contents(*ring) == "5 6", "after shrinking the ring held %", ring_contents(*ring));
|
|
|
|
freed = event_log_resize(*ring, 5);
|
|
expect(freed == 0, "growing freed %", freed);
|
|
expect(ring_contents(*ring) == "5 6", "after growing the ring held %", ring_contents(*ring));
|
|
|
|
for 7..9 if event_log_insert(*ring, new_entry(it)) dropped += 1;
|
|
expect(dropped == 2, "inserts into free slots dropped more, % in all", dropped);
|
|
expect(ring_contents(*ring) == "5 6 7 8 9", "after refilling the ring held %", ring_contents(*ring));
|
|
|
|
event_log_insert(*ring, new_entry(10));
|
|
expect(ring_contents(*ring) == "6 7 8 9 10", "past capacity again the ring held %", ring_contents(*ring));
|
|
}
|
|
|
|
//
|
|
// The setting.
|
|
//
|
|
|
|
settings_tests :: () {
|
|
group("event_log_capacity setting");
|
|
|
|
Case :: struct { value: string; seen: bool; capacity: s64; }
|
|
CASES :: Case.[
|
|
.{"100", true, 100},
|
|
.{" 250 ", true, 250},
|
|
.{"5", true, EVENT_LOG_CAPACITY_MIN},
|
|
.{"2000000", true, EVENT_LOG_CAPACITY_MAX},
|
|
.{"abc", false, EVENT_LOG_CAPACITY_DEFAULT},
|
|
.{"12abc", false, EVENT_LOG_CAPACITY_DEFAULT},
|
|
.{"", false, EVENT_LOG_CAPACITY_DEFAULT},
|
|
];
|
|
|
|
for CASES {
|
|
data := tprint("@version %\n[logging]\nevent_log_capacity: %\n", APP_USER_SETTINGS_VERSION, it.value);
|
|
settings: App_User_Settings;
|
|
ok, seen := settings_parse("logger_tests", data, *settings);
|
|
defer settings_free(*settings);
|
|
expect(ok, "the settings with % did not parse", it.value);
|
|
expect(seen.event_log == it.seen, "'%' was seen: %", it.value, seen.event_log);
|
|
expect(settings.event_log_capacity == it.capacity, "'%' gave capacity %, not %", it.value, settings.event_log_capacity, it.capacity);
|
|
}
|
|
|
|
data := tprint("@version %\n[window]\nwidth_fraction: 0.5\n", APP_USER_SETTINGS_VERSION);
|
|
settings: App_User_Settings;
|
|
ok, seen := settings_parse("logger_tests", data, *settings);
|
|
defer settings_free(*settings);
|
|
expect(ok && !seen.event_log && settings.event_log_capacity == EVENT_LOG_CAPACITY_DEFAULT,
|
|
"a file without the key gave seen % and capacity %", seen.event_log, settings.event_log_capacity);
|
|
}
|
|
|
|
#scope_export
|
|
|
|
run_logger_tests :: (builder: *String_Builder = null) -> bool {
|
|
output_builder = builder;
|
|
emit("=== logger test suite ===\n");
|
|
emit(" (expecting logged errors from the malformed settings)\n");
|
|
|
|
push_allocator(context.default_allocator);
|
|
mpsc_tests();
|
|
ring_tests();
|
|
settings_tests();
|
|
return report();
|
|
}
|