# Specl specifications for APIs in posix.c
#
# Specifications are topographically sorted, with fundamental calls specified
# at the top of the file, and calls with dependencies on correct functioning
# of earlier specifications further down.
#
# If you get a series of failed expectations, fixing the earliest failures
# first will often clear up later failures automatically!

before:
  this_module = 'posix'
  global_table = '_G'

  M = require(this_module)

specify posix:
- context when required:
  - it does not touch the global table:
      expect(show_apis {added_to=global_table, by=this_module}).
         to_equal {}

- describe euidaccess:
  - before: f = M.euidaccess
  - context with bad arguments:
      badargs.diagnose(f, "euidaccess(string, string)")


- describe errno:
  - context with bad arguments:
      badargs.diagnose(M.errno, "(?int)")


- describe set_errno:
  - context with bad arguments:
      badargs.diagnose(M.set_errno, "(int)")


- describe getopt:
  - context with bad arguments:
      badargs.diagnose(M.getopt, "(list, string, ?int, ?int)")


- describe openpty:
  - context with bad arguments:
      badargs.diagnose(M.openpty, "openpty()")


- describe execx:
  - before:
      fork = require 'posix.unistd'.fork
      wait = require 'posix.sys.wait'.wait

      f = function(...)
         if fork() == 0 then
            M.execx(...)
         end
         return ({wait()})[3]
      end

  - context with bad arguments:
      badargs.diagnose(M.execx, "execx(function|table, ?any*)")

  - it executes a command returning normal exit status:
      expect(f{"true"}).to_be(0)
  - it executes a command returning abnormal exit status:
      expect(f{"false"}).to_be(1)
  - it executes a command with additional arguments:
      expect(luaproc([[
         os.exit(require "posix".execx {"echo", "foo"})
      ]])).to_succeed_with "foo\n"
  - it propagates failed command errno:
      expect(f{"no-such-command-exists"}).to_be(require 'posix.errno'.ENOENT)
  - it executes a function returning normal exit status:
      expect(f(function() end)).to_be(0)
  - it executes a function and propagates result:
      expect(f(function() return 2 end)).to_be(2)
  - it executes a function with arguments and propagates result:
      expect(f(function(i) return i end, 3)).to_be(3)
  - it executes a function with output to stdout:
      expect(luaproc([[
         os.exit(require "posix".execx(function()
            io.stdout:write "foo\n"
         end))
      ]])).to_succeed_with "foo\n"


- describe spawn:
  - before: f = M.spawn

  - context with bad arguments:
      badargs.diagnose(f, "spawn(function|table, ?any*)")

  - it spawns a command and can detect success:
      expect(f({"true"})).to_be(0)
  - it can detect failure:
      expect(f({"false"})).to_be(1)
  - it can pass a table of arguments:
      expect(luaproc([[
         os.exit(require "posix".spawn {"echo", "foo"})
      ]])).to_succeed_with "foo\n"
  - it can pass a function:
      expect(luaproc([[
         os.exit(require "posix".spawn(function() io.stdout:write "foo\n" end))
      ]])).to_succeed_with "foo\n"

- describe popen:
  - before:
      popen, pclose = M.popen, M.pclose
      read, write, BUFSIZE = M.read, M.write, M.BUFSIZ
  - context with bad arguments:
      badargs.diagnose(popen, "popen(function|table, string, ?function)")

  - it spawns a command and reads its output:
      p = popen({"echo", "foo"}, "r")
      s = M.read(p.fd, M.BUFSIZ)
      pclose(p)
      expect(s).to_equal "foo\n"

  - it spawns a command and writes its input: |
      script = [[
         local M = require "posix"
         local p = M.popen({"wc"}, "w")
         local s = "foo bar baz\n"
         local r = M.write(p.fd, s)
         M.pclose(p)
         os.exit(r == #s and 0 or 1)
      ]]
      expect(luaproc(script)).to_succeed_while_matching "^%s*1%s+3%s+12\n$"

- describe popen_pipeline:
  - before:
      popen_pipeline, pclose = M.popen_pipeline, M.pclose
      read, write, BUFSIZE = M.read, M.write, M.BUFSIZ
  - context with bad arguments:
      badargs.diagnose(popen_pipeline, "popen_pipeline(function|table, string, ?function)")

  - it spawns a pipeline and reads its output:
      p = popen_pipeline({{"echo", "foo", "bar", "baz"}, {"wc"}, {"wc", "-l"}}, "r")
      expect(read(p.fd, M.BUFSIZ)).to_match("^%s*1\n")
      pclose(p)

  - it spawns a pipeline and writes its input: |
      script = [[
         local M = require "posix"
         local s = "foo bar baz\n"
         local p = M.popen_pipeline({{"cat"}, {"wc"}}, "w")
         local r = M.write(p.fd, s)
         M.pclose(p)
         os.exit(r == #s and 0 or 1)
      ]]
      expect(luaproc(script)).to_succeed_while_matching "^%s*1%s+3%s+12\n$"


- describe timeradd:
  - before:
      tv1 = {tv_sec=2, tv_usec=123456}
      tv2 = {tv_sec=1, tv_usec=876543}

      f = M.timeradd

  - context with bad arguments:
      badargs.diagnose(f, "timeradd(table, table)")

  - context with systime.gettimeofday timervals:
    - it adds both fields of a timerval:
        expect(f(tv1, tv2)).to_equal {sec=3, usec=999999}
    - it adds both fields of a systime timerval:
        expect(f(tv1, tv2)).to_equal {sec=3, usec=999999}
    - it carries overflow second:
        expect(f(tv2, tv2)).to_equal {sec=3, usec=753086}
    - it does not require 'tv_sec' field:
        expect(f(tv1, {tv_usec=876543})).
           to_equal {sec= 2, usec=999999}
        expect(f({tv_usec= 123456}, {tv_usec=876543})).
           to_equal {sec=0, usec=999999}
    - it does not require 'tv_usec' field:
        expect(f(tv1, {tv_sec=1})).
           to_equal {sec=3, usec=123456}
        expect(f({tv_sec=2}, {tv_sec=1})).
           to_equal {sec=3, usec=0}
  - context with legacy timervals:
    - before:
        stv1 = {sec=tv1.tv_sec, usec=tv1.tv_usec}
        stv2 = {sec=tv2.tv_sec, usec=tv2.tv_usec}
    - it adds both fields of a timerval:
        expect(f(stv1, stv2)).to_equal {sec=3, usec=999999}
    - it carries overflow second:
        expect(f(stv2, stv2)).to_equal {sec=3, usec=753086}
    - it does not require 'sec' field:
        expect(f(stv1, {usec=876543})).to_equal {sec=2, usec=999999}
        expect(f({usec=123456}, {usec=876543})).to_equal {sec=0, usec=999999}
    - it does not require 'usec' field:
        expect(f(stv1, {sec=1})).to_equal {sec=3, usec=123456}
        expect(f({sec=2}, {sec=1})).to_equal {sec=3, usec=0}


- describe timercmp:
  - before:
      tv1 = {tv_sec=2, tv_usec=123456}
      tv2 = {tv_sec=1, tv_usec=876543}
      f = M.timercmp

  - context with bad arguments:
      badargs.diagnose(f, "timercmp(table, table)")

  - context with systime.gettimeofday timervals:
    - it returns 0 if timers are equal:
        expect(f(tv1, tv1)).to_equal(0)
    - it returns positive integer if second timer is greater than first:
        expect(f(tv1, tv2) > 0).to_be(true)
    - it returns negative integer if first timer is greater than the second:
        expect(f(tv2, tv1) < 0).to_be(true)
  - context with legacy timervals:
    - before:
        stv1 = {sec=tv1.tv_sec, usec=tv1.tv_usec}
        stv2 = {sec=tv2.tv_sec, usec=tv2.tv_usec}
    - it returns 0 if timers are equal:
        expect(f(stv1, stv1)).to_equal(0)
    - it returns positive integer if second timer is greater than first:
        expect(f(stv1, stv2) > 0).to_be(true)
    - it returns negative integer if first timer is greater than the second:
        expect(f(stv2, stv1) < 0).to_be(true)


- describe timersub:
  - before:
      tv1 = {tv_sec=2, tv_usec=876543}
      tv2 = {tv_sec=1, tv_usec=123456}
      f = M.timersub

  - context with bad arguments:
      badargs.diagnose(f, "timersub(table, table)")

  - context with systime.gettimeofday timervals:
    - it subtracts both fields of a timerval:
        expect(f(tv1, tv2)).to_equal {sec=1, usec=753087}
    - it carries overflow second:
        expect(f(tv2, tv1)).to_equal {sec=-2, usec=246913}
    - it does not require 'tv_sec' field:
        expect(f(tv1, {tv_usec=123456})).to_equal {sec=2, usec=753087}
        expect(f({tv_usec=876543}, {tv_usec=123456})).
           to_equal {sec=0, usec=753087}
    - it does not require 'tv_usec' field:
        expect(f(tv1, {tv_sec=1})).to_equal {sec=1, usec=876543}
        expect(f({tv_sec=2}, {tv_sec=1})).to_equal {sec=1, usec=0}
  - context with legacy timervals:
    - before:
        stv1 = {sec=tv1.tv_sec, usec=tv1.tv_usec}
        stv2 = {sec=tv2.tv_sec, usec=tv2.tv_usec}
    - it subtracts both fields of a timerval:
        expect(f(stv1, stv2)).to_equal {sec=1, usec=753087}
    - it carries overflow second:
        expect(f(stv2, stv1)).to_equal {sec=-2, usec=246913}
    - it does not require 'sec' field:
        expect(f(stv1, {usec=123456})).to_equal {sec=2, usec=753087}
        expect(f({usec = 876543}, {usec = 123456})).
           to_equal {sec=0, usec=753087}
    - it does not require 'usec' field:
        expect(f(stv1, {sec=1})).to_equal {sec=1, usec=876543}
        expect(f({sec=2}, {sec=1})).to_equal {sec=1, usec=0}


- specify file descriptors:
  - describe fileno:
    - context with bad arguments:
        badargs.diagnose(M.fileno, "(file)")


  - describe rpoll:
    - context with bad arguments:
        badargs.diagnose(M.rpoll, "(int, int)")


  - describe poll:
    - context with bad arguments:
        badargs.diagnose(M.poll, "(table, ?int)")


  - describe close:
    - context with bad arguments:
        badargs.diagnose(M.close, "(int)")


  - describe dup:
    - context with bad arguments:
        badargs.diagnose(M.dup, "(int)")


  - describe dup2:
    - context with bad arguments:
        badargs.diagnose(M.dup2, "(int, int)")


  - describe pipe:
    - before:
        pipe, read, write = M.pipe, M.read, M.write
        data = "0123456789"
        pout, pin = pipe()

    - after:
        M.close(pin)
        M.close(pout)

    - context with bad arguments:
        badargs.diagnose(pipe, "()")

    - it creates a pipe:
        expect(pout > 0).to_be(true)
        expect(pin > 0).to_be(true)
    - it can buffer characters:
        write(pin, data)
        expect(read(pout, data:len())).to_be(data)


  - describe read:
    - context with bad arguments:
        badargs.diagnose(M.read, "(int, int)")


  - describe write:
    - before:
        EINVAL, read, write = M.EINVAL, M.read, M.write
        data = "0123456789"
        pout, pin = M.pipe()

    - after:
        M.close(pin)
        M.close(pout)

    - context with bad arguments:
        badargs.diagnose(M.write, "(int, string, ?int, ?int)")

    - context out of bounds:
        - 'it diagnoses argument #3 too small': |
            expect({write(pin, data, -1)}).to_equal {
              nil, "write: invalid attempt to access offset -1 in a buffer of length 10", EINVAL,
            }
        - 'it diagnoses argument #3 too large': |
            expect({write(pin, data, 99)}).to_equal {
              nil, "write: invalid attempt to access offset 99 in a buffer of length 10", EINVAL,
            }
        - 'it diagnoses argument #4 too small': |
            expect({write(pin, data, nil, -1)}).to_equal {
              nil, "write: invalid attempt to access offset -1 in a buffer of length 10", EINVAL,
            }
        - 'it diagnoses argument #4 too large': |
            expect({write(pin, data, nil, 99)}).to_equal {
              nil, "write: invalid attempt to access offset 99 in a buffer of length 10", EINVAL,
            }
            expect({write(pin, data, 6, 6)}).to_equal {
              nil, "write: invalid attempt to access offset 12 in a buffer of length 10", EINVAL,
            }

    - it returns number of bytes written:
        expect(write(pin, data)).to_be(data:len())
        expect(write(pin, data, 4)).to_be(4)
        expect(write(pin, data, nil, 4)).to_be(data:len() - 4)
    - it accepts a zero-length buffer:
        expect(write(pin, '')).to_be(0)
        expect(write(pin, data, 0)).to_be(0)
    - it writes buffer contents:
        write(pin, data)
        expect(read(pout, data:len())).to_be(data)
    - it writes first nbytes of buffer:
        write(pin, data, 4)
        expect(read(pout, data:len())).to_be '0123'
    - it writes a substring of buffer:
        write(pin, data, 4, 4)
        expect(read(pout, data:len())).to_be '4567'
    - it writes to end of buffer when nbytes is nil:
        write(pin, data, nil, 4)
        expect(read(pout, data:len())).to_be '456789'


  - describe fcntl:
    - before:
        F_GETLK, F_SETLK, F_SETLKW = M.F_GETLK, M.F_SETLK, M.F_SETLKW
        F_RDLCK, F_WRLCK, F_UNLCK = M.F_RDLCK, M.F_WRLCK, M.F_UNLCK
        SEEK_SET, SEEK_CUR, SEEK_END = M.SEEK_SET, M.SEEK_CUR, M.SEEK_END

        fcntl, typeerrors = init(posix, "fcntl")

    # M.fcntl diagnoses the third arg differently depending on
    # the value of `cmd`, which `diagnose.badargs` can't express; ergo
    # manual checks here...
    - context with bad arguments:
      - 'it diagnoses missing argument #1':
          expect(fcntl()).to_raise.any_of(typeerrors(1, "int"))
      - 'it diagnoses argument #1 type not int':
          expect(fcntl(false)).to_raise.any_of(typeerrors(1, "int", "boolean"))
      - 'it diagnoses missing argument #2':
          expect(fcntl(-1)).to_raise.any_of(typeerrors(2, "int"))
      - 'it diagnoses argument #2 type not int':
          expect(fcntl(-1, false)).to_raise.any_of(typeerrors(2, "int", "boolean"))
      - 'it diagnoses missing argument #3 to F_GETLK':
          expect(fcntl(-1, F_GETLK)).to_raise.any_of(typeerrors(3, "table"))
      - 'it diagnoses argument #3 type to F_GETLK not table':
          expect(fcntl(-1, F_GETLK, false)).
            to_raise.any_of(typeerrors(3, "table", "boolean"))
      - 'it diagnoses argument #3 type to non-F_GETLK not int':
          expect(fcntl(-1, 0, false)).
            to_raise.any_of(typeerrors(3, "?int", "boolean"))
      - 'it diagnoses too many arguments':
          expect(fcntl(-1, F_GETLK, {}, false)).to_raise.any_of(typeerrors(4))
          expect(fcntl(-1, 0, -1, false)).to_raise.any_of(typeerrors(4))

    - it has all needed constants:
        expect(type(F_GETLK)).to_be "number"
        expect(type(F_SETLK)).to_be "number"
        expect(type(F_SETLKW)).to_be "number"
        expect(type(F_RDLCK)).to_be "number"
        expect(type(F_WRLCK)).to_be "number"
        expect(type(F_UNLCK)).to_be "number"
        expect(type(SEEK_SET)).to_be "number"
        expect(type(SEEK_CUR)).to_be "number"
        expect(type(SEEK_END)).to_be "number"

    - context when file locking:
      - before:
          mkstemp, open, close = M.mkstemp, M.open, M.close
          fork, wait, errno = M.fork, M.wait, M.errno
          O_RDWR, EAGAIN, EACCES = M.O_RDWR, M.EAGAIN, M.EACCES
          P_CHILD = 0
          SUCCESS = 0
          fd, path = mkstemp "tmpXXXXXX"
          close(fd)
          parent_pid = M.getpid "pid"

          query_lock = {
             l_type = F_RDLCK, l_whence = SEEK_SET, l_start = 0, l_len = 0,
          }
          write_lock = {
             l_type = F_WRLCK, l_whence = SEEK_SET, l_start = 0, l_len = 0,
          }
      - after:
          os.remove(path)

      - it checks whether lock is possible with F_GETLK:
          fd = open(path, O_RDWR)
          result = fcntl(fd, F_GETLK, query_lock)
          expect(result).to_be(SUCCESS)
          expect(query_lock.l_type).to_be(F_UNLCK)
          close(fd)
      - it can lock file with F_SETLK: |
          parent_fd = open(path, O_RDWR)
          result = fcntl(parent_fd, F_SETLK, write_lock)
          expect(result).to_be(SUCCESS)
          process = fork()
          if process == P_CHILD then
             child_fd = open(path, O_RDWR)
             result = fcntl(child_fd, F_GETLK, query_lock)
             close(child_fd)
             --(not sure how to expect() in subprocess)
             if result ~= SUCCESS then
                os.exit(10)
             elseif query_lock.l_pid ~= parent_pid then
                os.exit(11)
             else
                os.exit(12)
             end
          else
             _, _, exit_code = wait(process)
             expect(exit_code).to_be(12)
          end
          close(parent_fd)
      - it returns error if cannot lock file with F_SETLK: |
          parent_fd = open(path, O_RDWR)
          result = fcntl(parent_fd, F_SETLK, write_lock)
          expect(result).to_be(SUCCESS)
          process = fork()
          if process == P_CHILD then
             child_fd = open(path, O_RDWR)
             result, errmsg, errcode = fcntl(child_fd, F_SETLK, write_lock)
             close(child_fd)
             --(not sure how to expect() in subprocess)
             if result == SUCCESS then
                os.exit(100)
             elseif result == nil and errcode ~= EACCES and errcode ~= EAGAIN then
                os.exit(errcode)
             else
                os.exit(101)
             end
          else
             _, _, exit_code = wait(process)
             expect({exit_code, errno(exit_code)}).to_equal {101, errno(101)}
          end
          close(parent_fd)


  - describe lseek:
    - before:
        close, lseek, open, read, write =
           M.close, M.lseek, M.open, M.read, M.write
        SEEK_SET, SEEK_CUR, SEEK_END =
           M.SEEK_SET, M.SEEK_CUR, M.SEEK_END

    - context with bad arguments:
        badargs.diagnose(lseek, "(int, int, int)")

    - it changes the current position of a file descriptor:
        _, path = M.mkstemp(template)
        fd = open(path, M.O_RDWR)
        expect(fd).not_to_be(nil)
        write(fd, "0123456789")
        lseek(fd, 3, SEEK_SET)
        expect(read(fd, 3)).to_be "345"
        lseek(fd, -2, SEEK_CUR)
        expect(read(fd, 3)).to_be "456"
        lseek(fd, -5, SEEK_END)
        expect(read(fd, 3)).to_be "567"
        close(fd)
        os.remove(path)



- specify file system:
  - before:
      # Make and change into a temporary subdirectory where we can
      # control all the contents for self-contained examples.
      link, mkdir, mkdtemp = M.link, M.mkdir, M.mkdtemp
      origwd = M.getcwd()
      dir, errmsg = mkdtemp(template)
      mkdir(dir .. "/subdir")
      link("subdir", dir .. "/soft", true)
      touch(dir .. "/file")
      link(dir .. "/file", dir .. "/hard")
      link("no such destination", dir .. "/dangling", true)
  - after:
      M.chdir(origwd)
      rmtmp(dir)


  - describe sync:
    - context with bad arguments:
        badargs.diagnose(M.sync, "()")


  - describe fsync:
    - context with bad arguments:
        badargs.diagnose(M.fsync, "(int)")


  - describe fdatasync:
    - context with bad arguments:
        if M.fdatasync then
          badargs.diagnose(M.fdatasync, "(int)")
        end


  - describe basename:
    - before:
        basename = M.basename

    - context with bad arguments:
        badargs.diagnose(basename, "(string)")

    - it returns a path without leading directories:
        expect(basename "/foo/bar").to_be "bar"


  - describe dirname:
    - before:
        dirname = M.dirname

    - context with bad arguments:
        badargs.diagnose(dirname, "(string)")

    - it return a path without final element:
        expect(dirname "/foo/bar").to_be "/foo"


  - describe dir:
    - context with bad arguments:
        badargs.diagnose(M.dir, "(?string)")


  - describe glob:
    - before:
        chdir, glob, mkdtemp = M.chdir, M.glob, M.mkdtemp
        mkdir = M.mkdir

    - context with bad arguments:
        badargs.diagnose(glob, "glob([?string|table])")

    - it matches files in the given directory:
        dir = mkdtemp(template)
        touch(dir .. "/test.1")
        touch(dir .. "/test.2")
        touch(dir .. "/extra_file")
        chdir(dir)
        globlist, errmsg = glob "test.*"
        expect(errmsg).to_be(nil)
        expect(type(globlist)).to_be "table"
        expect(globlist).to_equal {"test.1", "test.2"}
        rmtmp(dir)

    - it matches files and directories in the given directory:
        dir = mkdtemp(template)
        touch(dir .. "/test.1")
        chdir(dir)
        mkdir("foo")
        mkdir("bar")
        globlist = glob()
        expect(globlist).to_equal {"bar", "foo", "test.1"}
        rmtmp(dir)

    - it adds / to filenames if GLOB_MARK is true:
        dir = mkdtemp(template)
        chdir(dir)
        mkdir("foo")
        mkdir("bar")
        globlist = glob({pattern="*", MARK=true})
        expect(globlist).to_equal {"bar/", "foo/"}
        rmtmp(dir)

    - it does not add / to filenames if GLOB_MARK is false:
        dir = mkdtemp(template)
        chdir(dir)
        mkdir("foo")
        mkdir("bar")
        globlist = glob({MARK=false})
        expect(globlist).to_equal {"bar", "foo"}
        rmtmp(dir)

  - describe files:
    - before:
        files = M.files

    - context with bad arguments:
        badargs.diagnose(files, "(?string)")

    - it returns a table of files in the given directory:
        t = {}
        for f in files(dir) do
           table.insert(t, f)
        end
        table.sort(t)
        expect(t).to_equal {".", "..", "dangling", "file", "hard", "soft", "subdir"}


  - describe getcwd:
    - context with bad arguments:
        badargs.diagnose(M.getcwd, "()")


  - describe chdir:
    - before:
        chdir, chmod, getcwd, mkdir, rmdir =
           M.chdir, M.chmod, M.getcwd, M.mkdir, M.rmdir
        cwd = getcwd()
    - after:
        chdir(cwd)
        pcall(rmdir, "x")

    - context with bad arguments:
        badargs.diagnose(chdir, "(string)")

    - it changes to a relative directory:
        thisdir = M.basename(getcwd())
        expect(Emsg(chdir("../" .. thisdir))).
           not_to_contain "No such file or directory"
        expect(Emsg(chdir "..")).not_to_contain "No such file or directory"
    - it changes to an absolute directory:
        expect(Emsg(chdir "/var/tmp/")).
           not_to_contain "No such file or directory"
    - it diagnoses missing directory:
        expect(Emsg(chdir "very_unlikely_to_exist")).
          to_contain "No such file or directory"
    - it diagnoses insufficient permissions:
        mkdir "x"
        chmod("x", "a-rwx")
        expect(Emsg(chdir "x")).
           to_contain "Permission denied"
        rmdir "x"


  - describe rmdir:
    - before:
        mkdir, rmdir = M.mkdir, M.rmdir

    - context with bad arguments:
        badargs.diagnose(rmdir, "(string)")

    - it removes the named directory:
        mkdir "x"
        expect(Emsg(rmdir "x")).not_to_contain "No such file or directory"
    - it diagnoses missing directory:
        expect(Emsg(rmdir ".")).to_contain "Invalid argument"


  - describe unlink:
    - context with bad arguments:
        badargs.diagnose(M.unlink, "(string)")


  - describe link:
    - before:
        link, stat = M.link, M.stat
        touch "xxx"
    - after:
        os.remove "xxx"

    - context with bad arguments:
        badargs.diagnose(link, "(string, string, ?boolean)")

    - it creates hard links:
        expect(Emsg(link("xxx", "xxx-hard"))).to_be ""
        expect(stat("xxx-hard", "ino")).to_be(stat("xxx", "ino"))
        os.remove "xxx-hard"
    - it creates soft links:
        expect(Emsg(link("xxx", "xxx-soft", true))).to_be ""
        expect(stat("xxx-soft", "type")).to_be "link"
        os.remove "xxx-soft"

  - describe readlink:
    - before:
        readlink = M.readlink

    - context with bad arguments:
        badargs.diagnose(readlink, "(string)")

    - it diagnoses missing file: |
        _, err, code = readlink "does not exist!"
        expect(err).to_match "^does not exist!: "
        expect(code).to_be(M.ENOENT)
    - it diagnoses non-symbolic link: |
        _, err, code = readlink(dir .. "/file")
        expect(err).to_be(dir .. "/file: not a symbolic link")
        expect(code).to_be(M.EINVAL)
    - it reads the contents of a symbolic link:
        expect(readlink(dir .. "/soft")).to_be "subdir"
    - it reads the contents of a dangling symbolic link:
        expect(readlink(dir .. "/dangling")).to_be "no such destination"


  - describe access:
    - before:
        access = M.access
        touch "xxx"
    - after:
        os.remove "xxx"

    - context with bad arguments:
        badargs.diagnose(M.access, "(string, ?string)")

    - it checks whether a file is visible to the real user:
        expect(Emsg(access("xxx", "f"))).to_be ""
    - it checks whether a file is readable by the real user:
        expect(Emsg(access("xxx", "r"))).to_be ""
    - it checks whether a file is writable by the real user:
        expect(Emsg(access("xxx", "w"))).to_be ""
    - "it defaults to 'f' with no mode argument":
        expect(access("xxx")).to_be(access("xxx", "f"))
    - it diagnoses missing files:
        os.remove "xxx"
        expect(Emsg(access "xxx")).to_contain "No such file or directory"


  - describe utime:
    - before:
        stat, utime = M.stat, M.utime
        touch "xxx"
    - after:
        os.remove "xxx"

    - context with bad arguments:
        badargs.diagnose(M.utime, "(string, ?int, ?int)")

    - it sets the last file modification time:
        mtime = stat("/etc", "mtime")
        expect(stat("xxx", "mtime")).not_to_equal(mtime)
        expect(Emsg(utime("xxx", mtime))).to_be ""
        expect(stat("xxx", "mtime")).to_equal(mtime)
    - it sets the last file access time:
        atime = stat("/etc", "atime")
        expect(stat("xxx", "atime")).not_to_equal(atime)
        expect(Emsg(utime("xxx", nil, atime))).to_be ""
        expect(stat("xxx", "atime")).to_equal(atime)


- specify process management:
  - describe nice:
    - before:
        nice = M.nice

    - context with bad arguments:
        badargs.diagnose(nice, "(int)")

    - it adjusts the process priority:
        old = nice(1)
        expect(old).not_to_be(nil)
        new = nice(2)
        expect(new).to_be(old + 2)


  - describe fork:
    # NOTE: Calling expect in a child process does not report results
    #       back to parent, so we send test data over a pipe.
    - before:
        nice, execp, fork, getpid, getppid, wait =
           M.nice, M.execp, M.fork, M.getpid, M.getppid, M.wait
        _exit, close, pipe, read, write =
           M._exit, M.close, M.pipe, M.read, M.write
        P_CHILD = 0

    - context with bad arguments:
        badargs.diagnose(fork, "()")

    - it copies itself to a new child process: |
        r, w = pipe()
        process, status = fork(), 67
        if process == P_CHILD then
           close(r) -- close unused read end
           -- write parent pid and child pid to shared pipe.
           write(w, tostring(getppid()).."\n")
           write(w, tostring(getpid "pid").."\n")
           close(w)
           _exit(status)
        else
           M.close(w) -- close unused write end
           p, msg, ret = wait(process)
           expect(p).to_be(process)
           expect(msg).to_be "exited"
           expect(ret).to_be(status)
           -- check pids from child process.
           buf = M.read(r, 1024)
           cppid, cpid = string.match(buf, "(%d+)\n(%d+)\n")
           close(r)
           expect(cppid).to_be(tostring(getpid "pid"))
           expect(cpid).to_be(tostring(process))
        end


  - describe _exit:
    - context with bad arguments:
        badargs.diagnose(M. _exit, "(int)")


  - describe wait:
    - context with bad arguments:
        badargs.diagnose(M.wait, "(?int, ?int)")


  - describe setpid:
    - before:
        setpid, typeerrors = init(posix, "setpid")

    - context with bad arguments:
      - 'it diagnoses missing argument #1':
          expect(setpid()).to_raise.any_of(typeerrors(1, "string"))
      - 'it diagnoses argument #1 type not string':
          expect(setpid(false)).to_raise.any_of(typeerrors(1, "string", "boolean"))
      - 'it diagnoses argument #1 invalid option': |
          expect(setpid "fubar").to_raise.any_of {
             "bad argument #1 to '?' (invalid id option 'f')",
             "bad argument #1 to 'setpid' (invalid id option 'f')",
          }
      - 'it diagnoses missing argument #2':
          expect(setpid "p").to_raise.any_of(typeerrors(2, "int"))
      - 'it diagnoses argument #2 type not int':
          expect(setpid("p", false)).
             to_raise.any_of(typeerrors(2, "int", "boolean"))
      - 'it diagnoses missing argument #3':
          expect(setpid("p", 0)).to_raise.any_of(typeerrors(3, "int"))
      - 'it diagnoses argument #3 type not int':
          expect(setpid("p", 0, false)).
             to_raise.any_of(typeerrors(3, "int", "boolean"))
      - it diagnoses too many arguments:
          expect(setpid("p", 0, 0, false)).to_raise.any_of(typeerrors(4))
          expect(setpid("u", 0, false)).to_raise.any_of(typeerrors(3))


  - describe sleep:
    - context with bad arguments:
        badargs.diagnose(M.sleep, "(int)")


  - describe sched_setscheduler:
    - context with bad arguments:
        if M.sched_setscheduler then
           badargs.diagnose(M.sched_setscheduler, "(?int, ?int, ?int)")
        end


  - describe sched_getscheduler:
    - context with bad arguments:
        if M.sched_getscheduler then
           badargs.diagnose(M.sched_getscheduler, "(?int)")
        end


  - describe ctermid:
    - before:
        ctermid = M.ctermid

    - context with bad arguments:
        badargs.diagnose(M.ctermid, "()")

    - it returns the pathname of the controlling terminal:
        expect(ctermid()).to_match.any_of {"/.*pts.*", "/.*tty.*"}


  - describe isatty:
    - context with bad arguments:
        badargs.diagnose(M.isatty, "(int)")


  - describe tcsetattr:
    - context with bad arguments:
        badargs.diagnose(M.tcsetattr, "(int, int, table)")


  - describe tcgetattr:
    - context with bad arguments:
        badargs.diagnose(M.tcgetattr, "(int)")


  - describe tcsendbreak:
    - context with bad arguments:
        badargs.diagnose(M.tcsendbreak, "(int, int)")


  - describe tcdrain:
    - context with bad arguments:
        badargs.diagnose(M.tcdrain, "(int)")


  - describe tcflush:
    - context with bad arguments:
        badargs.diagnose(M.tcflush, "(int, int)")


  - describe tcflow:
    - context with bad arguments:
        badargs.diagnose(M.tcflow, "(int, int)")


  - describe tcgetwinsize:
    - context with bad arguments:
        if M.tcgetwinsize then
          badargs.diagnose(M.tcgetwinsize, "(int)")
        end


  - describe tcsetwinsize:
    - context with bad arguments:
        if M.tcsetwinsize then
          badargs.diagnose(M.tcsetwinsize, "(int, table)")
        end


- specify user management:
  - describe getlogin:
    - context with bad arguments:
        badargs.diagnose(M.getlogin, "()")


  - describe getgroups:
    - context with bad arguments:
        if M.getgroups then
           badargs.diagnose(M.getgroups, "()")
        end


  - describe crypt:
    - before:
        crypt = M.crypt
        key, salt = "hello", "pl"

    - context with bad arguments:
        if M.crypt then
           badargs.diagnose(M.crypt, "(string, string)")
        end

    - it can perform repeatable one-way hashing:
        hash = crypt(key, salt)
        expect(crypt(key, salt)).to_be(hash)
    - it encrypts differently for a different salt:
        expect(crypt(key, salt)).not_to_equal(crypt(key, "/."))
