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

robustify parametrize default name #113856

Closed
wants to merge 3 commits into from
Closed

Conversation

pmeier
Copy link
Collaborator

@pmeier pmeier commented Nov 16, 2023

Stack from ghstack (oldest at bottom):

#113340 was reverted initially due to a bad default parametrization name. The test looked like

@common_utils.parametrize(
    "type_fn",
    [
        type,
        lambda obj: obj.__class__,
    ],
)
def test_access_class_method_from_user_class(self, type_fn):

This is a valid parametrization, but results in these default test names:

❯ pytest test/dynamo/test_export.py -k test_access_class_method_from_user_class --co -q
test/dynamo/test_export.py::ExportTests::test_access_class_method_from_user_class_type_fn_<class 'type'>
test/dynamo/test_export.py::ExportTests::test_access_class_method_from_user_class_type_fn_<function ExportTests_<lambda> at 0x7f3be5de0c10>

Ignoring the whitespace in the test names, which can lead to other issues down the line, the problem in #113340 was that the lambda parameter included a memory address. IIUC, internally, the tests are not collected and run in the same process. Meaning, the address of the lambda and in turn the test name is no longer valid on the runner. This is fixed earlier in the stack by giving the parametrization an explicit name with subtest, but this PR is about preventing issues in the default case.

pytest solves this by simply using the name of the parameter plus its index as id in the test name:

import pytest

class Foo:
    def __repr__(self):
        return str(id(self))

@pytest.mark.parametrize(
    "bar",
    [
        pytest.param(type),
        pytest.param(lambda obj: obj.__class__),
        pytest.param(Foo()),
    ],
)
def test_foo(bar):
    pass
❯ pytest main.py --co -q
main.py::test_foo[type]
main.py::test_foo[<lambda>]
main.py::test_foo[bar2]

pytest has better defaults for type and lambda than we do, but is has a safe default for custom objects.

This PR aligns our default test name with pytest. Using the parametrization from above again, we now collect

❯ pytest test/dynamo/test_export.py -k test_access_class_method_from_user_class --co -q
test/dynamo/test_export.py::ExportTests::test_access_class_method_from_user_class_type_fn0
test/dynamo/test_export.py::ExportTests::test_access_class_method_from_user_class_type_fn1

which might not be as expressive at first glance, but at least prevents bugs.

@pmeier pmeier requested a review from a team as a code owner November 16, 2023 09:39
Copy link

pytorch-bot bot commented Nov 16, 2023

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/113856

Note: Links to docs will display an error until the docs builds have been completed.

✅ No Failures

As of commit 6f840f1 with merge base 5fb1d8f (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

pmeier added a commit that referenced this pull request Nov 16, 2023
ghstack-source-id: fbce670132357a159ded80611b8ddb73c8cb50db
Pull Request resolved: #113856
@pmeier pmeier requested a review from malfet November 16, 2023 09:54
""" Returns a string representation for the given arg that is suitable for use in test function names. """
if isinstance(value, torch.dtype):
return dtype_name(value)
elif isinstance(value, torch.device):
return str(value)
# Can't use isinstance as it would cause a circular import
elif value.__class__.__name__ == 'OpInfo' or value.__class__.__name__ == 'ModuleInfo':
elif type(value).__name__ in {'OpInfo', 'ModuleInfo'}:
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Driveby simplification.

return value.formatted_name
else:
# Include name and value separated by underscore.
return f"{name}_{str(value).replace('.', '_')}"
return f"{name}{idx}"
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the actual fix. Everything else are just modifications to get access to the index here.

#113340 was reverted initially due to a bad default parametrization name. The test looked like

```python
common_utils.parametrize(
    "type_fn",
    [
        type,
        lambda obj: obj.__class__,
    ],
)
def test_access_class_method_from_user_class(self, type_fn):
```

This is a valid parametrization, but results in these default test names:

```bash
❯ pytest test/dynamo/test_export.py -k test_access_class_method_from_user_class --co -q
test/dynamo/test_export.py::ExportTests::test_access_class_method_from_user_class_type_fn_<class 'type'>
test/dynamo/test_export.py::ExportTests::test_access_class_method_from_user_class_type_fn_<function ExportTests_<lambda> at 0x7f3be5de0c10>
```

Ignoring the whitespace in the test names, which can lead to other issues down the line, the problem in #113340 was that the lambda parameter included a memory address. IIUC, internally, the tests are not collected and run in the same process. Meaning, the address of the lambda and in turn the test name is no longer valid on the runner. This is fixed earlier in the stack by giving the parametrization an explicit name with `subtest`, but this PR is about preventing issues in the default case.

`pytest` solves this by simply using the name of the parameter plus its index as id in the test name:

```python
import pytest

class Foo:
    def __repr__(self):
        return str(id(self))

pytest.mark.parametrize(
    "bar",
    [
        pytest.param(type),
        pytest.param(lambda obj: obj.__class__),
        pytest.param(Foo()),
    ],
)
def test_foo(bar):
    pass
```

```
❯ pytest main.py --co -q
main.py::test_foo[type]
main.py::test_foo[<lambda>]
main.py::test_foo[bar2]
```

`pytest` has better defaults for `type` and `lambda` than we do, but is has a safe default for custom objects.

This PR aligns our default test name with `pytest`. Using the parametrization from above again, we now collect

```bash
❯ pytest test/dynamo/test_export.py -k test_access_class_method_from_user_class --co -q
test/dynamo/test_export.py::ExportTests::test_access_class_method_from_user_class_type_fn0
test/dynamo/test_export.py::ExportTests::test_access_class_method_from_user_class_type_fn1
```

which might not be as expressive at first glance, but at least prevents bugs.

[ghstack-poisoned]
pmeier added a commit that referenced this pull request Nov 16, 2023
ghstack-source-id: 7440bd933c5f503d69282529f577cee9e6948418
Pull Request resolved: #113856
return value.formatted_name
else:
# Include name and value separated by underscore.
elif isinstance(value, (int, float, str)):
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We keep the previous naming scheme for "safe" primitive type.

#113340 was reverted initially due to a bad default parametrization name. The test looked like

```python
common_utils.parametrize(
    "type_fn",
    [
        type,
        lambda obj: obj.__class__,
    ],
)
def test_access_class_method_from_user_class(self, type_fn):
```

This is a valid parametrization, but results in these default test names:

```bash
❯ pytest test/dynamo/test_export.py -k test_access_class_method_from_user_class --co -q
test/dynamo/test_export.py::ExportTests::test_access_class_method_from_user_class_type_fn_<class 'type'>
test/dynamo/test_export.py::ExportTests::test_access_class_method_from_user_class_type_fn_<function ExportTests_<lambda> at 0x7f3be5de0c10>
```

Ignoring the whitespace in the test names, which can lead to other issues down the line, the problem in #113340 was that the lambda parameter included a memory address. IIUC, internally, the tests are not collected and run in the same process. Meaning, the address of the lambda and in turn the test name is no longer valid on the runner. This is fixed earlier in the stack by giving the parametrization an explicit name with `subtest`, but this PR is about preventing issues in the default case.

`pytest` solves this by simply using the name of the parameter plus its index as id in the test name:

```python
import pytest

class Foo:
    def __repr__(self):
        return str(id(self))

pytest.mark.parametrize(
    "bar",
    [
        pytest.param(type),
        pytest.param(lambda obj: obj.__class__),
        pytest.param(Foo()),
    ],
)
def test_foo(bar):
    pass
```

```
❯ pytest main.py --co -q
main.py::test_foo[type]
main.py::test_foo[<lambda>]
main.py::test_foo[bar2]
```

`pytest` has better defaults for `type` and `lambda` than we do, but is has a safe default for custom objects.

This PR aligns our default test name with `pytest`. Using the parametrization from above again, we now collect

```bash
❯ pytest test/dynamo/test_export.py -k test_access_class_method_from_user_class --co -q
test/dynamo/test_export.py::ExportTests::test_access_class_method_from_user_class_type_fn0
test/dynamo/test_export.py::ExportTests::test_access_class_method_from_user_class_type_fn1
```

which might not be as expressive at first glance, but at least prevents bugs.

[ghstack-poisoned]
pmeier added a commit that referenced this pull request Nov 16, 2023
ghstack-source-id: 5b81b9d3cf6a8fd0eece8f1f79285365db5306a5
Pull Request resolved: #113856
Copy link
Contributor

@malfet malfet left a comment

Choose a reason for hiding this comment

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

Looks fine, though again, I think excessive parametrization is detrimental to UX (for example adding two lambdas to the list of parameters will result in two tests having identical names)

@pmeier
Copy link
Collaborator Author

pmeier commented Nov 16, 2023

@pytorchbot merge

@pytorch-bot pytorch-bot bot added the ciflow/trunk Trigger trunk jobs on your pull request label Nov 16, 2023
@pytorchmergebot
Copy link
Collaborator

Merge started

Your change will be merged once all checks pass (ETA 0-4 Hours).

Learn more about merging in the wiki.

Questions? Feedback? Please reach out to the PyTorch DevX Team

Advanced Debugging
Check the merge workflow status
here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants