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

Issue:
Update wiki reader to get rid of some of the more confusing parts
Improve some minor style issues in tag validator


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 48b645875..d87c6c958 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -41,7 +41,7 @@ jobs:
       - name: Test with unittest
         run: |
           coverage run -m unittest discover -s ./hedtools
-          coverage run -a -m unittest discover -s ./webtools
+          coverage run -a -m unittest discover -s ./webtools 
 
       - name: publish-coverages
         uses: paambaati/codeclimate-action@v2.7.5
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/hed/schema/wiki2schema.py b/hedtools/hed/schema/wiki2schema.py
index 225472d66..79c3a0ea9 100644
--- a/hedtools/hed/schema/wiki2schema.py
+++ b/hedtools/hed/schema/wiki2schema.py
@@ -76,9 +76,6 @@ def __init__(self, wiki_file_path, schema_as_string):
 
         self._schema = HedSchema()
         self._schema.filename = wiki_file_path
-        # Variables used while parsing.
-        self._found_sections = {}
-        self._current_section = HedSection.HeaderLine
 
         try:
             if wiki_file_path and schema_as_string:
@@ -98,29 +95,72 @@ def __init__(self, wiki_file_path, schema_as_string):
     @staticmethod
     def load_wiki(wiki_file_path=None, schema_as_string=None):
         parser = HedSchemaWikiParser(wiki_file_path, schema_as_string)
-
         return parser._schema
 
-    def _get_line_iter(self, wiki_lines):
-        """ This function iterates over the file line by line, keeping track of which file section it is currently in.
+    def _read_wiki(self, wiki_lines):
+        """
+        Calls the parsers for each section as this goes through the file.
+
         Parameters
         ----------
         wiki_lines : iter(str)
-            An opened .mediawiki file or other list of lines
+            An opened .mediawiki file
+        """
+        # Read header line as we need it to determine if this is a hed3 schema or not before locating sections
+        self._read_header_line(wiki_lines[0])
+
+        wiki_lines_by_section = self._split_lines_into_sections(wiki_lines)
+        parse_order = {
+            HedSection.HeaderLine: self._read_header_section,
+            HedSection.Prologue: self._read_prologue,
+            HedSection.Properties: self._read_properties,
+            HedSection.Epilogue: self._read_epilogue,
+            # Pass 2
+            HedSection.Attributes: self._read_attributes,
+            # Pass3
+            HedSection.Schema: self._read_schema,
+            HedSection.UnitsClasses: self._read_unit_classes,
+            HedSection.UnitModifiers: self._read_unit_modifiers,
+            HedSection.ValueClasses: self._read_value_classes,
+        }
+        self._parse_sections(wiki_lines_by_section, parse_order)
+
+        # Validate we didn't miss any required sections.
+        for section in required_sections:
+            if section not in wiki_lines_by_section:
+                error_code = HedExceptions.INVALID_SECTION_SEPARATOR
+                if section in ErrorsBySection:
+                    error_code = ErrorsBySection[section]
+                raise HedFileError(error_code,
+                                   f"Required section separator '{SectionNames[section]}' not found in file",
+                                   filename=self.filename)
+
+    def _split_lines_into_sections(self, wiki_lines):
         """
+            Takes a list of lines, and splits it into valid wiki sections.
+
+        Parameters
+        ----------
+        wiki_lines : [str]
+
+        Returns
+        -------
+        sections: {str: [str]}
+            A list of lines for each section of the schema(not including the identifying section line)
+        """
+        # We start having found the header and may still be in it
+        current_section = HedSection.HeaderLine
+        found_section = True
+        strings_for_section = {}
         for line in wiki_lines:
-            found_section = False
             for key, section_string in SectionStarts.items():
                 if line.startswith(section_string):
-                    if key in self._found_sections:
+                    if key in strings_for_section:
                         raise HedFileError(HedExceptions.INVALID_SECTION_SEPARATOR,
                                            f"Found section {SectionNames[key]} twice", filename=self.filename)
-                    self._found_sections[key] = True
 
-                    if self._current_section < key:
-                        self._current_section = key
-                        # this line is already handled and we are in a new section
-                        yield False
+                    if current_section < key:
+                        current_section = key
                         found_section = True
                         break
                     else:
@@ -133,11 +173,12 @@ def _get_line_iter(self, wiki_lines):
                                            filename=self.filename)
 
             if found_section:
+                strings_for_section[current_section] = []
+                found_section = False
                 continue
 
-            if (self._current_section != HedSection.Schema and line.startswith(wiki_constants.ROOT_TAG) and
+            if (current_section != HedSection.Schema and line.startswith(wiki_constants.ROOT_TAG) and
                     not (line.startswith(wiki_constants.OLD_SYNTAX_SECTION_NAME) and not self._schema.is_hed3_schema)):
-
                 raise HedFileError(HedExceptions.INVALID_SECTION_SEPARATOR,
                                    f"Invalid section separator '{line.strip()}'", filename=self.filename)
 
@@ -145,151 +186,89 @@ def _get_line_iter(self, wiki_lines):
                 raise HedFileError(HedExceptions.INVALID_SECTION_SEPARATOR,
                                    f"Invalid section separator '{line.strip()}'", filename=self.filename)
 
-            if self._current_section == HedSection.Prologue or self._current_section == HedSection.Epilogue:
+            if current_section == HedSection.Prologue or current_section == HedSection.Epilogue:
                 if line.strip():
                     # we want to preserve all formatting in the prologue and epilogue.
                     if line.endswith("\n"):
                         line = line[:-1]
-                    yield line
+                    strings_for_section[current_section].append(line)
             else:
                 line = self._remove_nowiki_tag_from_line(line.strip())
                 if line:
-                    yield line
-
-        self._current_section = None
-
-    def _read_wiki(self, wiki_lines):
-        """
-        Calls the parsers for each section as this goes through the file.
-
-        Parameters
-        ----------
-        wiki_lines : iter(str)
-            An opened .mediawiki file
-        """
-        parsers_pass1 = {
-            HedSection.HeaderLine: self._read_header_line,
-            HedSection.Prologue: self._read_prologue,
-            HedSection.Properties: self._read_properties,
-            HedSection.Epilogue: self._read_epilogue,
-        }
+                    strings_for_section[current_section].append(line)
 
-        parsers_pass2 = {
-            HedSection.Attributes: self._read_attributes,
-        }
+        return strings_for_section
 
-        parsers_pass3 = {
-            HedSection.Schema: self._read_schema,
-            HedSection.UnitsClasses: self._read_unit_classes,
-            HedSection.UnitModifiers: self._read_unit_modifiers,
-            HedSection.ValueClasses: self._read_value_classes,
-        }
+    def _parse_sections(self, wiki_lines_by_section, parse_order):
+        for section in parse_order:
+            lines_for_section = wiki_lines_by_section.get(section, [])
+            parse_func = parse_order[section]
+            parse_func(lines_for_section)
 
-        self._run_parse_pass(parsers_pass1, wiki_lines)
-        self._schema.add_default_properties()
-
-        self._run_parse_pass(parsers_pass2, wiki_lines)
-        self._schema.add_hed2_attributes()
-
-        self._run_parse_pass(parsers_pass3, wiki_lines)
-
-        # Validate we didn't miss any required sections.
-        for section in required_sections:
-            if section not in self._found_sections:
-                error_code = HedExceptions.INVALID_SECTION_SEPARATOR
-                if section in ErrorsBySection:
-                    error_code = ErrorsBySection[section]
-                raise HedFileError(error_code,
-                                   f"Required section separator '{SectionNames[section]}' not found in file",
-                                   filename=self.filename)
-
-    def _run_parse_pass(self, parser_dict, wiki_lines):
-        self._found_sections = {}
-        self._current_section = HedSection.HeaderLine
-        file_iter = self._get_line_iter(wiki_lines)
-        while self._current_section is not None:
-            # The iterator is responsible for updating the current_section variable.
-            # it will be set to None when at the end of the file.
-            parser_dict.get(self._current_section, self._skip_read_section)(file_iter)
-
-    def _read_header_line(self, file_iter):
-        """Adds the header line
-
-        Parameters
-        ----------
-        file_iter: iter
-            An iterator from self._get_line_iter.
-        """
-        first_line = next(file_iter)
-        # First line MUST be the HED line with full proper formatting.
-        if first_line.startswith(wiki_constants.HEADER_LINE_STRING):
-            hed_attributes = self._get_header_attributes(first_line[len(wiki_constants.HEADER_LINE_STRING):])
+    def _read_header_line(self, line):
+        if line.startswith(wiki_constants.HEADER_LINE_STRING):
+            hed_attributes = self._get_header_attributes(line[len(wiki_constants.HEADER_LINE_STRING):])
             self.issues = schema_validation_util.validate_attributes(hed_attributes, filename=self.filename)
             self.header_attributes = hed_attributes
             self._schema.issues = self.issues
             self._schema.header_attributes = hed_attributes
-        else:
-            raise HedFileError(HedExceptions.SCHEMA_HEADER_MISSING,
-                               f"First line of file should be HED, instead found: {first_line}", filename=self.filename)
+            return
+        raise HedFileError(HedExceptions.SCHEMA_HEADER_MISSING,
+                           f"First line of file should be HED, instead found: {line}", filename=self.filename)
 
-        for line in file_iter:
-            if line is False:
-                return
-
-            raise HedFileError(HedExceptions.SCHEMA_HEADER_INVALID,
-                               f"There should be no other content in the between the HED line in the header and either the prologue or schema sections.", filename=self.filename)
+    def _read_header_section(self, lines):
+        """Ensures the header has no content other than the initial line.
 
+        Parameters
+        ----------
+        lines: [str]
+            Lines for this section
+        """
+        for line in lines:
+            if line.strip():
+                raise HedFileError(HedExceptions.SCHEMA_HEADER_INVALID,
+                                   f"There should be no other content in the between the HED line in the header and either the prologue or schema sections.", filename=self.filename)
 
-    def _read_prologue(self, file_iter):
+    def _read_prologue(self, lines):
         """Adds the prologue
 
         Parameters
         ----------
-        file_iter: iter
-            An iterator from self._get_line_iter.
+        lines: [str]
+            Lines for this section
         """
         prologue = ""
-        for line in file_iter:
-            if line is False:
-                self._schema.prologue = prologue
-                return
-
+        for line in lines:
             if prologue:
                 prologue += "\n"
             prologue += line
+        self._schema.prologue = prologue
 
-    def _read_epilogue(self, file_iter):
+    def _read_epilogue(self, lines):
         """Adds the epilogue
 
         Parameters
         ----------
-        file_iter: iter
-            An iterator from self._get_line_iter.
+        lines: [str]
+            Lines for this section
         """
         epilogue = ""
-        for line in file_iter:
-            if line is False:
-                self._schema.epilogue = epilogue
-                break
-
+        for line in lines:
             if epilogue:
                 epilogue += "\n"
             epilogue += line
+        self._schema.epilogue = epilogue
 
-
-    def _read_schema(self, file_iter):
+    def _read_schema(self, lines):
         """Adds the main schema section
 
         Parameters
         ----------
-        file_iter: iter
-            An iterator from self._get_line_iter.
+        lines: [str]
+            Lines for this section
         """
         parent_tags = []
-        for line in file_iter:
-            if line is False:
-                return
-
+        for line in lines:
             if line.startswith(wiki_constants.ROOT_TAG):
                 parent_tags = []
                 new_tag = self._add_tag_line(parent_tags, line)
@@ -301,32 +280,17 @@ def _read_schema(self, file_iter):
                 new_tag = self._add_tag_line(parent_tags, line)
                 parent_tags.append(new_tag)
 
-    def _skip_read_section(self, file_iter):
-        """Reads from the file until it reaches a new section, discarding all lines.
-
-        Parameters
-        ----------
-        file_iter: iter
-            An iterator from self._get_line_iter.
-        """
-        for line in file_iter:
-            if line is False:
-                return
-
-    def _read_unit_classes(self, file_iter):
+    def _read_unit_classes(self, lines):
         """Adds the unit classes section
 
         Parameters
         ----------
-        file_iter: iter
-            An iterator from self._get_line_iter.
+        lines: [str]
+            Lines for this section
         """
         current_unit_class = ""
 
-        for line in file_iter:
-            if line is False:
-                return
-
+        for line in lines:
             unit_class = self._get_tag_name(line)
             level = self._get_tag_level(line)
             # This is a unit class
@@ -339,52 +303,43 @@ def _read_unit_classes(self, file_iter):
                 self._schema._add_unit_class_unit(current_unit_class, unit_class)
                 self._add_single_line(line, HedKey.Units, skip_adding_name=True)
 
-    def _read_unit_modifiers(self, file_iter):
+    def _read_unit_modifiers(self, lines):
         """Adds the unit modifiers section
 
         Parameters
         ----------
-        file_iter: iter
-            An iterator from self._get_line_iter.
+        lines: [str]
+            Lines for this section
         """
-        for line in file_iter:
-            if line is False:
-                return
-
+        for line in lines:
             self._add_single_line(line, HedKey.UnitModifiers)
 
-    def _read_value_classes(self, file_iter):
+    def _read_value_classes(self, lines):
         """Adds the unit modifiers section
 
-                Parameters
-                ----------
-                file_iter: iter
-                    An iterator from self._get_line_iter.
-                """
-        for line in file_iter:
-            if line is False:
-                return
-
+        Parameters
+        ----------
+        lines: [str]
+            Lines for this section
+        """
+        for line in lines:
             self._add_single_line(line, HedKey.ValueClasses)
 
-    def _read_properties(self, file_iter):
-        for line in file_iter:
-            if line is False:
-                return
-
+    def _read_properties(self, lines):
+        for line in lines:
             prop_name = self._get_tag_name(line)
             prop_desc = self._get_tag_description(line)
             self._schema._add_property_name_to_dict(prop_name, prop_desc)
+        self._schema.add_default_properties()
 
-    def _read_attributes(self, file_iter):
+    def _read_attributes(self, lines):
         self.attributes = {}
-        for line in file_iter:
-            if line is False:
-                return
+        for line in lines:
             attribute_name = self._get_tag_name(line)
 
             self._schema._add_attribute_name_to_dict(attribute_name)
             self._add_single_line(line, HedKey.Attributes)
+        self._schema.add_hed2_attributes()
 
     def _get_header_attributes(self, version_line):
         """Extracts all valid attributes like version from the HED line in .mediawiki format.
diff --git a/hedtools/hed/validator/tag_validator.py b/hedtools/hed/validator/tag_validator.py
index e4430a527..d777f38f9 100644
--- a/hedtools/hed/validator/tag_validator.py
+++ b/hedtools/hed/validator/tag_validator.py
@@ -26,7 +26,8 @@ class TagValidator:
     DATE_TIME_VALUE_CLASS = 'dateTime'
     NUMERIC_VALUE_CLASS = "numericClass"
 
-    DEFAULT_ALLOWED_PLACEHOLDER_CHARS = ".+-^ _"
+    # # sign is allowed by default as it is specifically checked for separately.
+    DEFAULT_ALLOWED_PLACEHOLDER_CHARS = ".+-^ _#"
     TAG_ALLOWED_CHARS = "-_/"
 
     def __init__(self, hed_schema=None, check_for_warnings=False, run_semantic_validation=True,
@@ -117,6 +118,7 @@ def run_individual_tag_validators(self, original_tag):
             elif original_tag.extension_or_value_portion:
                 validation_issues += self.check_for_invalid_extension_chars(original_tag)
 
+            validation_issues += self.check_for_placeholder(original_tag)
             validation_issues += self.check_tag_requires_child(original_tag)
         if self._check_for_warnings:
             validation_issues += self.check_capitalization(original_tag)
@@ -332,7 +334,6 @@ def check_tag_unit_class_units_are_valid(self, original_tag):
                     validation_issues += self._error_handler.format_error(ValidationErrors.HED_UNITS_INVALID,
                                                                           original_tag,
                                                                           unit_class_units=tag_unit_class_units)
-            validation_issues += self._check_for_placeholder(original_tag)
         return validation_issues
 
     def check_tag_value_class_valid(self, original_tag):
@@ -352,7 +353,6 @@ def check_tag_value_class_valid(self, original_tag):
             validation_issues += self._error_handler.format_error(ValidationErrors.HED_VALUE_INVALID,
                                                                   original_tag)
 
-        validation_issues += self._check_for_placeholder(original_tag)
         return validation_issues
 
     def _validate_value_class_portion(self, original_tag, portion_to_validate):
@@ -375,12 +375,8 @@ def _validate_value_class_portion(self, original_tag, portion_to_validate):
 
         valid_func = self.VALUE_CLASS_TYPE_DICT.get(value_class)
         if valid_func:
-            if valid_func(portion_to_validate):
-                return True
-        else:
-            return True
-
-        return False
+            return valid_func(portion_to_validate)
+        return True
 
     def check_tag_requires_child(self, original_tag):
         """Reports a validation error if the tag provided has the 'requireChild' attribute.
@@ -436,8 +432,6 @@ def check_for_invalid_extension_chars(self, original_tag):
         """
         allowed_chars = self.TAG_ALLOWED_CHARS
         allowed_chars += self.DEFAULT_ALLOWED_PLACEHOLDER_CHARS
-        if self._placeholders_allowed_in_strings:
-            allowed_chars += "#"
         allowed_chars += " "
         return self._check_invalid_chars(original_tag.extension_or_value_portion, allowed_chars, original_tag,
                                          starting_index=len(original_tag.org_base_tag) + 1)
@@ -695,7 +689,7 @@ def _character_is_delimiter(character):
         """
         return character == TagValidator.COMMA
 
-    def _check_for_placeholder(self, original_tag):
+    def check_for_placeholder(self, original_tag):
         """
             Checks for a placeholder character in the extension/value portion of a tag, unless they are allowed.
 
@@ -720,7 +714,7 @@ def _check_for_placeholder(self, original_tag):
 
         return validation_issues
 
-    def _check_invalid_chars(self, check_string, allowed_chars, source_tag, starting_index=0, actual_error=None):
+    def _check_invalid_chars(self, check_string, allowed_chars, source_tag, starting_index=0):
         validation_issues = []
         for i, character in enumerate(check_string):
             if character.isalnum():
@@ -732,7 +726,6 @@ def _check_invalid_chars(self, check_string, allowed_chars, source_tag, starting
                 continue
             validation_issues += self._error_handler.format_error(ValidationErrors.INVALID_TAG_CHARACTER,
                                                                   tag=source_tag, index_in_tag=starting_index + i,
-                                                                  index_in_tag_end=starting_index + i + 1,
-                                                                  actual_error=actual_error)
+                                                                  index_in_tag_end=starting_index + i + 1)
         return validation_issues
 
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


Test patch:
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 70%
rename from webtools/tests/test_events.py
rename to webtools/hedweb/tests/test_events.py
index e0d2ec6cf..407a79320 100644
--- a/webtools/tests/test_events.py
+++ b/webtools/hedweb/tests/test_events.py
@@ -1,37 +1,17 @@
 import os
-import shutil
 import unittest
 from werkzeug.test import create_environ
 from werkzeug.wrappers import Request
 
-import sys
-sys.path.append('hedtools')
+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 hed import schema as hedschema
-            hedschema.set_cache_directory(app.config['HED_CACHE_FOLDER'])
-            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
@@ -39,28 +19,28 @@ def test_get_input_from_events_form_empty(self):
                           "An exception should be raised if an empty request is passed")
         self.assertTrue(1, "Testing get_input_from_events_form")
 
-    # def test_get_input_from_events_form(self):
-    #     from hed.models import EventsInput
-    #     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')
-    #         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',
-    #                                          common.EVENTS_FILE: fpe, common.DEFS_EXPAND: 'on',
-    #                                          common.COMMAND_OPTION: common.COMMAND_ASSEMBLE})
-    #         request = Request(environ)
-    #         arguments = get_input_from_events_form(request)
-    #         self.assertIsInstance(arguments[common.EVENTS], EventsInput,
-    #                               "get_input_from_events_form should have an events object")
-    #         self.assertIsInstance(arguments[common.SCHEMA], HedSchema,
-    #                               "get_input_from_events_form should have a HED schema")
-    #         self.assertEqual(common.COMMAND_ASSEMBLE, arguments[common.COMMAND],
-    #                          "get_input_from_events_form should have a command")
-    #         self.assertTrue(arguments[common.DEFS_EXPAND],
-    #                         "get_input_from_events_form should have defs_expand true when on")
+    def test_get_input_from_events_form(self):
+        from hed.models import EventsInput
+        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')
+            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',
+                                             common.EVENTS_FILE: fpe, common.DEFS_EXPAND: 'on',
+                                             common.COMMAND_OPTION: common.COMMAND_ASSEMBLE})
+            request = Request(environ)
+            arguments = get_input_from_events_form(request)
+            self.assertIsInstance(arguments[common.EVENTS], EventsInput,
+                                  "get_input_from_events_form should have an events object")
+            self.assertIsInstance(arguments[common.SCHEMA], HedSchema,
+                                  "get_input_from_events_form should have a HED schema")
+            self.assertEqual(common.COMMAND_ASSEMBLE, arguments[common.COMMAND],
+                             "get_input_from_events_form should have a command")
+            self.assertTrue(arguments[common.DEFS_EXPAND],
+                            "get_input_from_events_form should have defs_expand true when on")
 
     def test_events_process_empty_file(self):
         # Test for empty events_path
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/hedweb/tests/test_routes/test_routes_events.py b/webtools/hedweb/tests/test_routes/test_routes_events.py
new file mode 100644
index 000000000..d8f79eb1a
--- /dev/null
+++ b/webtools/hedweb/tests/test_routes/test_routes_events.py
@@ -0,0 +1,135 @@
+import io
+import os
+import unittest
+from flask import Response
+from hedweb.tests.test_web_base import TestWebBase
+from hedweb.constants import common
+
+
+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')
+        self.assertTrue(isinstance(response, Response),
+                        'events_results validate should return a response object when empty events')
+        header_dict = dict(response.headers)
+        self.assertEqual("error", header_dict["Category"], "The header msg_category when no events is error ")
+        self.assertFalse(response.data, "The response data for empty events request is empty")
+
+    def test_events_results_assemble_valid(self):
+        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, 'r') as sc:
+            x = sc.read()
+        json_buffer = io.BytesIO(bytes(x, 'utf-8'))
+
+        with open(events_path, 'r') as sc:
+            y = sc.read()
+        events_buffer = io.BytesIO(bytes(y, 'utf-8'))
+
+        with self.app.app_context():
+            input_data = {common.SCHEMA_VERSION: '8.0.0-alpha.2',
+                          common.COMMAND_OPTION: common.COMMAND_ASSEMBLE,
+                          'json_file': (json_buffer, 'bids_events_alpha.json'),
+                          'events_file': (events_buffer, 'bids_events.tsv'),
+                          'defs_expand': 'on',
+                          common.CHECK_FOR_WARNINGS: 'on'}
+            response = self.app.test.post('/events_submit', content_type='multipart/form-data', data=input_data)
+            self.assertEqual(200, response.status_code, 'Assembly of a valid events file has a response')
+            headers_dict = dict(response.headers)
+            self.assertEqual("success", headers_dict["Category"],
+                             "The valid events file should assemble successfully")
+            self.assertTrue(response.data, "The assembled events file should not be empty")
+            json_buffer.close()
+            events_buffer.close()
+
+    def test_events_results_assemble_invalid(self):
+        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, 'r') as sc:
+            x = sc.read()
+        json_buffer = io.BytesIO(bytes(x, 'utf-8'))
+
+        with open(events_path, 'r') as sc:
+            y = sc.read()
+        events_buffer = io.BytesIO(bytes(y, 'utf-8'))
+
+        with self.app.app_context():
+            input_data = {common.SCHEMA_VERSION: '7.2.0',
+                          common.COMMAND_OPTION: common.COMMAND_ASSEMBLE,
+                          common.JSON_FILE: (json_buffer, 'bids_events_alpha.json'),
+                          common.EVENTS_FILE: (events_buffer, 'bids_events.tsv'),
+                          common.CHECK_FOR_WARNINGS: 'on'}
+            response = self.app.test.post('/events_submit', content_type='multipart/form-data', data=input_data)
+            self.assertEqual(200, response.status_code, 'Assembly of invalid events files has a response')
+            headers_dict = dict(response.headers)
+            self.assertEqual("warning", headers_dict["Category"],
+                             "Assembly with invalid events files generates a warning")
+            self.assertTrue(response.data,
+                            "The response data for invalid event assembly should have error messages")
+            json_buffer.close()
+
+    def test_events_results_validate_valid(self):
+        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, 'r') as sc:
+            x = sc.read()
+        json_buffer = io.BytesIO(bytes(x, 'utf-8'))
+
+        with open(events_path, 'r') as sc:
+            y = sc.read()
+        events_buffer = io.BytesIO(bytes(y, 'utf-8'))
+
+        with self.app.app_context():
+            input_data = {common.SCHEMA_VERSION: '8.0.0-alpha.2',
+                          common.COMMAND_OPTION: common.COMMAND_VALIDATE,
+                          common.JSON_FILE: (json_buffer, 'bids_events_alpha.json'),
+                          common.EVENTS_FILE: (events_buffer, 'bids_events.tsv'),
+                          common.CHECK_FOR_WARNINGS: 'on'}
+            response = self.app.test.post('/events_submit', content_type='multipart/form-data', data=input_data)
+            self.assertTrue(isinstance(response, Response),
+                            'events_submit validate should return a Response when events valid')
+            self.assertEqual(200, response.status_code, 'Validation of a valid events file has a valid status code')
+            headers_dict = dict(response.headers)
+            self.assertEqual("success", headers_dict["Category"],
+                             "The valid events file should validate successfully")
+            self.assertFalse(response.data, "The validated events file should not return data")
+            json_buffer.close()
+            events_buffer.close()
+
+    def test_events_results_validate_invalid(self):
+        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, 'r') as sc:
+            x = sc.read()
+        json_buffer = io.BytesIO(bytes(x, 'utf-8'))
+
+        with open(events_path, 'r') as sc:
+            y = sc.read()
+        events_buffer = io.BytesIO(bytes(y, 'utf-8'))
+
+        with self.app.app_context():
+            input_data = {common.SCHEMA_VERSION: '7.2.0',
+                          common.COMMAND_OPTION: common.COMMAND_VALIDATE,
+                          common.JSON_FILE: (json_buffer, 'bids_events_alpha.json'),
+                          common.EVENTS_FILE: (events_buffer, 'events_file'),
+                          common.CHECK_FOR_WARNINGS: 'on'}
+            response = self.app.test.post('/events_submit', content_type='multipart/form-data', data=input_data)
+            self.assertTrue(isinstance(response, Response),
+                            'events_submit validate should return a Response when events invalid')
+            self.assertEqual(200, response.status_code, 'Validation of invalid events files has a response')
+            headers_dict = dict(response.headers)
+            self.assertEqual("warning", headers_dict["Category"],
+                             "Validation of invalid events files generates a warning")
+            self.assertTrue(response.data,
+                            "The response data for invalid event validation should have error messages")
+            json_buffer.close()
+            events_buffer.close()
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/webtools/tests/test_routes/test_routes_render.py b/webtools/hedweb/tests/test_routes/test_routes_render.py
similarity index 72%
rename from webtools/tests/test_routes/test_routes_render.py
rename to webtools/hedweb/tests/test_routes/test_routes_render.py
index 5547ca5c8..6817f399a 100644
--- a/webtools/tests/test_routes/test_routes_render.py
+++ b/webtools/hedweb/tests/test_routes/test_routes_render.py
@@ -1,32 +1,8 @@
-import os
-import shutil
 import unittest
-from hedweb.app_factory import AppFactory
-import sys
-sys.path.append('hedtools')
+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 hed import schema as hedschema
-            hedschema.set_cache_directory(app.config['HED_CACHE_FOLDER'])
-            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 52303e08d..6d05b6de7 100644
--- a/webtools/tests/test_routes/test_routes_schema.py
+++ b/webtools/hedweb/tests/test_routes/test_routes_schema.py
@@ -1,33 +1,10 @@
 import io
 import os
-import shutil
 import unittest
-from hedweb.app_factory import AppFactory
-import sys
-sys.path.append('hedtools')
+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 hed import schema as hedschema
-            hedschema.set_cache_directory(app.config['HED_CACHE_FOLDER'])
-            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 61%
rename from webtools/tests/test_routes/test_routes_schema_versions.py
rename to webtools/hedweb/tests/test_routes/test_routes_schema_versions.py
index 4510b2dc3..d1ddad41a 100644
--- a/webtools/tests/test_routes/test_routes_schema_versions.py
+++ b/webtools/hedweb/tests/test_routes/test_routes_schema_versions.py
@@ -1,35 +1,12 @@
 import os
 import io
 import json
-import shutil
 import unittest
 from werkzeug.datastructures import FileStorage
-from hedweb.app_factory import AppFactory
-import sys
-sys.path.append('hedtools')
+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 hed import schema as hedschema
-            hedschema.set_cache_directory(app.config['HED_CACHE_FOLDER'])
-            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 90%
rename from webtools/tests/test_routes/test_routes_sidecar.py
rename to webtools/hedweb/tests/test_routes/test_routes_sidecar.py
index 2fdc5018d..ebdbcfa62 100644
--- a/webtools/tests/test_routes/test_routes_sidecar.py
+++ b/webtools/hedweb/tests/test_routes/test_routes_sidecar.py
@@ -1,36 +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
-import sys
-sys.path.append('hedtools')
-
-
-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 hed import schema as hedschema
-            hedschema.set_cache_directory(app.config['HED_CACHE_FOLDER'])
-            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 89%
rename from webtools/tests/test_routes/test_routes_spreadsheet.py
rename to webtools/hedweb/tests/test_routes/test_routes_spreadsheet.py
index c02a60c6c..d88f59ea3 100644
--- a/webtools/tests/test_routes/test_routes_spreadsheet.py
+++ b/webtools/hedweb/tests/test_routes/test_routes_spreadsheet.py
@@ -1,36 +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
-import sys
-sys.path.append('hedtools')
 
 
-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 hed import schema as hedschema
-            hedschema.set_cache_directory(app.config['HED_CACHE_FOLDER'])
-            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 85%
rename from webtools/tests/test_routes/test_routes_string.py
rename to webtools/hedweb/tests/test_routes/test_routes_string.py
index b9c745fb0..417db0490 100644
--- a/webtools/tests/test_routes/test_routes_string.py
+++ b/webtools/hedweb/tests/test_routes/test_routes_string.py
@@ -1,33 +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
-import sys
-sys.path.append('hedtools')
-
-
-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 hed import schema as hedschema
-            hedschema.set_cache_directory(app.config['HED_CACHE_FOLDER'])
-            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 81%
rename from webtools/tests/test_schema.py
rename to webtools/hedweb/tests/test_schema.py
index 783e6dc76..5aed9040a 100644
--- a/webtools/tests/test_schema.py
+++ b/webtools/hedweb/tests/test_schema.py
@@ -1,34 +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
-import sys
-sys.path.append('hedtools')
-
-
-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 hed import schema as hedschema
-            hedschema.set_cache_directory(app.config['HED_CACHE_FOLDER'])
-            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, {},
@@ -39,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 81%
rename from webtools/tests/test_services.py
rename to webtools/hedweb/tests/test_services.py
index 442c8a717..e61e76c06 100644
--- a/webtools/tests/test_services.py
+++ b/webtools/hedweb/tests/test_services.py
@@ -1,39 +1,16 @@
 import os
 import io
 import json
-import shutil
 import unittest
 from werkzeug.test import create_environ
 from werkzeug.wrappers import Request
-import sys
-sys.path.append('hedtools')
+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 hed import schema as hedschema
-            hedschema.set_cache_directory(app.config['HED_CACHE_FOLDER'])
-            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, {},
@@ -45,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 88%
rename from webtools/tests/test_sidecar.py
rename to webtools/hedweb/tests/test_sidecar.py
index 9b7f3d9f6..3b4cded4b 100644
--- a/webtools/tests/test_sidecar.py
+++ b/webtools/hedweb/tests/test_sidecar.py
@@ -4,35 +4,13 @@
 
 from werkzeug.test import create_environ
 from werkzeug.wrappers import Request
-import sys
-sys.path.append('hedtools')
+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 hed import schema as hedschema
-            hedschema.set_cache_directory(app.config['HED_CACHE_FOLDER'])
-            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, {},
@@ -43,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 89%
rename from webtools/tests/test_spreadsheet.py
rename to webtools/hedweb/tests/test_spreadsheet.py
index 1f3b09839..eebc187ea 100644
--- a/webtools/tests/test_spreadsheet.py
+++ b/webtools/hedweb/tests/test_spreadsheet.py
@@ -1,37 +1,14 @@
 import os
-import shutil
 import unittest
 from werkzeug.test import create_environ
 from werkzeug.wrappers import Request
-import sys
-sys.path.append('hedtools')
+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 hed import schema as hedschema
-            hedschema.set_cache_directory(app.config['HED_CACHE_FOLDER'])
-            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, {},
@@ -42,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 84%
rename from webtools/tests/test_strings.py
rename to webtools/hedweb/tests/test_strings.py
index f34715e4d..e47163194 100644
--- a/webtools/tests/test_strings.py
+++ b/webtools/hedweb/tests/test_strings.py
@@ -1,35 +1,14 @@
 import os
-import shutil
 import unittest
 from werkzeug.test import create_environ
 from werkzeug.wrappers import Request
-import sys
-sys.path.append('hedtools')
+
+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 hed import schema as hedschema
-            hedschema.set_cache_directory(app.config['HED_CACHE_FOLDER'])
-            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 93%
rename from webtools/tests/test_web_utils.py
rename to webtools/hedweb/tests/test_web_utils.py
index 8bc8cdc02..113660585 100644
--- a/webtools/tests/test_web_utils.py
+++ b/webtools/hedweb/tests/test_web_utils.py
@@ -1,34 +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
-import sys
-sys.path.append('hedtools')
 
 
-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 hed import schema as hedschema
-            hedschema.set_cache_directory(app.config['HED_CACHE_FOLDER'])
-            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'])
diff --git a/webtools/tests/test_routes/test_routes_events.py b/webtools/tests/test_routes/test_routes_events.py
deleted file mode 100644
index 7660384b8..000000000
--- a/webtools/tests/test_routes/test_routes_events.py
+++ /dev/null
@@ -1,158 +0,0 @@
-import io
-import os
-import shutil
-import unittest
-from flask import Response
-import sys
-sys.path.append('hedtools')
-from hedweb.app_factory import AppFactory
-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 hed import schema as hedschema
-            hedschema.set_cache_directory(app.config['HED_CACHE_FOLDER'])
-            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)
-
-    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')
-        self.assertTrue(isinstance(response, Response),
-                        'events_results validate should return a response object when empty events')
-        header_dict = dict(response.headers)
-        self.assertEqual("error", header_dict["Category"], "The header msg_category when no events is error ")
-        self.assertFalse(response.data, "The response data for empty events request is empty")
-
-    # def test_events_results_assemble_valid(self):
-    #     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, 'r') as sc:
-    #         x = sc.read()
-    #     json_buffer = io.BytesIO(bytes(x, 'utf-8'))
-    #
-    #     with open(events_path, 'r') as sc:
-    #         y = sc.read()
-    #     events_buffer = io.BytesIO(bytes(y, 'utf-8'))
-    #
-    #     with self.app.app_context():
-    #         input_data = {common.SCHEMA_VERSION: '8.0.0-alpha.2',
-    #                       common.COMMAND_OPTION: common.COMMAND_ASSEMBLE,
-    #                       'json_file': (json_buffer, 'bids_events_alpha.json'),
-    #                       'events_file': (events_buffer, 'bids_events.tsv'),
-    #                       'defs_expand': 'on',
-    #                       common.CHECK_FOR_WARNINGS: 'on'}
-    #         response = self.app.test.post('/events_submit', content_type='multipart/form-data', data=input_data)
-    #         self.assertEqual(200, response.status_code, 'Assembly of a valid events file has a response')
-    #         headers_dict = dict(response.headers)
-    #         self.assertEqual("success", headers_dict["Category"],
-    #                          "The valid events file should assemble successfully")
-    #         self.assertTrue(response.data, "The assembled events file should not be empty")
-    #         json_buffer.close()
-    #         events_buffer.close()
-    #
-    # def test_events_results_assemble_invalid(self):
-    #     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, 'r') as sc:
-    #         x = sc.read()
-    #     json_buffer = io.BytesIO(bytes(x, 'utf-8'))
-    #
-    #     with open(events_path, 'r') as sc:
-    #         y = sc.read()
-    #     events_buffer = io.BytesIO(bytes(y, 'utf-8'))
-    #
-    #     with self.app.app_context():
-    #         input_data = {common.SCHEMA_VERSION: '7.2.0',
-    #                       common.COMMAND_OPTION: common.COMMAND_ASSEMBLE,
-    #                       common.JSON_FILE: (json_buffer, 'bids_events_alpha.json'),
-    #                       common.EVENTS_FILE: (events_buffer, 'bids_events.tsv'),
-    #                       common.CHECK_FOR_WARNINGS: 'on'}
-    #         response = self.app.test.post('/events_submit', content_type='multipart/form-data', data=input_data)
-    #         self.assertEqual(200, response.status_code, 'Assembly of invalid events files has a response')
-    #         headers_dict = dict(response.headers)
-    #         self.assertEqual("warning", headers_dict["Category"],
-    #                          "Assembly with invalid events files generates a warning")
-    #         self.assertTrue(response.data,
-    #                         "The response data for invalid event assembly should have error messages")
-    #         json_buffer.close()
-    #
-    # def test_events_results_validate_valid(self):
-    #     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, 'r') as sc:
-    #         x = sc.read()
-    #     json_buffer = io.BytesIO(bytes(x, 'utf-8'))
-    #
-    #     with open(events_path, 'r') as sc:
-    #         y = sc.read()
-    #     events_buffer = io.BytesIO(bytes(y, 'utf-8'))
-    #
-    #     with self.app.app_context():
-    #         input_data = {common.SCHEMA_VERSION: '8.0.0-alpha.2',
-    #                       common.COMMAND_OPTION: common.COMMAND_VALIDATE,
-    #                       common.JSON_FILE: (json_buffer, 'bids_events_alpha.json'),
-    #                       common.EVENTS_FILE: (events_buffer, 'bids_events.tsv'),
-    #                       common.CHECK_FOR_WARNINGS: 'on'}
-    #         response = self.app.test.post('/events_submit', content_type='multipart/form-data', data=input_data)
-    #         self.assertTrue(isinstance(response, Response),
-    #                         'events_submit validate should return a Response when events valid')
-    #         self.assertEqual(200, response.status_code, 'Validation of a valid events file has a valid status code')
-    #         headers_dict = dict(response.headers)
-    #         self.assertEqual("success", headers_dict["Category"],
-    #                          "The valid events file should validate successfully")
-    #         self.assertFalse(response.data, "The validated events file should not return data")
-    #         json_buffer.close()
-    #         events_buffer.close()
-    #
-    # def test_events_results_validate_invalid(self):
-    #     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, 'r') as sc:
-    #         x = sc.read()
-    #     json_buffer = io.BytesIO(bytes(x, 'utf-8'))
-    #
-    #     with open(events_path, 'r') as sc:
-    #         y = sc.read()
-    #     events_buffer = io.BytesIO(bytes(y, 'utf-8'))
-    #
-    #     with self.app.app_context():
-    #         input_data = {common.SCHEMA_VERSION: '7.2.0',
-    #                       common.COMMAND_OPTION: common.COMMAND_VALIDATE,
-    #                       common.JSON_FILE: (json_buffer, 'bids_events_alpha.json'),
-    #                       common.EVENTS_FILE: (events_buffer, 'events_file'),
-    #                       common.CHECK_FOR_WARNINGS: 'on'}
-    #         response = self.app.test.post('/events_submit', content_type='multipart/form-data', data=input_data)
-    #         self.assertTrue(isinstance(response, Response),
-    #                         'events_submit validate should return a Response when events invalid')
-    #         self.assertEqual(200, response.status_code, 'Validation of invalid events files has a response')
-    #         headers_dict = dict(response.headers)
-    #         self.assertEqual("warning", headers_dict["Category"],
-    #                          "Validation of invalid events files generates a warning")
-    #         self.assertTrue(response.data,
-    #                         "The response data for invalid event validation should have error messages")
-    #         json_buffer.close()
-    #         events_buffer.close()
-
-
-if __name__ == '__main__':
-    unittest.main()