Skip to content

Commit 9f31aa4

Browse files
committed
GH-356: Write tests for JctCompilationFactoryImpl
- Add test pack for JctCompilationFactoryImpl. - Add new helper test class for Mockito matchers used within collection-like types. - Add a new method TeeWriter#getContent that gets the lines of content within a TeeWriter, as this is easier to stub than #toString due to how Mockito works internally. - Add a new method TeeWriter#wrapOutputStream that wraps a given OutputStream and Charset in an OutputStreamWriter and inserts that into a new TeeWriter that is returned. - Fix regression in JctCompilationFactoryImpl where the locale was not being set during compilations. - Fix bug in JctCompilationFactory where the TeeWriter was not being flushed, so buffered content may not have been written to System.out properly in some edge cases. - Improve performance of JctCompilationFactoryImpl for explicit class name lookup by avoiding creating a distinct collection that is iteratively resized with O(n) space complexity.
1 parent 3750e34 commit 9f31aa4

File tree

5 files changed

+907
-29
lines changed

5 files changed

+907
-29
lines changed

java-compiler-testing/src/main/java/io/github/ascopes/jct/compilers/impl/JctCompilationFactoryImpl.java

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
package io.github.ascopes.jct.compilers.impl;
1717

1818
import static java.util.Objects.requireNonNull;
19-
import static java.util.function.Function.identity;
2019
import static java.util.stream.Collectors.toList;
2120

2221
import io.github.ascopes.jct.compilers.JctCompilation;
@@ -30,7 +29,6 @@
3029
import io.github.ascopes.jct.filemanagers.PathFileObject;
3130
import io.github.ascopes.jct.utils.IterableUtils;
3231
import java.io.IOException;
33-
import java.io.OutputStreamWriter;
3432
import java.util.Collection;
3533
import java.util.HashSet;
3634
import java.util.List;
@@ -73,9 +71,11 @@ public JctCompilation createCompilation(
7371
) {
7472
try {
7573
return createCheckedCompilation(flags, fileManager, jsr199Compiler, classNames);
74+
7675
} catch (JctCompilerException ex) {
77-
// Fall through, do not rewrap these.
76+
// Rethrow JctCompilerExceptions -- we don't want to wrap these again.
7877
throw ex;
78+
7979
} catch (Exception ex) {
8080
throw new JctCompilerException(
8181
"Failed to perform compilation, an unexpected exception was raised", ex
@@ -92,13 +92,11 @@ private JctCompilation createCheckedCompilation(
9292
var compilationUnits = findFilteredCompilationUnits(fileManager, classNames);
9393

9494
if (compilationUnits.isEmpty()) {
95-
throw classNames == null
96-
? new JctCompilerException("No compilation units were found in the given workspace")
97-
: new JctCompilerException("No compilation units were found for the given class names");
95+
throw new JctCompilerException("No compilation units were found in the given workspace");
9896
}
9997

10098
// Do not close stdout, it breaks test engines, especially IntellIJ.
101-
var writer = new TeeWriter(new OutputStreamWriter(System.out, compiler.getLogCharset()));
99+
var writer = TeeWriter.wrapOutputStream(System.out, compiler.getLogCharset());
102100

103101
var diagnosticListener = new TracingDiagnosticListener<>(
104102
compiler.getDiagnosticLoggingMode() != LoggingMode.DISABLED,
@@ -119,6 +117,8 @@ private JctCompilation createCheckedCompilation(
119117
task.setProcessors(processors);
120118
}
121119

120+
task.setLocale(compiler.getLocale());
121+
122122
LOGGER
123123
.atInfo()
124124
.setMessage("Starting compilation with {} (found {} compilation units)")
@@ -133,6 +133,9 @@ private JctCompilation createCheckedCompilation(
133133
);
134134
var delta = (System.nanoTime() - start) / 1_000_000L;
135135

136+
// Ensure we commit the writer contents to the wrapped output stream in full.
137+
writer.flush();
138+
136139
LOGGER
137140
.atInfo()
138141
.setMessage("Compilation with {} {} after approximately {}ms (roughly {} classes/sec)")
@@ -149,16 +152,16 @@ private JctCompilation createCheckedCompilation(
149152

150153
return JctCompilationImpl
151154
.builder()
152-
.compilationUnits(compilationUnits)
155+
.compilationUnits(Set.copyOf(compilationUnits))
153156
.fileManager(fileManager)
154-
.outputLines(writer.toString().lines().collect(toList()))
157+
.outputLines(writer.getContent().lines().collect(toList()))
155158
.diagnostics(diagnosticListener.getDiagnostics())
156159
.success(success)
157160
.failOnWarnings(compiler.isFailOnWarnings())
158161
.build();
159162
}
160163

161-
private Set<JavaFileObject> findFilteredCompilationUnits(
164+
private Collection<JavaFileObject> findFilteredCompilationUnits(
162165
JctFileManager fileManager,
163166
@Nullable Collection<String> classNames
164167
) throws IOException {
@@ -175,7 +178,9 @@ private Set<JavaFileObject> findFilteredCompilationUnits(
175178
return filterCompilationUnitsByBinaryNames(compilationUnits, classNames);
176179
}
177180

178-
private Set<JavaFileObject> findCompilationUnits(JctFileManager fileManager) throws IOException {
181+
private Collection<JavaFileObject> findCompilationUnits(
182+
JctFileManager fileManager
183+
) throws IOException {
179184
var locations = IterableUtils
180185
.flatten(fileManager.listLocationsForModules(StandardLocation.MODULE_SOURCE_PATH));
181186

@@ -199,8 +204,8 @@ private Set<JavaFileObject> findCompilationUnits(JctFileManager fileManager) thr
199204
return objects;
200205
}
201206

202-
private Set<JavaFileObject> filterCompilationUnitsByBinaryNames(
203-
Set<JavaFileObject> compilationUnits,
207+
private Collection<JavaFileObject> filterCompilationUnitsByBinaryNames(
208+
Collection<JavaFileObject> compilationUnits,
204209
Collection<String> classNames
205210
) {
206211
var binaryNamesToCompilationUnits = compilationUnits
@@ -209,30 +214,27 @@ private Set<JavaFileObject> filterCompilationUnitsByBinaryNames(
209214
// care too much as the implementation should conform to this anyway. We just cannot enforce
210215
// it due to covariance rules.
211216
.map(PathFileObject.class::cast)
217+
.filter(fo -> classNames.contains(fo.getBinaryName()))
212218
.collect(Collectors.toMap(
213219
PathFileObject::getBinaryName,
214-
identity()
220+
JavaFileObject.class::cast
215221
));
216222

217-
var filteredCompilationUnits = new HashSet<JavaFileObject>();
218-
219223
for (var className : classNames) {
220224
var compilationUnit = binaryNamesToCompilationUnits.get(className);
221225
if (compilationUnit == null) {
222226
throw new JctCompilerException(
223227
"No compilation unit matching " + className + " found in the provided sources"
224228
);
225229
}
226-
227-
filteredCompilationUnits.add(compilationUnit);
228230
}
229231

230232
LOGGER.atDebug()
231233
.setMessage("Filtered {} candidate compilation units down to {} final compilation units")
232234
.addArgument(compilationUnits::size)
233-
.addArgument(filteredCompilationUnits::size)
235+
.addArgument(binaryNamesToCompilationUnits::size)
234236
.log();
235237

236-
return filteredCompilationUnits;
238+
return binaryNamesToCompilationUnits.values();
237239
}
238240
}

java-compiler-testing/src/main/java/io/github/ascopes/jct/diagnostics/TeeWriter.java

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616
package io.github.ascopes.jct.diagnostics;
1717

1818
import static java.util.Objects.requireNonNull;
19+
import static java.util.Objects.requireNonNullElse;
1920

2021
import java.io.IOException;
22+
import java.io.OutputStream;
23+
import java.io.OutputStreamWriter;
2124
import java.io.Writer;
25+
import java.nio.charset.Charset;
2226
import org.apiguardian.api.API;
2327
import org.apiguardian.api.API.Status;
2428

@@ -80,13 +84,31 @@ public void flush() throws IOException {
8084
}
8185
}
8286

83-
@Override
84-
public String toString() {
87+
/**
88+
* Get the content of the internal buffer.
89+
*
90+
* @return the content.
91+
* @since 0.2.1
92+
*/
93+
@API(since = "0.2.1", status = Status.STABLE)
94+
public String getContent() {
8595
synchronized (lock) {
8696
return builder.toString();
8797
}
8898
}
8999

100+
/**
101+
* Get the content of the internal buffer.
102+
*
103+
* <p>This calls {@link #getContent()} internally as of 0.2.1.
104+
*
105+
* @return the content.
106+
*/
107+
@Override
108+
public String toString() {
109+
return getContent();
110+
}
111+
90112
@Override
91113
public void write(char[] cbuf, int off, int len) throws IOException {
92114
synchronized (lock) {
@@ -104,4 +126,23 @@ private void ensureOpen() {
104126
throw new IllegalStateException("TeeWriter is closed");
105127
}
106128
}
129+
130+
/**
131+
* Create a tee writer for the given output stream.
132+
*
133+
* <p>Remember you may need to manually flush the tee writer for all contents to be committed to
134+
* the output stream.
135+
*
136+
* @param outputStream the output stream.
137+
* @param charset the charset.
138+
* @return the Tee Writer.
139+
* @since 0.2.1
140+
*/
141+
@API(since = "0.2.1", status = Status.STABLE)
142+
public static TeeWriter wrapOutputStream(OutputStream outputStream, Charset charset) {
143+
requireNonNull(outputStream, "outputStream");
144+
requireNonNull(charset, "charset");
145+
var writer = new OutputStreamWriter(outputStream, charset);
146+
return new TeeWriter(writer);
147+
}
107148
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright (C) 2022 - 2023, the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.github.ascopes.jct.tests.helpers;
17+
18+
import static org.mockito.ArgumentMatchers.argThat;
19+
20+
import java.util.ArrayList;
21+
import java.util.Arrays;
22+
import java.util.Collection;
23+
import java.util.Set;
24+
import org.mockito.ArgumentMatcher;
25+
26+
/**
27+
* Extra Mockito argument matchers.
28+
*
29+
* @author Ashley Scopes
30+
*/
31+
public final class ExtraArgumentMatchers {
32+
33+
private ExtraArgumentMatchers() {
34+
throw new UnsupportedOperationException("static-only class");
35+
}
36+
37+
@SafeVarargs
38+
public static <E, T extends Iterable<E>> T containsExactlyElements(E... expected) {
39+
return containsExactlyElements(Set.of(expected));
40+
}
41+
42+
public static <E, T extends Iterable<E>> T containsExactlyElements(Collection<E> expected) {
43+
return argThat(new ArgumentMatcher<>() {
44+
@Override
45+
public String toString() {
46+
return "containsExactlyElements(" + expected + ")";
47+
}
48+
49+
@Override
50+
public boolean matches(T actualIterable) {
51+
if (actualIterable == null) {
52+
return false;
53+
}
54+
55+
var actualElements = new ArrayList<E>();
56+
actualIterable.forEach(actualElements::add);
57+
58+
// All expected are in actual
59+
for (var expectedElement : expected) {
60+
if (!actualElements.contains(expectedElement)) {
61+
throw new IllegalArgumentException(
62+
"Expected element " + expectedElement + " was not in the actual collection"
63+
);
64+
}
65+
}
66+
67+
// All actual are in expected.
68+
var expectedSet = Set.copyOf(expected);
69+
for (var actualElement : actualElements) {
70+
if (!expectedSet.contains(actualElement)) {
71+
throw new IllegalArgumentException(
72+
"Actual element " + actualElement + " was not in the expected elements array"
73+
);
74+
}
75+
}
76+
77+
return true;
78+
}
79+
});
80+
}
81+
}

0 commit comments

Comments
 (0)