mmap: tool to mmap /sys resource files and poke pci registers

	* now with test cases!

Change-Id: I96ada77ff477868d34fd52de6c2ffc38832db641
diff --git a/cmds/Makefile b/cmds/Makefile
index dfbb363..11b2c8b 100644
--- a/cmds/Makefile
+++ b/cmds/Makefile
@@ -10,6 +10,7 @@
 	isoping \
 	isostream \
 	mcastreceive \
+	mmap \
 	randomdata \
 	setuid \
 	udpburst \
diff --git a/cmds/mmap.c b/cmds/mmap.c
new file mode 100644
index 0000000..1f60a98
--- /dev/null
+++ b/cmds/mmap.c
@@ -0,0 +1,559 @@
+/*
+ * This is a scriptable tool to use mmap and write to registers.
+ */
+
+#define _POSIX_SOURCE   /* for fileno! */
+#define _BSD_SOURCE     /* for usleep! */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <memory.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <linux/limits.h>
+#include <fcntl.h>
+
+#define MAXSLOTS  10
+#define MAXARGS    10
+
+struct slot {
+  char          path[PATH_MAX];
+  int           fd;
+  off_t         fileLength;
+  void*         map;
+  uint64_t      addr;
+  uint64_t      length;
+};
+
+static struct slot slots[MAXSLOTS];
+static char* posPrefix = "";
+
+void usage(const char* prog)
+{
+  fprintf(stderr, "Usage: %s [command-file]\n", prog);
+  fprintf(stderr, "\twhere command-file or stdin contains:\n");
+  fprintf(stderr, "\t\tfile 0 /sys/bus/pci/devices/0000:01:00.0/resource0 "
+                    "0 0x10000\n");
+  fprintf(stderr, "\t\tfile 2 /sys/bus/pci/devices/0000:01:00.0/resource2 "
+                    "0 0x10000\n");
+  fprintf(stderr, "\t\tfile 4 /sys/bus/pci/devices/0000:01:00.0/resource4 "
+                    "0 0x10000\n");
+  fprintf(stderr, "\t\tread 2 16 4  "
+                    "# read file 2, addr 16, length 4\n");
+  fprintf(stderr, "\t\twrite 4 18 4 0xffff  "
+                    "# write file 4, addr 18, length 4, value 0xffff\n");
+  fprintf(stderr, "\t\tdump 4 18 4 100  "
+                    "# dump file 4, addr 18, length 4, 100 values\n");
+  fprintf(stderr, "\t\tclose 0  "
+                    "# close a file\n");
+}
+
+int asUnsigned(uint64_t* ip, const char* str)
+{
+  char* end;
+
+  uint64_t value = strtoull(str, &end, 0);
+  if (end == str || *end != '\0') {
+    fprintf(stderr, "%s", posPrefix);
+    fprintf(stderr, "failed to parse '%s' as unsigned (eg 0x10 or 16)\n", str);
+    return -1;
+  }
+  *ip = value;
+  return 0;
+}
+
+int asFileAddr(uint64_t* ip, const char* str, int slot)
+{
+  if (asUnsigned(ip, str) < 0) {
+    return -1;
+  }
+  if (*ip >= (uint64_t) slots[slot].fileLength) {
+    fprintf(stderr, "%s", posPrefix);
+    fprintf(stderr, "address '%s' exceeds bounds of file\n", str);
+    return -1;
+  }
+  return 0;
+}
+
+int asFileLength(uint64_t* ip, const char* str, int slot, uint64_t fileAddr)
+{
+  if (asUnsigned(ip, str) < 0) {
+    return -1;
+  }
+  if (fileAddr + *ip >= (uint64_t) slots[slot].fileLength) {
+    fprintf(stderr, "%s", posPrefix);
+    fprintf(stderr, "length '%s' exceeds bounds of file\n", str);
+    return -1;
+  }
+  return 0;
+}
+
+int asAddr(uint64_t* ip, const char* str, int slot)
+{
+  if (asUnsigned(ip, str) < 0) {
+    return -1;
+  }
+  if (*ip >= (uint64_t) slots[slot].length) {
+    fprintf(stderr, "%s", posPrefix);
+    fprintf(stderr, "address '%s' out of range 0x%" PRIx64 "..0x%" PRIx64 "\n",
+            str, slots[slot].addr, slots[slot].addr + slots[slot].length-1);
+    return -1;
+  }
+  return 0;
+}
+
+int asLength(uint64_t* ip, const char* str, int slot, uint64_t addr)
+{
+  if (asUnsigned(ip, str) < 0) {
+    return -1;
+  }
+  if (addr + *ip >= addr + slots[slot].length) {
+    fprintf(stderr, "%s", posPrefix);
+    fprintf(stderr, "address '%s' out of range 0..0x%" PRIx64 "\n",
+            str, slots[slot].length-1);
+    return -1;
+  }
+  return 0;
+}
+
+int asWordLen(int *ip, const char* str)
+{
+  uint64_t value;
+  if (asUnsigned(&value, str) < 0) {
+    return -1;
+  }
+  if (value != 1 && value != 2 && value != 4 && value != 8) {
+    fprintf(stderr, "%s", posPrefix);
+    fprintf(stderr, "length '%s' must be 1, 2, 4 or 8\n", str);
+    return -1;
+  }
+  *ip = value;
+  return 0;
+}
+
+int asSlot(int *ip, const char* str, int wantOpen)
+{
+  uint64_t value;
+  if (asUnsigned(&value, str) < 0) {
+    return -1;
+  }
+  if (value >= MAXSLOTS) {
+    fprintf(stderr, "%s", posPrefix);
+    fprintf(stderr, "slot '%s' is out of range 0-%d or is not open\n",
+            str, MAXSLOTS-1);
+    return -1;
+  }
+  if (wantOpen && slots[value].map == NULL) {
+    fprintf(stderr, "%s", posPrefix);
+    fprintf(stderr, "slot '%s' is not open\n", str);
+    return -1;
+  } else if (!wantOpen && slots[value].map != NULL) {
+    fprintf(stderr, "%s", posPrefix);
+    fprintf(stderr, "slot '%s' is already open\n", str);
+    return -1;
+  }
+  *ip = value;
+  return 0;
+}
+
+int do_open(char* path, int slot, uint64_t fileAddr, uint64_t length)
+{
+  int fd = open(path, O_RDWR);
+  if (fd < 0) {
+    perror(path);
+    return -1;
+  }
+
+  struct stat stats;
+  if (fstat(fd, &stats) < 0) {
+    perror("fstat");
+    close(fd);
+    return -1;
+  }
+  uint64_t fileLength = stats.st_size;
+
+  if (fileAddr + length > fileLength) {
+    fprintf(stderr, "mapped range (0x%" PRIx64 ",0x%" PRIx64 ") "
+                      "is outside of size of file (0x%" PRIx64 ")\n",
+            fileAddr, length, fileLength);
+    close(fd);
+    return -1;
+  }
+
+  void* mm = mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_SHARED, fd, fileAddr);
+  if (mm == NULL) {
+    perror("mmap");
+    close(fd);
+    return -1;
+  }
+  strncpy(slots[slot].path, path, sizeof slots[slot].path);
+  slots[slot].fd = fd;
+  slots[slot].fileLength = fileLength;
+  slots[slot].map = mm;
+  slots[slot].addr = fileAddr;
+  slots[slot].length = length;
+  return 0;
+}
+
+void do_close(int slot) {
+  munmap(slots[slot].map, slots[slot].length);
+  close(slots[slot].fd);
+  memset(&slots[slot], 0, sizeof (slots[slot]));
+  slots[slot].fd = -1;
+}
+
+int do_read_helper(int slot, uint64_t addr, int wordlen, uint64_t* valueP)
+{
+  void* vp = slots[slot].map + addr;
+  unsigned char uc;
+  unsigned short us;
+  unsigned int ui;
+  unsigned long long ull;
+  void *dp;
+
+  switch (wordlen) {
+  case sizeof(ull):     dp = &ull; break;
+  case sizeof(ui):      dp = &ui; break;
+  case sizeof(us):      dp = &us; break;
+  case sizeof(uc):      dp = &uc; break;
+  default:
+        fprintf(stderr, "Can't find datatype for wordlen '%d'\n", wordlen);
+        return -1;
+  }
+
+  memcpy(dp, vp, wordlen);
+
+  switch (wordlen) {
+  case sizeof(ull):     *valueP = ull; break;
+  case sizeof(ui):      *valueP = ui; break;
+  case sizeof(us):      *valueP = us; break;
+  case sizeof(uc):      *valueP = uc; break;
+  default:
+        fprintf(stderr, "Can't find datatype for wordlen '%d'\n", wordlen);
+        return -1;
+  }
+  return 0;
+}
+
+int do_read(int slot, uint64_t addr, int wordlen)
+{
+  uint64_t value;
+
+  if (do_read_helper(slot, addr, wordlen, &value) < 0) {
+    return -1;
+  }
+  printf("0x%0*" PRIx64 "\n", 2*wordlen, value);
+  return 0;
+}
+
+void do_write(int slot, uint64_t addr, int wordlen, uint64_t value)
+{
+  void* vp = slots[slot].map + addr;
+  memcpy(vp, &value, wordlen);
+}
+
+int do_hexdump(int slot, uint64_t addr, int wordlen, uint64_t count)
+{
+  uint64_t perline = 16/wordlen;
+  int err = 0;
+
+  for (uint64_t i = 0; i < count; i += perline) {
+    printf("%08" PRIx64 ":", addr + i * wordlen);
+    for (uint64_t j = 0; j < perline; j++) {
+      if (i + j >= count) {
+        break;
+      }
+      uint64_t value = 0;
+      if (do_read_helper(slot, addr + (i + j) * wordlen, wordlen, &value) < 0) {
+        err = -1;
+      }
+      printf(" 0x%0*" PRIx64, 2*wordlen, value);
+    }
+    printf("\n");
+  }
+  return err;
+}
+
+int cmd_open(int ac, char* av[])
+{
+  char* usage = "open slot file offset length";
+
+  if (ac > 1 && strcmp(av[1], "help") == 0) {
+    printf("\t%s\n", usage);
+    return 0;
+  }
+  if (ac != 5) {
+    fprintf(stderr, "Usage: %s\n", usage);
+    return -1;
+  }
+
+  int slot;
+  char* path = av[2];
+  uint64_t offset;
+  uint64_t len;
+
+  if (asSlot(&slot, av[1], 0) < 0 ||
+      asUnsigned(&offset, av[3]) < 0 ||
+      asUnsigned(&len, av[4]) < 0) {
+    return -1;
+  }
+  if (do_open(path, slot, offset, len) < 0) {
+    return -1;
+  }
+  return 0;
+}
+
+int cmd_close(int ac, char* av[])
+{
+  char* usage = "close slot";
+
+  if (ac > 1 && strcmp(av[1], "help") == 0) {
+    printf("\t%s\n", usage);
+    return 0;
+  }
+  if (ac != 2) {
+    fprintf(stderr, "Usage: %s\n", usage);
+    return -1;
+  }
+
+  int slot;
+
+  if (asSlot(&slot, av[1], 1) < 0) {
+    return -1;
+  }
+  do_close(slot);
+  return 0;
+}
+
+int cmd_read(int ac, char* av[])
+{
+  char* usage = "read slot addr wordlen";
+
+  if (ac > 1 && strcmp(av[1], "help") == 0) {
+    printf("\t%s\n", usage);
+    return 0;
+  }
+  if (ac != 4) {
+    fprintf(stderr, "Usage: %s\n", usage);
+    return -1;
+  }
+
+  int slot;
+  uint64_t addr;
+  int wordlen;
+
+  if (asSlot(&slot, av[1], 1) < 0 ||
+      asAddr(&addr, av[2], slot) < 0 ||
+      asWordLen(&wordlen, av[3]) < 0) {
+    return -1;
+  }
+  if (do_read(slot, addr, wordlen) < 0) {
+    return -1;
+  }
+  return 0;
+}
+
+int cmd_write(int ac, char* av[])
+{
+  char* usage = "write slot addr wordlen value";
+
+  if (ac > 1 && strcmp(av[1], "help") == 0) {
+    printf("\t%s\n", usage);
+    return 0;
+  }
+  if (ac != 5) {
+    fprintf(stderr, "Usage: %s\n", usage);
+    return -1;
+  }
+
+  int slot;
+  uint64_t addr;
+  int wordlen;
+  uint64_t value;
+
+  if (asSlot(&slot, av[1], 1) < 0 ||
+      asAddr(&addr, av[2], slot) < 0 ||
+      asWordLen(&wordlen, av[3]) < 0 ||
+      asUnsigned(&value, av[4]) < 0) {
+    return -1;
+  }
+  do_write(slot, addr, wordlen, value);
+  return 0;
+}
+
+int cmd_dump(int ac, char* av[])
+{
+  char* usage = "dump slot addr wordlen count";
+
+  if (ac > 1 && strcmp(av[1], "help") == 0) {
+    printf("\t%s\n", usage);
+    return 0;
+  }
+  if (ac != 5) {
+    fprintf(stderr, "Usage: %s\n", usage);
+    return -1;
+  }
+
+  int slot;
+  uint64_t addr;
+  int wordlen;
+  uint64_t count;
+
+  if (asSlot(&slot, av[1], 1) < 0 ||
+      asUnsigned(&addr, av[2]) < 0 ||
+      asWordLen(&wordlen, av[3]) < 0 ||
+      asUnsigned(&count, av[4]) < 0) {
+    return -1;
+  }
+  if (do_hexdump(slot, addr, wordlen, count) < 0) {
+    return -1;
+  }
+  return 0;
+}
+
+int cmd_msleep(int ac, char* av[])
+{
+  char* usage = "msleep msecs";
+
+  if (ac > 1 && strcmp(av[1], "help") == 0) {
+    printf("\t%s\n", usage);
+    return 0;
+  }
+  if (ac != 2) {
+    fprintf(stderr, "Usage: %s\n", usage);
+    return -1;
+  }
+
+  uint64_t msecs;
+
+  if (asUnsigned(&msecs, av[1]) < 0) {
+    return -1;
+  }
+  usleep(msecs*1000);
+  return 0;
+}
+
+typedef int (*cmdFunc)(int ac, char* av[]);
+
+struct commands {
+  char*  name;
+  cmdFunc  func;
+};
+
+struct commands cmds[] = {
+  { "open", cmd_open, },
+  { "close", cmd_close, },
+  { "read", cmd_read, },
+  { "write", cmd_write, },
+  { "dump", cmd_dump, },
+  { "msleep", cmd_msleep, },
+  { NULL, NULL },
+};
+
+int main(int argc, char* argv[])
+{
+  char* fpName = "stdin";
+  FILE* fp = stdin;
+  char* delim= " \t\n";
+
+  setbuf(stdout, NULL);         // unbuffered
+
+  if (argc > 1) {
+    fpName = argv[1];
+    fp = fopen(fpName, "r");
+    if (fp == NULL) {
+      perror(fpName);
+      usage(argv[0]);
+      exit(1);
+    }
+  }
+  int isTTY = isatty(fileno(fp));
+
+  int lineno = 0;
+  int err = 0;
+  for (;;) {
+    char line[1024] = { 0 };
+    if (isTTY) {
+      printf("mmap>> ");
+    }
+    if (fgets(line, sizeof line, fp) == NULL) {
+      break;    // EOF
+    }
+
+    lineno++;
+    char fileLine[128];
+    sprintf(fileLine, "%s:%d: ", fpName, lineno);
+    posPrefix = fileLine;
+
+    int len = strlen(line);
+    if (len == 0) {
+      continue;
+    }
+    if (line[len-1] != '\n') {
+      fprintf(stderr, "%s", posPrefix);
+      fprintf(stderr, "line too long\n");
+      err++;
+      continue;
+    }
+    if (line[0] == '#') {
+      continue;    // skip comments
+    }
+
+    printf("# %s", line);
+
+    int ac = 0;
+    char* av[MAXARGS];
+    char** avP = av;
+
+    *avP = strtok(line, delim);
+    while (*avP != NULL) {
+      if (ac >= MAXARGS) {
+        break;
+      }
+      ac++;
+      avP++;
+      *avP = strtok(NULL, delim);
+    }
+    if (ac > MAXARGS) {
+      fprintf(stderr, "%s", posPrefix);
+      fprintf(stderr, "too many arguments\n");
+      err++;
+      continue;
+    }
+    if (ac == 0) {
+      continue;  // skip whitespace only
+    }
+
+    if (strcmp(av[0], "help") == 0) {
+      char* args[] = { "", "help" };
+      for (struct commands* cp = cmds; cp->func != NULL; cp++) {
+        cp->func(2, args);
+      }
+      continue;
+    }
+
+    int match = 0;
+    for (struct commands* cp = cmds; cp->func != NULL; cp++) {
+      if (strcmp(av[0], cp->name) == 0) {
+        if (cp->func(ac, av) < 0) {
+          fprintf(stderr, "%s", posPrefix);
+          fprintf(stderr, "command '%s' failed\n", av[0]);
+          err++;
+        }
+        match = 1;
+        break;
+      }
+    }
+    if (!match) {
+      fprintf(stderr, "%s", posPrefix);
+      fprintf(stderr, "unknown command '%s', try help\n", av[0]);
+      err++;
+      continue;
+    }
+  }
+  exit(err != 0);
+}
diff --git a/cmds/test-mmap.sh b/cmds/test-mmap.sh
new file mode 100755
index 0000000..15aabd2
--- /dev/null
+++ b/cmds/test-mmap.sh
@@ -0,0 +1,48 @@
+exit=0
+run=.run.$$
+for n in tests.mmap/*.test; do
+  rm -rf $run
+  mkdir $run
+  : > $run/LOG
+  (
+    cd $run
+    . ../$n
+    echo -n "$INPUT" > INPUT
+    echo -n "$OUTPUT" > WANT
+    for m in $FILES; do
+      eval IN="\$${m}_1"
+      eval OUT="\$${m}_2"
+      echo -n "$IN" > $m
+      echo -n "$OUT" > $m.expected
+    done
+    $PREFIX ../host-mmap < INPUT >& GOT
+    status=$?
+    if [ -n "$PREFIX" ]; then
+      sleep .5      # script mysteriously delays output
+    fi
+    if [ "$status" != "$EXIT" ]; then
+      echo "exit code: expected '$EXIT', got '$status'"
+    fi
+    if ! cmp WANT GOT; then
+      echo "output differs from expected:"
+      diff -u WANT GOT
+    fi
+    for m in $FILES; do
+      if ! cmp $m $m.expected; then
+        echo "mapped file result differs from expected:"
+        diff -u $m $m.expected
+      fi
+    done
+  ) >& $run/LOG
+  if [ ! -s $run/LOG ]; then
+    echo PASS $n
+  else
+    echo ======================================================================
+    echo FAIL $n
+    cat $run/LOG
+    echo ======================================================================
+    exit=1
+  fi
+done
+rm -rf $run
+exit $exit
diff --git a/cmds/tests.mmap/close_bad_slot.test b/cmds/tests.mmap/close_bad_slot.test
new file mode 100644
index 0000000..4914a5d
--- /dev/null
+++ b/cmds/tests.mmap/close_bad_slot.test
@@ -0,0 +1,16 @@
+EXIT=1
+
+FILES="A B"
+
+A_1='0123456789012345678901234567890123456789'
+A_2='0123456789012345678901234567890123456789'
+
+INPUT='
+close 0
+'
+
+OUTPUT="# 
+# close 0
+stdin:2: slot '0' is not open
+stdin:2: command 'close' failed
+"
diff --git a/cmds/tests.mmap/close_twice.test b/cmds/tests.mmap/close_twice.test
new file mode 100644
index 0000000..29d424a
--- /dev/null
+++ b/cmds/tests.mmap/close_twice.test
@@ -0,0 +1,20 @@
+EXIT=1
+
+FILES="A B"
+
+A_1='0123456789012345678901234567890123456789'
+A_2='0123456789012345678901234567890123456789'
+
+INPUT='
+open 0 A 0 40
+close 0
+close 0
+'
+
+OUTPUT="# 
+# open 0 A 0 40
+# close 0
+# close 0
+stdin:4: slot '0' is not open
+stdin:4: command 'close' failed
+"
diff --git a/cmds/tests.mmap/dump-unaligned.test b/cmds/tests.mmap/dump-unaligned.test
new file mode 100644
index 0000000..f2a8560
--- /dev/null
+++ b/cmds/tests.mmap/dump-unaligned.test
@@ -0,0 +1,36 @@
+EXIT=0
+
+FILES="A B"
+
+A_1='x0123456789012345678901234567890123456789'
+A_2='x0123456789012345678901234567890123456789'
+
+INPUT='
+open 0 A 0 40
+dump 0 1 1 40
+dump 0 1 2 20
+dump 0 1 4 10
+dump 0 1 8 5
+close 0
+'
+
+OUTPUT="# 
+# open 0 A 0 40
+# dump 0 1 1 40
+00000001: 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x30 0x31 0x32 0x33 0x34 0x35
+00000011: 0x36 0x37 0x38 0x39 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x30 0x31
+00000021: 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39
+# dump 0 1 2 20
+00000001: 0x3130 0x3332 0x3534 0x3736 0x3938 0x3130 0x3332 0x3534
+00000011: 0x3736 0x3938 0x3130 0x3332 0x3534 0x3736 0x3938 0x3130
+00000021: 0x3332 0x3534 0x3736 0x3938
+# dump 0 1 4 10
+00000001: 0x33323130 0x37363534 0x31303938 0x35343332
+00000011: 0x39383736 0x33323130 0x37363534 0x31303938
+00000021: 0x35343332 0x39383736
+# dump 0 1 8 5
+00000001: 0x3736353433323130 0x3534333231303938
+00000011: 0x3332313039383736 0x3130393837363534
+00000021: 0x3938373635343332
+# close 0
+"
diff --git a/cmds/tests.mmap/dump.test b/cmds/tests.mmap/dump.test
new file mode 100644
index 0000000..c209096
--- /dev/null
+++ b/cmds/tests.mmap/dump.test
@@ -0,0 +1,31 @@
+EXIT=0
+
+FILES="A B"
+
+A_1='0123456789012345678901234567890123456789'
+A_2='0123456789012345678901234567890123456789'
+
+INPUT='
+open 0 A 0 40
+dump 0 0 1 40
+dump 0 0 2 20
+dump 0 0 4 10
+close 0
+'
+
+OUTPUT="# 
+# open 0 A 0 40
+# dump 0 0 1 40
+00000000: 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x30 0x31 0x32 0x33 0x34 0x35
+00000010: 0x36 0x37 0x38 0x39 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x30 0x31
+00000020: 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39
+# dump 0 0 2 20
+00000000: 0x3130 0x3332 0x3534 0x3736 0x3938 0x3130 0x3332 0x3534
+00000010: 0x3736 0x3938 0x3130 0x3332 0x3534 0x3736 0x3938 0x3130
+00000020: 0x3332 0x3534 0x3736 0x3938
+# dump 0 0 4 10
+00000000: 0x33323130 0x37363534 0x31303938 0x35343332
+00000010: 0x39383736 0x33323130 0x37363534 0x31303938
+00000020: 0x35343332 0x39383736
+# close 0
+"
diff --git a/cmds/tests.mmap/help.test b/cmds/tests.mmap/help.test
new file mode 100644
index 0000000..2db1443
--- /dev/null
+++ b/cmds/tests.mmap/help.test
@@ -0,0 +1,33 @@
+EXIT=0
+
+INPUT='
+help
+open help
+close help
+read help
+write help
+dump help
+msleep help
+'
+
+OUTPUT='# 
+# help
+	open slot file offset length
+	close slot
+	read slot addr wordlen
+	write slot addr wordlen value
+	dump slot addr wordlen count
+	msleep msecs
+# open help
+	open slot file offset length
+# close help
+	close slot
+# read help
+	read slot addr wordlen
+# write help
+	write slot addr wordlen value
+# dump help
+	dump slot addr wordlen count
+# msleep help
+	msleep msecs
+'
diff --git a/cmds/tests.mmap/interactive.test b/cmds/tests.mmap/interactive.test
new file mode 100644
index 0000000..b50d751
--- /dev/null
+++ b/cmds/tests.mmap/interactive.test
@@ -0,0 +1,20 @@
+PREFIX="script -e -f -q -c"
+EXIT=0
+
+FILES="A B"
+
+A_1='0123456789012345678901234567890123456789'
+A_2='0123456789A12345678901234567890123456789'
+
+INPUT='
+open 0 A 0 40
+write 0 10 1 0x41
+close 0
+'
+
+OUTPUT=\
+"mmap>> # 
+mmap>> # open 0 A 0 40
+mmap>> # write 0 10 1 0x41
+mmap>> # close 0
+mmap>> "
diff --git a/cmds/tests.mmap/map_fail.test b/cmds/tests.mmap/map_fail.test
new file mode 100644
index 0000000..afb04de
--- /dev/null
+++ b/cmds/tests.mmap/map_fail.test
@@ -0,0 +1,20 @@
+EXIT=1
+
+FILES="A B"
+
+A_1='0123456789012345678901234567890123456789'
+A_2='0123456789012345678901234567890123456789'
+
+INPUT='
+open 0 A 0 41
+close 0
+'
+
+OUTPUT="# 
+# open 0 A 0 41
+mapped range (0x0,0x29) is outside of size of file (0x28)
+stdin:2: command 'open' failed
+# close 0
+stdin:3: slot '0' is not open
+stdin:3: command 'close' failed
+"
diff --git a/cmds/tests.mmap/missing_file.test b/cmds/tests.mmap/missing_file.test
new file mode 100644
index 0000000..c3136a0
--- /dev/null
+++ b/cmds/tests.mmap/missing_file.test
@@ -0,0 +1,16 @@
+EXIT=1
+
+FILES="A B"
+
+A_1='0123456789012345678901234567890123456789'
+A_2='0123456789012345678901234567890123456789'
+
+INPUT='
+open 0 this-file-does-not-exist 0 40
+'
+
+OUTPUT="# 
+# open 0 this-file-does-not-exist 0 40
+this-file-does-not-exist: No such file or directory
+stdin:2: command 'open' failed
+"
diff --git a/cmds/tests.mmap/msleep.test b/cmds/tests.mmap/msleep.test
new file mode 100644
index 0000000..569107e
--- /dev/null
+++ b/cmds/tests.mmap/msleep.test
@@ -0,0 +1,14 @@
+EXIT=0
+
+FILES="A B"
+
+A_1='0123456789012345678901234567890123456789'
+A_2='0123456789012345678901234567890123456789'
+
+INPUT='
+msleep 1000
+'
+
+OUTPUT="# 
+# msleep 1000
+"
diff --git a/cmds/tests.mmap/open_close.test b/cmds/tests.mmap/open_close.test
new file mode 100644
index 0000000..31f184d
--- /dev/null
+++ b/cmds/tests.mmap/open_close.test
@@ -0,0 +1,20 @@
+EXIT=0
+
+FILES="A B"
+
+A_1='0123456789012345678901234567890123456789'
+A_2='0123456789012345678901234567890123456789'
+
+INPUT='
+open 0 A 0 40
+close 0
+open 1 A 0 40
+close 1
+'
+
+OUTPUT='# 
+# open 0 A 0 40
+# close 0
+# open 1 A 0 40
+# close 1
+'
diff --git a/cmds/tests.mmap/open_twice.test b/cmds/tests.mmap/open_twice.test
new file mode 100644
index 0000000..34d23fe
--- /dev/null
+++ b/cmds/tests.mmap/open_twice.test
@@ -0,0 +1,18 @@
+EXIT=1
+
+FILES="A B"
+
+A_1='0123456789012345678901234567890123456789'
+A_2='0123456789012345678901234567890123456789'
+
+INPUT='
+open 0 A 0 40
+open 0 A 0 40
+'
+
+OUTPUT="# 
+# open 0 A 0 40
+# open 0 A 0 40
+stdin:3: slot '0' is already open
+stdin:3: command 'open' failed
+"
diff --git a/cmds/tests.mmap/read-unaligned.test b/cmds/tests.mmap/read-unaligned.test
new file mode 100644
index 0000000..6940acc
--- /dev/null
+++ b/cmds/tests.mmap/read-unaligned.test
@@ -0,0 +1,28 @@
+EXIT=0
+
+FILES="A B"
+
+A_1='x012345678901234567890123456789012345678'
+A_2='x012345678901234567890123456789012345678'
+
+INPUT='
+open 0 A 0 40
+read 0 1 1
+read 0 1 2
+read 0 1 4
+read 0 1 8
+close 0
+'
+
+OUTPUT="# 
+# open 0 A 0 40
+# read 0 1 1
+0x30
+# read 0 1 2
+0x3130
+# read 0 1 4
+0x33323130
+# read 0 1 8
+0x3736353433323130
+# close 0
+"
diff --git a/cmds/tests.mmap/read.test b/cmds/tests.mmap/read.test
new file mode 100644
index 0000000..7db9eac
--- /dev/null
+++ b/cmds/tests.mmap/read.test
@@ -0,0 +1,28 @@
+EXIT=0
+
+FILES="A B"
+
+A_1='0123456789012345678901234567890123456789'
+A_2='0123456789012345678901234567890123456789'
+
+INPUT='
+open 0 A 0 40
+read 0 0 1
+read 0 0 2
+read 0 0 4
+read 0 0 8
+close 0
+'
+
+OUTPUT="# 
+# open 0 A 0 40
+# read 0 0 1
+0x30
+# read 0 0 2
+0x3130
+# read 0 0 4
+0x33323130
+# read 0 0 8
+0x3736353433323130
+# close 0
+"
diff --git a/cmds/tests.mmap/signed.test b/cmds/tests.mmap/signed.test
new file mode 100644
index 0000000..0ba44aa
--- /dev/null
+++ b/cmds/tests.mmap/signed.test
@@ -0,0 +1,20 @@
+EXIT=1
+
+FILES="A B"
+
+A_1='0123456789012345678901234567890123456789'
+A_2='0123456789012345678901234567890123456789'
+
+INPUT='
+open 0 A 0 -1
+close 0
+'
+
+OUTPUT="# 
+# open 0 A 0 -1
+mapped range (0x0,0xffffffffffffffff) is outside of size of file (0x28)
+stdin:2: command 'open' failed
+# close 0
+stdin:3: slot '0' is not open
+stdin:3: command 'close' failed
+"
diff --git a/cmds/tests.mmap/wordlen.test b/cmds/tests.mmap/wordlen.test
new file mode 100644
index 0000000..d6b08bd
--- /dev/null
+++ b/cmds/tests.mmap/wordlen.test
@@ -0,0 +1,18 @@
+EXIT=1
+
+FILES="A B"
+
+A_1='0123456789012345678901234567890123456789'
+A_2='0123456789012345678901234567890123456789'
+
+INPUT='
+open 0 A 0 40
+write 0 0 0 0x41
+'
+
+OUTPUT="# 
+# open 0 A 0 40
+# write 0 0 0 0x41
+stdin:3: length '0' must be 1, 2, 4 or 8
+stdin:3: command 'write' failed
+"
diff --git a/cmds/tests.mmap/write-unaligned.test b/cmds/tests.mmap/write-unaligned.test
new file mode 100644
index 0000000..0652094
--- /dev/null
+++ b/cmds/tests.mmap/write-unaligned.test
@@ -0,0 +1,24 @@
+EXIT=0
+
+FILES="A B"
+
+A_1='x012345678901234567890123456789012345678'
+A_2='xA1CB4567GFED2345ONMLKJIH456789012345678'
+
+INPUT='
+open 0 A 0 40
+write 0 1 1 0x41
+write 0 3 2 0x4243
+write 0 9 4 0x44454647
+write 0 17 8 0x48494a4b4c4d4e4f
+close 0
+'
+
+OUTPUT="# 
+# open 0 A 0 40
+# write 0 1 1 0x41
+# write 0 3 2 0x4243
+# write 0 9 4 0x44454647
+# write 0 17 8 0x48494a4b4c4d4e4f
+# close 0
+"
diff --git a/cmds/tests.mmap/write.test b/cmds/tests.mmap/write.test
new file mode 100644
index 0000000..eb64c62
--- /dev/null
+++ b/cmds/tests.mmap/write.test
@@ -0,0 +1,24 @@
+EXIT=0
+
+FILES="A B"
+
+A_1='0123456789012345678901234567890123456789'
+A_2='A1CB4567GFED2345ONMLKJIH4567890123456789'
+
+INPUT='
+open 0 A 0 40
+write 0 0 1 0x41
+write 0 2 2 0x4243
+write 0 8 4 0x44454647
+write 0 16 8 0x48494a4b4c4d4e4f
+close 0
+'
+
+OUTPUT="# 
+# open 0 A 0 40
+# write 0 0 1 0x41
+# write 0 2 2 0x4243
+# write 0 8 4 0x44454647
+# write 0 16 8 0x48494a4b4c4d4e4f
+# close 0
+"
diff --git a/cmds/tests.mmap/write_fail.test b/cmds/tests.mmap/write_fail.test
new file mode 100644
index 0000000..ebd559a
--- /dev/null
+++ b/cmds/tests.mmap/write_fail.test
@@ -0,0 +1,20 @@
+EXIT=1
+
+FILES="A B"
+
+A_1='0123456789012345678901234567890123456789'
+A_2='0123456789012345678901234567890123456789'
+
+INPUT='
+open 0 A 0 40
+write 0 999 1 0x41
+close 0
+'
+
+OUTPUT="# 
+# open 0 A 0 40
+# write 0 999 1 0x41
+stdin:3: address '999' out of range 0x0..0x27
+stdin:3: command 'write' failed
+# close 0
+"