dkist-header-validator 5.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,494 @@
1
+ """
2
+ Test for the base validator
3
+ """
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+ import pytest
8
+ import voluptuous as vol
9
+ import yaml
10
+ from astropy.io import fits
11
+ from deepdiff import DeepDiff
12
+
13
+ from dkist_header_validator.base_validator import SpecSchema
14
+ from dkist_header_validator.base_validator import SpecValidator
15
+ from dkist_header_validator.exceptions import SpecSchemaDefinitionException
16
+ from dkist_header_validator.exceptions import SpecValidationException
17
+ from dkist_header_validator.exceptions import ValidationException
18
+
19
+
20
+ def test_parse_spec_schema_definitions():
21
+ """
22
+ Given: A valid schema definition, can be in one of the following formats:
23
+ dict[str, dict[str, Any]]
24
+ When: Validating the schema
25
+ Then: SpecSchema(definition) returns the schema definition in dict form
26
+ """
27
+ example_schema = {
28
+ "fits": {
29
+ "key_004": {
30
+ "expand": True,
31
+ "required": False,
32
+ "comment": "comment 4",
33
+ "type": "bool",
34
+ }
35
+ }
36
+ }
37
+ SpecSchema._parse_spec_schema_definitions(example_schema)
38
+
39
+
40
+ @pytest.fixture(scope="module")
41
+ def invalid_definition_format_params(tmpdir_factory):
42
+ """
43
+ Create a dict of invalid schema definition formats to be used in
44
+ failing format tests below.
45
+ """
46
+ invalid_dict = {"specxxx": {"this is not a valid yaml file"}}
47
+
48
+ temp_dir = tmpdir_factory.mktemp("invalid_definitions_temp")
49
+ file_name = temp_dir.join("invalid_yaml_file.yml")
50
+ file_name.write(yaml.safe_dump_all(invalid_dict))
51
+
52
+ test_params = {
53
+ "invalid Dict": dict(),
54
+ "invalid schema type1": "non valid schema type",
55
+ }
56
+
57
+ yield test_params
58
+
59
+
60
+ @pytest.fixture(
61
+ scope="function",
62
+ params=[
63
+ "invalid Dict",
64
+ "invalid schema type1",
65
+ ],
66
+ )
67
+ def invalid_definition_format(request, invalid_definition_format_params):
68
+ yield invalid_definition_format_params[request.param]
69
+
70
+
71
+ def test_parse_spec_schema_definitions_fail(invalid_definition_format):
72
+ """
73
+ Given: A schema definition that is empty or not in one of the valid formats:
74
+ List[dict]
75
+ When: Validating the schema
76
+ Then: raises a SpecSchemaDefinitionException
77
+ """
78
+ with pytest.raises(SpecSchemaDefinitionException):
79
+ SpecSchema._parse_spec_schema_definitions(invalid_definition_format)
80
+
81
+
82
+ @pytest.fixture(scope="module")
83
+ def valid_schema_definition_params():
84
+ """
85
+ Create a dict of schema definition parameters that spans the valid
86
+ parameter space. To be used in successful schema definition tests below.
87
+ """
88
+
89
+ test_params = {
90
+ "required int": {"fits": {"keyword_name": {"required": True, "type": "int"}}},
91
+ "optional int": {"fits": {"keyword_name": {"required": False, "type": "int"}}},
92
+ "required float": {"fits": {"keyword_name": {"required": True, "type": "float"}}},
93
+ "optional float": {"fits": {"keyword_name": {"required": False, "type": "float"}}},
94
+ "required str": {"fits": {"keyword_name": {"required": True, "type": "str"}}},
95
+ "optional str": {"fits": {"keyword_name": {"required": False, "type": "str"}}},
96
+ "required bool": {"fits": {"keyword_name": {"required": True, "type": "bool"}}},
97
+ "optional bool": {"fits": {"keyword_name": {"required": False, "type": "bool"}}},
98
+ "optional key-value pair 1": {
99
+ "fits": {"keyword_name": {"required": True, "type": "int", "optional1": "value1"}}
100
+ },
101
+ "optional key-value pair 2": {
102
+ "fits": {"keyword_name": {"required": False, "type": "int", "optional2": "value2"}}
103
+ },
104
+ "optional key-value pair 3": {
105
+ "fits": {"keyword_name": {"required": True, "type": "float", "optional3": "value3"}}
106
+ },
107
+ "optional key-value pair 4": {
108
+ "fits": {"keyword_name": {"required": False, "type": "float", "optional4": "value4"}}
109
+ },
110
+ "optional key-value pair 5": {
111
+ "fits": {"keyword_name": {"required": True, "type": "str", "optional5": "value5"}}
112
+ },
113
+ "optional key-value pair 6": {
114
+ "fits": {"keyword_name": {"required": False, "type": "str", "optional6": "value6"}}
115
+ },
116
+ "optional key-value pair 7": {
117
+ "fits": {"keyword_name": {"required": True, "type": "bool", "optional7": "value7"}}
118
+ },
119
+ "optional key-value pair 8": {
120
+ "fits": {"keyword_name": {"required": False, "type": "bool", "optional8": "value8"}}
121
+ },
122
+ }
123
+
124
+ return test_params
125
+
126
+
127
+ @pytest.fixture(
128
+ scope="function",
129
+ params=[
130
+ "required int",
131
+ "optional int",
132
+ "required float",
133
+ "optional float",
134
+ "required str",
135
+ "optional str",
136
+ "required bool",
137
+ "optional bool",
138
+ "optional key-value pair 1",
139
+ "optional key-value pair 2",
140
+ "optional key-value pair 3",
141
+ "optional key-value pair 4",
142
+ "optional key-value pair 5",
143
+ "optional key-value pair 6",
144
+ "optional key-value pair 7",
145
+ "optional key-value pair 8",
146
+ ],
147
+ )
148
+ def valid_schema_definition(request, valid_schema_definition_params):
149
+
150
+ yield valid_schema_definition_params[request.param]
151
+
152
+
153
+ def test_validate_spec_schema_definitions(valid_schema_definition):
154
+ """
155
+ Given: Valid schema definitions that span the parameter space of the definition schema
156
+ When: Validating the schema
157
+ Then: SpecSchema(definition) successfully validates the definition against
158
+ the definition schema. Success means no exception is raised.
159
+ """
160
+ SpecSchema(valid_schema_definition)
161
+
162
+
163
+ @pytest.fixture(scope="module")
164
+ def invalid_schema_definition_params():
165
+ """
166
+ Create a dict of invalid schema definition parameters to be used in
167
+ failing tests below.
168
+ """
169
+
170
+ test_params = {
171
+ "missing required": {"specxxx": {"keyword": {"type": "int"}}},
172
+ "missing type": {"specxxx": {"keyword": {"required": True}}},
173
+ "missing both, no optional": {"specxxx": {"keyword": {}}},
174
+ "missing both, with optional": {"specxxx": {"keyword": {"optional": "value"}}},
175
+ }
176
+
177
+ yield test_params
178
+
179
+
180
+ @pytest.fixture(
181
+ scope="function",
182
+ params=[
183
+ "missing required",
184
+ "missing type",
185
+ "missing both, no optional",
186
+ "missing both, with optional",
187
+ ],
188
+ )
189
+ def invalid_schema_definition(request, invalid_schema_definition_params):
190
+
191
+ yield invalid_schema_definition_params[request.param]
192
+
193
+
194
+ def test_validate_spec_schema_definitions_fail(invalid_schema_definition):
195
+ """
196
+ Given: Schema definitions that do not meet the definition schema
197
+ When: Validating the schema
198
+ Then: SpecSchema(definition) raises a SpecSchemaDefinitionException
199
+ """
200
+ with pytest.raises(SpecSchemaDefinitionException):
201
+ SpecSchema(invalid_schema_definition)
202
+
203
+
204
+ @pytest.fixture(scope="module")
205
+ def valid_values_list_params():
206
+ """
207
+ Create a dict of valid values lists to be used in successful tests below
208
+ """
209
+ test_params = {
210
+ "str list": ["a", "b", "c"],
211
+ "int list": [1, 2, 3],
212
+ "float list": [1.0, 2.0, 3.0],
213
+ "bool list": [True, False, False, True],
214
+ "mixed list": ["a", 1, 2.0, True],
215
+ }
216
+
217
+ yield test_params
218
+
219
+
220
+ @pytest.fixture(
221
+ scope="function", params=["str list", "int list", "float list", "bool list", "mixed list"]
222
+ )
223
+ def valid_values_list(request, valid_values_list_params):
224
+ yield valid_values_list_params[request.param]
225
+
226
+
227
+ def test_validate_spec_schema_definitions_values_list(valid_values_list):
228
+ """
229
+ Given: Schema definitions that have valid values lists
230
+ When: Validating the schema
231
+ Then: SpecSchema(definition) does not raise a SpecSchemaDefinitionException
232
+ """
233
+ test_schema = {
234
+ "fits": {"keyword_name": {"required": True, "type": "int", "values": valid_values_list}}
235
+ }
236
+ SpecSchema(test_schema)
237
+
238
+
239
+ @pytest.fixture(scope="module")
240
+ def invalid_values_list_params():
241
+ """
242
+ Create a dict of invalid values 'lists' to be used in failing tests below
243
+ """
244
+ test_params = {
245
+ "non-list (set)": {"a", 1, 2.0, True},
246
+ "non-list (tuple)": ("a", 1, 2.0, True),
247
+ "non-list (scalar)": 1,
248
+ "non-list (string)": "some string",
249
+ }
250
+
251
+ yield test_params
252
+
253
+
254
+ @pytest.fixture(
255
+ scope="function",
256
+ params=["non-list (set)", "non-list (tuple)", "non-list (scalar)", "non-list (string)"],
257
+ )
258
+ def invalid_values_list(request, invalid_values_list_params):
259
+ yield invalid_values_list_params[request.param]
260
+
261
+
262
+ def test_validate_spec_schema_definitions_values_list_fail(invalid_values_list):
263
+ """
264
+ Given: Schema definitions that have invalid values lists
265
+ When: Validating the schema
266
+ Then: SpecSchema(definition) raises a SpecSchemaDefinitionException
267
+ """
268
+ test_schema = {
269
+ "fits": {
270
+ "keyword_name": {
271
+ "required": True,
272
+ "type": "int",
273
+ "values": invalid_values_list,
274
+ }
275
+ }
276
+ }
277
+
278
+ with pytest.raises(SpecSchemaDefinitionException):
279
+ SpecSchema(test_schema)
280
+
281
+
282
+ @pytest.fixture(scope="module")
283
+ def define_test_schema_definition():
284
+ """
285
+ Create a test schema to be used tested against in both successful and failing
286
+ tests below.
287
+ """
288
+ test_schema_definition = {
289
+ "fits": {
290
+ "key_001": {
291
+ "expand": False,
292
+ "required": False,
293
+ "comment": "comment 1",
294
+ "type": "float",
295
+ },
296
+ "key_002": {
297
+ "expand": False,
298
+ "required": True,
299
+ "comment": "comment 2",
300
+ "type": "str",
301
+ },
302
+ "key_003": {
303
+ "expand": False,
304
+ "required": True,
305
+ "comment": "comment 3",
306
+ "type": "int",
307
+ },
308
+ "key_004": {
309
+ "expand": False,
310
+ "required": False,
311
+ "comment": "comment 4",
312
+ "type": "bool",
313
+ },
314
+ }
315
+ }
316
+
317
+ yield test_schema_definition
318
+
319
+
320
+ def test_create_spec_schema(define_test_schema_definition):
321
+ """
322
+ Given: A valid schema definition and a valid test schema for the definition
323
+ When: Creating the schema
324
+ Then: The valid test schema is successfully parsed by the schema definition
325
+ """
326
+ schema = SpecSchema(define_test_schema_definition)
327
+
328
+ input_dict = {"key_002": "value_002", "key_003": 314159, "key_001": 3.14159}
329
+
330
+ assert not DeepDiff(input_dict, schema(input_dict, extra=True), ignore_order=True)
331
+
332
+
333
+ def test_create_spec_schema_fail(define_test_schema_definition):
334
+ """
335
+ Given: A valid schema definition and an invalid test schema for the definition
336
+ When: Creating the schema
337
+ Then: The invalid test schema is not successfully parsed by the schema definition
338
+ and a voluptuous exception is raised.
339
+ """
340
+ schema = SpecSchema(define_test_schema_definition)
341
+
342
+ # Required key key_003 is missing:
343
+ input_dict = {"fits": {"key_002": "value_002", "key_004": False, "key_001": 3.14159}}
344
+ with pytest.raises(vol.MultipleInvalid):
345
+ schema(input_dict, extra=True)
346
+
347
+
348
+ @pytest.fixture(scope="module")
349
+ def spec_validator():
350
+ """
351
+ Create a schema validator to test fits headers against below.
352
+ """
353
+ test_schema = {
354
+ "fits": {
355
+ "BITPIX": {
356
+ "expand": False,
357
+ "required": True,
358
+ "type": "int",
359
+ "values": [8, 16, 32, 64, -32, -64],
360
+ },
361
+ "NAXIS": {"expand": True, "required": True, "type": "int", "values": [3]},
362
+ "NAXIS1": {"expand": False, "required": True, "type": "int"},
363
+ "NAXIS2": {"expand": False, "required": True, "type": "int"},
364
+ "NAXIS3": {"expand": False, "required": True, "type": "int", "default_value": 1},
365
+ }
366
+ }
367
+
368
+ spec_schema = SpecSchema(test_schema)
369
+ spec_validator = SpecValidator(spec_schema)
370
+
371
+ yield spec_validator
372
+
373
+
374
+ @pytest.fixture(scope="module")
375
+ def valid_test_header_params(tmpdir_factory):
376
+ """
377
+ Create a dict with a valid header in various formats to be successfully tested
378
+ against the schema defined above
379
+ """
380
+ temp_dir = tmpdir_factory.mktemp("valid test_headers_temp")
381
+ file_name = temp_dir.join("tmp_fits_file.fits")
382
+ valid_array = np.ones((1, 1, 1), dtype=float)
383
+ valid_hdu = fits.PrimaryHDU(valid_array)
384
+ valid_hdu_list = fits.HDUList([valid_hdu])
385
+ valid_hdu_list.writeto(str(file_name))
386
+
387
+ yield {
388
+ "valid_hdu_list": valid_hdu_list,
389
+ "valid_fits_header": valid_hdu_list[0].header,
390
+ "valid_fits_file": Path(file_name),
391
+ }
392
+
393
+
394
+ @pytest.fixture(
395
+ scope="function",
396
+ params=["valid_hdu_list", "valid_fits_header", "valid_fits_file"],
397
+ )
398
+ def valid_test_header(request, valid_test_header_params):
399
+ yield valid_test_header_params[request.param]
400
+
401
+
402
+ def test_spec_validator(spec_validator, valid_test_header):
403
+ """
404
+ Given: A valid schema and a valid fits header to test against the schema
405
+ When: Header validation
406
+ Then: SpecValidator successfully validates the header and does not raise an exception
407
+ """
408
+ spec_validator.validate(valid_test_header)
409
+
410
+
411
+ @pytest.fixture(scope="module")
412
+ def valid_test_compressed_file(tmpdir_factory):
413
+ """
414
+ Create a compressed file with a valid header in various formats to be successfully tested
415
+ against the schema defined above
416
+ """
417
+ temp_dir = tmpdir_factory.mktemp("valid test_headers_temp")
418
+ file_name = temp_dir.join("tmp_fits_file.fits")
419
+ valid_array = np.ones((1, 1, 1), dtype=float)
420
+ primary_hdu = fits.PrimaryHDU()
421
+ valid_hdu = fits.CompImageHDU(valid_array)
422
+ valid_hdu_list = fits.HDUList([primary_hdu, valid_hdu])
423
+ valid_hdu_list.writeto(str(file_name))
424
+
425
+ yield {
426
+ "valid_compressed.fits.fz": Path(file_name),
427
+ }
428
+
429
+
430
+ @pytest.fixture(
431
+ scope="function",
432
+ params=["valid_compressed.fits.fz"],
433
+ )
434
+ def valid_test_compressed_files(request, valid_test_compressed_file):
435
+ yield valid_test_compressed_file[request.param]
436
+
437
+
438
+ def test_spec_validator_file(spec_validator, valid_test_compressed_files):
439
+ """
440
+ Given: A valid schema and a valid compressed fits file to test against the schema
441
+ When: Header validation
442
+ Then: SpecValidator successfully validates the header and does not raise an exception
443
+ """
444
+ spec_validator.validate(valid_test_compressed_files)
445
+
446
+
447
+ @pytest.fixture(scope="module")
448
+ def invalid_test_header_params(tmpdir_factory):
449
+ """
450
+ Create a dict with an invalid header to be tested against the schema defined above.
451
+ The header is invalid because the schema requires NAXIS to be 3 and here we create
452
+ a 2D array which means NAXIS=2.
453
+ """
454
+ temp_dir = tmpdir_factory.mktemp("invalid_test_headers_temp")
455
+ file_name = temp_dir.join("tmp_fits_file.fits")
456
+ invalid_array = np.ones((1, 1), dtype=float)
457
+ primary_hdu = fits.PrimaryHDU()
458
+ invalid_hdu = fits.CompImageHDU(invalid_array)
459
+ invalid_hdu_list = fits.HDUList([primary_hdu, invalid_hdu])
460
+ invalid_hdu_list.writeto(str(file_name))
461
+
462
+ yield {
463
+ "invalid_hdu_list": invalid_hdu_list,
464
+ "invalid_fits_header": invalid_hdu_list[0].header,
465
+ "invalid_fits_file": Path(file_name),
466
+ }
467
+
468
+
469
+ @pytest.fixture(scope="function", params=["invalid_hdu_list", "invalid_fits_file"])
470
+ def invalid_test_header(request, invalid_test_header_params):
471
+ yield invalid_test_header_params[request.param]
472
+
473
+
474
+ def test_spec_validator_fail(spec_validator, invalid_test_header):
475
+ """
476
+ Given: A valid schema and an invalid fits header to test against the schema
477
+ When: Header validation
478
+ Then: SpecValidator raises a SpecValidationException
479
+ """
480
+ with pytest.raises(SpecValidationException):
481
+ spec_validator.validate(invalid_test_header)
482
+
483
+
484
+ def test_validation_exception():
485
+ """
486
+ Test ValidationException string format
487
+ Given: a ValidationException class
488
+ When: instance created with message and errors
489
+ Then: String of ValidationException is as expected
490
+ """
491
+ message = "test message"
492
+ errors = {"error1": "error text"}
493
+ s = ValidationException(message, errors=errors)
494
+ assert str(s) == f"{message}: errors={errors}"