22
22
import com .google .common .collect .ImmutableMap ;
23
23
import com .google .common .collect .ImmutableSet ;
24
24
import com .google .common .util .concurrent .ThreadFactoryBuilder ;
25
+ import org .junit .jupiter .api .TestInstance ;
25
26
import org .junit .jupiter .api .extension .AfterAllCallback ;
27
+ import org .junit .jupiter .api .extension .AfterEachCallback ;
26
28
import org .junit .jupiter .api .extension .BeforeAllCallback ;
27
29
import org .junit .jupiter .api .extension .BeforeEachCallback ;
28
30
import org .junit .jupiter .api .extension .ExtendWith ;
34
36
import java .util .Map ;
35
37
import java .util .Set ;
36
38
import java .util .concurrent .CompletableFuture ;
37
- import java .util .concurrent .CompletionStage ;
38
- import java .util .concurrent .ExecutorService ;
39
+ import java .util .concurrent .ExecutionException ;
40
+ import java .util .concurrent .Executor ;
39
41
import java .util .concurrent .Executors ;
40
42
import java .util .concurrent .Phaser ;
41
43
import java .util .concurrent .TimeUnit ;
44
+ import java .util .concurrent .TimeoutException ;
42
45
import java .util .concurrent .atomic .AtomicReference ;
43
46
import java .util .function .Function ;
47
+ import java .util .function .Supplier ;
44
48
import javax .annotation .processing .AbstractProcessor ;
45
49
import javax .annotation .processing .ProcessingEnvironment ;
46
50
import javax .annotation .processing .RoundEnvironment ;
67
71
*
68
72
* @author David van Leusen
69
73
*/
70
- public class CompilationExtension
71
- implements BeforeAllCallback , BeforeEachCallback , AfterAllCallback , ParameterResolver {
74
+ public class CompilationExtension implements BeforeAllCallback , BeforeEachCallback ,
75
+ AfterAllCallback , AfterEachCallback , ParameterResolver {
72
76
private static final JavaFileObject DUMMY =
73
77
JavaFileObjects .forSourceLines ("Dummy" , "final class Dummy {}" );
74
78
private static final ExtensionContext .Namespace NAMESPACE =
75
79
ExtensionContext .Namespace .create (CompilationExtension .class );
76
80
77
- private static final StoreAccessor <Phaser > PHASER_KEY = new StoreAccessor <>(Phaser .class );
78
- private static final StoreAccessor <AtomicReference <ProcessingEnvironment >> PROCESSINGENV_KEY =
79
- new StoreAccessor <>(ProcessingEnvironment .class );
80
- private static final StoreAccessor <CompletionStage <Compilation >> RESULT_KEY =
81
- new StoreAccessor <>(Compilation .class );
82
-
83
- private static final ExecutorService COMPILER_EXECUTOR = Executors .newCachedThreadPool (
81
+ private static final Executor DEFAULT_COMPILER_EXECUTOR = Executors .newCachedThreadPool (
84
82
new ThreadFactoryBuilder ().setDaemon (true ).setNameFormat ("async-compiler-%d" ).build ()
85
83
);
86
84
@@ -93,68 +91,63 @@ public class CompilationExtension
93
91
.build ();
94
92
}
95
93
94
+ private final Executor compilerExecutor ;
95
+
96
+ public CompilationExtension (Executor compilerExecutor ) {
97
+ this .compilerExecutor = compilerExecutor ;
98
+ }
99
+
100
+ public CompilationExtension () {
101
+ this (DEFAULT_COMPILER_EXECUTOR );
102
+ }
103
+
96
104
@ Override
97
- public void beforeAll (ExtensionContext context ) throws Exception {
98
- final Phaser sharedBarrier = new Phaser (2 ) {
99
- @ Override
100
- protected boolean onAdvance (int phase , int parties ) {
101
- // Terminate the phaser once all parties have deregistered
102
- return parties == 0 ;
103
- }
104
- };
105
-
106
- final AtomicReference <ProcessingEnvironment > sharedState
107
- = new AtomicReference <>(null );
108
-
109
- final CompletionStage <Compilation > futureResult = CompletableFuture .supplyAsync (
110
- () -> {
111
- try {
112
- return Compiler .javac ()
113
- .withProcessors (new EvaluatingProcessor (sharedBarrier , sharedState ))
114
- .compile (DUMMY );
115
- } finally {
116
- sharedBarrier .forceTermination ();
117
- }
118
- }, COMPILER_EXECUTOR );
119
-
120
- final ExtensionContext .Store store = context .getStore (NAMESPACE );
121
- PHASER_KEY .put (store , sharedBarrier );
122
- PROCESSINGENV_KEY .put (store , sharedState );
123
- RESULT_KEY .put (store , futureResult );
124
-
125
- // Wait until the processor is ready for testing, handle termination on error
126
- if (sharedBarrier .arriveAndAwaitAdvance () < 0 ) {
127
- // Rethrow the exception thrown by the compiler, otherwise throw based on the result.
128
- final Compilation result = futureResult .toCompletableFuture ()
129
- .get (5 , TimeUnit .SECONDS );
130
- throw new IllegalStateException (result .toString ());
131
- }
105
+ public void beforeAll (ExtensionContext context ) throws InterruptedException {
106
+ final CompilerState state = context .getStore (NAMESPACE ).getOrComputeIfAbsent (
107
+ CompilerState .class ,
108
+ ignored -> new CompilerState (this .compilerExecutor , TestInstance .Lifecycle .PER_CLASS ),
109
+ CompilerState .class
110
+ );
111
+
112
+ checkState (state .prepareForTests (), state );
132
113
}
133
114
134
115
@ Override
135
- public void beforeEach (ExtensionContext extensionContext ) {
136
- checkState (
137
- PHASER_KEY . get ( extensionContext . getStore ( NAMESPACE )) != null ,
138
- "CompilationExtension is only available as a class-level extension. " +
139
- "Using it as an instance-level extension through @RegisterExtension is not supported"
116
+ public void beforeEach (ExtensionContext context ) throws InterruptedException {
117
+ final CompilerState state = context . getStore ( NAMESPACE ). getOrComputeIfAbsent (
118
+ CompilerState . class ,
119
+ ignored -> new CompilerState ( this . compilerExecutor , TestInstance . Lifecycle . PER_METHOD ),
120
+ CompilerState . class
140
121
);
122
+
123
+ checkState (state .prepareForTests (), state );
141
124
}
142
125
143
126
@ Override
144
- public void afterAll (ExtensionContext context ) throws Exception {
145
- final ExtensionContext .Store store = context .getStore (NAMESPACE );
146
- final Phaser sharedPhaser = PHASER_KEY .get (store );
127
+ public void afterEach (ExtensionContext context ) throws Exception {
128
+ final CompilerState state = checkNotNull (context .getStore (NAMESPACE ).get (
129
+ CompilerState .class ,
130
+ CompilerState .class
131
+ ));
147
132
148
- // Allow the processor to finish
149
- sharedPhaser .arriveAndDeregister ();
133
+ if (state .getLifecycle () == TestInstance .Lifecycle .PER_METHOD ) {
134
+ // Created on a per-method basis, must clean up as a mirror action
135
+ final Compilation compilation = state .allowTermination ();
136
+ checkState (compilation .status ().equals (SUCCESS ), compilation );
137
+ }
138
+ }
150
139
151
- // Perform status checks, since processing is 'over' almost instantly
152
- final Compilation compilation = RESULT_KEY .get (store )
153
- .toCompletableFuture ().get (1 , TimeUnit .SECONDS );
154
- checkState (compilation .status ().equals (SUCCESS ), compilation );
140
+ @ Override
141
+ public void afterAll (ExtensionContext context ) throws ExecutionException , InterruptedException {
142
+ final CompilerState state = checkNotNull (context .getStore (NAMESPACE ).get (
143
+ CompilerState .class ,
144
+ CompilerState .class
145
+ ));
146
+
147
+ checkState (state .getLifecycle () == TestInstance .Lifecycle .PER_CLASS );
155
148
156
- // Check postcondition
157
- checkState (sharedPhaser . isTerminated (), "Phaser not terminated" );
149
+ final Compilation compilation = state . allowTermination ();
150
+ checkState (compilation . status (). equals ( SUCCESS ), compilation );
158
151
}
159
152
160
153
@ Override
@@ -171,52 +164,144 @@ public Object resolveParameter(
171
164
ParameterContext parameterContext ,
172
165
ExtensionContext extensionContext
173
166
) throws ParameterResolutionException {
174
- final ExtensionContext .Store store = extensionContext .getStore (NAMESPACE );
175
- final AtomicReference <ProcessingEnvironment > processingEnvironment
176
- = PROCESSINGENV_KEY .get (store );
167
+ final CompilerState state = extensionContext .getStore (NAMESPACE ).get (
168
+ CompilerState .class ,
169
+ CompilerState .class
170
+ );
171
+
172
+ checkState (state != null , "CompilerState not initialized" );
177
173
178
174
return SUPPORTED_PARAMETERS .getOrDefault (
179
175
parameterContext .getParameter ().getType (),
180
176
ignored -> {
181
177
throw new ParameterResolutionException ("Unknown parameter type" );
182
178
}
183
- ).apply (checkNotNull (
184
- processingEnvironment .get (),
185
- "ProcessingEnvironment not available: %s" ,
186
- RESULT_KEY .get (store )
187
- ));
179
+ ).apply (state .getProcessingEnvironment ());
188
180
}
189
181
190
- /**
191
- * Utility class to safely access {@link ExtensionContext.Store} when dealing with
192
- * parameterized types.
193
- */
194
- static final class StoreAccessor <R > {
195
- private final Object key ;
182
+ static final class CompilerState implements ExtensionContext .Store .CloseableResource {
183
+ private final AtomicReference <ProcessingEnvironment > sharedState ;
184
+ private final Phaser syncBarrier ;
185
+ private final CompletableFuture <Compilation > result ;
186
+ private final TestInstance .Lifecycle lifecycle ;
187
+
188
+ CompilerState (Executor compilerExecutor , TestInstance .Lifecycle lifecycle ) {
189
+ this .lifecycle = lifecycle ;
190
+ this .sharedState = new AtomicReference <>(null );
191
+ this .syncBarrier = new Phaser (2 ) {
192
+ @ Override
193
+ protected boolean onAdvance (int phase , int parties ) {
194
+ // Terminate the phaser once all parties have deregistered
195
+ return parties == 0 ;
196
+ }
197
+ };
198
+ this .result = CompletableFuture .supplyAsync (
199
+ new EvaluatingProcessor (syncBarrier , sharedState ),
200
+ compilerExecutor
201
+ );
202
+ }
203
+
204
+ ProcessingEnvironment getProcessingEnvironment () throws ParameterResolutionException {
205
+ // Only while the phaser is in phase 1 should the ProcessingEnvironment be valid.
206
+ if (this .syncBarrier .getPhase () != 1 ) {
207
+ throw new ParameterResolutionException (this .toString ());
208
+ }
196
209
197
- StoreAccessor (Object key ) {
198
- this .key = key ;
210
+ final ProcessingEnvironment processingEnvironment = this .sharedState .get ();
211
+ if (processingEnvironment != null ) {
212
+ return processingEnvironment ;
213
+ } else {
214
+ throw new ParameterResolutionException (
215
+ String .format ("ProcessingEnvironment was not initialized: %s" , this )
216
+ );
217
+ }
199
218
}
200
219
201
- @ SuppressWarnings ("unchecked" )
202
- R get (ExtensionContext .Store store ) {
203
- return (R ) store .get (key );
220
+ TestInstance .Lifecycle getLifecycle () {
221
+ return this .lifecycle ;
204
222
}
205
223
206
- void put (ExtensionContext .Store store , R value ) {
207
- store .put (key , value );
224
+ boolean prepareForTests () throws InterruptedException {
225
+ switch (this .syncBarrier .getPhase ()) {
226
+ case 0 : // Compiler has been started, but might not yet be initialized
227
+ return checkNotTerminated (this .syncBarrier .arriveAndAwaitAdvance ());
228
+ case 1 : // Compiler has been initialized, ready for tests
229
+ return true ;
230
+ default :
231
+ throw new IllegalStateException (this .toString ());
232
+ }
233
+ }
234
+
235
+ Compilation allowTermination () throws InterruptedException , ExecutionException {
236
+ if (this .syncBarrier .getPhase () == 1 ) {
237
+ checkState (this .syncBarrier .arriveAndDeregister () == 1 , this );
238
+ } else if (!this .syncBarrier .isTerminated ()) {
239
+ throw new IllegalStateException (this .toString ());
240
+ }
241
+
242
+ try {
243
+ final Compilation result = this .result .get (1 , TimeUnit .SECONDS );
244
+ checkState (this .syncBarrier .isTerminated (), this );
245
+ return result ;
246
+ } catch (TimeoutException e ) {
247
+ // This really should never happen, since the 'syncBarrier' is the only thing the
248
+ // processor blocks on, deregistering at this point should allow the processor
249
+ // to run until it finishes.
250
+ throw new AssertionError ("Timed out waiting for the compiler to finish" );
251
+ }
252
+ }
253
+
254
+ private boolean checkNotTerminated (int phaseNumber ) throws InterruptedException {
255
+ if (phaseNumber < 0 ) {
256
+ // Phaser has terminated unexpectedly, throw exception based on result.
257
+
258
+ try {
259
+ // 'Successful' result
260
+ final Compilation result = this .result .get (5 , TimeUnit .SECONDS );
261
+ throw new IllegalStateException (
262
+ String .format ("Anomalous compilation result: %s" , result )
263
+ );
264
+ } catch (ExecutionException e ) {
265
+ // Exception in the compiler
266
+ throw new IllegalStateException ("Exception during annotation processing" , e .getCause ());
267
+ } catch (TimeoutException e ) {
268
+ // This really should never happen, since the 'syncBarrier' is the only thing the
269
+ // processor blocks on, termination should mean it runs until it finished,
270
+ // resolving 'result'
271
+ throw new AssertionError ("Timed out waiting for the cause of termination" );
272
+ }
273
+ }
274
+
275
+ return true ;
276
+ }
277
+
278
+ @ Override
279
+ public void close () {
280
+ // If the owning ExtensionContext.Store is closed, ensure the compilation terminates as well
281
+ this .syncBarrier .forceTermination ();
282
+ }
283
+
284
+ @ Override
285
+ public String toString () {
286
+ return "CompilerState{" +
287
+ "sharedState=" + sharedState +
288
+ ", syncBarrier=" + syncBarrier +
289
+ ", result=" + result +
290
+ ", lifecycle=" + lifecycle +
291
+ '}' ;
208
292
}
209
293
}
210
294
211
- static final class EvaluatingProcessor extends AbstractProcessor {
212
- private final Phaser barrier ;
295
+ static final class EvaluatingProcessor extends AbstractProcessor
296
+ implements Supplier <Compilation > {
297
+ private final Phaser syncBarrier ;
213
298
private final AtomicReference <ProcessingEnvironment > sharedState ;
214
299
215
300
EvaluatingProcessor (
216
- Phaser barrier ,
301
+ Phaser syncBarrier ,
217
302
AtomicReference <ProcessingEnvironment > sharedState
218
303
) {
219
- this .barrier = barrier ;
304
+ this .syncBarrier = syncBarrier ;
220
305
this .sharedState = sharedState ;
221
306
}
222
307
@@ -235,26 +320,34 @@ public synchronized void init(ProcessingEnvironment processingEnvironment) {
235
320
super .init (processingEnvironment );
236
321
237
322
// Share the processing environment
238
- if (!sharedState .compareAndSet (null , processingEnvironment )) {
239
- // Invalid state, init() run twice
240
- barrier .forceTermination ();
241
- throw new IllegalStateException ("Processor initialized twice" );
242
- }
323
+ checkState (
324
+ sharedState .compareAndSet (null , processingEnvironment ),
325
+ "Shared ProcessingEnvironment was already initialized"
326
+ );
243
327
}
244
328
245
329
@ Override
246
330
public boolean process (Set <? extends TypeElement > annotations , RoundEnvironment roundEnv ) {
247
331
if (roundEnv .processingOver ()) {
248
332
// Synchronize on the beginning of the test run
249
- barrier .arriveAndAwaitAdvance ();
333
+ syncBarrier .arriveAndAwaitAdvance ();
250
334
251
335
// Now wait until testing is over
252
- barrier .awaitAdvance (barrier .arriveAndDeregister ());
336
+ syncBarrier .awaitAdvance (syncBarrier .arriveAndDeregister ());
253
337
254
338
// Clean up the shared state
255
- sharedState .getAndSet (null );
339
+ sharedState .lazySet (null );
256
340
}
257
341
return false ;
258
342
}
343
+
344
+ @ Override
345
+ public Compilation get () {
346
+ try {
347
+ return Compiler .javac ().withProcessors (this ).compile (DUMMY );
348
+ } finally {
349
+ syncBarrier .forceTermination ();
350
+ }
351
+ }
259
352
}
260
353
}
0 commit comments