Skip to content

Commit

Permalink
Initialize extensions per-suite & prepare resources (#378)
Browse files Browse the repository at this point in the history
Initialize test extensions per-test-suite. Each test suite is self-contained, with their own Docker containers and state.  It makes things much more simple if test extensions are created and initialised for a specific test suite.

Ensure extensions are automatically closed at the end of the test suite run, allowing them to release any resources.

Make use of the new `prepare` method on the `ResourceHandler` to instruct Creek extensions to prepare internal state to interact with the supplied resources.
  • Loading branch information
big-andy-coates authored Dec 22, 2023
1 parent 80b83f6 commit f84222e
Show file tree
Hide file tree
Showing 13 changed files with 480 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
import java.nio.file.Files;
import java.time.Duration;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.creekservice.api.base.type.JarVersion;
import org.creekservice.api.system.test.extension.test.model.TestExecutionResult;
import org.creekservice.api.system.test.parser.TestPackageParser;
import org.creekservice.api.system.test.parser.TestPackagesLoader;
import org.creekservice.internal.system.test.executor.api.SystemTest;
import org.creekservice.internal.system.test.executor.cli.PicoCliParser;
Expand Down Expand Up @@ -89,15 +91,7 @@ public static TestExecutionResult run(final ExecutorOptions options) {
"Not a directory: " + options.testDirectory().toUri());
}

final SystemTest api =
initializeApi(
options.serviceDebugInfo()
.map(ServiceDebugInfo::copyOf)
.orElse(ServiceDebugInfo.none()),
options.mountInfo(),
options.env());

final TestExecutionResult result = executor(options, api).execute();
final TestExecutionResult result = executor(options).execute();
if (result.isEmpty()) {
throw new TestExecutionFailedException(
"No tests found under: " + options.testDirectory().toUri());
Expand Down Expand Up @@ -134,24 +128,35 @@ private static String modulePath() {
.collect(Collectors.joining(" "));
}

private static TestPackagesExecutor executor(
final ExecutorOptions options, final SystemTest api) {
private static TestPackagesExecutor executor(final ExecutorOptions options) {

final Supplier<SystemTest> apiSupplier =
() ->
initializeApi(
options.serviceDebugInfo()
.map(ServiceDebugInfo::copyOf)
.orElse(ServiceDebugInfo.none()),
options.mountInfo(),
options.env());

final TestPackagesLoader loader =
testPackagesLoader(
options.testDirectory(),
yamlParser(
api.tests().model().modelTypes(),
new TestPackageParserObserver(LOGGER)),
options.suitesFilter());
options.testDirectory(), createParser(apiSupplier), options.suitesFilter());

return new TestPackagesExecutor(
loader,
new TestSuiteExecutor(
api, options.verifierTimeout().orElse(DEFAULT_VERIFIER_TIMEOUT)),
apiSupplier, options.verifierTimeout().orElse(DEFAULT_VERIFIER_TIMEOUT)),
new XmlResultsWriter(options.resultDirectory()));
}

private static TestPackageParser createParser(final Supplier<SystemTest> apiSupplier) {
// Initialize API and test extensions once here to obtain the list of model extensions:
final SystemTest api = apiSupplier.get();

return yamlParser(api.tests().model().modelTypes(), new TestPackageParserObserver(LOGGER));
}

private static final class TestExecutionFailedException extends RuntimeException {
TestExecutionFailedException(final String msg) {
super(msg);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.creekservice.internal.system.test.executor.execution.debug.ServiceDebugInfo;
import org.creekservice.internal.system.test.executor.execution.listener.AddServicesUnderTestListener;
import org.creekservice.internal.system.test.executor.execution.listener.InitializeResourcesListener;
import org.creekservice.internal.system.test.executor.execution.listener.PrepareResourcesListener;
import org.creekservice.internal.system.test.executor.execution.listener.StartServicesUnderTestListener;
import org.creekservice.internal.system.test.executor.execution.listener.SuiteCleanUpListener;
import org.creekservice.internal.system.test.executor.observation.LoggingTestEnvironmentListener;
Expand Down Expand Up @@ -77,6 +78,7 @@ static SystemTest initializeApi(
api.tests().env().listeners().append(addServicesListener);
creekTestExtensions.forEach(ext -> ext.initialize(api));
api.tests().env().listeners().append(new InitializeResourcesListener(api));
api.tests().env().listeners().append(new PrepareResourcesListener(api));
api.tests()
.env()
.listeners()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,5 +195,10 @@ public <T extends CreekExtension> T ensureExtension(
public ComponentModelCollection model() {
return api.components().model();
}

/** Close all extensions. */
public void close() {
api.extensions().close();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,41 +20,30 @@
import static org.creekservice.internal.system.test.executor.result.SuiteResult.testSuiteResult;

import java.time.Duration;
import java.util.function.Supplier;
import org.creekservice.api.base.annotation.VisibleForTesting;
import org.creekservice.api.system.test.extension.test.env.listener.TestListenerCollection;
import org.creekservice.api.system.test.extension.test.model.TestSuiteResult;
import org.creekservice.api.system.test.model.TestSuite;
import org.creekservice.internal.system.test.executor.api.SystemTest;
import org.creekservice.internal.system.test.executor.execution.input.Inputters;
import org.creekservice.internal.system.test.executor.result.SuiteResult;

/** Executor or test suites. */
/** Executor of test suites. */
public final class TestSuiteExecutor {

private final Inputters inputters;
private final TestListenerCollection listeners;
private final TestCaseExecutor testExecutor;
private final Supplier<SystemTest> apiSupplier;
private final Duration verifierTimeout;

/**
* @param api the system test api.
* @param apiSupplier Supplier of initialized system test api. A fresh api instance is created
* per test suite.
* @param verifierTimeout the default verifier timeout, i.e. how long to wait for expectations
* to be met.
*/
public TestSuiteExecutor(final SystemTest api, final Duration verifierTimeout) {
this(
api.tests().env().listeners(),
new Inputters(api.tests().model()),
new TestCaseExecutor(api, verifierTimeout));
}

@VisibleForTesting
TestSuiteExecutor(
final TestListenerCollection listeners,
final Inputters inputters,
final TestCaseExecutor testExecutor) {
this.listeners = requireNonNull(listeners, "listeners");
this.inputters = requireNonNull(inputters, "inputter");
this.testExecutor = requireNonNull(testExecutor, "testExecutor");
public TestSuiteExecutor(
final Supplier<SystemTest> apiSupplier, final Duration verifierTimeout) {
this.apiSupplier = requireNonNull(apiSupplier, "apiSupplier");
this.verifierTimeout = requireNonNull(verifierTimeout, "verifierTimeout");
}

/**
Expand All @@ -64,49 +53,75 @@ public TestSuiteExecutor(final SystemTest api, final Duration verifierTimeout) {
* @return the test result.
*/
public SuiteResult executeSuite(final TestSuite testSuite) {
final SuiteResult result = execute(testSuite);
return new Executor(apiSupplier.get(), verifierTimeout).executeSuite(testSuite);
}

try {
afterSuite(testSuite, result);
} catch (final Exception e) {
throw new SuiteExecutionFailedException("Suite teardown", testSuite, e);
@VisibleForTesting
static final class Executor {
private final TestListenerCollection listeners;
private final Inputters inputters;
private final TestCaseExecutor testExecutor;

Executor(final SystemTest api, final Duration verifierTimeout) {
this(
api.tests().env().listeners(),
new Inputters(api.tests().model()),
new TestCaseExecutor(api, verifierTimeout));
}

return result;
}
Executor(
final TestListenerCollection listeners,
final Inputters inputters,
final TestCaseExecutor testExecutor) {
this.listeners = requireNonNull(listeners, "listeners");
this.inputters = requireNonNull(inputters, "inputter");
this.testExecutor = requireNonNull(testExecutor, "testExecutor");
}

private SuiteResult execute(final TestSuite testSuite) {
final SuiteResult.Builder builder = testSuiteResult(testSuite);
SuiteResult executeSuite(final TestSuite testSuite) {
final SuiteResult result = execute(testSuite);

try {
beforeSuite(testSuite);
} catch (final Exception e) {
final SuiteExecutionFailedException cause =
new SuiteExecutionFailedException("Suite setup", testSuite, e);
try {
afterSuite(testSuite, result);
} catch (final Exception e) {
throw new SuiteExecutionFailedException("Suite teardown", testSuite, e);
}

return builder.buildError(cause);
return result;
}

runSuite(testSuite, builder);
private SuiteResult execute(final TestSuite testSuite) {
final SuiteResult.Builder builder = testSuiteResult(testSuite);

return builder.build();
}
try {
beforeSuite(testSuite);
} catch (final Exception e) {
final SuiteExecutionFailedException cause =
new SuiteExecutionFailedException("Suite setup", testSuite, e);

private void beforeSuite(final TestSuite testSuite) {
listeners.forEach(listener -> listener.beforeSuite(testSuite));
inputters.input(testSuite.pkg().seedData(), testSuite);
}
return builder.buildError(cause);
}

try {
runSuite(testSuite, builder);
return builder.build();
} catch (final Exception e) {
throw new SuiteExecutionFailedException("Suite execution", testSuite, e);
}
}

private void runSuite(final TestSuite testSuite, final SuiteResult.Builder builder) {
try {
private void beforeSuite(final TestSuite testSuite) {
listeners.forEach(listener -> listener.beforeSuite(testSuite));
inputters.input(testSuite.pkg().seedData(), testSuite);
}

private void runSuite(final TestSuite testSuite, final SuiteResult.Builder builder) {
testSuite.tests().stream().map(testExecutor::executeTest).forEach(builder::add);
} catch (final Exception e) {
throw new SuiteExecutionFailedException("Suite execution", testSuite, e);
}
}

private void afterSuite(final TestSuite testSuite, final TestSuiteResult result) {
listeners.forEachReverse(listener -> listener.afterSuite(testSuite, result));
private void afterSuite(final TestSuite testSuite, final SuiteResult result) {
listeners.forEachReverse(listener -> listener.afterSuite(testSuite, result));
}
}

private static final class SuiteExecutionFailedException extends RuntimeException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@

import static java.util.Objects.requireNonNull;

import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.creekservice.api.base.annotation.VisibleForTesting;
import org.creekservice.api.platform.metadata.ComponentDescriptor;
import org.creekservice.api.platform.metadata.OwnedResource;
import org.creekservice.api.platform.metadata.ResourceDescriptor;
import org.creekservice.api.platform.metadata.ServiceDescriptor;
import org.creekservice.api.platform.resource.ResourceInitializer;
import org.creekservice.api.system.test.extension.component.definition.ComponentDefinition;
Expand Down Expand Up @@ -57,7 +60,19 @@ public final class InitializeResourcesListener implements TestEnvironmentListene
public InitializeResourcesListener(final SystemTest api) {
this(
api,
ResourceInitializer.resourceInitializer(api.extensions().model()::resourceHandler));
ResourceInitializer.resourceInitializer(
new ResourceInitializer.ResourceCreator() {
@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public <T extends ResourceDescriptor & OwnedResource> void ensure(
final Collection<T> creatableResources) {
api.extensions()
.model()
.resourceHandler(
creatableResources.iterator().next().getClass())
.ensure((Collection) creatableResources);
}
}));
}

@VisibleForTesting
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2023 Creek Contributors (https://github.com/creek-service)
*
* 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 org.creekservice.internal.system.test.executor.execution.listener;

import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.groupingBy;

import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.creekservice.api.platform.metadata.ComponentDescriptor;
import org.creekservice.api.platform.metadata.ResourceDescriptor;
import org.creekservice.api.service.extension.component.model.ResourceHandler;
import org.creekservice.api.system.test.extension.component.definition.ComponentDefinition;
import org.creekservice.api.system.test.extension.test.env.listener.TestEnvironmentListener;
import org.creekservice.api.system.test.extension.test.model.CreekTestSuite;
import org.creekservice.internal.system.test.executor.api.SystemTest;

/**
* Test listener that calls back into each client extension to allow it to any initialise internal
* state required to service requests on the resources it handles.
*/
public final class PrepareResourcesListener implements TestEnvironmentListener {

private final SystemTest api;

public PrepareResourcesListener(final SystemTest api) {
this.api = requireNonNull(api, "api");
}

@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public void beforeSuite(final CreekTestSuite suite) {
final Map<URI, ResourceDescriptor> byId =
api.components().definitions().stream()
.map(ComponentDefinition::descriptor)
.flatMap(Optional::stream)
.flatMap(ComponentDescriptor::resources)
.collect(
groupingBy(
ResourceDescriptor::id,
Collectors.collectingAndThen(
Collectors.toList(), l -> l.get(0))));

final Map<Class<? extends ResourceDescriptor>, List<ResourceDescriptor>> byType =
byId.values().stream().collect(groupingBy(ResourceDescriptor::getClass));

byType.forEach(
(type, resources) -> {
final ResourceHandler handler = api.extensions().model().resourceHandler(type);
handler.prepare(resources);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

/**
* A test lifecycle listener that resets theServiceContainer and stops any services left running at
* the end of a test suite.
* the end of a test suite, and closes any extensions.
*/
public final class SuiteCleanUpListener implements TestEnvironmentListener {

Expand All @@ -47,5 +47,6 @@ public void beforeSuite(final CreekTestSuite suite) {
@Override
public void afterSuite(final CreekTestSuite suite, final TestSuiteResult result) {
api.tests().env().currentSuite().services().forEach(ServiceInstance::stop);
api.extensions().close();
}
}
Loading

0 comments on commit f84222e

Please sign in to comment.