Skip to content

Run formatter test cases from files under test-cases/ - #160

Merged
abashev merged 9 commits into
21.X.Xfrom
test/formatter-cases
Oct 28, 2026
Merged

abashev merged 9 commits into
21.X.Xfrom
test/formatter-cases

Conversation

@abashev

@abashev abashev commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Adding a formatter test meant a Java method with two text blocks and a formatter around them, or an .input/.output pair in testdata/. Now it can be one file in open-java-format/src/test/resources/test-cases/. In order:

  1. the suite, with one test moved into it;
  2. every before-and-after test of FormatterTest and JBangDirectivesTest that needs no settings;
  3. reading the cases byte for byte;
  4. the testdata/ pairs that need no settings, each as an LF, a CRLF and a CR case;
  5. a fix that keeps two pairs other modules read;
  6. the pairs whose input has no final line break, which the second dashes can now follow directly;
  7. 13 pairs of StringWrapperIntegrationTest, lengthened to cross 120 columns;
  8. a fix in the string wrapper that their move turned up, with the other five pairs; it changes what the formatter outputs, see below;
  9. five more testdata/ pairs, with copies for the modules that read two of them.

The case file

A header, six or more dashes that end a line, the code before formatting, a second run of such dashes and the code expected after it. The header may be empty, and then the file starts with the dashes. There are no settings yet, so the header takes blank lines and # comments, which explain the case; any other line fails the case.

# Why this case exists.
----------
class A{}
----------
class A {}

The file is read as it is. The before block runs from the line after the first dashes up to the second ones. On a line of their own they leave it its last line break; at the end of its last line they leave it none, for an input that does not end in one:

----------
class MyClass {
    private void myFile() {}
}----------
class MyClass {
    private void myFile() {}
}

The after block runs from the line after the second dashes to the end of the file, with a final line break or without. Line breaks stay \n, \r\n or \r, whichever the file has, and .gitattributes keeps git from converting them in *.case files. A line of code that ends in six or more dashes would be a separator itself, so it cannot be part of a case; none is, and such a file fails with three separators found.

The suite

FormatterCasesTest finds the .case files anywhere under test-cases/ on the test classpath and runs one test per file, named by its path without the extension. The before block through formatSourceAndFixImports in the OJF style must give the after block, and a second run must change nothing. A failure first names the line of the case file where the output departs from the after block, with both versions of that line, and then shows both texts:

[test-cases/nonNlsMarkersStayOnTheLineOfTheirLiteral.case, first run, line 25
  expected:                 "third one"); //$NON-NLS-6$
  actual:                   "third one"); //$NON-NLS-5$]

The output has to match the after block byte for byte. A file without exactly two lines of dashes fails its own test, and a missing or empty folder fails the suite rather than passing with no tests.

The tests moved into cases

The first commit moves nonNlsMarkersStayOnTheLineOfTheirLiteral. The second moves every test that formatted a string and compared the result with another string in the code, one file per test and one per row of a parameterized test: 23 tests of FormatterTest and all 40 of JBangDirectivesTest, which goes; its cases are test-cases/jbang/<method>-<row>.case. A one-off generator read the strings from the test sources through javac's tree, so none was retyped, and the comments of the tests became the # headers of their cases.

The 19 of them that formatted in the GOOGLE style now run in OJF, the one style the formatter has. Their inputs are unchanged and each expected output is the OJF one; for all but two it equals the old one with every indent doubled, so the test checks the same thing. The two that wrap at the line limit were checked by hand:

  • wrapMarkdownDocstringComment fitted in 120 columns, so its sentence got three more words and wraps again.
  • wrapMarkdownDocstringRunKeepsTheSlashPrefix now wraps right before @Deprecated. On JDK 25 a formatter from before the fix for Code won't compile after Markdown Javadoc is formatted palantir/palantir-java-format#1803 drops the /// there and makes it an annotation, so the case still catches the bug. It was skipped below JDK 23 and now runs on every JDK; every case has the same output on JDK 21, 25 and 27.

wrapLineComment stays in FormatterTest: its output, in GOOGLE as in OJF, changes on a second run, which the cases check. That is a formatter bug, to be fixed separately. It and the seven other tests that stay in FormatterTest but formatted in GOOGLE use an OJF formatter now; only wrapLineComment and blankLinesImportComment change their expected text.

The testdata pairs

FormatterIntegrationTest checked each pair four times: the formatted input against the output, then the output with LF, CR and CRLF line breaks against itself. 254 of the 256 pairs become three cases each, testdata/<name>-lf.case, -crlf.case and -cr.case, with those line breaks. A case formats its before block and has to give the after block, then formats that again and has to change nothing, so the CR and CRLF copies check a whole run with their line breaks, not only the output.

The 28 among them whose input does not end in a line break, JavaFileNoNewLine too, put the second dashes right after the input's last character. A case also runs the import pass, which FormatterIntegrationTest skipped, and for three pairs the after block differs from the old output: M loses the blank line between its two imports, Unformatted gets its imports sorted into one block, and ojf-issue-24-jbang-script gets a blank line between the comment after its last import and the annotated class. The cases of the other 251 pairs hold the pair byte for byte.

Other modules read two of the pairs and now keep their own copies: PalantirDeeplyNestedCalls.java of the benchmarks is a file instead of a link to palantir-deeply-nested-calls.input, and the IntelliJ plugin's PalantirJavaFormatFormattingServiceTest reads A.input and A.output from its own test resources.

Two pairs stay in testdata/: ModuleImport needs JDK 23, and the import pass of a case would remove the unused import of B21647014 before formatting, and one of the stray semicolons after it with it. FileBasedTests.VERSIONED_TESTS keeps only ModuleImport.

The string wrapper pairs

StringWrapperIntegrationTest reflowed strings at 40 columns, a setting a case does not have: a case wraps at 120, as the CLI does. Its 18 pairs become test-cases/stringwrapper/<n>-lf.case, -crlf.case and -cr.case, each with a # line on what it checks, and the test goes. The inputs keep their old shape and get longer, so that each case does at 120 columns what its pair did at 40: the sentence most of them share has the same two insertions everywhere, the runs of letters of 13 and 14 are three times as long, and 6, 7, 9, 10 and 16 cross 120 columns as they are. The releases from before the fixes behind 16 and the NON-NLS pair, 2.98.0.1 and 2.98.0.3, fail those cases. The outputs are the same on JDK 21, 25 and 27.

A fix in the string wrapper

Lengthened, pairs 1, 2, 5, 8 and 11 came out with the pieces of their string four columns in, where OJF continues a line eight columns in:

class T {
    String s = "one long incredibly unbroken sentence that went on through breakfast and lunch and dinner and long"
        + " into the night moving from topic to topic so that no-one sitting anywhere near the long dining table, not"
        + " even the host or the cook, had a chance to interrupt";
}

The wrapper laid the pieces out four columns in. Its second formatting pass moved them to eight, and when that pushed a piece past the limit, it reflowed the text again and left it unformatted. A plain reformat of such output moved the lines, while running the formatter again gave the same output, so neither the old test nor the second run of a case noticed; release 2.98.0.1 does the same. The reflow now takes the continuation indent from the formatter, four columns times the multiplier of its style, for the width of the lines after the first and for the column of their +:

class T {
    String s = "one long incredibly unbroken sentence that went on through breakfast and lunch and dinner and long"
            + " into the night moving from topic to topic so that no-one sitting anywhere near the long dining table,"
            + " not even the host or the cook, had a chance to interrupt";
}

Without the fix 21 of the 54 cases fail: those five pairs and 0 and 3, whose continuation line of exactly 120 columns now ends a word earlier, because the reflow fills a continuation line up to 119 columns. A plain reformat leaves the after block of every case as it is. Of the 15,747 files of the JDK 21 sources, 319 format differently: 292 whose old output a plain reformat changed, and 27 with a continuation line of exactly 120 columns.

Test counts on JDK 21

before after
FormatterTest 43 19
JBangDirectivesTest 40 –
StringWrapperIntegrationTest 72 –
FormatterIntegrationTest 1024 8
FormatterCasesTest – 880
CaseFileTest – 16
module 1658 1402

Every other class is unchanged. Each moved test of FormatterTest and JBangDirectivesTest is one case, and each moved pair is its three cases, where the integration tests counted four checks; that is why the module has fewer tests. 14 tests are skipped instead of 15, because the slash-prefix case runs below JDK 23 now. ./gradlew test --rerun passes in every module.

Next

The fix for the unstable comment wrap, with wrapLineComment moved into a case; the two pairs left in testdata/; and the other classes that still format in GOOGLE, with Formatter.create() defaulting to OJF.

A formatter test was a Java method with two text blocks and a formatter
around them, or an .input/.output pair in testdata/. Now it can be one
file: src/test/resources/test-cases/<name>.case holds the code before
formatting and the code expected after it, separated by a line of six or
more dashes. A file with two such lines starts with a settings block.
There are no settings yet, so that block takes blank lines and # comments,
which explain the case; any other line fails the case.

FormatterCasesTest finds the .case files anywhere under test-cases/ on
the test classpath and runs one test per file, named by its path without
the extension. The before block through formatSourceAndFixImports in the
OJF style must give the after block, and a second run must change
nothing. A failure first names the line of the case file where the output
departs from the after block, with what the case expects there and what
the formatter wrote, and then shows both texts. A file with no separator
or more than two fails its own test, and a missing or empty folder fails
the suite rather than passing with no tests. CaseFile splits the file
with String.lines(), so both blocks end their lines in \n whatever the
file uses.

nonNlsMarkersStayOnTheLineOfTheirLiteral moves from FormatterTest to the
first case. Its blocks were generated from the method's text blocks and
are byte for byte the strings it compared; its comment became the header.
Changing one character of the expected output fails the case at its line.

Tests on JDK 21: FormatterTest 43 -> 42, FormatterCasesTest 0 -> 1 under
the same name, CaseFileTest 0 -> 12, every other class unchanged; the
module goes from 1658 to 1670 tests, 15 skipped before and after.
…est into test-cases/

Every test that formatted a string and compared the result with a second
string in the code becomes a case file, one per test and one per row of
a parameterized test: 23 tests of FormatterTest and all 40 of
JBangDirectivesTest, which goes. A one-off generator read the strings
from the test sources through javac's tree, so none was retyped, and
the comments of the tests became the # headers of their cases.

The 19 of them that formatted in the GOOGLE style now run in OJF, the
one style the formatter has. Their inputs are unchanged and each
expected output is the OJF one; for all but two it equals the old one
with every indent doubled, so the test checks the same thing. The two
that wrap at the line limit were checked by hand.
wrapMarkdownDocstringComment fitted in 120 columns, so its sentence got
three more words and wraps again.
wrapMarkdownDocstringRunKeepsTheSlashPrefix now wraps right before
@deprecated; on JDK 25 a formatter from before the fix for
palantir#1803 drops the /// there, so the case
still catches it. It was skipped below JDK 23 and now runs on every
JDK, with the same output on 21, 25 and 27, as all the cases.

wrapLineComment stays in FormatterTest: its output, in GOOGLE as in OJF,
changes on a second run, which the cases check. It and the seven other
tests that stay in FormatterTest but formatted in GOOGLE use an OJF
formatter now; only wrapLineComment and blankLinesImportComment change
their expected text.

Tests on JDK 21: FormatterTest 42 -> 19, JBangDirectivesTest 40 -> 0,
FormatterCasesTest 1 -> 64, every other class unchanged; 1670 tests
before and after, 14 skipped instead of 15. Each of the 63 old test
names maps to one case.
A case went through String.lines() and came back with \n after every
line, so a file written with \r\n or \r tested the same thing as one
with \n, and a line break was added where a file had none at its end.
CaseFile now cuts the blocks out of the text as it is: the before block
runs from the line after the first line of dashes to the second, its
last line break included, and the after block from the line after the
second to the end of the file, and the formatter's output has to match
it byte for byte. Every case therefore has exactly two lines of dashes,
and one without a header starts with them: the 15 cases that started
with their code got the leading line, the other 49 had a header already.

.gitattributes keeps git from converting the line breaks of *.case
files, which the comparison now sees.

Tests on JDK 21 and 25: CaseFileTest 12 -> 14, the 64 cases unchanged
and passing, 1672 tests in the module. Copies of a case with \r\n and
with \r pass as well.
…LF, CRLF and CR copies

FormatterIntegrationTest checked each pair four times: the formatted
input against the output, then the output with LF, CR and CRLF line
breaks against itself. 223 of the 256 pairs become three cases each,
testdata/<name>-lf, -crlf and -cr, the pair byte for byte with those
line breaks. A case formats its before block and has to give the after
block, then formats that again and has to change nothing, so the CR and
CRLF copies check a whole run with their line breaks, not only the
output.

33 pairs stay. ModuleImport needs JDK 23. Under formatSourceAndFixImports
B21647014 loses an unused import, Unformatted gets its imports sorted,
and M and ojf-issue-24-jbang-script change the blank lines around them.
And 28 inputs, JavaFileNoNewLine among them, do not end in a line break,
which a case cannot hold: its before block ends where the second line of
dashes starts. FileBasedTests.VERSIONED_TESTS keeps the entries of the
pairs that stay.

Tests on JDK 21: FormatterIntegrationTest 1024 -> 132, FormatterCasesTest
64 -> 733, every other class unchanged; the module goes from 1672 to
1449 tests, as four checks per pair become three cases.
The benchmarks link src/test/resources/PalantirDeeplyNestedCalls.java to
palantir-deeply-nested-calls.input, and the IntelliJ plugin's
PalantirJavaFormatFormattingServiceTest reads A.input and A.output by
path. Moving both pairs into cases broke the build: CI failed in
:open-java-format-benchmarks:processTestResources on the dangling link.
The two pairs go back to testdata/ and their six cases go. Level.java
pointed at palantir-expression-lambdas.input, which moved; its comment
now names the cases.

Tests on JDK 21: FormatterIntegrationTest 132 -> 140, FormatterCasesTest
733 -> 727, 1451 tests in the module; ./gradlew test --rerun passes in
every module.
…ashes ending the input

A case could not hold an input that does not end in a line break: its
before block ran up to a line of dashes, so it always ended in one. Now
six or more dashes are a separator wherever they end a line, so the
before block can stop right before them, as in a closing brace followed
by the dashes, and keeps no line break of its own. The price is that a
line of code that ends in six or more dashes cannot be part of a case.
None of the cases or pairs has one, and such a file fails with three
separators found rather than quietly.

The 28 testdata pairs whose input has no final line break, among them
JavaFileNoNewLine, become their LF, CRLF and CR cases like the other
pairs, and FileBasedTests.VERSIONED_TESTS keeps only ModuleImport.
testdata/ now holds the seven pairs that need a setting or that other
modules read.

Tests on JDK 21: CaseFileTest 14 -> 16, FormatterIntegrationTest
140 -> 28, FormatterCasesTest 727 -> 811; 1425 tests in the module.
…cross 120 columns

StringWrapperIntegrationTest reflowed strings at 40 columns in the
GOOGLE style, a setting a case does not have: a case runs OJF, whose
strings wrap at 120. Run that way, 11 of the 15 pairs that reflow at
40 columns reflowed nothing, the chain of 14 fit on one line, and the
literal of 15 no longer reached the limit.

Thirteen pairs become their LF, CRLF and CR cases in
test-cases/stringwrapper/, each with a line on what it checks. Eight
of them get longer inputs in their old shape: the sentence has the
same two insertions everywhere, after "sentence" and after "no-one",
and the runs of letters of 13 and 14 are three times as long, as the
limit is. 6, 7, 9, 10 and 16 cross 120 columns as they are. Each case
does at 120 columns what its pair did at 40, and the formatters from
before the fixes behind 16 and the NON-NLS pair, 2.98.0.1 and
2.98.0.3, fail them. The outputs are the same on JDK 21, 25 and 27.

Pairs 1, 2, 5, 8 and 11 stay in StringWrapperIntegrationTest for now.
Lengthened the same way, they come out with continuation lines four
columns in instead of the eight of OJF, which a plain reformat moves;
the second run of a case wraps again and does not notice. That is a
formatter bug, already in 2.98.0.1; the next commit fixes it and moves
those five.

Tests on JDK 21: StringWrapperIntegrationTest 72 -> 20,
FormatterCasesTest 811 -> 850; 1412 tests in the module.
The pieces of a reflowed string were laid out four columns in, while
OJF continues a line eight columns in. The second formatting pass of
the wrapper moved them to eight; when that pushed a piece past the
limit, the wrapper reflowed the text again and left it unformatted,
four columns in. A plain reformat of such output moved the lines,
while running the formatter again gave the same output, so nothing
noticed.

The reflow now takes the continuation indent from the formatter, four
columns times the multiplier of its style, for the width of the lines
after the first and for the column of their +.

Pairs 1, 2, 5, 8 and 11 of StringWrapperIntegrationTest, lengthened
like the others, become their LF, CRLF and CR cases. They fail without
the fix, and so do 0 and 3, whose continuation line of exactly 120
columns now ends a word earlier: the reflow fills a continuation line
up to 119 columns. StringWrapperIntegrationTest and its folder go. A
plain reformat leaves the expected output of every case as it is.

Of the 15,747 files of the JDK 21 sources, 319 format differently: 292
whose old output a plain reformat changed, and 27 with a continuation
line of exactly 120 columns.

Tests on JDK 21: FormatterCasesTest 850 -> 865,
StringWrapperIntegrationTest 20 -> 0; 1407 tests in the module.
… that read two of them

A and palantir-deeply-nested-calls stayed in testdata/ because other
modules read them. Those modules now keep their own copies: the
IntelliJ plugin test reads A.input and A.output from its own test
resources, and PalantirDeeplyNestedCalls.java of the benchmarks is a
file instead of a symlink into testdata/. Both pairs become their LF,
CRLF and CR cases byte for byte.

M, Unformatted and ojf-issue-24-jbang-script become cases whose after
block also takes the import pass, which a case runs and
FormatterIntegrationTest skipped: M loses the blank line between its
two imports, Unformatted gets its imports sorted into one block, and
the JBang script gets a blank line between the comment after its last
import and the annotated class. The rest of each output is as before.

testdata/ keeps B21647014 and ModuleImport. The import pass would
remove the unused import of B21647014 before formatting, and one of
the stray semicolons after it with it; ModuleImport needs JDK 23.

Tests on JDK 21: FormatterIntegrationTest 28 -> 8, FormatterCasesTest
865 -> 880; 1402 tests in the module.
@abashev
abashev merged commit bebecbb into 21.X.X Oct 28, 2026
17 checks passed
@abashev
abashev deleted the test/formatter-cases branch October 28, 2026 06:57
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.

1 participant