Repository: chrovis/cljam
Pull request #178 (2019-09-10 08:52:35)

Fix creating temp directory to support multiple users
In the current implementation of creating temporary user's directory, there are two problems: always creates the directory of the same name `cljam`, and doesn't delete it after all is done. That might cause `Permission denied` if multiple users run cljam on the same machine.

This PR fixes the behavior; `with-temp-dir` macro creates an unique directory, and finally deletes it. Also, fixes a path to temporary directory such that the test code creates user's directory instead of test's directory.

codecov[bot]: # [Codecov](https://codecov.io/gh/chrovis/cljam/pull/178?src=pr&el=h1) Report
> Merging [#178](https://codecov.io/gh/chrovis/cljam/pull/178?src=pr&el=desc) into [master](https://codecov.io/gh/chrovis/cljam/commit/9c9f78792e80ca90dc5c04c7e32e99173d92c6de?src=pr&el=desc) will **decrease** coverage by `0.06%`.
> The diff coverage is `74.35%`.

[![Impacted file tree graph](https://codecov.io/gh/chrovis/cljam/pull/178/graphs/tree.svg?width=650&token=hrCuiz6NWa&height=150&src=pr)](https://codecov.io/gh/chrovis/cljam/pull/178?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##           master    #178      +/-   ##
=========================================
- Coverage   86.86%   86.8%   -0.07%     
=========================================
  Files          74      74              
  Lines        5750    5759       +9     
  Branches      492     493       +1     
=========================================
+ Hits         4995    4999       +4     
- Misses        263     267       +4     
- Partials      492     493       +1
```


| [Impacted Files](https://codecov.io/gh/chrovis/cljam/pull/178?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/cljam/util.clj](https://codecov.io/gh/chrovis/cljam/pull/178/diff?src=pr&el=tree#diff-c3JjL2NsamFtL3V0aWwuY2xq) | `89.36% <66.66%> (-5.52%)` | :arrow_down: |
| [src/cljam/algo/level.clj](https://codecov.io/gh/chrovis/cljam/pull/178/diff?src=pr&el=tree#diff-c3JjL2NsamFtL2FsZ28vbGV2ZWwuY2xq) | `81.25% <73.33%> (-1.73%)` | :arrow_down: |
| [src/cljam/algo/sorter.clj](https://codecov.io/gh/chrovis/cljam/pull/178/diff?src=pr&el=tree#diff-c3JjL2NsamFtL2FsZ28vc29ydGVyLmNsag==) | `88.67% <83.33%> (-0.95%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/chrovis/cljam/pull/178?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/chrovis/cljam/pull/178?src=pr&el=footer). Last update [9c9f787...a9bba94](https://codecov.io/gh/chrovis/cljam/pull/178?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

r6eve: Thank you for the helpful suggestion! I didn't know that.
To avoid the costs, I decided to make two functions and use them in expanded forms. 48205643031551d63e6f0f68a1c64df3f216fe69
athos: Ah, sorry if I confused you 🙇 

 I think providing `with-temp-dir` as an internal utility is completely fine. By "using them (the functions) in the expanded form", I just meant something like:

```clojure
(defmacro with-temp-dir [bindings & body]
  (cond ...
        (symbol? (bindings 0))
        (let [[dir prefix] bindings]
          `(let [~dir (create-temp-dir ~prefix)]
             (try
               (with-temp-dir ~(subvec bindings 2) ~@body)
               (finally
                 (delete-temp-dir! ~dir)))))
        ...))
  ```

Patch:
diff --git a/src/cljam/algo/level.clj b/src/cljam/algo/level.clj
index d986aa0..e86a61a 100644
--- a/src/cljam/algo/level.clj
+++ b/src/cljam/algo/level.clj
@@ -40,10 +40,10 @@
 
 (defn- cache-path
   "Create a path to the cache file."
-  [rdr i]
+  [rdr temp-dir i]
   (->> i
        (format "%s_%06d.bam" (util/basename (protocols/reader-url rdr)))
-       (cio/file util/temp-dir)
+       (cio/file temp-dir)
        (.getCanonicalPath)))
 
 (defn- add-level-mt
@@ -54,23 +54,23 @@
     (sam/write-header wtr hdr)
     (sam/write-refs wtr hdr)
     (cp/with-shutdown! [p (cp/threadpool (get-exec-n-threads))]
-      ;; split and compute levels
-      (let [xs (cp/pfor p [[i {:keys [name]}] (map vector (range) (sam/read-refs rdr))]
-                 (let [cache (cache-path rdr i)]
-                   (with-open [r (sam/reader rdr)
-                               w (sam/writer cache)]
-                     (sam/write-header w hdr)
-                     (sam/write-refs w hdr)
-                     (->> {:chr name}
-                          (sam/read-alignments r)
-                          (map (partial add-level! (volatile! [])))
-                          (#(sam/write-alignments w % hdr))))
-                   cache))]
-        ;; merge
-        (doseq [cache xs]
-          (with-open [r (sam/reader cache)]
-            (sam/write-blocks wtr (sam/read-blocks r)))
-          (.delete (cio/file cache)))))))
+      (util/with-temp-dir [temp-dir "cljam.algo.level"]
+        ;; split and compute levels
+        (let [xs (cp/pfor p [[i {:keys [name]}] (map vector (range) (sam/read-refs rdr))]
+                          (let [cache (cache-path rdr temp-dir i)]
+                            (with-open [r (sam/reader rdr)
+                                        w (sam/writer cache)]
+                              (sam/write-header w hdr)
+                              (sam/write-refs w hdr)
+                              (->> {:chr name}
+                                   (sam/read-alignments r)
+                                   (map (partial add-level! (volatile! [])))
+                                   (#(sam/write-alignments w % hdr))))
+                            cache))]
+          ;; merge
+          (doseq [cache xs]
+            (with-open [r (sam/reader cache)]
+              (sam/write-blocks wtr (sam/read-blocks r)))))))))
 
 (defmethod add-level :bam
   [rdr wtr & {:keys [n-threads]
diff --git a/src/cljam/algo/sorter.clj b/src/cljam/algo/sorter.clj
index f7c67eb..e9822e6 100644
--- a/src/cljam/algo/sorter.clj
+++ b/src/cljam/algo/sorter.clj
@@ -134,34 +134,27 @@
 ;; Sorter
 ;; ------
 
-(defn- clean-all!
-  "Deletes all files in list."
-  [files]
-  (doseq [f (map cio/file files)
-          :when (.exists ^File f)]
-    (.delete ^File f)))
-
 (defn- gen-cache-filename
   "Generates i-th cache filename."
-  [fmt prefix i]
-  (format "%s/%s_%05d.%s" util/temp-dir prefix i fmt))
+  [fmt temp-dir prefix i]
+  (format "%s/%s_%05d.%s" temp-dir prefix i fmt))
 
 (defn sort!
   "Sorts alignments of rdr by mode and writes them to wtr.
   :coordinate and :queryname are available for mode."
   [rdr wtr {:keys [mode chunk-size cache-fmt] :or {cache-fmt :bam}}]
-  (let [name-fn (->> rdr
-                     protocols/reader-url
-                     util/basename
-                     (partial gen-cache-filename (name cache-fmt)))
-        key-fn ({header/order-coordinate (partial coordinate-key (refmap (sam/read-refs rdr)))
-                 header/order-queryname queryname-key} mode)
-        splitted-files (split* rdr chunk-size name-fn mode #(sort-by-index key-fn %))
-        hdr (header/sorted-by mode (header/update-version (sam/read-header rdr)))]
-    (logging/info (str "Merging from " (count splitted-files) " files..."))
-    (merge* wtr hdr splitted-files mode key-fn)
-    (logging/info "Deleting cache files...")
-    (clean-all! splitted-files)))
+  (util/with-temp-dir [temp-dir "cljam.algo.sorter"]
+    (let [name-fn (->> rdr
+                       protocols/reader-url
+                       util/basename
+                       (partial gen-cache-filename (name cache-fmt) temp-dir))
+          key-fn ({header/order-coordinate (partial coordinate-key (refmap (sam/read-refs rdr)))
+                   header/order-queryname queryname-key} mode)
+          splitted-files (split* rdr chunk-size name-fn mode #(sort-by-index key-fn %))
+          hdr (header/sorted-by mode (header/update-version (sam/read-header rdr)))]
+      (logging/info (str "Merging from " (count splitted-files) " files..."))
+      (merge* wtr hdr splitted-files mode key-fn)
+      (logging/info "Deleting cache files..."))))
 
 (defn sort-by-pos
   "Sorts alignments by chromosomal position."
diff --git a/src/cljam/util.clj b/src/cljam/util.clj
index 3653cb9..cb18b82 100644
--- a/src/cljam/util.clj
+++ b/src/cljam/util.clj
@@ -2,17 +2,56 @@
   "General utilities."
   (:require [clojure.java.io :refer [file] :as cio])
   (:import [java.net MalformedURLException URL]
+           [java.nio.file Files FileVisitor FileVisitResult]
+           [java.nio.file.attribute FileAttribute]
            [org.apache.commons.compress.compressors
             CompressorStreamFactory CompressorException]))
 
 ;; Disk cache
 ;; ----------
 
-(def temp-dir (let [system-tmp-dir-path (or (System/getenv "TMPDIR")
-                                            (System/getProperty "java.io.tmpdir"))
-                    dir-path (.getPath (file system-tmp-dir-path "cljam"))]
-                (.mkdirs (file dir-path))
-                dir-path))
+(defn create-temp-dir
+  "Returns a created temporary directory with the given `prefix`."
+  [prefix]
+  (->> 0
+       (make-array FileAttribute)
+       (Files/createTempDirectory prefix)
+       .toFile))
+
+(defn delete-temp-dir!
+  "Takes the temporary directory created by `create-temp-dir` and deletes the
+  `dir` and the files in `dir`."
+  [^java.io.File dir]
+  (Files/walkFileTree
+   (.toPath dir)
+   (reify FileVisitor
+     (visitFile [this# file# attrs#]
+       (Files/deleteIfExists file#)
+       FileVisitResult/CONTINUE)
+     (visitFileFailed [this# file# exc#]
+       FileVisitResult/CONTINUE)
+     (preVisitDirectory [this# dir# attrs#]
+       FileVisitResult/CONTINUE)
+     (postVisitDirectory [this# dir# exc#]
+       (Files/deleteIfExists dir#)
+       FileVisitResult/CONTINUE))))
+
+(defmacro with-temp-dir
+  "bindings => [dir prefix ...]
+  Creates a temporary directory with the given `prefix` and binds the given
+  `dir` to it. Finally, deletes `dir` and the files in `dir`."
+  [bindings & body]
+  (assert (vector? bindings) "bindings must be a vector")
+  (assert (even? (count bindings)) "bindings must have an even number of forms")
+  (cond
+    (zero? (count bindings)) `(do ~@body)
+    (symbol? (bindings 0)) (let [[dir prefix] bindings]
+                             `(let [~dir (create-temp-dir ~prefix)]
+                                (try
+                                  (with-temp-dir ~(subvec bindings 2) ~@body)
+                                  (finally
+                                    (delete-temp-dir! ~dir)))))
+    :else (throw (IllegalArgumentException. "binding must be a symbol"))))
 
 ;; byte array
 ;; ----------


Test patch:
diff --git a/test/cljam/io/bcf/writer_test.clj b/test/cljam/io/bcf/writer_test.clj
index c13ea81..31df50c 100644
--- a/test/cljam/io/bcf/writer_test.clj
+++ b/test/cljam/io/bcf/writer_test.clj
@@ -1,8 +1,10 @@
 (ns cljam.io.bcf.writer-test
   (:require [clojure.test :refer :all]
+            [clojure.java.io :as cio]
             [clojure.string :as cstr]
             [cljam.io.bcf.writer :as bcf-writer]
-            [cljam.io.util.bgzf :as bgzf])
+            [cljam.io.util.bgzf :as bgzf]
+            [cljam.test-common :refer :all])
   (:import [java.nio ByteBuffer ByteOrder]
            [java.io ByteArrayOutputStream File]))
 
@@ -238,12 +240,12 @@
          vec)))
 
 (defn- write-variants&read-blocks [meta-info header variants]
-  (let [tmp (File/createTempFile "write-variants" ".bcf")]
-    (with-open [w (bcf-writer/writer tmp meta-info header)]
-      (bcf-writer/write-variants w variants))
-    (let [blocks (read-variant-blocks! tmp)]
-      (.delete tmp)
-      blocks)))
+  (with-before-after {:before (prepare-cache!)
+                      :after (clean-cache!)}
+    (let [tmp (cio/file temp-dir "write-variants.bcf")]
+      (with-open [w (bcf-writer/writer tmp meta-info header)]
+        (bcf-writer/write-variants w variants))
+      (read-variant-blocks! tmp))))
 
 (deftest write-variants
   (let [meta-info {:fileformat "VCFv4.3"
diff --git a/test/cljam/io/pileup_test.clj b/test/cljam/io/pileup_test.clj
index 4d195a0..bfd3a74 100644
--- a/test/cljam/io/pileup_test.clj
+++ b/test/cljam/io/pileup_test.clj
@@ -281,9 +281,10 @@
   (testing "with-ref"
     (are [?input]
          (= ?input
-            (let [tmp (File/createTempFile "pileup-regression" ".fa")
-                  idx (File/createTempFile "pileup-regression" ".fai")]
-              (try
+            (with-before-after {:before (prepare-cache!)
+                                :after (clean-cache!)}
+              (let [tmp (cio/file temp-dir "pileup-regression.fa")
+                    idx (cio/file temp-dir "pileup-regression.fai")]
                 (with-open [w (cseq/writer (.getCanonicalPath tmp))]
                   (cseq/write-sequences w [{:name "chr1" :sequence "NNNNNNNNNATGC"}]))
                 (fai/create-index tmp idx)
@@ -291,10 +292,7 @@
                             sw (StringWriter.)
                             w (plpio/writer sw tmp)]
                   (plpio/write-piles w (plpio/read-piles r))
-                  (str sw))
-                (finally
-                  (when (.isFile (cio/file tmp))
-                    (cio/delete-file (cio/file tmp)))))))
+                  (str sw)))))
       "chr1\t10\tA\t1\t.\tI\n"
       "chr1\t10\tA\t4\t.,Tt\tIABC\n"
       "chr1\t10\tA\t4\t^].+3TTT,-2tg$Tt\tIABC\n")))
diff --git a/test/cljam/io/sam_test.clj b/test/cljam/io/sam_test.clj
index eafc9ff..1b25470 100644
--- a/test/cljam/io/sam_test.clj
+++ b/test/cljam/io/sam_test.clj
@@ -3,8 +3,7 @@
             [clojure.java.io :as cio]
             [cljam.test-common :refer :all]
             [cljam.io.sam :as sam]
-            [cljam.io.protocols :as protocols]
-            [cljam.util :as util]))
+            [cljam.io.protocols :as protocols]))
 
 (def temp-sam-file (str temp-dir "/test.sam"))
 (def temp-bam-file (str temp-dir "/test.bam"))
@@ -260,15 +259,21 @@
 
 (deftest writer-test
   (testing "sam"
-    (with-open [wtr (sam/writer (.getAbsolutePath (cio/file util/temp-dir "temp.sam")))]
-      (is (instance? cljam.io.sam.writer.SAMWriter wtr))))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (with-open [wtr (sam/writer (.getAbsolutePath (cio/file temp-dir "temp.sam")))]
+        (is (instance? cljam.io.sam.writer.SAMWriter wtr)))))
   (testing "bam"
-    (with-open [wtr (sam/writer (.getAbsolutePath (cio/file util/temp-dir "temp.bam")))]
-      (is (instance? cljam.io.bam.writer.BAMWriter wtr))))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (with-open [wtr (sam/writer (.getAbsolutePath (cio/file temp-dir "temp.bam")))]
+        (is (instance? cljam.io.bam.writer.BAMWriter wtr)))))
   (testing "throws Exception"
-    (are [f] (thrown? Exception (sam/writer (.getAbsolutePath (cio/file util/temp-dir f))))
-      "temp.baam"
-      "temp.bai")))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (are [f] (thrown? Exception (sam/writer (.getAbsolutePath (cio/file temp-dir f))))
+        "temp.baam"
+        "temp.bai"))))
 
 (def test-options
   [{:Xa {:type "A", :value \p}}
diff --git a/test/cljam/io/sequence_test.clj b/test/cljam/io/sequence_test.clj
index a50eda8..2430c63 100644
--- a/test/cljam/io/sequence_test.clj
+++ b/test/cljam/io/sequence_test.clj
@@ -5,8 +5,7 @@
             [cljam.test-common :refer :all]
             [cljam.io.fasta.core :as fa-core]
             [cljam.io.sequence :as cseq]
-            [cljam.io.protocols :as protocols]
-            [cljam.util :as util]))
+            [cljam.io.protocols :as protocols]))
 
 (def temp-test-fa-file (str temp-dir "/test.fa"))
 (def temp-medium-fa-file (str temp-dir "/medium.fa"))
@@ -219,15 +218,21 @@
 
 (deftest writer-test
   (testing "fasta"
-    (with-open [wtr (cseq/writer (.getAbsolutePath (cio/file util/temp-dir "temp.fa")))]
-      (is (instance? cljam.io.fasta.writer.FASTAWriter wtr))))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (with-open [wtr (cseq/writer (.getAbsolutePath (cio/file temp-dir "temp.fa")))]
+        (is (instance? cljam.io.fasta.writer.FASTAWriter wtr)))))
   (testing "twobit"
-    (with-open [wtr (cseq/writer (.getAbsolutePath (cio/file util/temp-dir "temp.2bit")))]
-      (is (instance? cljam.io.twobit.writer.TwoBitWriter wtr))))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (with-open [wtr (cseq/writer (.getAbsolutePath (cio/file temp-dir "temp.2bit")))]
+        (is (instance? cljam.io.twobit.writer.TwoBitWriter wtr)))))
   (testing "throws Exception"
-    (are [f] (thrown? Exception (cseq/writer (.getAbsolutePath (cio/file util/temp-dir f))))
-      "temp.fsta"
-      "temp.fa.fai")))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (are [f] (thrown? Exception (cseq/writer (.getAbsolutePath (cio/file temp-dir f))))
+        "temp.fsta"
+        "temp.fa.fai"))))
 
 (deftest write-sequences-fasta-test
   (with-before-after {:before (prepare-cache!)
diff --git a/test/cljam/io/util_test.clj b/test/cljam/io/util_test.clj
index c3b6c4f..e263f00 100644
--- a/test/cljam/io/util_test.clj
+++ b/test/cljam/io/util_test.clj
@@ -2,7 +2,6 @@
   (:require [clojure.test :refer :all]
             [clojure.java.io :as cio]
             [cljam.test-common :refer :all]
-            [cljam.util :as util]
             [cljam.io.bed :as bed]
             [cljam.io.fastq :as fastq]
             [cljam.io.sam :as sam]
@@ -303,137 +302,155 @@
 
 (deftest writer-predicates-test
   (testing "sam writer"
-    (with-open [r (sam/writer (.getAbsolutePath (cio/file util/temp-dir "temp.sam")))]
-      (are [?pred ?expected] (= (?pred r) ?expected)
-        io-util/alignment-writer? true
-        io-util/sam-writer? true
-        io-util/bam-writer? false
-        io-util/sequence-writer? false
-        io-util/fasta-writer? false
-        io-util/twobit-writer? false
-        io-util/variant-writer? false
-        io-util/vcf-writer? false
-        io-util/bcf-writer? false
-        io-util/fastq-writer? false
-        io-util/bed-writer? false
-        io-util/wig-writer? false)))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (with-open [r (sam/writer (.getAbsolutePath (cio/file temp-dir "temp.sam")))]
+        (are [?pred ?expected] (= (?pred r) ?expected)
+          io-util/alignment-writer? true
+          io-util/sam-writer? true
+          io-util/bam-writer? false
+          io-util/sequence-writer? false
+          io-util/fasta-writer? false
+          io-util/twobit-writer? false
+          io-util/variant-writer? false
+          io-util/vcf-writer? false
+          io-util/bcf-writer? false
+          io-util/fastq-writer? false
+          io-util/bed-writer? false
+          io-util/wig-writer? false))))
   (testing "bam writer"
-    (with-open [r (sam/writer (.getAbsolutePath (cio/file util/temp-dir "temp.bam")))]
-      (are [?pred ?expected] (= (?pred r) ?expected)
-        io-util/alignment-writer? true
-        io-util/sam-writer? false
-        io-util/bam-writer? true
-        io-util/sequence-writer? false
-        io-util/fasta-writer? false
-        io-util/twobit-writer? false
-        io-util/variant-writer? false
-        io-util/vcf-writer? false
-        io-util/bcf-writer? false
-        io-util/fastq-writer? false
-        io-util/bed-writer? false
-        io-util/wig-writer? false)))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (with-open [r (sam/writer (.getAbsolutePath (cio/file temp-dir "temp.bam")))]
+        (are [?pred ?expected] (= (?pred r) ?expected)
+          io-util/alignment-writer? true
+          io-util/sam-writer? false
+          io-util/bam-writer? true
+          io-util/sequence-writer? false
+          io-util/fasta-writer? false
+          io-util/twobit-writer? false
+          io-util/variant-writer? false
+          io-util/vcf-writer? false
+          io-util/bcf-writer? false
+          io-util/fastq-writer? false
+          io-util/bed-writer? false
+          io-util/wig-writer? false))))
   (testing "fasta writer"
-    (with-open [r (cseq/writer (.getAbsolutePath (cio/file util/temp-dir "temp.fa")))]
-      (are [?pred ?expected] (= (?pred r) ?expected)
-        io-util/alignment-writer? false
-        io-util/sam-writer? false
-        io-util/bam-writer? false
-        io-util/sequence-writer? true
-        io-util/fasta-writer? true
-        io-util/twobit-writer? false
-        io-util/variant-writer? false
-        io-util/vcf-writer? false
-        io-util/bcf-writer? false
-        io-util/fastq-writer? false
-        io-util/bed-writer? false
-        io-util/wig-writer? false)))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (with-open [r (cseq/writer (.getAbsolutePath (cio/file temp-dir "temp.fa")))]
+        (are [?pred ?expected] (= (?pred r) ?expected)
+          io-util/alignment-writer? false
+          io-util/sam-writer? false
+          io-util/bam-writer? false
+          io-util/sequence-writer? true
+          io-util/fasta-writer? true
+          io-util/twobit-writer? false
+          io-util/variant-writer? false
+          io-util/vcf-writer? false
+          io-util/bcf-writer? false
+          io-util/fastq-writer? false
+          io-util/bed-writer? false
+          io-util/wig-writer? false))))
   (testing "twobit writer"
-    (with-open [r (cseq/writer (.getAbsolutePath (cio/file util/temp-dir "temp.2bit")))]
-      (are [?pred ?expected] (= (?pred r) ?expected)
-        io-util/alignment-writer? false
-        io-util/sam-writer? false
-        io-util/bam-writer? false
-        io-util/sequence-writer? true
-        io-util/fasta-writer? false
-        io-util/twobit-writer? true
-        io-util/variant-writer? false
-        io-util/vcf-writer? false
-        io-util/bcf-writer? false
-        io-util/fastq-writer? false
-        io-util/bed-writer? false
-        io-util/wig-writer? false)))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (with-open [r (cseq/writer (.getAbsolutePath (cio/file temp-dir "temp.2bit")))]
+        (are [?pred ?expected] (= (?pred r) ?expected)
+          io-util/alignment-writer? false
+          io-util/sam-writer? false
+          io-util/bam-writer? false
+          io-util/sequence-writer? true
+          io-util/fasta-writer? false
+          io-util/twobit-writer? true
+          io-util/variant-writer? false
+          io-util/vcf-writer? false
+          io-util/bcf-writer? false
+          io-util/fastq-writer? false
+          io-util/bed-writer? false
+          io-util/wig-writer? false))))
   (testing "vcf writer"
-    (with-open [r (vcf/writer (.getAbsolutePath (cio/file util/temp-dir "temp.vcf")) {} [])]
-      (are [?pred ?expected] (= (?pred r) ?expected)
-        io-util/alignment-writer? false
-        io-util/sam-writer? false
-        io-util/bam-writer? false
-        io-util/sequence-writer? false
-        io-util/fasta-writer? false
-        io-util/twobit-writer? false
-        io-util/variant-writer?  true
-        io-util/vcf-writer? true
-        io-util/bcf-writer? false
-        io-util/fastq-writer? false
-        io-util/bed-writer? false
-        io-util/wig-writer? false)))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (with-open [r (vcf/writer (.getAbsolutePath (cio/file temp-dir "temp.vcf")) {} [])]
+        (are [?pred ?expected] (= (?pred r) ?expected)
+          io-util/alignment-writer? false
+          io-util/sam-writer? false
+          io-util/bam-writer? false
+          io-util/sequence-writer? false
+          io-util/fasta-writer? false
+          io-util/twobit-writer? false
+          io-util/variant-writer?  true
+          io-util/vcf-writer? true
+          io-util/bcf-writer? false
+          io-util/fastq-writer? false
+          io-util/bed-writer? false
+          io-util/wig-writer? false))))
   (testing "bcf writer"
-    (with-open [r (vcf/writer (.getAbsolutePath (cio/file util/temp-dir "temp.bcf")) {} [])]
-      (are [?pred ?expected] (= (?pred r) ?expected)
-        io-util/alignment-writer? false
-        io-util/sam-writer? false
-        io-util/bam-writer? false
-        io-util/sequence-writer? false
-        io-util/fasta-writer? false
-        io-util/twobit-writer? false
-        io-util/variant-writer?  true
-        io-util/vcf-writer? false
-        io-util/bcf-writer? true
-        io-util/fastq-writer? false
-        io-util/bed-writer? false
-        io-util/wig-writer? false)))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (with-open [r (vcf/writer (.getAbsolutePath (cio/file temp-dir "temp.bcf")) {} [])]
+        (are [?pred ?expected] (= (?pred r) ?expected)
+          io-util/alignment-writer? false
+          io-util/sam-writer? false
+          io-util/bam-writer? false
+          io-util/sequence-writer? false
+          io-util/fasta-writer? false
+          io-util/twobit-writer? false
+          io-util/variant-writer?  true
+          io-util/vcf-writer? false
+          io-util/bcf-writer? true
+          io-util/fastq-writer? false
+          io-util/bed-writer? false
+          io-util/wig-writer? false))))
   (testing "fastq writer"
-    (with-open [r (fastq/writer (.getAbsolutePath (cio/file util/temp-dir "temp.fq")))]
-      (are [?pred ?expected] (= (?pred r) ?expected)
-        io-util/alignment-writer? false
-        io-util/sam-writer? false
-        io-util/bam-writer? false
-        io-util/sequence-writer? false
-        io-util/fasta-writer? false
-        io-util/twobit-writer? false
-        io-util/variant-writer?  false
-        io-util/vcf-writer? false
-        io-util/bcf-writer? false
-        io-util/fastq-writer? true
-        io-util/bed-writer? false
-        io-util/wig-writer? false)))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (with-open [r (fastq/writer (.getAbsolutePath (cio/file temp-dir "temp.fq")))]
+        (are [?pred ?expected] (= (?pred r) ?expected)
+          io-util/alignment-writer? false
+          io-util/sam-writer? false
+          io-util/bam-writer? false
+          io-util/sequence-writer? false
+          io-util/fasta-writer? false
+          io-util/twobit-writer? false
+          io-util/variant-writer?  false
+          io-util/vcf-writer? false
+          io-util/bcf-writer? false
+          io-util/fastq-writer? true
+          io-util/bed-writer? false
+          io-util/wig-writer? false))))
   (testing "bed writer"
-    (with-open [r (bed/writer (.getAbsolutePath (cio/file util/temp-dir "temp.bed")))]
-      (are [?pred ?expected] (= (?pred r) ?expected)
-        io-util/alignment-writer? false
-        io-util/sam-writer? false
-        io-util/bam-writer? false
-        io-util/sequence-writer? false
-        io-util/fasta-writer? false
-        io-util/twobit-writer? false
-        io-util/variant-writer?  false
-        io-util/vcf-writer? false
-        io-util/bcf-writer? false
-        io-util/fastq-writer? false
-        io-util/bed-writer? true
-        io-util/wig-writer? false)))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (with-open [r (bed/writer (.getAbsolutePath (cio/file temp-dir "temp.bed")))]
+        (are [?pred ?expected] (= (?pred r) ?expected)
+          io-util/alignment-writer? false
+          io-util/sam-writer? false
+          io-util/bam-writer? false
+          io-util/sequence-writer? false
+          io-util/fasta-writer? false
+          io-util/twobit-writer? false
+          io-util/variant-writer?  false
+          io-util/vcf-writer? false
+          io-util/bcf-writer? false
+          io-util/fastq-writer? false
+          io-util/bed-writer? true
+          io-util/wig-writer? false))))
   (testing "wig writer"
-    (with-open [r (wig/writer (.getAbsolutePath (cio/file util/temp-dir "temp.wig")))]
-      (are [?pred ?expected] (= (?pred r) ?expected)
-        io-util/alignment-writer? false
-        io-util/sam-writer? false
-        io-util/bam-writer? false
-        io-util/sequence-writer? false
-        io-util/fasta-writer? false
-        io-util/twobit-writer? false
-        io-util/variant-writer?  false
-        io-util/vcf-writer? false
-        io-util/bcf-writer? false
-        io-util/fastq-writer? false
-        io-util/bed-writer? false
-        io-util/wig-writer? true))))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (with-open [r (wig/writer (.getAbsolutePath (cio/file temp-dir "temp.wig")))]
+        (are [?pred ?expected] (= (?pred r) ?expected)
+          io-util/alignment-writer? false
+          io-util/sam-writer? false
+          io-util/bam-writer? false
+          io-util/sequence-writer? false
+          io-util/fasta-writer? false
+          io-util/twobit-writer? false
+          io-util/variant-writer?  false
+          io-util/vcf-writer? false
+          io-util/bcf-writer? false
+          io-util/fastq-writer? false
+          io-util/bed-writer? false
+          io-util/wig-writer? true)))))
diff --git a/test/cljam/io/vcf_test.clj b/test/cljam/io/vcf_test.clj
index 0f18de3..c8dd05b 100644
--- a/test/cljam/io/vcf_test.clj
+++ b/test/cljam/io/vcf_test.clj
@@ -2,8 +2,7 @@
   (:require [clojure.test :refer :all]
             [clojure.java.io :as cio]
             [cljam.test-common :refer :all]
-            [cljam.io.vcf :as vcf]
-            [cljam.util :as util])
+            [cljam.io.vcf :as vcf])
   (:import bgzf4j.BGZFException))
 
 (def ^:private temp-file (str temp-dir "/test.vcf"))
@@ -124,17 +123,25 @@
 
 (deftest writer-test
   (testing "vcf"
-    (with-open [wtr (vcf/writer (.getAbsolutePath (cio/file util/temp-dir "temp.vcf")) {} [])]
-      (is (instance? cljam.io.vcf.writer.VCFWriter wtr))))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (with-open [wtr (vcf/writer (.getAbsolutePath (cio/file temp-dir "temp.vcf")) {} [])]
+        (is (instance? cljam.io.vcf.writer.VCFWriter wtr)))))
   (testing "bcf"
-    (with-open [wtr (vcf/writer (.getAbsolutePath (cio/file util/temp-dir "temp.bcf")) {} [])]
-      (is (instance? cljam.io.bcf.writer.BCFWriter wtr)))
-    (with-open [wtr (vcf/writer (.getAbsolutePath (cio/file util/temp-dir "temp.bcf"))
-                                {:filter [{:id "PASS", :description "All filters passed"}]} [])]
-      (is (instance? cljam.io.bcf.writer.BCFWriter wtr))))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (with-open [wtr (vcf/writer (.getAbsolutePath (cio/file temp-dir "temp.bcf")) {} [])]
+        (is (instance? cljam.io.bcf.writer.BCFWriter wtr))))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (with-open [wtr (vcf/writer (.getAbsolutePath (cio/file temp-dir "temp.bcf"))
+                                  {:filter [{:id "PASS", :description "All filters passed"}]} [])]
+        (is (instance? cljam.io.bcf.writer.BCFWriter wtr)))))
   (testing "throws Exception"
-    (is (thrown? Exception
-                 (vcf/writer (.getAbsolutePath (cio/file util/temp-dir "temp.vccf")) {} [])))))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (is (thrown? Exception
+                   (vcf/writer (.getAbsolutePath (cio/file temp-dir "temp.vccf")) {} []))))))
 
 (deftest about-writing-vcf-v4_0-deep
   (with-before-after {:before (prepare-cache!)
diff --git a/test/cljam/io/wig_test.clj b/test/cljam/io/wig_test.clj
index 2ac6065..3b9ecc1 100644
--- a/test/cljam/io/wig_test.clj
+++ b/test/cljam/io/wig_test.clj
@@ -3,8 +3,7 @@
             [clojure.java.io :as cio]
             [clojure.string :as cstr]
             [cljam.test-common :refer :all]
-            [cljam.io.wig :as wig]
-            [cljam.util :as util])
+            [cljam.io.wig :as wig])
   (:import [java.io BufferedReader InputStreamReader ByteArrayInputStream
             ByteArrayOutputStream OutputStreamWriter BufferedWriter]
            [cljam.io.wig WIGReader WIGWriter]))
@@ -189,8 +188,10 @@
 
 (deftest writer
   (testing "make writer instance"
-    (with-open [wtr (wig/writer (.getAbsolutePath (cio/file util/temp-dir "temp.wig")))]
-      (is (instance? cljam.io.wig.WIGWriter wtr))))
+    (with-before-after {:before (prepare-cache!)
+                        :after (clean-cache!)}
+      (with-open [wtr (wig/writer (.getAbsolutePath (cio/file temp-dir "temp.wig")))]
+        (is (instance? cljam.io.wig.WIGWriter wtr)))))
 
   (testing "write wig"
     (is (= (-> "variableStep chrom=chr1 span=10\n1 1\n101 2" str->wig wig->str)
diff --git a/test/cljam/util_test.clj b/test/cljam/util_test.clj
index 5f775f5..b25a5b7 100644
--- a/test/cljam/util_test.clj
+++ b/test/cljam/util_test.clj
@@ -1,9 +1,54 @@
 (ns cljam.util-test
   "Tests for cljam.util."
   (:require [clojure.java.io :as cio]
+            [clojure.string :as cstr]
             [clojure.test :refer :all]
+            [cljam.test-common :refer :all]
             [cljam.util :as util])
-  (:import [java.net URL]))
+  (:import [java.io File]
+           [java.net URL]
+           [java.nio.file Files]
+           [java.nio.file.attribute FileAttribute]))
+
+(deftest with-temp-dir-test
+  (testing "name with given prefixes"
+    (let [x "foo", y "bar"]
+      (util/with-temp-dir [d x, e y]
+        (let [has-prefix? (fn [name' prefix]
+                            (and (> (count name') (count prefix))
+                                 (cstr/starts-with? name' prefix)))]
+          (is (has-prefix? (.getName ^File d) x))
+          (is (has-prefix? (.getName ^File e) y))))))
+  (testing "no temp items outside of the macro's scope"
+    (let [[d e] (util/with-temp-dir [d "foo", e "bar"] [d e])
+          deleted? (fn [dir] (not (.exists (cio/file dir))))]
+      (is (deleted? d))
+      (is (deleted? e))))
+  (testing "users can delete temp directories before entering a finally clause"
+    (try
+      (util/with-temp-dir [d "foo", e "bar"]
+        (cio/delete-file d true)
+        (cio/delete-file e true))
+      (catch Exception e (is false e))
+      (finally (is true))))
+  (testing "automatically deletes subdirectories created by users"
+    (let [sub-dirs (util/with-temp-dir [d "foo"]
+                     (let [sub-dirs [(cio/file d "bar") (cio/file d "qux")]]
+                       (run! (fn [^File dir] (.mkdir dir)) sub-dirs)
+                       sub-dirs))
+          deleted? (fn [dir] (not (.exists (cio/file dir))))]
+      (is (every? deleted? sub-dirs))))
+  (testing "keeps original nodes if symbolic links are deleted"
+    (let [orig (cio/file temp-dir "orig")]
+      (with-before-after {:before (prepare-cache!)
+                          :after (clean-cache!)}
+        (.mkdir orig)
+        (util/with-temp-dir [d "foo"]
+          (Files/createSymbolicLink
+           (.toPath (cio/file d "orig-link"))
+           (.toPath orig)
+           (make-array FileAttribute 0)))
+        (is (.exists orig))))))
 
 (deftest ubyte
   (are [?n ?expected] (= (util/ubyte ?n) ?expected)