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

Enhanced Serialization Test Infrastructure #579

Merged
merged 21 commits into from
Feb 25, 2022

Conversation

nimakarimipour
Copy link
Contributor

@nimakarimipour nimakarimipour commented Feb 23, 2022

Following Type Change Serialization PR NullAway can serialize information regarding the source code through the creation of different objects (e.g. ErrorInfo and SuggestedFixInfo).

The current testing infrastructure works as below:

A SerializationTestHelper object is instantiated and the input source code is passed with the API below:

SerializationTestHelper tester = new SerializationTestHelper();
tester.addSourceLines(...)

And the expected FixDisplay is passed to the tester using the API below:

tester.setExpectedFixes(
       new FixDisplay(), new FixDisplay(), ...);

And finally after setting the correct flags, the doTest() method is invoked.

At this moment, SerializationTestHelper instance will only look for fixes.tsv file, and for each line in the file, it creates the corresponding FixDisplay instances, therefore, the expected output can only be in the type of FixDisplay. Also, no test is created to verify correctness of error serialization feature. Since SerializationTestHelper is not flexible to test different output types, it cannot be reused in testing upcoming features of serialization package.

This PR generalizes the SerializationTestHelper class to be reusable with any type of serialization output.

SerializationTestHelper<T>

SerializationTestHelper<FixDisplay> // to test fix displays
SerializationTestHelper<ErrorDisplay> // to test error serialization

With the new implementation, tester instance will look for the target file which is configurable, verifies the correctness of the header and then creates instances of T per line in the output file and executes the tests.

Also tests to cover error serialization is added.

Regards

Copy link
Collaborator

@msridhar msridhar left a comment

Choose a reason for hiding this comment

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

Overall this change looks good. I have some minor comments

values.length == 10,
"Needs exactly 10 values to create FixDisplay object but found: " + values.length);
// Fixes are written in Temp Directory and is not known at compile time, therefore,
// relative
Copy link
Collaborator

Choose a reason for hiding this comment

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

extra newline in 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.

public class SerializationTestHelper {
private FixDisplay[] expectedOutputFixes;
private final Path outputPath;
public class SerializationTestHelper<T> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
public class SerializationTestHelper<T> {
public class SerializationTestHelper<T extends Display> {

See other comment about Display interface

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@nimakarimipour
Copy link
Contributor Author

Hi @msridhar, I believe this is ready for another review.

Copy link
Collaborator

@msridhar msridhar left a comment

Choose a reason for hiding this comment

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

LGTM, just a couple more minor things

private final DisplayFactory<FixDisplay> fixDisplayFactory;
private final DisplayFactory<ErrorDisplay> errorDisplayFactory;

enum Modes {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This enum seems like overkill. I think you could just have four static final fields, two for the filenames and two for the headers, and use those.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

}

private void clearOutput() {
public SerializationTestHelper<T> setOutputFileName(String fileName, String header) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
public SerializationTestHelper<T> setOutputFileName(String fileName, String header) {
public SerializationTestHelper<T> setOutputFileNameAndHeader(String fileName, String header) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@nimakarimipour
Copy link
Contributor Author

@msridhar, resolved the comments. Ready for review.

@msridhar
Copy link
Collaborator

Adding @lazaroclapp in case he wants to review before landing

Copy link
Collaborator

@lazaroclapp lazaroclapp left a comment

Choose a reason for hiding this comment

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

For the most part, this looks good, some comments below!


/**
* Helper class to represent a {@link com.uber.nullaway.fixserialization.out.ErrorInfo} contents in
* {@code String}.
Copy link
Collaborator

Choose a reason for hiding this comment

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

"in String" or "in a test case's (expected or actual) output." ? (Similar fix for FixDispaly, although it wasn't introduced in this PR)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

}
ErrorDisplay that = (ErrorDisplay) o;
return type.equals(that.type)
&& (message.contains(that.message) || that.message.contains(message))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a reason for this double contains test as opposed to .equals(...) between the strings? What am I missing?

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, the reason is that actual complete error messages from NullAway can be really long and may make the tests less readable and may not be the primary intention of the test. For instance, an actual error reported by NullAway is:
initializer method does not guarantee @NonNull field foo (line 4) is initialized along all control-flow paths (remember to check for exceptions or early returns).
Which with this approach it is simply written in the test as initializer method does not guarantee @NonNull field foo.
This is very similar to // BUG: Diagnostic contains: approach. Please let me know if you rather to see the complete error message.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah, makes sense to me. I think I was thinking this was an &&, but it's basically if either message is a substring of the other. Makes sense! Maybe worth a quick comment on that line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

public class SerializationTestHelper<T extends Display> {

private final Path outputDir;
private List<T> expectedOutputs;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Any reason for this being a mutable list rather than ImmutableList?

Note: I get why actualOutputs is mutable, but we don't ever change expectedOutputs (which makes sense), so it probably should be immutable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

.append("\n");
}
fail(errorMessage.toString());
}

private FixDisplay[] readOutputFixes() {
ArrayList<FixDisplay> fixDisplays = new ArrayList<>();
private List<T> readActualOutputs(Path output, String header) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The name of the argument output refers to the file path, the name outputs refers to individual items of type T found in the output of the test case. This is confusing (it is easy, when reading the code below, to fall into the trap of thinking than output is an element of outputs). I'd actually recommend this argument keep the older name of outputPath to distinguish it from the actual outputs.

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, sure. Thank you very much for noting that.

112e88e

}
compilationTestHelper.doTest();
List<T> actualOutputs = readActualOutputs(outputPath, header);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is header passed here, if it's already a field of this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh sorry, it was left from old codes.
c10a7d2

// paths are getting compared.
fixDisplay.uri = fixDisplay.uri.substring(fixDisplay.uri.indexOf("com/uber/"));
fixDisplays.add(fixDisplay);
T outputInstance = factory.fromValuesInString(line.split("\\t"));
Copy link
Collaborator

Choose a reason for hiding this comment

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

If the argument is renamed outputPath, then it would make a lot of sense for this to be T output.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

.doTest();
}

@Test
public void test_custom_annot() {
public void SuggestCustomAnnotTest() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a method, name should start with lowercase.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

.doTest();
}

@Test
public void test_method_param_protection_test() {
public void ExamineMethodParamProtectionTest() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

(See comment above: lowercase first letter)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

private String configPath;
private Path root;
Copy link
Collaborator

Choose a reason for hiding this comment

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

@msridhar The output files these test read are controlled by the path passed when constructing the SerializationTestHelper<> object plus the file passed to .expectedOutputs(...). For most of these tests, that is ${this.root}/${this.SUGGEST_FIX_FILE_NAME}.

That means these tests can't ever run in parallel. Just out of curiosity, is there any guarantee for gradle/JUnit such as: different test classes run in parallel, but test cases within the same @RunWith annotated class run serially?

That would make sense to me, and would mean this is correct. But I can't find no such guarantee in the gradle parallel testing docs, just a warning that:

When using parallel test execution, make sure your tests are properly isolated from one another. Tests that interact with the filesystem are particularly prone to conflict, causing intermittent test failures.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@lazaroclapp AFAIK Gradle will not run test methods within the same test class in parallel with each other. It only runs test methods from different test classes in parallel. This could cause a problem down the road if we split this test into multiple tests I guess. Could we fix by using a unique temp (sub-)directory root in every test?

Copy link
Collaborator

Choose a reason for hiding this comment

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

This would work even if we split the test into multiple classes, because each such class will likely have its own private Path root instance. My only worry here is if there is some aggressively parallel JUnit+Gradle mode that runs a unit tests within the same test class in parallel, if you don't think that's likely (and it would break a lot of setUp(...) methods I've seen), then this is probably fine as it is.

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 am assuming the latest decision is to leave this as it is. In that case this is ready for another round of review.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Looking more, I think the way @Rule works, the temporary folder should already get created fresh for each test and deleted afterward. Due to how these things work, I don't think any test runner would ever attempt to run tests within a class in parallel. So I think this code is fine as-is. That said, I did find another change we should make that I will comment on separately.

@@ -59,12 +59,12 @@ public SerializationTestHelper(Path outputDir) {

@SafeVarargs
public final SerializationTestHelper<T> setExpectedOutputs(T... outputs) {
this.expectedOutputs = Arrays.asList(outputs);
this.expectedOutputs = ImmutableList.copyOf(Arrays.asList(outputs));
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can directly pass the outputs array to copyOf(...). There is a version of that method that takes an array and this avoids the double list construction.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, great. Looks much better.

aaf7d47

@@ -70,8 +102,9 @@ public void cleanup() {
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This cleanup() method should be deleted. JUnit will already take care of deleting the temporaryFolder after each test.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh great, Deleted it.

7587be4

Copy link
Collaborator

@lazaroclapp lazaroclapp left a comment

Choose a reason for hiding this comment

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

LGTM!

@msridhar msridhar merged commit 743e2b6 into uber:master Feb 25, 2022
@nimakarimipour nimakarimipour deleted the serialization_test branch March 21, 2022 23:56
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.

3 participants