diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 166b97a..4364c56 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,6 +54,8 @@ jobs: luarocks install luacov luarocks install testcase luarocks install assert + luarocks install os-pipe + luarocks install time-clock - name: Install run: | diff --git a/README.md b/README.md index de0f7f0..bfde4e8 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,14 @@ luarocks install io-reader the following functions return the `error` object created by https://github.com/mah0x211/lua-errno module. -## r, err = io.reader.new(f) +## r, err = io.reader.new( f [, sec] ) create a new reader instance that reads data from a file or file descriptor. **Parameters** - `f:file*|string|integer`: file, filename or file descriptor. +- `sec:number`: timeout seconds. (default `nil` means no timeout) **Returns** @@ -63,17 +64,16 @@ print(dump({ ``` -## s, err, timeout = reader:read(fmt, sec) +## s, err, timeout = reader:read( [fmt] ) read data from the file or file descriptor. **Parameters** - `fmt:integer|string`: size of data to read, or format string as follows: (`*` prefix can be omitted) - - `*a`: reads all data. - `*l`: reads a line. (default) - `*L`: reads a line with the newline character. -- `sec:number`: timeout seconds. + - `*a`: reads all data. **Returns** diff --git a/reader.lua b/reader.lua index dfd7c4a..42b8cc8 100644 --- a/reader.lua +++ b/reader.lua @@ -32,17 +32,20 @@ local wait_readable = require('gpoll').wait_readable --- @class io.reader --- @field private fd integer --- @field private file? file* +--- @field private waitsec? number --- @field private buf string local Reader = {} --- init --- @param fd integer --- @param f file* +--- @param sec number? --- @return io.reader -function Reader:init(fd, f) +function Reader:init(fd, f, sec) self.fd = fd self.file = f self.buf = '' + self.waitsec = sec return self end @@ -68,14 +71,12 @@ end --- read --- wait_readable --- @param fmt string|integer? ---- @param sec number? --- @return string? data --- @return any err --- @return boolean? timeout -function Reader:read(fmt, sec) +function Reader:read(fmt) assert(fmt == nil or type(fmt) == 'number' or type(fmt) == 'string', 'fmt must be integer, string or nil') - assert(sec == nil or type(sec) == 'number', 'sec must be number or nil') local t = type(fmt) if t == 'number' then @@ -90,7 +91,7 @@ function Reader:read(fmt, sec) local buf = self.buf local len = #buf if len < n then - local data, err, timeout = read(self.fd, n - len, sec) + local data, err, timeout = read(self.fd, n - len, self.waitsec) if not data then return nil, err, timeout end @@ -120,14 +121,14 @@ function Reader:read(fmt, sec) return buf end -- read all data from the file - return read(self.fd, nil, sec) + return read(self.fd, nil, self.waitsec) end local buf = self.buf local head, tail = find(buf, '\r?\n', 1) while not head do -- need to read more data - local data, err, timeout = read(self.fd, nil, sec) + local data, err, timeout = read(self.fd, nil, self.waitsec) if not data then return nil, err, timeout end @@ -155,9 +156,12 @@ Reader = require('metamodule').new(Reader) --- new --- @param file string|integer|file* +--- @param sec number? --- @return io.reader? rdr --- @return any err -local function new(file) +local function new(file, sec) + assert(sec == nil or type(sec) == 'number', 'sec must be number or nil') + local t = type(file) local f, err if isfile(file) then @@ -174,7 +178,7 @@ local function new(file) if not f then return nil, err end - return Reader(fileno(f), f) + return Reader(fileno(f), f, sec) end return { diff --git a/test/reader_test.lua b/test/reader_test.lua index acb99d6..4f54377 100644 --- a/test/reader_test.lua +++ b/test/reader_test.lua @@ -3,6 +3,8 @@ local testcase = require('testcase') local assert = require('assert') local fileno = require('io.fileno') local reader = require('io.reader') +local pipe = require('os.pipe') +local gettime = require('time.clock').gettime local TEST_TXT = 'test.txt' @@ -25,6 +27,11 @@ function testcase.new() assert.is_nil(err) assert.match(r, '^io.reader: ', false) + -- test that create a new reader from file with timeout seconds + r, err = reader.new(f, 1) + assert.is_nil(err) + assert.match(r, '^io.reader: ', false) + -- test that create a new reader from filename r, err = reader.new(TEST_TXT) assert.is_nil(err) @@ -40,6 +47,13 @@ function testcase.new() assert.is_nil(err) assert.match(r, '^io.reader: ', false) + -- test that create a new reader from pipe file descriptor + local pr, _, perr = pipe(true) + assert(perr == nil, perr) + r, err = reader.new(pr:fd()) + assert.is_nil(err) + assert.match(r, '^io.reader: ', false) + -- test that return err if file descriptor is invalid r, err = reader.new(-1) assert.is_nil(r) @@ -49,6 +63,10 @@ function testcase.new() r, err = reader.new(true) assert.is_nil(r) assert.match(err, 'FILE*, pathname or file descriptor expected, got boolean') + + -- test that throws an error if invalid sec argument + err = assert.throws(reader.new, f, true) + assert.match(err, 'sec must be number or nil') end function testcase.read_with_format_string() @@ -112,3 +130,38 @@ function testcase.read_nbyte() err = assert.throws(r.read, r, -1) assert.match(err, 'negative number') end + +function testcase.read_with_timeout() + local pr, pw, perr = pipe(true) + assert(perr == nil, perr) + + -- test that read timeout after 0.5 second + local r = assert(reader.new(pr:fd(), .5)) + local t = gettime() + local s, err, again = r:read() + t = gettime() - t + assert.is_nil(err) + assert.is_nil(s) + assert.is_true(again) + assert.is_true(t >= .5 and t < .6) + + -- test that read line from pipe + pw:write('hello\nworld!\n') + s, err, again = r:read() + assert.is_nil(err) + assert.is_nil(again) + assert.equal(s, 'hello') + + -- test that read line from pipe even if peer of pipe is closed + pw:close() + s, err, again = r:read() + assert.is_nil(err) + assert.is_nil(again) + assert.equal(s, 'world!') + + -- test that return nil if eof + s, err, again = r:read() + assert.is_nil(s) + assert.is_nil(err) + assert.is_nil(again) +end