This is an automated email from the ASF dual-hosted git repository.

rkk pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/sdap-nexus.git


The following commit(s) were added to refs/heads/develop by this push:
     new 868b7b9  SDAP-521 - Improved SDAP testing suite (#325)
868b7b9 is described below

commit 868b7b98ff156f7fbfbacbe3d29a89f02d5474ce
Author: Riley Kuttruff <[email protected]>
AuthorDate: Wed Oct 16 07:11:11 2024 -0700

    SDAP-521 - Improved SDAP testing suite (#325)
    
    * SDAP-520 Added RC eval guide to RTD
    
    * remove incubator
    
    * add to toctree
    
    * Granule download script for testing
    
    * Move & update test script
    
    Made current with what was deployed for the CDMS project. Will need 
extensive editing.
    
    * Test script pruning + guide start
    
    * Updates
    
    * Updates
    
    * Guide for install and run
    
    * Attempt to fix the many Sphinx warnings on build
    
    * Fix bad ref
    
    * Fix bad ref
    
    * Fix bad ref (third time's the charm?)
    
    * Removal of ingested test data
    
    * Reduced datainbounds L2 test bbox to ease memory footprint
    
    * Revert "Reduced datainbounds L2 test bbox to ease memory footprint"
    
    This reverts commit 46cf5ad73763497b031483ca7f4c7db0606fd5cc.
    
    * Update docs for missing test collection
    
    * SDAP-521 Updated quickstart and test guide. (#327)
    
    * SDAP-521 Updated quickstart and test guide.
    
    * SDAP-521 Updated solr start up env variables to be consistent with helm 
chart.
    
    * SDAP-521 Updated README.md
    
    ---------
    
    Co-authored-by: rileykk <[email protected]>
    Co-authored-by: Nga Chung <[email protected]>
---
 docs/index.rst                     |    1 +
 docs/quickstart.rst                |   19 +-
 docs/release.rst                   |    2 +-
 docs/test.rst                      |  116 ++++
 tests/.gitignore                   |    1 +
 tests/{regression => }/README.md   |    2 +-
 tests/cdms_reader.py               |  250 +++++++
 tests/{regression => }/conftest.py |   16 +-
 tests/download_data.sh             |  342 ++++++++++
 tests/regression/cdms_reader.py    |    1 -
 tests/regression/test_cdms.py      |  746 ---------------------
 tests/requirements.txt             |    9 +
 tests/test_collections.yaml        |   73 ++
 tests/test_sdap.py                 | 1300 ++++++++++++++++++++++++++++++++++++
 14 files changed, 2110 insertions(+), 768 deletions(-)

diff --git a/docs/index.rst b/docs/index.rst
index 160c30f..2f27b8a 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -11,6 +11,7 @@ Welcome to the Apache SDAP project documentation!
    build
    dockerimages
    release
+   test
 
 
 Check out the :ref:`quickstart guide<quickstart>`.
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index 34553b1..12de37b 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -49,11 +49,11 @@ Pull the necessary Docker images from the `Apache SDAP 
repository <https://hub.d
 
   export CASSANDRA_VERSION=3.11.6-debian-10-r138
   export RMQ_VERSION=3.8.9-debian-10-r37
-  export COLLECTION_MANAGER_VERSION=1.2.0
-  export GRANULE_INGESTER_VERSION=1.2.0
-  export WEBAPP_VERSION=1.2.0
-  export SOLR_VERSION=1.2.0
-  export SOLR_CLOUD_INIT_VERSION=1.2.0
+  export COLLECTION_MANAGER_VERSION=1.3.0
+  export GRANULE_INGESTER_VERSION=1.3.0
+  export WEBAPP_VERSION=1.3.0
+  export SOLR_VERSION=1.3.0
+  export SOLR_CLOUD_INIT_VERSION=1.3.0
   export ZK_VERSION=3.5.5
 
   export JUPYTER_VERSION=1.0.0-rc2
@@ -143,7 +143,7 @@ To start Solr using a volume mount and expose the admin 
webapp on port 8983:
 
   export SOLR_DATA=~/nexus-quickstart/solr
   mkdir -p ${SOLR_DATA}
-  docker run --name solr --network sdap-net -v ${SOLR_DATA}/:/bitnami -p 
8983:8983 -e SDAP_ZK_SERVICE_HOST="host.docker.internal" -d 
${REPO}/sdap-solr-cloud:${SOLR_VERSION}
+  docker run --name solr --network sdap-net -v ${SOLR_DATA}/:/bitnami -p 
8983:8983 -e SOLR_ZK_HOSTS="host.docker.internal:2181" -e 
SOLR_ENABLE_CLOUD_MODE="yes" -d ${REPO}/sdap-solr-cloud:${SOLR_VERSION}
 
 This will start an instance of Solr. To initialize it, we need to run the 
``solr-cloud-init`` image.
 
@@ -215,7 +215,7 @@ Choose a location that is mountable by Docker (typically 
needs to be under the u
 
 .. code-block:: bash
 
-    export DATA_DIRECTORY=~/nexus-quickstart/data/avhrr-granules
+    export DATA_DIRECTORY=~/nexus-quickstart/data/
     mkdir -p ${DATA_DIRECTORY}
 
 .. _quickstart-step7:
@@ -290,7 +290,8 @@ Then go ahead and download 1 month worth of AVHRR netCDF 
files.
 
 .. code-block:: bash
 
-  cd $DATA_DIRECTORY
+  mkdir -p ${DATA_DIRECTORY}/avhrr-granules
+  cd $DATA_DIRECTORY/avhrr-granules
 
   curl -O 
https://raw.githubusercontent.com/apache/incubator-sdap-nexus/master/docs/granule-download.sh
   chmod 700 granule-download.sh
@@ -314,7 +315,7 @@ The collection configuration is a ``.yml`` file that tells 
the collection manage
   cat << EOF >> ${CONFIG_DIR}/collectionConfig.yml
   collections:
     - id: AVHRR_OI_L4_GHRSST_NCEI
-      path: /data/granules/*.nc
+      path: /data/granules/avhrr-granules/*AVHRR_OI-GLOB-v02.0-fv02.0.nc
       priority: 1
       forward-processing-priority: 5
       projection: Grid
diff --git a/docs/release.rst b/docs/release.rst
index c476ecc..c4434f2 100644
--- a/docs/release.rst
+++ b/docs/release.rst
@@ -108,7 +108,7 @@ Verify the images are working by using them in the 
:ref:`Quickstart Guide<quicks
 Extended Testing
 ----------------
 
-Section coming soon...
+See :ref:`this guide<testing>` for info about running SDAP tests.
 
 Vote
 ====
diff --git a/docs/test.rst b/docs/test.rst
new file mode 100644
index 0000000..39983ea
--- /dev/null
+++ b/docs/test.rst
@@ -0,0 +1,116 @@
+.. _testing:
+
+******************
+SDAP Testing Guide
+******************
+
+This guide covers how to set up and run SDAP testing and how to clean up 
afterwards.
+
+.. note::
+
+  Unless otherwise specified, all commands run in this guide are run from the 
``<repo root>/tests`` directory.
+
+Before You Begin
+================
+
+Ensure you have SDAP up and running by running through the :ref:`Quickstart 
Guide<quickstart>`. For now, you just need to have
+Solr, Cassandra, and RabbitMQ started and initialized, but you can run through 
the complete guide if you desire.
+
+.. note::
+
+  You'll need to use the same shell you used in the quickstart guide here as 
this guide refers to some of the same environment variables.
+
+Download and Ingest Test Data
+=============================
+
+The tests utilize data from multiple source collections. We've prepared a 
script to download only the necessary input files
+and arrange them in subdirectories of your SDAP data directory.
+
+.. code-block:: bash
+
+  ./download_data.sh
+
+Now you will need to define the collections in the collection config. If 
you've already started the Collection Manager,
+you can simply update the config and it should look for the files within about 
30 seconds or so.
+
+.. code-block:: bash
+
+  tail -n +2 test_collections.yaml >> ${CONFIG_DIR}/collectionConfig.yml
+
+If the Collection Manager does not appear to be detecting the data, try 
restarting it.
+
+If you have not started the Collection Manager, start it now:
+
+.. code-block:: bash
+
+  docker run --name collection-manager --network sdap-net -v 
${DATA_DIRECTORY}:/data/granules/ -v ${CONFIG_DIR}:/home/ingester/config/ -e 
COLLECTIONS_PATH="/home/ingester/config/collectionConfig.yml" -e 
HISTORY_URL="http://host.docker.internal:8983/"; -e 
RABBITMQ_HOST="host.docker.internal:5672" -e RABBITMQ_USERNAME="user" -e 
RABBITMQ_PASSWORD="bitnami" -d 
${REPO}/sdap-collection-manager:${COLLECTION_MANAGER_VERSION}
+
+Refer to the :ref:`Quickstart Guide<quickstart>` to see how many files are 
enqueued for ingest, there should be 207 total.
+(This may appear to be less if you have ingesters running. We recommend not 
starting the ingesters until all data is queued.
+You may also see more if the Collection Manager was running during the data 
download. This is a known issue where the Collection
+Manager queues downloading files more than once as they're seen as modified.)
+
+Once the data is ready for ingest, start up the ingester(s) and wait for them 
to finish. After that, you can stop the Collection Manager,
+ingester and RabbitMQ containers and start the webapp container if it is not 
already running.
+
+Set Up pytest
+=============
+
+Before running the tests, you must first set up an environment and install 
dependencies:
+
+.. code-block:: bash
+
+  python -m venv env
+  source env/bin/activate
+  pip install -r requirements.txt
+
+Run the Tests!
+==============
+
+To execute the tests, simply run
+
+.. code-block:: bash
+
+  pytest --with-integration
+
+You can also target the tests to an SDAP instance running at a different 
location, say a remote deployment, but be sure
+it has the required data ingested under the correct collection names, 
otherwise most tests will fail.
+
+.. code-block:: bash
+
+  export TEST_HOST=<SDAP URL>
+
+Cleanup
+=======
+
+If you would like to remove the test data ingested in this guide, use the 
following steps to delete it.
+
+For the tile data itself, there's a tool for that exact purpose:
+
+.. code-block:: bash
+
+  cd ../tools/deletebyquery
+  pip install -r requirements.txt
+  # Run once for each dataset to avoid catching any other datasets with a 
wildcard query
+  python deletebyquery.py --solr localhost:8983 --cassandra localhost 
--cassandraUsername cassandra --cassandraPassword cassandra -q 
'dataset_s:ASCATB-L2-Coastal_test'
+  python deletebyquery.py --solr localhost:8983 --cassandra localhost 
--cassandraUsername cassandra --cassandraPassword cassandra -q 
'dataset_s:VIIRS_NPP-2018_Heatwave_test'
+  python deletebyquery.py --solr localhost:8983 --cassandra localhost 
--cassandraUsername cassandra --cassandraPassword cassandra -q 
'dataset_s:OISSS_L4_multimission_7day_v1_test'
+  python deletebyquery.py --solr localhost:8983 --cassandra localhost 
--cassandraUsername cassandra --cassandraPassword cassandra -q 
'dataset_s:MUR25-JPL-L4-GLOB-v04.2_test'
+  python deletebyquery.py --solr localhost:8983 --cassandra localhost 
--cassandraUsername cassandra --cassandraPassword cassandra -q 
'dataset_s:SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5_test'
+
+Unfortunately, this does not remove records of the datasets or ingested input 
granules themselves, they need to be removed manually.
+
+.. code-block:: bash
+
+  curl -g 'http://localhost:8983/solr/nexusgranules/update?commit=true' -H 
'Content-Type: application/json' -d '{"delete": {"query": 
"dataset_s:MUR25-JPL-L4-GLOB-v04.2_test"}}'
+  curl -g 'http://localhost:8983/solr/nexusgranules/update?commit=true' -H 
'Content-Type: application/json' -d '{"delete": {"query": 
"dataset_s:ASCATB-L2-Coastal_test"}}'
+  curl -g 'http://localhost:8983/solr/nexusgranules/update?commit=true' -H 
'Content-Type: application/json' -d '{"delete": {"query": 
"dataset_s:OISSS_L4_multimission_7day_v1_test"}}'
+  curl -g 'http://localhost:8983/solr/nexusgranules/update?commit=true' -H 
'Content-Type: application/json' -d '{"delete": {"query": 
"dataset_s:VIIRS_NPP-2018_Heatwave_test"}}'
+  curl -g 'http://localhost:8983/solr/nexusgranules/update?commit=true' -H 
'Content-Type: application/json' -d '{"delete": {"query": 
"dataset_s:SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5_test"}}'
+
+  curl -g 'http://localhost:8983/solr/nexusdatasets/update?commit=true' -H 
'Content-Type: application/json' -d '{"delete": {"query": 
"dataset_s:MUR25-JPL-L4-GLOB-v04.2_test"}}'
+  curl -g 'http://localhost:8983/solr/nexusdatasets/update?commit=true' -H 
'Content-Type: application/json' -d '{"delete": {"query": 
"dataset_s:ASCATB-L2-Coastal_test"}}'
+  curl -g 'http://localhost:8983/solr/nexusdatasets/update?commit=true' -H 
'Content-Type: application/json' -d '{"delete": {"query": 
"dataset_s:OISSS_L4_multimission_7day_v1_test"}}'
+  curl -g 'http://localhost:8983/solr/nexusdatasets/update?commit=true' -H 
'Content-Type: application/json' -d '{"delete": {"query": 
"dataset_s:VIIRS_NPP-2018_Heatwave_test"}}'
+  curl -g 'http://localhost:8983/solr/nexusdatasets/update?commit=true' -H 
'Content-Type: application/json' -d '{"delete": {"query": 
"dataset_s:SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5_test"}}'
+
diff --git a/tests/.gitignore b/tests/.gitignore
new file mode 100644
index 0000000..f933b64
--- /dev/null
+++ b/tests/.gitignore
@@ -0,0 +1 @@
+responses/
diff --git a/tests/regression/README.md b/tests/README.md
similarity index 95%
rename from tests/regression/README.md
rename to tests/README.md
index bb470c7..db53aac 100644
--- a/tests/regression/README.md
+++ b/tests/README.md
@@ -3,7 +3,7 @@
 ### To run:
 
 ```shell
-pytest test_cdms.py --with-integration [--force-subset]
+pytest test_sdap.py --with-integration [--force-subset]
 ```
 
 ### Options
diff --git a/tests/cdms_reader.py b/tests/cdms_reader.py
new file mode 100644
index 0000000..ebbc08e
--- /dev/null
+++ b/tests/cdms_reader.py
@@ -0,0 +1,250 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import string
+from netCDF4 import Dataset, num2date
+import sys
+import datetime
+import csv
+from collections import OrderedDict
+import logging
+
+#TODO: Get rid of numpy errors?
+#TODO: Update big SDAP README
+
+LOGGER =  logging.getLogger("cdms_reader")
+
+def assemble_matches(filename):
+    """
+    Read a CDMS netCDF file and return a list of matches.
+    
+    Parameters
+    ----------
+    filename : str
+        The CDMS netCDF file name.
+    
+    Returns
+    -------
+    matches : list
+        List of matches. Each list element is a dictionary.
+        For match m, netCDF group GROUP (SatelliteData or InsituData), and
+        group variable VARIABLE:
+        matches[m][GROUP]['matchID']: MatchedRecords dimension ID for the match
+        matches[m][GROUP]['GROUPID']: GROUP dim dimension ID for the record
+        matches[m][GROUP][VARIABLE]: variable value 
+    """
+   
+    try:
+        # Open the netCDF file
+        with Dataset(filename, 'r') as cdms_nc:
+            # Check that the number of groups is consistent w/ the 
MatchedGroups
+            # dimension
+            assert len(cdms_nc.groups) == 
cdms_nc.dimensions['MatchedGroups'].size,\
+                ("Number of groups isn't the same as MatchedGroups dimension.")
+            
+            matches = []
+            matched_records = cdms_nc.dimensions['MatchedRecords'].size
+            
+            # Loop through the match IDs to assemble matches
+            for match in range(0, matched_records):
+                match_dict = OrderedDict()
+                # Grab the data from each platform (group) in the match
+                for group_num, group in enumerate(cdms_nc.groups):
+                    match_dict[group] = OrderedDict()
+                    match_dict[group]['matchID'] = match
+                    ID = cdms_nc.variables['matchIDs'][match][group_num]
+                    match_dict[group][group + 'ID'] = ID
+                    for var in cdms_nc.groups[group].variables.keys():
+                        match_dict[group][var] = cdms_nc.groups[group][var][ID]
+                    
+                    # Create a UTC datetime field from timestamp
+                    dt = num2date(match_dict[group]['time'],
+                                  cdms_nc.groups[group]['time'].units)
+                    match_dict[group]['datetime'] = dt
+                LOGGER.info(match_dict)
+                matches.append(match_dict)
+            
+            return matches
+    except (OSError, IOError) as err:
+        LOGGER.exception("Error reading netCDF file " + filename)
+        raise err
+    
+def matches_to_csv(matches, csvfile):
+    """
+    Write the CDMS matches to a CSV file. Include a header of column names
+    which are based on the group and variable names from the netCDF file.
+    
+    Parameters
+    ----------
+    matches : list
+        The list of dictionaries containing the CDMS matches as returned from
+        assemble_matches.      
+    csvfile : str
+        The name of the CSV output file.
+    """
+    # Create a header for the CSV. Column names are GROUP_VARIABLE or
+    # GROUP_GROUPID.
+    header = []
+    for key, value in matches[0].items():
+        for otherkey in value.keys():
+            header.append(key + "_" + otherkey)
+    
+    try:
+        # Write the CSV file
+        with open(csvfile, 'w') as output_file:
+            csv_writer = csv.writer(output_file)
+            csv_writer.writerow(header)
+            for match in matches:
+                row = []
+                for group, data in match.items():
+                    for value in data.values():
+                        row.append(value)
+                csv_writer.writerow(row)
+    except (OSError, IOError) as err:
+        LOGGER.exception("Error writing CSV file " + csvfile)
+        raise err
+
+def get_globals(filename):
+    """
+    Write the CDMS  global attributes to a text file. Additionally,
+     within the file there will be a description of where all the different
+     outputs go and how to best utlize this program.
+    
+    Parameters
+    ----------      
+    filename : str
+        The name of the original '.nc' input file.
+    
+    """
+    x0 = "README / cdms_reader.py Program Use and Description:\n"
+    x1 = "\nThe cdms_reader.py program reads a CDMS netCDF (a NETCDF file with 
a matchIDs variable)\n"
+    x2 = "file into memory, assembles a list of matches of satellite and in 
situ data\n"
+    x3 = "(or a primary and secondary dataset), and optionally\n"
+    x4 = "output the matches to a CSV file. Each matched pair contains one 
satellite\n"
+    x5 = "data record and one in situ data record.\n"
+    x6 = "\nBelow, this file wil list the global attributes of the .nc 
(NETCDF) file.\n"
+    x7 = "If you wish to see a full dump of the data from the .nc file,\n"
+    x8 = "please utilize the ncdump command from NETCDF (or look at the CSV 
file).\n"
+    try:
+        with Dataset(filename, "r", format="NETCDF4") as ncFile:
+            txtName = filename.replace(".nc", ".txt")
+            with open(txtName, "w") as txt:
+                txt.write(x0 + x1 +x2 +x3 + x4 + x5 + x6 + x7 + x8)
+                txt.write("\nGlobal Attributes:")
+                for x in ncFile.ncattrs():
+                    txt.write(f'\t :{x} = "{ncFile.getncattr(x)}" ;\n')
+
+
+    except (OSError, IOError) as err:
+        LOGGER.exception("Error reading netCDF file " + filename)
+        print("Error reading file!")
+        raise err
+
+def create_logs(user_option, logName):
+    """
+    Write the CDMS log information to a file. Additionally, the user may
+    opt to print this information directly to stdout, or discard it entirely.
+    
+    Parameters
+    ----------      
+    user_option : str
+        The result of the arg.log 's interpretation of
+         what option the user selected.
+    logName : str
+        The name of the log file we wish to write to,
+        assuming the user did not use the -l option.
+    """
+    if user_option == 'N':
+        print("** Note: No log was created **")
+
+
+    elif user_option == '1':
+        #prints the log contents to stdout
+        logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s',
+                        level=logging.INFO,
+                        datefmt='%Y-%m-%d %H:%M:%S',
+                        handlers=[
+                            logging.StreamHandler(sys.stdout)
+                            ])
+                
+    else:
+        #prints log to a .log file
+        logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s',
+                        level=logging.INFO,
+                        datefmt='%Y-%m-%d %H:%M:%S',
+                        handlers=[
+                            logging.FileHandler(logName)
+                            ])
+        if user_option != 1 and user_option != 'Y':
+            print(f"** Bad usage of log option. Log will print to {logName} 
**")
+
+    
+
+
+
+if __name__ == '__main__':
+    """
+    Execution:
+        python cdms_reader.py filename
+        OR
+        python3 cdms_reader.py filename 
+        OR
+        python3 cdms_reader.py filename -c -g 
+        OR
+        python3 cdms_reader.py filename --csv --meta
+
+    Note (For Help Try):
+            python3 cdms_reader.py -h
+            OR
+            python3 cdms_reader.py --help
+
+    """
+   
+    u0 = '\n%(prog)s -h OR --help \n'
+    u1 = '%(prog)s filename -c -g\n%(prog)s filename --csv --meta\n'
+    u2 ='Use -l OR -l1 to modify destination of logs'
+    p = argparse.ArgumentParser(usage= u0 + u1 + u2)
+
+    #below block is to customize user options
+    p.add_argument('filename', help='CDMS netCDF file to read')
+    p.add_argument('-c', '--csv', nargs='?', const= 'Y', default='N',
+     help='Use -c or --csv to retrieve CSV output')
+    p.add_argument('-g', '--meta', nargs='?', const='Y', default='N',
+     help='Use -g or --meta to retrieve global attributes / metadata')
+    p.add_argument('-l', '--log', nargs='?', const='N', default='Y',
+     help='Use -l or --log to AVOID creating log files, OR use -l1 to print to 
stdout/console') 
+
+    #arguments are processed by the next line
+    args = p.parse_args()
+
+    logName = args.filename.replace(".nc", ".log")
+    create_logs(args.log, logName)
+    
+    cdms_matches = assemble_matches(args.filename)
+
+    if args.csv == 'Y' :
+        matches_to_csv(cdms_matches, args.filename.replace(".nc",".csv"))
+
+    if args.meta == 'Y' :
+        get_globals(args.filename)
+
+
+
+
+    
+
+    
+    
\ No newline at end of file
diff --git a/tests/regression/conftest.py b/tests/conftest.py
similarity index 74%
rename from tests/regression/conftest.py
rename to tests/conftest.py
index a99e35c..2bb4e42 100644
--- a/tests/regression/conftest.py
+++ b/tests/conftest.py
@@ -17,8 +17,12 @@ import pytest
 
 
 def pytest_addoption(parser):
-    parser.addoption("--skip-matchup", action="store_true")
-    parser.addoption("--force-subset", action="store_true")
+    parser.addoption("--skip-matchup", action="store_true",
+                     help="Skip matchup_spark test. (Only for script testing 
purposes)")
+    parser.addoption('--matchup-warn-on-miscount', action='store_false',
+                     help='Issue a warning for matchup tests if they return an 
unexpected number of matches; '
+                          'otherwise fail')
+
 
 def pytest_collection_modifyitems(config, items):
     skip_matchup = config.getoption("--skip-matchup")
@@ -28,11 +32,3 @@ def pytest_collection_modifyitems(config, items):
         for item in items:
             if "matchup_spark" in item.name:
                 item.add_marker(skip)
-
-    force = config.getoption("--force-subset")
-
-    if not force:
-        skip = pytest.mark.skip(reason="Waiting for Zarr integration before 
this case is run")
-        for item in items:
-            if "cdmssubset" in item.name:
-                item.add_marker(skip)
diff --git a/tests/download_data.sh b/tests/download_data.sh
new file mode 100755
index 0000000..ed88e83
--- /dev/null
+++ b/tests/download_data.sh
@@ -0,0 +1,342 @@
+#!/bin/bash
+
+GREP_OPTIONS=''
+
+cookiejar=$(mktemp cookies.XXXXXXXXXX)
+netrc=$(mktemp netrc.XXXXXXXXXX)
+chmod 0600 "$cookiejar" "$netrc"
+function finish {
+  rm -rf "$cookiejar" "$netrc"
+}
+
+trap finish EXIT
+WGETRC="$wgetrc"
+
+prompt_credentials() {
+    echo "Enter your Earthdata Login or other provider supplied credentials"
+    read -p "Username: " username
+    username=${username}
+    if [ -z "${username}" ]; then
+      exit_with_error "Username is required"
+    fi
+    read -s -p "Password: " password
+    if [ -z "${password}" ]; then
+      exit_with_error "Password is required"
+    fi
+    echo "machine urs.earthdata.nasa.gov login $username password $password" 
>> $netrc
+    echo
+}
+
+exit_with_error() {
+    echo
+    echo "Unable to Retrieve Data"
+    echo
+    echo $1
+    echo
+    exit 1
+}
+
+if [ -z "${DATA_DIRECTORY}" ]; then
+  exit_with_error "DATA_DIRECTORY variable unset or empty. This is needed so 
we know where to store the data"
+fi
+
+prompt_credentials
+  detect_app_approval() {
+    approved=`curl -s -b "$cookiejar" -c "$cookiejar" -L --max-redirs 5 
--netrc-file "$netrc" 
https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/OISSS_L4_multimission_7day_v1/OISSS_L4_multimission_global_7d_v1.0_2018-10-02.nc
 -w '\n%{http_code}' | tail  -1`
+    if [ "$approved" -ne "200" ] && [ "$approved" -ne "301" ] && [ "$approved" 
-ne "302" ]; then
+        # User didn't approve the app. Direct users to approve the app in URS
+        exit_with_error "Provided credentials are unauthorized to download the 
data"
+    fi
+}
+
+setup_auth_curl() {
+    # Firstly, check if it require URS authentication
+    status=$(curl -s -z "$(date)" -w '\n%{http_code}' 
https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/OISSS_L4_multimission_7day_v1/OISSS_L4_multimission_global_7d_v1.0_2018-10-02.nc
 | tail -1)
+    if [[ "$status" -ne "200" && "$status" -ne "304" ]]; then
+        # URS authentication is required. Now further check if the 
application/remote service is approved.
+        detect_app_approval
+    fi
+}
+
+setup_auth_wget() {
+    # The safest way to auth via curl is netrc. Note: there's no checking or 
feedback
+    # if login is unsuccessful
+    touch ~/.netrc
+    chmod 0600 ~/.netrc
+    credentials=$(grep 'machine urs.earthdata.nasa.gov' ~/.netrc)
+    if [ -z "$credentials" ]; then
+        cat "$netrc" >> ~/.netrc
+    fi
+}
+
+fetch_urls() {
+  echo "Downloading files for collection ${collection}"
+
+  download_dir=${DATA_DIRECTORY}/$collection
+  mkdir -p $download_dir
+
+  if command -v curl >/dev/null 2>&1; then
+      setup_auth_curl
+      while read -r line; do
+        # Get everything after the last '/'
+        filename="${line##*/}"
+
+        # Strip everything after '?'
+        stripped_query_params="${filename%%\?*}"
+
+        echo "Downloading ${line}"
+        curl -s -f -b "$cookiejar" -c "$cookiejar" -L --netrc-file "$netrc" -g 
-o $download_dir/$stripped_query_params -- $line || exit_with_error "Command 
failed with error. Please retrieve the data manually."
+      done;
+  elif command -v wget >/dev/null 2>&1; then
+      # We can't use wget to poke provider server to get info whether or not 
URS was integrated without download at least one of the files.
+      echo
+      echo "WARNING: Can't find curl, use wget instead."
+      echo "WARNING: Script may not correctly identify Earthdata Login 
integrations."
+      echo
+      setup_auth_wget
+      while read -r line; do
+        # Get everything after the last '/'
+        filename="${line##*/}"
+
+        # Strip everything after '?'
+        stripped_query_params="${filename%%\?*}"
+
+        echo "Downloading ${line}"
+        wget -q --load-cookies "$cookiejar" --save-cookies "$cookiejar" 
--output-document $download_dir/$stripped_query_params --keep-session-cookies 
-- $line && echo || exit_with_error "Command failed with error. Please retrieve 
the data manually."
+      done;
+  else
+      exit_with_error "Error: Could not find a command-line downloader.  
Please install curl or wget"
+  fi
+}
+
+collection="MUR25-JPL-L4-GLOB-v04.2_test"
+
+fetch_urls <<'EDSCEOF'
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20181001090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180930090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180929090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180928090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180927090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180926090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180925090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180924090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180923090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180922090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180921090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180920090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180919090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180918090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180917090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180916090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180915090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180914090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180913090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180912090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180911090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180910090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180909090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180908090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180907090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180906090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180905090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180904090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180903090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180902090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180901090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180831090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180830090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180829090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180828090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180827090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180826090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180825090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180824090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180823090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180822090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180821090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180820090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180819090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180818090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180817090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180816090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180815090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180814090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180813090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180812090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180811090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180810090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180809090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180808090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180807090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180806090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180805090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180804090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180803090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180802090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180801090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180731090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180730090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180729090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180728090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180727090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180726090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180725090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180724090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180723090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180722090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180721090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180720090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180719090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180718090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180717090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180716090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180715090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180714090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180713090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180712090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180711090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180710090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180709090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180708090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180707090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180706090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/MUR25-JPL-L4-GLOB-v04.2/20180705090000-JPL-L4_GHRSST-SSTfnd-MUR25-GLOB-v02.0-fv04.2.nc
+EDSCEOF
+
+collection="ASCATB-L2-Coastal_test"
+
+fetch_urls <<'EDSCEOF'
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180704_041200_metopb_30055_eps_o_coa_2401_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180704_174500_metopb_30063_eps_o_coa_2401_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180705_053300_metopb_30070_eps_o_coa_2401_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180705_172400_metopb_30077_eps_o_coa_2401_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180706_051200_metopb_30084_eps_o_coa_2401_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180706_170300_metopb_30091_eps_o_coa_2401_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180801_025100_metopb_30452_eps_o_coa_2401_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180801_144200_metopb_30459_eps_o_coa_2401_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180801_162400_metopb_30460_eps_o_coa_2401_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180802_023000_metopb_30466_eps_o_coa_2401_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180802_041200_metopb_30467_eps_o_coa_2401_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180802_160300_metopb_30474_eps_o_coa_2401_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180924_073900_metopb_31222_eps_o_coa_2401_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180924_091800_metopb_31223_eps_o_coa_2401_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180924_210900_metopb_31230_eps_o_coa_2401_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180925_085700_metopb_31237_eps_o_coa_3201_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180925_204800_metopb_31244_eps_o_coa_3201_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180926_083600_metopb_31251_eps_o_coa_3201_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180926_202700_metopb_31258_eps_o_coa_3201_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180926_220900_metopb_31259_eps_o_coa_3201_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180927_081500_metopb_31265_eps_o_coa_3201_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180927_095700_metopb_31266_eps_o_coa_3201_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180927_200600_metopb_31272_eps_o_coa_3201_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180927_214800_metopb_31273_eps_o_coa_3201_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180928_075400_metopb_31279_eps_o_coa_3201_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180928_093600_metopb_31280_eps_o_coa_3201_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180928_194500_metopb_31286_eps_o_coa_3201_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180928_212700_metopb_31287_eps_o_coa_3201_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180929_091500_metopb_31294_eps_o_coa_3201_ovw.l2.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/ASCATB-L2-Coastal/ascat_20180929_210600_metopb_31301_eps_o_coa_3201_ovw.l2.nc
+EDSCEOF
+
+collection="VIIRS_NPP-2018_Heatwave_test"
+
+fetch_urls <<'EDSCEOF'
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/VIIRS_NPP-JPL-L2P-v2016.2/20180705204800-JPL-L2P_GHRSST-SSTskin-VIIRS_NPP-D-v02.0-fv01.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/VIIRS_NPP-JPL-L2P-v2016.2/20180705093001-JPL-L2P_GHRSST-SSTskin-VIIRS_NPP-N-v02.0-fv01.0.nc
+EDSCEOF
+
+collection="OISSS_L4_multimission_7day_v1_test"
+
+fetch_urls <<'EDSCEOF'
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/OISSS_L4_multimission_7day_v1/OISSS_L4_multimission_global_7d_v1.0_2018-10-02.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/OISSS_L4_multimission_7day_v1/OISSS_L4_multimission_global_7d_v1.0_2018-09-28.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/OISSS_L4_multimission_7day_v1/OISSS_L4_multimission_global_7d_v1.0_2018-09-24.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/OISSS_L4_multimission_7day_v1/OISSS_L4_multimission_global_7d_v1.0_2018-09-20.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/OISSS_L4_multimission_7day_v1/OISSS_L4_multimission_global_7d_v1.0_2018-09-16.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/OISSS_L4_multimission_7day_v1/OISSS_L4_multimission_global_7d_v1.0_2018-09-12.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/OISSS_L4_multimission_7day_v1/OISSS_L4_multimission_global_7d_v1.0_2018-09-08.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/OISSS_L4_multimission_7day_v1/OISSS_L4_multimission_global_7d_v1.0_2018-09-04.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/OISSS_L4_multimission_7day_v1/OISSS_L4_multimission_global_7d_v1.0_2018-08-31.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/OISSS_L4_multimission_7day_v1/OISSS_L4_multimission_global_7d_v1.0_2018-08-27.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/OISSS_L4_multimission_7day_v1/OISSS_L4_multimission_global_7d_v1.0_2018-08-23.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/OISSS_L4_multimission_7day_v1/OISSS_L4_multimission_global_7d_v1.0_2018-08-19.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/OISSS_L4_multimission_7day_v1/OISSS_L4_multimission_global_7d_v1.0_2018-08-15.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/OISSS_L4_multimission_7day_v1/OISSS_L4_multimission_global_7d_v1.0_2018-08-11.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/OISSS_L4_multimission_7day_v1/OISSS_L4_multimission_global_7d_v1.0_2018-08-07.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/OISSS_L4_multimission_7day_v1/OISSS_L4_multimission_global_7d_v1.0_2018-08-03.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/OISSS_L4_multimission_7day_v1/OISSS_L4_multimission_global_7d_v1.0_2018-07-30.nc
+EDSCEOF
+
+collection="SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5_test"
+
+fetch_urls <<'EDSCEOF'
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/273/SMAP_L3_SSS_20181004_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/272/SMAP_L3_SSS_20181003_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/271/SMAP_L3_SSS_20181002_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/270/SMAP_L3_SSS_20181001_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/269/SMAP_L3_SSS_20180930_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/268/SMAP_L3_SSS_20180929_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/267/SMAP_L3_SSS_20180928_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/266/SMAP_L3_SSS_20180927_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/265/SMAP_L3_SSS_20180926_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/264/SMAP_L3_SSS_20180925_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/263/SMAP_L3_SSS_20180924_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/262/SMAP_L3_SSS_20180923_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/261/SMAP_L3_SSS_20180922_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/260/SMAP_L3_SSS_20180921_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/259/SMAP_L3_SSS_20180920_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/258/SMAP_L3_SSS_20180919_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/257/SMAP_L3_SSS_20180918_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/256/SMAP_L3_SSS_20180917_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/255/SMAP_L3_SSS_20180916_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/254/SMAP_L3_SSS_20180915_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/253/SMAP_L3_SSS_20180914_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/252/SMAP_L3_SSS_20180913_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/251/SMAP_L3_SSS_20180912_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/250/SMAP_L3_SSS_20180911_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/249/SMAP_L3_SSS_20180910_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/248/SMAP_L3_SSS_20180909_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/247/SMAP_L3_SSS_20180908_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/246/SMAP_L3_SSS_20180907_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/245/SMAP_L3_SSS_20180906_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/244/SMAP_L3_SSS_20180905_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/243/SMAP_L3_SSS_20180904_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/242/SMAP_L3_SSS_20180903_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/241/SMAP_L3_SSS_20180902_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/240/SMAP_L3_SSS_20180901_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/239/SMAP_L3_SSS_20180831_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/238/SMAP_L3_SSS_20180830_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/237/SMAP_L3_SSS_20180829_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/236/SMAP_L3_SSS_20180828_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/235/SMAP_L3_SSS_20180827_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/234/SMAP_L3_SSS_20180826_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/233/SMAP_L3_SSS_20180825_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/232/SMAP_L3_SSS_20180824_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/231/SMAP_L3_SSS_20180823_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/230/SMAP_L3_SSS_20180822_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/229/SMAP_L3_SSS_20180821_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/228/SMAP_L3_SSS_20180820_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/227/SMAP_L3_SSS_20180819_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/226/SMAP_L3_SSS_20180818_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/225/SMAP_L3_SSS_20180817_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/224/SMAP_L3_SSS_20180816_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/223/SMAP_L3_SSS_20180815_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/222/SMAP_L3_SSS_20180814_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/221/SMAP_L3_SSS_20180813_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/220/SMAP_L3_SSS_20180812_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/219/SMAP_L3_SSS_20180811_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/218/SMAP_L3_SSS_20180810_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/217/SMAP_L3_SSS_20180809_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/216/SMAP_L3_SSS_20180808_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/215/SMAP_L3_SSS_20180807_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/214/SMAP_L3_SSS_20180806_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/213/SMAP_L3_SSS_20180805_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/212/SMAP_L3_SSS_20180804_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/211/SMAP_L3_SSS_20180803_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/210/SMAP_L3_SSS_20180802_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/209/SMAP_L3_SSS_20180801_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/208/SMAP_L3_SSS_20180731_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/207/SMAP_L3_SSS_20180730_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/206/SMAP_L3_SSS_20180729_8DAYS_V5.0.nc
+https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5/2018/205/SMAP_L3_SSS_20180728_8DAYS_V5.0.nc
+EDSCEOF
diff --git a/tests/regression/cdms_reader.py b/tests/regression/cdms_reader.py
deleted file mode 120000
index 3c3895c..0000000
--- a/tests/regression/cdms_reader.py
+++ /dev/null
@@ -1 +0,0 @@
-../../tools/cdms/cdms_reader.py
\ No newline at end of file
diff --git a/tests/regression/test_cdms.py b/tests/regression/test_cdms.py
deleted file mode 100644
index 3c95bca..0000000
--- a/tests/regression/test_cdms.py
+++ /dev/null
@@ -1,746 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import copy
-import csv
-import io
-import os
-import warnings
-from datetime import datetime
-from math import cos, radians
-from tempfile import NamedTemporaryFile as Temp
-from urllib.parse import urljoin
-from zipfile import ZipFile
-
-import pandas as pd
-import pytest
-import requests
-from bs4 import BeautifulSoup
-from dateutil.parser import parse
-from pytz import timezone, UTC
-from shapely import wkt
-from shapely.geometry import Polygon, Point, box
-
-import cdms_reader
-
-
-#########################
-#
-# export TEST_HOST=http://localhost:8083/
-# unset TEST_HOST
-#
-#########################
-
-
[email protected]()
-def host():
-    return os.getenv('TEST_HOST', 'http://doms.jpl.nasa.gov')
-
-
[email protected]()
-def insitu_endpoint():
-    return os.getenv(
-        'INSITU_ENDPOINT',
-        'http://doms.jpl.nasa.gov/insitu/1.0/query_data_doms_custom_pagination'
-    )
-
-
[email protected]()
-def insitu_swagger_endpoint():
-    return os.getenv(
-        'INSITU_SWAGGER_ENDPOINT',
-        'http://doms.jpl.nasa.gov/insitu/1.0/insitu_query_swagger/'
-    )
-
-
[email protected](scope="module")
-def eid():
-    return {
-        'successful': False,
-        'eid': [],
-        'params': []
-    }
-
-
-def skip(msg=""):
-    raise pytest.skip(msg)
-
-
-def b_to_polygon(b):
-    west, south, east, north = [float(p) for p in b.split(",")]
-    polygon = Polygon([(west, south), (east, south), (east, north), (west, 
north), (west, south)])
-    return polygon
-
-
-def iso_time_to_epoch(str_time):
-    EPOCH = timezone('UTC').localize(datetime(1970, 1, 1))
-
-    return (datetime.strptime(str_time, "%Y-%m-%dT%H:%M:%SZ").replace(
-        tzinfo=UTC) - EPOCH).total_seconds()
-
-
-def expand_by_tolerance(point, rt):
-    def add_meters_to_lon_lat(point, meters):
-        lon = point.x
-        lat = point.y
-
-        longitude = lon + ((meters / 111111) * cos(radians(lat)))
-        latitude = lat + (meters / 111111)
-
-        return longitude, latitude
-
-    min_lon, min_lat = add_meters_to_lon_lat(point, -1 * rt)
-    max_lon, max_lat = add_meters_to_lon_lat(point, rt)
-
-    return box(min_lon, min_lat, max_lon, max_lat)
-
-
-def translate_global_rows(rows):
-    translated = {}
-
-    for row in rows:
-        parts = row.split(',', 1)
-        translated[parts[0]] = parts[1]
-
-    return translated
-
-
-def translate_matchup_rows(rows):
-    headers = rows[0].split(',')
-
-    translated_rows = []
-
-    for row in rows[1:]:
-        translated_row = {}
-
-        buf = io.StringIO(row)
-        reader = csv.reader(buf)
-        fields = list(reader)[0]
-
-        assert len(headers) == len(fields)
-
-        for i, field in enumerate(fields):
-            header = headers[i]
-
-            if header not in translated_row:
-                translated_row[header] = field
-            else:
-                translated_row[f"{header}_secondary"] = field
-
-        translated_rows.append(translated_row)
-
-    return translated_rows
-
-
-def lat_lon_to_point(lat, lon):
-    return wkt.loads(f"Point({lon} {lat})")
-
-
-def format_time(timestamp):
-    t = parse(timestamp)
-
-    ISO_8601 = '%Y-%m-%dT%H:%M:%SZ'
-
-    return t.strftime(ISO_8601)
-
-
-def verify_match(match, point, time, s_point, s_time, params, bounding_poly):
-    # Check primary point is as expected
-    assert match['point'] == point
-    assert match['time'] == time
-
-    # Check primary point within search bounds
-    assert iso_time_to_epoch(params['startTime']) \
-           <= match['time'] \
-           <= iso_time_to_epoch(params['endTime'])
-    assert bounding_poly.contains(wkt.loads(match['point']))
-
-    secondary = match['matches'][0]
-
-    # Check secondary point is as expected
-    assert secondary['point'] == s_point
-    assert secondary['time'] == s_time
-
-    # Check secondary point within specified spatial & temporal tolerances for 
matched primary
-    assert expand_by_tolerance(
-        wkt.loads(match['point']),
-        params['rt']
-    ).contains(wkt.loads(secondary['point']))
-
-    assert (match['time'] - params['tt']) \
-           <= secondary['time'] \
-           <= (match['time'] + params['tt'])
-
-
[email protected]
-def test_matchup_spark(host, eid):
-    url = urljoin(host, 'match_spark')
-
-    params = {
-        "primary": "MUR25-JPL-L4-GLOB-v04.2",
-        "startTime": "2018-08-01T09:00:00Z",
-        "endTime": "2018-09-01T00:00:00Z",
-        "tt": 43200,
-        "rt": 1000,
-        "b": "-100,20,-79,30",
-        "depthMin": -20,
-        "depthMax": 10,
-        "matchOnce": True,
-        "secondary": "ICOADS Release 3.0",
-        "resultSizeLimit": 7000,
-        "platforms": "42"
-    }
-
-    response = requests.get(url, params=params)
-
-    assert response.status_code == 200
-
-    bounding_poly = b_to_polygon(params['b'])
-
-    body = response.json()
-    data = body['data']
-
-    assert body['count'] == len(data)
-
-    data.sort(key=lambda e: e['point'])
-    body['data'] = data
-
-    eid['eid'].append(body['executionId'])
-    eid['params'].append(copy.deepcopy(params))
-
-    verify_match(
-        data[0],    'Point(-86.125 27.625)',
-        1535360400, 'Point(-86.13 27.63)',
-        1535374800,  params, bounding_poly
-    )
-
-    verify_match(
-        data[1],    'Point(-90.125 27.625)',
-        1534496400, 'Point(-90.13 27.63)',
-        1534491000,  params, bounding_poly
-    )
-
-    verify_match(
-        data[2],    'Point(-90.125 28.125)',
-        1534928400, 'Point(-90.13 28.12)',
-        1534899600,  params, bounding_poly
-    )
-
-    verify_match(
-        data[3],    'Point(-90.375 28.125)',
-        1534842000, 'Point(-90.38 28.12)',
-        1534813200,  params, bounding_poly
-    )
-
-    params['primary'] = 'JPL-L4-MRVA-CHLA-GLOB-v3.0'
-
-    response = requests.get(url, params=params)
-
-    assert response.status_code == 200
-
-    body = response.json()
-
-    data = body['data']
-
-    assert body['count'] == len(data)
-
-    data.sort(key=lambda e: e['point'])
-    body['data'] = data
-
-    eid['eid'].append(body['executionId'])
-    eid['params'].append(copy.deepcopy(params))
-
-    verify_match(
-        data[0],    'Point(-86.125 27.625)',
-        1535371200, 'Point(-86.13 27.63)',
-        1535374800,  params, bounding_poly
-    )
-
-    verify_match(
-        data[1],    'Point(-90.125 27.625)',
-        1534507200, 'Point(-90.13 27.63)',
-        1534491000,  params, bounding_poly
-    )
-
-    verify_match(
-        data[2],    'Point(-90.125 28.125)',
-        1534939200, 'Point(-90.13 28.12)',
-        1534899600,  params, bounding_poly
-    )
-
-    verify_match(
-        data[3],    'Point(-90.375 28.125)',
-        1534852800, 'Point(-90.38 28.12)',
-        1534813200,  params, bounding_poly
-    )
-
-    eid['successful'] = True
-
-
[email protected]
-def test_domsresults_json(host, eid):
-    url = urljoin(host, 'domsresults')
-
-    # Skip the test automatically if the matchup request was not successful
-    if not eid['successful']:
-        skip('Matchup request was unsuccessful so there are no results to get 
from domsresults')
-
-    def fetch_result(eid, output):
-        return requests.get(url, params={"id": eid, "output": output})
-
-    eids = eid['eid']
-    param_list = eid['params']
-
-    response = fetch_result(eids[0], "JSON")
-
-    assert response.status_code == 200
-
-    body = response.json()
-
-    data = body['data']
-    assert len(data) == 4
-
-    for m in data:
-        m['point'] = f"Point({m['lon']} {m['lat']})"
-        for s in m['matches']:
-            s['point'] = f"Point({s['lon']} {s['lat']})"
-
-    data.sort(key=lambda e: e['point'])
-
-    params = param_list[0]
-    bounding_poly = b_to_polygon(params['b'])
-
-    verify_match(data[0],    'Point(-86.125 27.625)',
-                 1535360400, 'Point(-86.13 27.63)',
-                 1535374800, params, bounding_poly
-                 )
-
-    verify_match(data[1],    'Point(-90.125 27.625)',
-                 1534496400, 'Point(-90.13 27.63)',
-                 1534491000,  params, bounding_poly
-                 )
-
-    verify_match(data[2],    'Point(-90.125 28.125)',
-                 1534928400, 'Point(-90.13 28.12)',
-                 1534899600,  params, bounding_poly
-                 )
-
-    verify_match(data[3],    'Point(-90.375 28.125)',
-                 1534842000, 'Point(-90.38 28.12)',
-                 1534813200,  params, bounding_poly
-                 )
-
-    response = fetch_result(eids[1], "JSON")
-
-    assert response.status_code == 200
-
-    body = response.json()
-
-    data = body['data']
-    assert len(data) == 4
-
-    for m in data:
-        m['point'] = f"Point({m['lon']} {m['lat']})"
-        for s in m['matches']:
-            s['point'] = f"Point({s['lon']} {s['lat']})"
-
-    data.sort(key=lambda e: e['point'])
-
-    params = param_list[1]
-    bounding_poly = b_to_polygon(params['b'])
-
-    verify_match(data[0],    'Point(-86.125 27.625)',
-                 1535371200, 'Point(-86.13 27.63)',
-                 1535374800,  params, bounding_poly
-                 )
-
-    verify_match(data[1],    'Point(-90.125 27.625)',
-                 1534507200, 'Point(-90.13 27.63)',
-                 1534491000,  params, bounding_poly
-                 )
-
-    verify_match(data[2],    'Point(-90.125 28.125)',
-                 1534939200, 'Point(-90.13 28.12)',
-                 1534899600,  params, bounding_poly
-                 )
-
-    verify_match(data[3],    'Point(-90.375 28.125)',
-                 1534852800, 'Point(-90.38 28.12)',
-                 1534813200,  params, bounding_poly
-                 )
-
-
[email protected]
-def test_domsresults_csv(host, eid):
-    url = urljoin(host, 'domsresults')
-
-    # Skip the test automatically if the matchup request was not successful
-    if not eid['successful']:
-        skip('Matchup request was unsuccessful so there are no results to get 
from domsresults')
-
-    def fetch_result(eid, output):
-        return requests.get(url, params={"id": eid, "output": output})
-
-    eids = eid['eid']
-    param_list = eid['params']
-
-    response = fetch_result(eids[0], "CSV")
-    params = param_list[0]
-    bounding_poly = b_to_polygon(params['b'])
-
-    assert response.status_code == 200
-
-    rows = response.text.split('\r\n')
-    index = rows.index('')
-
-    global_rows = rows[:index]
-    matchup_rows = rows[index + 1:-1]  # Drop trailing empty string from 
trailing newline
-
-    global_rows = translate_global_rows(global_rows)
-    matchup_rows = translate_matchup_rows(matchup_rows)
-
-    assert len(matchup_rows) == int(global_rows['CDMS_num_primary_matched'])
-
-    for row in matchup_rows:
-        primary_point = lat_lon_to_point(row['lat'], row['lon'])
-
-        assert bounding_poly.contains(primary_point)
-        assert params['startTime'] <= format_time(row['time']) <= 
params['endTime']
-
-        secondary_point = lat_lon_to_point(row['lat_secondary'], 
row['lon_secondary'])
-
-        assert expand_by_tolerance(primary_point, 
params['rt']).contains(secondary_point)
-        assert (iso_time_to_epoch(params['startTime']) - params['tt']) \
-               <= iso_time_to_epoch(format_time(row['time_secondary'])) \
-               <= (iso_time_to_epoch(params['endTime']) + params['tt'])
-
-    response = fetch_result(eids[1], "CSV")
-    params = param_list[1]
-    bounding_poly = b_to_polygon(params['b'])
-
-    assert response.status_code == 200
-
-    rows = response.text.split('\r\n')
-    index = rows.index('')
-
-    global_rows = rows[:index]
-    matchup_rows = rows[index + 1:-1]  # Drop trailing empty string from 
trailing newline
-
-    global_rows = translate_global_rows(global_rows)
-    matchup_rows = translate_matchup_rows(matchup_rows)
-
-    assert len(matchup_rows) == int(global_rows['CDMS_num_primary_matched'])
-
-    for row in matchup_rows:
-        primary_point = lat_lon_to_point(row['lat'], row['lon'])
-
-        assert bounding_poly.contains(primary_point)
-        assert params['startTime'] <= format_time(row['time']) <= 
params['endTime']
-
-        secondary_point = lat_lon_to_point(row['lat_secondary'], 
row['lon_secondary'])
-
-        assert expand_by_tolerance(primary_point, 
params['rt']).contains(secondary_point)
-        assert (iso_time_to_epoch(params['startTime']) - params['tt']) \
-               <= iso_time_to_epoch(format_time(row['time_secondary'])) \
-               <= (iso_time_to_epoch(params['endTime']) + params['tt'])
-
-
[email protected]
[email protected]
-def test_domsresults_netcdf(host, eid):
-    warnings.filterwarnings('ignore')
-
-    url = urljoin(host, 'domsresults')
-
-    # Skip the test automatically if the matchup request was not successful
-    if not eid['successful']:
-        skip('Matchup request was unsuccessful so there are no results to get 
from domsresults')
-
-    def fetch_result(eid, output):
-        return requests.get(url, params={"id": eid, "output": output})
-
-    eids = eid['eid']
-    param_list = eid['params']
-
-    temp_file = Temp(mode='wb+', suffix='.csv.tmp', prefix='CDMSReader_')
-
-    response = fetch_result(eids[0], "NETCDF")
-    params = param_list[0]
-    bounding_poly = b_to_polygon(params['b'])
-
-    assert response.status_code == 200
-
-    temp_file.write(response.content)
-    temp_file.flush()
-    temp_file.seek(0)
-
-    matches = cdms_reader.assemble_matches(temp_file.name)
-
-    cdms_reader.matches_to_csv(matches, temp_file.name)
-
-    with open(temp_file.name) as f:
-        reader = csv.DictReader(f)
-        rows = list(reader)
-
-    for row in rows:
-        primary_point = lat_lon_to_point(row['PrimaryData_lat'], 
row['PrimaryData_lon'])
-
-        assert bounding_poly.contains(primary_point)
-        assert iso_time_to_epoch(params['startTime']) \
-               <= float(row['PrimaryData_time']) \
-               <= iso_time_to_epoch(params['endTime'])
-
-        secondary_point = lat_lon_to_point(row['SecondaryData_lat'], 
row['SecondaryData_lon'])
-
-        assert expand_by_tolerance(primary_point, 
params['rt']).contains(secondary_point)
-        assert (iso_time_to_epoch(params['startTime']) - params['tt']) \
-               <= float(row['SecondaryData_time']) \
-               <= (iso_time_to_epoch(params['endTime']) + params['tt'])
-
-    response = fetch_result(eids[1], "NETCDF")
-    params = param_list[1]
-    bounding_poly = b_to_polygon(params['b'])
-
-    assert response.status_code == 200
-
-    temp_file.write(response.content)
-    temp_file.flush()
-    temp_file.seek(0)
-
-    matches = cdms_reader.assemble_matches(temp_file.name)
-
-    cdms_reader.matches_to_csv(matches, temp_file.name)
-
-    with open(temp_file.name) as f:
-        reader = csv.DictReader(f)
-        rows = list(reader)
-
-    for row in rows:
-        primary_point = lat_lon_to_point(row['PrimaryData_lat'], 
row['PrimaryData_lon'])
-
-        assert bounding_poly.contains(primary_point)
-        assert iso_time_to_epoch(params['startTime']) \
-               <= float(row['PrimaryData_time']) \
-               <= iso_time_to_epoch(params['endTime'])
-
-        secondary_point = lat_lon_to_point(row['SecondaryData_lat'], 
row['SecondaryData_lon'])
-
-        assert expand_by_tolerance(primary_point, 
params['rt']).contains(secondary_point)
-        assert (iso_time_to_epoch(params['startTime']) - params['tt']) \
-               <= float(row['SecondaryData_time']) \
-               <= (iso_time_to_epoch(params['endTime']) + params['tt'])
-
-    temp_file.close()
-    warnings.filterwarnings('default')
-
-
[email protected]
-def test_domslist(host):
-    url = urljoin(host, 'domslist')
-
-    response = requests.get(url)
-
-    assert response.status_code == 200
-
-    body = response.json()
-
-    data = body['data']
-
-    num_satellite = len(data['satellite'])
-    num_insitu = len(data['insitu'])
-
-    assert num_insitu > 0
-    assert num_satellite > 0
-
-    # assert body['count'] == num_satellite + num_insitu
-
-
[email protected]
-def test_cdmssubset(host):
-    url = urljoin(host, 'cdmssubset')
-
-    params = {
-        "dataset": "MUR25-JPL-L4-GLOB-v04.2",
-        "parameter": "sst",
-        "startTime": "2018-09-24T00:00:00Z",
-        "endTime": "2018-09-30T00:00:00Z",
-        "b": "160,-30,180,-25",
-        "output": "ZIP"
-    }
-
-    response = requests.get(url, params=params)
-
-    assert response.status_code == 200
-
-    bounding_poly = b_to_polygon(params['b'])
-
-    response_buf = io.BytesIO(response.content)
-
-    with ZipFile(response_buf) as data:
-        namelist = data.namelist()
-
-        assert namelist == ['MUR25-JPL-L4-GLOB-v04.2.csv']
-
-        csv_buf = io.StringIO(data.read(namelist[0]).decode('utf-8'))
-        csv_data = pd.read_csv(csv_buf)
-
-    def validate_row_bounds(row):
-        assert bounding_poly.contains(Point(row['longitude'], row['latitude']))
-        assert params['startTime'] <= row['time'] <= params['endTime']
-
-    for i in range(0, len(csv_data)):
-        validate_row_bounds(csv_data.iloc[i])
-
-    params['dataset'] = 'OISSS_L4_multimission_7day_v1'
-
-    response = requests.get(url, params=params)
-
-    assert response.status_code == 200
-
-    response_buf = io.BytesIO(response.content)
-
-    with ZipFile(response_buf) as data:
-        namelist = data.namelist()
-
-        assert namelist == ['OISSS_L4_multimission_7day_v1.csv']
-
-        csv_buf = io.StringIO(data.read(namelist[0]).decode('utf-8'))
-        csv_data = pd.read_csv(csv_buf)
-
-    for i in range(0, len(csv_data)):
-        validate_row_bounds(csv_data.iloc[i])
-
-
[email protected]
-def test_insitu(insitu_endpoint):
-    params = {
-        'itemsPerPage': 1000,
-        'startTime': '2018-05-15T00:00:00Z',
-        'endTime': '2018-06-01T00:00:00Z',
-        'bbox': '-80,25,-75,30',
-        'minDepth': 0.0,
-        'maxDepth': 5.0,
-        'provider': 'NCAR',
-        'project': 'ICOADS Release 3.0',
-        'platform': '42',
-        'markerTime': '2018-05-15T00:00:00Z'
-    }
-
-    response = requests.get(insitu_endpoint, params=params)
-
-    assert response.status_code == 200
-
-    body = response.json()
-
-    if body['total'] <= params['itemsPerPage']:
-        assert body['total'] == len(body['results'])
-    else:
-        assert len(body['results']) == params['itemsPerPage']
-
-    bounding_poly = b_to_polygon(params['bbox'])
-
-    for result in body['results']:
-        assert bounding_poly.contains(
-            wkt.loads(f"Point({result['longitude']} {result['latitude']})")
-        )
-
-        if result['depth'] != -99999.0:
-            assert params['minDepth'] <= result['depth'] <= params['maxDepth']
-
-        assert params['startTime'] <= result['time'] <= params['endTime']
-
-
[email protected]
-def test_swaggerui_sdap(host):
-    url = urljoin(host, 'apidocs/')
-
-    response = requests.get(url)
-
-    assert response.status_code == 200
-    assert 'swagger-ui' in response.text
-
-    try:
-        # There's probably a better way to do this, but extract the .yml file 
for the docs from the returned text
-        soup = BeautifulSoup(response.text, 'html.parser')
-
-        script = str([tag for tag in soup.find_all('script') if tag.attrs == 
{}][0])
-
-        start_index = script.find('url:')
-        end_index = script.find('",\n', start_index)
-
-        script = script[start_index:end_index]
-
-        yml_filename = script.split('"')[1]
-
-        url = urljoin(url, yml_filename)
-
-        response = requests.get(url)
-
-        assert response.status_code == 200
-    except AssertionError:
-        raise
-    except:
-        try:
-            url = urljoin(url, 'openapi.yml')
-
-            response = requests.get(url)
-
-            assert response.status_code == 200
-
-            warnings.warn("Could not extract documentation yaml filename from 
response text, "
-                          "but using an assumed value worked successfully")
-        except:
-            raise ValueError("Could not verify documentation yaml file, 
assumed value also failed")
-
-
[email protected]
-def test_swaggerui_insitu(insitu_swagger_endpoint):
-    response = requests.get(insitu_swagger_endpoint)
-
-    assert response.status_code == 200
-    assert 'swagger-ui' in response.text
-
-    try:
-        # There's probably a better way to do this, but extract the .yml file 
for the docs from the returned text
-        soup = BeautifulSoup(response.text, 'html.parser')
-
-        script = str([tag for tag in soup.find_all('script') if tag.attrs == 
{}][0])
-
-        start_index = script.find('url:')
-        end_index = script.find('",\n', start_index)
-
-        script = script[start_index:end_index]
-
-        yml_filename = script.split('"')[1]
-
-        url = urljoin(insitu_swagger_endpoint, yml_filename)
-
-        response = requests.get(url)
-
-        assert response.status_code == 200
-    except AssertionError:
-        raise
-    except:
-        try:
-            url = urljoin(insitu_swagger_endpoint, 'insitu-spec-0.0.1.yml')
-
-            response = requests.get(url)
-
-            assert response.status_code == 200
-
-            warnings.warn("Could not extract documentation yaml filename from 
response text, "
-                          "but using an assumed value worked successfully")
-        except:
-            raise ValueError("Could not verify documentation yaml file, 
assumed value also failed")
diff --git a/tests/requirements.txt b/tests/requirements.txt
new file mode 100644
index 0000000..8172d07
--- /dev/null
+++ b/tests/requirements.txt
@@ -0,0 +1,9 @@
+pandas
+pytest
+pytest-integration
+requests
+beautifulsoup4
+python-dateutil
+pytz
+shapely
+geopy
\ No newline at end of file
diff --git a/tests/test_collections.yaml b/tests/test_collections.yaml
new file mode 100644
index 0000000..0331ebe
--- /dev/null
+++ b/tests/test_collections.yaml
@@ -0,0 +1,73 @@
+collections:
+  - id: MUR25-JPL-L4-GLOB-v04.2_test
+    path: /data/granules/MUR25-JPL-L4-GLOB-v04.2_test/*.nc
+    priority: 1
+    forward-processing-priority: 6
+    projection: Grid
+    dimensionNames:
+      latitude: lat
+      longitude: lon
+      time: time
+      variable: analysed_sst
+    slices:
+      time: 1
+      lat: 100
+      lon: 100
+  - id: ASCATB-L2-Coastal_test
+    path: /data/granules/ASCATB-L2-Coastal_test/*.nc
+    priority: 1
+    projection: SwathMulti
+    dimensionNames:
+      latitude: lat
+      longitude: lon
+      variables:
+        - wind_speed
+        - wind_dir
+      time: time
+    slices:
+      NUMROWS: 15
+      NUMCELLS: 15
+  - id: OISSS_L4_multimission_7day_v1_test
+    path: /data/granules/OISSS_L4_multimission_7day_v1_test/*.nc
+    priority: 1
+    forward-processing-priority: 1
+    projection: Grid
+    dimensionNames:
+      latitude: latitude
+      longitude: longitude
+      time: time
+      variable: sss
+    slices:
+      time: 1
+      latitude: 100
+      longitude: 100
+  - id: VIIRS_NPP-2018_Heatwave_test
+    path: /data/granules/VIIRS_NPP-2018_Heatwave_test/*.nc
+    priority: 1
+    projection: Swath
+    dimensionNames:
+      latitude: lat
+      longitude: lon
+      time: time
+      variable: sea_surface_temperature
+    slices:
+      ni: 30
+      nj: 30
+    preprocess:
+      - name: squeeze
+        dimensions:
+          - time
+  - id: SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5_test
+    path: /data/granules/SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5_test/*.nc
+    priority: 1
+    forward-processing-priority: 1
+    projection: Grid
+    dimensionNames:
+      latitude: latitude
+      longitude: longitude
+      time: time
+      variable: smap_sss
+    slices:
+      time: 1
+      latitude: 100
+      longitude: 100
\ No newline at end of file
diff --git a/tests/test_sdap.py b/tests/test_sdap.py
new file mode 100644
index 0000000..a67e8f0
--- /dev/null
+++ b/tests/test_sdap.py
@@ -0,0 +1,1300 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import csv
+import datetime
+import io
+import json
+import os
+import re
+import warnings
+from datetime import datetime
+from pathlib import Path
+from tempfile import NamedTemporaryFile as Temp
+from time import sleep
+from urllib.parse import urljoin, urlparse, urlunparse
+from zipfile import ZipFile
+
+import pandas as pd
+import pytest
+import requests
+from bs4 import BeautifulSoup
+from dateutil.parser import parse
+from geopy.distance import geodesic
+from pytz import timezone, UTC
+from shapely import wkt
+from shapely.geometry import Polygon, Point
+
+import cdms_reader
+
+
+#########################
+#
+# export TEST_HOST=http://localhost:8083/
+# unset TEST_HOST
+#
+#########################
+
+# TODO: Consider removing old helper methods & fixtures for old CDMS tests 
that aren't used here (mainly insitu stuff)
+
[email protected](scope="session")
+def host():
+    return os.getenv('TEST_HOST', 'http://localhost:8083/')
+
+
[email protected](scope="session")
+def eid():
+    return {
+        'successful': False,
+        'eid': [],
+        'params': []
+    }
+
+
+start_time = None
+
+
[email protected](scope="session")
+def start():
+    global start_time
+
+    if start_time is None:
+        start_time = datetime.now().strftime("%G%m%d%H%M%S%z")
+
+    return start_time
+
+
[email protected]()
+def timeouts():
+    connect_timeout = 9.05  # Recommended to be just above a multiple of 3 
seconds
+    read_timeout = 303  # Just above current gateway timeout
+    timeouts = (connect_timeout, read_timeout)
+
+    return timeouts
+
+
[email protected]()
+def fail_on_miscount(request):
+    return request.config.getoption('--matchup-warn-on-miscount', 
default=False)
+
+
[email protected](scope='session')
+def distance_vs_time_query(host, start):
+    result = {
+        'distances': {  # Tuples: sec_lat, sec_lon, sec_time
+            'min_dist': (),
+            'min_time': ()
+        },
+        'backup': {  # Tuples: sec_lat, sec_lon, sec_time
+            'min_dist': ("26.6141296", "-130.0827904", 1522637640),
+            'min_time': ("26.6894016", "-130.0547072", 1522626840)
+        },
+        'success': False
+    }
+
+    url = urljoin(host, 'match_spark')
+
+    params = {
+        "primary": "JPL-L4-MRVA-CHLA-GLOB-v3.0",
+        "secondary": "shark-2018",
+        "startTime": "2018-04-01T00:00:00Z",
+        "endTime": "2018-04-01T23:59:59Z",
+        "b": "-131,26,-130,27",
+        "depthMin": -5,
+        "depthMax": 5,
+        "tt": 86400,
+        "rt": 10000,
+        "matchOnce": False,
+        "resultSizeLimit": 0,
+        "platforms": "3B",
+        "parameter": "mass_concentration_of_chlorophyll_in_sea_water",
+    }
+
+    try:
+        body = run_matchup(url, params)
+
+        data = body['data']
+
+        assert body['count'] == len(data)
+        check_count(len(data), 1, True)
+
+        primary_point = data[0]
+
+        def compute_distance(primary, secondary):
+            return geodesic((primary['lat'], primary['lon']), 
(secondary['lat'], secondary['lon'])).m
+
+        def compute_time(primary, secondary):
+            return abs(primary['time'] - secondary['time'])
+
+        distances = [
+            (s['lat'], s['lon'], s['time'], compute_distance(primary_point, 
s), compute_time(primary_point, s))
+            for s in primary_point['matches']
+        ]
+
+        try_save('computed_distances', start, distances)
+
+        min_dist = min(distances, key=lambda x: x[3])
+        min_time = min(distances, key=lambda x: x[4])
+
+        result['distances']['min_dist'] = min_dist[:3]
+        result['distances']['min_time'] = min_time[:3]
+
+        result['success'] = True
+    except:
+        warnings.warn('Could not determine point distances for prioritization 
tests, using backup values instead')
+
+    return result
+
+
[email protected]()
+def matchup_params():
+    return {
+        'gridded_to_gridded': {
+            "primary": "MUR25-JPL-L4-GLOB-v04.2_test",
+            "secondary": "SMAP_JPL_L3_SSS_CAP_8DAY-RUNNINGMEAN_V5_test",
+            "startTime": "2018-08-01T00:00:00Z",
+            "endTime": "2018-08-02T00:00:00Z",
+            "b": "-100,20,-90,30",
+            "depthMin": -20,
+            "depthMax": 10,
+            "tt": 43200,
+            "rt": 1000,
+            "matchOnce": True,
+            "resultSizeLimit": 7000,
+            "platforms": "42"
+        },
+        'gridded_to_swath': {
+            "primary": "MUR25-JPL-L4-GLOB-v04.2_test",
+            "secondary": "ASCATB-L2-Coastal_test",
+            "startTime": "2018-07-05T00:00:00Z",
+            "endTime": "2018-07-05T23:59:59Z",
+            "b": "-127,32,-120,40",
+            "depthMin": -20,
+            "depthMax": 10,
+            "tt": 12000,
+            "rt": 1000,
+            "matchOnce": True,
+            "resultSizeLimit": 7000,
+            "platforms": "42"
+        },
+        'swath_to_gridded': {
+            "primary": "ASCATB-L2-Coastal_test",
+            "secondary": "MUR25-JPL-L4-GLOB-v04.2_test",
+            "startTime": "2018-08-01T00:00:00Z",
+            "endTime": "2018-08-02T00:00:00Z",
+            "b": "-100,20,-90,30",
+            "depthMin": -20,
+            "depthMax": 10,
+            "tt": 43200,
+            "rt": 1000,
+            "matchOnce": True,
+            "resultSizeLimit": 7000,
+            "platforms": "65"
+        },
+        'swath_to_swath': {
+            "primary": "VIIRS_NPP-2018_Heatwave_test",
+            "secondary": "ASCATB-L2-Coastal_test",
+            "startTime": "2018-07-05T00:00:00Z",
+            "endTime": "2018-07-05T23:59:59Z",
+            "b": "-120,28,-118,30",
+            "depthMin": -20,
+            "depthMax": 10,
+            "tt": 43200,
+            "rt": 1000,
+            "matchOnce": True,
+            "resultSizeLimit": 7000,
+            "platforms": "42"
+        },
+        'long': {  # TODO: Find something for this; it's copied atm
+            "primary": "VIIRS_NPP-2018_Heatwave_test",
+            "secondary": "ASCATB-L2-Coastal_test",
+            "startTime": "2018-07-05T00:00:00Z",
+            "endTime": "2018-07-05T23:59:59Z",
+            "b": "-120,28,-118,30",
+            "depthMin": -20,
+            "depthMax": 10,
+            "tt": 43200,
+            "rt": 1000,
+            "matchOnce": True,
+            "resultSizeLimit": 7000,
+            "platforms": "42"
+        },
+    }
+
+
+def skip(msg=""):
+    raise pytest.skip(msg)
+
+
+def b_to_polygon(b):
+    west, south, east, north = [float(p) for p in b.split(",")]
+    polygon = Polygon([(west, south), (east, south), (east, north), (west, 
north), (west, south)])
+    return polygon
+
+
+def iso_time_to_epoch(str_time):
+    epoch = timezone('UTC').localize(datetime(1970, 1, 1))
+
+    return (datetime.strptime(str_time, "%Y-%m-%dT%H:%M:%SZ").replace(
+        tzinfo=UTC) - epoch).total_seconds()
+
+
+def verify_secondary_in_tolerance(primary, secondary, rt):
+    distance = geodesic((primary['lat'], primary['lon']), (secondary['lat'], 
secondary['lon'])).m
+
+    assert distance <= rt
+
+
+def translate_global_rows(rows):
+    translated = {}
+
+    for row in rows:
+        parts = row.split(',', 1)
+        translated[parts[0]] = parts[1]
+
+    return translated
+
+
+def translate_matchup_rows(rows):
+    headers = rows[0].split(',')
+
+    translated_rows = []
+
+    for row in rows[1:]:
+        translated_row = {}
+
+        buf = io.StringIO(row)
+        reader = csv.reader(buf)
+        fields = list(reader)[0]
+
+        assert len(headers) == len(fields)
+
+        for i, field in enumerate(fields):
+            header = headers[i]
+
+            if header not in translated_row:
+                translated_row[header] = field
+            else:
+                translated_row[f"{header}_secondary"] = field
+
+        translated_rows.append(translated_row)
+
+    return translated_rows
+
+
+def lat_lon_to_point(lat, lon):
+    return wkt.loads(f"Point({lon} {lat})")
+
+
+def format_time(timestamp):
+    t = parse(timestamp)
+    return t.strftime('%Y-%m-%dT%H:%M:%SZ')
+
+
+def verify_match(match, point, time, s_point, s_time, params, bounding_poly):
+    # Check primary point is as expected
+    assert match['point'] == point
+    assert match['time'] == time
+
+    # Check primary point within search bounds
+    assert iso_time_to_epoch(params['startTime']) \
+           <= match['time'] \
+           <= iso_time_to_epoch(params['endTime'])
+    assert bounding_poly.intersects(wkt.loads(match['point']))
+
+    secondary = match['matches'][0]
+
+    # Check secondary point is as expected
+    assert secondary['point'] == s_point
+    assert secondary['time'] == s_time
+
+    # Check secondary point within specified spatial & temporal tolerances for 
matched primary
+    verify_secondary_in_tolerance(match, secondary, params['rt'])
+
+    assert (match['time'] - params['tt']) \
+           <= secondary['time'] \
+           <= (match['time'] + params['tt'])
+
+
+def verify_match_consistency(match, params, bounding_poly):
+    # Check primary point within search bounds
+    assert iso_time_to_epoch(params['startTime']) \
+           <= match['time'] \
+           <= iso_time_to_epoch(params['endTime'])
+    assert bounding_poly.intersects(wkt.loads(match['point']))
+
+    for secondary in match['matches']:
+        # Check secondary point within specified spatial & temporal tolerances 
for matched primary
+        verify_secondary_in_tolerance(match, secondary, params['rt'])
+
+        assert (match['time'] - params['tt']) \
+               <= secondary['time'] \
+               <= (match['time'] + params['tt'])
+
+
+def validate_insitu(body, params, test):
+    if body['total'] <= params['itemsPerPage']:
+        assert body['total'] == len(body['results'])
+    else:
+        assert len(body['results']) == params['itemsPerPage']
+
+    if len(body['results']) == 0:
+        warnings.warn(f'Insitu test ({test}) returned no results!')
+
+    bounding_poly = b_to_polygon(params['bbox'])
+
+    for result in body['results']:
+        assert bounding_poly.intersects(
+            wkt.loads(f"Point({result['longitude']} {result['latitude']})")
+        )
+
+        if result['depth'] != -99999.0:
+            assert params['minDepth'] <= result['depth'] <= params['maxDepth']
+
+        assert params['startTime'] <= result['time'] <= params['endTime']
+
+
+def try_save(name, time, response, ext='json', mode='w'):
+    Path(f'responses/{time}/').mkdir(parents=True, exist_ok=True)
+
+    try:
+        with open(f'responses/{time}/{name}.{ext}', mode=mode) as f:
+            if ext == 'json':
+                json.dump(response, f, indent=4)
+            elif ext == 'csv':
+                f.write(response.text)
+            else:
+                f.write(response.content)
+    except Exception as e:
+        warnings.warn(f"Failed to save response for {name}\n{e}", 
RuntimeWarning)
+
+
+def uniq_primaries(primaries, xfail=False, case=None):
+    class Primary:
+        def __init__(self, p):
+            self.platform = p['platform']
+            self.device = p['device']
+            self.lon = p['lon']
+            self.lat = p['lat']
+            self.point = p['point']
+            self.time = p['time']
+            self.depth = p['depth']
+            self.fileurl = p['fileurl']
+            self.id = p['id']
+            self.source = p['source']
+            self.primary = p['primary']
+            self.matches = p['matches']
+
+        def __eq__(self, other):
+            if not isinstance(other, Primary):
+                return False
+
+            return self.platform == other.platform and \
+                self.device == other.device and \
+                self.lon == other.lon and \
+                self.lat == other.lat and \
+                self.point == other.point and \
+                self.time == other.time and \
+                self.depth == other.depth and \
+                self.fileurl == other.fileurl and \
+                self.id == other.id and \
+                self.source == other.source and \
+                self.primary == other.primary
+
+        def __str__(self):
+            primary = {
+                "platform": self.platform,
+                "device": self.device,
+                "lon": self.lon,
+                "lat": self.lat,
+                "point": self.point,
+                "time": self.time,
+                "depth": self.depth,
+                "fileurl": self.fileurl,
+                "id": self.id,
+                "source": self.source,
+                "primary": self.primary,
+            }
+
+            return json.dumps(primary, indent=4)
+
+    points = [Primary(p) for p in primaries]
+
+    checked = []
+    duplicates = {}
+
+    for p in points:
+        for c in checked:
+            if p == c:
+                if p.id not in duplicates:
+                    duplicates[p.id] = [p, c]
+                else:
+                    duplicates[p.id].append(p)
+                break
+        checked.append(p)
+
+    if len(duplicates) > 0:
+        m = print if not xfail else warnings.warn
+
+        msg = f'Duplicate point(s) found ({len(duplicates)} total)'
+
+        if case is not None:
+            msg += f' for case {case}'
+
+        msg += '\n\n-----\n\n'
+
+        for d in duplicates:
+            d = duplicates[d]
+
+            msg += 'Primary point:\n' + str(d[0]) + '\n\n'
+
+            matches = [p.matches for p in d]
+
+            msg += f'Matches to ({len(matches)}):\n'
+            msg += json.dumps(matches, indent=4)
+            msg += '\n\n'
+
+        m(msg)
+
+        if xfail:
+            pytest.xfail('Duplicate points found')
+        else:
+            assert False, 'Duplicate points found'
+
+
+def check_count(count, expected, fail_on_mismatch):
+    if count == expected:
+        return
+    elif fail_on_mismatch:
+        raise AssertionError(f'Incorrect count: Expected {expected}, got 
{count}')
+    else:
+        warnings.warn(f'Incorrect count: Expected {expected}, got {count}')
+
+
+def url_scheme(scheme, url):
+    if urlparse(url).scheme == scheme:
+        return url
+    else:
+        return urlunparse(tuple([scheme] + list(urlparse(url)[1:])))
+
+
+# Run the matchup query and return json output (and eid?)
+# Should be able to work if match_spark is synchronous or asynchronous
+def run_matchup(url, params, page_size=3500):
+    TIMEOUT = 60 * 60
+    # TIMEOUT = float('inf')
+
+    response = requests.get(url, params=params)
+
+    scheme = urlparse(url).scheme
+
+    assert response.status_code == 200, 'Initial match_spark query failed'
+    response_json = response.json()
+
+    asynchronous = 'status' in response_json
+
+    if not asynchronous:
+        return response_json
+    else:
+        start = datetime.utcnow()
+        job_url = [link for link in response_json['links'] if link['rel'] == 
'self'][0]['href']
+
+        job_url = url_scheme(scheme, job_url)
+
+        retries = 3
+        timeouts = [2, 5, 10]
+
+        while response_json['status'] == 'running' and (datetime.utcnow() - 
start).total_seconds() <= TIMEOUT:
+            status_response = requests.get(job_url)
+            status_code = response.status_code
+
+            # /job poll may fail internally. This does not necessarily 
indicate job failure (ie, Cassandra read
+            # timed out). Retry it a couple of times and fail the test if it 
persists.
+            if status_code == 500 and retries > 0:
+                warnings.warn('/job poll failed; retrying')
+                sleep(timeouts[3 - retries])
+                retries -= 1
+                continue
+
+            assert status_response.status_code == 200, '/job status polling 
failed'
+            response_json = status_response.json()
+
+            if response_json['status'] == 'running':
+                sleep(10)
+
+        job_status = response_json['status']
+
+        if job_status == 'running':
+            skip(f'Job has been running too long ({(datetime.utcnow() - 
start)}), skipping to run other tests')
+        elif job_status in ['cancelled', 'failed']:
+            raise ValueError(f'Async matchup job finished with incomplete 
status ({job_status})')
+        else:
+            stac_url = [
+                link for link in response_json['links'] if 'STAC' in 
link['title']
+            ][0]['href']
+
+            stac_url = url_scheme(scheme, stac_url)
+
+            catalogue_response = requests.get(stac_url)
+            assert catalogue_response.status_code == 200, 'Catalogue fetch 
failed'
+
+            catalogue_response = catalogue_response.json()
+
+            json_cat_url = [
+                link for link in catalogue_response['links'] if 'JSON' in 
link['title']
+            ][0]['href']
+
+            json_cat_url = url_scheme(scheme, json_cat_url)
+
+            catalogue_response = requests.get(json_cat_url)
+            assert catalogue_response.status_code == 200, 'Catalogue fetch 
failed'
+
+            catalogue_response = catalogue_response.json()
+
+            results_urls = [
+                url_scheme(scheme, link['href']) for link in
+                catalogue_response['links'] if 'output=JSON' in link['href']
+                # link['href'] for link in response_json['links'] if 
link['type'] == 'application/json'
+            ]
+
+            def get_results(url):
+                retries = 3
+                retry_delay = 1.5
+
+                while retries > 0:
+                    response = requests.get(url)
+
+                    try:
+                        response.raise_for_status()
+                        result = response.json()
+
+                        assert result['count'] == len(result['data'])
+
+                        return result
+                    except:
+                        retries -= 1
+                        sleep(retry_delay)
+                        retry_delay *= 2
+
+            assert len(results_urls) > 0, 'STAC catalogue returned no result 
queries'
+
+            matchup_result = get_results(results_urls[0])
+
+            for url in results_urls[1:]:
+                matchup_result['data'].extend(get_results(url)['data'])
+
+            return matchup_result
+
+
[email protected]
+def test_version(host, start):
+    url = urljoin(host, 'version')
+
+    response = requests.get(url)
+
+    assert response.status_code == 200
+    assert re.match(r'^\d+\.\d+\.\d+(-.+)?$', response.text)
+
+
[email protected]
+def test_capabilities(host, start):
+    url = urljoin(host, 'capabilities')
+
+    response = requests.get(url)
+
+    assert response.status_code == 200
+
+    capabilities = response.json()
+
+    try_save('test_capabilities', start, capabilities)
+
+    assert len(capabilities) > 0
+
+    for capability in capabilities:
+        assert all([k in capability for k in ['name', 'path', 'description', 
'parameters']])
+        assert all([isinstance(k, str) for k in ['name', 'path', 
'description']])
+
+        assert isinstance(capability['parameters'], (dict, list))
+
+        for param in capability['parameters']:
+            if isinstance(capability['parameters'], dict):
+                param = capability['parameters'][param]
+
+            assert isinstance(param, dict)
+            assert all([k in param and isinstance(param[k], str) for k in 
['name', 'type', 'description']])
+
+
[email protected]
+def test_endpoints(host, start):
+    url = urljoin(host, 'capabilities')
+
+    response = requests.get(url)
+
+    if response.status_code != 200:
+        skip('Could not get endpoints list. Expected if test_capabilities has 
failed')
+
+    capabilities = response.json()
+
+    endpoints = [c['path'] for c in capabilities]
+
+    non_existent_endpoints = []
+
+    for endpoint in endpoints:
+        status = requests.head(urljoin(host, endpoint)).status_code
+
+        if status == 404:
+            # Strip special characters because some endpoints have 
wildcards/regex characters
+            # This may not work forever though
+            stripped_endpoint = re.sub(r'[^a-zA-Z0-9/_-]', '', endpoint)
+
+            status = requests.head(urljoin(host, 
stripped_endpoint)).status_code
+
+            if status == 404:
+                non_existent_endpoints.append(([endpoint, stripped_endpoint], 
status))
+
+    assert len(non_existent_endpoints) == 0, non_existent_endpoints
+
+
[email protected]
+def test_heartbeat(host, start):
+    url = urljoin(host, 'heartbeat')
+
+    response = requests.get(url)
+
+    assert response.status_code == 200
+    heartbeat = response.json()
+
+    assert isinstance(heartbeat, dict)
+    assert all(heartbeat.values())
+
+
[email protected]
+def test_swaggerui_sdap(host):
+    url = urljoin(host, 'apidocs/')
+
+    response = requests.get(url)
+
+    assert response.status_code == 200
+    assert 'swagger-ui' in response.text
+
+    try:
+        # There's probably a better way to do this, but extract the .yml file 
for the docs from the returned text
+        soup = BeautifulSoup(response.text, 'html.parser')
+
+        script = str([tag for tag in soup.find_all('script') if tag.attrs == 
{}][0])
+
+        start_index = script.find('url:')
+        end_index = script.find('",\n', start_index)
+
+        script = script[start_index:end_index]
+
+        yml_filename = script.split('"')[1]
+
+        url = urljoin(url, yml_filename)
+
+        response = requests.get(url)
+
+        assert response.status_code == 200
+    except AssertionError:
+        raise
+    except:
+        try:
+            url = urljoin(url, 'openapi.yml')
+
+            response = requests.get(url)
+
+            assert response.status_code == 200
+
+            warnings.warn("Could not extract documentation yaml filename from 
response text, "
+                          "but using an assumed value worked successfully")
+        except:
+            raise ValueError("Could not verify documentation yaml file, 
assumed value also failed")
+
+
[email protected]
+def test_list(host, start):
+    url = urljoin(host, 'list')
+
+    response = requests.get(url)
+
+    assert response.status_code == 200
+
+    body = response.json()
+    try_save("test_list", start, body)
+
+    assert isinstance(body, list)
+
+    if len(body) == 0:
+        warnings.warn('/list returned no datasets. This could be correct if 
SDAP has no data ingested, otherwise '
+                      'this should be considered a failure')
+
+
[email protected]
[email protected](
+    ['collection'],
+    [('MUR25-JPL-L4-GLOB-v04.2_test',), 
('OISSS_L4_multimission_7day_v1_test',)]
+)
+def test_subset_L4(host, start, collection):
+    url = urljoin(host, 'datainbounds')
+
+    params = {
+        "ds": collection,
+        "startTime": "2018-09-24T00:00:00Z",
+        "endTime": "2018-09-30T00:00:00Z",
+        "b": "160,-30,180,-25",
+    }
+
+    response = requests.get(url, params=params)
+    assert response.status_code == 200
+
+    data = response.json()
+    try_save(f"test_datainbounds_L4_{collection}", start, data)
+
+    bounding_poly = b_to_polygon(params['b'])
+
+    epoch = datetime(1970, 1, 1, tzinfo=UTC)
+
+    start = (datetime.strptime(params['startTime'], 
'%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=UTC) - epoch).total_seconds()
+    end = (datetime.strptime(params['endTime'], 
'%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=UTC) - epoch).total_seconds()
+
+    for p in data:
+        assert bounding_poly.intersects(Point(float(p['longitude']), 
float(p['latitude'])))
+        assert start <= p['time'] <= end
+
[email protected]
+def test_subset_L2(host, start):
+    url = urljoin(host, 'datainbounds')
+
+    params = {
+        "ds": "ASCATB-L2-Coastal_test",
+        "startTime": "2018-09-24T00:00:00Z",
+        "endTime": "2018-09-30T00:00:00Z",
+        "b": "160,-30,180,-25",
+    }
+
+    response = requests.get(url, params=params)
+    assert response.status_code == 200
+
+    data = response.json()
+    try_save("test_datainbounds_L2", start, data)
+
+    bounding_poly = b_to_polygon(params['b'])
+
+    epoch = datetime(1970, 1, 1, tzinfo=UTC)
+
+    start = (datetime.strptime(params['startTime'], 
'%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=UTC) - epoch).total_seconds()
+    end = (datetime.strptime(params['endTime'], 
'%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=UTC) - epoch).total_seconds()
+
+    for p in data:
+        assert bounding_poly.intersects(Point(float(p['longitude']), 
float(p['latitude'])))
+        assert start <= p['time'] <= end
+
[email protected]
+def test_timeseries_spark(host, start):
+    url = urljoin(host, 'timeSeriesSpark')
+
+    params = {
+        "ds": "MUR25-JPL-L4-GLOB-v04.2_test",
+        "b": "-135,-10,-80,10",
+        "startTime": "2018-07-05T00:00:00Z",
+        "endTime": "2018-09-30T23:59:59Z",
+    }
+
+    response = requests.get(url, params=params)
+
+    assert response.status_code == 200
+
+    data = response.json()
+    try_save('test_timeseries_spark', start, data)
+
+    assert len(data['data']) == len(pd.date_range(params['startTime'], 
params['endTime'], freq='D'))
+
+    epoch = datetime(1970, 1, 1, tzinfo=UTC)
+
+    start = (datetime.strptime(params['startTime'], 
'%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=UTC) - epoch).total_seconds()
+    end = (datetime.strptime(params['endTime'], 
'%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=UTC) - epoch).total_seconds()
+
+    for p in data['data']:
+        assert start <= p[0]['time'] <= end
+
+
[email protected]
+def test_cdmslist(host, start):
+    url = urljoin(host, 'cdmslist')
+
+    response = requests.get(url)
+
+    assert response.status_code == 200
+
+    body = response.json()
+    try_save("test_cdmslist", start, body)
+
+    data = body['data']
+
+    num_satellite = len(data['satellite'])
+    num_insitu = len(data['insitu'])
+
+    if num_satellite == 0:
+        warnings.warn('/cdmslist returned no satellite datasets. This could be 
correct if SDAP has no data ingested, '
+                      'otherwise this should be considered a failure')
+
+    if num_insitu == 0:
+        warnings.warn('/cdmslist returned no insitu datasets. This could be 
correct if SDAP has no insitu data '
+                      'ingested, otherwise this should be considered a 
failure')
+
+
[email protected]
[email protected](
+    ['collection'],
+    [('MUR25-JPL-L4-GLOB-v04.2_test',), 
('OISSS_L4_multimission_7day_v1_test',)]
+)
+def test_cdmssubset_L4(host, start, collection):
+    url = urljoin(host, 'cdmssubset')
+
+    params = {
+        "dataset": collection,
+        "parameter": "sst",
+        "startTime": "2018-09-24T00:00:00Z",
+        "endTime": "2018-09-30T00:00:00Z",
+        "b": "160,-30,180,-25",
+        "output": "ZIP"
+    }
+
+    response = requests.get(url, params=params)
+
+    assert response.status_code == 200
+
+    try_save(f"test_cdmssubset_L4_{collection}", start, response, "zip", 'wb')
+
+    bounding_poly = b_to_polygon(params['b'])
+
+    response_buf = io.BytesIO(response.content)
+
+    with ZipFile(response_buf) as data:
+        namelist = data.namelist()
+
+        assert namelist == [f'{collection}.csv']
+
+        csv_buf = io.StringIO(data.read(namelist[0]).decode('utf-8'))
+        csv_data = pd.read_csv(csv_buf)
+
+    def validate_row_bounds(row):
+        assert bounding_poly.intersects(Point(float(row['longitude']), 
float(row['latitude'])))
+        assert params['startTime'] <= row['time'] <= params['endTime']
+
+    for i in range(0, len(csv_data)):
+        validate_row_bounds(csv_data.iloc[i])
+
+
[email protected]
+def test_cdmssubset_L2(host, start):
+    url = urljoin(host, 'cdmssubset')
+
+    params = {
+        "dataset": "ASCATB-L2-Coastal_test",
+        "startTime": "2018-09-24T00:00:00Z",
+        "endTime": "2018-09-30T00:00:00Z",
+        "b": "160,-30,180,-25",
+        "output": "ZIP"
+    }
+
+    response = requests.get(url, params=params)
+
+    assert response.status_code == 200
+
+    try_save("test_cdmssubset_L2", start, response, "zip", 'wb')
+
+    bounding_poly = b_to_polygon(params['b'])
+
+    response_buf = io.BytesIO(response.content)
+
+    with ZipFile(response_buf) as data:
+        namelist = data.namelist()
+
+        assert namelist == ['ASCATB-L2-Coastal_test.csv']
+
+        csv_buf = io.StringIO(data.read(namelist[0]).decode('utf-8'))
+        csv_data = pd.read_csv(csv_buf)
+
+    def validate_row_bounds(row):
+        assert bounding_poly.intersects(Point(float(row['longitude']), 
float(row['latitude'])))
+        assert params['startTime'] <= row['time'] <= params['endTime']
+
+    for i in range(0, len(csv_data)):
+        validate_row_bounds(csv_data.iloc[i])
+
+
[email protected]
[email protected](
+    ['match', 'expected'],
+    list(zip(
+        ['gridded_to_gridded', 'gridded_to_swath', 'swath_to_gridded', 
'swath_to_swath'],
+        [1058, 6, 21, 4026]
+    ))
+)
+def test_match_spark(host, start, fail_on_miscount, matchup_params, match, 
expected):
+    url = urljoin(host, 'match_spark')
+
+    params = matchup_params[match]
+
+    bounding_poly = b_to_polygon(params['b'])
+
+    body = run_matchup(url, params)
+    try_save(f"test_matchup_spark_{match}", start, body)
+    data = body['data']
+
+    for match in data:
+        verify_match_consistency(match, params, bounding_poly)
+
+    uniq_primaries(data, case=f"test_matchup_spark_{match}")
+    check_count(len(data), expected, fail_on_miscount)
+
+
[email protected]
+def test_match_spark_job_cancellation(host, start, matchup_params):
+    url = urljoin(host, 'match_spark')
+
+    params = matchup_params['long']
+
+    response = requests.get(url, params=params)
+
+    assert response.status_code == 200, 'Initial match_spark query failed'
+    response_json = response.json()
+
+    asynchronous = 'status' in response_json
+
+    if not asynchronous:
+        skip('Deployed SDAP version does not have asynchronous matchup')
+    else:
+        sleep(1)  # Time to allow spark workers to start working
+
+        if response_json['status'] != 'running':
+            skip('Job finished before it could be cancelled')
+        else:
+            cancel_url = [link for link in response_json['links'] if 
link['rel'] == 'cancel'][0]['href']
+
+            cancel_url = url_scheme(
+                urlparse(url).scheme,
+                cancel_url
+            )
+
+            cancel_response = requests.get(cancel_url)
+            assert cancel_response.status_code == 200, 'Cancellation query 
failed'
+
+            cancel_json = cancel_response.json()
+
+            assert cancel_json['status'] != 'running', 'Job did not cancel'
+
+            if cancel_json['status'] in ['success', 'failed']:
+                warnings.warn(f'Job status after cancellation is not 
\'cancelled\' ({cancel_json["status"]}), passing '
+                              f'case because it is no longer \'running\', but 
actual cancellation could not be tested '
+                              f'here.')
+
+
[email protected]
[email protected]('Test not re-implemented yet')
+def test_cdmsresults_json(host, eid, start):
+    url = urljoin(host, 'cdmsresults')
+
+    # Skip the test automatically if the matchup request was not successful
+    if not eid['successful']:
+        skip('Matchup request was unsuccessful so there are no results to get 
from domsresults')
+
+    def fetch_result(execution_id, output):
+        return requests.get(url, params={"id": execution_id, "output": output})
+
+    eid_list = eid['eid']
+    param_list = eid['params']
+
+    response = fetch_result(eid_list[0], "JSON")
+
+    assert response.status_code == 200
+
+    body = response.json()
+    try_save("test_cdmsresults_json_A", start, body)
+
+    data = body['data']
+    assert len(data) == 5
+
+    for m in data:
+        m['point'] = f"Point({m['lon']} {m['lat']})"
+        for s in m['matches']:
+            s['point'] = f"Point({s['lon']} {s['lat']})"
+
+    data.sort(key=lambda e: e['point'])
+
+    params = param_list[0]
+    bounding_poly = b_to_polygon(params['b'])
+
+    verify_match(
+        data[0], 'Point(-86.125 27.625)',
+        1535360400, 'Point(-86.13 27.63)',
+        1535374800, params, bounding_poly
+    )
+
+    verify_match(
+        data[1], 'Point(-88.875 27.875)',
+        1534669200, 'Point(-88.88 27.88)',
+        1534698000, params, bounding_poly
+    )
+
+    verify_match(
+        data[2], 'Point(-90.125 27.625)',
+        1534496400, 'Point(-90.13 27.63)',
+        1534491000, params, bounding_poly
+    )
+
+    verify_match(
+        data[3], 'Point(-90.125 28.125)',
+        1534928400, 'Point(-90.13 28.12)',
+        1534899600, params, bounding_poly
+    )
+
+    verify_match(
+        data[4], 'Point(-90.375 28.125)',
+        1534842000, 'Point(-90.38 28.12)',
+        1534813200, params, bounding_poly
+    )
+
+    response = fetch_result(eid_list[1], "JSON")
+
+    assert response.status_code == 200
+
+    body = response.json()
+    try_save("test_cdmsresults_json_B", start, body)
+
+    data = body['data']
+    assert len(data) == 5
+
+    for m in data:
+        m['point'] = f"Point({m['lon']} {m['lat']})"
+        for s in m['matches']:
+            s['point'] = f"Point({s['lon']} {s['lat']})"
+
+    data.sort(key=lambda e: e['point'])
+
+    params = param_list[1]
+    bounding_poly = b_to_polygon(params['b'])
+
+    verify_match(
+        data[0], 'Point(-86.125 27.625)',
+        1535371200, 'Point(-86.13 27.63)',
+        1535374800, params, bounding_poly
+    )
+
+    verify_match(
+        data[1], 'Point(-88.875 27.875)',
+        1534680000, 'Point(-88.88 27.88)',
+        1534698000, params, bounding_poly
+    )
+
+    verify_match(
+        data[2], 'Point(-90.125 27.625)',
+        1534507200, 'Point(-90.13 27.63)',
+        1534491000, params, bounding_poly
+    )
+
+    verify_match(
+        data[3], 'Point(-90.125 28.125)',
+        1534939200, 'Point(-90.13 28.12)',
+        1534899600, params, bounding_poly
+    )
+
+    verify_match(
+        data[4], 'Point(-90.375 28.125)',
+        1534852800, 'Point(-90.38 28.12)',
+        1534813200, params, bounding_poly
+    )
+
+
[email protected]
[email protected]('Test not re-implemented yet')
+def test_cdmsresults_csv(host, eid, start):
+    url = urljoin(host, 'cdmsresults')
+
+    # Skip the test automatically if the matchup request was not successful
+    if not eid['successful']:
+        skip('Matchup request was unsuccessful so there are no results to get 
from domsresults')
+
+    def fetch_result(execution_id, output):
+        return requests.get(url, params={"id": execution_id, "output": output})
+
+    eid_list = eid['eid']
+    param_list = eid['params']
+
+    response = fetch_result(eid_list[0], "CSV")
+    params = param_list[0]
+    bounding_poly = b_to_polygon(params['b'])
+
+    assert response.status_code == 200
+
+    try_save("test_cdmsresults_csv_A", start, response, "csv")
+
+    rows = response.text.split('\r\n')
+    index = rows.index('')
+
+    global_rows = rows[:index]
+    matchup_rows = rows[index + 1:-1]  # Drop trailing empty string from 
trailing newline
+
+    global_rows = translate_global_rows(global_rows)
+    matchup_rows = translate_matchup_rows(matchup_rows)
+
+    assert len(matchup_rows) == int(global_rows['CDMS_num_primary_matched'])
+
+    for row in matchup_rows:
+        primary_point = lat_lon_to_point(row['lat'], row['lon'])
+
+        assert bounding_poly.intersects(primary_point)
+        assert params['startTime'] <= format_time(row['time']) <= 
params['endTime']
+
+        verify_secondary_in_tolerance(
+            {'lat': row['lat'], 'lon': row['lon']},
+            {'lat': row['lat_secondary'], 'lon': row['lon_secondary']},
+            params['rt']
+        )
+        assert (iso_time_to_epoch(params['startTime']) - params['tt']) \
+               <= iso_time_to_epoch(format_time(row['time_secondary'])) \
+               <= (iso_time_to_epoch(params['endTime']) + params['tt'])
+
+    response = fetch_result(eid_list[1], "CSV")
+    params = param_list[1]
+    bounding_poly = b_to_polygon(params['b'])
+
+    assert response.status_code == 200
+
+    try_save("test_cdmsresults_csv_B", start, response, "csv")
+
+    rows = response.text.split('\r\n')
+    index = rows.index('')
+
+    global_rows = rows[:index]
+    matchup_rows = rows[index + 1:-1]  # Drop trailing empty string from 
trailing newline
+
+    global_rows = translate_global_rows(global_rows)
+    matchup_rows = translate_matchup_rows(matchup_rows)
+
+    assert len(matchup_rows) == int(global_rows['CDMS_num_primary_matched'])
+
+    for row in matchup_rows:
+        primary_point = lat_lon_to_point(row['lat'], row['lon'])
+
+        assert bounding_poly.intersects(primary_point)
+        assert params['startTime'] <= format_time(row['time']) <= 
params['endTime']
+
+        verify_secondary_in_tolerance(
+            {'lat': row['lat'], 'lon': row['lon']},
+            {'lat': row['lat_secondary'], 'lon': row['lon_secondary']},
+            params['rt']
+        )
+        assert (iso_time_to_epoch(params['startTime']) - params['tt']) \
+               <= iso_time_to_epoch(format_time(row['time_secondary'])) \
+               <= (iso_time_to_epoch(params['endTime']) + params['tt'])
+
+
[email protected]
[email protected]('Test not re-implemented yet')
+def test_cdmsresults_netcdf(host, eid, start):
+    warnings.filterwarnings('ignore')
+
+    url = urljoin(host, 'cdmsresults')
+
+    # Skip the test automatically if the matchup request was not successful
+    if not eid['successful']:
+        skip('Matchup request was unsuccessful so there are no results to get 
from domsresults')
+
+    def fetch_result(execution_id, output):
+        return requests.get(url, params={"id": execution_id, "output": output})
+
+    eid_list = eid['eid']
+    param_list = eid['params']
+
+    temp_file = Temp(mode='wb+', suffix='.csv.tmp', prefix='CDMSReader_')
+
+    response = fetch_result(eid_list[0], "NETCDF")
+    params = param_list[0]
+    bounding_poly = b_to_polygon(params['b'])
+
+    assert response.status_code == 200
+
+    try_save("test_cdmsresults_netcdf_A", start, response, "nc", 'wb')
+
+    temp_file.write(response.content)
+    temp_file.flush()
+    temp_file.seek(0)
+
+    matches = cdms_reader.assemble_matches(temp_file.name)
+
+    cdms_reader.matches_to_csv(matches, temp_file.name)
+
+    with open(temp_file.name) as f:
+        reader = csv.DictReader(f)
+        rows = list(reader)
+
+    for row in rows:
+        primary_point = lat_lon_to_point(row['PrimaryData_lat'], 
row['PrimaryData_lon'])
+
+        assert bounding_poly.intersects(primary_point)
+        assert iso_time_to_epoch(params['startTime']) \
+               <= float(row['PrimaryData_time']) \
+               <= iso_time_to_epoch(params['endTime'])
+
+        verify_secondary_in_tolerance(
+            {'lat': row['PrimaryData_lat'], 'lon': row['PrimaryData_lon']},
+            {'lat': row['SecondaryData_lat'], 'lon': row['SecondaryData_lon']},
+            params['rt']
+        )
+        assert (iso_time_to_epoch(params['startTime']) - params['tt']) \
+               <= float(row['SecondaryData_time']) \
+               <= (iso_time_to_epoch(params['endTime']) + params['tt'])
+
+    response = fetch_result(eid_list[1], "NETCDF")
+    params = param_list[1]
+    bounding_poly = b_to_polygon(params['b'])
+
+    assert response.status_code == 200
+
+    try_save("test_cdmsresults_netcdf_B", start, response, "nc", 'wb')
+
+    temp_file.write(response.content)
+    temp_file.flush()
+    temp_file.seek(0)
+
+    matches = cdms_reader.assemble_matches(temp_file.name)
+
+    cdms_reader.matches_to_csv(matches, temp_file.name)
+
+    with open(temp_file.name) as f:
+        reader = csv.DictReader(f)
+        rows = list(reader)
+
+    for row in rows:
+        primary_point = lat_lon_to_point(row['PrimaryData_lat'], 
row['PrimaryData_lon'])
+
+        assert bounding_poly.intersects(primary_point)
+        assert iso_time_to_epoch(params['startTime']) \
+               <= float(row['PrimaryData_time']) \
+               <= iso_time_to_epoch(params['endTime'])
+
+        verify_secondary_in_tolerance(
+            {'lat': row['PrimaryData_lat'], 'lon': row['PrimaryData_lon']},
+            {'lat': row['SecondaryData_lat'], 'lon': row['SecondaryData_lon']},
+            params['rt']
+        )
+        assert (iso_time_to_epoch(params['startTime']) - params['tt']) \
+               <= float(row['SecondaryData_time']) \
+               <= (iso_time_to_epoch(params['endTime']) + params['tt'])
+
+    temp_file.close()
+    warnings.filterwarnings('default')

Reply via email to