Landin library reference discuss source

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

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_value

core/io/fake/clock.ldn:30

A 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_space

core/io/fake/clock.ldn:12

Caller-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_value

core/io/fake/entropy.ldn:25

Entropy 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_space

core/io/fake/entropy.ldn:10

Caller-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_value

core/io/fake/network.ldn:104

A 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_socket

core/io/fake/network.ldn:46

One 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_space

core/io/fake/network.ldn:19

Caller-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 screen

core/io/fake/terminals.ldn:6

One terminal of terminals_over's fake: the file it is and its size.

terminals type

public terminals: type = screens_value

core/io/fake/terminals.ldn:18

Terminals 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_value

core/io/fake/volume.ldn:93

A 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_node

core/io/fake/volume.ldn:21

One 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_opening

core/io/fake/volume.ldn:33

One 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_space

core/io/fake/volume.ldn:57

Caller-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 = 65507

core/io/fake/network.ldn:397

The longest datagram a memory network carries, as IPv4's UDP does.

volume_openings value

public volume_openings: usize = 8

core/io/fake/volume.ldn:43

How many files a memory volume holds open at once.

advance function

public advance: (host: clock, span: time.duration) -> none

core/io/fake/clock.ldn:44

Move 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)

core/io/fake/clock.ldn:34

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)

core/io/fake/entropy.ldn:29

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)

core/io/fake/network.ldn:109

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)

core/io/fake/network.ldn:67

A free socket record whose received bytes wait in buffer.

terminals_over function

public terminals_over: (screens: []screen)
                       -> (host: terminals from screens)

core/io/fake/terminals.ldn:22

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)

core/io/fake/volume.ldn:111

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)

core/io/fake/volume.ldn:98

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.