Repository: hed-standard/hed-python (language: python)

Issue:
Consolidate validator
Allow HEDTools EEGLAB plugin to use python web validator through HTTP requests. Added new path, request handler, and routing logic for sending and validating hed strings associated with EEG events from plugin.

I encountered some errors while trying to deploy the Docker container. I edited the configuration files to fix the errors. 

I also changed the deploy script to use my forked repo. This should be changed back to the official repo.


Discussion / hints:

Fix patch:
diff --git a/.coveragerc b/.coveragerc
deleted file mode 100644
index ff30f811a..000000000
--- a/.coveragerc
+++ /dev/null
@@ -1,10 +0,0 @@
-[run]
-branch = True
-source = ./hedtools/*
-include = ./hedtools/*
-omit =
-    */setup.py
-    ./hedtools/examples/*
-    ./hedtools/tests/data/*
-[report]
-include = ./hedtools/*
\ No newline at end of file
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 6573e5d0c..d87c6c958 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -29,6 +29,7 @@ jobs:
           pip install flake8
           pip install coverage
           if [ -f hedtools/requirements.txt ]; then pip install -r hedtools/requirements.txt; fi
+          if [ -f webtools/requirements.txt ]; then pip install -r webtools/requirements.txt; fi
 
       - name: Lint with flake8
         run: |
@@ -40,14 +41,12 @@ jobs:
       - name: Test with unittest
         run: |
           coverage run -m unittest discover -s ./hedtools
+          coverage run -a -m unittest discover -s ./webtools 
 
-  coverage:
-    needs: [build]
-    runs-on: ubuntu-latest
-    # python-version: 3.7
-
-    steps:
       - name: publish-coverages
         uses: paambaati/codeclimate-action@v2.7.5
         env:
           CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
+        with:
+            coverageCommand: coverage xml
+            debug: true
diff --git a/.gitignore b/.gitignore
index 006613973..8bfb1395e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -69,10 +69,18 @@ pip-delete-this-directory.txt
 # Unit test / coverage reports
 htmlcov/
 .tox/
+.nox/
 .coverage
+.coverage.*
 .cache
 nosetests.xml
 coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
 
 # Translations
 *.mo
diff --git a/hedtools/.coveragerc b/hedtools/.coveragerc
deleted file mode 100644
index 9790dd89c..000000000
--- a/hedtools/.coveragerc
+++ /dev/null
@@ -1,11 +0,0 @@
-[run]
-branch = True
-source = hedtools/*
-include = hedtools/*
-omit =
-    */setup.py
-    */external/*
-    */due.py
-    */*version.py
-[report]
-include = hedtools/*
\ No newline at end of file
diff --git a/hedtools/hed/schema/hed_cache.py b/hedtools/hed/schema/hed_cache.py
index 79f72093b..ac0a91d9f 100644
--- a/hedtools/hed/schema/hed_cache.py
+++ b/hedtools/hed/schema/hed_cache.py
@@ -46,6 +46,7 @@ def set_cache_directory(new_cache_dir):
     if new_cache_dir:
         global HED_CACHE_DIRECTORY
         HED_CACHE_DIRECTORY = new_cache_dir
+        os.makedirs(new_cache_dir, exist_ok=True)
 
 
 def get_all_hed_versions(local_hed_directory=None, include_deprecated=False):
diff --git a/hedtools/hed/schema/hed_schema.py b/hedtools/hed/schema/hed_schema.py
index 6419e2ea1..f09f826ff 100644
--- a/hedtools/hed/schema/hed_schema.py
+++ b/hedtools/hed/schema/hed_schema.py
@@ -273,28 +273,28 @@ def has_value_classes(self):
 
     def __eq__(self, other):
         # Comment the following back in for easy debugging of schema that should be equal.
-        dict_keys = set(list(self.dictionaries.keys()) + list(other.dictionaries.keys()))
-        for dict_key in dict_keys:
-            if dict_key not in self.dictionaries:
-                print(f"{dict_key} dict not in self")
-                continue
-            if dict_key not in other.dictionaries:
-                print(f"{dict_key} dict not in other")
-                continue
-            dict1 = self.dictionaries[dict_key]
-            dict2 = other.dictionaries[dict_key]
-            if dict1 != dict2:
-                print(f"DICT {dict_key} NOT EQUAL")
-                key_union = set(list(dict1.keys()) + list(dict2.keys()))
-                for key in key_union:
-                    if key not in dict1:
-                        print(f"{key} not in dict1")
-                        continue
-                    if key not in dict2:
-                        print(f"{key} not in dict2")
-                        continue
-                    if dict1[key] != dict2[key]:
-                        print(f"{key} doesn't match.  '{dict1[key]}' vs '{dict2[key]}'")
+        # dict_keys = set(list(self.dictionaries.keys()) + list(other.dictionaries.keys()))
+        # for dict_key in dict_keys:
+        #     if dict_key not in self.dictionaries:
+        #         print(f"{dict_key} dict not in self")
+        #         continue
+        #     if dict_key not in other.dictionaries:
+        #         print(f"{dict_key} dict not in other")
+        #         continue
+        #     dict1 = self.dictionaries[dict_key]
+        #     dict2 = other.dictionaries[dict_key]
+        #     if dict1 != dict2:
+        #         print(f"DICT {dict_key} NOT EQUAL")
+        #         key_union = set(list(dict1.keys()) + list(dict2.keys()))
+        #         for key in key_union:
+        #             if key not in dict1:
+        #                 print(f"{key} not in dict1")
+        #                 continue
+        #             if key not in dict2:
+        #                 print(f"{key} not in dict2")
+        #                 continue
+        #             if dict1[key] != dict2[key]:
+        #                 print(f"{key} doesn't match.  '{dict1[key]}' vs '{dict2[key]}'")
         if self.dictionaries != other.dictionaries:
             return False
         if self.header_attributes != other.header_attributes:
diff --git a/webtools/.gitignore b/webtools/.gitignore
index 01d1c96cf..006556445 100644
--- a/webtools/.gitignore
+++ b/webtools/.gitignore
@@ -89,7 +89,7 @@ coverage.xml
 /venv/
 .idea
 config.py
-tests/xtemp_testing/*
+hedweb/tests/xtemp_testing/*
 
 # Rope
 .ropeproject
diff --git a/webtools/config_template.py b/webtools/config.py
similarity index 94%
rename from webtools/config_template.py
rename to webtools/config.py
index b9a194fe7..9615419d2 100644
--- a/webtools/config_template.py
+++ b/webtools/config.py
@@ -10,8 +10,9 @@
 
 
 class Config(object):
+    BASE_DIRECTORY = os.getcwd()
     # Folder the program stores hed cache, log, etc.  Make sure the program has access to this directory.
-    BASE_DIRECTORY = '/path/to/base/folder'
+    #BASE_DIRECTORY = '/path/to/base/folder'
     LOG_DIRECTORY = os.path.join(BASE_DIRECTORY, 'log')
     os.makedirs(LOG_DIRECTORY, exist_ok=True)
     LOG_FILE = os.path.join(LOG_DIRECTORY, 'error.log')


Test patch:
diff --git a/hedtools/tests/schema/test_schema_wiki_fatal_errors.py b/hedtools/tests/schema/test_schema_wiki_fatal_errors.py
index fa1434314..b93a56efd 100644
--- a/hedtools/tests/schema/test_schema_wiki_fatal_errors.py
+++ b/hedtools/tests/schema/test_schema_wiki_fatal_errors.py
@@ -28,8 +28,8 @@ def test_invalid_schema(self):
         for filename, error in self.files_and_errors.items():
             full_filename = self.full_base_folder + filename
 
-            print(filename)
-            print(f"Expected Error {error}")
+            #print(filename)
+            #print(f"Expected Error {error}")
             hed_schema = None
             try:
                 hed_schema = schema.load_schema(full_filename)
@@ -37,6 +37,6 @@ def test_invalid_schema(self):
                 self.assertFalse(True)
             except HedFileError as e:
                 formated_error = e.format_error_message()
-                print(f"Testing Error {e.error_type}")
+                #print(f"Testing Error {e.error_type}")
                 self.assertEqual(e.error_type, error)
                 pass
diff --git a/hedtools/tests/tools/test_tag_format.py b/hedtools/tests/tools/test_tag_format.py
index 443f5052c..e06b7d842 100644
--- a/hedtools/tests/tools/test_tag_format.py
+++ b/hedtools/tests/tools/test_tag_format.py
@@ -28,9 +28,9 @@ def converter_base(self, test_strings, expected_results, expected_errors, conver
             expected_result = expected_results[test_key]
 
             expected_issue = self.really_format_errors(error_handler, hed_string=test_string_obj, params=expected_params)
-            print(test_key)
-            print(expected_issue)
-            print(test_issues)
+            # print(test_key)
+            # print(expected_issue)
+            # print(test_issues)
             self.assertEqual(string_result, expected_result, test_strings[test_key])
             self.assertCountEqual(test_issues, expected_issue, test_strings[test_key])
 
diff --git a/hedtools/tests/validator/test_tag_validator.py b/hedtools/tests/validator/test_tag_validator.py
index f31b2c61f..7cc26444c 100644
--- a/hedtools/tests/validator/test_tag_validator.py
+++ b/hedtools/tests/validator/test_tag_validator.py
@@ -100,9 +100,9 @@ def validator_base(self, test_strings, expected_results, expected_issues, test_f
             expected_issue = self.really_format_errors(error_handler, hed_string=hed_string_obj,
                                                        params=expected_params)
 
-            print(test_key)
-            print(str(expected_issue))
-            print(str(test_issues))
+            # print(test_key)
+            # print(str(expected_issue))
+            # print(str(test_issues))
             error_handler.pop_error_context()
             self.assertEqual(test_result, expected_result, test_strings[test_key])
             self.assertCountEqual(test_issues, expected_issue, test_strings[test_key])
diff --git a/webtools/tests/__init__.py b/webtools/hedweb/tests/__init__.py
similarity index 100%
rename from webtools/tests/__init__.py
rename to webtools/hedweb/tests/__init__.py
diff --git a/webtools/tests/data/ExcelMultipleSheets.xlsx b/webtools/hedweb/tests/data/ExcelMultipleSheets.xlsx
similarity index 100%
rename from webtools/tests/data/ExcelMultipleSheets.xlsx
rename to webtools/hedweb/tests/data/ExcelMultipleSheets.xlsx
diff --git a/webtools/tests/data/ExcelOneSheet.xlsx b/webtools/hedweb/tests/data/ExcelOneSheet.xlsx
similarity index 100%
rename from webtools/tests/data/ExcelOneSheet.xlsx
rename to webtools/hedweb/tests/data/ExcelOneSheet.xlsx
diff --git a/webtools/tests/data/HED-generation2-schema-7.2.0.mediawiki b/webtools/hedweb/tests/data/HED-generation2-schema-7.2.0.mediawiki
similarity index 100%
rename from webtools/tests/data/HED-generation2-schema-7.2.0.mediawiki
rename to webtools/hedweb/tests/data/HED-generation2-schema-7.2.0.mediawiki
diff --git a/webtools/tests/data/HED-generation2-schema-7.2.0Bad.mediawiki b/webtools/hedweb/tests/data/HED-generation2-schema-7.2.0Bad.mediawiki
similarity index 100%
rename from webtools/tests/data/HED-generation2-schema-7.2.0Bad.mediawiki
rename to webtools/hedweb/tests/data/HED-generation2-schema-7.2.0Bad.mediawiki
diff --git a/webtools/tests/data/HED-generation3-schema-8.0.0-alpha.3Bad.mediawiki b/webtools/hedweb/tests/data/HED-generation3-schema-8.0.0-alpha.3Bad.mediawiki
similarity index 100%
rename from webtools/tests/data/HED-generation3-schema-8.0.0-alpha.3Bad.mediawiki
rename to webtools/hedweb/tests/data/HED-generation3-schema-8.0.0-alpha.3Bad.mediawiki
diff --git a/webtools/tests/data/HED-generation3-schema-8.0.0-beta.4.mediawiki b/webtools/hedweb/tests/data/HED-generation3-schema-8.0.0-beta.4.mediawiki
similarity index 100%
rename from webtools/tests/data/HED-generation3-schema-8.0.0-beta.4.mediawiki
rename to webtools/hedweb/tests/data/HED-generation3-schema-8.0.0-beta.4.mediawiki
diff --git a/webtools/tests/data/HED7.2.0.xml b/webtools/hedweb/tests/data/HED7.2.0.xml
similarity index 100%
rename from webtools/tests/data/HED7.2.0.xml
rename to webtools/hedweb/tests/data/HED7.2.0.xml
diff --git a/webtools/tests/data/HED8.0.0-beta.3.xsd b/webtools/hedweb/tests/data/HED8.0.0-beta.3.xsd
similarity index 100%
rename from webtools/tests/data/HED8.0.0-beta.3.xsd
rename to webtools/hedweb/tests/data/HED8.0.0-beta.3.xsd
diff --git a/webtools/tests/data/HED8.0.0-beta.4.xml b/webtools/hedweb/tests/data/HED8.0.0-beta.4.xml
similarity index 100%
rename from webtools/tests/data/HED8.0.0-beta.4.xml
rename to webtools/hedweb/tests/data/HED8.0.0-beta.4.xml
diff --git a/webtools/tests/data/HED8.0.0.xml b/webtools/hedweb/tests/data/HED8.0.0.xml
similarity index 100%
rename from webtools/tests/data/HED8.0.0.xml
rename to webtools/hedweb/tests/data/HED8.0.0.xml
diff --git a/webtools/tests/data/HEDbad.xml b/webtools/hedweb/tests/data/HEDbad.xml
similarity index 100%
rename from webtools/tests/data/HEDbad.xml
rename to webtools/hedweb/tests/data/HEDbad.xml
diff --git a/webtools/tests/data/LKTEventCodesHED3.tsv b/webtools/hedweb/tests/data/LKTEventCodesHED3.tsv
similarity index 100%
rename from webtools/tests/data/LKTEventCodesHED3.tsv
rename to webtools/hedweb/tests/data/LKTEventCodesHED3.tsv
diff --git a/webtools/tests/data/bids_events.json b/webtools/hedweb/tests/data/bids_events.json
similarity index 100%
rename from webtools/tests/data/bids_events.json
rename to webtools/hedweb/tests/data/bids_events.json
diff --git a/webtools/tests/data/bids_events.tsv b/webtools/hedweb/tests/data/bids_events.tsv
similarity index 100%
rename from webtools/tests/data/bids_events.tsv
rename to webtools/hedweb/tests/data/bids_events.tsv
diff --git a/webtools/tests/data/bids_events_alpha.json b/webtools/hedweb/tests/data/bids_events_alpha.json
similarity index 100%
rename from webtools/tests/data/bids_events_alpha.json
rename to webtools/hedweb/tests/data/bids_events_alpha.json
diff --git a/webtools/tests/data/wakeman-henson-tags.tsv b/webtools/hedweb/tests/data/wakeman-henson-tags.tsv
similarity index 100%
rename from webtools/tests/data/wakeman-henson-tags.tsv
rename to webtools/hedweb/tests/data/wakeman-henson-tags.tsv
diff --git a/webtools/tests/test_events.py b/webtools/hedweb/tests/test_events.py
similarity index 88%
rename from webtools/tests/test_events.py
rename to webtools/hedweb/tests/test_events.py
index 6d03ddcb1..407a79320 100644
--- a/webtools/tests/test_events.py
+++ b/webtools/hedweb/tests/test_events.py
@@ -1,33 +1,17 @@
 import os
-import shutil
 import unittest
 from werkzeug.test import create_environ
 from werkzeug.wrappers import Request
 
+from hedweb.tests.test_web_base import TestWebBase
 from hed import schema as hedschema
 from hed import models
 from hed.errors.exceptions import HedFileError
 from hedweb.constants import common
-from hedweb.app_factory import AppFactory
 
 
-class Test(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-        cls.upload_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/upload')
-        app = AppFactory.create_app('config.TestConfig')
-        with app.app_context():
-            from hedweb.routes import route_blueprint
-            app.register_blueprint(route_blueprint)
-            if not os.path.exists(cls.upload_directory):
-                os.mkdir(cls.upload_directory)
-            app.config['UPLOAD_FOLDER'] = cls.upload_directory
-            cls.app = app
-            cls.app.test = app.test_client()
-
-    @classmethod
-    def tearDownClass(cls):
-        shutil.rmtree(cls.upload_directory)
+class Test(TestWebBase):
+    cache_schemas = True
 
     def test_get_input_from_events_form_empty(self):
         from hedweb.events import get_input_from_events_form
@@ -40,8 +24,8 @@ def test_get_input_from_events_form(self):
         from hed.schema import HedSchema
         from hedweb.events import get_input_from_events_form
         with self.app.test:
-            json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), './data/bids_events_alpha.json')
-            events_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), './data/bids_events.tsv')
+            json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/bids_events_alpha.json')
+            events_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/bids_events.tsv')
             with open(json_path, 'rb') as fp:
                 with open(events_path, 'rb') as fpe:
                     environ = create_environ(data={common.JSON_FILE: fp, common.SCHEMA_VERSION: '8.0.0-alpha.1',
diff --git a/webtools/tests/test_routes/__init__.py b/webtools/hedweb/tests/test_routes/__init__.py
similarity index 100%
rename from webtools/tests/test_routes/__init__.py
rename to webtools/hedweb/tests/test_routes/__init__.py
diff --git a/webtools/tests/test_routes/test_routes_events.py b/webtools/hedweb/tests/test_routes/test_routes_events.py
similarity index 88%
rename from webtools/tests/test_routes/test_routes_events.py
rename to webtools/hedweb/tests/test_routes/test_routes_events.py
index befec76db..d8f79eb1a 100644
--- a/webtools/tests/test_routes/test_routes_events.py
+++ b/webtools/hedweb/tests/test_routes/test_routes_events.py
@@ -1,31 +1,12 @@
 import io
 import os
-import shutil
 import unittest
 from flask import Response
-from hedweb.app_factory import AppFactory
+from hedweb.tests.test_web_base import TestWebBase
 from hedweb.constants import common
 
 
-class Test(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-        cls.upload_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/upload')
-        app = AppFactory.create_app('config.TestConfig')
-        with app.app_context():
-            from hedweb.routes import route_blueprint
-            app.register_blueprint(route_blueprint)
-            if not os.path.exists(cls.upload_directory):
-                os.mkdir(cls.upload_directory)
-            app.config['UPLOAD_FOLDER'] = cls.upload_directory
-            app.config['WTF_CSRF_ENABLED'] = False
-            cls.app = app
-            cls.app.test = app.test_client()
-
-    @classmethod
-    def tearDownClass(cls):
-        shutil.rmtree(cls.upload_directory)
-
+class Test(TestWebBase):
     def test_events_results_empty_data(self):
         response = self.app.test.post('/events_submit')
         self.assertEqual(200, response.status_code, 'HED events request succeeds even when no data')
diff --git a/webtools/tests/test_routes/test_routes_render.py b/webtools/hedweb/tests/test_routes/test_routes_render.py
similarity index 75%
rename from webtools/tests/test_routes/test_routes_render.py
rename to webtools/hedweb/tests/test_routes/test_routes_render.py
index 8152170f8..6817f399a 100644
--- a/webtools/tests/test_routes/test_routes_render.py
+++ b/webtools/hedweb/tests/test_routes/test_routes_render.py
@@ -1,28 +1,8 @@
-import os
-import shutil
 import unittest
-from hedweb.app_factory import AppFactory
+from hedweb.tests.test_web_base import TestWebBase
 
 
-class Test(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-        cls.upload_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/upload')
-        app = AppFactory.create_app('config.TestConfig')
-        with app.app_context():
-            from hedweb.routes import route_blueprint
-            app.register_blueprint(route_blueprint)
-            if not os.path.exists(cls.upload_directory):
-                os.mkdir(cls.upload_directory)
-            app.config['UPLOAD_FOLDER'] = cls.upload_directory
-            app.config['WTF_CSRF_ENABLED'] = False
-            cls.app = app
-            cls.app.test = app.test_client()
-
-    @classmethod
-    def tearDownClass(cls):
-        shutil.rmtree(cls.upload_directory)
-
+class Test(TestWebBase):
     def test_render_additional_examples_page(self):
         response = self.app.test.get('/additional-examples')
         self.assertEqual(response.status_code, 200, "The additional-examples content page should exist")
diff --git a/webtools/tests/test_routes/test_routes_schema.py b/webtools/hedweb/tests/test_routes/test_routes_schema.py
similarity index 94%
rename from webtools/tests/test_routes/test_routes_schema.py
rename to webtools/hedweb/tests/test_routes/test_routes_schema.py
index 82a1983f5..6d05b6de7 100644
--- a/webtools/tests/test_routes/test_routes_schema.py
+++ b/webtools/hedweb/tests/test_routes/test_routes_schema.py
@@ -1,29 +1,10 @@
 import io
 import os
-import shutil
 import unittest
-from hedweb.app_factory import AppFactory
+from hedweb.tests.test_web_base import TestWebBase
 
 
-class Test(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-        cls.upload_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/upload')
-        app = AppFactory.create_app('config.TestConfig')
-        with app.app_context():
-            from hedweb.routes import route_blueprint
-            app.register_blueprint(route_blueprint)
-            if not os.path.exists(cls.upload_directory):
-                os.mkdir(cls.upload_directory)
-            app.config['UPLOAD_FOLDER'] = cls.upload_directory
-            app.config['WTF_CSRF_ENABLED'] = False
-            cls.app = app
-            cls.app.test = app.test_client()
-
-    @classmethod
-    def tearDownClass(cls):
-        shutil.rmtree(cls.upload_directory)
-
+class Test(TestWebBase):
     def test_schema_results_empty_data(self):
         response = self.app.test.post('/schema_submit')
         self.assertEqual(200, response.status_code, 'HED schema request succeeds even when no data')
diff --git a/webtools/tests/test_routes/test_routes_schema_versions.py b/webtools/hedweb/tests/test_routes/test_routes_schema_versions.py
similarity index 65%
rename from webtools/tests/test_routes/test_routes_schema_versions.py
rename to webtools/hedweb/tests/test_routes/test_routes_schema_versions.py
index 9f8d13db5..d1ddad41a 100644
--- a/webtools/tests/test_routes/test_routes_schema_versions.py
+++ b/webtools/hedweb/tests/test_routes/test_routes_schema_versions.py
@@ -1,31 +1,12 @@
 import os
 import io
 import json
-import shutil
 import unittest
 from werkzeug.datastructures import FileStorage
-from hedweb.app_factory import AppFactory
+from hedweb.tests.test_web_base import TestWebBase
 
 
-class Test(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-        cls.upload_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/upload')
-        app = AppFactory.create_app('config.TestConfig')
-        with app.app_context():
-            from hedweb.routes import route_blueprint
-            app.register_blueprint(route_blueprint)
-            if not os.path.exists(cls.upload_directory):
-                os.mkdir(cls.upload_directory)
-            app.config['UPLOAD_FOLDER'] = cls.upload_directory
-            app.config['WTF_CSRF_ENABLED'] = False
-            cls.app = app
-            cls.app.test = app.test_client()
-
-    @classmethod
-    def tearDownClass(cls):
-        shutil.rmtree(cls.upload_directory)
-
+class Test(TestWebBase):
     def test_schema_versions(self):
         with self.app.app_context():
             response = self.app.test.post('/schema_versions')
diff --git a/webtools/tests/test_routes/test_routes_sidecar.py b/webtools/hedweb/tests/test_routes/test_routes_sidecar.py
similarity index 91%
rename from webtools/tests/test_routes/test_routes_sidecar.py
rename to webtools/hedweb/tests/test_routes/test_routes_sidecar.py
index 54c8484b8..ebdbcfa62 100644
--- a/webtools/tests/test_routes/test_routes_sidecar.py
+++ b/webtools/hedweb/tests/test_routes/test_routes_sidecar.py
@@ -1,32 +1,13 @@
 import io
 import os
-import shutil
 import unittest
 from flask import Response
 from werkzeug.datastructures import FileStorage
-from hedweb.app_factory import AppFactory
+from hedweb.tests.test_web_base import TestWebBase
 from hedweb.constants import common
 
 
-class Test(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-        cls.upload_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/upload')
-        app = AppFactory.create_app('config.TestConfig')
-        with app.app_context():
-            from hedweb.routes import route_blueprint
-            app.register_blueprint(route_blueprint)
-            if not os.path.exists(cls.upload_directory):
-                os.mkdir(cls.upload_directory)
-            app.config['UPLOAD_FOLDER'] = cls.upload_directory
-            app.config['WTF_CSRF_ENABLED'] = False
-            cls.app = app
-            cls.app.test = app.test_client()
-
-    @classmethod
-    def tearDownClass(cls):
-        shutil.rmtree(cls.upload_directory)
-
+class Test(TestWebBase):
     def test_sidecar_results_empty_data(self):
         response = self.app.test.post('/sidecar_submit')
         self.assertEqual(200, response.status_code, 'HED sidecar request succeeds even when no data')
diff --git a/webtools/tests/test_routes/test_routes_spreadsheet.py b/webtools/hedweb/tests/test_routes/test_routes_spreadsheet.py
similarity index 90%
rename from webtools/tests/test_routes/test_routes_spreadsheet.py
rename to webtools/hedweb/tests/test_routes/test_routes_spreadsheet.py
index e8d3a5893..d88f59ea3 100644
--- a/webtools/tests/test_routes/test_routes_spreadsheet.py
+++ b/webtools/hedweb/tests/test_routes/test_routes_spreadsheet.py
@@ -1,32 +1,13 @@
 import io
 import os
-import shutil
 import unittest
 from flask import Response
 from werkzeug.datastructures import FileStorage
-from hedweb.app_factory import AppFactory
+from hedweb.tests.test_web_base import TestWebBase
 from hedweb.constants import common
 
 
-class Test(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-        cls.upload_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/upload')
-        app = AppFactory.create_app('config.TestConfig')
-        with app.app_context():
-            from hedweb.routes import route_blueprint
-            app.register_blueprint(route_blueprint)
-            if not os.path.exists(cls.upload_directory):
-                os.mkdir(cls.upload_directory)
-            app.config['UPLOAD_FOLDER'] = cls.upload_directory
-            app.config['WTF_CSRF_ENABLED'] = False
-            cls.app = app
-            cls.app.test = app.test_client()
-
-    @classmethod
-    def tearDownClass(cls):
-        shutil.rmtree(cls.upload_directory)
-
+class Test(TestWebBase):
     def test_spreadsheet_results_empty_data(self):
         response = self.app.test.post('/spreadsheet_submit')
         self.assertEqual(200, response.status_code, 'HED spreadsheet request succeeds even when no data')
diff --git a/webtools/tests/test_routes/test_routes_string.py b/webtools/hedweb/tests/test_routes/test_routes_string.py
similarity index 87%
rename from webtools/tests/test_routes/test_routes_string.py
rename to webtools/hedweb/tests/test_routes/test_routes_string.py
index bd4324ab7..417db0490 100644
--- a/webtools/tests/test_routes/test_routes_string.py
+++ b/webtools/hedweb/tests/test_routes/test_routes_string.py
@@ -1,29 +1,10 @@
-import os
 import json
-import shutil
 import unittest
-from hedweb.app_factory import AppFactory
+from hedweb.tests.test_web_base import TestWebBase
 from hedweb.constants import common
 
 
-class Test(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-        cls.upload_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../data/upload')
-        app = AppFactory.create_app('config.TestConfig')
-        with app.app_context():
-            from hedweb.routes import route_blueprint
-            app.register_blueprint(route_blueprint)
-            if not os.path.exists(cls.upload_directory):
-                os.mkdir(cls.upload_directory)
-            app.config['UPLOAD_FOLDER'] = cls.upload_directory
-            app.config['WTF_CSRF_ENABLED'] = False
-            cls.app = app
-            cls.app.test = app.test_client()
-
-    @classmethod
-    def tearDownClass(cls):
-        shutil.rmtree(cls.upload_directory)
+class Test(TestWebBase):
 
     def test_string_results_empty_data(self):
         response = self.app.test.post('/string_submit')
diff --git a/webtools/tests/test_schema.py b/webtools/hedweb/tests/test_schema.py
similarity index 83%
rename from webtools/tests/test_schema.py
rename to webtools/hedweb/tests/test_schema.py
index 4a1b2280b..5aed9040a 100644
--- a/webtools/tests/test_schema.py
+++ b/webtools/hedweb/tests/test_schema.py
@@ -1,30 +1,11 @@
 import os
-import shutil
 import unittest
 from werkzeug.test import create_environ
 from werkzeug.wrappers import Request
+from hedweb.tests.test_web_base import TestWebBase
 
-from hedweb.app_factory import AppFactory
-
-
-class Test(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-        cls.upload_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/upload')
-        app = AppFactory.create_app('config.TestConfig')
-        with app.app_context():
-            from hedweb.routes import route_blueprint
-            app.register_blueprint(route_blueprint)
-            if not os.path.exists(cls.upload_directory):
-                os.mkdir(cls.upload_directory)
-            app.config['UPLOAD_FOLDER'] = cls.upload_directory
-            cls.app = app
-            cls.app.test = app.test_client()
-
-    @classmethod
-    def tearDownClass(cls):
-        shutil.rmtree(cls.upload_directory)
 
+class Test(TestWebBase):
     def test_generate_input_from_schema_form_empty(self):
         from hedweb.schema import get_input_from_schema_form
         self.assertRaises(TypeError, get_input_from_schema_form, {},
@@ -35,7 +16,7 @@ def test_get_input_from_schema_form_valid(self):
         from hedweb.constants import common
         from hedweb.schema import get_input_from_schema_form
         with self.app.test:
-            schema_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), './data/HED8.0.0-beta.4.xml')
+            schema_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED8.0.0-beta.4.xml')
             with open(schema_path, 'rb') as fp:
                 environ = create_environ(data={common.SCHEMA_FILE: fp,
                                                common.SCHEMA_UPLOAD_OPTIONS: common.SCHEMA_FILE_OPTION,
diff --git a/webtools/tests/test_services.py b/webtools/hedweb/tests/test_services.py
similarity index 83%
rename from webtools/tests/test_services.py
rename to webtools/hedweb/tests/test_services.py
index 67fdbc10c..e61e76c06 100644
--- a/webtools/tests/test_services.py
+++ b/webtools/hedweb/tests/test_services.py
@@ -1,36 +1,16 @@
 import os
 import io
 import json
-import shutil
 import unittest
 from werkzeug.test import create_environ
 from werkzeug.wrappers import Request
-
+from hedweb.tests.test_web_base import TestWebBase
 from hed import schema as hedschema
 from hed import models
 from hedweb.constants import common
 
-from hedweb.app_factory import AppFactory
-
-
-class Test(unittest.TestCase):
-    @classmethod
-    def setUp(cls):
-        cls.upload_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/upload')
-        app = AppFactory.create_app('config.TestConfig')
-        with app.app_context():
-            from hedweb.routes import route_blueprint
-            app.register_blueprint(route_blueprint)
-            if not os.path.exists(cls.upload_directory):
-                os.mkdir(cls.upload_directory)
-            app.config['UPLOAD_FOLDER'] = cls.upload_directory
-            cls.app = app
-            cls.app.test = app.test_client()
-
-    @classmethod
-    def tearDownClass(cls):
-        shutil.rmtree(cls.upload_directory)
 
+class Test(TestWebBase):
     def test_get_input_from_service_request_empty(self):
         from hedweb.services import get_input_from_service_request
         self.assertRaises(TypeError, get_input_from_service_request, {},
@@ -42,8 +22,8 @@ def test_get_input_from_service_request(self):
         from hed.schema import HedSchema
         from hedweb.services import get_input_from_service_request
         with self.app.test:
-            json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), './data/bids_events_alpha.json')
-            events_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), './data/bids_events.tsv')
+            json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/bids_events_alpha.json')
+            events_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/bids_events.tsv')
             with open(json_path, 'rb') as fp:
                 json_string = fp.read().decode('ascii')
             json_data = {common.JSON_STRING: json_string, common.CHECK_FOR_WARNINGS: 'on',
diff --git a/webtools/tests/test_sidecar.py b/webtools/hedweb/tests/test_sidecar.py
similarity index 90%
rename from webtools/tests/test_sidecar.py
rename to webtools/hedweb/tests/test_sidecar.py
index 12e4fc852..3b4cded4b 100644
--- a/webtools/tests/test_sidecar.py
+++ b/webtools/hedweb/tests/test_sidecar.py
@@ -4,31 +4,13 @@
 
 from werkzeug.test import create_environ
 from werkzeug.wrappers import Request
+from hedweb.tests.test_web_base import TestWebBase
 import hed.schema as hedschema
 from hed import models
 from hedweb.constants import common
 
-from hedweb.app_factory import AppFactory
-
-
-class Test(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-        cls.upload_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/upload')
-        app = AppFactory.create_app('config.TestConfig')
-        with app.app_context():
-            from hedweb.routes import route_blueprint
-            app.register_blueprint(route_blueprint)
-            if not os.path.exists(cls.upload_directory):
-                os.mkdir(cls.upload_directory)
-            app.config['UPLOAD_FOLDER'] = cls.upload_directory
-            cls.app = app
-            cls.app.test = app.test_client()
-
-    @classmethod
-    def tearDownClass(cls):
-        shutil.rmtree(cls.upload_directory)
 
+class Test(TestWebBase):
     def test_generate_input_from_sidecar_form_empty(self):
         from hedweb.sidecar import get_input_from_sidecar_form
         self.assertRaises(TypeError, get_input_from_sidecar_form, {},
@@ -39,7 +21,7 @@ def test_generate_input_from_sidecar_form(self):
         from hed.schema import HedSchema
         from hedweb.sidecar import get_input_from_sidecar_form
         with self.app.test:
-            json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), './data/bids_events_alpha.json')
+            json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/bids_events_alpha.json')
             with open(json_path, 'rb') as fp:
                 environ = create_environ(data={common.JSON_FILE: fp, common.SCHEMA_VERSION: '8.0.0-alpha.1',
                                          common.COMMAND_OPTION: common.COMMAND_TO_LONG})
diff --git a/webtools/tests/test_spreadsheet.py b/webtools/hedweb/tests/test_spreadsheet.py
similarity index 90%
rename from webtools/tests/test_spreadsheet.py
rename to webtools/hedweb/tests/test_spreadsheet.py
index e3b800d24..eebc187ea 100644
--- a/webtools/tests/test_spreadsheet.py
+++ b/webtools/hedweb/tests/test_spreadsheet.py
@@ -1,34 +1,14 @@
 import os
-import shutil
 import unittest
 from werkzeug.test import create_environ
 from werkzeug.wrappers import Request
-
+from hedweb.tests.test_web_base import TestWebBase
 import hed.schema as hedschema
 from hed import models
 from hedweb.constants import common
 
-from hedweb.app_factory import AppFactory
-
-
-class Test(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-        cls.upload_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/upload')
-        app = AppFactory.create_app('config.TestConfig')
-        with app.app_context():
-            from hedweb.routes import route_blueprint
-            app.register_blueprint(route_blueprint)
-            if not os.path.exists(cls.upload_directory):
-                os.mkdir(cls.upload_directory)
-            app.config['UPLOAD_FOLDER'] = cls.upload_directory
-            cls.app = app
-            cls.app.test = app.test_client()
-
-    @classmethod
-    def tearDownClass(cls):
-        shutil.rmtree(cls.upload_directory)
 
+class Test(TestWebBase):
     def test_get_input_from_spreadsheet_form_empty(self):
         from hedweb.spreadsheet import get_input_from_spreadsheet_form
         self.assertRaises(TypeError, get_input_from_spreadsheet_form, {},
@@ -39,8 +19,8 @@ def test_get_input_from_spreadsheet_form(self):
         from hed.schema import HedSchema
         from hedweb.spreadsheet import get_input_from_spreadsheet_form
         with self.app.test:
-            spreadsheet_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), './data/ExcelOneSheet.xlsx')
-            schema_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), './data/HED8.0.0-beta.4.xml')
+            spreadsheet_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/ExcelOneSheet.xlsx')
+            schema_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED8.0.0-beta.4.xml')
             with open(spreadsheet_path, 'rb') as fp:
                 with open(schema_path, 'rb') as sp:
                     environ = create_environ(data={common.SPREADSHEET_FILE: fp,
diff --git a/webtools/tests/test_strings.py b/webtools/hedweb/tests/test_strings.py
similarity index 86%
rename from webtools/tests/test_strings.py
rename to webtools/hedweb/tests/test_strings.py
index f56bc985e..e47163194 100644
--- a/webtools/tests/test_strings.py
+++ b/webtools/hedweb/tests/test_strings.py
@@ -1,31 +1,14 @@
 import os
-import shutil
 import unittest
 from werkzeug.test import create_environ
 from werkzeug.wrappers import Request
+
+from hedweb.tests.test_web_base import TestWebBase
 import hed.schema as hedschema
 from hedweb.constants import common
-from hedweb.app_factory import AppFactory
-
-
-class Test(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-        cls.upload_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/upload')
-        app = AppFactory.create_app('config.TestConfig')
-        with app.app_context():
-            from hedweb.routes import route_blueprint
-            app.register_blueprint(route_blueprint)
-            if not os.path.exists(cls.upload_directory):
-                os.mkdir(cls.upload_directory)
-            app.config['UPLOAD_FOLDER'] = cls.upload_directory
-            cls.app = app
-            cls.app.test = app.test_client()
-
-    @classmethod
-    def tearDownClass(cls):
-        shutil.rmtree(cls.upload_directory)
 
+
+class Test(TestWebBase):
     def test_get_input_from_string_form_empty(self):
         from hedweb.strings import get_input_from_string_form
         self.assertRaises(TypeError, get_input_from_string_form, {},
diff --git a/webtools/hedweb/tests/test_web_base.py b/webtools/hedweb/tests/test_web_base.py
new file mode 100644
index 000000000..01c1a8155
--- /dev/null
+++ b/webtools/hedweb/tests/test_web_base.py
@@ -0,0 +1,33 @@
+import os
+import shutil
+import unittest
+from hedweb.app_factory import AppFactory
+import sys
+sys.path.append('hedtools')
+
+
+class TestWebBase(unittest.TestCase):
+    enable_csrf = False
+    cache_schemas = False
+
+    @classmethod
+    def setUpClass(cls):
+        cls.upload_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/upload')
+        app = AppFactory.create_app('config.TestConfig')
+        with app.app_context():
+            from hed import schema as hedschema
+            hedschema.set_cache_directory(app.config['HED_CACHE_FOLDER'])
+            if cls.cache_schemas:
+                hedschema.cache_all_hed_xml_versions()
+            from hedweb.routes import route_blueprint
+            app.register_blueprint(route_blueprint)
+            if not os.path.exists(cls.upload_directory):
+                os.mkdir(cls.upload_directory)
+            app.config['UPLOAD_FOLDER'] = cls.upload_directory
+            app.config['WTF_CSRF_ENABLED'] = cls.enable_csrf
+            cls.app = app
+            cls.app.test = app.test_client()
+
+    @classmethod
+    def tearDownClass(cls):
+        shutil.rmtree(cls.upload_directory)
\ No newline at end of file
diff --git a/webtools/tests/test_web_utils.py b/webtools/hedweb/tests/test_web_utils.py
similarity index 94%
rename from webtools/tests/test_web_utils.py
rename to webtools/hedweb/tests/test_web_utils.py
index 32b9ffa57..113660585 100644
--- a/webtools/tests/test_web_utils.py
+++ b/webtools/hedweb/tests/test_web_utils.py
@@ -1,30 +1,13 @@
 import os
-import shutil
 import unittest
 from werkzeug.test import create_environ
 from werkzeug.wrappers import Request, Response
-from hedweb.app_factory import AppFactory
 
 
-class Test(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-        cls.upload_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), './data/upload')
-        app = AppFactory.create_app('config.TestConfig')
-        with app.app_context():
-            from hedweb.routes import route_blueprint
-            app.register_blueprint(route_blueprint)
-            if not os.path.exists(cls.upload_directory):
-                os.mkdir(cls.upload_directory)
-            app.config['UPLOAD_FOLDER'] = cls.upload_directory
-            app.config['WTF_CSRF_ENABLED'] = False
-            cls.app = app
-            cls.app.test = app.test_client()
+from hedweb.tests.test_web_base import TestWebBase
 
-    @classmethod
-    def tearDownClass(cls):
-        shutil.rmtree(cls.upload_directory)
 
+class Test(TestWebBase):
     def test_file_extension_is_valid(self):
         from hedweb.web_utils import file_extension_is_valid
         is_valid = file_extension_is_valid('abc.xml', ['.xml', '.txt'])