001/* 002 * Copyright (C) 2022 - 2023, 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.apiguardian.api.API; 025import org.apiguardian.api.API.Status; 026import org.jspecify.annotations.Nullable; 027import org.junit.jupiter.api.extension.AfterAllCallback; 028import org.junit.jupiter.api.extension.AfterEachCallback; 029import org.junit.jupiter.api.extension.BeforeAllCallback; 030import org.junit.jupiter.api.extension.BeforeEachCallback; 031import org.junit.jupiter.api.extension.Extension; 032import org.junit.jupiter.api.extension.ExtensionContext; 033import org.slf4j.Logger; 034import org.slf4j.LoggerFactory; 035 036/** 037 * JUnit5 extension that will manage the lifecycle of {@link Managed}-annotated {@link Workspace} 038 * fields within JUnit5 test classes. 039 * 040 * <pre><code> 041 * {@literal @ExtendWith(JctExtension.class)} 042 * class MyTest { 043 * {@literal @Managed} 044 * Workspace workspace; 045 * 046 * {@literal @JavacCompilerTest} 047 * void myTest(JctCompiler compiler) { 048 * // Given 049 * workspace 050 * .createSourcePathPackage() 051 * ...; 052 * 053 * // When 054 * var compilation = compiler.compile(workspace); 055 * 056 * // Then 057 * ... 058 * } 059 * } 060 * </code></pre> 061 * 062 * @author Ashley Scopes 063 * @since 0.4.0 064 */ 065@API(since = "0.4.0", status = Status.STABLE) 066public final class JctExtension implements 067 Extension, BeforeEachCallback, BeforeAllCallback, AfterEachCallback, AfterAllCallback { 068 069 private static final Logger LOGGER = LoggerFactory.getLogger(JctExtension.class); 070 071 /** 072 * Initialise this extension. 073 * 074 * <p>You shouldn't ever need to call this directly. See the class description for an example 075 * of how to use this. 076 */ 077 public JctExtension() { 078 // Nothing to do. 079 } 080 081 @Override 082 public void beforeAll(ExtensionContext context) throws Exception { 083 for (var field : getManagedStaticWorkspaceFields(context.getRequiredTestClass())) { 084 initWorkspaceForField(field, null); 085 } 086 } 087 088 @Override 089 public void beforeEach(ExtensionContext context) throws Exception { 090 for (var instance : context.getRequiredTestInstances().getAllInstances()) { 091 for (var field : getManagedInstanceWorkspaceFields(instance.getClass())) { 092 initWorkspaceForField(field, instance); 093 } 094 } 095 } 096 097 @Override 098 public void afterAll(ExtensionContext context) throws Exception { 099 for (var field : getManagedStaticWorkspaceFields(context.getRequiredTestClass())) { 100 closeWorkspaceForField(field, null); 101 } 102 } 103 104 @Override 105 public void afterEach(ExtensionContext context) throws Exception { 106 for (var instance : context.getRequiredTestInstances().getAllInstances()) { 107 for (var field : getManagedInstanceWorkspaceFields(instance.getClass())) { 108 closeWorkspaceForField(field, instance); 109 } 110 } 111 } 112 113 private List<Field> getManagedStaticWorkspaceFields(Class<?> clazz) { 114 // Do not recurse for static fields, as the state of any parent classes may be shared 115 // with other classes running in parallel. Need to look up how JUnit expects us to handle that 116 // case, if at all. 117 118 var fields = new ArrayList<Field>(); 119 120 for (var field : clazz.getDeclaredFields()) { 121 if (isWorkspaceField(field) && Modifier.isStatic(field.getModifiers())) { 122 fields.add(field); 123 } 124 } 125 126 return fields; 127 } 128 129 private List<Field> getManagedInstanceWorkspaceFields(Class<?> clazz) { 130 // For instances, discover all the fields recursively in superclasses as well that are 131 // non-static. 132 133 var fields = new ArrayList<Field>(); 134 135 while (clazz != null) { 136 for (var field : clazz.getDeclaredFields()) { 137 if (isWorkspaceField(field) && !Modifier.isStatic(field.getModifiers())) { 138 fields.add(field); 139 } 140 } 141 clazz = clazz.getSuperclass(); 142 } 143 144 return fields; 145 } 146 147 private boolean isWorkspaceField(Field field) { 148 return field.getType().equals(Workspace.class) 149 && field.isAnnotationPresent(Managed.class); 150 } 151 152 private void initWorkspaceForField(Field field, @Nullable Object instance) throws Exception { 153 LOGGER 154 .atTrace() 155 .setMessage("Initialising 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 field.setAccessible(true); 163 var managedWorkspace = field.getAnnotation(Managed.class); 164 var workspace = Workspaces.newWorkspace(managedWorkspace.pathStrategy()); 165 field.set(instance, workspace); 166 } 167 168 private void closeWorkspaceForField(Field field, @Nullable Object instance) throws Exception { 169 LOGGER 170 .atTrace() 171 .setMessage("Closing workspace for field in {}: {} {} on instance {}") 172 .addArgument(() -> field.getDeclaringClass().getSimpleName()) 173 .addArgument(() -> field.getType().getSimpleName()) 174 .addArgument(field::getName) 175 .addArgument(instance) 176 .log(); 177 178 field.setAccessible(true); 179 ((Workspace) field.get(instance)).close(); 180 } 181}