core/io/fake
Shared: available on every enabled target.
Fakes of a system's providers for tests that must give the same answer every run and on every target: a file system, a network, a clock, entropy and terminals, each over the caller's records.
Each fake is named one way: its provider type for what it fakes, fake.volume, fake.network, fake.clock, fake.entropy and fake.terminals, its caller-owned state that name with _space, declared zeroed, and its constructor that name with _over, which fills the state and returns the handle (D348). The memory world, which is firmware's own world rather than a fake of a system's, stays in core/io as io.memory.
volume_over makes a fake.volume, the fake of file_system and of reader, writer, files and directories over the same files: a file system in memory over the caller's volume_node records, each a path, a kind and room for a file's bytes or a link's target, and a volume_space the caller declares zeroed, whose time stamps each change and whose fail_at fails a chosen call, counting every call of the volume, each read_entry, entry_size and same_file among them. A node with no path is free for a new file, a file grows only into its node's room, and a write that finds none is io_failed, as a full device is; a symbolic link is a node the caller makes, followed where a path is followed. A node a file is open on cannot be removed or renamed over, where POSIX would keep the open file without its name. terminals_over makes fake.terminals over the caller's screen records, each a file of a size, and every other file is no terminal. runtime/volume-memory holds every entry of the volume to its oracles; it links every entry of its conformances and does not fit a Cortex-M0 image, and runtime/file-system-firmware runs the temporary names and the memory terminals there.
Items
- clock type
- clock_space type
- entropy type
- entropy_space type
- network type
- network_socket type
- network_space type
- screen type
- terminals type
- volume type
- volume_node type
- volume_opening type
- volume_space type
- longest_datagram value
- volume_openings value
- advance function
- clock_over function
- entropy_over function
- network_over function
- socket_over function
- terminals_over function
- volume_find function
- volume_over function
Executable example
This complete program is maintained in the repository runtime tests. View source.
-- core/io's memory volume, the file system in memory that fakes -- `file_system` and with it `reader`, `writer`, `files` and -- `directories`, each with its failure oracles; and core/io's memory -- terminals. The volume's nodes and their storage are the program's, -- so it runs the same everywhere, firmware included. import core/cmp import core/io import core/io/fake import core/mem import core/text -- Something came back other than what the volume promises. wrong: atom -- How a call to the volume may fail. failure: type = io.already_exists | io.not_found | io.no_access | io.io_failed -- Rooms for the nodes' paths and contents. mut paths: [8][32]u8 = zeroed mut contents: [8][32]u8 = zeroed -- A free node over the room at INDEX. free_node: (index: usize) -> (node: fake.volume_node) = node = (path: paths[index][0..<32], path_length: 0, contents: contents[index][0..<32], length: 0, kind: io.regular, read_only: false, modified: (seconds: 0, nanoseconds: 0)) end free_node -- Give the node at INDEX the path PATH, the kind KIND and the bytes -- BYTES, as a test sets up a tree. given: (inout node: fake.volume_node, path: []u8, kind: io.entry_kind, bytes: []u8) -> none ! ... = try mem.copy(node.path[0..<lenof path], path) node.path_length = lenof path try mem.copy(node.contents[0..<lenof bytes], bytes) node.length = lenof bytes node.kind = kind end given -- Whether BYTES are WANTED. same: (bytes: []u8, wanted: []u8) -> (yes: bool) = yes = cmp.compare(bytes, wanted) == cmp.equal end same -- Whether opening PATH with OPTIONS fails as WANTED says. refused: (host: fake.volume, path: []u8, options: io.open_options, wanted: failure) -> (yes: bool) = yes = false opened: io.file = io.open_file(host, text.host_over(path), options) else (problem) yes = problem == wanted return end io.close(host, opened) else none end refused -- Whether renaming OLD to NEW fails as WANTED says. rename_refused: (host: fake.volume, old: []u8, new: []u8, wanted: failure) -> (yes: bool) = yes = false io.rename(host, text.host_over(old), text.host_over(new)) else (problem) yes = problem == wanted end end rename_refused -- Whether the status of PATH fails as WANTED says. status_refused: (host: fake.volume, path: []u8, links: io.link_handling, wanted: failure) -> (yes: bool) = yes = false found: io.status = io.status_of(host, text.host_over(path), links) else (problem) yes = problem == wanted return end _ = found end status_refused -- The bytes of the file at PATH, read into INTO. read_back: (host: fake.volume, path: []u8, into: []mut u8) -> (bytes: []u8 from into) ! ... = opened: io.file = try io.open_read(host, text.host_over(path)) defer io.close(host, opened) else none count: usize = try io.read(host, opened, into) bytes = into[0..<count] end read_back -- Opening, writing and reading: a new file, a taken name, a missing -- directory, a path too long for any node, appending, a write past the -- end, a read past it and a file that fills its room. files: (host: fake.volume) -> none ! ... = made: io.file = try io.open_file(host, text.host_over("docs/new.txt"), io.creating) try io.write(host, made, "abc") try io.write_at(host, made, u64(6), "xyz") mut buffer: [16]u8 = [16 of 255] hole: usize = try io.read_at(host, made, u64(2), buffer[0..<16]) past: usize = try io.read_at(host, made, u64(9), buffer[8..<16]) fail wrong when hole <> 7 or past <> 0 or not same(buffer[0..<7], "c\x00\x00\x00xyz") try io.close(host, made) -- A file that fills its node's room: the write that finds no room is -- refused after what fitted. filling: io.file = try io.open_file(host, text.host_over("docs/full"), io.creating) full: [40]u8 = [40 of 65] mut met: bool = false io.write(host, filling, full[0..<40]) else (problem) met = problem == io.io_failed end filled: io.status = try io.status_of_file(host, filling) try io.close(host, filling) try io.remove_file(host, text.host_over("docs/full")) fail wrong when not met or filled.size <> 32 fail wrong when not refused(host, "docs/new.txt", io.creating, io.already_exists) or not refused(host, "nowhere/new.txt", io.creating, io.not_found) or not refused(host, "docs/a-name-far-longer-than-any-node-room", io.creating, io.io_failed) or not refused(host, "docs/missing", io.updating, io.not_found) log: io.file = try io.open_file(host, text.host_over("docs/readme"), io.appending) try io.write(host, log, "!") try io.close(host, log) fail wrong when not same(try read_back(host, "docs/readme", buffer[0..<16]), "hello\n!") end files -- A link's status followed and not, a dangling link, and a read-only file -- refusing an open for writing until it is writable again. statuses: (host: fake.volume) -> none ! ... = followed: io.status = try io.status_of(host, text.host_over("pointer"), io.following) itself: io.status = try io.status_of(host, text.host_over("pointer"), io.not_following) fail wrong when followed.kind <> io.regular or followed.size <> 7 or itself.kind <> io.symbolic_link or itself.size <> 11 fail wrong when not status_refused(host, "dangling", io.following, io.not_found) or not status_refused(host, "absent", io.not_following, io.not_found) root: io.status = try io.status_of(host, text.host_over("."), io.not_following) fail wrong when root.kind <> io.subdirectory try io.set_read_only(host, text.host_over("docs/readme"), true) locked: io.status = try io.status_of(host, text.host_over("docs/readme"), io.not_following) fail wrong when not locked.read_only or not refused(host, "docs/readme", io.updating, io.no_access) try io.set_read_only(host, text.host_over("docs/readme"), false) opened: io.file = try io.open_file(host, text.host_over("docs/readme"), io.updating) try io.truncate(host, opened, u64(2)) try io.truncate(host, opened, u64(4)) try io.synchronize(host, opened, io.data_and_status) cut: io.status = try io.status_of_file(host, opened) try io.close(host, opened) fail wrong when cut.size <> 4 end statuses -- Renaming: onto a taken name, which it replaces; a directory, which -- takes what is in it along; a directory into itself; a missing name; -- and a name a file is open on. renames: (host: fake.volume) -> none ! ... = mut buffer: [16]u8 = zeroed try io.rename(host, text.host_over("docs/new.txt"), text.host_over("docs/readme")) fail wrong when not same(try read_back(host, "docs/readme", buffer[0..<16]), "abc\x00\x00\x00xyz") try io.rename(host, text.host_over("docs"), text.host_over("notes")) moved: io.status = try io.status_of(host, text.host_over("notes/readme"), io.not_following) fail wrong when moved.size <> 9 fail wrong when not rename_refused(host, "notes", "notes/inner", io.io_failed) or not rename_refused(host, "absent", "notes/x", io.not_found) opened: io.file = try io.open_read(host, text.host_over("notes/readme")) fail wrong when not rename_refused(host, "pointer", "notes/readme", io.io_failed) try io.close(host, opened) end renames -- Listing: the root's entries and their kinds, sizes by name, and a -- directory that is not empty refusing removal. listing: (host: fake.volume) -> none ! ... = mut buffer: [64]u8 = zeroed mut opened: io.directory = try io.open_directory(host, text.host_over("."), buffer[0..<64]) mut count: usize = 0 mut links: usize = 0 loop do item: io.entry = try io.read_entry(host, opened) name: []u8 = text.host_bytes(item.name) break when lenof name == 0 inc count if item.kind == io.symbolic_link then inc links end if end loop size: u64 = try io.entry_size(host, opened, text.host_over("data")) try io.close_directory(host, opened) fail wrong when count <> 4 or links <> 2 or size <> 7 mut met: bool = false io.remove_directory(host, text.host_over("notes")) else (problem) met = problem == io.io_failed end fail wrong when not met try io.remove_file(host, text.host_over("notes/readme")) try io.remove_directory(host, text.host_over("notes")) try io.make_directory(host, text.host_over("fresh")) met = false io.make_directory(host, text.host_over("fresh")) else (problem) met = problem == io.already_exists end fail wrong when not met end listing -- Temporary files named from scripted entropy, the second name taken and -- tried again, and the working directory. temporaries: (host: fake.volume) -> none ! ... = bytes: [24]u8 = [1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17] mut entropy_state: fake.entropy_space = zeroed names := fake.entropy_over(entropy_state, bytes[0..<24]) mut first_room: [40]u8 = zeroed first: io.temporary = try io.make_temporary(host, names, text.host_over("fresh"), first_room[0..<40]) try io.close(host, first.opened) mut second_room: [40]u8 = zeroed second: io.temporary = try io.make_temporary(host, names, text.host_over("fresh"), second_room[0..<40]) try io.write(host, second.opened, "temporary") try io.close(host, second.opened) fail wrong when not text.host_equal(first.path, "fresh/.tmp-0102030405060708") or not text.host_equal(second.path, "fresh/.tmp-0a0b0c0d0e0f1011") or entropy_state.requests <> 3 mut room: [16]u8 = zeroed here: text.host_string = try io.current_directory(host, room[0..<16]) fail wrong when not text.host_equal(here, "/volume") end temporaries -- The call the test chose fails, and the next succeeds. injected: (host: fake.volume) -> none ! ... = fail wrong when not status_refused(host, "data", io.following, io.io_failed) again: io.status = try io.status_of(host, text.host_over("data"), io.following) fail wrong when again.size <> 7 end injected -- Memory terminals: one screen is a terminal of its size, and every other -- file none. screens: () -> none ! ... = given: [1]fake.screen = [(stream: io.file(1), size: (rows: 25, columns: 80))] host := fake.terminals_over(given[0..<1]) size: io.window = try io.window_size(host, io.file(1)) fail wrong when not io.is_terminal(host, io.file(1)) or io.is_terminal(host, io.file(2)) or size.columns <> 80 mut met: bool = false other: io.window = io.window_size(host, io.file(2)) else (problem) met = problem == io.not_a_terminal size end fail wrong when not met or other.rows <> 25 end screens public main: () -> (code: i32) = code = 1 mut nodes: [8]fake.volume_node = [free_node(0), free_node(1), free_node(2), free_node(3), free_node(4), free_node(5), free_node(6), free_node(7)] given(nodes[0], "docs", io.subdirectory, "") else (problem) _ = problem return end given(nodes[1], "docs/readme", io.regular, "hello\n") else (problem) _ = problem return end given(nodes[2], "data", io.regular, "numbers") else (problem) _ = problem return end given(nodes[3], "pointer", io.symbolic_link, "docs/readme") else (problem) _ = problem return end given(nodes[4], "dangling", io.symbolic_link, "absent") else (problem) _ = problem return end mut state: fake.volume_space = zeroed host := fake.volume_over(state, nodes[0..<8], "/volume") code = 2 files(host) else (problem) _ = problem return end code = 3 statuses(host) else (problem) _ = problem return end code = 4 renames(host) else (problem) _ = problem return end return when state.renames < 5 code = 5 listing(host) else (problem) _ = problem return end code = 6 temporaries(host) else (problem) _ = problem return end return when state.temporary_tries <> 3 code = 7 state.fail_at = state.calls + 1 injected(host) else (problem) _ = problem return end code = 8 screens() else (problem) _ = problem return end code = 42 end main
clock type
public clock: type = manual_valueA clock that moves only when it is told: advance moves it, and sleep advances it by exactly the span asked and returns at once, so a test of code that sleeps or times itself takes no time and gives the same answer every run. Both of its clocks move together. A provider of clock and wall_clock; copies share the state.
clock_space type
public clock_space: type = struct
monotonic: u64
wall_seconds: i64
wall_nanoseconds: u32
readings: usize
sleeps: usize
slept: u64
end clock_spaceCaller-owned state of a manual clock: what its monotonic clock reads, in nanoseconds, what its wall clock reads, and how often each was read and slept on and how long. Declare it zeroed and give it to clock_over, which fills it; it must outlive the clock's handle. Its fields may be read, and the readings set, while the handle is in use: set monotonic only forward, as a monotonic clock never goes back, and wall_seconds either way, as a system's wall clock may be set.
entropy type
public entropy: type = scripted_entropy_valueEntropy whose bytes are the caller's, given in order, so a program that seeds itself from entropy runs the same way in every test: a provider of entropy. A request for more than what is left of them fails io_failed and gives nothing, so a test can make the source run dry. Copies share the state.
entropy_space type
public entropy_space: type = struct
used: usize
requests: usize
end entropy_spaceCaller-owned state of scripted entropy: how many of its bytes it has given and how often it was asked. Declare it zeroed and give it to entropy_over; it must outlive the handle, and may be read while the handle is in use.
network type
public network: type = network_valueA network of one machine in memory, a provider of network, reader and writer over the caller's socket records; copies share them. Its only interfaces are the loopback addresses, 127.0.0.0/8 and ::1: binding another address is address_unavailable, connecting to one unreachable, and connecting to a port nothing listens on refused. A port is in use for a binding of its family that the address of another socket of its protocol at that port would also take, the unspecified address taking every one, as Linux has it; Darwin and FreeBSD let a listener take a specific address beside one on the unspecified address. A connection and a datagram arrive at once, whole and in order, and a datagram sent to a port no socket holds, or larger than the room its socket has left, is lost, as UDP's may be.
Nothing in it waits, since in one thread nothing else could ever end the wait: where a real socket would wait, for a connection to accept, for bytes to receive or for room to send into, it fails timed_out at once, whether or not a limit was set, after a send has delivered what fits. A send to a connection whose peer has closed is disconnected at once, where a real one may take one send before it says so. It does not model loss on a connection, a peer's reset, partial reads, a port held after its socket closed, nor a zone.
network_socket type
public network_socket: type = struct
buffer: []mut u8
role: socket_role
local: net.address
peer: net.address
partner: usize
listener: usize
order: usize
pending: bool
start: usize
filled: usize
receiving_closed: bool
sending_closed: bool
peer_finished: bool
peer_gone: bool
receive_limit: u64
send_limit: u64
no_delay: bool
end network_socketOne socket of a memory network, the caller's: buffer holds what has arrived for it and not yet been received, its receive buffer, so a send that does not fit waits as a real one would. Make each with socket_over; the other fields are the network's. A free record is a socket the network can open; one that is open is a file of the network until close_socket frees it again.
network_space type
public network_space: type = struct
next_port: usize
arrivals: usize
calls: usize
fail_at: usize
without_ipv6: bool
connections: usize
datagrams: usize
losses: usize
end network_spaceCaller-owned state of a memory network: the next port it gives a socket bound to port 0, its counters, and what a test may set. Declare it zeroed and give it to network_over, which fills it; it must outlive the network's handle. without_ipv6 makes IPv6 missing, as on a system that does not have it, so every IPv6 address is address_unavailable; fail_at makes the network's call of that number, counted from one in calls, fail io_failed, and zero never. connections counts connections made, datagrams datagrams delivered and losses datagrams no socket took.
screen type
public screen: type = struct
stream: io.file
size: io.window
end screenOne terminal of terminals_over's fake: the file it is and its size.
terminals type
public terminals: type = screens_valueTerminals a test gives: each screen is a terminal of its size, and every other file is no terminal. A provider of terminals; copies share the screens.
volume type
public volume: type = volume_valueA file system in memory over the caller's nodes and their storage: a provider of reader, writer, files, directories and file_system. A file grows only into its node's contents, and a write that finds no room left is io_failed, as a full device is; a new file takes a free node, and a path longer than its room, or a volume with no free node, is io_failed too. A symbolic link is a node the caller makes, followed as a system follows one: always before a path's last part, and in its last part where the call follows links, a target that begins with / from the volume's root and any other from the link's own directory. Opening a dangling link to create a file creates its target. More than eight links in one path are io_failed, as ELOOP is, and a path that grows past 256 bytes as its links' targets are put in it is io_failed too. A node a file is open on cannot be removed or replaced: io_failed, where POSIX would keep the open file without its name, and a node whose path is longer than 256 bytes is not renamed. Read-only files refuse writing, no_access, as they do to every process but a privileged one. Copies share the volume.
volume_node type
public volume_node: type = struct
path: []mut u8
path_length: usize
contents: []mut u8
length: usize
kind: io.entry_kind
read_only: bool
modified: time.wall_time
end volume_nodeOne node of a memory volume: a regular file, a directory or a symbolic link, named by its whole path from the volume's root. The caller gives each node room for a path, path, and for a file's bytes or a link's target, contents, sets path_length to zero for a node the volume may use for a new file, and gives the others their path, length, kind, read_only and modified. A path has no leading ./, no trailing /, and no . or .. part; its directory must be a node too, or the root, .. The volume keeps the fields as files change; a test reads them.
volume_opening type
public volume_opening: type = struct
node: usize
position: u64
readable: bool
writable: bool
appending: bool
open: bool
end volume_openingOne file a memory volume has open: which node, how it may be used, and the position read and write use. The volume's own.
volume_space type
public volume_space: type = struct
now_seconds: i64
now_nanoseconds: u32
openings: [8]volume_opening
calls: usize
fail_at: usize
opens: usize
renames: usize
synchronizations: usize
temporary_tries: usize
end volume_spaceCaller-owned state of a memory volume: the wall-clock time each change is stamped with, which the test sets, the files it holds open, call counts, and a call to fail. Declare it zeroed and give it to volume_over, which fills it; it must outlive the handle, and its fields may be read, and the time and the failure set, while the handle is in use. calls counts the calls made of the volume, from one: every call of reader, writer, files, directories and file_system counts once, each read_entry, entry_size and same_file among them, except that write counts each piece write_some writes and make_temporary counts once and again for each name it tries to open. The call fail_at names fails io_failed; zero fails none.
longest_datagram value
public longest_datagram: usize = 65507The longest datagram a memory network carries, as IPv4's UDP does.
volume_openings value
public volume_openings: usize = 8How many files a memory volume holds open at once.
advance function
public advance: (host: clock, span: time.duration) -> noneMove a manual clock's monotonic and wall clocks forward by span; it traps past the monotonic clock's last instant.
clock_over function
public clock_over: (inout state: clock_space, start: time.instant,
wall: time.wall_time) -> (host: clock from state)Fill state for a manual clock whose monotonic clock reads start and whose wall clock reads wall, and return its handle.
entropy_over function
public entropy_over: (inout state: entropy_space, bytes: []u8)
-> (host: entropy from state, bytes)Fill state for scripted entropy over bytes, which the handle lends; keep them alive while it is used.
network_over function
public network_over: (inout state: network_space,
sockets: []mut network_socket)
-> (host: network from state, sockets)Fill state for a memory network over sockets, every one of them free, and return its handle. Ports bound to port 0 are given from 49152 up.
socket_over function
public socket_over: (buffer: []mut u8)
-> (slot: network_socket from buffer)A free socket record whose received bytes wait in buffer.
terminals_over function
public terminals_over: (screens: []screen)
-> (host: terminals from screens)Terminals over screens, which the handle lends; keep them alive while it is used.
volume_find function
public volume_find: (host: volume, path: []u8) -> (index: usize)The node at path, without following a link, or the node count when there is none; a test reads what it holds there.
volume_over function
public volume_over: (inout state: volume_space, nodes: []mut volume_node,
working: []u8)
-> (host: volume from state, nodes, working)Fill state for a memory volume over nodes, whose working directory is working, and return its handle, which lends both; keep them alive while it is used.