Add test suite for core:io

This commit is contained in:
Feoramund
2024-08-19 04:50:48 -04:00
committed by Laytan
parent 741ccd7ff5
commit 5b9e9fb822
2 changed files with 550 additions and 0 deletions

View File

@@ -0,0 +1,549 @@
package test_core_io
import "core:bytes"
import "core:io"
import "core:log"
import "core:os"
import "core:strings"
import "core:testing"
Passed_Tests :: distinct io.Stream_Mode_Set
_test_stream :: proc(
t: ^testing.T,
stream: io.Stream,
buffer: []u8,
reading_consumes: bool = false,
resets_on_empty: bool = false,
do_destroy: bool = true,
loc := #caller_location
) -> (passed: Passed_Tests, ok: bool) {
// We only test what the stream reports to support.
mode_set := io.query(stream)
// Can't feature-test anything if Query isn't supported.
testing.expectf(t, .Query in mode_set, "stream does not support .Query: %v", mode_set, loc = loc) or_return
passed += { .Query }
size := i64(len(buffer))
// Do some basic Seek sanity testing.
if .Seek in mode_set {
pos, err := io.seek(stream, 0, io.Seek_From(-1))
testing.expectf(t, pos == 0 && err == .Invalid_Whence,
"Seek(-1) didn't fail with Invalid_Whence: %v, %v", pos, err, loc = loc) or_return
pos, err = io.seek(stream, 0, .Start)
testing.expectf(t, pos == 0 && err == nil,
"Seek Start isn't 0: %v, %v", pos, err, loc = loc) or_return
pos, err = io.seek(stream, 0, .Current)
testing.expectf(t, pos == 0 && err == nil,
"Seek Current isn't 0 at the start: %v, %v", pos, err, loc = loc) or_return
pos, err = io.seek(stream, -1, .Start)
testing.expectf(t, pos == 0 && err == .Invalid_Offset,
"Seek Start-1 wasn't Invalid_Offset: %v, %v", pos, err, loc = loc) or_return
pos, err = io.seek(stream, -1, .Current)
testing.expectf(t, pos == 0 && err == .Invalid_Offset,
"Seek Current-1 wasn't Invalid_Offset: %v, %v", pos, err, loc = loc) or_return
pos, err = io.seek(stream, 0, .End)
testing.expectf(t, pos == size && err == nil,
"Seek End+0 failed: %v != size<%i>, %v", pos, size, err, loc = loc) or_return
pos, err = io.seek(stream, 0, .Current)
testing.expectf(t, pos == size && err == nil,
"Seek Current isn't size<%v> at the End: %v, %v", size, pos, err, loc = loc) or_return
// Seeking past the End is accepted throughout the API.
//
// It's _reading_ past the End which is erroneous.
pos, err = io.seek(stream, 1, .End)
testing.expectf(t, pos == size+1 && err == nil,
"Seek End+1 failed: %v, %v", pos, err, loc = loc) or_return
// Reset our position for future tests.
pos, err = io.seek(stream, 0, .Start)
testing.expectf(t, pos == 0 && err == nil,
"Seek Start reset failed: %v, %v", pos, err, loc = loc) or_return
passed += { .Seek }
}
// Test Size.
if .Size in mode_set {
api_size, size_err := io.size(stream)
testing.expectf(t, api_size == size,
"Size reports %v for its size; expected %v", api_size, size, loc = loc) or_return
testing.expectf(t, size_err == nil,
"Size expected no error: %v", size_err, loc = loc) or_return
passed += { .Size }
}
// Test Read_At.
if .Read_At in mode_set {
read_buf, alloc_err := make([]u8, size)
testing.expect_value(t, alloc_err, nil, loc = loc) or_return
defer delete(read_buf)
for start in 0..<size {
for end in 1+start..<size {
subsize := end - start
bytes_read, err := io.read_at(stream, read_buf[:subsize], start)
testing.expectf(t, i64(bytes_read) == subsize && err == nil,
"Read_At(%i) of %v bytes failed: %v, %v", start, subsize, bytes_read, err, loc = loc) or_return
testing.expectf(t, bytes.compare(read_buf[:subsize], buffer[start:end]) == 0,
"Read_At buffer compare failed: read_buf<%v> != buffer<%v>", read_buf, buffer, loc = loc) or_return
}
}
// Test empty streams and EOF.
one_buf: [1]u8
bytes_read, err := io.read_at(stream, one_buf[:], size)
testing.expectf(t, bytes_read == 0 && err == .EOF,
"Read_At at end of stream failed: %v, %v", bytes_read, err, loc = loc) or_return
// Make sure size is still sane.
if .Size in mode_set {
api_size, size_err := io.size(stream)
testing.expectf(t, api_size == size,
"Read_At+Size reports %v for its size after Read_At tests; expected %v", api_size, size, loc = loc) or_return
testing.expectf(t, size_err == nil,
"Read_At+Size expected no error: %v", size_err, loc = loc) or_return
}
passed += { .Read_At }
}
// Test Read.
if .Read in mode_set {
if size > 0 {
read_buf, alloc_err := make([]u8, size)
testing.expectf(t, alloc_err == nil, "allocation failed", loc = loc) or_return
defer delete(read_buf)
bytes_read, err := io.read(stream, read_buf[:1])
testing.expectf(t, bytes_read == 1 && err == nil,
"Read 1 byte at start failed: %v, %v", bytes_read, err, loc = loc) or_return
testing.expectf(t, read_buf[0] == buffer[0],
"Read of first byte failed: read_buf[0]<%v> != buffer[0]<%v>", read_buf[0], buffer[0], loc = loc) or_return
// Test rolling back the stream one byte then reading it again.
if .Seek in mode_set {
pos, seek_err := io.seek(stream, -1, .Current)
testing.expectf(t, pos == 0 && err == nil,
"Read+Seek Current-1 reset to 0 failed: %v, %v", pos, seek_err, loc = loc) or_return
bytes_read, err = io.read(stream, read_buf[:1])
testing.expectf(t, bytes_read == 1 && err == nil,
"Read 1 byte at start after Seek reset failed: %v, %v", bytes_read, err, loc = loc) or_return
testing.expectf(t, read_buf[0] == buffer[0] ,
"re-Read of first byte failed: read_buf[0]<%v> != buffer[0]<%v>", read_buf[0], buffer[0], loc = loc) or_return
}
// Make sure size is still sane.
if .Size in mode_set {
api_size, size_err := io.size(stream)
expected_api_size := size - 1 if reading_consumes else size
testing.expectf(t, api_size == expected_api_size,
"Read+Size reports %v for its size after Read tests; expected %v", api_size, expected_api_size, loc = loc) or_return
testing.expectf(t, size_err == nil,
"Read+Size expected no error: %v", size_err, loc = loc) or_return
}
// Read the rest.
if size > 1 {
bytes_read, err = io.read(stream, read_buf[1:])
testing.expectf(t, i64(bytes_read) == size - 1 && err == nil,
"Read rest of stream failed: %v != %v, %v", bytes_read, size-1, err, loc = loc) or_return
testing.expectf(t, bytes.compare(read_buf, buffer) == 0,
"Read buffer compare failed: read_buf<%v> != buffer<%v>", read_buf, buffer, loc = loc) or_return
}
}
// Test empty streams and EOF.
one_buf: [1]u8
bytes_read, err := io.read(stream, one_buf[:])
testing.expectf(t, bytes_read == 0 && err == .EOF,
"Read at end of stream failed: %v, %v", bytes_read, err, loc = loc) or_return
if !resets_on_empty && .Size in mode_set {
// Make sure size is still sane.
api_size, size_err := io.size(stream)
testing.expectf(t, api_size == size,
"Read+Size reports %v for its size after Read tests; expected %v", api_size, size, loc = loc) or_return
testing.expectf(t, size_err == nil,
"Read+Size expected no error: %v", size_err, loc = loc) or_return
}
passed += { .Read }
}
// Test Write_At.
if .Write_At in mode_set {
if size > 0 {
write_buf, write_buf_alloc_err := make([]u8, size)
testing.expectf(t, write_buf_alloc_err == nil, "allocation failed", loc = loc) or_return
defer delete(write_buf)
for i in 0..<size {
write_buf[i] = buffer[i] ~ 0xAA
}
bytes_written, write_err := io.write_at(stream, write_buf[:], 0)
testing.expectf(t, i64(bytes_written) == size && write_err == nil,
"Write_At failed: bytes_written<%v> != size<%v>: %v", bytes_written, size, write_err, loc = loc) or_return
// Test reading what we've written.
if .Read_At in mode_set {
read_buf, read_buf_alloc_err := make([]u8, size)
testing.expectf(t, read_buf_alloc_err == nil, "allocation failed", loc = loc) or_return
defer delete(read_buf)
bytes_read, read_err := io.read_at(stream, read_buf[:], 0)
testing.expectf(t, i64(bytes_read) == size && read_err == nil,
"Write_At+Read_At failed: bytes_read<%i> != size<%i>, %v", bytes_read, size, read_err, loc = loc) or_return
testing.expectf(t, bytes.compare(read_buf, write_buf) == 0,
"Write_At+Read_At buffer compare failed: write_buf<%v> != read_buf<%v>", write_buf, read_buf, loc = loc) or_return
}
} else {
// Expect that it should be okay to write a single byte to an empty stream.
x_buf: [1]u8 = { 'Z' }
bytes_written, write_err := io.write_at(stream, x_buf[:], 0)
testing.expectf(t, i64(bytes_written) == 1 && write_err == nil,
"Write_At(0) with 'Z' on empty stream failed: bytes_written<%v>, %v", bytes_written, write_err, loc = loc) or_return
// Test reading what we've written.
if .Read_At in mode_set {
x_buf[0] = 0
bytes_read, read_err := io.read_at(stream, x_buf[:], 0)
testing.expectf(t, i64(bytes_read) == 1 && read_err == nil,
"Write_At(0)+Read_At(0) failed expectation: bytes_read<%v> != 1, %q != 'Z', %v", bytes_read, x_buf[0], read_err, loc = loc) or_return
}
}
passed += { .Write_At }
}
// Test Write.
if .Write in mode_set {
write_buf, write_buf_alloc_err := make([]u8, size)
testing.expectf(t, write_buf_alloc_err == nil, "allocation failed", loc = loc) or_return
defer delete(write_buf)
for i in 0..<size {
write_buf[i] = buffer[i] ~ 0xAA
}
pos: i64 = -1
before_write_size: i64 = -1
// Do a Seek sanity check after past tests.
if .Seek in mode_set {
seek_err: io.Error
pos, seek_err = io.seek(stream, 0, .Current)
testing.expectf(t, seek_err == nil,
"Write+Seek(Current) failed: pos<%i>, %v", pos, seek_err) or_return
}
// Get the Size before writing.
if .Size in mode_set {
size_err: io.Error
before_write_size, size_err = io.size(stream)
testing.expectf(t, size_err == nil,
"Write+Size failed: %v", size_err, loc = loc) or_return
}
bytes_written, write_err := io.write(stream, write_buf[:])
testing.expectf(t, i64(bytes_written) == size && write_err == nil,
"Write %i bytes failed: %i, %v", size, bytes_written, write_err, loc = loc) or_return
// Size sanity check, part 2.
if before_write_size >= 0 && .Size in mode_set {
after_write_size, size_err := io.size(stream)
testing.expectf(t, size_err == nil,
"Write+Size.part_2 failed: %v", size_err, loc = loc) or_return
testing.expectf(t, after_write_size == before_write_size + size,
"Write+Size.part_2 failed: %v != %v + %v", after_write_size, before_write_size, size, loc = loc) or_return
}
// Test reading what we've written directly with Read_At.
if pos >= 0 && .Read_At in mode_set {
read_buf, read_buf_alloc_err := make([]u8, size)
testing.expectf(t, read_buf_alloc_err == nil, "allocation failed", loc = loc) or_return
defer delete(read_buf)
bytes_read, read_err := io.read_at(stream, read_buf[:], pos)
testing.expectf(t, i64(bytes_read) == size && read_err == nil,
"Write+Read_At(%i) failed: bytes_read<%i> != size<%i>, %v", pos, bytes_read, size, read_err, loc = loc) or_return
testing.expectf(t, bytes.compare(read_buf, write_buf) == 0,
"Write+Read_At buffer compare failed: read_buf<%v> != write_buf<%v>", read_buf, write_buf, loc = loc) or_return
}
// Test resetting the pointer and reading what we've written with Read.
if .Read in mode_set && .Seek in mode_set {
seek_err: io.Error
pos, seek_err = io.seek(stream, 0, .Start)
testing.expectf(t, pos == 0 && seek_err == nil,
"Write+Read+Seek(Start) failed: pos<%i>, %v", pos, seek_err) or_return
read_buf, read_buf_alloc_err := make([]u8, size)
testing.expectf(t, read_buf_alloc_err == nil, "allocation failed", loc = loc) or_return
defer delete(read_buf)
bytes_read, read_err := io.read(stream, read_buf[:])
testing.expectf(t, i64(bytes_read) == size && read_err == nil,
"Write+Read failed: bytes_read<%i> != size<%i>, %v", bytes_read, size, read_err, loc = loc) or_return
testing.expectf(t, bytes.compare(read_buf, write_buf) == 0,
"Write+Readbuffer compare failed: read_buf<%v> != write_buf<%v>", read_buf, write_buf, loc = loc) or_return
}
passed += { .Write }
}
// Test the other modes.
if .Flush in mode_set {
err := io.flush(stream)
testing.expectf(t, err == nil, "stream failed to Flush: %v", err, loc = loc) or_return
passed += { .Flush }
}
if .Close in mode_set {
close_err := io.close(stream)
testing.expectf(t, close_err == nil, "stream failed to Close: %v", close_err, loc = loc) or_return
passed += { .Close }
// TODO(Feoramund): The OS<->IO interface will need updating for more
// specific error messages.
//
// This test will catch it when they start using them, but so long as
// they actually error, it should be okay for now.
//
// .EOF would be a better message for all of these.
if .Seek in mode_set {
pos, err := io.seek(stream, 0, .Start)
testing.expectf(t, pos == 0 && err == .Invalid_Offset,
"Seek after Close unexpected output: pos<%v>, %v", pos, err, loc = loc) or_return
}
if .Read in mode_set {
eof_buf: [1]u8
bytes_read, err := io.read(stream, eof_buf[:])
testing.expectf(t, bytes_read == 0 && err == .Unknown,
"Read after Close unexpected output: bytes_read<%v>, %v", bytes_read, err, loc = loc) or_return
}
if size > 0 && .Read_At in mode_set {
eof_buf: [1]u8
bytes_read, err := io.read_at(stream, eof_buf[:], 0)
testing.expectf(t, bytes_read == 0 && err == .Unknown,
"Read_At after Close unexpected output: bytes_read<%v>, %v", bytes_read, err, loc = loc) or_return
}
if .Write in mode_set {
eof_buf: [1]u8 = {'Z'}
bytes_written, err := io.write(stream, eof_buf[:])
testing.expectf(t, bytes_written == 0 && err == .Unknown,
"Write after Close unexpected output: bytes_written<%v>, %v", bytes_written, err, loc = loc) or_return
}
if .Write_At in mode_set {
eof_buf: [1]u8 = {'Z'}
bytes_written, err := io.write_at(stream, eof_buf[:], 0)
testing.expectf(t, bytes_written == 0 && err == .Unknown,
"Write after Close unexpected output: bytes_written<%v>, %v", bytes_written, err, loc = loc) or_return
}
}
if do_destroy && .Destroy in mode_set {
err := io.destroy(stream)
testing.expectf(t, err == nil, "stream failed to Destroy: %v", err, loc = loc) or_return
passed += { .Destroy }
}
ok = true
return
}
@test
test_bytes_reader :: proc(t: ^testing.T) {
buf: [32]u8
for i in 0..<u8(len(buf)) {
buf[i] = 'A' + i
}
br: bytes.Reader
results: Passed_Tests
ok: bool
for end in 0..<i64(len(buf)) {
results, ok =_test_stream(t, bytes.reader_init(&br, buf[:end]), buf[:end])
if !ok {
log.debugf("buffer[:%i] := %v", end, buf[:end])
return
}
}
log.debugf("%#v", results)
}
@test
test_bytes_buffer_stream :: proc(t: ^testing.T) {
buf: [32]u8
for i in 0..<u8(len(buf)) {
buf[i] = 'A' + i
}
results: Passed_Tests
ok: bool
for end in 0..<i64(len(buf)) {
bb: bytes.Buffer
// Mind that `bytes.buffer_init` copies the entire underlying slice.
bytes.buffer_init(&bb, buf[:end])
// `bytes.Buffer` has a behavior of decreasing its size with each read
// until it eventually clears the underlying buffer when it runs out of
// data to read.
results, ok = _test_stream(t, bytes.buffer_to_stream(&bb), buf[:end],
reading_consumes = true, resets_on_empty = true)
if !ok {
log.debugf("buffer[:%i] := %v", end, buf[:end])
return
}
}
log.debugf("%#v", results)
}
@test
test_limited_reader :: proc(t: ^testing.T) {
buf: [32]u8
for i in 0..<u8(len(buf)) {
buf[i] = 'A' + i
}
br: bytes.Reader
bs := bytes.reader_init(&br, buf[:])
lr: io.Limited_Reader
results: Passed_Tests
ok: bool
for end in 0..<i64(len(buf)) {
io.seek(bs, 0, .Start)
results, ok = _test_stream(t, io.limited_reader_init(&lr, bs, end), buf[:end])
if !ok {
log.debugf("buffer[:%i] := %v", end, buf[:end])
return
}
}
log.debugf("%#v", results)
}
@test
test_section_reader :: proc(t: ^testing.T) {
buf: [32]u8
for i in 0..<u8(len(buf)) {
buf[i] = 'A' + i
}
br: bytes.Reader
bs := bytes.reader_init(&br, buf[:])
sr: io.Section_Reader
results: Passed_Tests
ok: bool
for start in 0..<i64(len(buf)) {
for end in start..<i64(len(buf)) {
results, ok = _test_stream(t, io.section_reader_init(&sr, bs, start, end-start), buf[start:end])
if !ok {
log.debugf("buffer[%i:%i] := %v", start, end, buf[start:end])
return
}
}
}
log.debugf("%#v", results)
}
@test
test_string_builder_stream :: proc(t: ^testing.T) {
sb := strings.builder_make()
defer strings.builder_destroy(&sb)
// String builders do not support reading, so we'll have to set up a few
// things outside the main test.
buf: [32]u8
expected_buf: [64]u8
for i in 0..<u8(len(buf)) {
buf[i] = 'A' + i
expected_buf[i] = 'A' + i
strings.write_byte(&sb, 'A' + i)
}
for i in 32..<u8(len(expected_buf)) {
expected_buf[i] = ('A' + i-len(buf)) ~ 0xAA
}
results, _ := _test_stream(t, strings.to_stream(&sb), buf[:],
do_destroy = false)
testing.expectf(t, bytes.compare(sb.buf[:], expected_buf[:]) == 0, "string builder stream failed:\nbuilder<%q>\n!=\nbuffer <%q>", sb.buf[:], expected_buf[:])
log.debugf("%#v", results)
}
@test
test_os_file_stream :: proc(t: ^testing.T) {
defer if !testing.failed(t) {
testing.expect_value(t, os.remove(TEMPORARY_FILENAME), nil)
}
buf: [32]u8
for i in 0..<u8(len(buf)) {
buf[i] = 'A' + i
}
TEMPORARY_FILENAME :: "test_core_io_os_file_stream"
fd, open_err := os.open(TEMPORARY_FILENAME, os.O_RDWR | os.O_CREATE | os.O_TRUNC, 0o644)
if !testing.expectf(t, open_err == nil, "error on opening %q: %v", TEMPORARY_FILENAME, open_err) {
return
}
stream := os.stream_from_handle(fd)
bytes_written, write_err := io.write(stream, buf[:])
if !testing.expectf(t, bytes_written == len(buf) && write_err == nil,
"failed to Write initial buffer: bytes_written<%v> != len_buf<%v>, %v", bytes_written, len(buf), write_err) {
return
}
flush_err := io.flush(stream)
if !testing.expectf(t, flush_err == nil,
"failed to Flush initial buffer: %v", write_err) {
return
}
results, _ := _test_stream(t, stream, buf[:])
log.debugf("%#v", results)
}

View File

@@ -23,6 +23,7 @@ download_assets :: proc() {
@(require) import "encoding/xml"
@(require) import "flags"
@(require) import "fmt"
@(require) import "io"
@(require) import "math"
@(require) import "math/big"
@(require) import "math/linalg/glsl"