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 * Implicit extension that will manage the lifecycle of {@link Managed}-annotated {@link Workspace} 038 * fields within JUnit5 test classes. 039 * 040 * @author Ashley Scopes 041 * @since 0.4.0 042 */ 043@API(since = "0.4.0", status = Status.STABLE) 044public final class JctExtension implements 045 Extension, BeforeEachCallback, BeforeAllCallback, AfterEachCallback, AfterAllCallback { 046 047 private static final Logger LOGGER = LoggerFactory.getLogger(JctExtension.class); 048 049 @Override 050 public void beforeAll(ExtensionContext context) throws Exception { 051 for (var field : getManagedStaticWorkspaceFields(context.getRequiredTestClass())) { 052 initWorkspaceForField(field, null); 053 } 054 } 055 056 @Override 057 public void beforeEach(ExtensionContext context) throws Exception { 058 for (var instance : context.getRequiredTestInstances().getAllInstances()) { 059 for (var field : getManagedInstanceWorkspaceFields(instance.getClass())) { 060 initWorkspaceForField(field, instance); 061 } 062 } 063 } 064 065 @Override 066 public void afterAll(ExtensionContext context) throws Exception { 067 for (var field : getManagedStaticWorkspaceFields(context.getRequiredTestClass())) { 068 closeWorkspaceForField(field, null); 069 } 070 } 071 072 @Override 073 public void afterEach(ExtensionContext context) throws Exception { 074 for (var instance : context.getRequiredTestInstances().getAllInstances()) { 075 for (var field : getManagedInstanceWorkspaceFields(instance.getClass())) { 076 closeWorkspaceForField(field, instance); 077 } 078 } 079 } 080 081 private List<Field> getManagedStaticWorkspaceFields(Class<?> clazz) { 082 // Do not recurse for static fields, as the state of any parent classes may be shared 083 // with other classes running in parallel. Need to look up how JUnit expects us to handle that 084 // case, if at all. 085 086 var fields = new ArrayList<Field>(); 087 088 for (var field : clazz.getDeclaredFields()) { 089 if (isWorkspaceField(field) && Modifier.isStatic(field.getModifiers())) { 090 fields.add(field); 091 } 092 } 093 094 return fields; 095 } 096 097 private List<Field> getManagedInstanceWorkspaceFields(Class<?> clazz) { 098 // For instances, discover all the fields recursively in superclasses as well that are 099 // non-static. 100 101 var fields = new ArrayList<Field>(); 102 103 while (clazz != null) { 104 for (var field : clazz.getDeclaredFields()) { 105 if (isWorkspaceField(field) && !Modifier.isStatic(field.getModifiers())) { 106 fields.add(field); 107 } 108 } 109 clazz = clazz.getSuperclass(); 110 } 111 112 return fields; 113 } 114 115 private boolean isWorkspaceField(Field field) { 116 return field.getType().equals(Workspace.class) 117 && field.isAnnotationPresent(Managed.class); 118 } 119 120 private void initWorkspaceForField(Field field, @Nullable Object instance) throws Exception { 121 LOGGER 122 .atTrace() 123 .setMessage("Initialising workspace for field in {}: {} {} on instance {}") 124 .addArgument(() -> field.getDeclaringClass().getSimpleName()) 125 .addArgument(() -> field.getType().getSimpleName()) 126 .addArgument(field::getName) 127 .addArgument(instance) 128 .log(); 129 130 field.setAccessible(true); 131 var managedWorkspace = field.getAnnotation(Managed.class); 132 var workspace = Workspaces.newWorkspace(managedWorkspace.pathStrategy()); 133 field.set(instance, workspace); 134 } 135 136 private void closeWorkspaceForField(Field field, @Nullable Object instance) throws Exception { 137 LOGGER 138 .atTrace() 139 .setMessage("Closing workspace for field in {}: {} {} on instance {}") 140 .addArgument(() -> field.getDeclaringClass().getSimpleName()) 141 .addArgument(() -> field.getType().getSimpleName()) 142 .addArgument(field::getName) 143 .addArgument(instance) 144 .log(); 145 146 field.setAccessible(true); 147 ((Workspace) field.get(instance)).close(); 148 } 149}