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

Introduce JUnitValueSource check #188

Merged
merged 15 commits into from
Mar 2, 2023
Merged

Introduce JUnitValueSource check #188

merged 15 commits into from
Mar 2, 2023

Conversation

oxkitsune
Copy link
Contributor

Implement BugPattern to migrate parameterized JUnit tests with a single argument that's known at compile time to the @ValueSource annotation.

The @ValueSource annotation supports:

  • all primitives and their java.lang wrapper
  • java.lang.String
  • java.lang.Class

Example:

- @ParameterizedTest
- @MethodSource("fooProvider")
- void foo (int number) {
-   assertThat(number).isNotEqualTo(0);
- }
- 
- private static Stream<Arguments> fooProvider () {
-   return Stream.of(
-     arguments(1),
-     arguments(2)
-   );
- }

+ @ParameterizedTest
+ @ValueSource(ints = {1, 2})
+ void foo (int number) {
+   assertThat(number).isNotEqualTo(0);
+ }

Suggested commit message:

Introduce `JUnitValueSource` check (#188)

@rickie rickie self-requested a review August 11, 2022 12:59
Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

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

A parameterized test to check the parameterized test is some mind blowing stuff 🤯.
Pretty cool stuff man. This definitely is a nice check!

I'm in doubt whether it's the right approach for this PR. To be honest, to make the test easier to comprehend it'd actually lean towards not using this setup.

For the identification test it'll be quite some code but make the test way easier to read.
In the identification we can show all the variants it can flag. In the replacement test, we don't need to test every variant that we can rewrite. It'll be enough to show how the replacement is performed and that it works.

If we do it like that, I think the size will be reasonable still.

Perhaps @Stephan202 can give some input as well.

WDYT of this proposal @oxkitsune?


I left some other comments as well :).

A general note on method names:
Try to look at the method names in "isolation" do they explain what they are really doing? Is it clear what we're trying to achieve with that method?

Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

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

Flushing some comments.

@rickie
Copy link
Member

rickie commented Aug 24, 2022

(Note, this was a WIP commit)

Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

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

By changing the way we use the Matcher at the start we can already rule out some checks. Now we know it is a method that is eligible to rewrite. That makes it easier. We can make some more assumptions (that we are sure of), to make the code more elegant.

Let's discuss tomorrow morning, would be nice to add some extra explanation here and there.

@rickie rickie added this to the 0.2.1 milestone Sep 9, 2022
Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

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

Hmm, one big open point for me is how we currently handle the Stream.of(1, 2).map(Arguments::arguments) in the code. It is an edge case that prevents us from making the code quite a bit cleaner. Will have to get back to that 🤔.

Added a commit and left some comments.

Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

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

Forgot to remove this and the comment above that.

@oxkitsune oxkitsune requested a review from rickie September 19, 2022 09:21
@rickie rickie modified the milestones: 0.3.0, 0.4.0 Sep 19, 2022
@rickie rickie modified the milestones: 0.4.0, 0.5.0 Oct 5, 2022
Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

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

Added a few commits. PTAL @oxkitsune 😄.

There are two XXX's which I consider to be out of scope for now but important to write down.

The PR is now ready for your 👀 @Stephan202.

Copy link
Member

@Stephan202 Stephan202 left a comment

Choose a reason for hiding this comment

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

Rebased and added a commit. Cool feature!

You will notice that I refactored quite a bit. While I do agree that we should be reasonable when defining the scope of a test, we should:

  1. Err on the side of correctness, but to avoid false-positives and to ensure a good first impression of users who try out the library.
  2. (As a result) aim for very high test coverage.
  3. (Where reasonable) try to support code bases which aren't as uniform as Picnic's.

Still TODO:

  • Extend test coverage, primarily "should not match" cases. (See the XXX comments I left.)
  • ~One more full pass over the code.

Comment on lines 80 to 82
AnnotationTree annotationTree =
ASTHelpers.getAnnotationWithSimpleName(
tree.getModifiers().getAnnotations(), "MethodSource");
Copy link
Member

Choose a reason for hiding this comment

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

We can avoid this if we use the annotations matcher defined above; will push a proposal.

tree.getModifiers().getAnnotations(), "MethodSource");

return tryConstructValueSourceFix(parameterType, annotationTree, state)
.map(fix -> describeMatch(tree, fix.build()))
Copy link
Member

Choose a reason for hiding this comment

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

Since there's only one caller, we can push the .build() into tryConstructValueSourceFix.

Copy link
Member

Choose a reason for hiding this comment

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

Right now we're flagging the issue on tree, but I think it's more intuitive if annotationTree is flagged.

case "Integer":
return "ints";
default:
return type.toLowerCase() + "s";
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return type.toLowerCase() + "s";
return type.toLowerCase(Locale.ROOT) + "s";

(We should have a check for this 🤔.)

Copy link
Member

Choose a reason for hiding this comment

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

Fixed :).

Comment on lines 149 to 150
.filter(MethodInvocationTree.class::isInstance)
.map(MethodInvocationTree.class::cast)
Copy link
Member

Choose a reason for hiding this comment

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

This surprised me a bit, until I realized that this is about unwrapping the argument(...) call. Let's make that more explicit.

Edit: the code I'll push doesn't have this limitation.

Comment on lines 151 to 152
.flatMap(mit -> mit.getArguments().stream())
.filter(JUnitValueSource::isCompileTimeConstant)
Copy link
Member

Choose a reason for hiding this comment

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

In combination with the test below we're assuming that the arguments(...) call accepts precisely one argument, while there could be multiple (for example if the value factory is used in multiple places (and those places may be in another class)).

methodSourceAnnotation,
String.format(
"@ValueSource(%s = {%s})",
toValueSourceAttributeName(parameterType.toString()), attributeValue))
Copy link
Member

Choose a reason for hiding this comment

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

The .toString() here will yield e.g. java.lang.String, which is not what is intended; the resultant code won't compile. (There should be test cases for each case in toValueSourceAttributeName.)

NB: For cases like these it's useful to run ./run-mutation-tests.sh, then inspect error-prone-contrib/target/pit-reports/index.html. Integrating PIT with the build is on the TODO list.

Copy link
Member

Choose a reason for hiding this comment

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

For generic types such as Class<?> this will also create an invalid @ValueSource argument.

private static Optional<ReturnTree> getReturnTree(MethodTree methodTree) {
return methodTree.getBody().getStatements().stream()
.filter(ReturnTree.class::isInstance)
.findFirst()
Copy link
Member

Choose a reason for hiding this comment

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

If there is more than one we should back out, not just take the first one.

Comment on lines 140 to 141
.filter(method -> method.getName().contentEquals(factoryMethodName))
.findFirst()
Copy link
Member

Choose a reason for hiding this comment

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

There may be overloads; we need to select the nullary version.

Comment on lines 62 to 63
private static final Matcher<ExpressionTree> STREAM_OF_ARGUMENTS =
staticMethod().onClass(Stream.class.getName()).named("of");
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't actually test that the values are arguments 🤔 (While the code below does assume that the Stream.of arguments are method invocations.)

Comment on lines 36 to 67
"",
" private static Stream<Arguments> fooTestCases() {",
" return Stream.of(arguments('a'), arguments('b'), arguments(CONST_CHAR));",
" }",
Copy link
Member

Choose a reason for hiding this comment

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

Internally we generally place the value factory methods before the associated test method; suggest we do the same here.

@rickie rickie requested review from Stephan202 and rickie November 1, 2022 08:42
@rickie
Copy link
Member

rickie commented Nov 1, 2022

Oops, just meant to re-request review from myself 😬.

Copy link
Member

@Stephan202 Stephan202 left a comment

Choose a reason for hiding this comment

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

@oxkitsune have you started working on the tests? Asking to prevent double work :).

"",
" @ParameterizedTest",
" @MethodSource(\"streamOfClassesAndClassArguments\")",
" void string(Class<?> c) {}",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
" void string(Class<?> c) {}",
" void clazz(Class<?> c) {}",

(Don't have the code checked out right now; something to fix later.)

Copy link
Member

Choose a reason for hiding this comment

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

Applied.

@oxkitsune
Copy link
Contributor Author

@oxkitsune have you started working on the tests? Asking to prevent double work :).

I have!

@github-actions
Copy link

  • Surviving mutants in this change: 3
  • Killed mutants in this change: 79
class surviving killed
🧟tech.picnic.errorprone.bugpatterns.JUnitValueSource 1 72
🧟tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers 1 7
🧟tech.picnic.errorprone.bugpatterns.JUnitValueSource$1 1 0

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

Copy link
Member

@Stephan202 Stephan202 left a comment

Choose a reason for hiding this comment

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

So a bit more work is needed. Can have a closer look later.

Copy link
Member

@Stephan202 Stephan202 left a comment

Choose a reason for hiding this comment

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

Documenting some additional caveats; will try to cover these in one go.

Copy link
Member

@Stephan202 Stephan202 left a comment

Choose a reason for hiding this comment

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

Rebased and added one more commit. Documented as many limitations as I could identify; those are for another day.

Suggested commit message:

Introduce `JUnitValueSource` check (#188)

This new check replaces JUnit `@MethodSource` usages with an equivalent
`@ValueSource` annotation where possible.

public JUnitValueSource() {}

@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
Copy link
Member

Choose a reason for hiding this comment

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

Had another look at this. It's easier not to special-case the nullary () syntax; instead we can back-off if there are overloads.

@github-actions
Copy link

  • Surviving mutants in this change: 3
  • Killed mutants in this change: 77
class surviving killed
🧟tech.picnic.errorprone.bugpatterns.JUnitValueSource 2 69
🧟tech.picnic.errorprone.bugpatterns.JUnitValueSource$1 1 0
🎉tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers 0 8

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

Copy link
Member

@Stephan202 Stephan202 left a comment

Choose a reason for hiding this comment

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

Added one more commit.


private static Matcher<ExpressionTree> isArrayArgumentValueCandidate() {
return anyOf(
classLiteral((tree, state) -> true), (tree, state) -> ASTHelpers.constValue(tree) != null);
Copy link
Member

Choose a reason for hiding this comment

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

Just realized we can write this in a nicer way.

@github-actions
Copy link

  • Surviving mutants in this change: 3
  • Killed mutants in this change: 75
class surviving killed
🧟tech.picnic.errorprone.bugpatterns.JUnitValueSource 2 67
🧟tech.picnic.errorprone.bugpatterns.JUnitValueSource$1 1 0
🎉tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers 0 8

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

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

Yoooooo the quality and epicness of this check 🤯🤯🤯.

Great improvements @Stephan202 , let's get this beast merged and see the impact 😄.

Added a commit to drop a space and will rebase. After that when its 🍏 ==> merge!

@rickie rickie force-pushed the gdejong/PSM-1552 branch from 05ba782 to e03f680 Compare March 2, 2023 09:34
@github-actions
Copy link

github-actions bot commented Mar 2, 2023

  • Surviving mutants in this change: 3
  • Killed mutants in this change: 75
class surviving killed
🧟tech.picnic.errorprone.bugpatterns.JUnitValueSource 2 67
🧟tech.picnic.errorprone.bugpatterns.JUnitValueSource$1 1 0
🎉tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers 0 8

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

1 similar comment
@github-actions
Copy link

github-actions bot commented Mar 2, 2023

  • Surviving mutants in this change: 3
  • Killed mutants in this change: 75
class surviving killed
🧟tech.picnic.errorprone.bugpatterns.JUnitValueSource 2 67
🧟tech.picnic.errorprone.bugpatterns.JUnitValueSource$1 1 0
🎉tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers 0 8

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

@rickie rickie merged commit 2d972fd into master Mar 2, 2023
@rickie rickie deleted the gdejong/PSM-1552 branch March 2, 2023 09:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

3 participants