diff --git a/.travis.yml b/.travis.yml
index a1b1b96..44fc227 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,10 +1,7 @@
 language: clojure
-lein: 2.7.1
 jdk:
-  - oraclejdk7
-  - openjdk7
-  - openjdk6
-script: ./ext/travisci/test.sh
+  - openjdk8
+  - openjdk11
 notifications:
   email: false
   hipchat:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 947b5e1..0c03c94 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,38 @@
+## 2.5.2
+
+This is a minor maintenance release.
+
+Maintenance:
+* Fix adding URLs to classpath under Java 9.
+
+## 2.5.1
+
+This is a minor maintenance release.
+
+Maintenance:
+* Fix symbol redef warnings under Clojure 1.9
+
+## 2.5.0
+
+This is a minor feature release.
+
+Features:
+* add a `stream->sha256` function for hashing the contents of an InputStream
+
+## 2.4.0
+
+This is a minor feature and improvement release.
+
+Features:
+* add a `utf8-string->sha256` function, directly analogous to `utf8-string->sha1`
+* add a `file->sha256` function, equivalent to reading a file's contents as a
+  UTF-8 string and hashing the result. Uses an InputStream internally to avoid
+  reading the entire file into memory at once.
+
+Improvement:
+* the `open-port-num` function should now return a random port number from the
+  entire traditional ephemeral port range of 49152 through 65535.
+
 ## 2.3.0
 
 This is a minor feature release.
@@ -21,7 +56,7 @@ Features:
 
 Maintenance:
 
-* Update to dynapath 0.2.5, to address some compatability issues with Java 9.
+* Update to dynapath 0.2.5, to address some compatibility issues with Java 9.
 
 ## 2.1.1
 
diff --git a/MAINTAINERS b/MAINTAINERS
index fe2d42b..d7a28a0 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -18,6 +18,11 @@
       "github": "camlow325",
       "email": "jeremy.barlow@puppet.com",
       "name": "Jeremy Barlow"
+    },
+    {
+      "github": "aperiodic",
+      "email": "dlp@puppet.com",
+      "name": "Dan Lidral-Porter"
     }
   ]
 }
diff --git a/classpath-test/does-not-exist-anywhere-else b/classpath-test/does-not-exist-anywhere-else
new file mode 100644
index 0000000..e69de29
diff --git a/debian/changelog b/debian/changelog
index 0ae2a84..307eda7 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,11 @@
+kitchensink-clojure (3.0.0-1) UNRELEASED; urgency=medium
+
+  * New upstream release.
+  * Drop patch 0001-maint-Fix-Clojure-1.9-warnings.patch, present
+    upstream.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Tue, 19 Nov 2019 07:59:00 +0000
+
 kitchensink-clojure (2.3.0-2) unstable; urgency=medium
 
   * Team upload.
diff --git a/debian/patches/0001-maint-Fix-Clojure-1.9-warnings.patch b/debian/patches/0001-maint-Fix-Clojure-1.9-warnings.patch
deleted file mode 100644
index 3f7caec..0000000
--- a/debian/patches/0001-maint-Fix-Clojure-1.9-warnings.patch
+++ /dev/null
@@ -1,29 +0,0 @@
-From 648486306882013f383baf45df680d970d382a4a Mon Sep 17 00:00:00 2001
-From: Russell Mull <russell.mull@puppetlabs.com>
-Date: Tue, 26 Sep 2017 13:59:08 -0700
-Subject: [PATCH] (maint) Fix Clojure 1.9 warnings
-
-In verison 1.9, clojure gets the boolean? and uuid? predicates in clojure.core.
-Exclude these from kitchensink.core to avoid duplicate definition warnings.
----
- CHANGELOG.md                        | 7 +++++++
- src/puppetlabs/kitchensink/core.clj | 1 +
- 2 files changed, 8 insertions(+)
----
-Note: CHANGELOG.md part skipped in this patch.
-
-diff --git a/src/puppetlabs/kitchensink/core.clj b/src/puppetlabs/kitchensink/core.clj
-index d3d9f68..2ce0f11 100644
---- a/src/puppetlabs/kitchensink/core.clj
-+++ b/src/puppetlabs/kitchensink/core.clj
-@@ -5,6 +5,7 @@
- ;; altogether. But who has time for that?
- 
- (ns puppetlabs.kitchensink.core
-+  (:refer-clojure :exclude [boolean? uuid?])
-   (:import [org.ini4j Ini Config BasicProfileSection]
-            [javax.naming.ldap LdapName]
-            [java.io StringWriter Reader File])
--- 
-2.11.0
-
diff --git a/debian/patches/series b/debian/patches/series
index 3d1b3ff..e69de29 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1 +0,0 @@
-0001-maint-Fix-Clojure-1.9-warnings.patch
diff --git a/ext/travisci/test.sh b/ext/travisci/test.sh
deleted file mode 100755
index db011da..0000000
--- a/ext/travisci/test.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-
-lein2 test
diff --git a/project.clj b/project.clj
index 6a9759c..6908713 100644
--- a/project.clj
+++ b/project.clj
@@ -1,11 +1,11 @@
-(defproject puppetlabs/kitchensink "2.3.0"
+(defproject puppetlabs/kitchensink "3.0.0"
   :description "Clojure utility functions"
   :license {:name "Apache License, Version 2.0"
             :url "http://www.apache.org/licenses/LICENSE-2.0.html"}
 
-  :min-lein-version "2.7.1"
+  :min-lein-version "2.9.1"
 
-  :parent-project {:coords [puppetlabs/clj-parent "0.1.3"]
+  :parent-project {:coords [puppetlabs/clj-parent "4.0.0"]
                    :inherit [:managed-dependencies]}
 
   ;; Abort when version ranges or version conflicts are detected in
@@ -18,14 +18,13 @@
                  [org.clojure/tools.cli]
 
                  [clj-time]
-                 [me.raynes/fs]
+                 [clj-commons/fs]
                  [slingshot]
                  [cheshire]
 
                  [org.ini4j/ini4j "0.5.2"]
-                 [org.tcrawley/dynapath "0.2.5"]
+                 [org.tcrawley/dynapath]
                  [digest "1.4.3"]
-
                  ]
 
   ;; By declaring a classifier here and a corresponding profile below we'll get an additional jar
@@ -38,7 +37,10 @@
 
   ;; this plugin is used by jenkins jobs to interrogate the project version
   :plugins [[lein-project-version "0.1.0"]
-            [lein-parent "0.3.1"]]
+            [lein-parent "0.3.7"]]
+
+  :test-selectors {:default (complement :slow)
+                   :slow :slow}
 
   :deploy-repositories [["releases" {:url "https://clojars.org/repo"
                                      :username :env/clojars_jenkins_username
diff --git a/src/puppetlabs/kitchensink/classpath.clj b/src/puppetlabs/kitchensink/classpath.clj
index 4614b6a..4babc4c 100644
--- a/src/puppetlabs/kitchensink/classpath.clj
+++ b/src/puppetlabs/kitchensink/classpath.clj
@@ -35,6 +35,15 @@
   [cl]
   (dp/addable-classpath? cl))
 
+(defn- ensure-modifiable-classloader
+  "Check if there is a modifiable classloader in the current hierarchy, and add
+  one if not."
+  []
+  (let [classloader (.. Thread currentThread getContextClassLoader)]
+    (when (not-any? modifiable-classloader? (classloader-hierarchy classloader))
+      (let [new-cl (clojure.lang.DynamicClassLoader. classloader)]
+        (.. Thread currentThread (setContextClassLoader new-cl))))))
+
 (defn add-classpath
   "A corollary to the (deprecated) `add-classpath` in clojure.core. This implementation
    requires a java.io.File or String path to a jar file or directory, and will attempt
@@ -57,6 +66,7 @@
    (if-not (dp/add-classpath-url classloader (.toURL (file jar-or-dir)))
      (throw (IllegalStateException. (str classloader " is not a modifiable classloader")))))
   ([jar-or-dir]
+   (ensure-modifiable-classloader)
    (let [classloaders (classloader-hierarchy)]
      (if-let [cl (last (filter modifiable-classloader? classloaders))]
        (add-classpath jar-or-dir cl)
diff --git a/src/puppetlabs/kitchensink/core.clj b/src/puppetlabs/kitchensink/core.clj
index 95dca89..2ce0f11 100644
--- a/src/puppetlabs/kitchensink/core.clj
+++ b/src/puppetlabs/kitchensink/core.clj
@@ -966,6 +966,31 @@ to be a zipper."
   (let [bytes (.getBytes s "UTF-8")]
     (digest/sha-1 [bytes])))
 
+(defn utf8-string->sha256
+  "Compute a SHA-256 hash for the UTF-8 encoded version of the supplied
+  string"
+  [^String s]
+  {:pre  [(string? s)]
+   :post [(string? %)]}
+  (let [bytes (.getBytes s "UTF-8")]
+    (digest/sha-256 [bytes])))
+
+(defn stream->sha256
+  "Compute a SHA-256 hash for the given java.io.InputStream object."
+  [^java.io.InputStream stream]
+  {:pre  [(instance? java.io.InputStream stream)]
+   :post [(string? %)]}
+  (digest/sha-256 stream))
+
+(defn file->sha256
+  "Compute a SHA-256 hash for the given java.io.File object.
+  Uses an InputStream to read the file, so it doesn't load all the contents into
+  memory at once."
+  [^java.io.File file]
+  {:pre  [(instance? java.io.File file)]
+   :post [(string? %)]}
+  (digest/sha-256 file))
+
 (defn bounded-memoize
   "Similar to memoize, but the cache will be reset if the number of entries
   exceeds the specified `bound`."
@@ -1068,11 +1093,25 @@ to be a zipper."
            ~@(interleave (repeat g) (map pstep forms))]
        ~g)))
 
+(defn- port-open?
+  [port]
+  (try
+    (with-open [_ (java.net.ServerSocket. port)]
+      port)
+    (catch java.net.BindException e
+      (when-not (re-find #"already in use" (.getMessage e))
+        (throw e)))))
+
 (defn open-port-num
-  "Returns a currently open port number"
+  "Returns a currently open port number in the traditional ephemeral port range
+  of 49152 through 65535."
   []
-  (with-open [s (java.net.ServerSocket. 0)]
-    (.getLocalPort s)))
+  (let [lo 49152
+        hi 65536] ; one higher because the upper limit is exclusive
+    (if-let [open-port (some port-open? (shuffle (range lo hi)))]
+      open-port
+      (throw (java.net.BindException.
+               "All ephemeral ports are already in use (Bind failed)")))))
 
 (defmacro assoc-if-new
   "Assocs the provided values with the corresponding keys if and only
diff --git a/test/puppetlabs/kitchensink/classpath_test.clj b/test/puppetlabs/kitchensink/classpath_test.clj
index 7e85eb1..c280d2c 100644
--- a/test/puppetlabs/kitchensink/classpath_test.clj
+++ b/test/puppetlabs/kitchensink/classpath_test.clj
@@ -4,16 +4,14 @@
   (:import (java.net URL)))
 
 (deftest with-additional-classpath-entries-test
-  (let [paths ["/foo" "/bar"]
-        get-urls #(into #{}
-                        (.getURLs (.getContextClassLoader (Thread/currentThread))))]
+  (let [paths ["classpath-test"]
+        get-resource #(-> (Thread/currentThread)
+                          .getContextClassLoader
+                          (.getResource "does-not-exist-anywhere-else"))]
     (with-additional-classpath-entries
       paths
-      (testing "classloader now includes the new paths"
-        (let [urls (get-urls)]
-          (is (contains? urls (URL. "file:/foo")))
-          (is (contains? urls (URL. "file:/bar"))))))
-    (testing "classloader has been restored to its previous state"
-      (let [urls (get-urls)]
-        (is (not (contains? urls (URL. "file:/foo"))))
-        (is (not (contains? urls (URL. "file:/bar"))))))))
+      (testing "classloader now includes the new path"
+        (is (get-resource))))
+
+    (testing "classloader no longer includes the new path"
+      (is (not (get-resource))))))
diff --git a/test/puppetlabs/kitchensink/core_test.clj b/test/puppetlabs/kitchensink/core_test.clj
index 7c85cf4..85250f0 100644
--- a/test/puppetlabs/kitchensink/core_test.clj
+++ b/test/puppetlabs/kitchensink/core_test.clj
@@ -7,7 +7,8 @@
             [clj-time.core :as t]
             [puppetlabs.kitchensink.testutils :as testutils]
             [clojure.zip :as zip])
-  (:import (java.util ArrayList)))
+  (:import (java.util ArrayList)
+           (java.io ByteArrayInputStream)))
 
 (deftest array?-test
   (testing "array?"
@@ -416,7 +417,49 @@
 
     (testing "should produce the correct hash"
       (is (= "8843d7f92416211de9ebb963ff4ce28125932878"
-            (utf8-string->sha1 "foobar"))))))
+             (utf8-string->sha1 "foobar")))))
+
+  (testing "Computing a SHA-256 for a UTF-8 string"
+    (testing "should fail if not passed a string"
+      (is (thrown? AssertionError (utf8-string->sha256 1234))))
+
+    (testing "should produce a stable hash"
+      (is (= (utf8-string->sha256 "foobar")
+             (utf8-string->sha256 "foobar"))))
+
+    (testing "should produce the correct hash"
+      (is (= "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"
+             (utf8-string->sha256 "foobar"))))))
+
+(deftest stream-hashing
+  (testing "Computing a SHA-256 hash for an input stream"
+    (testing "should fail if not passed an input stream"
+      (is (thrown? AssertionError (stream->sha256 "what"))))
+
+    (let [stream-fn #(ByteArrayInputStream. (.getBytes "foobar" "UTF-8"))]
+      (testing "should produce a stable hash"
+        (is (= (stream->sha256 (stream-fn))
+               (stream->sha256 (stream-fn)))))
+
+      (testing "should produce the correct hash"
+        (is (= "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"
+               (stream->sha256 (stream-fn))))))))
+
+(deftest file-hashing
+  (testing "Computing a SHA-256 hash for a file"
+    (testing "should fail if not passed a file"
+      (is (thrown? AssertionError (file->sha256 "what"))))
+
+    (let [f (temp-file "sha256" ".txt")]
+      (spit f "foobar")
+
+      (testing "should produce a stable hash"
+        (is (= (file->sha256 f)
+               (file->sha256 f))))
+
+      (testing "should produce the correct hash"
+        (is (= "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"
+               (file->sha256 f)))))))
 
 (deftest temp-file-name-test
   (testing "The file should not exist."
@@ -749,7 +792,7 @@
                (with-timeout 1 false
                  (wait-return 1005 true))))))))
 
-(deftest open-port-num-test
+(deftest ^:slow open-port-num-test
   (let [port-in-use (open-port-num)]
     (with-open [s (java.net.ServerSocket. port-in-use)]
       (let [open-ports (set (take 60000 (repeatedly open-port-num)))]