diff --git a/test/py/tests/test_ums.py b/test/py/tests/test_ums.py
index a137221c7a5b..f482cfeca1b1 100644
--- a/test/py/tests/test_ums.py
+++ b/test/py/tests/test_ums.py
@@ -2,13 +2,17 @@
 #
 # SPDX-License-Identifier: GPL-2.0
 
-# Test U-Boot's "ums" command. At present, this test only ensures that a UMS
-# device can be enumerated by the host/test machine. In the future, this test
-# should be enhanced to validate disk IO.
+# Test U-Boot's "ums" command. The test starts UMS in U-Boot, waits for USB
+# device enumeration on the host, reads a small block of data from the UMS
+# block device, optionally mounts a partition and performs filesystem-based
+# read/write tests, and finally aborts the "ums" command in U-Boot.
 
 import os
+import os.path
 import pytest
+import re
 import time
+import u_boot_utils
 
 '''
 Note: This test relies on:
@@ -17,13 +21,36 @@ a) boardenv_* to contain configuration values to define which USB ports are
 available for testing. Without this, this test will be automatically skipped.
 For example:
 
+# Leave this list empty if you have no block_devs below with writable
+# partitions defined.
+env__mount_points = (
+    "/mnt/ubtest-mnt-p2371-2180-na",
+)
+
 env__usb_dev_ports = (
-    {'tgt_usb_ctlr': '0', 'host_ums_dev_node': '/dev/disk/by-path/pci-0000:00:14.0-usb-0:13:1.0-scsi-0:0:0:0'},
+    {
+        "tgt_usb_ctlr": "0",
+        "host_ums_dev_node": "/dev/disk/by-path/pci-0000:00:14.0-usb-0:13:1.0-scsi-0:0:0:0",
+    },
 )
 
 env__block_devs = (
-    {'type': 'mmc', 'id': '0'}, # eMMC; always present
-    {'type': 'mmc', 'id': '1'}, # SD card; present since I plugged one in
+    # eMMC; always present
+    {
+        "type": "mmc",
+        "id": "0",
+        # The following two properties are optional.
+        # If present, the partition will be mounted and a file written-to and
+        # read-from it. If missing, only a simple block read test will be
+        # performed.
+        "writable_fs_partition": 1,
+        "writable_fs_subdir": "tmp/",
+    },
+    # SD card; present since I plugged one in
+    {
+        "type": "mmc",
+        "id": "1"
+    },
 )
 
 b) udev rules to set permissions on devices nodes, so that sudo is not
@@ -34,47 +61,42 @@ ACTION=="add", SUBSYSTEM=="block", SUBSYSTEMS=="usb", KERNELS=="3-13", MODE:="66
 (You may wish to change the group ID instead of setting the permissions wide
 open. All that matters is that the user ID running the test can access the
 device.)
-'''
 
-def open_ums_device(host_ums_dev_node):
-    '''Attempt to open a device node, returning either the opened file handle,
-    or None on any error.'''
+c) /etc/fstab entries to allow the block device to be mounted without requiring
+root permissions. For example:
 
-    try:
-        return open(host_ums_dev_node, 'rb')
-    except:
-        return None
-
-def wait_for_ums_device(host_ums_dev_node):
-    '''Continually attempt to open the device node exported by the "ums"
-    command, and either return the opened file handle, or raise an exception
-    after a timeout.'''
-
-    for i in xrange(100):
-        fh = open_ums_device(host_ums_dev_node)
-        if fh:
-            return fh
-        time.sleep(0.1)
-    raise Exception('UMS device did not appear')
-
-def wait_for_ums_device_gone(host_ums_dev_node):
-    '''Continually attempt to open the device node exported by the "ums"
-    command, and either return once the device has disappeared, or raise an
-    exception if it does not before a timeout occurs.'''
-
-    for i in xrange(100):
-        fh = open_ums_device(host_ums_dev_node)
-        if not fh:
-            return
-        fh.close()
-        time.sleep(0.1)
-    raise Exception('UMS device did not disappear')
+/dev/disk/by-path/pci-0000:00:14.0-usb-0:13:1.0-scsi-0:0:0:0-part1 /mnt/ubtest-mnt-p2371-2180-na ext4 noauto,user,nosuid,nodev
+
+This entry is only needed if any block_devs above contain a
+writable_fs_partition value.
+'''
 
 @pytest.mark.buildconfigspec('cmd_usb_mass_storage')
 def test_ums(u_boot_console, env__usb_dev_port, env__block_devs):
     '''Test the "ums" command; the host system must be able to enumerate a UMS
-    device when "ums" is running, and this device must disappear when "ums" is
-    aborted.'''
+    device when "ums" is running, block and optionally file I/O are tested,
+    and this device must disappear when "ums" is aborted.
+
+    Args:
+        u_boot_console: A U-Boot console connection.
+        env__usb_dev_port: The single USB device-mode port specification on
+            which to run the test. See the file-level comment above for
+            details of the format.
+        env__block_devs: The list of block devices that the target U-Boot
+            device has attached. See the file-level comment above for details
+            of the format.
+
+    Returns:
+        Nothing.
+    '''
+
+    have_writable_fs_partition = 'writable_fs_partition' in env__block_devs[0]
+    if not have_writable_fs_partition:
+        # If 'writable_fs_subdir' is missing, we'll skip all parts of the
+        # testing which mount filesystems.
+        u_boot_console.log.warning(
+            'boardenv missing "writable_fs_partition"; ' +
+            'UMS testing will be limited.')
 
     tgt_usb_ctlr = env__usb_dev_port['tgt_usb_ctlr']
     host_ums_dev_node = env__usb_dev_port['host_ums_dev_node']
@@ -84,11 +106,129 @@ def test_ums(u_boot_console, env__usb_dev_port, env__block_devs):
     # device list here. We'll test each block device somewhere else.
     tgt_dev_type = env__block_devs[0]['type']
     tgt_dev_id = env__block_devs[0]['id']
+    if have_writable_fs_partition:
+        mount_point = u_boot_console.config.env['env__mount_points'][0]
+        mount_subdir = env__block_devs[0]['writable_fs_subdir']
+        part_num = env__block_devs[0]['writable_fs_partition']
+        host_ums_part_node = '%s-part%d' % (host_ums_dev_node, part_num)
+    else:
+        host_ums_part_node = host_ums_dev_node
+
+    test_f = u_boot_utils.PersistentRandomFile(u_boot_console, 'ums.bin',
+        1024 * 1024);
+    if have_writable_fs_partition:
+        mounted_test_fn = mount_point + '/' + mount_subdir + test_f.fn
+
+    def start_ums():
+        '''Start U-Boot's ums shell command.
+
+        This also waits for the host-side USB enumeration process to complete.
+
+        Args:
+            None.
+
+        Returns:
+            Nothing.
+        '''
+
+        u_boot_console.log.action(
+            'Starting long-running U-Boot ums shell command')
+        cmd = 'ums %s %s %s' % (tgt_usb_ctlr, tgt_dev_type, tgt_dev_id)
+        u_boot_console.run_command(cmd, wait_for_prompt=False)
+        u_boot_console.wait_for(re.compile('UMS: LUN.*[\r\n]'))
+        fh = u_boot_utils.wait_until_open_succeeds(host_ums_part_node)
+        u_boot_console.log.action('Reading raw data from UMS device')
+        fh.read(4096)
+        fh.close()
+
+    def mount():
+        '''Mount the block device that U-Boot exports.
+
+        Args:
+            None.
+
+        Returns:
+            Nothing.
+        '''
+
+        u_boot_console.log.action('Mounting exported UMS device')
+        cmd = ('/bin/mount', host_ums_part_node)
+        u_boot_utils.run_and_log(u_boot_console, cmd)
 
-    cmd = 'ums %s %s %s' % (tgt_usb_ctlr, tgt_dev_type, tgt_dev_id)
-    u_boot_console.run_command('ums 0 mmc 0', wait_for_prompt=False)
-    fh = wait_for_ums_device(host_ums_dev_node)
-    fh.read(4096)
-    fh.close()
-    u_boot_console.ctrlc()
-    wait_for_ums_device_gone(host_ums_dev_node)
+    def umount(ignore_errors):
+        '''Unmount the block device that U-Boot exports.
+
+        Args:
+            ignore_errors: Ignore any errors. This is useful if an error has
+                already been detected, and the code is performing best-effort
+                cleanup. In this case, we do not want to mask the original
+                error by "honoring" any new errors.
+
+        Returns:
+            Nothing.
+        '''
+
+        u_boot_console.log.action('Unmounting UMS device')
+        cmd = ('/bin/umount', host_ums_part_node)
+        u_boot_utils.run_and_log(u_boot_console, cmd, ignore_errors)
+
+    def stop_ums(ignore_errors):
+        '''Stop U-Boot's ums shell command from executing.
+
+        This also waits for the host-side USB de-enumeration process to
+        complete.
+
+        Args:
+            ignore_errors: Ignore any errors. This is useful if an error has
+                already been detected, and the code is performing best-effort
+                cleanup. In this case, we do not want to mask the original
+                error by "honoring" any new errors.
+
+        Returns:
+            Nothing.
+        '''
+
+        u_boot_console.log.action(
+            'Stopping long-running U-Boot ums shell command')
+        u_boot_console.ctrlc()
+        u_boot_utils.wait_until_file_open_fails(host_ums_part_node,
+            ignore_errors)
+
+    ignore_cleanup_errors = True
+    try:
+        start_ums()
+        if not have_writable_fs_partition:
+            # Skip filesystem-based testing if not configured
+            return
+        try:
+            mount()
+            u_boot_console.log.action('Writing test file via UMS')
+            cmd = ('rm', '-f', mounted_test_fn)
+            u_boot_utils.run_and_log(u_boot_console, cmd)
+            if os.path.exists(mounted_test_fn):
+                raise Exception('Could not rm target UMS test file')
+            cmd = ('cp', test_f.abs_fn, mounted_test_fn)
+            u_boot_utils.run_and_log(u_boot_console, cmd)
+            ignore_cleanup_errors = False
+        finally:
+            umount(ignore_errors=ignore_cleanup_errors)
+    finally:
+        stop_ums(ignore_errors=ignore_cleanup_errors)
+
+    ignore_cleanup_errors = True
+    try:
+        start_ums()
+        try:
+            mount()
+            u_boot_console.log.action('Reading test file back via UMS')
+            read_back_hash = u_boot_utils.md5sum_file(mounted_test_fn)
+            cmd = ('rm', '-f', mounted_test_fn)
+            u_boot_utils.run_and_log(u_boot_console, cmd)
+            ignore_cleanup_errors = False
+        finally:
+            umount(ignore_errors=ignore_cleanup_errors)
+    finally:
+        stop_ums(ignore_errors=ignore_cleanup_errors)
+
+    written_hash = test_f.content_hash
+    assert(written_hash == read_back_hash)
diff --git a/test/ums/README b/test/ums/README
deleted file mode 100644
index c80fbfefbf52..000000000000
--- a/test/ums/README
+++ /dev/null
@@ -1,30 +0,0 @@
-UMS test script.
-
-ums_gadget_test.sh
-==================
-
-Example usage:
-1. On the target:
-   create UMS exportable partitions (with e.g. gpt write), or specify a
-   partition number (PART_NUM) as "-" to use the entire device
-   ums 0 mmc 0
-2. On the host:
-   sudo test/ums/ums_gadget_test.sh VID PID PART_NUM [-f FILE_SYSTEM] [test_file]
-   e.g. sudo test/ums/ums_gadget_test.sh 0525 a4a5 6 -f vfat ./dat_14M.img
-
-... where:
-    VID - UMS device USB Vendor ID
-    PID - UMS device USB Product ID
-    PART_NUM - is the partition number on which UMS operates or "-" to use the
-	       whole device
-
-Information about available partitions on the target one can read with using
-the 'mmc part' or 'part list' commands.
-
-The partition num (PART_NUM) can be specified as '-' for using the whole device.
-
-The [-f FILE_SYSTEM] optional switch allows for formatting target partition to
-FILE_SYSTEM.
-
-The last, optional [test_file] parameter is for specifying the exact test file
-to use.
diff --git a/test/ums/ums_gadget_test.sh b/test/ums/ums_gadget_test.sh
deleted file mode 100755
index 9da486b266ce..000000000000
--- a/test/ums/ums_gadget_test.sh
+++ /dev/null
@@ -1,183 +0,0 @@
-#! /bin/bash
-
-# Copyright (C) 2014 Samsung Electronics
-# Lukasz Majewski <l.majewski@samsung.com>
-#
-# UMS operation test script
-#
-# SPDX-License-Identifier:	GPL-2.0+
-
-clear
-
-COLOUR_RED="\33[31m"
-COLOUR_GREEN="\33[32m"
-COLOUR_ORANGE="\33[33m"
-COLOUR_DEFAULT="\33[0m"
-
-DIR=./
-SUFFIX=img
-RCV_DIR=rcv/
-LOG_FILE=./log/log-`date +%d-%m-%Y_%H-%M-%S`
-
-cd `dirname $0`
-../dfu/dfu_gadget_test_init.sh 33M 97M
-
-cleanup () {
-    rm -rf $RCV_DIR $MNT_DIR
-}
-
-control_c()
-# run if user hits control-c
-{
-	echo -en "\n*** CTRL+C ***\n"
-	umount $MNT_DIR
-	cleanup
-	exit 0
-}
-
-# trap keyboard interrupt (control-c)
-trap control_c SIGINT
-
-die () {
-    printf "   $COLOUR_RED FAILED $COLOUR_DEFAULT \n"
-    cleanup
-    exit 1
-}
-
-calculate_md5sum () {
-    MD5SUM=`md5sum $1`
-    MD5SUM=`echo $MD5SUM | cut -d ' ' -f1`
-    echo "md5sum:"$MD5SUM
-}
-
-ums_test_file () {
-    printf "$COLOUR_GREEN========================================================================================= $COLOUR_DEFAULT\n"
-    printf "File:$COLOUR_GREEN %s $COLOUR_DEFAULT\n" $1
-
-    mount /dev/$MEM_DEV $MNT_DIR
-    if [ -f $MNT_DIR/dat_* ]; then
-	rm $MNT_DIR/dat_*
-    fi
-
-    cp ./$1 $MNT_DIR
-
-    while true; do
-	umount $MNT_DIR > /dev/null 2>&1
-	if [ $? -eq 0 ]; then
-	    break
-	fi
-	printf "$COLOUR_ORANGE\tSleeping to wait for umount...$COLOUR_DEFAULT\n"
-	sleep 1
-    done
-
-    echo -n "TX: "
-    calculate_md5sum $1
-
-    MD5_TX=$MD5SUM
-    sleep 1
-    N_FILE=$DIR$RCV_DIR${1:2}"_rcv"
-
-    mount /dev/$MEM_DEV $MNT_DIR
-    cp $MNT_DIR/$1 $N_FILE || die $?
-    rm $MNT_DIR/$1
-    umount $MNT_DIR
-
-    echo -n "RX: "
-    calculate_md5sum $N_FILE
-    MD5_RX=$MD5SUM
-
-    if [ "$MD5_TX" == "$MD5_RX" ]; then
-	printf "   $COLOUR_GREEN -------> OK $COLOUR_DEFAULT \n"
-    else
-	printf "   $COLOUR_RED -------> FAILED $COLOUR_DEFAULT \n"
-	cleanup
-	exit 1
-    fi
-}
-
-printf "$COLOUR_GREEN========================================================================================= $COLOUR_DEFAULT\n"
-echo "U-boot UMS test program"
-
-if [ $EUID -ne 0 ]; then
-   echo "You must be root to do this." 1>&2
-   exit 100
-fi
-
-if [ $# -lt 3 ]; then
-    echo "Wrong number of arguments"
-    echo "Example:"
-    echo "sudo ./ums_gadget_test.sh VID PID PART_NUM [-f ext4] [test_file]"
-    die
-fi
-
-MNT_DIR="/mnt/tmp-ums-test"
-
-VID=$1; shift
-PID=$1; shift
-PART_NUM=$1; shift
-
-if [ "$1" == "-f" ]; then
-    shift
-    FS_TO_FORMAT=$1; shift
-fi
-
-TEST_FILE=$1
-
-for f in `find /sys -type f -name idProduct`; do
-     d=`dirname ${f}`
-     if [ `cat ${d}/idVendor` != "${VID}" ]; then
-	 continue
-     fi
-     if [ `cat ${d}/idProduct` != "${PID}" ]; then
-	 continue
-     fi
-     USB_DEV=${d}
-     break
-done
-
-if [ -z "${USB_DEV}" ]; then
-     echo "Connect target"
-     echo "e.g. ums 0 mmc 0"
-     exit 1
-fi
-
-MEM_DEV=`find $USB_DEV -type d -name "sd[a-z]" | awk -F/ '{print $(NF)}' -`
-
-mkdir -p $RCV_DIR
-if [ ! -d $MNT_DIR ]; then
-    mkdir -p $MNT_DIR
-fi
-
-if [ "$PART_NUM" == "-" ]; then
-    PART_NUM=""
-fi
-MEM_DEV=$MEM_DEV$PART_NUM
-
-if [ -n "$FS_TO_FORMAT" ]; then
-    echo -n "Formatting partition /dev/$MEM_DEV to $FS_TO_FORMAT"
-    mkfs -t $FS_TO_FORMAT /dev/$MEM_DEV > /dev/null 2>&1
-    if [ $? -eq 0 ]; then
-	printf " $COLOUR_GREEN DONE $COLOUR_DEFAULT \n"
-    else
-	die
-    fi
-fi
-
-printf "Mount: /dev/$MEM_DEV \n"
-
-if [ -n "$TEST_FILE" ]; then
-    if [ ! -e $TEST_FILE ]; then
-	echo "No file: $TEST_FILE"
-	die
-    fi
-    ums_test_file $TEST_FILE
-else
-    for file in $DIR*.$SUFFIX
-    do
-	ums_test_file $file
-    done
-fi
-
-cleanup
-
-exit 0
