blob: fc635dd170bfd319208646897cd53b4dc69a5b1c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you 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.apache.calcite.test;
import org.apache.calcite.config.CalciteSystemProperty;
import org.apache.calcite.util.Sources;
import org.apache.calcite.util.TestUtil;
import org.apache.cassandra.concurrent.Stage;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.service.CassandraDaemon;
import org.apache.cassandra.service.StorageService;
import org.apache.commons.lang3.SystemUtils;
import com.datastax.oss.driver.api.core.CqlSession;
import com.google.common.collect.ImmutableMap;
import org.cassandraunit.utils.EmbeddedCassandraServerHelper;
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.reflect.Field;
import java.time.Duration;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
/**
* JUnit5 extension to start and stop embedded Cassandra server.
*
* <p>Note that tests will be skipped if running on JDK11+, Eclipse OpenJ9 JVM or Windows
* (the first isn't supported by cassandra-unit, the last two by Cassandra) see
* <a href="https://github.com/jsevellec/cassandra-unit/issues/294">cassandra-unit issue #294</a>
* <a href="https://issues.apache.org/jira/browse/CASSANDRA-14883">CASSANDRA-14883</a>,
* and <a href="https://issues.apache.org/jira/browse/CASSANDRA-16956">CASSANDRA-16956</a>,
* respectively.
*/
class CassandraExtension implements ParameterResolver, ExecutionCondition {
private static final ExtensionContext.Namespace NAMESPACE =
ExtensionContext.Namespace.create(CassandraExtension.class);
private static final String KEY = "cassandra";
@Override public boolean supportsParameter(final ParameterContext parameterContext,
final ExtensionContext extensionContext) throws ParameterResolutionException {
final Class<?> type = parameterContext.getParameter().getType();
return CqlSession.class.isAssignableFrom(type);
}
@Override public Object resolveParameter(final ParameterContext parameterContext,
final ExtensionContext extensionContext) throws ParameterResolutionException {
Class<?> type = parameterContext.getParameter().getType();
if (CqlSession.class.isAssignableFrom(type)) {
return getOrCreate(extensionContext).session;
}
throw new ExtensionConfigurationException(
String.format(Locale.ROOT, "%s supports only %s but yours was %s",
CassandraExtension.class.getSimpleName(), CqlSession.class.getName(), type.getName()));
}
static ImmutableMap<String, String> getDataset(String resourcePath) {
return ImmutableMap.of("model",
Sources.of(Objects.requireNonNull(CassandraExtension.class.getResource(resourcePath)))
.file().getAbsolutePath());
}
/** Registers a Cassandra resource in root context, so it can be shared with
* other tests. */
private static CassandraResource getOrCreate(ExtensionContext context) {
// same cassandra instance should be shared across all extension instances
return context.getRoot()
.getStore(NAMESPACE)
.getOrComputeIfAbsent(KEY, key -> new CassandraResource(), CassandraResource.class);
}
/**
* Whether to run this test.
*
* <p>Enabled by default, unless explicitly disabled
* from command line ({@code -Dcalcite.test.cassandra=false}) or running on incompatible JDK
* version or JVM (see below).
*
* <p>cassandra-unit does not support JDK11+ yet, therefore all cassandra tests will be skipped
* if this JDK version is used.
*
* @see <a href="https://github.com/jsevellec/cassandra-unit/issues/294">cassandra-unit issue #294</a>
*
* <p>Cassandra does not support Eclipse OpenJ9 JVM, therefore all cassandra tests will be
* skipped if this JVM version is used.
*
* @see <a href="https://issues.apache.org/jira/browse/CASSANDRA-14883">CASSANDRA-14883</a>
*
* <p>Cassandra requires method
* {@link com.google.common.collect.ImmutableSet#builderWithExpectedSize(int)}
* and therefore Guava 23 or higher.
*
* <p>Cassandra dropped support for Windows in 4.1.x
* @see <a href="https://issues.apache.org/jira/browse/CASSANDRA-16956">CASSANDRA-16956</a>
*
* @return {@code true} if test is compatible with current environment,
* {@code false} otherwise
*/
@Override public ConditionEvaluationResult evaluateExecutionCondition(
final ExtensionContext context) {
boolean enabled = CalciteSystemProperty.TEST_CASSANDRA.value();
// remove JDK version check once cassandra-unit supports JDK11+
boolean compatibleJdk = TestUtil.getJavaMajorVersion() < 11;
boolean compatibleGuava = TestUtil.getGuavaMajorVersion() >= 23;
// remove JVM check once Cassandra supports Eclipse OpenJ9 JVM
boolean compatibleJVM = !"Eclipse OpenJ9".equals(TestUtil.getJavaVirtualMachineVendor());
boolean compatibleOS = !SystemUtils.IS_OS_WINDOWS;
if (enabled && compatibleJdk && compatibleGuava && compatibleJVM && compatibleOS) {
return ConditionEvaluationResult.enabled("Cassandra tests enabled");
}
final StringBuilder sb = new StringBuilder("Cassandra tests disabled:");
if (!compatibleJdk) {
sb.append("\n - JDK11+ is not supported");
}
if (!compatibleGuava) {
sb.append("\n - Guava versions < 23 are not supported");
}
if (!compatibleJVM) {
sb.append("\n - Eclipse OpenJ9 JVM is not supported");
}
if (!compatibleOS) {
sb.append("\n - Cassandra doesn't support Windows");
}
return ConditionEvaluationResult.disabled(sb.toString());
}
/** Cassandra resource. */
private static class CassandraResource
implements ExtensionContext.Store.CloseableResource {
private final CqlSession session;
private CassandraResource() {
startCassandra();
this.session = EmbeddedCassandraServerHelper.getSession();
}
/**
* Best effort to gracefully shutdown <strong>embedded</strong> cassandra
* cluster.
*
* <p>Since it uses many static variables as well as {@link System#exit(int)}
* during close, clean shutdown (as part of unit test) is not
* straightforward.
*/
@Override public void close() {
session.close();
CassandraDaemon daemon = extractDaemon();
daemon.stopNativeTransport();
StorageService storage = StorageService.instance;
storage.setRpcReady(false);
storage.stopClient();
storage.stopTransports();
try {
storage.drain(); // try to close all resources
} catch (IOException | ExecutionException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Stage.shutdownNow();
}
private static void startCassandra() {
// This static init is necessary otherwise tests fail with CassandraUnit in IntelliJ (jdk10)
// should be called right after constructor
// NullPointerException for DatabaseDescriptor.getDiskFailurePolicy
// for more info see
// https://github.com/jsevellec/cassandra-unit/issues/249
// https://github.com/jsevellec/cassandra-unit/issues/221
DatabaseDescriptor.daemonInitialization();
// Apache Jenkins often fails with
// Cassandra daemon did not start within timeout (20 sec by default)
try {
EmbeddedCassandraServerHelper.startEmbeddedCassandra(Duration.ofMinutes(2).toMillis());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Extract {@link CassandraDaemon} instance using reflection. It will be used
* to shutdown the cluster
*/
private static CassandraDaemon extractDaemon() {
try {
Field field = EmbeddedCassandraServerHelper.class.getDeclaredField("cassandraDaemon");
field.setAccessible(true);
CassandraDaemon daemon = (CassandraDaemon) field.get(null);
if (daemon == null) {
throw new IllegalStateException("Cassandra daemon was not initialized by "
+ EmbeddedCassandraServerHelper.class.getSimpleName());
}
return daemon;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
}