diff --git a/test/Controller_test.py b/test/Controller_test.py
index 4b1992c98daa32abac7c47ffa35f9c806f773465..32caec56d3c9afb1fb328cf8eb823a6107a381b4 100644
--- a/test/Controller_test.py
+++ b/test/Controller_test.py
@@ -3,6 +3,18 @@ from geomagio import Controller, TimeseriesFactory
 from geomagio.algorithm import Algorithm
 from nose.tools import assert_is_instance
 
+# needed to read outputs generated by Controller and test data
+from geomagio.iaga2002 import IAGA2002Factory
+# needed to emulate geomag.py script
+from geomagio.Controller import _main, parse_args
+# needed to copy SqDistAlgorithm statefile
+from shutil import copy
+# needed to determine a valid (and writable) temp folder
+from tempfile import gettempdir
+
+from numpy.testing import assert_allclose
+from obspy.core import UTCDateTime
+
 
 def test_controller():
     """Controller_test.test_controller()
@@ -17,3 +29,161 @@ def test_controller():
     assert_is_instance(controller._inputFactory, TimeseriesFactory)
     assert_is_instance(controller._outputFactory, TimeseriesFactory)
     assert_is_instance(controller._algorithm, Algorithm)
+
+
+def test_controller_update_sqdist():
+    """Controller_test.test_controller_update_sqdist().
+
+    This is an end-to-end test of the Controller, more-or-less how it would be
+    invoked via the geomag.py command line script. We specifically test the
+    Controller's run() logic using the SqDistAlgoritm and carefully
+    constructed inputs since this is one of the most complicated anticipated
+    use-cases. Some liberties have been taken to avoid repeatedly parsing all
+    arguments or reloading the interpreter.
+
+    This test also takes advantage of the fact that args.realtime is processed
+    at the end of main() before _main() is called.  This test explicitly
+    sets starttime, endtime, and realtime argument values to override what
+    may otherwise be expected during normal command line operations.
+    """
+    # define folder for testing
+    tmp_dir = gettempdir()
+
+    # create list of string command line arguments
+    fake_argv = [
+        '--input', 'iaga2002',
+        '--input-url',
+          'file://etc/controller/{obs}{date:%Y%m%d}_XYZF_{t}{i}.{i}',
+        '--observatory', 'BOU',
+        '--algorithm', 'sqdist',
+        '--sqdist-m', '1440',
+        '--sqdist-alpha', '2.3148e-5',
+        '--sqdist-gamma', '3.3333e-2',
+        '--sqdist-smooth', '180',
+        '--inchannels', 'X', 'Y', 'Z', 'F',
+        '--interval', 'minute',
+        '--rename-output-channel', 'H_Dist', 'MDT',
+        '--rename-output-channel', 'H_SQ', 'MSQ',
+        '--rename-output-channel', 'H_SV', 'MSV',
+        '--rename-output-channel', 'H_Sigma', 'MSS',
+        '--outchannels', 'MDT', 'MSQ', 'MSV', 'MSS',
+        '--sqdist-mag',
+        '--sqdist-statefile', tmp_dir + '/sqdistBOU_h_state.json',
+        '--type', 'variation',
+        '--output', 'iaga2002',
+        '--output-url',
+          'file://' + tmp_dir + '/{obs}{date:%Y%m%d}_DQVS_{t}{i}.{i}',
+        '--realtime', '600'
+    ]
+    # parse arguments and create initial args object
+    args = parse_args(fake_argv)
+
+    # read in test and latest output and compare
+    actual_factory = IAGA2002Factory(
+        urlTemplate=('file://' +
+            tmp_dir + '/{obs}{date:%Y%m%d}_DQVS_{t}{i}.{i}'),
+        urlInterval=86400,
+        observatory='BOU',
+        channels=['MDT', 'MSQ', 'MSV', 'MSS']
+    )
+    expected_factory = IAGA2002Factory(
+        urlTemplate='url template, individual tests change the template below',
+        urlInterval=86400,
+        observatory='BOU',
+        channels=['MDT', 'MSQ', 'MSV', 'MSS']
+    )
+
+    # setup test data
+    # copy SqDistAlgorithm statefile and empty DQVS output file to tmp folder
+    copy('etc/controller/sqdistBOU_h_state.json',
+        tmp_dir)
+    copy('etc/controller/bou20181024_DQVS_test0_vmin.min',
+        tmp_dir + '/bou20181024_DQVS_vmin.min')
+
+    # TEST 1 - include a gap at end that is less than realtime (10 minutes),
+    # expect sqdist not to project SQ/SV/SS
+    starttime1 = args.starttime = UTCDateTime('2018-10-24T00:00:00Z')
+    endtime1 = args.endtime = UTCDateTime('2018-10-24T00:19:00Z')
+    _main(args)
+    # compare results
+    actual = actual_factory.get_timeseries(
+            starttime=starttime1, endtime=endtime1)
+    expected_factory.urlTemplate = \
+            'file://etc/controller/{obs}{date:%Y%m%d}_DQVS_test1_{t}{i}.{i}'
+    expected = expected_factory.get_timeseries(
+            starttime=starttime1, endtime=endtime1)
+    assert_allclose(actual, expected)
+
+    # TEST 2 - start after next_starttime (00:10),
+    # expect SQDist to project sq/sv/ss values over gap,
+    # then process until last gap starting at 00:38
+    args.startime = UTCDateTime('2018-10-24T00:20:00Z')
+    endtime2 = args.endtime = UTCDateTime('2018-10-24T00:39:00Z')
+    _main(args)
+    # compare results
+    actual = actual_factory.get_timeseries(
+            starttime=starttime1, endtime=endtime2)
+    expected_factory.urlTemplate = \
+            'file://etc/controller/{obs}{date:%Y%m%d}_DQVS_test2_{t}{i}.{i}'
+    expected = expected_factory.get_timeseries(
+            starttime=starttime1, endtime=endtime2)
+    assert_allclose(actual, expected)
+
+    # TEST 3 - start after next_starttime (00:38),
+    # expect SQDist to project over gap,
+    # then process until last gap starting at 00:58
+    args.starttime = UTCDateTime('2018-10-24T00:40:00Z')
+    endtime3 = args.endtime = UTCDateTime('2018-10-24T00:59:00Z')
+    _main(args)
+    # compare results
+    actual = actual_factory.get_timeseries(
+            starttime=starttime1, endtime=endtime3)
+    expected_factory.urlTemplate = \
+            'file://etc/controller/{obs}{date:%Y%m%d}_DQVS_test3_{t}{i}.{i}'
+    expected = expected_factory.get_timeseries(
+            starttime=starttime1, endtime=endtime3)
+    assert_allclose(actual, expected)
+
+    # TEST 4 - start after next_starttime (00:58),
+    # exptect SQDist to project over gap,
+    # then process until last gap starting at 01:16
+    args.starttime = UTCDateTime('2018-10-24T01:00:00Z')
+    endtime4 = args.endtime = UTCDateTime('2018-10-24T01:19:00Z')
+    _main(args)
+    # compare results
+    actual = actual_factory.get_timeseries(
+            starttime=starttime1, endtime=endtime4)
+    expected_factory.urlTemplate = \
+            'file://etc/controller/{obs}{date:%Y%m%d}_DQVS_test4_{t}{i}.{i}'
+    expected = expected_factory.get_timeseries(
+            starttime=starttime1, endtime=endtime4)
+    assert_allclose(actual, expected)
+
+    # TEST 5 - start after next_starttime (01:16),
+    # expect SQDist to project until beginning of realtime gap,
+    # starting at 01:30 (01:39 - 600 seconds)
+    args.starttime = UTCDateTime('2018-10-24T01:20:00Z')
+    endtime5 = args.endtime = UTCDateTime('2018-10-24T01:39:00Z')
+    _main(args)
+    # compare results
+    actual = actual_factory.get_timeseries(
+            starttime=starttime1, endtime=endtime5)
+    expected_factory.urlTemplate = \
+            'file://etc/controller/{obs}{date:%Y%m%d}_DQVS_test5_{t}{i}.{i}'
+    expected = expected_factory.get_timeseries(
+            starttime=starttime1, endtime=endtime5)
+    assert_allclose(actual, expected)
+
+    # TEST 6 - set starttime before next_starttime (which is 01:30)
+    # expect sqdist to pick up where it left off
+    args.starttime = UTCDateTime('2018-10-24T01:20:00Z')
+    endtime6 = args.endtime = UTCDateTime('2018-10-24T01:59:00Z')
+    _main(args)
+    # compare results
+    actual = actual_factory.get_timeseries(
+            starttime=starttime1, endtime=endtime6)
+    expected_factory.urlTemplate = \
+            'file://etc/controller/{obs}{date:%Y%m%d}_DQVS_test6_{t}{i}.{i}'
+    expected = expected_factory.get_timeseries(
+            starttime=starttime1, endtime=endtime6)
+    assert_allclose(actual, expected)