 .gitignore               |    4 +-
 LICENSE                  |   10 +-
 README                   |   28 ++---
 UPGRADING                |    8 +-
 bindings/python/setup.py |    2 +-
 doc/assumptions          |   13 ++
 libjio/Makefile          |  107 ++++++++++-------
 libjio/common.c          |    2 +-
 libjio/common.h          |    2 +-
 libjio/journal.c         |   35 ++----
 libjio/libjio.h          |    4 +-
 libjio/trans.c           |   20 ++--
 tests/README             |   50 ++++++++
 tests/stress/jiostress   |  307 ++++++++++++++++++++++++++++++++++++----------
 14 files changed, 418 insertions(+), 174 deletions(-)

diff --git a/.gitignore b/.gitignore
index 5520f01..3bab0cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,8 @@
 *.so.*
 *.gcda
 *.gcno
+*.gcov
+libjio/build/
 jiofsck
 libjio.pc
 samples/full
@@ -16,6 +18,6 @@ tests/performance/random
 *.pyo
 libjio/doxygen/doc.internal
 libjio/doxygen/doc.public
-libjio/doxygen/Doxygen.base
+libjio/doxygen/Doxyfile.base
 libjio/build-flags
 
diff --git a/LICENSE b/LICENSE
index edb80f0..987d3b9 100644
--- a/LICENSE
+++ b/LICENSE
@@ -5,7 +5,7 @@ using. But I also believe that it's important that people share and give back;
 so I'm placing this work under the following license.
 
 
-BOLA - Buena Onda License Agreement (v1.0)
+BOLA - Buena Onda License Agreement (v1.1)
 ------------------------------------------
 
 This work is provided 'as-is', without any express or implied warranty. In no
@@ -19,12 +19,12 @@ However, if you want to be "buena onda", you should:
 
 1. Not take credit for it, and give proper recognition to the authors.
 2. Share your modifications, so everybody benefits from them.
-4. Do something nice for the authors.
-5. Help someone who needs it: sign up for some volunteer work or help your
+3. Do something nice for the authors.
+4. Help someone who needs it: sign up for some volunteer work or help your
    neighbour paint the house.
-6. Don't waste. Anything, but specially energy that comes from natural
+5. Don't waste. Anything, but specially energy that comes from natural
    non-renewable resources. Extra points if you discover or invent something
    to replace them.
-7. Be tolerant. Everything that's good in nature comes from cooperation.
+6. Be tolerant. Everything that's good in nature comes from cooperation.
 
 
diff --git a/README b/README
index 0413faf..f560f08 100644
--- a/README
+++ b/README
@@ -1,32 +1,26 @@
 
 libjio - A library for Journaled I/O
-Alberto Bertogli (albertito@blitiri.com.ar)
--------------------------------------------
+------------------------------------
 
-As the name says, this is a simple userspace library to do journaled,
-transaction-oriented I/O.
+libjio is a simple userspace library to do journaled, transaction-oriented
+I/O.
 
-It provides a very simple transaction API to commit and rollback transactions,
-and on top of that a unix-alike set of functions to perform most regular
-operations (ie. open, read, write).
+It provides a transaction API to commit and rollback transactions, and on top
+of that a unix-alike set of functions to perform most regular operations (ie.
+open, read, write, etc.).
 
-On the disk, the file you work on is exactly like a regular one, but a
-special directory is created to store in-flight transactions.
+The library guarantees file integrity even after unexpected crashes, never
+leaving your files in an inconsistent state.
 
-This allows both simple file manipulation, recovery and debugging because
-everything is isolated.
-
-There are more detailed documents: a programming guide, a brief introduction
-to the design and inner workings, and the manpage; all in the doc/ directory.
+You can find more detailed documents, including the programmer's guide, in the
+doc/ directory, and the manpage in the libjio/ directory.
 
 
 To see how to install it, please read the INSTALL file.
 
 It is in the public domain, see the LICENSE file for more details.
 
-Comments and patches are always welcome; please send them to my email address,
+Comments and patches are always welcome; please send them to
 albertito@blitiri.com.ar.
 
-Thanks,
-		Alberto
 
diff --git a/UPGRADING b/UPGRADING
index 0db68b4..68d843d 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -2,9 +2,11 @@
 Here you can find a summary of the changes in the library that could require
 an effort from its users, like the ones affecting the API, ABI or semantics.
 
-You should always clean all your files before upgrading. While I don't expect
-the transaction on-disk format to change, it's a good practise and it doesn't
-take much effort. When it's mandatory, it will be noted.
+Normally, nothing should be required when upgrading between two stable
+releases from the same branch.
+
+
+------- 1.00: Stable release
 
 -> 0.90 (On-disk format change, pre 1.0 freeze)
   - The way transactions are stored on disk has changed. It is mandatory that
diff --git a/bindings/python/setup.py b/bindings/python/setup.py
index eea600f..c594e5d 100644
--- a/bindings/python/setup.py
+++ b/bindings/python/setup.py
@@ -20,7 +20,7 @@ libjio = Extension("libjio",
 
 setup(
 	name = 'libjio',
-	version = '0.90',
+	version = '1.00',
 	description = "A library for journaled, transactional I/O",
 	author = "Alberto Bertogli",
 	author_email = "albertito@blitiri.com.ar",
diff --git a/doc/assumptions b/doc/assumptions
new file mode 100644
index 0000000..e7a0862
--- /dev/null
+++ b/doc/assumptions
@@ -0,0 +1,13 @@
+
+The following list documents the assumptions the library makes. All of them
+should be well within normal behaviour for decent filesystems.
+
+ - fcntl() locking works.
+ - fsync(), fdatasync() and sync_file_range(WAIT_AFTER) return after the data
+   has been stored on stable storage, like they're supposed to.
+ - If a crash occurs while a write is in progress, the data within the
+   relevant range may contain garbage, but the data outside the range will not
+   be touched. Failure in doing so is normally considered corruption and
+   covering from it is outside the scope of the library.
+
+
diff --git a/libjio/Makefile b/libjio/Makefile
index 2e25ca3..a32828f 100644
--- a/libjio/Makefile
+++ b/libjio/Makefile
@@ -1,4 +1,7 @@
 
+# output directory, will be created if it doesn't exist
+O = build
+
 CFLAGS = -std=c99 -pedantic -Wall -O3
 
 MANDATORY_CFLAGS := \
@@ -10,11 +13,16 @@ MANDATORY_LDFLAGS := $(shell getconf LFS_LIBS 2>/dev/null)
 ALL_CFLAGS += $(CFLAGS) $(MANDATORY_CFLAGS) -fPIC
 ALL_LDFLAGS += $(LDFLAGS) $(MANDATORY_LDFLAGS) -fPIC
 
+
 # some platforms do not have librt, we only use it if available
 NEED_LIBRT := $(shell ld -o rtcheck.so -shared -lrt 2>/dev/null && echo -lrt; \
 	rm -f rtcheck.so)
+
 LIBS = -lpthread $(NEED_LIBRT)
 
+
+# shorthands for common build variants
+
 ifdef DEBUG
 ALL_CFLAGS += -g
 ALL_LDFLAGS += -g
@@ -30,6 +38,7 @@ ALL_CFLAGS += -DFIU_ENABLE=1
 LIBS += -lfiu
 endif
 
+
 # prefix for installing the binaries
 PREFIX = /usr/local
 
@@ -39,87 +48,99 @@ DESTDIR=$(PREFIX)
 # install utility, we assume it's GNU/BSD compatible
 INSTALL=install
 
+
+# nicer output
 ifneq ($(V), 1)
-        NICE_CC = @echo "  CC  $@"; $(CC)
-        NICE_AR = @echo "  AR  $@"; $(AR)
+        N_CC = @echo "  CC  $(subst $O/,,$@)"; $(CC)
+        N_AR = @echo "  AR  $(subst $O/,,$@)"; $(AR)
 else
-        NICE_CC = $(CC)
-        NICE_AR = $(AR)
+        N_CC = $(CC)
+        N_AR = $(AR)
 endif
 
 
-LIB_VER=0.90
-LIB_SO_VER=0
+# library version, used for soname and generated documentation
+LIB_VER=1.00
+LIB_SO_VER=1
+
+
+OBJS = $(addprefix $O/,autosync.o checksum.o common.o compat.o trans.o \
+               check.o journal.o unix.o ansi.o)
 
 
-# objects to build
-OBJS = autosync.o checksum.o common.o compat.o trans.o check.o journal.o \
-       unix.o ansi.o
+# targets
 
-# rules
 default: all
 
-all: libjio.so libjio.a libjio.pc jiofsck
+all: $O/libjio.so $O/libjio.a $O/libjio.pc $O/jiofsck
 
-libjio.so: build-flags $(OBJS)
-	$(NICE_CC) -shared $(ALL_LDFLAGS) \
+# used to rebuild everything when the build flags have changed
+BF = $(CC) ~ $(ALL_CFLAGS) ~ $(PREFIX)
+$O/build-flags: .force-build-flags
+	@mkdir -p $O
+	@if [ x"$(BF)" != x"`cat $O/build-flags 2>/dev/null`" ]; then \
+		if [ -f $O/build-flags ]; then \
+			echo "build flags changed, rebuilding"; \
+		fi; \
+		echo "$(BF)" > $O/build-flags; \
+	fi
+
+$(OBJS): $O/build-flags
+
+$O/%.o: %.c
+	@mkdir -p $O
+	$(N_CC) $(ALL_CFLAGS) -MMD -MF $@.mak -MP -MT $@ -c $< -o $@
+
+sinclude $(OBJS:.o=.o.mak)
+
+$O/libjio.so: $O/build-flags $(OBJS)
+	$(N_CC) -shared $(ALL_LDFLAGS) \
 		-Wl,-soname,libjio.so.$(LIB_SO_VER) \
-		$(LIBS) $(OBJS) -o libjio.so.$(LIB_VER)
-	ln -fs libjio.so.$(LIB_VER) libjio.so
+		$(LIBS) $(OBJS) -o $O/libjio.so.$(LIB_VER)
+	@echo "  LN  libjio.so.$(LIB_VER)"
+	@ln -fs libjio.so.$(LIB_VER) $O/libjio.so
 
-libjio.a: build-flags $(OBJS)
-	$(NICE_AR) cr libjio.a $(OBJS)
+$O/libjio.a: $O/build-flags $(OBJS)
+	$(N_AR) cr $@ $(OBJS)
 
-libjio.pc: build-flags libjio.pc.in
-	@echo "generating libjio.pc"
+$O/libjio.pc: $O/build-flags libjio.pc.in
+	@echo "  GEN libjio.pc"
 	@cat libjio.pc.in | \
 		sed 's@++PREFIX++@$(DESTDIR)@g' | \
 		sed 's@++VERSION++@$(LIB_VER)@g' | \
 		sed 's@++CFLAGS++@$(MANDATORY_CFLAGS)@g' \
-		> libjio.pc
+		> $O/libjio.pc
 
-jiofsck: build-flags jiofsck.o libjio.a
-	$(NICE_CC) $(ALL_LDFLAGS) jiofsck.o libjio.a $(LIBS) -o jiofsck
+$O/jiofsck: $O/build-flags $O/jiofsck.o $O/libjio.a
+	$(N_CC) $(ALL_LDFLAGS) $O/jiofsck.o $O/libjio.a $(LIBS) -o $@
 
 install: all
 	$(INSTALL) -d $(PREFIX)/lib
-	$(INSTALL) -m 0755 libjio.so.$(LIB_VER) $(PREFIX)/lib
+	$(INSTALL) -m 0755 $O/libjio.so.$(LIB_VER) $(PREFIX)/lib
 	ln -fs libjio.so.$(LIB_VER) $(PREFIX)/lib/libjio.so
 	ln -fs libjio.so.$(LIB_VER) $(PREFIX)/lib/libjio.so.$(LIB_SO_VER)
-	$(INSTALL) -m 0644 libjio.a $(PREFIX)/lib
+	$(INSTALL) -m 0644 $O/libjio.a $(PREFIX)/lib
 	$(INSTALL) -d $(PREFIX)/include
 	$(INSTALL) -m 0644 libjio.h $(PREFIX)/include
 	$(INSTALL) -d $(PREFIX)/lib/pkgconfig
-	$(INSTALL) -m 644 libjio.pc $(PREFIX)/lib/pkgconfig
+	$(INSTALL) -m 644 $O/libjio.pc $(PREFIX)/lib/pkgconfig
 	$(INSTALL) -d $(PREFIX)/bin
-	$(INSTALL) -m 0775 jiofsck $(PREFIX)/bin
+	$(INSTALL) -m 0775 $O/jiofsck $(PREFIX)/bin
 	$(INSTALL) -d $(PREFIX)/share/man/man3
 	$(INSTALL) -m 0644 libjio.3 $(PREFIX)/share/man/man3/
 	@echo
 	@echo "Please run ldconfig to update your library cache"
 	@echo
 
-BF = $(ALL_CFLAGS) ~ $(PREFIX)
-build-flags: .force-build-flags
-	@if [ x"$(BF)" != x"`cat build-flags 2>/dev/null`" ]; then \
-		if [ -f build-flags ]; then \
-			echo "build flags changed, rebuilding"; \
-		fi; \
-		echo "$(BF)" > build-flags; \
-	fi
-
-$(OBJS): build-flags
-
-.c.o:
-	$(NICE_CC) $(ALL_CFLAGS) -c $< -o $@
-
 doxygen:
 	$(MAKE) LIB_VER=$(LIB_VER) -C doxygen
 
 clean:
-	rm -f libjio.a libjio.so libjio.so.$(LIB_VER) libjio.pc
-	rm -f $(OBJS) jiofsck.o jiofsck
-	rm -f *.bb *.bbg *.da *.gcov *.gcno *.gcda gmon.out build-flags
+	rm -f $O/libjio.a $O/libjio.so $O/libjio.so.$(LIB_VER) $O/libjio.pc
+	rm -f $(OBJS) $O/jiofsck.o $O/jiofsck
+	rm -f $O/*.bb $O/*.bbg $O/*.da $O/*.gcov $O/*.gcno $O/*.gcda $O/gmon.out
+	rm -f $O/build-flags $O/*.o.mak
+
 	$(MAKE) -C doxygen $@
 
 
diff --git a/libjio/common.c b/libjio/common.c
index 30b7e93..436f5b8 100644
--- a/libjio/common.c
+++ b/libjio/common.c
@@ -126,7 +126,7 @@ ssize_t swritev(int fd, struct iovec *iov, int iovcnt)
 		t = 0;
 		for (i = 0; i < iovcnt; i++) {
 			if (t + iov[i].iov_len > rv) {
-				iov[i].iov_base = (unsigned char *)
+				iov[i].iov_base = (char *)
 					iov[i].iov_base + rv - t;
 				iov[i].iov_len -= rv - t;
 				break;
diff --git a/libjio/common.h b/libjio/common.h
index 71ccc49..5131e0a 100644
--- a/libjio/common.h
+++ b/libjio/common.h
@@ -9,8 +9,8 @@
 #include <sys/types.h>	/* for ssize_t and off_t */
 #include <stdint.h>	/* for uint*_t */
 #include <sys/uio.h>	/* for struct iovec */
+#include <pthread.h>	/* pthread_mutex_t */
 
-#include "libjio.h"	/* for struct jfs */
 #include "fiu-local.h"	/* for fault injection functions */
 
 #define _F_READ		0x00001
diff --git a/libjio/journal.c b/libjio/journal.c
index 7c8d9bf..fc34ffc 100644
--- a/libjio/journal.c
+++ b/libjio/journal.c
@@ -215,7 +215,7 @@ static int corrupt_journal_file(struct journal_op *jop)
 	if (pos == (off_t) -1)
 		return -1;
 
-	if (pwrite(jop->fd, (unsigned char *) &trailer, sizeof(trailer), pos)
+	if (pwrite(jop->fd, (void *) &trailer, sizeof(trailer), pos)
 			!= sizeof(trailer))
 		return -1;
 
@@ -250,24 +250,6 @@ static int is_broken(struct jfs *fs)
 	return access(broken_path, F_OK) == 0;
 }
 
-/* Open and lock (exclusive) the given file name. Returns the file descriptor,
- * or -1 on error. */
-static int open_and_lockw(const char *name, int flags, int mode)
-{
-	int fd;
-
-	fd = open(name, flags, mode);
-	if (fd < 0)
-		return -1;
-
-	if (plockf(fd, F_LOCKW, 0, 0) != 0) {
-		close(fd);
-		return -1;
-	}
-
-	return fd;
-}
-
 
 /*
  * Journal functions
@@ -301,10 +283,13 @@ struct journal_op *journal_new(struct jfs *fs, unsigned int flags)
 
 	/* open the transaction file */
 	get_jtfile(fs, id, name);
-	fd = open_and_lockw(name, O_RDWR | O_CREAT | O_TRUNC, 0600);
+	fd = open(name, O_RDWR | O_CREAT | O_TRUNC, 0600);
 	if (fd < 0)
 		goto error;
 
+	if (plockf(fd, F_LOCKW, 0, 0) != 0)
+		goto unlink_error;
+
 	jop->id = id;
 	jop->fd = fd;
 	jop->numops = 0;
@@ -320,7 +305,7 @@ struct journal_op *journal_new(struct jfs *fs, unsigned int flags)
 	hdr.flags = flags;
 	hdr_hton(&hdr);
 
-	iov[0].iov_base = (unsigned char *) &hdr;
+	iov[0].iov_base = (void *) &hdr;
 	iov[0].iov_len = sizeof(hdr);
 	rv = swritev(fd, iov, 1);
 	if (rv != sizeof(hdr))
@@ -357,12 +342,12 @@ int journal_add_op(struct journal_op *jop, unsigned char *buf, size_t len,
 	ophdr.offset = offset;
 	ophdr_hton(&ophdr);
 
-	iov[0].iov_base = (unsigned char *) &ophdr;
+	iov[0].iov_base = (void *) &ophdr;
 	iov[0].iov_len = sizeof(ophdr);
 	jop->csum = checksum_buf(jop->csum, (unsigned char *) &ophdr,
 			sizeof(ophdr));
 
-	iov[1].iov_base = buf;
+	iov[1].iov_base = (void *) buf;
 	iov[1].iov_len = len;
 	jop->csum = checksum_buf(jop->csum, buf, len);
 
@@ -404,7 +389,7 @@ int journal_commit(struct journal_op *jop)
 	ophdr.len = 0;
 	ophdr.offset = 0;
 	ophdr_hton(&ophdr);
-	iov[0].iov_base = (unsigned char *) &ophdr;
+	iov[0].iov_base = (void *) &ophdr;
 	iov[0].iov_len = sizeof(ophdr);
 	jop->csum = checksum_buf(jop->csum, (unsigned char *) &ophdr,
 			sizeof(ophdr));
@@ -412,7 +397,7 @@ int journal_commit(struct journal_op *jop)
 	trailer.checksum = jop->csum;
 	trailer.numops = jop->numops;
 	trailer_hton(&trailer);
-	iov[1].iov_base = (unsigned char *) &trailer;
+	iov[1].iov_base = (void *) &trailer;
 	iov[1].iov_len = sizeof(trailer);
 
 	rv = swritev(jop->fd, iov, 2);
diff --git a/libjio/libjio.h b/libjio/libjio.h
index 0476c6e..446b81a 100644
--- a/libjio/libjio.h
+++ b/libjio/libjio.h
@@ -156,7 +156,7 @@ jtrans_t *jtrans_new(jfs_t *fs, unsigned int flags);
  * The file will not be touched (not even locked) until commit time, where the
  * first count bytes of buf will be written at offset.
  *
- * Transactions will be applied in order, and overlapping operations are
+ * Operations will be applied in order, and overlapping operations are
  * permitted, in which case the latest one will prevail.
  *
  * The buffer will be copied internally and can be free()d right after this
@@ -183,7 +183,7 @@ int jtrans_add_w(jtrans_t *ts, const void *buf, size_t count, off_t offset);
  * amount of bytes, the commit will fail, so do not attempt to read beyond EOF
  * (you can use jread() for that purpose).
  *
- * Transactions will be applied in order, and overlapping operations are
+ * Operations will be applied in order, and overlapping operations are
  * permitted, in which case the latest one will prevail.
  *
  * In case of an error in jtrans_commit(), the contents of the buffer are
diff --git a/libjio/trans.c b/libjio/trans.c
index 91970d7..e1747a8 100644
--- a/libjio/trans.c
+++ b/libjio/trans.c
@@ -258,12 +258,22 @@ ssize_t jtrans_commit(struct jtrans *ts)
 	if (ts->numops_w && (ts->flags & J_RDONLY))
 		goto exit;
 
+	/* Lock all the regions we're going to work with; otherwise there
+	 * could be another transaction trying to write the same spots and we
+	 * could end up with interleaved writes, that could break atomicity
+	 * warantees if we need to rollback.
+	 * Note we do this before creating a new transaction, so we know it's
+	 * not possible to have two overlapping transactions on disk at the
+	 * same time. */
+	if (lock_file_ranges(ts, F_LOCKW) != 0)
+		goto unlock_exit;
+
 	/* create and fill the transaction file only if we have at least one
 	 * write operation */
 	if (ts->numops_w) {
 		jop = journal_new(ts->fs, ts->flags);
 		if (jop == NULL)
-			goto exit;
+			goto unlock_exit;
 	}
 
 	for (op = ts->op; op != NULL; op = op->next) {
@@ -282,13 +292,6 @@ ssize_t jtrans_commit(struct jtrans *ts)
 
 	fiu_exit_on("jio/commit/tf_data");
 
-	/* lock all the regions we're going to work with; otherwise there
-	 * could be another transaction trying to write the same spots and we
-	 * could end up with interleaved writes, that could break atomicity
-	 * warantees if we need to rollback */
-	if (lock_file_ranges(ts, F_LOCKW) != 0)
-		goto unlink_exit;
-
 	if (!(ts->flags & J_NOROLLBACK)) {
 		for (op = ts->op; op != NULL; op = op->next) {
 			if (op->direction == D_READ)
@@ -433,6 +436,7 @@ unlink_exit:
 		jop = NULL;
 	}
 
+unlock_exit:
 	/* always unlock everything at the end; otherwise we could have
 	 * half-overlapping transactions applying simultaneously, and if
 	 * anything goes wrong it would be possible to break consistency */
diff --git a/tests/README b/tests/README
new file mode 100644
index 0000000..2e11857
--- /dev/null
+++ b/tests/README
@@ -0,0 +1,50 @@
+
+The library is normally tested using several tools, which are described below.
+
+Each commit must pass at least the behaviour and stress tests successfully,
+invasive commits must be tested with the ones that are most likely affected by
+it, and releases must be checked with all the tests.
+
+In any case, remember that testing is not a replacement for careful code
+inspection.
+
+
+Behaviour tests:
+  Check how the library behaves in different situations, from basic ones to
+  weird crash scenarios, using fault injection. They can be found in the
+  behaviour/ directory.
+
+Stress test:
+  It's a randomized stress test that performs different transactions and
+  checks the result was the one expected. Can also run with randomized fault
+  injection both in libjio's predetermined points and in POSIX functions. It
+  can be found in the stress/ directory.
+
+Performance tests:
+  Check the performance of simple operations like streaming and randomized
+  writes. Not the most interesting tests, but can be useful for profiling.
+
+Valgrind:
+  Run the other tests under valgrind and see there are no libjio-related
+  issues. Performance tests are the easier ones, sadly behaviour and stress
+  tests are more painful because Python makes valgrind emit lots of warnings,
+  but it's definitely worth the effort and should be done frequently.
+
+Code coverage:
+  Use gcov and lcov to check that the behaviour and stress tests cover most of
+  the code. Currently, it's over 90%, and it shouldn't go down. However, this
+  test shouldn't be given more relevance that the one it deserves, since it's
+  mostly useful to see if we're missing some real scenario in the other tests.
+
+Profiling:
+  Use kcachegrind and prof (the Linux Kernel tool) to check for suspicious
+  bottlenecks.
+
+Static checkers:
+  Build using sparse (make CC=cgcc) and clang (make CC=ccc), and run cppcheck.
+
+Portability:
+  Build in some different architectures. At least Linux and some BSD must
+  be checked before releases.
+
+
diff --git a/tests/stress/jiostress b/tests/stress/jiostress
index 1712c04..02f65cf 100755
--- a/tests/stress/jiostress
+++ b/tests/stress/jiostress
@@ -66,8 +66,225 @@ def comp_cont(bytes):
 		l.append((prev, c))
 		prev = b
 		c = 1
+	l.append((b, c))
 	return l
 
+def pread(fd, start, end):
+	ppos = fd.tell()
+	fd.seek(start, 0)
+	r = bytes()
+	c = 0
+	total = end - start
+	while c < total:
+		n = fd.read(total - c)
+		if (n == ''):
+			break
+		c += len(n)
+		r += n
+	fd.seek(ppos, 0)
+	assert c == end - start
+	return r
+
+#
+# A range of bytes inside a file, used inside the transactions
+#
+# Note it can't "remember" the fd as it may change between prepare() and
+# verify().
+#
+
+class Range:
+	def __init__(self, fsize, maxlen):
+		# public
+		self.start, self.end = randfrange(fsize, maxlen)
+		self.new_data = None
+		self.type = 'r'
+
+		# private
+		self.prev_data = None
+		self.new_data_ctx = None
+		self.read_buf = None
+
+		# read an extended range so we can check we
+		# only wrote what we were supposed to
+		self.ext_start = max(0, self.start - 32)
+		self.ext_end = min(fsize, self.end + 32)
+
+	def overlaps(self, other):
+		if (other.ext_start <= self.ext_start <= other.ext_end) or \
+		   (other.ext_start <= self.ext_end <= other.ext_end) or \
+		   (self.ext_start <= other.ext_start <= self.ext_end) or \
+		   (self.ext_start <= other.ext_end <= self.ext_end):
+			return True
+		return False
+
+	def prepare_r(self):
+		self.type = 'r'
+		self.read_buf = bytearray(self.end - self.start)
+
+	def verify_r(self, fd):
+		real_data = pread(fd, self.start, self.end)
+		if real_data != self.read_buf:
+			print('Corruption detected')
+			self.show(fd)
+			raise ConsistencyError
+
+	def prepare_w(self, fd):
+		self.type = 'w'
+		self.prev_data = pread(fd, self.ext_start, self.ext_end)
+
+		self.new_data = getbytes(self.end - self.start)
+		self.new_data_ctx = \
+			self.prev_data[:self.start - self.ext_start] \
+			+ self.new_data \
+			+ self.prev_data[- (self.ext_end - self.end):]
+
+		return self.new_data, self.start
+
+	def verify_w(self, fd):
+		# NOTE: fd must be a real file
+		real_data = pread(fd, self.ext_start, self.ext_end)
+		if real_data not in (self.prev_data, self.new_data_ctx):
+			print('Corruption detected')
+			self.show(fd)
+			raise ConsistencyError
+
+	def verify(self, fd):
+		if self.type == 'r':
+			self.verify_r(fd)
+		else:
+			self.verify_w(fd)
+
+	def show(self, fd):
+		real_data = pread(fd, self.start, self.end)
+		print('Range:', self.ext_start, self.ext_end)
+		print('Real:', comp_cont(real_data))
+		if self.type == 'w':
+			print('Prev:', comp_cont(self.prev_data))
+			print('New: ', comp_cont(self.new_data_ctx))
+		else:
+			print('Buf:', comp_cont(self.read_buf))
+		print()
+
+
+#
+# Transactions
+#
+
+class T_base:
+	"Interface for the transaction types"
+	def __init__(self, f, jf, fsize):
+		pass
+
+	def prepare(self):
+		pass
+
+	def apply(self):
+		pass
+
+	def verify(self, write_only = False):
+		pass
+
+class T_jwrite (T_base):
+	def __init__(self, f, jf, fsize):
+		self.f = f
+		self.jf = jf
+		self.fsize = fsize
+
+		self.maxoplen = min(int(fsize / 256), 16 * 1024 * 1024)
+		self.range = Range(self.fsize, self.maxoplen)
+
+	def prepare(self):
+		self.range.prepare_w(self.f)
+
+	def apply(self):
+		self.jf.pwrite(self.range.new_data, self.range.start)
+
+	def verify(self, write_only = False):
+		self.range.verify(self.f)
+
+class T_writeonly (T_base):
+	def __init__(self, f, jf, fsize):
+		self.f = f
+		self.jf = jf
+		self.fsize = fsize
+
+		# favour many small ops
+		self.maxoplen = 1 * 1024 * 1024
+		self.nops = random.randint(1, 26)
+
+		self.ranges = []
+
+		c = 0
+		while len(self.ranges) < self.nops and c < self.nops * 1.25:
+			candidate = Range(self.fsize, self.maxoplen)
+			safe = True
+			for r in self.ranges:
+				if candidate.overlaps(r):
+					safe = False
+					break
+			if safe:
+				self.ranges.append(candidate)
+			c += 1
+
+	def prepare(self):
+		for r in self.ranges:
+			r.prepare_w(self.f)
+
+	def apply(self):
+		t = self.jf.new_trans()
+		for r in self.ranges:
+			t.add_w(r.new_data, r.start)
+		t.commit()
+
+	def verify(self, write_only = False):
+		try:
+			for r in self.ranges:
+				r.verify(self.f)
+		except ConsistencyError:
+			# show context on errors
+			print("-" * 50)
+			for r in self.ranges:
+				r.show(self.f)
+			print("-" * 50)
+			raise
+
+class T_readwrite (T_writeonly):
+	def __init__(self, f, jf, fsize):
+		T_writeonly.__init__(self, f, jf, fsize)
+		self.read_ranges = []
+
+	def prepare(self):
+		for r in self.ranges:
+			if random.choice((True, False)):
+				r.prepare_w(self.f)
+			else:
+				r.prepare_r()
+
+	def apply(self):
+		t = self.jf.new_trans()
+		for r in self.ranges:
+			if r.type == 'r':
+				t.add_r(r.read_buf, r.start)
+			else:
+				t.add_w(r.new_data, r.start)
+		t.commit()
+
+	def verify(self, write_only = False):
+		try:
+			for r in self.ranges:
+				if write_only and r.type == 'r':
+					continue
+				r.verify(self.f)
+		except ConsistencyError:
+			# show context on errors
+			print("-" * 50)
+			for r in self.ranges:
+				r.show(self.f)
+			print("-" * 50)
+			raise
+
+t_list = [T_jwrite, T_writeonly, T_readwrite]
+
 
 #
 # The test itself
@@ -81,9 +298,6 @@ class Stresser:
 		self.use_fi = use_fi
 		self.use_as = use_as
 
-		self.maxoplen = min(int(self.fsize / 256),
-					64 * 1024)
-
 		jflags = 0
 		if use_as:
 			jflags = libjio.J_LINGER
@@ -97,60 +311,27 @@ class Stresser:
 		if use_as:
 			self.jf.autosync_start(5, 10 * 1024 * 1024)
 
-		# data used for consistency checks
-		self.current_range = (0, 0)
-		self.prev_data = b""
-		self.new_data = b""
-
-	def pread(self, start, end):
-		ppos = self.f.tell()
-		self.f.seek(start, 0)
-		r = bytes()
-		c = 0
-		total = end - start
-		while c < total:
-			n = self.f.read(total - c)
-			if (n == ''):
-				break
-			c += len(n)
-			r += n
-		self.f.seek(ppos, 0)
-		assert c == end - start
-		return r
-
-	def prep_randwrite(self):
-		start, end = randfrange(self.fsize, self.maxoplen)
-
-		# read an extended range so we can check we
-		# only wrote what we were supposed to
-		estart = max(0, start - 32)
-		eend = min(self.fsize, end + 32)
-		self.current_range = (estart, eend)
-		self.prev_data = self.pread(estart, eend)
-
-		nd = getbytes(end - start)
-		self.new_data = self.prev_data[:start - estart] \
-			+ nd + self.prev_data[- (eend - end):]
-		return nd, start
-
-	def randwrite(self, nd, start):
-		self.jf.pwrite(nd, start)
+	def apply(self, trans):
+		trans.prepare()
+		trans.apply()
+		trans.verify()
 		return True
 
-	def randwrite_fork(self):
+	def apply_fork(self, trans):
 		# do the prep before the fork so we can verify() afterwards
-		nd, start = self.prep_randwrite()
+		trans.prepare()
+
 		sys.stdout.flush()
 		pid = os.fork()
 		if pid == 0:
 			# child
 			try:
 				self.fiu_enable()
-				self.randwrite(nd, start)
+				trans.apply()
 				self.fiu_disable()
 			except (IOError, MemoryError):
 				try:
-					self.reopen()
+					self.reopen(trans)
 				except (IOError, MemoryError):
 					pass
 				except:
@@ -165,6 +346,7 @@ class Stresser:
 				self.fiu_disable()
 				traceback.print_exc()
 				sys.exit(1)
+			trans.verify()
 			sys.exit(0)
 		else:
 			# parent
@@ -176,24 +358,11 @@ class Stresser:
 				return False
 			return True
 
-	def verify(self):
-		# NOTE: must not use self.jf
-		real_data = self.pread(self.current_range[0],
-				self.current_range[1])
-		if real_data not in (self.prev_data, self.new_data):
-			print('Corruption detected')
-			print('Range:', self.current_range)
-			print('Real:', comp_cont(real_data))
-			print('Prev:', comp_cont(self.prev_data))
-			print('New: ', comp_cont(self.new_data))
-			print()
-			raise ConsistencyError
-
-	def reopen(self):
+	def reopen(self, trans):
 		self.jf = None
 		r = jfsck(self.fname)
 
-		self.verify()
+		trans.verify(write_only = True)
 
 		self.jf = libjio.open(self.fname,
 			libjio.O_RDWR | libjio.O_CREAT, 0o600)
@@ -226,6 +395,7 @@ class Stresser:
 	def run(self):
 		nfailures = 0
 		sys.stdout.write("  ")
+
 		for i in range(1, self.nops + 1):
 			sys.stdout.write(".")
 			if i % 10 == 0:
@@ -235,15 +405,18 @@ class Stresser:
 				sys.stdout.write("  ")
 			sys.stdout.flush()
 
+			trans = random.choice(t_list)(self.f, self.jf,
+					self.fsize)
+
 			if self.use_fi:
-				r = self.randwrite_fork()
+				r = self.apply_fork(trans)
 			else:
-				nd, start = self.prep_randwrite()
-				r = self.randwrite(nd, start)
+				r = self.apply(trans)
 			if not r:
 				nfailures += 1
-				r = self.reopen()
-			self.verify()
+				r = self.reopen(trans)
+				trans.verify(write_only = True)
+
 		sys.stdout.write("\n")
 		sys.stdout.flush()
 		return nfailures
@@ -258,7 +431,7 @@ def usage():
 Use: jiostress <file name> <file size in Mb> [<number of operations>]
 	[--fi] [--as]
 
-If the number of operations is not provided, the default (1000) will be
+If the number of operations is not provided, the default (500) will be
 used.
 
 If the "--fi" option is passed, the test will perform fault injection. This
@@ -273,7 +446,7 @@ def main():
 	try:
 		fname = sys.argv[1]
 		fsize = int(sys.argv[2]) * 1024 * 1024
-		nops = 1000
+		nops = 500
 		if len(sys.argv) >= 4 and sys.argv[3].isnumeric():
 			nops = int(sys.argv[3])
 
