BinderChildContextInitializerTests.java

/*
 * Copyright 2022-present the original author or authors.
 *
 * 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
 *
 *      https://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 org.springframework.cloud.stream.binder;

import java.util.function.Consumer;
import java.util.function.Supplier;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import org.springframework.aot.AotDetector;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.annotation.UserConfigurations;
import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.cloud.stream.config.BinderFactoryAutoConfiguration;
import org.springframework.cloud.stream.config.BindingServiceConfiguration;
import org.springframework.cloud.stream.function.FunctionConfiguration;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.aot.ApplicationContextAotGenerator;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.core.log.LogAccessor;
import org.springframework.core.test.tools.CompileWithForkedClassLoader;
import org.springframework.core.test.tools.TestCompiler;
import org.springframework.javapoet.ClassName;
import org.springframework.messaging.MessageChannel;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.Mockito.mock;

/**
 * Tests for the {@link BinderChildContextInitializer}.
 *
 * @author Chris Bono
 */
@ExtendWith(OutputCaptureExtension.class)
class BinderChildContextInitializerTests {

	private static final LogAccessor LOG = new LogAccessor(BinderChildContextInitializerTests.class);

	@Test
	@CompileWithForkedClassLoader
	void shouldStartDefaultBinderChildContextFromAotContributions(CapturedOutput output) {

		// Test description:
		// -----------------------
		// Use context runner to create a boostrap context that we can then pass into AOT processor.
		// The AOT processor will then generate the ACI for the default binder (no user declared binders).
		// We then initialize a fresh app context using the generated ACI and verify the expected output.

		// For some reasons, the logging level in the child context is not adjustable from the test.
		// Therefore, we are relying on the fact that the logging levels are the default INFO level.
		// It only happens for tests and this doesn't seem to be the case in real applications,
		// i.e. we can control the logging level in real applications.

		ApplicationContextRunner contextRunner = new ApplicationContextRunner()
				.withConfiguration(AutoConfigurations.of(BinderFactoryAutoConfiguration.class,
						BindingServiceConfiguration.class, FunctionConfiguration.class))
				.withInitializer(new ConfigDataApplicationContextInitializer())
				.withConfiguration(UserConfigurations.of(TestFooBinderAppConfiguration.class));
		contextRunner.prepare(context -> {
			TestGenerationContext generationContext = new TestGenerationContext(TestTarget.class);
			ClassName className = new ApplicationContextAotGenerator().processAheadOfTime(
					(GenericApplicationContext) context.getSourceApplicationContext(), generationContext);
			generationContext.writeGeneratedContent();
			TestCompiler compiler = TestCompiler.forSystem();
			compiler.with(generationContext).compile(compiled -> {
				// Initialize the context w/ the generated ACI
				GenericApplicationContext freshApplicationContext = new GenericApplicationContext();
				System.out.println("*** Before ACI init -> OUTPUT: " + output.getAll());
				ApplicationContextInitializer<GenericApplicationContext> initializer = compiled
						.getInstance(ApplicationContextInitializer.class, className.toString());
				initializer.initialize(freshApplicationContext);
				System.out.println("*** After ACI init -> OUTPUT: " + output.getAll());
				assertThat(output).contains("Beginning AOT processing for binder child contexts");
				assertThat(output).contains("Pre-creating binder child context (AOT) for mock");
				assertThat(output).contains("Generating AOT child context initializer for mock");

				// Refresh the initialized context and verify the binder child contexts are used
				TestPropertyValues.of(AotDetector.AOT_ENABLED + "=true")
						.applyToSystemProperties(freshApplicationContext::refresh);
				assertThat(output).contains("Replacing instance w/ one that uses child context initializers");
				assertThat(output).contains("Setting binder child context initializers on binder factory");

				// Make sure we can get the binders
				DefaultBinderFactory binderFactory = freshApplicationContext.getBean(DefaultBinderFactory.class);
				Binder<MessageChannel, ?, ?> mockBinder = binderFactory.getBinder("mock", MessageChannel.class);
				assertThat(mockBinder).isNotNull();
				assertThat(output).contains("Caching the binder: mock");

				// no default or name given - uses single available binder
				assertThat(binderFactory.getBinder(null, MessageChannel.class)).isSameAs(mockBinder);
				assertThat(output).contains("No specific name or default given - using single available child initializer 'mock'");

				assertThatIllegalStateException().isThrownBy(
								() -> binderFactory.getBinder("mockBinder1", MessageChannel.class))
						.withMessageContaining("Requested binder 'mockBinder1' did not match available binders");

				binderFactory.setDefaultBinder("mock");
				assertThat(binderFactory.getBinder(null, MessageChannel.class)).isSameAs(mockBinder);
			});
		});
	}

	@Test
	@CompileWithForkedClassLoader
	@SuppressWarnings("unchecked")
	void shouldStartDeclardBinderChildContextsFromAotContributions(CapturedOutput output) {

		// Test description:
		// -----------------------
		// Use context runner to create a boostrap context that we can then pass into AOT processor.
		// The AOT processor will then generate the ACI for each binder child context defined in the application.yml.
		// We then initialize a fresh app context using the generated ACIs and verify the expected output.
		ApplicationContextRunner contextRunner = new ApplicationContextRunner()
				.withConfiguration(AutoConfigurations.of(BinderFactoryAutoConfiguration.class,
						BindingServiceConfiguration.class, FunctionConfiguration.class))
				.withInitializer(new ConfigDataApplicationContextInitializer())
				.withPropertyValues("spring.config.location=classpath:binder-aot-test/")
				.withConfiguration(UserConfigurations.of(TestFooBinderAppConfiguration.class));

		contextRunner.prepare(context -> {
			TestGenerationContext generationContext = new TestGenerationContext(TestTarget.class);
			ClassName className = new ApplicationContextAotGenerator().processAheadOfTime(
					(GenericApplicationContext) context.getSourceApplicationContext(), generationContext);
			generationContext.writeGeneratedContent();
			TestCompiler compiler = TestCompiler.forSystem();
			compiler.with(generationContext).compile(compiled -> {
				// Initialize the context w/ the generated ACI
				GenericApplicationContext freshApplicationContext = new GenericApplicationContext();
				System.out.println("*** Before ACI init -> OUTPUT: " + output.getAll());
				ApplicationContextInitializer<GenericApplicationContext> initializer = compiled
						.getInstance(ApplicationContextInitializer.class, className.toString());
				initializer.initialize(freshApplicationContext);
				System.out.println("*** After ACI init -> OUTPUT: " + output.getAll());
				assertThat(output).contains("Beginning AOT processing for binder child contexts");
				assertThat(output).contains("Pre-creating binder child context (AOT) for mockBinder2");
				assertThat(output).contains("Pre-creating binder child context (AOT) for mockBinder1");
				assertThat(output).contains("Generating AOT child context initializer for mockBinder2");
				assertThat(output).contains("Generating AOT child context initializer for mockBinder1");

				// Refresh the initialized context and verify the binder child contexts are used
				TestPropertyValues.of(AotDetector.AOT_ENABLED + "=true")
						.applyToSystemProperties(freshApplicationContext::refresh);

				assertThat(output).contains("Replacing instance w/ one that uses child context initializers");
				assertThat(output).contains("Setting binder child context initializers on binder factory");

				// Make sure we can get the binders
				DefaultBinderFactory binderFactory = freshApplicationContext.getBean(DefaultBinderFactory.class);
				assertThat(binderFactory.getBinder("mockBinder1", MessageChannel.class)).isNotNull();
				assertThat(output).contains("Caching the binder: mockBinder1");

				Binder mockBinder2 = binderFactory.getBinder("mockBinder2", MessageChannel.class);
				assertThat(mockBinder2).isNotNull();
				assertThat(output).contains("Caching the binder: mockBinder2");

				assertThatIllegalStateException().isThrownBy(
						() -> binderFactory.getBinder("mockBinder3", MessageChannel.class))
						.withMessageContaining("Requested binder 'mockBinder3' did not match available binders");

				assertThatIllegalStateException().isThrownBy(
								() -> binderFactory.getBinder(null, MessageChannel.class))
						.withMessageContaining("No specific name or default given - can't determine which binder to use");

				binderFactory.setDefaultBinder("mockBinder2");
				assertThat(binderFactory.getBinder(null, MessageChannel.class)).isSameAs(mockBinder2);
			});
		});
	}

	static class TestTarget {
	}

	@EnableAutoConfiguration
	@Configuration(proxyBeanMethods = false)
	static class TestFooBinderAppConfiguration {

		@Bean
		GenericConversionService integrationConversionService() {
			return mock(GenericConversionService.class);
		}

		@Bean
		Supplier<String> fooSource() {
			return () -> "foo-" + System.currentTimeMillis();
		}

		@Bean
		Consumer<String> fooSink() {
			return (foo) -> LOG.info("*** FOO: " + foo);
		}
	}

}