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

Tests for the image_io sub-module #48

Merged
merged 10 commits into from
Mar 14, 2024
Prev Previous commit
split shuffle into a fixture
  • Loading branch information
K-Meech committed Mar 14, 2024
commit b3bd48578d848b9215b747ea1c7105006364c28e
39 changes: 25 additions & 14 deletions tests/tests/test_image_io.py
Original file line number Diff line number Diff line change
@@ -28,7 +28,27 @@ def image_array(request, array_2d, array_3d):
return array_3d


def write_tiff_sequence_with_txt_file(txt_path, image_array, shuffle=False):
@pytest.fixture()
def shuffled_txt_path(tmp_path, array_3d):
"""
Return the path to a text file containing the paths of a series of 2D tiffs
in a random order
"""
txt_path = tmp_path / "imgs_file.txt"
write_tiff_sequence_with_txt_file(txt_path, array_3d)

# Shuffle paths in the text file into a random order
with open(txt_path, "r+") as f:
tiff_paths = f.read().splitlines()
random.Random(4).shuffle(tiff_paths)
f.seek(0)
f.writelines(line + "\n" for line in tiff_paths)
f.truncate()

return txt_path


def write_tiff_sequence_with_txt_file(txt_path, image_array):
"""
Write an image array to a series of tiffs, and write a text file
Copy link
Member

Choose a reason for hiding this comment

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

Is it worth to briefly mention a typical use case for that text file?
Reading this, I understood what was going on, but wasn't sure why.

[Edit:] after reviewing the rest of the PR, my understanding as to why we need this is that we support this input format, but never write to it, so we want to have this to test. Presumably this format is useful for parallel/lazy IO? Could this be a fixture just for consistency - so all our test code is fixtures or test functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes that's right - it's needed as we support reading from a txt file with load_img_sequence, but there's no corresponding save function. I'm not sure what the use-case is for such a text file - I guess if you have tiff files spread over multiple directories it could be useful?

I'll look at changing this into a fixture now

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@alessandrofelder - I'm trying to convert this to a fixture, but having problems with allowing it to be optionally shuffled like here. Is there a simple way to pass a parameter like shuffle=True to a fixture? Otherwise, I could have two fixtures - one for a shuffled text file and one for not, but they will have pretty much identical code in both unless I refactor it out into some kind of helper function. Any ideas?

Copy link
Contributor

Choose a reason for hiding this comment

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

Fixtures can't take optional arguments, but can be indirectly parameterised:

import pytest

@pytest.fixture
def my_fixture(msg: str, reps: int) -> str:
    return msg * reps


@pytest.mark.parametrize(
    "msg, reps, expected",
    [
        pytest.param("Hi", 2, "HiHi"),
        pytest.param("Hi", 1, "Hi"),
    ],
)
def test_thing(my_fixture, expected):
    assert my_fixture == expected

will pass, for example. However if you change reps: int to reps: int = 2 in the fixture definition, the invoking pytest will fail.

However I don't think write_tiff_sequence_with_txt_file can be made into a fixture (depending on how you're using it). I'm prepared to be proven wrong though 😅 But in my understanding, fixtures have to either:

  • Return an object that can be constructed a-priori to the test running.
  • Be a sequence of executable steps that serves as either the setup or teardown of a test.

Here, write_tiff_sequence_with_txt_file needs to be run in the middle of a test, so it has to be a function, and can't be a fixture. You could try redesigning it as a setup fixture though - assuming you're only ever using it at the start of a test, before anything else runs:

@pytest.fixture
def write_tiff_sequence_with_txt_file(txt_file, input_array, shuffle):
    # Do things prior to running a test
    yield # This is where the test will run


@pytest.mark.usefixtures("write_tiff_sequency_with_txt_file")
@pytest.mark.parametrize(
    "txt_file, input_array, shuffle, other_args_used_by_test",
    [
        ["foo.txt", [1,2,3], True, "some_fancy_args"],
    ],
)
def test_things(txt_file, input_array, shuffle, other_args_used_by_test) -> None:
    # write_tiff_sequence_with_txt_file will run here before the actual test instructions
    assert other_args_used_by_test
    # Note that txt_file, input_array, shuffle don't actually have to be in the test-function declaration,
    # test_things(other_args_used_by_test) also works fine.
    # But if you DO include them, the values of the variables will be usable within the test-function scope too.
    # IE the line
    assert (
        txt_file is not None
    )  
    # Only works if test_things(txt_file, other_args_used_by_test) is used as the definition, 
    # but not if test_things(other_args_used_by_test) is used instead.

Otherwise, I don't think it can be "fixture-ised".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the info @willGraham01! That makes it a lot clearer.
I think I could redesign it as a setup fixture, but this would mean I'd need a separate test case for txt files with load_any, rather than having it as a parameter here

As we support loading txt files in image_io via load_any and load_img_sequence, perhaps it would make sense for this to be moved into image_io to allow saving this kind of file also? Then the test functions could simply call this?

Copy link
Member

Choose a reason for hiding this comment

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

Interesting. Yea fixture parameters are tricky!

In this specific case, IIUC we need the shuffle=True part for testing the sort argument of load_img_sequence only, whereas we will need the shuffle=False case in the source code (as part of addressing #50). I don't think we'd ever want to intentionally shuffle the order of paths in a text file in our source code?

So I'd suggest

  • keeping write_tiff_sequence_with_txt_file not a fixture
  • splitting the shuffle part into a fixture that overwrites the textfile with a shuffled version of it.
  • as part of Add a save_any function to image_io #50, move write_tiff_sequence_with_txt_file to the source code

Is that sensible?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good to me! I'll add these points to the issue + split the shuffle part now

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@alessandrofelder I split shuffle into a fixture now. Could you take a look + merge if you're happy with it? Then I'll work on #50

containing all the tiff file paths in order (one per line).
@@ -42,8 +62,6 @@ def write_tiff_sequence_with_txt_file(txt_path, image_array, shuffle=False):
Filepath of text file to create
image_array : np.ndarray
Image to write as sequence of tiffs
shuffle : bool
Whether to shuffle the order of filepaths in the text file
"""
directory = txt_path.parent

@@ -54,8 +72,6 @@ def write_tiff_sequence_with_txt_file(txt_path, image_array, shuffle=False):

# Write txt file containing all tiff file paths (one per line)
tiff_paths = sorted(sub_dir.iterdir())
if shuffle:
random.Random(4).shuffle(tiff_paths)
txt_path.write_text(
"\n".join([str(sub_dir / fname) for fname in tiff_paths])
)
@@ -177,18 +193,13 @@ def test_load_img_sequence_from_txt(tmp_path, array_3d):
"sort",
[True, False],
)
def test_sort_img_sequence_from_txt(tmp_path, array_3d, sort):
def test_sort_img_sequence_from_txt(shuffled_txt_path, array_3d, sort):
"""
Test that filepaths read from a txt file can be sorted correctly
Test that shuffled filepaths read from a txt file can be sorted correctly
"""
img_sequence_file = tmp_path / "imgs_file.txt"
write_tiff_sequence_with_txt_file(
img_sequence_file, array_3d, shuffle=True
)

# Load image from paths in text file
# Load image from shuffled paths in text file
reloaded_array = load.load_img_sequence(
str(img_sequence_file), 1, 1, 1, sort=sort
str(shuffled_txt_path), 1, 1, 1, sort=sort
)
if sort:
assert (reloaded_array == array_3d).all()
Loading