From 81414a9f4e97941f2311bc2102e23da8ed777b84 Mon Sep 17 00:00:00 2001 From: Alexis Maya-Isabelle Shuping Date: Fri, 18 Oct 2024 12:13:37 -0500 Subject: [PATCH] Bring `Variable` and `VariableSet` back to 100% coverage --- exseos/types/Variable.py | 18 +- test/types/test_Variable.py | 301 +++++++++++++++------------------ test/types/test_VariableSet.py | 252 +++++++++++++++++++++++++++ 3 files changed, 394 insertions(+), 177 deletions(-) create mode 100644 test/types/test_VariableSet.py diff --git a/exseos/types/Variable.py b/exseos/types/Variable.py index aa338aa..2a3a489 100644 --- a/exseos/types/Variable.py +++ b/exseos/types/Variable.py @@ -402,10 +402,8 @@ def __init__(self, v1: Variable, v2: Variable, note: str = ""): msg = ( f"{type(v1).__name__} {v1.name} has {v1_type}, " - + f"but {type(v2).__name} {v2.name} has {v2_type}!" - + f" {note}" - if note - else "" + + f"but {type(v2).__name__} {v2.name} has {v2_type}!" + + (f" {note}" if note else "") ) super().__init__(msg) @@ -436,10 +434,8 @@ def __init__(self, v1: Variable, v2: Variable, note: str = ""): msg = ( f"{type(v1).__name__} {v1.name} has {v1_type}, " - + f"but {type(v2).__name} {v2.name} has {v2_type}!" - + f" {note}" - if note - else "" + + f"but {type(v2).__name__} {v2.name} has {v2_type}!" + + (f" {note}" if note else "") ) super().__init__(msg) @@ -693,7 +689,11 @@ def assert_types_match( if v1.var_type == Nothing() or v2.var_type == Nothing(): return Okay(None) - if issubclass(v2.var_type.val, v1.var_type.val): + if issubclass(v2.var_type.val, v1.var_type.val) and not ( + # Special case: bools are apparently a subclass of int in python. + # We don't want that behavior here. + v2.var_type.val is bool and v1.var_type.val is int + ): return Okay(None) if v1.var_type_inferred or v2.var_type_inferred: diff --git a/test/types/test_Variable.py b/test/types/test_Variable.py index 9c08726..7a7cc15 100644 --- a/test/types/test_Variable.py +++ b/test/types/test_Variable.py @@ -16,21 +16,58 @@ along with this program. If not, see . """ +import itertools +from exseos.types.Result import Fail, Okay, Warn from exseos.types.Variable import ( + ExplicitTypeMismatchError, + InferredTypeMismatchWarning, UnboundVariable, BoundVariable, - VariableSet, - UnboundVariableError, - AmbiguousVariableError, + Variable, + assert_types_match, constant, ensure_from_name, ensure_from_name_arr, ) from exseos.types.Option import Nothing, Some -from exseos.types.Result import Okay, Warn, Fail +from abc import ABC import pytest -from pytest import raises + + +def test_eq(): + assert BoundVariable("x", 2) == BoundVariable("x", 2) + assert UnboundVariable("y") == UnboundVariable("y") + + assert BoundVariable("x", 2) != UnboundVariable("x") + + assert BoundVariable("x", 2, int) == BoundVariable("x", 2, int) + assert BoundVariable("x", 1, int) != BoundVariable("x", 1, str) + assert UnboundVariable("y", int) == UnboundVariable("y", int) + assert UnboundVariable("y", int) != UnboundVariable("y", str) + + assert BoundVariable("x", 1, desc="My Test Variable") == BoundVariable( + "x", 1, desc="My Test Variable" + ) + assert BoundVariable("x", 1, desc="My Test Variable") != BoundVariable( + "x", 1, desc="My Other Test Variable" + ) + assert UnboundVariable("y", desc="My Test Variable") == UnboundVariable( + "y", desc="My Test Variable" + ) + assert UnboundVariable("y", desc="My Test Variable") != UnboundVariable( + "y", desc="My Other Test Variable" + ) + + assert BoundVariable("x", 1) != BoundVariable("x", 2) + + assert BoundVariable("x", 1, default=1) == BoundVariable("x", 1, default=1) + assert BoundVariable("x", 1, default=1) != BoundVariable("x", 1, default=2) + assert UnboundVariable("y", default="yes") == UnboundVariable("y", default="yes") + assert UnboundVariable("y", default="yes") != UnboundVariable("y", default="no") + + assert BoundVariable("x", 2) != 2 + assert UnboundVariable("y") != "y" def test_val(): @@ -112,6 +149,19 @@ class TestSubclass2(TestSuperclass): assert BoundVariable("x", TestSubclass1(), default=Some(1)).var_type == Nothing() +def test_overbroad_type_inference(): + class OverbroadA(ABC): + pass + + class OverbroadB(ABC): + pass + + b = BoundVariable("x", OverbroadA(), default=Some(OverbroadB())) + + assert b.var_type == Some(ABC) + assert b.var_type_inferred + + @pytest.mark.parametrize( "c", [1, 2, -1, None, "test", True, BoundVariable("test_var", 1)] ) @@ -119,200 +169,115 @@ def test_constant(c): assert constant(c) == BoundVariable(f"Constant::{c}", c) -def test_set(): - vs = [ - BoundVariable("x", 1), - BoundVariable("y", "test"), - UnboundVariable("z", default=-1.22), - ] +@pytest.mark.parametrize("name", ["test", "a", "b", "name with spaces"]) +def test_ensure_from_name_str(name): + assert ensure_from_name(name) == UnboundVariable(name) - vset = VariableSet(vs) - assert vset.status == Okay(None) - assert vset.x == 1 - assert vset.y == "test" - assert vset.z == -1.22 +@pytest.mark.parametrize( + "var", + [ + BoundVariable("test", 1), + UnboundVariable("a"), + UnboundVariable("b", int, "description", default=0), + BoundVariable("name with spaces", bool, "desc with spaces", default=False), + ], +) +def test_ensure_from_name_var(var): + assert ensure_from_name(var) == var -def test_set_ambiguous(): - vs = [ - BoundVariable("x", 1), - BoundVariable("y", "test"), - UnboundVariable("z", default=-1.22), - BoundVariable("x", 2), +def test_ensure_from_name_arr(): + ar = [ + BoundVariable("test", 1), + "a", + UnboundVariable("b", int, "description", default=0), + "name with spaces", ] - vset = VariableSet(vs) - - expect = Warn( - [ - AmbiguousVariableError( - "x", - ( - BoundVariable("x", 1), - BoundVariable("x", 2), - ), - "(while constructing a `VariableSet`)", - ) - ], - None, - ) - - print(f"expected: {expect}") - print(f"actual : {vset.status}") - - assert vset.status == expect - - assert vset.y == "test" - assert vset.z == -1.22 + assert ensure_from_name_arr(ar) == [ + BoundVariable("test", 1), + UnboundVariable("a"), + UnboundVariable("b", int, "description", default=0), + UnboundVariable("name with spaces"), + ] - # note that which variable takes precedence is undefined - assert vset.x == 1 or vset.x == 2 +tm_base_vars_explicit: tuple[Variable] = ( + UnboundVariable("x", int), + UnboundVariable("φ", int), + BoundVariable("σ", 1, var_type=int), +) -def test_set_unbound(): - vs = [BoundVariable("x", 1), BoundVariable("y", "test"), UnboundVariable("z")] +tm_base_vars_implicit: tuple[Variable] = ( + BoundVariable("ψ", 1), + UnboundVariable("y", default=1), +) - vset = VariableSet(vs) +tm_mismatched_vars_explicit: tuple[Variable] = ( + UnboundVariable("x", str), + UnboundVariable("φ", bool), + BoundVariable("σ", Exception(), var_type=Exception), +) - with raises(UnboundVariableError): - vset.z +tm_mismatched_vars_implicit: tuple[Variable] = ( + BoundVariable("ψ", "oops"), + UnboundVariable("y", default=False), +) +tm_ambiguous_vars: tuple[Variable] = ( + UnboundVariable("τ"), + BoundVariable("η", 1.0, default="whoops this isn't a float"), +) -def test_set_undefined(): - vs = [BoundVariable("x", 1), BoundVariable("y", "test"), UnboundVariable("z")] - vset = VariableSet(vs) +test_assert_types_match_params = itertools.product( + itself := tm_base_vars_explicit + tm_base_vars_implicit, itself +) - with raises(AttributeError): - vset.potatoes +@pytest.mark.parametrize(("v1", "v2"), test_assert_types_match_params) +def test_assert_types_match(v1, v2): + assert assert_types_match(v1, v2, True) == Okay(None) + assert assert_types_match(v1, v2, False) == Okay(None) -def test_set_check(): - vs = [ - BoundVariable("x", 1), - BoundVariable("y", "test"), - UnboundVariable("z", default=-1.22), - UnboundVariable("undefined"), - UnboundVariable("undefined2"), - ] - vset = VariableSet(vs) - - assert vset.check("x") == Okay(None) - assert vset.check("x", "y") == Okay(None) - assert vset.check("undefined") == Fail( - [ - UnboundVariableError( - UnboundVariable("undefined"), - "(while retrieving a `Variable` from a `VariableSet`)", - ) - ] - ) +test_assert_types_inferred_mismatch_params = itertools.product( + tm_base_vars_explicit + tm_base_vars_implicit, tm_mismatched_vars_implicit +) - assert vset.check("x", "undefined") == Fail( - [ - UnboundVariableError( - UnboundVariable("undefined"), - "(while retrieving a `Variable` from a `VariableSet`)", - ) - ] - ) - assert vset.check("x", "undefined", "undefined2") == Fail( - [ - UnboundVariableError( - UnboundVariable("undefined"), - "(while retrieving a `Variable` from a `VariableSet`)", - ), - UnboundVariableError( - UnboundVariable("undefined2"), - "(while retrieving a `Variable` from a `VariableSet`)", - ), - ] +@pytest.mark.parametrize(("v1", "v2"), test_assert_types_inferred_mismatch_params) +def test_assert_types_inferred_mismatch(v1, v2): + assert assert_types_match(v1, v2, True) == Warn( + [InferredTypeMismatchWarning(v1, v2)], None ) - assert vset.check("not in set") == Fail( - [AttributeError("No variable named not in set in this `VariableSet`!")] + assert assert_types_match(v1, v2, False) == Warn( + [InferredTypeMismatchWarning(v1, v2)], None ) - assert vset.check("not in set", "undefined", "undefined2") == Fail( - [ - AttributeError("No variable named not in set in this `VariableSet`!"), - UnboundVariableError( - UnboundVariable("undefined"), - "(while retrieving a `Variable` from a `VariableSet`)", - ), - UnboundVariableError( - UnboundVariable("undefined2"), - "(while retrieving a `Variable` from a `VariableSet`)", - ), - ] - ) +test_assert_types_explicit_mismatch_params = itertools.product( + tm_base_vars_explicit, tm_mismatched_vars_explicit +) -def test_set_check_all(): - vs_good = [ - BoundVariable("x", 1), - BoundVariable("y", "test"), - UnboundVariable("z", default=-1.22), - ] - vs_bad = [ - BoundVariable("x", 1), - BoundVariable("y", "test"), - UnboundVariable("z", default=-1.22), - UnboundVariable("undefined"), - UnboundVariable("undefined2"), - ] +@pytest.mark.parametrize(("v1", "v2"), test_assert_types_explicit_mismatch_params) +def test_assert_types_explicit_mismatch(v1, v2): + assert assert_types_match(v1, v2, True) == Fail([ExplicitTypeMismatchError(v1, v2)]) - vset_good = VariableSet(vs_good) - vset_bad = VariableSet(vs_bad) - - assert vset_good.check_all() == Okay(None) - assert vset_bad.check_all() == Fail( - [ - UnboundVariableError( - UnboundVariable("undefined"), - "(while retrieving a `Variable` from a `VariableSet`)", - ), - UnboundVariableError( - UnboundVariable("undefined2"), - "(while retrieving a `Variable` from a `VariableSet`)", - ), - ] + assert assert_types_match(v1, v2, False) == Warn( + [ExplicitTypeMismatchError(v1, v2)], None ) -@pytest.mark.parametrize("name", ["test", "a", "b", "name with spaces"]) -def test_ensure_from_name_str(name): - assert ensure_from_name(name) == UnboundVariable(name) - - -@pytest.mark.parametrize( - "var", - [ - BoundVariable("test", 1), - UnboundVariable("a"), - UnboundVariable("b", int, "description", default=0), - BoundVariable("name with spaces", bool, "desc with spaces", default=False), - ], +test_assert_types_explicit_ambiguous_params = itertools.product( + tm_base_vars_explicit + tm_base_vars_implicit, tm_ambiguous_vars ) -def test_ensure_from_name_var(var): - assert ensure_from_name(var) == var -def test_ensure_from_name_arr(): - ar = [ - BoundVariable("test", 1), - "a", - UnboundVariable("b", int, "description", default=0), - "name with spaces", - ] - - assert ensure_from_name_arr(ar) == [ - BoundVariable("test", 1), - UnboundVariable("a"), - UnboundVariable("b", int, "description", default=0), - UnboundVariable("name with spaces"), - ] +@pytest.mark.parametrize(("v1", "v2"), test_assert_types_explicit_ambiguous_params) +def test_assert_types_ambiguous(v1, v2): + assert assert_types_match(v1, v2, True) == Okay(None) + assert assert_types_match(v1, v2, False) == Okay(None) diff --git a/test/types/test_VariableSet.py b/test/types/test_VariableSet.py new file mode 100644 index 0000000..9143561 --- /dev/null +++ b/test/types/test_VariableSet.py @@ -0,0 +1,252 @@ +# ExSeOS-H Hardware ML Workflow Manager +# Copyright (C) 2024 Alexis Maya-Isabelle Shuping + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from exseos.types.Variable import ( + UnboundVariable, + BoundVariable, + VariableSet, + UnboundVariableError, + AmbiguousVariableError, +) +from exseos.types.Result import Okay, Warn, Fail + +from pytest import raises + + +def test_basic(): + vs = [ + BoundVariable("x", 1), + BoundVariable("y", "test"), + UnboundVariable("z", default=-1.22), + ] + + vset = VariableSet(vs) + + assert vset.status == Okay(None) + assert vset.x == 1 + assert vset.y == "test" + assert vset.z == -1.22 + + +def test_eq(): + vs = [ + BoundVariable("x", 1), + BoundVariable("y", "test"), + UnboundVariable("z", default=-1.22), + ] + + vs_eq = [ + BoundVariable("x", 1), + BoundVariable("y", "test"), + UnboundVariable("z", default=-1.22), + ] + + vs_neq = [ + BoundVariable("x", 2), + BoundVariable("5", "test"), + UnboundVariable("z", default=-1.22), + ] + + vs_extra = [ + BoundVariable("x", 1), + BoundVariable("y", "test"), + UnboundVariable("z", default=-1.22), + UnboundVariable("ϑ"), + ] + + assert VariableSet(vs) == VariableSet(vs_eq) + assert VariableSet(vs) != VariableSet(vs_neq) + assert VariableSet(vs) != VariableSet(vs_extra) + assert VariableSet(vs) != vs + + +def test_ambiguous(): + vs = [ + BoundVariable("x", 1), + BoundVariable("y", "test"), + UnboundVariable("z", default=-1.22), + BoundVariable("x", 2), + ] + + vset = VariableSet(vs) + + expect = Warn( + [ + AmbiguousVariableError( + "x", + ( + BoundVariable("x", 1), + BoundVariable("x", 2), + ), + "(while constructing a `VariableSet`)", + ) + ], + None, + ) + + print(f"expected: {expect}") + print(f"actual : {vset.status}") + + assert vset.status == expect + + assert vset.y == "test" + assert vset.z == -1.22 + + # note that which variable takes precedence is undefined + assert vset.x == 1 or vset.x == 2 + + +def test_unbound(): + vs = [BoundVariable("x", 1), BoundVariable("y", "test"), UnboundVariable("z")] + + vset = VariableSet(vs) + + with raises(UnboundVariableError): + vset.z + + +def test_undefined(): + vs = [BoundVariable("x", 1), BoundVariable("y", "test"), UnboundVariable("z")] + + vset = VariableSet(vs) + + with raises(AttributeError): + vset.potatoes + + +def test_check(): + vs = [ + BoundVariable("x", 1), + BoundVariable("y", "test"), + UnboundVariable("z", default=-1.22), + UnboundVariable("undefined"), + UnboundVariable("undefined2"), + ] + + vset = VariableSet(vs) + + assert vset.check("x") == Okay(None) + assert vset.check("x", "y") == Okay(None) + assert vset.check("undefined") == Fail( + [ + UnboundVariableError( + UnboundVariable("undefined"), + "(while retrieving a `Variable` from a `VariableSet`)", + ) + ] + ) + + assert vset.check("x", "undefined") == Fail( + [ + UnboundVariableError( + UnboundVariable("undefined"), + "(while retrieving a `Variable` from a `VariableSet`)", + ) + ] + ) + + assert vset.check("x", "undefined", "undefined2") == Fail( + [ + UnboundVariableError( + UnboundVariable("undefined"), + "(while retrieving a `Variable` from a `VariableSet`)", + ), + UnboundVariableError( + UnboundVariable("undefined2"), + "(while retrieving a `Variable` from a `VariableSet`)", + ), + ] + ) + + assert vset.check("not in set") == Fail( + [AttributeError("No variable named not in set in this `VariableSet`!")] + ) + + assert vset.check("not in set", "undefined", "undefined2") == Fail( + [ + AttributeError("No variable named not in set in this `VariableSet`!"), + UnboundVariableError( + UnboundVariable("undefined"), + "(while retrieving a `Variable` from a `VariableSet`)", + ), + UnboundVariableError( + UnboundVariable("undefined2"), + "(while retrieving a `Variable` from a `VariableSet`)", + ), + ] + ) + + +def test_check_all(): + vs_good = [ + BoundVariable("x", 1), + BoundVariable("y", "test"), + UnboundVariable("z", default=-1.22), + ] + + vs_bad = [ + BoundVariable("x", 1), + BoundVariable("y", "test"), + UnboundVariable("z", default=-1.22), + UnboundVariable("undefined"), + UnboundVariable("undefined2"), + ] + + vset_good = VariableSet(vs_good) + vset_bad = VariableSet(vs_bad) + + assert vset_good.check_all() == Okay(None) + assert vset_bad.check_all() == Fail( + [ + UnboundVariableError( + UnboundVariable("undefined"), + "(while retrieving a `Variable` from a `VariableSet`)", + ), + UnboundVariableError( + UnboundVariable("undefined2"), + "(while retrieving a `Variable` from a `VariableSet`)", + ), + ] + ) + + +def test_str(): + vs = [ + BoundVariable("x", 1), + BoundVariable("y", "test"), + UnboundVariable("ϕ", default=-1.22), + ] + + assert str(VariableSet(vs)) == ( + "VariableSet {\n" + + f" x: {str(vs[0])}\n" + + f" y: {str(vs[1])}\n" + + f" ϕ: {str(vs[2])}\n" + + "}" + ) + + +def test_repr(): + vs = [ + BoundVariable("x", 1), + BoundVariable("y", "test"), + UnboundVariable("ϕ", default=-1.22), + ] + + assert ( + repr(VariableSet(vs)) + == f"VariableSet({repr(vs[0])}, {repr(vs[1])}, {repr(vs[2])})" + )