001/* 002 * Copyright (C) 2022 - 2024, the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package io.github.ascopes.jct.junit; 017 018import io.github.ascopes.jct.workspaces.Workspace; 019import io.github.ascopes.jct.workspaces.Workspaces; 020import java.lang.reflect.Field; 021import java.lang.reflect.Modifier; 022import java.util.ArrayList; 023import java.util.List; 024import org.jspecify.annotations.Nullable; 025import org.junit.jupiter.api.extension.AfterAllCallback; 026import org.junit.jupiter.api.extension.AfterEachCallback; 027import org.junit.jupiter.api.extension.BeforeAllCallback; 028import org.junit.jupiter.api.extension.BeforeEachCallback; 029import org.junit.jupiter.api.extension.Extension; 030import org.junit.jupiter.api.extension.ExtensionContext; 031import org.slf4j.Logger; 032import org.slf4j.LoggerFactory; 033 034/** 035 * JUnit5 extension that will manage the lifecycle of {@link Managed}-annotated {@link Workspace} 036 * fields within JUnit5 test classes. 037 * 038 * <pre><code> 039 * {@literal @ExtendWith(JctExtension.class)} 040 * class MyTest { 041 * {@literal @Managed} 042 * Workspace workspace; 043 * 044 * {@literal @JavacCompilerTest} 045 * void myTest(JctCompiler compiler) { 046 * // Given 047 * workspace 048 * .createSourcePathPackage() 049 * ...; 050 * 051 * // When 052 * var compilation = compiler.compile(workspace); 053 * 054 * // Then 055 * ... 056 * } 057 * } 058 * </code></pre> 059 * 060 * @author Ashley Scopes 061 * @since 0.4.0 062 */ 063public final class JctExtension 064 implements Extension, BeforeEachCallback, BeforeAllCallback, AfterEachCallback, AfterAllCallback { 065 066 private static final Logger log = LoggerFactory.getLogger(JctExtension.class); 067 068 /** 069 * Initialise this extension. 070 * 071 * <p>You shouldn't ever need to call this directly. See the class description for an example 072 * of how to use this. 073 */ 074 public JctExtension() { 075 // Nothing to do. 076 } 077 078 @Override 079 public void beforeAll(ExtensionContext context) throws Exception { 080 for (var field : getManagedWorkspaceFields(context.getRequiredTestClass(), true)) { 081 initWorkspaceForField(field, null); 082 } 083 } 084 085 @Override 086 public void beforeEach(ExtensionContext context) throws Exception { 087 for (var instance : context.getRequiredTestInstances().getAllInstances()) { 088 for (var field : getManagedWorkspaceFields(instance.getClass(), false)) { 089 initWorkspaceForField(field, instance); 090 } 091 } 092 } 093 094 @Override 095 public void afterAll(ExtensionContext context) throws Exception { 096 for (var field : getManagedWorkspaceFields(context.getRequiredTestClass(), true)) { 097 closeWorkspaceForField(field, null); 098 } 099 } 100 101 @Override 102 public void afterEach(ExtensionContext context) throws Exception { 103 for (var instance : context.getRequiredTestInstances().getAllInstances()) { 104 for (var field : getManagedWorkspaceFields(instance.getClass(), false)) { 105 closeWorkspaceForField(field, instance); 106 } 107 } 108 } 109 110 private List<Field> getManagedWorkspaceFields(Class<?> clazz, boolean wantStatic) { 111 var fields = new ArrayList<Field>(); 112 113 Class<?> currentClass = clazz; 114 115 do { 116 for (var field : currentClass.getDeclaredFields()) { 117 var isWorkspace = field.getType().equals(Workspace.class); 118 var isManaged = field.isAnnotationPresent(Managed.class); 119 var isDesiredScope = Modifier.isStatic(field.getModifiers()) == wantStatic; 120 121 if (isWorkspace && isManaged && isDesiredScope) { 122 field.setAccessible(true); 123 fields.add(field); 124 } 125 } 126 127 // Only recurse if we are checking instance scope. We don't manage annotated fields 128 // in superclasses that are static as we cannot guarantee they are not shared with a 129 // different class running in parallel. 130 currentClass = wantStatic 131 ? null 132 : currentClass.getSuperclass(); 133 134 } while (currentClass != null); 135 136 return fields; 137 } 138 139 private void initWorkspaceForField(Field field, @Nullable Object instance) throws Exception { 140 log.atTrace() 141 .setMessage("Initialising workspace for field in {}: {} {} on instance {}") 142 .addArgument(() -> field.getDeclaringClass().getSimpleName()) 143 .addArgument(() -> field.getType().getSimpleName()) 144 .addArgument(field::getName) 145 .addArgument(instance) 146 .log(); 147 148 var managedWorkspace = field.getAnnotation(Managed.class); 149 var workspace = Workspaces.newWorkspace(managedWorkspace.pathStrategy()); 150 field.set(instance, workspace); 151 } 152 153 private void closeWorkspaceForField(Field field, @Nullable Object instance) throws Exception { 154 log.atTrace() 155 .setMessage("Closing workspace for field in {}: {} {} on instance {}") 156 .addArgument(() -> field.getDeclaringClass().getSimpleName()) 157 .addArgument(() -> field.getType().getSimpleName()) 158 .addArgument(field::getName) 159 .addArgument(instance) 160 .log(); 161 162 var workspace = (Workspace) field.get(instance); 163 workspace.close(); 164 } 165}