From 2f270f915c1d89864ccd72417dd89c56002ce8b6 Mon Sep 17 00:00:00 2001 From: Brett Date: Mon, 21 Oct 2024 18:07:27 -0400 Subject: [PATCH] add tests --- .../jwst/_kwtool/_tests/__init__.py | 0 .../jwst/_kwtool/_tests/conftest.py | 148 ++++++++++++++++++ .../jwst/_kwtool/_tests/test_cli.py | 23 +++ .../jwst/_kwtool/_tests/test_compare.py | 135 ++++++++++++++++ .../jwst/_kwtool/_tests/test_dmd.py | 41 +++++ .../jwst/_kwtool/_tests/test_kwd.py | 37 +++++ src/stdatamodels/jwst/_kwtool/compare.py | 2 +- 7 files changed, 385 insertions(+), 1 deletion(-) create mode 100644 src/stdatamodels/jwst/_kwtool/_tests/__init__.py create mode 100644 src/stdatamodels/jwst/_kwtool/_tests/conftest.py create mode 100644 src/stdatamodels/jwst/_kwtool/_tests/test_cli.py create mode 100644 src/stdatamodels/jwst/_kwtool/_tests/test_compare.py create mode 100644 src/stdatamodels/jwst/_kwtool/_tests/test_dmd.py create mode 100644 src/stdatamodels/jwst/_kwtool/_tests/test_kwd.py diff --git a/src/stdatamodels/jwst/_kwtool/_tests/__init__.py b/src/stdatamodels/jwst/_kwtool/_tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/stdatamodels/jwst/_kwtool/_tests/conftest.py b/src/stdatamodels/jwst/_kwtool/_tests/conftest.py new file mode 100644 index 00000000..d40d7ecb --- /dev/null +++ b/src/stdatamodels/jwst/_kwtool/_tests/conftest.py @@ -0,0 +1,148 @@ +import json + +import pytest + +from stdatamodels.jwst._kwtool import dmd, kwd +from stdatamodels.jwst.datamodels import ImageModel, JwstDataModel + + +@pytest.fixture(scope="module") +def fake_kwd_path(tmp_path_factory): + kwd_path = tmp_path_factory.mktemp("kwd") + + # make 1 top file and 3 ref file + _top = { + "type": "object", + "title": "root", + "properties": { + "meta": { + "title" : "FGS Keywords Schema Metadata", + "type" : "object", + "properties": { + "program": { + "title": "Programmatic information", + "type": "object", + "properties": { + "$ref": "core.program.schema.json" + }, + }, + "observation": { + "title" : "Observation identifiers", + "type" : "object", + "properties" : { + "allOf" : [ + { + "$ref" : "core.observation.schema.json", + }, + { + "$ref" : "science.observation.schema.json", + }, + ], + } + } + } + } + } + } + _core_program = { + "title" : { + "fits_keyword" : "TITLE", + "title" : "Proposal title", + "description" : "na", # actual is longer + "type" : "string", + "units" : "", + "example" : "Cryo2 SIC NIRISS", + "default_value" : "UNKNOWN", + "source" : "Proposal and Planning System (PPS)", + "sw_source" : "PPS:dms_program_view.title", + "calculation" : "", + "destination" : ["ScienceCommon.title", "GuideStar.title"], + "sql_dtype" : "nvarchar(200)", + "si" : "Multiple", + "mode" : "All", + "level" : "1b", + "fits_hdu" : "PRIMARY", + "section" : "Programmatic information", + "misc" : "", + "comment_line" : "/ Program information" + }, + } + _core_observation = { + "obs_id": { + "fits_keyword": "OBS_IDD", # modified to trigger compare error + # modified title to trigger compare error + "title" : "Programmatic observation identifier modified", + "description": "na", # actual is longer + "type" : "integer", # modified to trigger compare error + "units" : "", + "example" : "V80600004001P0000000002101", + "default_value" : "", + "special_processing" : "VALUE_REQUIRED", + "source" : "Science Image Header", + "sw_source" : "", + "calculation" : "", + "destination" : ["ScienceCommon.obs_id","GuideStar.obs_id"], + "sql_dtype" : "nvarchar(26)", + "si" : "Multiple", + "mode" : "All", + "level" : "1a", + "fits_hdu" : "SCI", # modified to trigger compare error + "section" : "Observation identifiers", + "misc" : "" + } + } + _science_observation = { + "engineering_quality" : { + "fits_keyword" : "ENG_QUAL", + "title" : "Engineering DB quality indicator", + "description" : "na", # actual is longer + "type" : "string", + "enum" : ["OK", "SUSPECT", "NOT_IN_DATAMODEL"], # modified to trigger compare error + "units" : "", + "example" : "OK", + "default_value" : "OK", + "source" : "Science Data Processing (SDP)", + "sw_source" : "", + "calculation" : "", + "destination" : ["ScienceCommon.eng_qual"], + "sql_dtype" : "nvarchar(15)", + "si" : "ALL", + "mode" : "All", + "level" : "1b", + "fits_hdu" : "PRIMARY", + "section" : "Observation identifiers", + "misc" : "", + "comment_line" : "/ Visit information" + }, + } + for fn, schema in [ + ("top.fgs.schema.json", _top), + ("core.program.schema.json", _core_program), + ("core.observation.schema.json", _core_observation), + ("science.observation.schema.json", _science_observation) + ]: + schema_path = kwd_path / fn + with open(schema_path, "w") as f: + json.dump(schema, f) + return kwd_path + + +@pytest.fixture(scope="module") +def fake_kwd(fake_kwd_path): + return kwd.load(fake_kwd_path) + + +@pytest.fixture(scope="module") +def limit_dmd_models(): + # we can't use the monkeypatch fixture as it's function scoped + with pytest.MonkeyPatch.context() as mp: + mp.setattr(dmd, "_get_subclasses", lambda k: [ImageModel]) + yield + + +@pytest.fixture(scope="module") +def fake_dmd(limit_dmd_models): + """ + A fake datamodel dictionary that includes only 1 datamodel: ImageModel + """ + return dmd.load() diff --git a/src/stdatamodels/jwst/_kwtool/_tests/test_cli.py b/src/stdatamodels/jwst/_kwtool/_tests/test_cli.py new file mode 100644 index 00000000..070fa374 --- /dev/null +++ b/src/stdatamodels/jwst/_kwtool/_tests/test_cli.py @@ -0,0 +1,23 @@ +import os +import sys + +from stdatamodels.jwst._kwtool import cli + + +def test_cli(monkeypatch, tmp_path_factory, fake_kwd_path, limit_dmd_models): + # move to a new, empty directory + temp_cwd = tmp_path_factory.mktemp("cwd") + os.chdir(temp_cwd) + + output_dir = tmp_path_factory.mktemp("output") + output_path = output_dir / "my_report.html" + + monkeypatch.setattr(sys, "argv", [sys.argv[0], str(fake_kwd_path), "-o", str(output_path)]) + + cli._from_cmdline() + + # make sure we didn't write out any local files + assert not os.listdir(temp_cwd) + + # and that our output file exists + assert output_path.exists() diff --git a/src/stdatamodels/jwst/_kwtool/_tests/test_compare.py b/src/stdatamodels/jwst/_kwtool/_tests/test_compare.py new file mode 100644 index 00000000..3177c073 --- /dev/null +++ b/src/stdatamodels/jwst/_kwtool/_tests/test_compare.py @@ -0,0 +1,135 @@ +import pytest + +from stdatamodels.jwst._kwtool import compare + + +def test_filter_standard(): + keywords = { + ("PRIMARY", "PCOUNT"): 1, + ("PRIMARY", "OBS_ID"): 2, + } + result = compare._filter_non_standard(keywords) + assert ("PRIMARY", "OBS_ID") in result + assert ("PRIMARY", "PCOUNT") not in result + + +def test_filter_non_pattern(): + keywords = { + ("PRIMARY", "P_BAND"): 1, + ("PRIMARY", "PBAND"): 2, + ("PRIMARY", "BAND"): 3, + } + result = compare._filter_non_pattern(keywords) + assert ("PRIMARY", "PBAND") in result + assert ("PRIMARY", "BAND") in result + assert ("PRIMARY", "P_BAND") not in result + + +def test_compare_path_no_destination(): + """ + Ignore path differences for keywords that have + no destination. + """ + k = [{ + "keyword": { + # no destination + "title": "foo", + }, + "path": "a.b.c".split("."), + }] + d = [{ + "path": "d.e.f".split("."), + }] + assert compare._compare_path(k, d) is None + + +def test_compare_path(): + # make sure k has a desintation so the difference isn't ignored + k = [{"path": "a.b.c".split("."), "keyword": {"destination": "somewhere"}}] + d = [{"path": "d.e.f".split(".")}] + assert compare._compare_path(k, d) == {"kwd": {"a.b.c"}, "dmd": {"d.e.f"}} + + +@pytest.mark.parametrize( + "kv, dv, expected", [ + ("a", "a", None), + ("a", "z", {"kwd": {"a"}, "dmd": {"z"}}), + ("z", "a", {"kwd": {"z"}, "dmd": {"a"}}), + ("a", None, {"kwd": {"a"}, "dmd": {compare._MISSING_VALUE}}), + (None, "a", {"kwd": {compare._MISSING_VALUE}, "dmd": {"a"}}), + (None, None, None), + ] +) +def test_compare_keyword_subitem(kv, dv, expected): + # if kv/dv is None, don't fill title + if kv is None: + k = [{"keyword": {}}] + else: + k = [{"keyword": {"title": kv}}] + if dv is None: + d = [{"keyword": {}}] + else: + d = [{"keyword": {"title": dv}}] + assert compare._compare_keyword_subitem(k, d, "title") == expected + + +@pytest.mark.parametrize( + "ktype, dtype, expected", [ + ("string", "string", None), + ("integer", "string", {"kwd": {"integer"}, "dmd": {"string"}}), + ("string", "integer", {"kwd": {"string"}, "dmd": {"integer"}}), + ("float", "number", None), + ] +) +def test_compare_type(ktype, dtype, expected): + k = [{"keyword": {"type": ktype}}] + d = [{"keyword": {"type": dtype}}] + assert compare._compare_type(k, d) == expected + + +def test_compare_enum(): + # TODO + pass + + +@pytest.fixture(scope="module") +def compare_result(fake_kwd_path): + return compare.compare_keywords(fake_kwd_path) + + +def test_obs_id(compare_result): + """ + We constructed a fake keyword dictionary with OBS_IDD + and the datamodels have OBS_ID. Check this was found. + """ + in_k, in_d, in_both, def_diff, k_keys, d_keys = compare_result + assert ("SCI", "OBS_IDD") in in_k + assert ("PRIMARY", "OBS_ID") in in_d + + +def test_title(compare_result): + """ + TITLE should be in both and agree + """ + in_k, in_d, in_both, def_diff, k_keys, d_keys = compare_result + key = ("PRIMARY", "TITLE") + assert key in in_both + assert key not in def_diff + + +def test_eng_qual(compare_result): + """ + ENG_QUAL is in both but the enum differs. + + There are other differences (due to our test setup) but we'll + ignore these. + """ + in_k, in_d, in_both, def_diff, k_keys, d_keys = compare_result + key = ("PRIMARY", "ENG_QUAL") + assert key in in_both + assert key in def_diff + assert "enum" in def_diff[key] + assert def_diff[key]["enum"] == { + "dmd": {"OK", "SUSPECT"}, + "kwd": {"OK", "SUSPECT", "NOT_IN_DATAMODEL"}, + } diff --git a/src/stdatamodels/jwst/_kwtool/_tests/test_dmd.py b/src/stdatamodels/jwst/_kwtool/_tests/test_dmd.py new file mode 100644 index 00000000..30479500 --- /dev/null +++ b/src/stdatamodels/jwst/_kwtool/_tests/test_dmd.py @@ -0,0 +1,41 @@ +""" +monkeypatch _get_subclasses to only return 1 datamodel (to make things easier) +separately test _get_subclasses +""" +import pytest + +from stdatamodels.jwst.datamodels import JwstDataModel, FgsImgPhotomModel +from stdatamodels.jwst._kwtool import dmd + + +def test_get_subclasses(): + # more of a smoke test + assert FgsImgPhotomModel in dmd._get_subclasses(JwstDataModel) + + +@pytest.fixture(params=[("PRIMARY", "TITLE"), ("PRIMARY", "OBS_ID"), ("PRIMARY", "ENG_QUAL")]) +def keyword_list(request, fake_dmd): + """ + There are many more keywords than the ones used here. These + were chosen to match the ones added in the fake keyword dictionary. + """ + return fake_dmd[request.param] + + +@pytest.fixture() +def keyword(keyword_list): + """ + The real datamodel dictionary will contain multiple entires for 1 "keyword" + (a FITS_HDU, FITS_KEYWORD pair). The test dictionary does not, this + fixture helps to test just the first found entry. + """ + return keyword_list[0] + + +def test_found(keyword_list): + """ + This uses the keyword_list fixture to make sure that the test dictionary + only contains one entry per keyword pair (an assumption made by the + keyword fixture above). + """ + assert len(keyword_list) == 1 diff --git a/src/stdatamodels/jwst/_kwtool/_tests/test_kwd.py b/src/stdatamodels/jwst/_kwtool/_tests/test_kwd.py new file mode 100644 index 00000000..0ae65e29 --- /dev/null +++ b/src/stdatamodels/jwst/_kwtool/_tests/test_kwd.py @@ -0,0 +1,37 @@ +""" +Tests here use the fake_kwd fixture in conftest +""" +import pytest + + +@pytest.fixture(params=[("PRIMARY", "TITLE"), ("SCI", "OBS_IDD"), ("PRIMARY", "ENG_QUAL")]) +def keyword_list(request, fake_kwd): + return fake_kwd[request.param] + + +@pytest.fixture() +def keyword(keyword_list): + """ + The real keyword dictionary will contain multiple entires for 1 "keyword" + (a FITS_HDU, FITS_KEYWORD pair). The test dictionary does not, this + fixture helps to test just the first found entry. + """ + return keyword_list[0] + + +def test_fits_keyword_count(fake_kwd): + assert len(fake_kwd) == 3 + + +def test_found(keyword_list): + """ + This uses the keyword_list fixture to make sure that the test dictionary + only contains one entry per keyword pair (an assumption made by the + keyword fixture above). + """ + assert len(keyword_list) == 1 + + +@pytest.mark.parametrize("missing_key", ["allOf", "properties"]) +def test_path_sanitized(missing_key, keyword): + assert missing_key not in keyword["path"] diff --git a/src/stdatamodels/jwst/_kwtool/compare.py b/src/stdatamodels/jwst/_kwtool/compare.py index 4f0da3bf..25ab79f1 100644 --- a/src/stdatamodels/jwst/_kwtool/compare.py +++ b/src/stdatamodels/jwst/_kwtool/compare.py @@ -44,7 +44,7 @@ def _compare_path(k, d): # that have an archive destination TODO document this for i in k: if i['keyword'].get('destination'): - # since there is a destination, report the differenc + # since there is a destination, report the difference return paths return None