Skip to content

Conversation

@cadenmyers13
Copy link
Contributor

@cadenmyers13 cadenmyers13 commented Oct 13, 2025

This test runs 70 tests. This breakdown is from the following 14 copy scenarios. Each copy scenario is written for each caseX generated in the temp dir. we have 5 cases so 14 * 5 = 70

# The copy scenarios are:
# a) copy to cwd
# b) copy to target dir

# 1) copy one example
# 2) copy list of examples from same pack
# 3) copy list of examples from different packs
# 4) copy all examples from a pack
# 5) copy all examples from list of packs
# 6) copy all examples from all packs

@cadenmyers13
Copy link
Contributor Author

cadenmyers13 commented Oct 13, 2025

@sbillinge ready for review

I recognize that the code for this test is somewhat particular because of the temp dir structures we are testing, but I think it should be robust to the UCs listed above. I wanted to reuse the same temp dir that we created for the other test so that this test casts a broader net of cases it can check. However, we could simplify this test if you think this might be a bit overkill.

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.

I reviewed the comments but not the code yet. I can do that later but I don't see the function signature and am on my phone so will do it later

@cadenmyers13
Copy link
Contributor Author

@sbillinge So I dont override these changes before your review, here's a simpler test i wrote.

@pytest.mark.parametrize(
    "path",
    [None, Path("target/path")],
)
@pytest.mark.parametrize(
    "input,expected",
    [
        ("case1", [Path("")]),
        ("case2", [Path("ex1/solution/diffpy-cmi/script1.py"), Path("ex2/random/path/script1.py"), Path("ex2/random/path/script2.py")]),
        ("case3", [Path("ex1/script1.py"), Path("ex2/solutions/script1.py")]),
        ("case4", [Path("")]),
        ("case5", [Path("ex1/path1/script1.py"), Path("ex1/path2/script2.py")]),
    ]
)
def test_copy_examples(input, path, expected, example_cases):
    case_dir = example_cases / input
    if path is None:
        target_dir = None
    else:
        target_dir = case_dir / path
        target_dir.mkdir(parents=True, exist_ok=True)
    paths_before_copy = [p.relative_to(case_dir) for p in case_dir.rglob("*")]
    pm = PacksManager(case_dir)
    examples_dict = pm.available_examples()
    copy_examples(examples_dict, target_dir=target_dir)
    if not expected:
        paths_after_copy = [p.relative_to(case_dir) for p in case_dir.rglob("*")]
        assert paths_after_copy == paths_before_copy
    else:
        dest_root = target_dir or case_dir
        for rel_path in expected:
            dest_file = dest_root / rel_path
            assert dest_file.exists(), f"{dest_file} missing"
  

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 content

examples_dict : dict
Dictionary mapping pack name -> list of (example, path) tuples.
user_input : list of str
List of user-specified pack(s) or example(s) to copy.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

please review. Think this would be useful for parsing examples_dict

@cadenmyers13
Copy link
Contributor Author

@sbillinge I changed the copy_examples() API to accept user_input to help with parsing examples (see inline comment). Let me know if you think this works.

@cadenmyers13
Copy link
Contributor Author

@sbillinge Ready for review. I updated the test such that it takes the actual cli command

@sbillinge
Copy link
Contributor

sbillinge commented Oct 14, 2025

for the tests I like the new test, it is cleaner, but my suggestion would be to:

  1. in our tmpfile infrastructure create files called cwd and user_target. do this in conftest.py
  2. when we start the test do os.chdir('cwd') or sthg like that, so the actual cwd is always tmpdir / "cwd"
  3. In the pytest.parametrize hvae inputs as (<case>, <target>), where <target> is either None or "user_target".
  4. You can test the copied files are in the directory that is relative to the current directory, because cwd is always tmpdir / "cwd" and you can figure out the relative path from there.

@cadenmyers13
Copy link
Contributor Author

cadenmyers13 commented Oct 14, 2025

@sbillinge ready for review. I pasted the new tmp dir structure here #54 (comment)

Note, still need to update behavior comments, but will do that after lunch!

assert dest_file.exists()
else:
empty_dir = list(target_dir.rglob("*"))
assert not empty_dir, f"Expected nothing, but found: {empty_dir}"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because we have expected paths that are empty (ie case1 and case4), these cases pass even though copy_examples isnt built. This will fail if copy_examples copies anything into the target directory.

tests/test_cli.py::test_copy_examples[case1-user_inputs0-expected0-None] PASSED                                                                                                                       [ 10%]
tests/test_cli.py::test_copy_examples[case1-user_inputs0-expected0-user_target] PASSED                                                                                                                [ 20%]
tests/test_cli.py::test_copy_examples[case2-user_inputs1-expected1-None] FAILED                                                                                                                       [ 30%]
tests/test_cli.py::test_copy_examples[case2-user_inputs1-expected1-user_target] FAILED                                                                                                                [ 40%]
tests/test_cli.py::test_copy_examples[case3-user_inputs2-expected2-None] FAILED                                                                                                                       [ 50%]
tests/test_cli.py::test_copy_examples[case3-user_inputs2-expected2-user_target] FAILED                                                                                                                [ 60%]
tests/test_cli.py::test_copy_examples[case4-user_inputs3-expected3-None] PASSED                                                                                                                       [ 70%]
tests/test_cli.py::test_copy_examples[case4-user_inputs3-expected3-user_target] PASSED                                                                                                                [ 80%]
tests/test_cli.py::test_copy_examples[case5-user_inputs4-expected4-None] FAILED                                                                                                                       [ 90%]
tests/test_cli.py::test_copy_examples[case5-user_inputs4-expected4-user_target] FAILED                                                                                                                [100%]

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.

the cases are all good now, but this testing is all in the wrong direction to my taste. Very convoluted and not simple clear actual == expected things. I recommend to start over and make it just follow the pattern that in the paramaterize we pass in something like [(input1, expected1)] and the test doesn't have any, or a tiny amount, of logic it just looks something like

def test_function(inputs, expected)
    actual = function(inputs)
    assert actual == expected

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 comments

@cadenmyers13
Copy link
Contributor Author

@sbillinge Alright, ready for review!

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.

this is very close now. Just a few cleaning edits and we are done.

@cadenmyers13
Copy link
Contributor Author

@sbillinge ready for review.

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 comments

examples_dir = example_cases / "case5"
pm = PacksManager(root_path=examples_dir)
target_dir = example_cases / "user_target"
actual = pm.copy_examples(examples_to_copy=input, target_dir=target_dir)
Copy link
Contributor

Choose a reason for hiding this comment

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

I see two possible issues here that are issues with the function signature.

  1. it looks as if examples_to_copy is an optional parameter but maybe it should be required. If it remains optional we need to decide what the default behavior is. I guess, arguably, the default behavior could be to copy all the examples. So the decision is (a) make it required, not optional. or (b) leave it optional but have the default behavior to copy everything.
  2. You seem to assigned what this function returns to be actual, but this is not testing the behavior. The test should be that the files are actually copied correctly, not something that the function returns. In fact, arguably, the function should return None.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For 1, It isn't an optional parameter. it should be require. I think the confusion comes from me being explicit about the variable name (ie examples_to_copy=input rather than input). I'll remove the param name here (and everywhere else that i may have left it).

For 2, I'll work on this right now. I'll get the path to the copied examples separately. Then, copy_examples will return None.

actual = pm.copy_examples(examples_to_copy=input, target_dir=target_dir)
expected = []
for path in expected_paths:
root_path = target_dir / path
Copy link
Contributor

Choose a reason for hiding this comment

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

root path is a bad name here. In what sense is this a root_path?

How about this for this work:

expected = [target_dir / path for path in expected_paths]

Then to test the files were copied correctly we would need something like

actual_gen = target_dir.rglob('*')

and syntax that iterates through the lists and (a) checks that they contain the same elements and (b) that the files exist and (c) optionally, but not necessarily, check that the files contain the right contents. I think (c) may be overkill, but strictly if we don't we could write the function that passes the test just by touching those files and not copying them. It will be some work though because I think we are creating those files empty so we would have to write something in them. I think there is a way to create and write them in one line though in conftest, so it could be quick to do. Let's just think about it.

examples_dir = example_cases / "case5"
os.chdir(example_cases / "cwd")
pm = PacksManager(root_path=examples_dir)
paths = pm.copy_examples(examples_to_copy=["packA"], target_dir=input)
Copy link
Contributor

Choose a reason for hiding this comment

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

the comments above are relevant here.

[
( # input not found (example or pack)
["bad_example"],
ValueError,
Copy link
Contributor

Choose a reason for hiding this comment

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

normally we would check the error message. In this case we use the tests to define good (helpful) error messages.

ValueError,
None,
),
( # path to dir already exists
Copy link
Contributor

Choose a reason for hiding this comment

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

please use the pattern # description of case. Expected behavior In general, this pattern is better above too, though the behavior is simple and captured well in the test, but here it deserves discussion. What do we want the function to do in this situation?

From a user perspective, I probably don't want a crash. For example, let's say I copy packA to begin with, then I love the code so much that I want to just copy all later, but I want the examples nice and tidily in the same location. When I type all it will find the already present packA examples. Rather than a crash here, I would probably want it to either skip over the already existing examples or overwrite them. To accomplish this, we could interrupt the program and ask user input, but much better is probably to have a default behavior that skips directories that exist and sends a warning, but also to have an option in the cli that is --force or --overwrite, that does the other behavior. The warning that gets printed could mention to rerun the program using --force if you want to overwrite existing examples. SOmething like that.

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, I like the --force option, added it here.

@pytest.mark.parametrize(
"bad_inputs,expected",
[
(Path("nonexistent/path/target"), FileNotFoundError), # doesn't exist
Copy link
Contributor

Choose a reason for hiding this comment

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

please test error messages here too.

@cadenmyers13
Copy link
Contributor Author

cadenmyers13 commented Oct 17, 2025

@sbillinge ready for review. I also left one response here #60 (comment).

I used the following syntax to capture error messages, let me know if there is a better way to capture this

    with pytest.raises(Exception, match=re.escape(expected)):

Also, before you merge, let me know if it looks good and i can put this all on a clean PR

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 comments

expected = [target_dir / path for path in expected_paths]
assert actual == expected
for path in expected_paths:
dest = target_dir / path
Copy link
Contributor

Choose a reason for hiding this comment

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

this probably easier to read if we use the actual expected language rather than source and dest as we are not moving or copying anything. This looks like code that chatGPT would write, but please try and think like a reviewer if/when adopting such code.....

# 4) Path to directory already exists.
# Expected: Raise a warning with the message.
["ex1"],
"Target directory already exists. To overwrite use --force.",
Copy link
Contributor

Choose a reason for hiding this comment

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

put the behavior explicitly, something like ".... . Current versions of existing files have been left. To overwrite with new versions, please rerun specifying --force" Also, is it target directory or `"example directory: ex1" '

What behavior/message do we want if multiple examples are bad? We don't have test cases for that I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think print the list instead dirs will be okay, something like Target directory(ies) <list> already exist...

Path("docs/examples/"),
),
(
# 5) No input provided.
Copy link
Contributor

Choose a reason for hiding this comment

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

this one is not needed. IT will be handled in the cli not here

(
# 1) Target doesn't exist.
# Expected: Raise an error that the target directory is missing.
Path("nonexistent/path/target"),
Copy link
Contributor

Choose a reason for hiding this comment

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

if the path doesn't exist, don't we want to create it?

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 we can create it

"Target directory does not exist",
),
(
# 2) Target is a file.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we need this one. We don't have to worry about this case. I guess the code will crash if we try and create a directory that is a file but we don't need a friendly error message that we test.

"Target path is not a directory",
),
(
# 3) Target is nested in a file.
Copy link
Contributor

Choose a reason for hiding this comment

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

see above. TL:DR, I think we can remove this whole paramaterize

Copy link
Contributor Author

Choose a reason for hiding this comment

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

okay, will remove

@cadenmyers13
Copy link
Contributor Author

@sbillinge ready for review

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.

OK, 100% good on the tests now. Do you want to make a new PR with this version and then punch out the function?

@cadenmyers13
Copy link
Contributor Author

@sbillinge yep let's do that! I can probably get to it later today. On my phone right now and not home

@cadenmyers13
Copy link
Contributor Author

closing bc opened a clean PR

@cadenmyers13 cadenmyers13 deleted the copy-func2 branch November 3, 2025 14:47
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