diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6524a3fd7cb85e8093c49e99c381fdbc7c0230c9..d74e7c03214c69162c040c133e954dd3f759604c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -20,17 +20,71 @@ stages:
 before_script:
 - pip install -e .[dev]
 
+linting_python2:
+  image: python:2.7
+  tags:
+  - passoft
+  stage: test
+  script:
+  - flake8 --ignore=E722,E712 data2passcal
+  - flake8 --ignore=E722,E712 tests
 
-python2:
+linting_python3:
+  image: python:3.6
+  tags:
+  - passoft
+  stage: test
+  script:
+  - flake8 --ignore=E722,E712 data2passcal
+  - flake8 --ignore=E722,E712 tests
+
+python2.7:
   image: python:2.7
   tags:
   - passoft
   stage: test
-  script: tox -e py27
+  script:
+  - cd tests/
+  - pwd
+  - ls
+  - python -m unittest test_data2passcal
 
-python3:
+python3.5:
+  image: python:3.5
+  tags:
+  - passoft
+  stage: test
+  script:
+  - pwd
+  - ls
+  - python -m unittest
+
+python3.6:
   image: python:3.6
   tags:
   - passoft
   stage: test
-  script: tox -e py36
+  script:
+  - pwd
+  - ls
+  - python -m unittest
+
+python3.7:
+  image: python:3.7
+  tags:
+  - passoft
+  stage: test
+  script:
+  - pwd
+  - ls
+  - python -m unittest
+
+python3.8:
+  image: python:3.8
+  tags:
+  - passoft
+  stage: test
+  script:
+  - pwd
+  - ls
+  - python -m unittest
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 8029b2489bd99e83141f2a6e55f9aeb1f9c6f30c..207d6dd429fa08cf357717a87a587314d3012b29 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -10,4 +10,4 @@ Development Lead
 Contributors
 ------------
 
-None yet. Why not be the first?
+* Maeva Pourpoint <software-support@passcal.nmt.edu>
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index e8536cc925d7042cd527fed67034e6b6781936cf..69b8d731421c58d03df0b213f114b991d723a89d 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -45,7 +45,8 @@ articles, and such.
 Submit Feedback
 ~~~~~~~~~~~~~~~
 
-The best way to send feedback is to file an issue at https://git.passcal.nmt.edu/passoft/data2passcal/issues.
+The best way to send feedback is to file an issue at
+https://git.passcal.nmt.edu/passoft/data2passcal/issues.
 
 If you are proposing a feature:
 
@@ -59,31 +60,32 @@ Get Started!
 
 Ready to contribute? Here's how to set up `data2passcal` for local development.
 
-1. Cone the `data2passcal` repo::
+1. Clone the `data2passcal` repo:
 
     $ git clone https://git.passcal.nmt.edu/passoft/data2passcal.git
 
-3. Install your local copy::
+2. Install your local copy:
 
     $ pip install -e .[dev]
 
-4. Create a branch for local development::
+3. Create a branch for local development:
 
     $ git checkout -b name-of-your-bugfix-or-feature
 
    Now you can make your changes locally.
 
-5. When you're done making changes, check that your changes pass the
-   tests::
+4. When you're done making changes, check that your changes pass the
+   tests:
 
-    $ python setup.py test
-6. Commit your changes and push your branch to GitHub::
+    $ python -m unittest (or python -m unittest tests.test_data2passcal under Python2)
+
+5. Commit your changes and push your branch to GitHub:
 
     $ git add .
     $ git commit -m "Your detailed description of your changes."
     $ git push origin name-of-your-bugfix-or-feature
 
-7. Submit a merge request through the Gitlab website.
+6. Submit a merge request through the Gitlab website.
 
 Pull Request Guidelines
 -----------------------
@@ -94,7 +96,7 @@ Before you submit a merge request, check that it meets these guidelines:
 2. If the merge request adds functionality, the docs should be updated. Put
    your new functionality into a function with a docstring, and add the
    feature to the list in README.rst.
-3. The pull request should work for Python 2.7
+3. The pull request should work for Python 2.7 and Python 3.[5,6,7,8]
 
 Tips
 ----
@@ -111,4 +113,3 @@ Then run::
 
 $ git push
 $ git push --tags
-
diff --git a/HISTORY.rst b/HISTORY.rst
index a94f3900674ff45112c0d5377c4db9759781243d..bc694a413f909aeeab250acdc5351d3444f184ed 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -10,3 +10,39 @@ History
 2018.228 (2018-08-16)
 ------------------
 * Updated to work with python 2 and 3
+
+2020.093 (2020-04-02)
+------------------
+* Updated to work with python 2 and 3.
+* Resources used: Python-Future project and  Futurize tool.
+* Changes tested using unittest module on individual test methods(tests.test_data2passcal)
+* Changes tested against real seismic dataset (AB.2002 - 2012)
+
+2020.107 (2020-04-16)
+------------------
+* Added some unit tests to ensure basic functionality of data2passcal
+* Created configuration file (.ini file) to store and access ftp and test related variables
+* Updated list of platform specific dependencies to be installed when installing data2passcal in dev mode (see setup.py)
+* Installed and tested data2passcal against Python2.7 and Python3.6 using tox
+* Formatted Python code to conform to the PEP8 style guide (exception: E722, E712, E501)
+
+2020.114 (2020-04-23)
+------------------
+* Created conda packages for "data2passcal" that can run on Python2.7 and Python3.6
+=> To create package:
+   - clone the "data2passcal" repo
+   - "cd" in "data2passcal" directory
+   - run "conda-build ."
+=> To install locally with dependencies:
+   - run "conda install -c ${CONDA_PREFIX}/conda-bld/ data2passcal"
+
+2020.119 (2020-04-28)
+------------------
+* Created conda packages for "data2passcal" that can run on Python3.[5,7,8]
+* Tested data2passcal against Python3.[5,7,8] using tox
+* Updated .gitlab-ci.yml to run a linter and unit tests for Python2 and Python3.[5,6,7,8] in GitLab CI pipeline
+
+2020.213 (2020-07-31)
+------------------
+* Updated meta.yaml to build architecture and platform independent package
+* Updated mock unit tests
diff --git a/MANIFEST.in b/MANIFEST.in
index 965b2dda7db7c49f68857dc3aea9af37e30a745e..2387d6e279b924229fa0c8c485eeeebf49784996 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -5,7 +5,5 @@ include LICENSE
 include README.rst
 
 recursive-include tests *
-recursive-exclude * __pycache__
-recursive-exclude * *.py[co]
 
-recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif
+recursive-include docs *.rst conf.py
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 11b99c413bc5977bb09368c418fbdb440c9fd4fe..0000000000000000000000000000000000000000
--- a/Makefile
+++ /dev/null
@@ -1,89 +0,0 @@
-.PHONY: clean clean-test clean-pyc clean-build docs help
-.DEFAULT_GOAL := help
-
-define BROWSER_PYSCRIPT
-import os, webbrowser, sys
-
-try:
-	from urllib import pathname2url
-except:
-	from urllib.request import pathname2url
-
-webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
-endef
-export BROWSER_PYSCRIPT
-
-define PRINT_HELP_PYSCRIPT
-import re, sys
-
-for line in sys.stdin:
-	match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line)
-	if match:
-		target, help = match.groups()
-		print("%-20s %s" % (target, help))
-endef
-export PRINT_HELP_PYSCRIPT
-
-BROWSER := python -c "$$BROWSER_PYSCRIPT"
-
-help:
-	@python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
-
-clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
-
-clean-build: ## remove build artifacts
-	rm -fr build/
-	rm -fr dist/
-	rm -fr .eggs/
-	find . -name '*.egg-info' -exec rm -fr {} +
-	find . -name '*.egg' -exec rm -f {} +
-
-clean-pyc: ## remove Python file artifacts
-	find . -name '*.pyc' -exec rm -f {} +
-	find . -name '*.pyo' -exec rm -f {} +
-	find . -name '*~' -exec rm -f {} +
-	find . -name '__pycache__' -exec rm -fr {} +
-
-clean-test: ## remove test and coverage artifacts
-	rm -fr .tox/
-	rm -f .coverage
-	rm -fr htmlcov/
-	rm -fr .pytest_cache
-
-lint: ## check style with flake8
-	flake8 data2passcal tests
-
-test: ## run tests quickly with the default Python
-	python setup.py test
-
-
-test-all: ## run tests on every Python version with tox
-	tox
-
-coverage: ## check code coverage quickly with the default Python
-	coverage run --source data2passcal setup.py test
-	coverage report -m
-	coverage html
-	$(BROWSER) htmlcov/index.html
-
-docs: ## generate Sphinx HTML documentation, including API docs
-	rm -f docs/data2passcal.rst
-	rm -f docs/modules.rst
-	sphinx-apidoc -o docs/ data2passcal
-	$(MAKE) -C docs clean
-	$(MAKE) -C docs html
-	$(BROWSER) docs/_build/html/index.html
-
-servedocs: docs ## compile the docs watching for changes
-	watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D .
-
-release: dist ## package and upload a release
-	twine upload dist/*
-
-dist: clean ## builds source and wheel package
-	python setup.py sdist
-	python setup.py bdist_wheel
-	ls -l dist
-
-install: clean ## install the package to the active Python's site-packages
-	python setup.py install
diff --git a/README.rst b/README.rst
index 1d6dee089e41341f1618e01f5a91e7ee72a5c3e0..0b4fbe637614c42f65ae7fa28b8969d817cc5251 100644
--- a/README.rst
+++ b/README.rst
@@ -2,23 +2,9 @@
 data2passcal
 ============
 
+* Description: Send MSEED files ready for archival at the DMC to PASSCAL's QC system
+               Only accept day-long MSEED files.
 
-Prepare SEED data for shipment to PASSCAL.
-
+* Usage: data2passcal dir
 
 * Free software: GNU General Public License v3 (GPLv3)
-
-
-
-Features
---------
-
-* TODO
-
-Credits
--------
-
-This package was created with Cookiecutter_ and the `passoft/cookiecutter`_ project template.
-
-.. _Cookiecutter: https://github.com/audreyr/cookiecutter
-.. _`passoft/cookiecutter`: https://git.passcal.nmt.edu/passoft/cookiecutter
diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml
index 260432777941989967c8ee02178c86241ee4d03c..f3415cb81340c77cf2e5185a76631b57e2ab1dd9 100644
--- a/conda.recipe/meta.yaml
+++ b/conda.recipe/meta.yaml
@@ -1,24 +1,32 @@
 package:
   name: data2passcal
-  version: 2014.125
+  version: 2020.213
 
 source:
-  path: ..
+  path: ../
 
 build:
-  # If the installation is complex, or different between Unix and Windows, use
-  # separate bld.bat and build.sh files instead of this key.  Add the line
-  # "skip: True  # [py<35]" (for example) to limit to Python 3.5 and newer, or
-  # "skip: True  # [not win]" to limit to Windows.
-  script: python setup.py install --single-version-externally-managed --record=record.txt
+  noarch: python
+  number: 1
+  script: {{ PYTHON }} -m pip install . --no-deps -vv
 
 requirements:
   build:
-    - python
-    - setuptools
+    - python >=2.7
+    - pip
   run:
-    - python
+    - python >=2.7
+
+test:
+  requires:
+    - mock # [py<33]
+  source_files:
+    - tests
+  commands:
+    - python -m unittest tests.test_data2passcal
 
 about:
   home: https://git.passcal.nmt.edu/passoft/data2passcal
-  summary: Prepare SEED data for shipment to PASSCAL.
+  license: GPLv3
+  license_file: LICENSE
+  summary: Send MSEED files ready for archival at the DMC to PASSCAL's QC system
diff --git a/data2passcal/__init__.py b/data2passcal/__init__.py
index 1636f688fe53d37abbe1eb0fb8f667675ef3b664..a49f05ebad00ac286528b632fcd8b03962ed1726 100644
--- a/data2passcal/__init__.py
+++ b/data2passcal/__init__.py
@@ -4,4 +4,4 @@
 
 __author__ = """IRIS PASSCAL"""
 __email__ = 'software-support@passcal.nmt.edu'
-__version__ = '2018.228'
+__version__ = '2020.213'
diff --git a/data2passcal/data2passcal.py b/data2passcal/data2passcal.py
index 7632678c2c3cef869cf1a7490bd7f69fbbd63b97..4b97b5c2f5dddde2916c4efd92e8755847d4e8a5 100644
--- a/data2passcal/data2passcal.py
+++ b/data2passcal/data2passcal.py
@@ -4,23 +4,40 @@ data2passcal
 
 Ship miniseed data to passcal via ftp for qc before archival at DMC DMS
 Lloyd Carothers IRIS/PASSCAL
+
+Maeva Pourpoint
+July 2020
+Updates to work under Python 2 and 3.
+Unit tests to ensure basic functionality.
+Code cleanup to conform to the PEP8 style guide.
+Directory cleanup (remove unused files introduced by Cookiecutter).
+Packaged with conda.
 '''
-from __future__ import print_function
-VERSION = '2018.228'
+from __future__ import division, print_function
 
-import ftplib
-import sys, os, signal, struct, pickle, logging
-from time import time, sleep
 import datetime
+import ftplib
+import logging
+import os
+import pickle
+import re
+import signal
+import socket
+import struct
+import sys
 
-#TESTMODE = True
-TESTMODE = False
-# Verbosity of console log from logging module from DEBUG to CRITICAL
-DEBUG =  logging.DEBUG
-# After transmission or on interrupt try to ftp logfile to PIC
-#SEND_LOGFILE = True
+from io import open
+from time import sleep, time
 
-#FTP related
+try:
+    from urllib.request import urlopen
+except ImportError:
+    from urllib2 import urlopen
+
+VERSION = '2020.213'
+
+
+# FTP related
 FTP_IP = '129.138.26.29'
 FTP_HOST = 'qc.passcal.nmt.edu'
 FTP_USER = 'ftp'
@@ -28,64 +45,70 @@ FTP_PASSWORD = 'data2passcal'
 FTP_DIR = 'AUTO/MSEED'
 FTP_TIMEOUT = 120
 FTP_RECONNECT_WAIT = 60
-if TESTMODE:
-    FTP_DIR = 'AUTO/TEST'
-    FTP_TIMEOUT = 5
-    FTP_RECONNECT_WAIT = 5
 FTP_BLOCKSIZE = 8192
-#number of time to try to open the ftp connection
-#FTP_CONNECT_ATTEMPTS = 3
-# testing will retry for a week. Why not?
-FTP_CONNECT_ATTEMPTS = 60*60*24*7  / FTP_RECONNECT_WAIT
-#number of times a single file with try to be sent
+# number of time to try to open the ftp connection
+FTP_CONNECT_ATTEMPTS = 60 * 60 * 24 * 7 / FTP_RECONNECT_WAIT
+# number of times a single file with try to be sent
 FTP_SEND_ATTEMPTS = 3
 # debug level of ftplib, 0-3
 FTP_DEBUG_LEVEL = 0
-#store the files sent in ~/data2passcal.sent
-SENTFILE = os.path.join( os.path.expanduser('~'), '.data2passcal.sent')
-# If this file exists open it, incorporate, and save to new name, and delete old
-SENTFILE_OLD = os.path.join( os.path.expanduser('~'), '.send2passcal.sent')
+
+# Files related
 LOGFILE = 'data2passcal.log'
+# store the files sent in ~/data2passcal.sent
+SENTFILE = os.path.join(os.path.expanduser('~'), '.data2passcal.sent')
+# If this file exists open it, incorporate and save to new name, and delete old
+SENTFILE_OLD = os.path.join(os.path.expanduser('~'), '.send2passcal.sent')
 
 HELP = '''
 data2passcal
 VERSION: %s
 Usage: data2passcal dir
-  data2passcal is a utility that sends MSEED files ready for archival at the DMC to PASSCAL's QC system by:
-  Scans all files below directory dir.
-  Filters out non-miniseed files, by inspecting the first blockette of each file.
-  Sends the files to the automated system for ingestion into the QC system via ftp.
+  data2passcal is a utility that sends day-long MSEED files ready for archival
+  at the DMC to PASSCAL's QC system by:
+  - Scanning all files below directory dir.
+  - Filtering out non-miniseed files, by inspecting the first blockette of each
+    file.
+  - Sending the files to the automated system for ingestion into the QC system
+    via ftp.
   You can send a SIGTERM (ctl-c) to data2passcal and it will shutdown cleanly.
-  A list of sent files is kept in ~/.data2passcal.sent. Subsequent runs of send2passcal will not send files already sent.
+  A list of sent files is kept in ~/.data2passcal.sent.
+  Subsequent runs of send2passcal will not send files already sent.
   A log is stored in data2passcal.log in the current directory.
 ''' % VERSION
 
+# Verbosity of console log from logging module from DEBUG to CRITICAL
+DEBUG = logging.DEBUG
+
 # Configure logging file and stdout
-# todo if sending a file do we want to send all previous logs even if already sent
+# todo if sending a file do we want to send all previous logs even if already
+# sent
 logger = logging.getLogger('__name__')
 logger.setLevel(logging.DEBUG)
 # Log to a file with debug level
 logfh = logging.FileHandler(LOGFILE)
 logfh.setLevel(logging.DEBUG)
-logfh.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s -\t%(message)s', datefmt='%Y-%m-%d %H:%M:%S %z'))
+logfh.setFormatter(logging.Formatter(
+                   '%(asctime)s - %(levelname)s -\t%(message)s',
+                   datefmt='%Y-%m-%d %H:%M:%S %z'))
 logger.addHandler(logfh)
 # Log to stdout
 logconsole = logging.StreamHandler()
 logconsole.setLevel(logging.INFO)
-logconsole.setFormatter(logging.Formatter('%(message)s' ))
+logconsole.setFormatter(logging.Formatter('%(message)s'))
 logger.addHandler(logconsole)
 logger.debug('Program starting.')
-logger.debug('%s - v%s - Python:%s' % (sys.argv, VERSION, sys.version) )
+logger.debug('%s - v%s - Python:%s' % (sys.argv, VERSION, sys.version))
 logger.debug('CWD: %s' % os.getcwd())
-logger.info('Version: '+ VERSION)
+logger.info('Version: ' + VERSION)
 logger.info('TIMEOUT: %d' % FTP_TIMEOUT)
 logger.info('FTP RECONNECT WAIT: %d' % FTP_RECONNECT_WAIT)
 # -- End logging
 
+
 def scan_dir(dir):
     '''Returns a list of absolute file names found below root dir'''
     rootdir = dir
-    print()
     logger.info('Scanning: %s' % os.path.abspath(dir))
     filelist = []
     filesize = 0
@@ -98,29 +121,30 @@ def scan_dir(dir):
             try:
                 filesize += os.path.getsize(f)
             except OSError:
-                logger.debug('Can not stat %s'% f)
+                logger.exception('Can not stat %s' % f)
             else:
                 filelist.append(f)
-    logger.info('Total Size = %s'% format_size(filesize) )
-    logger.info('Total Files = %s'% len(filelist) )
-    logger.info('Total Dirs = %s'% foldercount)
-    logger.info('Scan time = %0.2fs'% (time() - starttime))
-    print()
-
+    logger.info('Total Size = %s' % format_size(filesize))
+    logger.info('Total Files = %s' % len(filelist))
+    logger.info('Total Dirs = %s' % foldercount)
+    logger.info('Scan time = %0.2fs' % (time() - starttime))
     return filelist
 
+
 def sendable(file):
-    '''Filters files scanned returning a new list of files to send i.e. miniseed'''
+    '''Filters files scanned returning a new list of files to send'''
     if os.path.basename(file).startswith('.'):
         return False
     if ismseed(file):
         return True
     return False
 
-#Basic Miniseed file metadata extracted from filename
-import re
-#This includes .p files which we may accept in the future but won't h
-MseedRE = re.compile(r'\A(.*)\.([A-Z0-9][A-Z0-9])\.(.*)\.([A-Z][A-Z]\w)\.([0-9]{4})\.([0-9]{3})(?:\.p)*')
+
+# Basic Miniseed file metadata extracted from filename
+# This includes .p files which we may accept in the future
+MseedRE = re.compile(r'\A(.*)\.([A-Z0-9][A-Z0-9])\.(.*)\.'
+                     r'([A-Z][A-Z]\w)\.([0-9]{4})\.([0-9]{3})(?:\.p)*')
+
 
 def filename_qc_format(file):
     '''Used to filter filename if correct for qc system'''
@@ -129,16 +153,19 @@ def filename_qc_format(file):
 
 def format_size(num):
     '''Format bytes into human readble with suffix'''
-    for suffix in [ 'bytes', 'KB', 'MB', 'GB'  ]:
+    for suffix in ['bytes', 'KB', 'MB', 'GB']:
         if num < 1024.0:
             return '%3.3f %s' % (num, suffix)
         num /= 1024.0
     return '%3.3f %s' % (num, 'TB')
 
+
 def ismseed(file):
     try:
-        ms = open(file,'rb')
-    except:
+        ms = open(file, 'rb')
+    except Exception:
+        logger.exception('Failed to open file %s in binary mode'
+                         % os.path.basename(file))
         return None
     order = ByteOrder(ms)
     ms.close()
@@ -149,24 +176,25 @@ def ismseed(file):
         return False
 
 #########################################################
-#needs import sys,struct
-#Taken from Bruces LibTrace
-def ByteOrder(infile, seekval=20) :
+# Taken from Bruces LibTrace
+
+
+def ByteOrder(infile, seekval=20):
     """
     read file as if it is mseed just pulling time info
     from fixed header and determine if it makes sense unpacked
     as big endian or little endian
     """
     Order = "unknown"
-    try :
-        #seek to timeblock and read
+    try:
+        # seek to timeblock and read
         infile.seek(seekval)
-        timeblock=infile.read(10)
+        timeblock = infile.read(10)
 
-        #assume big endian
-        (Year, Day, Hour, Min, Sec, junk, Micro)=\
-         struct.unpack('>HHBBBBH',timeblock)
-        #test if big endian read makes sense
+        # assume big endian
+        (Year, Day, Hour, Min, Sec, junk, Micro) =\
+            struct.unpack('>HHBBBBH', timeblock)
+        # test if big endian read makes sense
         if 1950 <= Year <= 2050 and \
            1 <= Day <= 366 and \
            0 <= Hour <= 23 and \
@@ -174,23 +202,25 @@ def ByteOrder(infile, seekval=20) :
            0 <= Sec <= 59:
             Order = "big"
         else:
-            #try little endian read
-            (Year, Day, Hour, Min, Sec, junk, Micro)=\
-             struct.unpack('<HHBBBBH',timeblock)
-            #test if little endian read makes sense
+            # try little endian read
+            (Year, Day, Hour, Min, Sec, junk, Micro) =\
+                struct.unpack('<HHBBBBH', timeblock)
+            # test if little endian read makes sense
             if 1950 <= Year <= 2050 and \
                1 <= Day <= 366 and \
                0 <= Hour <= 23 and \
                0 <= Min <= 59 and \
                0 <= Sec <= 59:
                 Order = "little"
-    except Exception as e:
+    except Exception:
+        logger.exception('Failed to read time info from fixed header')
         pass
 
     return Order
 #########################################################
 
-def get_sent_file_list(sentfile = SENTFILE):
+
+def get_sent_file_list(sentfile=SENTFILE):
     sentlist = []
     if os.path.isfile(sentfile):
         logger.debug('Using sentfile %s' % sentfile)
@@ -204,29 +234,42 @@ def get_sent_file_list(sentfile = SENTFILE):
                 sentlist = pickle.load(f)
     return sentlist
 
-def write_sent_file_list(sentlist , sentfile = SENTFILE ):
+
+def write_sent_file_list(sentlist, sentfile=SENTFILE):
     logger.info('Saving list of files sent to passcal')
     with open(sentfile, 'wb+') as f:
         pickle.dump(sentlist, f, protocol=2)
 
 
 def get_FTP():
-    '''returns a FTP connection or None if a connection could not be made after attempts allowed'''
+    '''
+    returns a FTP connection or None if a connection could not be made
+    after number of attempts allowed
+    '''
     trys = 0
     while trys < FTP_CONNECT_ATTEMPTS:
         trys += 1
         try:
-            import socket
-            logger.info('Connecting to FTP host %s from %s. Attempt %d of %d' % ( FTP_HOST, socket.gethostbyname(socket.gethostname()), trys, FTP_CONNECT_ATTEMPTS))
-            FTP =  ftplib.FTP(host=FTP_HOST, user=FTP_USER, passwd=FTP_PASSWORD, timeout=FTP_TIMEOUT)
+            local_ip = socket.gethostbyname(socket.gethostname())
+        except socket.gaierror:
+            logger.exception('Valid address-to-host mapping does not exist')
+            local_ip = ''
+        logger.info('Connecting to FTP host %s from %s. Attempt %d of %d'
+                    % (FTP_HOST,
+                       local_ip,
+                       trys,
+                       FTP_CONNECT_ATTEMPTS))
+        try:
+            FTP = ftplib.FTP(host=FTP_HOST, user=FTP_USER,
+                             passwd=FTP_PASSWORD, timeout=FTP_TIMEOUT)
             FTP.set_debuglevel(FTP_DEBUG_LEVEL)
             FTP.cwd(FTP_DIR)
             FTP.set_pasv(True)
-        except ftplib.all_errors as e:
+        except (ftplib.all_errors + (AttributeError,)):
             logger.error('Failed to open FTP connection to %s' % FTP_HOST)
-            logger.error(e)
             test_network()
-            logger.info('Waiting %d seconds before trying to reconnect...' % FTP_RECONNECT_WAIT)
+            logger.info('Waiting %.2f seconds before trying to reconnect...'
+                        % FTP_RECONNECT_WAIT)
             sleep(FTP_RECONNECT_WAIT)
         else:
             logger.info('Success: Connected to PASSCAL FTP')
@@ -234,54 +277,58 @@ def get_FTP():
     logger.error('Giving up.')
     return None
 
+
 def test_network():
     passcal_http_reachable()
     google_http_reachable()
     passcal_ftp_reachable()
 
+
 def passcal_http_reachable():
     '''download and time passcal home page'''
     url = 'http://www.passcal.nmt.edu/'
     return url_reachable(url=url)
 
+
 def google_http_reachable():
-    '''download and time passcal home page'''
+    '''download and time google home page'''
     url = 'http://www.google.com/'
     return url_reachable(url=url)
 
+
 def passcal_ftp_reachable():
     '''Download a small file from passcals general ftp'''
-    url = 'ftp://ftp.passcal.nmt.edu/download/public/test.dat'
+    url = 'ftp://ftp.passcal.nmt.edu/download/public/DO_NOT_DELETE.dat'
     return url_reachable(url=url)
 
-def url_reachable(url = 'http://www.passcal.nmt.edu/'):
+
+def url_reachable(url='http://www.passcal.nmt.edu/'):
     '''fetches a url returns True or False'''
-    import urllib.request, urllib.error, urllib.parse
     start = time()
     try:
-        f = urllib.request.urlopen(url)
-    except Exception as e:
-        logger.error("Failed to open connection to %s" % url)
-        logger.error(e)
+        f = urlopen(url)
+    except Exception:
+        logger.exception("Failed to open connection to %s" % url)
         return False
-    logger.info('connection made to %s in %f sec' % ( url, time() - start) )
+    logger.info('connection made to %s in %f sec' % (url, time() - start))
     data = f.read()
     runtime = time() - start
     size = len(data)
-    rate = size/runtime
-    logger.info('%d B/sec in %f sec' % (rate, runtime) )
+    rate = size / runtime
+    logger.info('%d B/sec in %f sec' % (rate, runtime))
     return True
 
 
 def test_FTP(FTP):
     try:
-        assert isinstance(FTP, ftplib.FTP)
         FTP.voidcmd('NOOP')
-    except ftplib.all_errors as e :
-        logger.error(e)
+    except ftplib.all_errors:
+        logger.error("Failed to send a simple command string to the server"
+                     "and handle the response")
         return False
-    except AssertionError as e:
-        logger.error(e)
+    except AssertionError:
+        logger.error("Failed to send a simple command string to the server"
+                     "and handle the response")
         return False
     else:
         return True
@@ -290,42 +337,48 @@ def test_FTP(FTP):
 def send2passcal(mslist, sentlist=None):
     '''Send the list of files in mslist to passcal via FTP'''
 
-    # Handle SIGINT while in this function: to gracefully close connection and save sentlist to disk file
+    # Handle SIGINT while in this function: to gracefully close connection and
+    # save sentlist to disk file
     def signal_handler(signum, frame):
-        print()
-        logger.info('Caught interrupt while FTPing. Aborting transfer %s' % current_file )
-        logger.info('Sent %d of %d' % (num_sent, num_to_send) )
-        logger.info('Sent %s of %s' % (format_size(size_sent), format_size(size_to_send)) )
-        logger.info('%s /sec' % ( format_size(size_sent / (time() - starttime )) ) )
-        logger.info('Ran for %f sec' % (time()-starttime) )
+        logger.info('Caught interrupt while FTPing. Aborting transfer %s'
+                    % current_file)
+        logger.info('Sent %d of %d' % (num_sent, num_to_send))
+        logger.info('Sent %s of %s'
+                    % (format_size(size_sent), format_size(size_to_send)))
+        logger.info('%s /sec'
+                    % (format_size(size_sent / (time() - starttime))))
+        logger.info('Ran for %f sec' % (time() - starttime))
         write_sent_file_list(sentlist)
         try:
             if FTP:
                 FTP.abort()
                 FTP.quit()
-        except Exception as e:
-            logger.debug(e)
+        except Exception:
+            logger.error("Failed to abort file transfer and close FTP "
+                         "connection")
         os._exit(1)
 
     def update(data):
         '''Updates the terminal display.'''
         signal.signal(signal.SIGINT, signal_handler)
         update.bytes_sent += len(data)
-        print('\r' + str(PB) + ' %s /sec ' % ( format_size(size_sent / (time() - starttime )) ), end=' ')
+        print('\r' + str(PB) + ' %s /sec '
+              % (format_size(size_sent / (time() - starttime))), end=' ')
         '''
-        print '%s %0.2f%%. %0.10d / %0.10d.' % ( current_file.center(20), 
-                                                (update.bytes_sent / update.file_size)*100, 
-                                                update.bytes_sent, 
-                                                file_size, 
-                                                ) , 
+        print '%s %0.2f%%. %0.10d / %0.10d.'
+              % (current_file.center(20),
+                 (update.bytes_sent / update.file_size) * 100,
+                 update.bytes_sent,
+                 file_size)
         '''
-        ETA_sec = ((time() - starttime) / size_sent ) * (size_to_send - size_sent)
-        print('ETA %s %s %s' % (  str(datetime.timedelta(seconds=(ETA_sec))), current_file.center(20), ' '*20 ), end=' ')
+        ETA_sec = ((time() - starttime) / size_sent) * \
+            (size_to_send - size_sent)
+        print('ETA %s %s %s' % (str(datetime.timedelta(seconds=(ETA_sec))),
+                                current_file.center(20), ' ' * 20), end=' ')
         sys.stdout.flush()
 
     if sentlist is None:
         sentlist = []
-    print()
     logger.info('Sending MSEED files to PASSCAL')
     num_to_send = len(mslist)
     num_sent = 0
@@ -333,7 +386,8 @@ def send2passcal(mslist, sentlist=None):
     size_sent = 1
     current_file = ''
     signal.signal(signal.SIGINT, signal_handler)
-    logger.info('Sending %d, %s files to PASSCAL' % (num_to_send, format_size (size_to_send)))
+    logger.info('Sending %d, %s files to PASSCAL' %
+                (num_to_send, format_size(size_to_send)))
     FTP = get_FTP()
     starttime = time()
     PB = ProgressBar(num_to_send)
@@ -344,30 +398,39 @@ def send2passcal(mslist, sentlist=None):
         while trys < FTP_SEND_ATTEMPTS:
             trys += 1
             update.bytes_sent = 0
+            fh = open(f, 'rb')
+            file_size = os.path.getsize(f)
+            update.file_size = float(file_size)
             try:
-                fh = open(f, 'rb')
-                file_size = os.path.getsize(f)
-                update.file_size = float(file_size)
-                FTP.storbinary('STOR %s' % current_file , fh, blocksize=FTP_BLOCKSIZE, callback=update)
-            except ftplib.error_perm as e:
-                # This is permission and the error when 550 for the .in file already exists so we should just continue with the next file
-                # todo create a list of failed files and resend those at the end instead of requiring a rerun
-                logger.error( 'Failed to send file %s, permission error. Skipping...' % current_file)
-                logger.error(e)
+                FTP.storbinary('STOR %s' % current_file, fh,
+                               blocksize=FTP_BLOCKSIZE, callback=update)
+            except ftplib.error_perm:
+                # This is permission and the error when 550 for the .in file
+                # already exists so we should just continue with the next file
+                # todo create a list of failed files and resend those at the
+                # end instead of requiring a rerun
+                logger.error('Failed to send file %s, permission error.'
+                             % current_file)
+                fh.close()
                 break
-            except (ftplib.all_errors, AttributeError) as e :
-                #since we can restore with how we have proftp setup. There is nothing more we can do with this file
-                #Until the server rms the .in.file
-                logger.error('Failed to send file %s.' % ( current_file) ) #, trys, FTP_SEND_ATTEMPTS)
-                logger.error(e)
-                #if DEBUG: print "Waiting %d..." % FTP_TIMEOUT; sleep(FTP_TIMEOUT)
+            except (ftplib.all_errors + (AttributeError,)):
+                # since we can restore with how we have proftp setup.
+                # There is nothing more we can do with this file
+                # Until the server rms the .in.file
+                # , trys, FTP_SEND_ATTEMPTS)
+                logger.error('Failed to send file %s.' % (current_file))
+                # if DEBUG:
+                # print "Waiting %d..." %FTP_TIMEOUT; sleep(FTP_TIMEOUT)
                 try:
                     if FTP:
                         FTP.abort()
                         FTP.quit()
-                except:
+                except Exception:
+                    logger.exception("Failed to abort file transfer and close"
+                                     "FTP connection")
                     pass
                 FTP = get_FTP()
+                fh.close()
                 if FTP is None:
                     signal_handler(None, None)
             else:
@@ -378,17 +441,19 @@ def send2passcal(mslist, sentlist=None):
                 sentlist.append(f)
                 break
 
-    print()
-    logger.info( 'Sent %d of %d' % (num_sent, num_to_send) )
-    logger.info( 'Sent %s of %s' % (format_size(size_sent), format_size(size_to_send)) )
-    logger.info( '%s /sec' % ( format_size(size_sent / (time() - starttime )) ) )
-    logger.info( 'Ran for %f sec' % (time()-starttime) )
+    logger.info('Sent %d of %d' % (num_sent, num_to_send))
+    logger.info('Sent %s of %s'
+                % (format_size(size_sent), format_size(size_to_send)))
+    logger.info('%s /sec' % (format_size(size_sent / (time() - starttime))))
+    logger.info('Ran for %f sec' % (time() - starttime))
     if FTP and test_FTP(FTP):
         FTP.quit()
 
 ########################################################
 # From progressbar 3rd party class will eventually dump not awesome
-class ProgressBar:
+
+
+class ProgressBar(object):
     def __init__(self, duration):
         self.duration = duration
         self.prog_bar = '[]'
@@ -408,13 +473,14 @@ class ProgressBar:
 
     def update_time(self, elapsed_secs):
         self.__update_amount((elapsed_secs / float(self.duration)) * 100.0)
-        #self.prog_bar += '  %d/%s files' % (elapsed_secs, self.duration)
+        # self.prog_bar += '  %d/%s files' % (elapsed_secs, self.duration)
 
     def __update_amount(self, new_amount):
         percent_done = int(round((new_amount / 100.0) * 100.0))
         all_full = self.width - 2
         num_hashes = int(round((percent_done / 100.0) * all_full))
-        self.prog_bar = '[' + self.fill_char * num_hashes + ' ' * (all_full - num_hashes) + ']'
+        self.prog_bar = '[' + self.fill_char * \
+            num_hashes + ' ' * (all_full - num_hashes) + ']'
         pct_place = int((len(self.prog_bar) / 2) - len(str(percent_done)))
         pct_string = '%d%%' % percent_done
         self.prog_bar = self.prog_bar[0:pct_place] + \
@@ -425,8 +491,10 @@ class ProgressBar:
 
 # End progressbar
 ########################################################
+
+
 def main():
-    if len(sys.argv) < 2 or sys.argv[1] in [ '-h', '--help', '-?']:
+    if len(sys.argv) < 2 or sys.argv[1] in ['-h', '--help', '-?']:
         print(HELP)
         sys.exit()
     # find all files below dir
@@ -435,19 +503,20 @@ def main():
     logger.info('Removing improperly named files.')
     msnamedlist = list(filter(filename_qc_format, filelist))
     logger.info('Properly named files files: %d' % len(msnamedlist))
-    logger.info('Other files: %d'% (len(filelist) - len(msnamedlist)))
-    print()
+    logger.info('Other files: %d' % (len(filelist) - len(msnamedlist)))
     logger.info('Removing files that are not Miniseed.')
     mslist = list(filter(sendable, msnamedlist))
     logger.info('MiniSEED files: %d' % len(mslist))
-    logger.info('Properly named but not miniseed files: %d'% (len(msnamedlist) - len(mslist)))
-    print()
+    logger.info('Properly named but not miniseed files: %d'
+                % (len(msnamedlist) - len(mslist)))
     logger.info('Removing files already sent to PASSCAL')
     sentlist = get_sent_file_list()
     unsentms = [f for f in mslist if f not in sentlist]
-    logger.info('%d miniSEED files have already been sent, not resending.' % (len( mslist) - len(unsentms)))
+    logger.info('%d miniSEED files have already been sent, not resending.'
+                % (len(mslist) - len(unsentms)))
     send2passcal(unsentms, sentlist)
     write_sent_file_list(sentlist)
 
+
 if __name__ == '__main__':
     main()
diff --git a/setup.cfg b/setup.cfg
index bd5e695214d2220c9df4bb5f53418783cc3037b2..3e16c9aa4de92883ce48eebabea3688a4ead6f10 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,22 +1,5 @@
-[bumpversion]
-current_version = 2018.228
-commit = True
-tag = True
-
-[bumpversion:file:setup.py]
-search = version='{current_version}'
-replace = version='{new_version}'
-
-[bumpversion:file:data2passcal/__init__.py]
-search = __version__ = '{current_version}'
-replace = __version__ = '{new_version}'
-
-[bdist_wheel]
-universal = 1
-
 [flake8]
 exclude = docs
 
 [aliases]
 # Define setup.py command aliases here
-
diff --git a/setup.py b/setup.py
index 3c7b133727cf7718141b7ce8e55399c30d670526..f5ef9304bcbf2900e1a70d4b2bedb65501384efd 100644
--- a/setup.py
+++ b/setup.py
@@ -5,10 +5,10 @@
 
 from setuptools import setup, find_packages
 
-with open('README.rst') as readme_file:
+with open('README.rst', 'rt') as readme_file:
     readme = readme_file.read()
 
-with open('HISTORY.rst') as history_file:
+with open('HISTORY.rst', 'rt') as history_file:
     history = history_file.read()
 
 
@@ -21,7 +21,10 @@ setup(
         'License :: OSI Approved ::  GNU General Public License v3 (GPLv3)',
         'Natural Language :: English',
         'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3.8',
     ],
     description="Prepare SEED data for shipment to PASSCAL.",
     entry_points={
@@ -30,18 +33,12 @@ setup(
         ],
     },
     install_requires=[],
-    setup_requires = [],
+    setup_requires=[],
     extras_require={
         'dev': [
-            'pip',
-            'bumpversion',
-            'wheel',
-            'watchdog',
             'flake8',
             'tox',
-            'coverage',
-            'Sphinx',
-            'twine',
+            "mock;python_version<'3.3'"
         ]
     },
     license="GNU General Public License v3",
@@ -50,8 +47,7 @@ setup(
     keywords='data2passcal',
     name='data2passcal',
     packages=find_packages(include=['data2passcal']),
-    test_suite='tests',
     url='https://git.passcal.nmt.edu/passoft/data2passcal',
-    version='2018.228',
+    version='2020.213',
     zip_safe=False,
 )
diff --git a/tests/test_data/ST00.AB..BHZ.2007.160 b/tests/test_data/ST00.AB..BHZ.2007.160
new file mode 100644
index 0000000000000000000000000000000000000000..668ccee282506f7101f4f6de9e5175a3103d0043
Binary files /dev/null and b/tests/test_data/ST00.AB..BHZ.2007.160 differ
diff --git a/tests/test_data/ST00.AB..BHZ.2007.161 b/tests/test_data/ST00.AB..BHZ.2007.161
new file mode 100644
index 0000000000000000000000000000000000000000..f3fa1ece76f3655d59e123b5d92c235b4f5e9e4a
Binary files /dev/null and b/tests/test_data/ST00.AB..BHZ.2007.161 differ
diff --git a/tests/test_data/ST00.AB..BHZ.2007.162 b/tests/test_data/ST00.AB..BHZ.2007.162
new file mode 100644
index 0000000000000000000000000000000000000000..75c29514caed9cc89925cc00f5edca42c11a3950
Binary files /dev/null and b/tests/test_data/ST00.AB..BHZ.2007.162 differ
diff --git a/tests/test_data/ST00.AB..BHZ.2007.163 b/tests/test_data/ST00.AB..BHZ.2007.163
new file mode 100644
index 0000000000000000000000000000000000000000..907b07ca7097a791aa3c346e26df10f9c7788851
Binary files /dev/null and b/tests/test_data/ST00.AB..BHZ.2007.163 differ
diff --git a/tests/test_data/ST00.AB..BHZ.2007.164 b/tests/test_data/ST00.AB..BHZ.2007.164
new file mode 100644
index 0000000000000000000000000000000000000000..b925f35dace4e038f204d9b993113d9318f277e5
Binary files /dev/null and b/tests/test_data/ST00.AB..BHZ.2007.164 differ
diff --git a/tests/test_data2passcal.py b/tests/test_data2passcal.py
index e102d31f6b7762155c53b59bb37b4039d921533f..6515358cf0f180243f86bfc0c3c28b8b0fa2d25b 100644
--- a/tests/test_data2passcal.py
+++ b/tests/test_data2passcal.py
@@ -3,26 +3,137 @@
 
 """Tests for `data2passcal` package."""
 
-import unittest
+from __future__ import division, print_function
+
+import ftplib
+import os
 import sys
+import unittest
+import data2passcal.data2passcal as d2p
+
+if sys.version_info < (3, 3):
+    from mock import patch
+else:
+    from unittest.mock import patch
+
+VERSION = '2020.213'
+
+SEND4REAL = os.environ.get('SEND4REAL', 'False')
+print("SEND4REAL=False by default. If one wants to test sending data to "
+      "PASSCAL for 'real', set SEND4REAL=True as environment variable. "
+      "ex: SEND4REAL=True python -m unittest test_data2passcal")
+
+TEST_DIR = os.path.dirname(os.path.realpath(__file__)) + '/test_data'
+MS_FILELIST = ['ST00.AB..BHZ.2007.160', 'ST00.AB..BHZ.2007.161',
+               'ST00.AB..BHZ.2007.162', 'ST00.AB..BHZ.2007.163',
+               'ST00.AB..BHZ.2007.164']
+
+d2p.FTP_TIMEOUT = 1
+d2p.FTP_RECONNECT_WAIT = 0.01
+d2p.FTP_CONNECT_ATTEMPTS = 2
+d2p.FTP_SEND_ATTEMPTS = 2
 
-try:
-    import data2passcal
-except ImportError:
-     pass
 
 class TestData2passcal(unittest.TestCase):
     """Tests for `data2passcal` package."""
 
-    def setUp(self):
-        """Set up test fixtures, if any."""
+    def test_scan_dir(self):
+        """
+        Test basic functionality of scan_dir function
+        """
+        filelist = [os.path.join(TEST_DIR, f) for f in MS_FILELIST]
+        if sys.version_info < (3, 2):
+            self.assertItemsEqual(filelist, d2p.scan_dir(TEST_DIR),
+                                  'scan_dir did not find the correct file(s)')
+        else:
+            self.assertCountEqual(filelist, d2p.scan_dir(TEST_DIR),
+                                  'scan_dir did not find the correct file(s)')
 
-    def tearDown(self):
-        """Tear down test fixtures, if any."""
+    def test_ismseed(self):
+        """Test basic functionality of ismseed function"""
+        filelist = [os.path.join(TEST_DIR, f) for f in MS_FILELIST]
+        for f in filelist:
+            self.assertTrue(d2p.ismseed(f), '{} is not a miniseed file'
+                            .format(os.path.basename(f)))
+
+    @patch('data2passcal.data2passcal.ftplib.FTP', autospec=True)
+    def test_get_FTP_mock(self, mock_ftp_constructor):
+        """Mock test creating ftp connection to PASSCAL"""
+        mock_ftp = mock_ftp_constructor.return_value
+        d2p.get_FTP()
+        self.assertLess(mock_ftp_constructor.call_count,
+                        d2p.FTP_CONNECT_ATTEMPTS,
+                        'Number of ftp connection attempts exceeeds {}'
+                        .format(d2p.FTP_CONNECT_ATTEMPTS))
+        mock_ftp.quit()
+
+    @patch('data2passcal.data2passcal.urlopen', autospec=True)
+    @patch('data2passcal.data2passcal.ftplib.FTP', autospec=True)
+    def test_get_FTP_failure_mock(self, mock_ftp_constructor, mock_urlopen):
+        """
+        Mock test failure to create ftp connection to PASSCAL and exercise
+        get_FTP()
+        """
+        mock_ftp_constructor.return_value = ftplib.error_temp
+        d2p.get_FTP()
+        self.assertGreater(mock_ftp_constructor.call_count, 1,
+                           "ftplib.FTP() called only once - get_FTP() not "
+                           "fully exercised!")
+
+    @patch('data2passcal.data2passcal.ftplib.FTP', autospec=True)
+    def test_send_data_mock(self, mock_ftp_constructor):
+        """Mock test sending MSEED files (test data) to PASSCAL's QC system"""
+        mock_ftp = mock_ftp_constructor.return_value
+        filelist = [os.path.join(TEST_DIR, f) for f in MS_FILELIST[0:2]]
+        d2p.send2passcal(filelist)
+        self.assertTrue(mock_ftp.storbinary.called, 'No data sent')
+        self.assertEqual(mock_ftp.storbinary.call_count, len(filelist),
+                         'Failed to send all files - Sent {0} of {1}'
+                         .format(mock_ftp.storbinary.call_count,
+                                 len(filelist)))
+        files_sent = []
+        for x in mock_ftp.storbinary.call_args_list:
+            args, kwargs = x
+            files_sent.append(args[0].split(' ')[1])
+        for f in MS_FILELIST[0:2]:
+            self.assertLess(files_sent.count(f), d2p.FTP_SEND_ATTEMPTS,
+                            'Attempted to send file {0} more than {1} times'
+                            .format(f, d2p.FTP_SEND_ATTEMPTS))
+
+    @patch('data2passcal.data2passcal.os._exit', autospec=True)
+    @patch('data2passcal.data2passcal.urlopen', autospec=True)
+    @patch('data2passcal.data2passcal.ftplib.FTP', autospec=True)
+    def test_send_data_failure_mock(self, mock_ftp_constructor,
+                                    mock_urlopen, mock_exit):
+        """
+        Mock test failure to create ftp connection to PASSCAL and exercise
+        send_data()
+        """
+        mock_ftp_constructor.return_value = ftplib.error_temp
+        filelist = [os.path.join(TEST_DIR, f) for f in MS_FILELIST[0:2]]
+        d2p.send2passcal(filelist)
+        self.assertTrue(mock_exit.called, "os._exit(1) never called - "
+                                          "send_data() not fully exercised!")
+
+    @unittest.skipIf(SEND4REAL == 'False', "skipping real send2passcal test")
+    def test_send_data(self):
+        """Test sending MSEED files (test data) to PASSCAL's QC system"""
+        ftp = d2p.get_FTP()
+        filelist = [os.path.join(TEST_DIR, f) for f in MS_FILELIST[0:2]]
+        d2p.send2passcal(filelist)
+        wdir = ftp.pwd()
+        try:
+            files_sent = [os.path.basename(x) for x in ftp.nlst(wdir)]
+        except ftplib.error_perm() as resp:
+            if str(resp) == "550 No files found":
+                print("No files found in this directory")
+            else:
+                raise
+        for f in MS_FILELIST[0:2]:
+            self.assertIn(f, files_sent, 'File {} was not sent to PASSCAL'
+                          .format(f))
+        ftp.quit()
 
-    def test_import(self):
-        if 'data2passcal' in sys.modules:
-            self.assert_(True, "data2passcal loaded")
-        else:
-            self.fail()
 
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tox.ini b/tox.ini
index 60d2b1bb52fdc8539b7f39176385af7599226b5a..954a1f07c917bc9587afdaf4ecffe26723fe41ff 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,19 +1,21 @@
 [tox]
-envlist = py27, py36 flake8
-
-[travis]
-python =
-    2.7: py27
-    3.6: py36
+envlist = py27, py35, py36, py37, py38, flake8
 
 [testenv:flake8]
 basepython = python
 deps = flake8
-commands = flake8 data2passcal
-
-[testenv]
-setenv =
-    PYTHONPATH = {toxinidir}
-commands=python setup.py test
+commands = flake8 --ignore=E722,E712 data2passcal
+           flake8 --ignore=E722,E712 tests
 
+[testenv:py27]
+changedir = tests
+passenv = SEND4REAL
+deps =
+      mock
+      timeout-decorator
+commands = python -m unittest test_data2passcal
 
+[testenv]
+passenv = SEND4REAL
+deps = timeout-decorator
+commands = python -m unittest