Repository: zimeon/ocfl-py

Issue:
Generate E064 for fixture E060_E064_root_inventory_digest_mismatch
See https://github.com/OCFL/fixtures/pull/75

Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/39028252/badge)](https://coveralls.io/builds/39028252)

Coverage increased (+0.1%) to 94.63% when pulling **b1d1acda8f7797dea5240edae9cba9bb8e3ed2f1 on issue-51** into **b955d49be56e58ab0a012617f929ff6ec5eda100 on develop**.


Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 281c5a4..bc78768 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -4,6 +4,7 @@
 
   * Validation:
     * Correct handling of missing files mentioned in manifest or fixity
+    * Add test bad inventory examples in `extra_fixtures/bad-objects`
 
 ## 2021-04-18 v1.2.1
 
diff --git a/extra_fixtures/bad-objects/E046_versions_keys_not_zero_padded/v001/inventory.json b/extra_fixtures/bad-inventories/inventory_E042a_bad_content_paths.json
similarity index 93%
rename from extra_fixtures/bad-objects/E046_versions_keys_not_zero_padded/v001/inventory.json
rename to extra_fixtures/bad-inventories/inventory_E042a_bad_content_paths.json
index c51115b..bcadac6 100644
--- a/extra_fixtures/bad-objects/E046_versions_keys_not_zero_padded/v001/inventory.json
+++ b/extra_fixtures/bad-inventories/inventory_E042a_bad_content_paths.json
@@ -4,7 +4,7 @@
   "id": "uri:something451",
   "manifest": {
     "43a43fe8a8a082d3b5343dfaf2fd0c8b8e370675b1f376e92e9994612c33ea255b11298269d72f797399ebb94edeefe53df243643676548f584fb8603ca53a0f": [
-      "v001/content/a_file.txt"
+      "v1/a/a_file.txt", "v1/no_content_dir.txt"
     ]
   },
   "type": "https://ocfl.io/1.0/spec/#inventory",
diff --git a/extra_fixtures/bad-inventories/inventory_E042b_unknown_version.json b/extra_fixtures/bad-inventories/inventory_E042b_unknown_version.json
new file mode 100644
index 0000000..fb4716b
--- /dev/null
+++ b/extra_fixtures/bad-inventories/inventory_E042b_unknown_version.json
@@ -0,0 +1,26 @@
+{
+  "digestAlgorithm": "sha512",
+  "head": "v1",
+  "id": "uri:something451",
+  "manifest": {
+    "43a43fe8a8a082d3b5343dfaf2fd0c8b8e370675b1f376e92e9994612c33ea255b11298269d72f797399ebb94edeefe53df243643676548f584fb8603ca53a0f": [
+      "v12345/content/a_file.txt", "v0/content/a_file.txt"
+    ]
+  },
+  "type": "https://ocfl.io/1.0/spec/#inventory",
+  "versions": {
+    "v1": {
+      "created": "2019-01-01T01:01:01Z",
+      "state": {
+        "43a43fe8a8a082d3b5343dfaf2fd0c8b8e370675b1f376e92e9994612c33ea255b11298269d72f797399ebb94edeefe53df243643676548f584fb8603ca53a0f": [
+          "a_file.txt"
+        ]
+      },
+      "message": "Intial import",
+      "user": {
+        "name": "A person",
+        "address": "https://orcid.org/0000-0000-0000-0000"
+      }
+    }
+  }
+}
diff --git a/extra_fixtures/bad-objects/E046_versions_keys_not_zero_padded/inventory.json b/extra_fixtures/bad-inventories/inventory_E042b_zero_padding_mismatch.json
similarity index 100%
rename from extra_fixtures/bad-objects/E046_versions_keys_not_zero_padded/inventory.json
rename to extra_fixtures/bad-inventories/inventory_E042b_zero_padding_mismatch.json
diff --git a/extra_fixtures/bad-objects/E042_bad_manifest_content_path/v1/inventory.json b/extra_fixtures/bad-objects/E042_bad_manifest_content_path/v1/inventory.json
index c4c4da1..765a7dc 100644
--- a/extra_fixtures/bad-objects/E042_bad_manifest_content_path/v1/inventory.json
+++ b/extra_fixtures/bad-objects/E042_bad_manifest_content_path/v1/inventory.json
@@ -4,7 +4,7 @@
   "id": "ark:123/abc",
   "manifest": {
     "43a43fe8a8a082d3b5343dfaf2fd0c8b8e370675b1f376e92e9994612c33ea255b11298269d72f797399ebb94edeefe53df243643676548f584fb8603ca53a0f": [
-      "v1/content/a_file.txt", "v1/content/./bad_content_path"
+      "v1/content/a_file.txt", "v1/bad_content_path"
     ]
   },
   "type": "https://ocfl.io/1.0/spec/#inventory",
diff --git a/extra_fixtures/bad-objects/E042_bad_manifest_content_path/v1/inventory.json.sha512 b/extra_fixtures/bad-objects/E042_bad_manifest_content_path/v1/inventory.json.sha512
index 9761ee6..a81b074 100644
--- a/extra_fixtures/bad-objects/E042_bad_manifest_content_path/v1/inventory.json.sha512
+++ b/extra_fixtures/bad-objects/E042_bad_manifest_content_path/v1/inventory.json.sha512
@@ -1,1 +1,1 @@
-8a9a1612265d7077a1257282d627dd604afff68945a6a5b51fcb6d2f287e0b1b6579d647a59925f9cb4d79af70ec73bc8f4749362482ae80db274eea2419ea6c  inventory.json
+b960491e781731c954d48a18fc37a7e64e8d31fde77a1af00f1e57d94faa55e06074e8fe481ffc46a699e1a58d3e215ac5cf4fa1331f76d9c0daf60ce367b1a6 inventory.json
diff --git a/extra_fixtures/bad-objects/E046_versions_keys_not_zero_padded/0=ocfl_object_1.0 b/extra_fixtures/bad-objects/E046_versions_keys_not_zero_padded/0=ocfl_object_1.0
deleted file mode 100644
index 4d1d62c..0000000
--- a/extra_fixtures/bad-objects/E046_versions_keys_not_zero_padded/0=ocfl_object_1.0
+++ /dev/null
@@ -1,1 +0,0 @@
-ocfl_object_1.0
diff --git a/extra_fixtures/bad-objects/E046_versions_keys_not_zero_padded/inventory.json.sha512 b/extra_fixtures/bad-objects/E046_versions_keys_not_zero_padded/inventory.json.sha512
deleted file mode 100644
index 11f2322..0000000
--- a/extra_fixtures/bad-objects/E046_versions_keys_not_zero_padded/inventory.json.sha512
+++ /dev/null
@@ -1,1 +0,0 @@
-a662f8139848c99b4045c7955e438a1ecd0c6586c64e2dbfdc028c336ab9dd24695981a8c6e55031a2f55e0b5a1ce38a2e4f6ee4dd3f6462a071c49b8137eda3 inventory.json
diff --git a/extra_fixtures/bad-objects/E046_versions_keys_not_zero_padded/v001/content/a_file.txt b/extra_fixtures/bad-objects/E046_versions_keys_not_zero_padded/v001/content/a_file.txt
deleted file mode 100644
index 2baefe4..0000000
--- a/extra_fixtures/bad-objects/E046_versions_keys_not_zero_padded/v001/content/a_file.txt
+++ /dev/null
@@ -1,1 +0,0 @@
-Hello! I am a file.
diff --git a/extra_fixtures/bad-objects/E046_versions_keys_not_zero_padded/v001/inventory.json.sha512 b/extra_fixtures/bad-objects/E046_versions_keys_not_zero_padded/v001/inventory.json.sha512
deleted file mode 100644
index 11f2322..0000000
--- a/extra_fixtures/bad-objects/E046_versions_keys_not_zero_padded/v001/inventory.json.sha512
+++ /dev/null
@@ -1,1 +0,0 @@
-a662f8139848c99b4045c7955e438a1ecd0c6586c64e2dbfdc028c336ab9dd24695981a8c6e55031a2f55e0b5a1ce38a2e4f6ee4dd3f6462a071c49b8137eda3 inventory.json
diff --git a/ocfl/data/validation-errors.json b/ocfl/data/validation-errors.json
index 1ef793b..56eed29 100644
--- a/ocfl/data/validation-errors.json
+++ b/ocfl/data/validation-errors.json
@@ -194,12 +194,18 @@
        "en": "OCFL Object %s inventory manifest block is not a JSON object"
     }
   },
-  "E042": {
+  "E042a": {
     "params": ["where", "path"],
     "description": {
        "en": "OCFL Object %s inventory manifest includes invalid content path %s"
     }
   },
+  "E042b": {
+    "params": ["where", "path"],
+    "description": {
+       "en": "OCFL Object %s inventory manifest includes content path %s with invalid version directory"
+    }
+  },
   "E044": {
     "params": ["where"],
     "description": {
@@ -209,7 +215,7 @@
   "E046": {
     "params": ["version_dir"],
     "description": {
-       "en": "OCFL Object root inventory describes versions %s but no corresponding version directory is present"
+       "en": "OCFL Object root inventory describes version %s but no corresponding version directory is present"
     }
   },
   "E048": {
diff --git a/ocfl/inventory_validator.py b/ocfl/inventory_validator.py
index 32bbbc4..4a918b5 100644
--- a/ocfl/inventory_validator.py
+++ b/ocfl/inventory_validator.py
@@ -85,10 +85,12 @@ class InventoryValidator():
                 self.error("E018")
             else:
                 self.content_directory = cd
+        manifest_files_correct_format = None
         if 'manifest' not in inventory:
             self.error("E041a")
         else:
-            (self.manifest_files, self.unnormalized_digests) = self.validate_manifest(inventory['manifest'])
+            (self.manifest_files, manifest_files_correct_format, self.unnormalized_digests) = self.validate_manifest(inventory['manifest'])
+        digests_used = []
         if 'versions' not in inventory:
             self.error("E041b")
         else:
@@ -104,8 +106,11 @@ class InventoryValidator():
             # Abort tests is we don't have a valid version sequence, otherwise
             # there will likely be spurious subsequent error reports
             return
-        if 'manifest' in inventory and 'versions' in inventory:
-            self.check_digests_present_and_used(self.manifest_files, digests_used)
+        if len(self.all_versions) > 0:
+            if manifest_files_correct_format is not None:
+                self.check_content_paths_map_to_versions(manifest_files_correct_format, self.all_versions)
+            if self.manifest_files is not None:
+                self.check_digests_present_and_used(self.manifest_files, digests_used)
         if 'fixity' in inventory:
             self.validate_fixity(inventory['fixity'], self.manifest_files)
 
@@ -113,12 +118,17 @@ class InventoryValidator():
         """Validate manifest block in inventory.
 
         Returns:
-          * manifest_files, a mapping from file to digest for each file in
+          * manifest_files - a mapping from file to digest for each file in
               the manifest
+          * manifest_files_correct_format - a simple list of the manifest file
+              path that passed initial checks. They need to be checked for valid
+              version directories later, when we know what version directories
+              are valid
           * unnormalized_digests - a set of the original digests in unnormalized
               form that MUST match exactly the values used in state blocks
         """
         manifest_files = {}
+        manifest_files_correct_format = []
         unnormalized_digests = set()
         manifest_digests = set()
         if not isinstance(manifest, dict):
@@ -142,13 +152,13 @@ class InventoryValidator():
                         manifest_digests.add(norm_digest)
                     for file in manifest[digest]:
                         manifest_files[file] = norm_digest
-                        self.check_content_path(file, content_paths, content_directories)
+                        if self.check_content_path(file, content_paths, content_directories):
+                            manifest_files_correct_format.append(file)
             # Check for conflicting content paths
             for path in content_directories:
                 if path in content_paths:
                     self.error("E101", path=path)
-
-        return (manifest_files, unnormalized_digests)
+        return manifest_files, manifest_files_correct_format, unnormalized_digests
 
     def validate_fixity(self, fixity, manifest_files):
         """Validate fixity block in inventory.
@@ -336,6 +346,18 @@ class InventoryValidator():
                     self.error("E095", version=version, path=path)
         return digests
 
+    def check_content_paths_map_to_versions(self, manifest_files, all_versions):
+        """Check that every content path starts with a valid version.
+
+        The content directory component has already been checked in
+        check_content_path(). We have already tested all paths enough
+        to know that they can be split into at least 2 components.
+        """
+        for path in manifest_files:
+            version_dir, dummy_rest = path.split('/', 1)
+            if version_dir not in all_versions:
+                self.error('E042b', path=path)
+
     def check_digests_present_and_used(self, manifest_files, digests_used):
         """Check all digests in manifest that are needed are present and used."""
         in_manifest = set(manifest_files.values())
@@ -379,23 +401,26 @@ class InventoryValidator():
     def check_content_path(self, path, content_paths, content_directories):
         """Check logical path and accumulate paths/directories for E101 check.
 
-        Only adds good paths to the accumulated paths/directories.
+        Returns True if valid, else False. Only adds good paths to the
+        accumulated paths/directories. We don't yet know the set of valid
+        version directories so the check here is just for 'v' + digits.
         """
         if path.startswith('/') or path.endswith('/'):
             self.error("E100", path=path)
-        else:
-            m = re.match(r'''^(v\d+/''' + self.content_directory + r''')/(.*)''', path)
-            if m:
-                elements = m.group(2).split('/')
-                for element in elements:
-                    if element in ('', '.', '..'):
-                        self.error("E099", path=path)
-                        return
-                # Accumulate paths and directories
-                content_paths.add(path)
-                content_directories.add('/'.join([m.group(1)] + elements[0:-1]))
-            else:
-                self.error("E042", path=path)
+            return False
+        m = re.match(r'''^(v\d+/''' + self.content_directory + r''')/(.+)''', path)
+        if not m:
+            self.error("E042a", path=path)
+            return False
+        elements = m.group(2).split('/')
+        for element in elements:
+            if element in ('', '.', '..'):
+                self.error("E099", path=path)
+                return False
+        # Accumulate paths and directories
+        content_paths.add(path)
+        content_directories.add('/'.join([m.group(1)] + elements[0:-1]))
+        return True
 
     def validate_as_prior_version(self, prior):
         """Check that prior is a valid InventoryValidator for a prior version of the current inventory object.
diff --git a/ocfl/validator.py b/ocfl/validator.py
index f6ac7f6..4d5ecf6 100644
--- a/ocfl/validator.py
+++ b/ocfl/validator.py
@@ -92,20 +92,19 @@ class Validator():
             return False
         try:
             inventory, inv_validator = self.validate_inventory(inv_file)
+            inventory_is_valid = self.log.num_errors == 0
             self.root_inv_validator = inv_validator
             all_versions = inv_validator.all_versions
             self.content_directory = inv_validator.content_directory
             self.digest_algorithm = inv_validator.digest_algorithm
             self.validate_inventory_digest(inv_file, self.digest_algorithm)
-            if self.log.num_errors > 0:
-                # Don't look at storage if inventory fails validation
-                return False
             # Object root
             self.validate_object_root(all_versions)
             # Version inventory files
             prior_manifest_digests = self.validate_version_inventories(all_versions)
-            # Object content
-            self.validate_content(inventory, all_versions, prior_manifest_digests)
+            if inventory_is_valid:
+                # Object content
+                self.validate_content(inventory, all_versions, prior_manifest_digests)
         except ValidatorAbortException:
             pass
         return self.log.num_errors == 0


Test patch:
diff --git a/tests/test_inventory_validator.py b/tests/test_inventory_validator.py
index 1551ca1..5db1512 100644
--- a/tests/test_inventory_validator.py
+++ b/tests/test_inventory_validator.py
@@ -1,4 +1,5 @@
 """Identity dispositor tests."""
+import json
 import unittest
 from ocfl.inventory_validator import InventoryValidator
 
@@ -78,13 +79,13 @@ class TestAll(unittest.TestCase):
         """Test validate_manifest method."""
         log = TLogger()
         iv = InventoryValidator(log=log)
-        self.assertEqual(iv.validate_manifest("not a manifest"), ({}, set()))
+        self.assertEqual(iv.validate_manifest("not a manifest"), ({}, [], set()))
         self.assertIn('E041c', log.errors)
         log.clear()
-        self.assertEqual(iv.validate_manifest({"xxx": []}), ({}, set()))
+        self.assertEqual(iv.validate_manifest({"xxx": []}), ({}, [], set()))
         self.assertIn('E025a', log.errors)
         log.clear()
-        self.assertEqual(iv.validate_manifest({"067eca3f5b024afa00aeac03a3c42dc0042bf43cba56104037abea8b365c0cf672f0e0c14c91b82bbce6b1464e231ac285d630a82cd4d4a7b194bea04d4b2eb7": "not an array"}), ({}, set()))
+        self.assertEqual(iv.validate_manifest({"067eca3f5b024afa00aeac03a3c42dc0042bf43cba56104037abea8b365c0cf672f0e0c14c91b82bbce6b1464e231ac285d630a82cd4d4a7b194bea04d4b2eb7": "not an array"}), ({}, [], set()))
         self.assertIn('E092', log.errors)
         log.clear()
         iv.lax_digests = True
@@ -92,7 +93,7 @@ class TestAll(unittest.TestCase):
             {
                 "067eca3f5b024afa00aeac03a3c42dc0042bf43cba56104037abea8b365c0cf672f0e0c14c91b82bbce6b1464e231ac285d630a82cd4d4a7b194bea04d4b2eb7": [],
                 "067ECA3f5b024afa00aeac03a3c42dc0042bf43cba56104037abea8b365c0cf672f0e0c14c91b82bbce6b1464e231ac285d630a82cd4d4a7b194bea04d4b2eb7": []
-            }), ({}, set([
+            }), ({}, [], set([
                 "067ECA3f5b024afa00aeac03a3c42dc0042bf43cba56104037abea8b365c0cf672f0e0c14c91b82bbce6b1464e231ac285d630a82cd4d4a7b194bea04d4b2eb7",
                 "067eca3f5b024afa00aeac03a3c42dc0042bf43cba56104037abea8b365c0cf672f0e0c14c91b82bbce6b1464e231ac285d630a82cd4d4a7b194bea04d4b2eb7"
             ])))
@@ -327,57 +328,57 @@ class TestAll(unittest.TestCase):
         iv = InventoryValidator(log=log)
         cp = set()
         cd = set()
-        iv.check_content_path('v1/content/anything', cp, cd)
+        self.assertTrue(iv.check_content_path('v1/content/anything', cp, cd))
         self.assertEqual(log.errors, [])
         self.assertEqual(cp, set(['v1/content/anything']))
         self.assertEqual(cd, set(['v1/content']))
         log.clear()
         iv.content_directory = 'xyz'
-        iv.check_content_path('v1/content/anything', cp, cd)
-        self.assertEqual(log.errors, ['E042'])
+        self.assertFalse(iv.check_content_path('v1/content/anything', cp, cd))
+        self.assertEqual(log.errors, ['E042a'])
         log.clear()
-        iv.check_content_path('v1/xyz/anything', cp, cd)
+        self.assertTrue(iv.check_content_path('v1/xyz/anything', cp, cd))
         self.assertEqual(log.errors, [])
         # Error cases
         log.clear()
-        iv.check_content_path('1/xyz/1', cp, cd)
-        self.assertEqual(log.errors, ['E042'])
+        self.assertFalse(iv.check_content_path('1/xyz/1', cp, cd))
+        self.assertEqual(log.errors, ['E042a'])
         log.clear()
-        iv.check_content_path('vv1/xyz/1', cp, cd)
-        self.assertEqual(log.errors, ['E042'])
+        self.assertFalse(iv.check_content_path('vv1/xyz/1', cp, cd))
+        self.assertEqual(log.errors, ['E042a'])
         log.clear()
-        iv.check_content_path('v1/x/1', cp, cd)
-        self.assertEqual(log.errors, ['E042'])
+        self.assertFalse(iv.check_content_path('v1/x/1', cp, cd))
+        self.assertEqual(log.errors, ['E042a'])
         log.clear()
-        iv.check_content_path('v1/xyz/1/', cp, cd)
+        self.assertFalse(iv.check_content_path('v1/xyz/1/', cp, cd))
         self.assertEqual(log.errors, ['E100'])
         log.clear()
-        iv.check_content_path('/v1/xyz/1', cp, cd)
+        self.assertFalse(iv.check_content_path('/v1/xyz/1', cp, cd))
         self.assertEqual(log.errors, ['E100'])
         log.clear()
-        iv.check_content_path('v1/xyz/', cp, cd)
+        self.assertFalse(iv.check_content_path('v1/xyz/', cp, cd))
         self.assertEqual(log.errors, ['E100'])
         log.clear()
-        iv.check_content_path('v1/xyz//1', cp, cd)
+        self.assertFalse(iv.check_content_path('v1/xyz//1', cp, cd))
         self.assertEqual(log.errors, ['E099'])
         log.clear()
-        iv.check_content_path('v1/xyz/abc//d', cp, cd)
+        self.assertFalse(iv.check_content_path('v1/xyz/abc//d', cp, cd))
         self.assertEqual(log.errors, ['E099'])
         log.clear()
-        iv.check_content_path('v1/xyz/abc/./d', cp, cd)
+        self.assertFalse(iv.check_content_path('v1/xyz/abc/./d', cp, cd))
         self.assertEqual(log.errors, ['E099'])
         log.clear()
-        iv.check_content_path('v1/xyz/abc/../d', cp, cd)
+        self.assertFalse(iv.check_content_path('v1/xyz/abc/../d', cp, cd))
         self.assertEqual(log.errors, ['E099'])
         log.clear()
-        iv.check_content_path('v1/xyz/.', cp, cd)
+        self.assertFalse(iv.check_content_path('v1/xyz/.', cp, cd))
         self.assertEqual(log.errors, ['E099'])
         # Good cases
         log.clear()
-        iv.check_content_path('v1/xyz/.secret/d', cp, cd)
+        self.assertTrue(iv.check_content_path('v1/xyz/.secret/d', cp, cd))
         self.assertEqual(log.errors, [])
         log.clear()
-        iv.check_content_path('v1/xyz/.a', cp, cd)
+        self.assertTrue(iv.check_content_path('v1/xyz/.a', cp, cd))
         self.assertEqual(log.errors, [])
         # Check good paths accumulated
         self.assertEqual(cp, set(('v1/xyz/anything', 'v1/xyz/.a', 'v1/content/anything', 'v1/xyz/.secret/d')))
@@ -404,3 +405,16 @@ class TestAll(unittest.TestCase):
         # And check only good paths recorded
         self.assertEqual(lp, set(('almost anything goes', 'including/this/long/path')))
         self.assertEqual(ld, set(('', 'including/this/long')))
+
+    def test_bad_inventory_files(self):
+        """Test bad inventory files."""
+        for bad, codes in {'inventory_E042a_bad_content_paths': ['E042a'],
+                           'inventory_E042b_unknown_version': ['E042b'],
+                           'inventory_E042b_zero_padding_mismatch': ['E042b']}.items():
+            filepath = 'extra_fixtures/bad-inventories/' + bad + '.json'
+            with open(filepath, 'r') as fh:
+                inventory = json.load(fh)
+            log = TLogger()
+            iv = InventoryValidator(log=log)
+            iv.validate(inventory)
+            self.assertEqual(set(codes), set(log.errors), msg="for object at " + filepath)
diff --git a/tests/test_validator.py b/tests/test_validator.py
index f211636..c1a852c 100644
--- a/tests/test_validator.py
+++ b/tests/test_validator.py
@@ -29,7 +29,7 @@ class TestAll(unittest.TestCase):
                            'E003_no_decl': ['E003a'],
                            'E008_E036_no_versions_no_head': ['E008', 'E036d'],
                            'E009_version_two_only': ['E009'],
-                           'E015_content_not_in_content_dir': ['E042'],  # FIXME - What should test case and error be?
+                           'E015_content_not_in_content_dir': ['E042a'],  # FIXME - What should test case and error be?
                            'E023_extra_file': ['E023'],
                            'E023_missing_file': ['E092b'],
                            'E024_empty_dir_in_content': ['E024'],
@@ -38,10 +38,9 @@ class TestAll(unittest.TestCase):
                            'E040_wrong_head_doesnt_exist': ['E040'],
                            'E040_wrong_head_format': ['E040'],
                            'E041_no_manifest': ['E041a'],
-                           "E041_manifest_not_object": ['E041c'],
-                           'E042_bad_manifest_content_path': ['E042'],
+                           'E041_manifest_not_object': ['E041c'],
+                           'E042_bad_manifest_content_path': ['E042a'],
                            'E046_missing_version_dir': ['E046'],
-                           'E046_versions_keys_not_zero_padded': ['E046'],
                            'E049_E050_E054_bad_version_block_values': ['E049d', 'E050b', 'E050c', 'E054a', 'E094'],
                            'E049_created_no_timezone': ['E049a'],
                            'E049_created_not_to_seconds': ['E049b'],