Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
seongahjo committed Feb 3, 2024
1 parent 3f8282f commit a825f51
Show file tree
Hide file tree
Showing 17 changed files with 933 additions and 2 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ subprojects {
}
}

if (!name.endsWith("tests")) {
if (!name.endsWith("tests") && name != "fixture-monkey-spring") {
tasks.withType<Test> {
useJUnitPlatform {
includeEngines("jqwik")
Expand Down
39 changes: 39 additions & 0 deletions fixture-monkey-spring/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
plugins {
id("com.navercorp.fixturemonkey.gradle.plugin.java-conventions")
id("com.navercorp.fixturemonkey.gradle.plugin.maven-publish-conventions")
id("org.springframework.boot") version "3.0.6"
id("io.spring.dependency-management") version "1.1.0"
}

java {
toolchain { languageVersion = JavaLanguageVersion.of(17) }
}

tasks.bootJar { enabled = false }

configurations {
testImplementation {
exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging")
exclude(group = "ch.qos.logback", module = "logback-classic")
exclude(group = "org.apache.logging.log4j", module = "log4j-to-slf4j")
}
}

dependencies {
api(project(":fixture-monkey"))
implementation("org.springframework.boot:spring-boot-starter-aop")
implementation("org.springframework.boot:spring-boot-starter-test")
implementation("io.projectreactor:reactor-core:3.5.6")
implementation("org.projectlombok:lombok:${Versions.LOMBOK}")
annotationProcessor("org.projectlombok:lombok:${Versions.LOMBOK}")

testImplementation("org.springframework.boot:spring-boot-starter-webflux")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.projectreactor:reactor-test")
testCompileOnly("org.projectlombok:lombok")
testAnnotationProcessor("org.projectlombok:lombok")
}

tasks.withType<Test> {
useJUnitPlatform()
}
3 changes: 3 additions & 0 deletions fixture-monkey-spring/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
artifactId=fixture-monkey-spring
artifactName=Fixture Monkey spring
artifactDescription=Fixture Monkey supports Spring Framework
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Fixture Monkey
*
* Copyright (c) 2021-present NAVER Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.navercorp.fixturemonkey.spring.interceptor;

import java.util.List;
import java.util.Optional;

import org.aopalliance.aop.Advice;
import org.springframework.aop.Advisor;
import org.springframework.aop.Pointcut;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.aop.support.Pointcuts;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

import com.navercorp.fixturemonkey.FixtureMonkey;
import com.navercorp.fixturemonkey.spring.interceptor.properties.AspectJPointcutProperty;
import com.navercorp.fixturemonkey.spring.interceptor.properties.MethodNamePointcutProperty;

@SuppressWarnings("SpringFacetCodeInspection")
@AutoConfiguration
@EnableAspectJAutoProxy
@ConfigurationPropertiesScan
public class FixtureMonkeyInterceptorConfiguration {
@Bean
@ConditionalOnMissingBean
public Pointcut fixtureMonkeyPointcut(
AspectJPointcutProperty aspectJPointcutProperty,
MethodNamePointcutProperty methodNamePointcutProperty
) {
Pointcut combinedPointcut = null;
if (aspectJPointcutProperty.getExpression() != null) {
AspectJExpressionPointcut aspectJPointcut = new AspectJExpressionPointcut();
aspectJPointcut.setExpression(aspectJPointcutProperty.getExpression());
combinedPointcut = aspectJPointcut;
}

if (methodNamePointcutProperty.getNames() != null) {
List<String> methodNames = methodNamePointcutProperty.getNames();
NameMatchMethodPointcut nameMatchMethodPointcut = new NameMatchMethodPointcut();
for (String methodName : methodNames) {
nameMatchMethodPointcut.addMethodName(methodName);
}

if (combinedPointcut == null) {
combinedPointcut = nameMatchMethodPointcut;
} else {
combinedPointcut = Pointcuts.union(combinedPointcut, nameMatchMethodPointcut);
}
}

if (combinedPointcut == null) {
throw new IllegalStateException("Pointcut is not set.");
}

return combinedPointcut;
}

@Bean
public Advice fixtureMonkeyMethodInterceptor(Optional<FixtureMonkey> fixtureMonkey) {
return new FixtureMonkeyMethodInterceptor(fixtureMonkey.orElse(FixtureMonkey.create()));
}

@Bean
public Advisor fixtureMonkeyAdvisor(
@Qualifier("fixtureMonkeyPointcut") Pointcut pointcut,
@Qualifier("fixtureMonkeyMethodInterceptor") Advice advice
) {
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* Fixture Monkey
*
* Copyright (c) 2021-present NAVER Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.navercorp.fixturemonkey.spring.interceptor;

import static com.navercorp.fixturemonkey.api.experimental.TypedExpressionGenerator.typedString;
import static java.util.Objects.requireNonNull;

import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import javax.annotation.Nullable;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;

import reactor.core.publisher.Mono;

import com.navercorp.fixturemonkey.FixtureMonkey;
import com.navercorp.fixturemonkey.api.type.Types;
import com.navercorp.fixturemonkey.experimental.ExperimentalArbitraryBuilder;
import com.navercorp.fixturemonkey.spring.interceptor.MethodInterceptorContext.RequestTarget.FixtureMonkeyManipulation.ManipulationObject;
import com.navercorp.fixturemonkey.spring.interceptor.MethodInterceptorContext.RequestTarget.FixtureMonkeyManipulation.ManipulationType;
import com.navercorp.fixturemonkey.spring.interceptor.MethodInterceptorContext.RequestTarget.RequestMethod;

public final class FixtureMonkeyMethodInterceptor implements MethodInterceptor {
private final FixtureMonkey fixtureMonkey;
private final ThreadLocal<Map<MethodCallIdentifier, Map<String, Object>>> initialCachesByMethodCallIdentifier =
ThreadLocal.withInitial(HashMap::new);

public FixtureMonkeyMethodInterceptor(FixtureMonkey fixtureMonkey) {
this.fixtureMonkey = fixtureMonkey;
}

@SuppressWarnings({"rawtypes", "unchecked", "ReactiveStreamsUnusedPublisher"})
@Nullable
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Object methodReturnObject = invocation.proceed();

Method method = invocation.getMethod();
if (Modifier.isStatic(method.getModifiers())) {
return methodReturnObject;
}

Class<?> callerType = requireNonNull(invocation.getThis()).getClass();
Class<?> returnType = getReturnType(method);

if (AopUtils.isAopProxy(invocation.getThis()) && invocation.getThis() instanceof Advised advised) {
callerType = advised.getProxiedInterfaces()[0];
}

Map<String, ManipulationObject> manipulators = MethodInterceptorContext.getManipulatingObjectsByExpression(
callerType,
new RequestMethod(returnType, method.getName())
);

MethodCallIdentifier methodCallIdentifier = new MethodCallIdentifier(
method.getName(),
method.getReturnType(),
Arrays.asList(invocation.getArguments())
);

Map<String, Object> initialValuesByExpression = initialCachesByMethodCallIdentifier.get().computeIfAbsent(
methodCallIdentifier,
identifier -> new HashMap<>()
);

if (methodReturnObject instanceof Mono mono) {
ExperimentalArbitraryBuilder<?> fallbackBuilder = fixtureMonkey.giveMeExperimentalBuilder(returnType);
applyManipulators(manipulators, fallbackBuilder, initialValuesByExpression);
return mono.defaultIfEmpty(fallbackBuilder.sample())
.onErrorResume(throwable -> Mono.just(fallbackBuilder.sample()))
.map(value -> {
ExperimentalArbitraryBuilder<?> builder = fixtureMonkey.giveMeExperimentalBuilder(value);
applyManipulators(manipulators, builder, initialValuesByExpression);
return builder.sample();
})
.onErrorResume(throwable -> Mono.just(fixtureMonkey.giveMeOne(returnType)));
} else if (methodReturnObject instanceof Optional optional) {
return optional.map(value -> {
ExperimentalArbitraryBuilder<?> builder = fixtureMonkey.giveMeExperimentalBuilder(value);
applyManipulators(manipulators, builder, initialValuesByExpression);
return builder.sample();
});
}

if (manipulators.isEmpty()) {
return methodReturnObject;
}

ExperimentalArbitraryBuilder<?> builder;
if (methodReturnObject == null) {
builder = fixtureMonkey.giveMeExperimentalBuilder(returnType);
} else {
builder = fixtureMonkey.giveMeExperimentalBuilder(methodReturnObject);
}
applyManipulators(manipulators, builder, initialValuesByExpression);
return builder.sample();
}

private static void applyManipulators(
Map<String, ManipulationObject> manipulators,
ExperimentalArbitraryBuilder<?> builder,
Map<String, Object> initialValuesByExpression
) {
manipulators.forEach(
(expression, value) -> {
if (value.getManipulationType() == ManipulationType.WITH) {
builder.customizeProperty(typedString(expression), arbitrary ->
arbitrary.map(sampled -> {
Object initialValue = initialValuesByExpression.putIfAbsent(expression, sampled);

if (initialValue == null || Objects.equals(initialValue, sampled)) {
return value.getValue();
}

return sampled;
})
);
} else {
builder.set(expression, value.getValue());
}
}
);
}

private static Class<?> getReturnType(Method method) {
Class<?> returnType;
AnnotatedType annotatedReturnType = method.getAnnotatedReturnType();
List<AnnotatedType> genericsTypes = Types.getGenericsTypes(annotatedReturnType);
if (genericsTypes.isEmpty()) {
returnType = method.getReturnType();
} else {
returnType = Types.getActualType(genericsTypes.get(0));
}
return returnType;
}

public record MethodCallIdentifier(
String methodName,

Class<?> returnType,

List<Object> arguments
) {
}
}
Loading

0 comments on commit a825f51

Please sign in to comment.