Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor test_transfor.py: pass variables to pytest parametrize, use conftest.py for common error messages #236

Merged
merged 8 commits into from
Dec 16, 2024

Conversation

bobleesj
Copy link
Contributor

@bobleesj bobleesj commented Dec 15, 2024

For non-UC behavior, we've decided to use the following format in the group as the group standard:

@pytest.mark.parametrize(
    "wavelength, q, expected_tth",
    [
        # Case 1: Allow empty arrays for q
        # 1. Empty q values, no wavelength, return empty arrays
        (None, np.empty((0)), np.empty((0))),
        # 2. Empty q values, wavelength specified, return empty arrays
        (4 * np.pi, np.empty((0)), np.empty(0)),
       
        # Case 2: Allow wavelength to be missing.
        # Valid q values, no wavelength, return index array
        (
            None,
            np.array([0, 0.2, 0.4, 0.6, 0.8, 1]),
            np.array([0, 1, 2, 3, 4, 5]),
        ),

        # Case 3: Correctly specified q and wavelength
        # Expected tth values are 2*arcsin(q) in degrees
        (4 * np.pi, np.array([0, 1 / np.sqrt(2), 1.0]), np.array([0, 90.0, 180.0])),
    ],
)

Copy link

Warning! No news item is found for this PR. If this is a user-facing change/feature/fix,
please add a news item by copying the format from news/TEMPLATE.rst.

Copy link

codecov bot commented Dec 15, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 100.00%. Comparing base (d919fef) to head (d962c1b).
Report is 48 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #236   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            8         8           
  Lines          362       372   +10     
=========================================
+ Hits           362       372   +10     
Files with missing lines Coverage Δ
tests/conftest.py 100.00% <100.00%> (ø)
tests/test_transforms.py 100.00% <100.00%> (ø)

... and 1 file with indirect coverage changes

@@ -36,3 +36,21 @@ def _load(filename):
@pytest.fixture
def do_minimal_tth():
return DiffractionObject(wavelength=2 * np.pi, xarray=np.array([30, 60]), yarray=np.array([1, 2]), xtype="tth")


@pytest.fixture
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The following two warning messages are used often in test_transform.py We could have access to this string within tests via conftest? We aren't really importing from src... so I think it really increases readability for some test cases (not all):

For example

From:

params_d_to_tth_bad = [
    # UC1: user specified invalid d values that result in tth > 180 degrees
    (
        [4 * np.pi, np.array([1.2, 1, 0.8, 0.6, 0.4, 0.2])],
        [
            ValueError,
            "The supplied input array and wavelength will result in an impossible two-theta. "
            "Please check these values and re-instantiate the DiffractionObject with correct values.",
        ],
    ),
    # UC2: user specified a wrong wavelength that result in tth > 180 degrees
    (
        [100, np.array([1, 0.8, 0.6, 0.4, 0.2, 0])],
        [
            ValueError,
            "The supplied input array and wavelength will result in an impossible two-theta. "
            "Please check these values and re-instantiate the DiffractionObject with correct values.",
        ],
    ),
]


@pytest.mark.parametrize("inputs, expected", params_d_to_tth_bad)
def test_d_to_tth_bad(inputs, expected):
    with pytest.raises(expected[0], match=expected[1]):
        d_to_tth(inputs[1], inputs[0])

to:

@pytest.mark.parametrize(
    "wavelength, d, expected_error_type",
    [
        # UC1: user specified invalid d values that result in tth > 180 degrees
        (4 * np.pi, np.array([1.2, 1, 0.8, 0.6, 0.4, 0.2]), ValueError),
        # UC2: user specified a wrong wavelength that result in tth > 180 degrees
        (100, np.array([1, 0.8, 0.6, 0.4, 0.2, 0]), ValueError),
    ],
)
def test_d_to_tth_bad(wavelength, d, expected_error_type, invalid_q_or_d_or_wavelength_error_msg):
    expected_error_msg = invalid_q_or_d_or_wavelength_error_msg
    with pytest.raises(expected_error_type, match=expected_error_msg):
        d_to_tth(d, wavelength)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes on that

Copy link
Contributor Author

@bobleesj bobleesj Dec 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No numbers have been modified.

Here primarily removed extra variables such as params_q_to_tth. First thought was that, it would be best to not have variables shared globally at the file level.

Second thought was that

@pytest.mark.parametrize(
    "wavelength, q, expected_tth",
    [
        # UC1: Empty q values, no wavelength, return empty arrays
        (None, np.empty((0)), np.empty((0))),

Feels more natural - the first two lines are like - "hey these are testing variables that we provide. The the last parameter is what we expect. Of course, if you are interested... take a look at the rest of the lines for specific UCs."

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree. In future we would like to spend 75% of thinking/planning/reviewing time on the tests, then 25% on the code target than vice versa and so the more the tests are readable and interpretable and syntactically codified the better. This dress well, as you describe with an extra comment that captures the behavioral intent.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even better for the behavior would be something like. "Minimally support empty arrays"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or both comments, behavior and example of behavior

Copy link
Contributor Author

@bobleesj bobleesj Dec 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pytest.mark.parametrize(
    "wavelength, q, expected_tth",
    [
        # UC1.1: User specified empty 'q' and no 'wavelength'.
        # Expect empty 'tth' array and UserWarning about missing wavelength.
        (None, np.empty((0)), np.empty((0))),

        # UC1.2: User specified empty 'q' and 'wavelength'. Expect empty 'tth' array.
        (4 * np.pi, np.empty((0)), np.empty(0)),

        # UC2.1: User specified non-empty 'q' values and no 'wavelength'.
        # Expect non-empty 'tth' array and UserWarning about missing wavelength.
        (
            None,
            np.array([0, 0.2, 0.4, 0.6, 0.8, 1]),
            np.array([0, 1, 2, 3, 4, 5]),
        ),

        # UC2.2: User specified non-empty 'q' values and 'wavelength'.
        # Expect tth values are 2*arcsin(q) in degrees.
        (4 * np.pi, np.array([0, 1 / np.sqrt(2), 1.0]), np.array([0, 90.0, 180.0])),
    ],
)

This is the change made since the last commit - it's hard to read below, so pasting the change here. How do you like it? I've used UC1.1 and UC1.2 (w/o q) vs. UC2.1 and UC2.2 (with q).

Also having a blank line between each UC feels a bit more readable?

Copy link
Contributor Author

@bobleesj bobleesj Dec 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sbillinge ready for review - this is the only change made since your last review. (It's getting a bit big..)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not quite what I had in mind. I think it was good before, but just needed a kind of overriding framing. Also, this is not the correct use of UCs per how we do it in the group, so I don't want to propagate wrong ideas by calling these UCs.

Maybe it is easier if I put what I had in mind below

@pytest.mark.parametrize(
    "wavelength, q, expected_tth",
    [
        # Case 1: Allow empty arrays
        # 1. Empty q values, no wavelength, return empty arrays
        (None, np.empty((0)), np.empty((0))),
        # 2: Empty q values, wavelength specified, return empty arrays
        (4 * np.pi, np.empty((0)), np.empty(0)),
       
        # Case 2: allow wavelength to be missing.
        # 3: user specified valid q values, no wavelength, return index array
        (
            None,
            np.array([0, 0.2, 0.4, 0.6, 0.8, 1]),
            np.array([0, 1, 2, 3, 4, 5]),
        ),

        # Case 3: correctly specified q and wavelength
        # expected tth values are 2*arcsin(q) in degrees
        (4 * np.pi, np.array([0, 1 / np.sqrt(2), 1.0]), np.array([0, 90.0, 180.0])),
    ],
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pytest.mark.parametrize(
    "wavelength, q, expected_tth",
    [
        # Case 1: Allow empty arrays for q
        # 1. Empty q values, no wavelength, return empty arrays
        (None, np.empty((0)), np.empty((0))),
        # 2. Empty q values, wavelength specified, return empty arrays
        (4 * np.pi, np.empty((0)), np.empty(0)),
       
        # Case 2: Allow wavelength to be missing.
        # Valid q values, no wavelength, return index array
        (
            None,
            np.array([0, 0.2, 0.4, 0.6, 0.8, 1]),
            np.array([0, 1, 2, 3, 4, 5]),
        ),

        # Case 3: Correctly specified q and wavelength
        # Expected tth values are 2*arcsin(q) in degrees
        (4 * np.pi, np.array([0, 1 / np.sqrt(2), 1.0]), np.array([0, 90.0, 180.0])),
    ],
)

Thank you @sbillinge. Above is used for one of the refactored test functions. For other test functions, I could further make improvements via new PRs. Adding more here is a bit hard for you and me to track things..

assert np.allclose(actual_q, expected_q)


params_tth_to_q_bad = [
Copy link
Contributor Author

@bobleesj bobleesj Dec 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lots of red lines above/below, but again, no manual fixes have been done..

Copy link
Contributor Author

@bobleesj bobleesj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ready for review! @sbillinge and cc'ing @yucongalicechen for your second look!

@bobleesj bobleesj marked this pull request as ready for review December 15, 2024 05:28
@bobleesj bobleesj changed the title Refactor test_transfor.py: pass variables to functions, maintain function scope, use conftest.py for common error messages Refactor test_transfor.py: pass variables to pytest parametrize, use conftest.py for common error messages Dec 15, 2024
@bobleesj bobleesj mentioned this pull request Dec 15, 2024
59 tasks
Copy link
Contributor

@sbillinge sbillinge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I like this very much. Two thumbs up. Please see my inline comments, but let's use this as our own kind of testing PEP.

For the behavior/example we could group then with s blank line of there are a few different rxamples of the same behavior.

@@ -36,3 +36,21 @@ def _load(filename):
@pytest.fixture
def do_minimal_tth():
return DiffractionObject(wavelength=2 * np.pi, xarray=np.array([30, 60]), yarray=np.array([1, 2]), xtype="tth")


@pytest.fixture
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes on that

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree. In future we would like to spend 75% of thinking/planning/reviewing time on the tests, then 25% on the code target than vice versa and so the more the tests are readable and interpretable and syntactically codified the better. This dress well, as you describe with an extra comment that captures the behavioral intent.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even better for the behavior would be something like. "Minimally support empty arrays"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or both comments, behavior and example of behavior

@bobleesj
Copy link
Contributor Author

bobleesj commented Dec 15, 2024

@sbillinge ready for review - made a change shown in the latest commit below and replied to your in-line comments.

ef75197

Copy link
Contributor

@sbillinge sbillinge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please see reply to your comment

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not quite what I had in mind. I think it was good before, but just needed a kind of overriding framing. Also, this is not the correct use of UCs per how we do it in the group, so I don't want to propagate wrong ideas by calling these UCs.

Maybe it is easier if I put what I had in mind below

@pytest.mark.parametrize(
    "wavelength, q, expected_tth",
    [
        # Case 1: Allow empty arrays
        # 1. Empty q values, no wavelength, return empty arrays
        (None, np.empty((0)), np.empty((0))),
        # 2: Empty q values, wavelength specified, return empty arrays
        (4 * np.pi, np.empty((0)), np.empty(0)),
       
        # Case 2: allow wavelength to be missing.
        # 3: user specified valid q values, no wavelength, return index array
        (
            None,
            np.array([0, 0.2, 0.4, 0.6, 0.8, 1]),
            np.array([0, 1, 2, 3, 4, 5]),
        ),

        # Case 3: correctly specified q and wavelength
        # expected tth values are 2*arcsin(q) in degrees
        (4 * np.pi, np.array([0, 1 / np.sqrt(2), 1.0]), np.array([0, 90.0, 180.0])),
    ],
)

Copy link
Contributor Author

@bobleesj bobleesj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sbillinge ready for review - replied to your in-line comment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pytest.mark.parametrize(
    "wavelength, q, expected_tth",
    [
        # Case 1: Allow empty arrays for q
        # 1. Empty q values, no wavelength, return empty arrays
        (None, np.empty((0)), np.empty((0))),
        # 2. Empty q values, wavelength specified, return empty arrays
        (4 * np.pi, np.empty((0)), np.empty(0)),
       
        # Case 2: Allow wavelength to be missing.
        # Valid q values, no wavelength, return index array
        (
            None,
            np.array([0, 0.2, 0.4, 0.6, 0.8, 1]),
            np.array([0, 1, 2, 3, 4, 5]),
        ),

        # Case 3: Correctly specified q and wavelength
        # Expected tth values are 2*arcsin(q) in degrees
        (4 * np.pi, np.array([0, 1 / np.sqrt(2), 1.0]), np.array([0, 90.0, 180.0])),
    ],
)

Thank you @sbillinge. Above is used for one of the refactored test functions. For other test functions, I could further make improvements via new PRs. Adding more here is a bit hard for you and me to track things..

@sbillinge sbillinge merged commit e6a006d into diffpy:main Dec 16, 2024
4 of 5 checks passed
@bobleesj bobleesj deleted the pytest-warnings branch December 16, 2024 14:30
@bobleesj bobleesj mentioned this pull request Dec 16, 2024
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants