317 lines
11 KiB
Zig
317 lines
11 KiB
Zig
const Simulator = @This();
|
|
|
|
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
const Trace = @import("trace.zig").Trace;
|
|
const Scheduler = @import("scheduler.zig");
|
|
const Network = @import("network.zig");
|
|
const Node = @import("node.zig");
|
|
const PartitionPolicy = @import("partition_policy.zig");
|
|
const sometimes_mod = @import("sometimes.zig");
|
|
const Sometimes = sometimes_mod.Sometimes;
|
|
|
|
pub const EntryPoint = Scheduler.MainEntryPoint;
|
|
pub const PartitionShape = PartitionPolicy.Shape;
|
|
pub const PartitionShapeWeights = PartitionPolicy.ShapeWeights;
|
|
|
|
pub const PartitionFaultOptions = struct {
|
|
weights: PartitionShapeWeights = .{},
|
|
min_interval_us: u64 = 1_000,
|
|
max_interval_us: u64 = 10_000,
|
|
};
|
|
|
|
// Process-global pointer to the active simulation's sometimes-assertion
|
|
// registry. Programs under test only receive an `std.Io` and have no handle to
|
|
// the Simulator, so assertSometimes() reaches the registry through this global.
|
|
// Everything runs single-threaded in userspace, so a plain global is
|
|
// deterministic. It is set in init() and cleared in deinit(), giving each
|
|
// simulation a fresh, per-run registry.
|
|
var g_sometimes: ?*Sometimes = null;
|
|
|
|
// Records that `cond` was observed at this point of the simulation. The call
|
|
// site (@src()) is the identity; `label` is an optional human-readable name
|
|
// shown in the end-of-simulation report. Pass null when no label is wanted:
|
|
// assertSometimes(x > 0, @src(), null);
|
|
//
|
|
// This never changes the behaviour of the program under test: if no simulation
|
|
// is active the call is a no-op.
|
|
pub fn assertSometimes(cond: bool, comptime src: std.builtin.SourceLocation, comptime label: ?[]const u8) void {
|
|
const site = sometimes_mod.registerSite(src, label, .assert);
|
|
const registry = g_sometimes orelse return;
|
|
registry.record(cond, site.*);
|
|
}
|
|
|
|
// Records that this call site was reached. This is the conditionless form of
|
|
// assertSometimes() for branches where reaching the line is the coverage signal.
|
|
pub fn reachableSometimes(comptime src: std.builtin.SourceLocation, comptime label: ?[]const u8) void {
|
|
const site = sometimes_mod.registerSite(src, label, .reachable);
|
|
const registry = g_sometimes orelse return;
|
|
registry.record(true, site.*);
|
|
}
|
|
|
|
// Prints every compiled assertSometimes()/reachableSometimes() call site known
|
|
// to this binary. This is independent of whether a simulation has run.
|
|
pub fn reportSometimesSites() void {
|
|
sometimes_mod.reportCompileTimeSites();
|
|
}
|
|
|
|
const SpawnOptions = struct {
|
|
stack_size: usize = 64 * 1024,
|
|
addresses: []const u32 = &[0]u32{}, // TODO: How do I make an empty slice?
|
|
};
|
|
|
|
const SpawnError = error{
|
|
InvalidCommand,
|
|
NoSuchProgram,
|
|
} || Allocator.Error || std.process.Environ.CreateMapError;
|
|
|
|
const ExecutableName = struct {
|
|
name: []const u8,
|
|
entry: EntryPoint,
|
|
};
|
|
|
|
gpa: Allocator,
|
|
trace: Trace,
|
|
prng: std.Random.DefaultPrng,
|
|
scheduler: Scheduler,
|
|
network: Network,
|
|
partition_policy: PartitionPolicy,
|
|
partition_endpoints: std.ArrayList(Network.HostID),
|
|
partition_policy_enabled: bool,
|
|
partition_target_selected: bool,
|
|
partition_fault_options: PartitionFaultOptions,
|
|
next_partition_step_time_us: u64,
|
|
nodes: std.ArrayList(*Node),
|
|
executables: std.ArrayList(ExecutableName),
|
|
real_io: std.Io,
|
|
next_node_id: u32,
|
|
sometimes: Sometimes,
|
|
|
|
pub fn init(self: *Simulator, gpa: Allocator, real_io: std.Io, seed: u64) void {
|
|
self.gpa = gpa;
|
|
self.trace.init(&self.scheduler, real_io);
|
|
self.prng = std.Random.DefaultPrng.init(seed);
|
|
self.scheduler.init(gpa, &self.trace, &self.prng);
|
|
self.network.init(gpa);
|
|
self.partition_policy.init(gpa, .{});
|
|
self.partition_endpoints = .empty;
|
|
self.partition_policy_enabled = false;
|
|
self.partition_target_selected = false;
|
|
self.partition_fault_options = .{};
|
|
self.next_partition_step_time_us = 0;
|
|
self.nodes = .empty;
|
|
self.executables = .empty;
|
|
self.real_io = real_io;
|
|
self.next_node_id = 0;
|
|
self.sometimes.init(gpa);
|
|
self.sometimes.seedCompileTimeSites();
|
|
g_sometimes = &self.sometimes;
|
|
}
|
|
|
|
pub fn deinit(self: *Simulator) void {
|
|
// Print the end-of-simulation coverage report unless the caller already
|
|
// asked for it explicitly via reportSometimes().
|
|
if (!self.sometimes.reported)
|
|
self.sometimes.report();
|
|
g_sometimes = null;
|
|
self.sometimes.deinit();
|
|
self.partition_policy.deinit();
|
|
self.partition_endpoints.deinit(self.gpa);
|
|
|
|
for (self.nodes.items) |node| {
|
|
node.deinit();
|
|
self.gpa.destroy(node);
|
|
}
|
|
self.executables.deinit(self.gpa);
|
|
self.nodes.deinit(self.gpa);
|
|
self.network.deinit();
|
|
self.scheduler.deinit();
|
|
self.trace.deinit();
|
|
}
|
|
|
|
// Prints which sometimes-assertions were taken and which were reached
|
|
// but never satisfied. Called automatically by deinit(); expose it so
|
|
// callers can choose exactly when the report is emitted.
|
|
pub fn reportSometimes(self: *Simulator) void {
|
|
self.sometimes.report();
|
|
}
|
|
|
|
pub fn sometimesCovered(self: *Simulator) bool {
|
|
return self.sometimes.allReached();
|
|
}
|
|
|
|
pub const InvalidNodeError = error{
|
|
InvalidNode,
|
|
};
|
|
|
|
pub const BreakLinkError = InvalidNodeError || Allocator.Error;
|
|
|
|
pub fn breakLink(self: *Simulator, a: u32, b: u32) BreakLinkError!void {
|
|
try self.network.breakLink(
|
|
self.hostIDForNodeID(a) orelse return InvalidNodeError.InvalidNode,
|
|
self.hostIDForNodeID(b) orelse return InvalidNodeError.InvalidNode,
|
|
);
|
|
}
|
|
|
|
pub fn healLink(self: *Simulator, a: u32, b: u32) InvalidNodeError!void {
|
|
self.network.healLink(
|
|
self.hostIDForNodeID(a) orelse return InvalidNodeError.InvalidNode,
|
|
self.hostIDForNodeID(b) orelse return InvalidNodeError.InvalidNode,
|
|
);
|
|
}
|
|
|
|
pub fn linkIsBroken(self: *const Simulator, a: u32, b: u32) InvalidNodeError!bool {
|
|
return self.network.linkIsBroken(
|
|
self.hostIDForNodeID(a) orelse return InvalidNodeError.InvalidNode,
|
|
self.hostIDForNodeID(b) orelse return InvalidNodeError.InvalidNode,
|
|
);
|
|
}
|
|
|
|
pub fn setPartitionShapeWeights(self: *Simulator, weights: PartitionShapeWeights) void {
|
|
self.partition_policy.weights = weights;
|
|
}
|
|
|
|
pub fn enablePartitionFaults(self: *Simulator, options: PartitionFaultOptions) void {
|
|
std.debug.assert(options.min_interval_us > 0);
|
|
std.debug.assert(options.min_interval_us <= options.max_interval_us);
|
|
|
|
self.partition_fault_options = options;
|
|
self.partition_policy.weights = options.weights;
|
|
self.partition_policy_enabled = true;
|
|
self.partition_target_selected = false;
|
|
self.next_partition_step_time_us = self.scheduler.current_time;
|
|
}
|
|
|
|
pub fn disablePartitionFaults(self: *Simulator) void {
|
|
self.partition_policy_enabled = false;
|
|
}
|
|
|
|
pub fn pickPartitionTarget(self: *Simulator) Allocator.Error!PartitionShape {
|
|
try self.refreshPartitionEndpoints();
|
|
const shape = try self.partition_policy.pickTarget(self.partition_endpoints.items, self.prng.random());
|
|
self.partition_target_selected = true;
|
|
return shape;
|
|
}
|
|
|
|
pub fn setPartitionTarget(self: *Simulator, shape: PartitionShape) Allocator.Error!void {
|
|
try self.refreshPartitionEndpoints();
|
|
try self.partition_policy.setTarget(self.partition_endpoints.items, shape, self.prng.random());
|
|
self.partition_target_selected = true;
|
|
}
|
|
|
|
pub fn driftPartitionOne(self: *Simulator) Allocator.Error!bool {
|
|
try self.refreshPartitionEndpoints();
|
|
return self.partition_policy.driftOne(&self.network.partitions, self.partition_endpoints.items, self.prng.random());
|
|
}
|
|
|
|
pub fn partitionAtTarget(self: *Simulator) Allocator.Error!bool {
|
|
try self.refreshPartitionEndpoints();
|
|
return self.partition_policy.atTarget(&self.network.partitions, self.partition_endpoints.items);
|
|
}
|
|
|
|
fn automaticPartitionStep(self: *Simulator) Allocator.Error!void {
|
|
if (!self.partition_policy_enabled)
|
|
return;
|
|
|
|
const now = self.scheduler.current_time;
|
|
if (now < self.next_partition_step_time_us)
|
|
return;
|
|
|
|
try self.refreshPartitionEndpoints();
|
|
if (self.partition_endpoints.items.len < 2)
|
|
return;
|
|
|
|
if (!self.partition_target_selected or self.partition_policy.atTarget(&self.network.partitions, self.partition_endpoints.items)) {
|
|
_ = try self.partition_policy.pickTarget(self.partition_endpoints.items, self.prng.random());
|
|
self.partition_target_selected = true;
|
|
}
|
|
|
|
_ = try self.partition_policy.driftOne(&self.network.partitions, self.partition_endpoints.items, self.prng.random());
|
|
self.next_partition_step_time_us = std.math.add(u64, now, randomPartitionInterval(self)) catch std.math.maxInt(u64);
|
|
}
|
|
|
|
fn randomPartitionInterval(self: *Simulator) u64 {
|
|
const min = self.partition_fault_options.min_interval_us;
|
|
const max = self.partition_fault_options.max_interval_us;
|
|
if (min == max)
|
|
return min;
|
|
|
|
return min + self.prng.random().uintLessThan(u64, max - min + 1);
|
|
}
|
|
|
|
fn hostIDForNodeID(self: *const Simulator, id: u32) ?Network.HostID {
|
|
for (self.nodes.items) |node| {
|
|
if (node.id == id)
|
|
return node.network_host.id;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn refreshPartitionEndpoints(self: *Simulator) Allocator.Error!void {
|
|
self.partition_endpoints.clearRetainingCapacity();
|
|
for (self.nodes.items) |node| {
|
|
try self.partition_endpoints.append(self.gpa, node.network_host.id);
|
|
}
|
|
}
|
|
|
|
pub fn setTraceOutputFile(self: *Simulator, path: []const u8) !void {
|
|
try self.trace.setOutputFile(path);
|
|
}
|
|
|
|
pub fn addExecutable(self: *Simulator, name: []const u8, entry: EntryPoint) Allocator.Error!void {
|
|
try self.executables.append(self.gpa, ExecutableName{
|
|
.name = name,
|
|
.entry = entry,
|
|
});
|
|
}
|
|
|
|
pub fn spawn(self: *Simulator, command: []const u8, options: SpawnOptions) SpawnError!void {
|
|
const name = extractProgramNameFromCommand(command) orelse return SpawnError.InvalidCommand;
|
|
const entry = self.getExecutableEntryPoint(name) orelse return SpawnError.NoSuchProgram;
|
|
|
|
const node_id = self.next_node_id;
|
|
self.next_node_id += 1;
|
|
|
|
const node = try self.gpa.create(Node);
|
|
errdefer self.gpa.destroy(node);
|
|
|
|
try node.init(self.real_io, &self.trace, &self.prng, &self.scheduler, &self.network, node_id, command, options.addresses, self.gpa);
|
|
|
|
try self.nodes.append(self.gpa, node);
|
|
errdefer _ = self.nodes.swapRemove(self.nodes.items.len - 1);
|
|
|
|
try self.scheduler.spawn(node, entry, options.stack_size);
|
|
}
|
|
|
|
pub fn scheduleOne(self: *Simulator) bool {
|
|
self.automaticPartitionStep() catch {};
|
|
return self.scheduler.scheduleOne();
|
|
}
|
|
|
|
pub fn dumpFiles(self: *Simulator) void {
|
|
for (self.nodes.items) |node| {
|
|
node.dumpFiles();
|
|
}
|
|
}
|
|
|
|
// Reads the first word of a command
|
|
// "program arg1 arg2 arg3" -> "program"
|
|
fn extractProgramNameFromCommand(command: []const u8) ?[]const u8 {
|
|
var cur: usize = 0;
|
|
while (cur < command.len and command[cur] != ' ')
|
|
cur += 1;
|
|
|
|
if (cur == 0)
|
|
return null;
|
|
return command[0..cur];
|
|
}
|
|
|
|
fn getExecutableEntryPoint(self: *Simulator, name: []const u8) ?EntryPoint {
|
|
for (self.executables.items) |executable| {
|
|
if (std.mem.eql(u8, executable.name, name))
|
|
return executable.entry;
|
|
}
|
|
return null;
|
|
}
|