/**
* Copyright 2011-2017 Asakusa Framework Team.
*
* 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.asakusafw.testdriver;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.apache.hadoop.conf.Configuration;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import com.asakusafw.runtime.core.BatchContext;
import com.asakusafw.runtime.core.Report;
import com.asakusafw.runtime.directio.DataFormat;
import com.asakusafw.runtime.flow.RuntimeResourceManager;
import com.asakusafw.runtime.model.DataModel;
import com.asakusafw.runtime.stage.StageConstants;
import com.asakusafw.runtime.testing.MockResult;
import com.asakusafw.runtime.util.VariableTable;
import com.asakusafw.runtime.util.VariableTable.RedefineStrategy;
import com.asakusafw.testdriver.core.DataModelDefinition;
import com.asakusafw.testdriver.core.DataModelSourceFactory;
import com.asakusafw.testdriver.core.TestContext;
import com.asakusafw.testdriver.core.TestDataToolProvider;
import com.asakusafw.testdriver.core.TestToolRepository;
import com.asakusafw.testdriver.core.TestingEnvironmentConfigurator;
import com.asakusafw.testdriver.hadoop.ConfigurationFactory;
import com.asakusafw.testdriver.loader.BasicDataLoader;
import com.asakusafw.testdriver.loader.DataLoader;
import com.asakusafw.utils.io.Provider;
import com.asakusafw.utils.io.Source;
/**
* An <em>Operator DSL</em> test helper which enables framework APIs.
* Application developers can use this class like as following:
<pre><code>
@Rule
public OperatorTestEnvironment resource = new OperatorTestEnvironment();
</code></pre>
* The above activates a configuration file {@code asakusa-resources.xml} on the current class-path,
* and enables framework APIs (e.g. {@link Report Report API}) using the configuration.
* Clients can also use alternative configuration files by specifying their paths:
<pre><code>
@Rule
public OperatorTestEnvironment resource = new OperatorTestEnvironment("com/example/testing.xml");
</code></pre>
* Additionally, clients can also put batch arguments or extra configuration items:
<pre><code>
@Rule
public OperatorTestEnvironment resource = new OperatorTestEnvironment(...);
@Test
public void sometest() {
resource.configure("key", "value");
resource.setBatchArg("date", "2011/03/31");
...
resource.reload();
<test code>
}
</code></pre>
* @since 0.1.0
* @version 0.9.1
*/
public class OperatorTestEnvironment extends DriverElementBase implements TestRule {
static {
TestingEnvironmentConfigurator.initialize();
}
/**
* The embedded default configuration file.
* @since 0.7.0
*/
static final String DEFAULT_CONFIGURATION_PATH = "default-asakusa-resources.xml"; //$NON-NLS-1$
private RuntimeResourceManager manager;
private final String configurationPath;
private final boolean explicitConfigurationPath;
private final Map<String, String> batchArguments;
private final Map<String, String> extraConfigurations;
private boolean dirty;
private volatile Class<?> testClass;
private volatile TestToolRepository testTools;
/**
* Creates a new instance with the default configuration file.
*/
public OperatorTestEnvironment() {
this(RuntimeResourceManager.CONFIGURATION_FILE_NAME, false);
}
/**
* Creates a new instance.
* @param configurationPath the configuration file location (relative from the class-path)
* @throws IllegalArgumentException if the parameter is {@code null}
*/
public OperatorTestEnvironment(String configurationPath) {
this(configurationPath, true);
}
private OperatorTestEnvironment(String configurationPath, boolean explicit) {
if (configurationPath == null) {
throw new IllegalArgumentException("configurationPath must not be null"); //$NON-NLS-1$
}
this.configurationPath = configurationPath;
this.explicitConfigurationPath = explicit;
this.extraConfigurations = new HashMap<>();
this.batchArguments = new HashMap<>();
this.dirty = false;
}
@Override
public Statement apply(Statement base, Description description) {
reset(description.getTestClass());
return new Statement() {
@Override
public void evaluate() throws Throwable {
before();
try {
base.evaluate();
} finally {
after();
}
}
};
}
OperatorTestEnvironment reset(Class<?> contextClass) {
this.testClass = contextClass;
return this;
}
@Override
protected Class<?> getCallerClass() {
if (testClass == null) {
throw new IllegalStateException(
Messages.getString("OperatorTestEnvironment.errorNotInitialized")); //$NON-NLS-1$
}
return testClass;
}
@Override
protected TestDataToolProvider getTestTools() {
TestToolRepository result = testTools;
if (result == null) {
Class<?> caller = getCallerClass();
result = new TestToolRepository(caller.getClassLoader());
testTools = result;
}
return result;
}
/**
* Invoked before running test case.
*/
protected void before() {
Configuration conf = createConfig();
for (Map.Entry<String, String> entry : extraConfigurations.entrySet()) {
conf.set(entry.getKey(), entry.getValue());
}
if (batchArguments.isEmpty() == false) {
VariableTable variables = new VariableTable(RedefineStrategy.OVERWRITE);
for (Map.Entry<String, String> entry : batchArguments.entrySet()) {
variables.defineVariable(entry.getKey(), entry.getValue());
}
conf.set(StageConstants.PROP_ASAKUSA_BATCH_ARGS, variables.toSerialString());
}
manager = new RuntimeResourceManager(conf);
try {
manager.setup();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Adds a configuration item.
* Please invoke {@link #reload()} to active this change before executing the test target.
* @param key the configuration key name
* @param value the configuration value, or {@code null} to unset the target configuration
* @throws IllegalArgumentException if {@code key} is {@code null}
* @see #reload()
*/
public void configure(String key, String value) {
if (key == null) {
throw new IllegalArgumentException("key must not be null"); //$NON-NLS-1$
}
if (value != null) {
extraConfigurations.put(key, value);
} else {
extraConfigurations.remove(key);
}
dirty = true;
}
/**
* Adds a batch argument.
* Clients can obtain batch arguments via {@link BatchContext#get(String) context API}.
* Please invoke {@link #reload()} to active this change before executing the test target.
* @param key the argument name
* @param value the argument value, or {@code null} to unset the target argument
* @throws IllegalArgumentException if {@code key} is {@code null}
* @see #reload()
*/
public void setBatchArg(String key, String value) {
if (key == null) {
throw new IllegalArgumentException("key must not be null"); //$NON-NLS-1$
}
if (value != null) {
batchArguments.put(key, value);
} else {
batchArguments.remove(key);
}
dirty = true;
}
/**
* Reloads the configuration file and activates changes.
*/
public void reload() {
dirty = false;
after();
before();
}
/**
* Returns a new configuration object for {@link RuntimeResourceManager}.
* @return the created configuration object
*/
protected Configuration createConfig() {
Configuration conf = ConfigurationFactory.getDefault().newInstance();
URL resource = conf.getClassLoader().getResource(configurationPath);
if (resource == null && explicitConfigurationPath == false) {
// if implicit configuration file is not found, we use the embedded default configuration file
resource = OperatorTestEnvironment.class.getResource(DEFAULT_CONFIGURATION_PATH);
}
if (resource == null) {
throw new IllegalStateException(MessageFormat.format(
Messages.getString("OperatorTestEnvironment.errorMissingConfigurationFile"), //$NON-NLS-1$
configurationPath));
}
for (Map.Entry<String, String> entry : extraConfigurations.entrySet()) {
conf.set(entry.getKey(), entry.getValue());
}
conf.addResource(resource);
return conf;
}
/**
* Returns the {@link TestContext} for the current environment.
* @return {@link TestContext} object
* @since 0.7.3
*/
public TestContext getTestContext() {
return new Context(getCallerClass().getClassLoader(), batchArguments);
}
/**
* Returns the Configuration object for the current environment.
* @return the Configuration object
* @since 0.7.3
*/
public Configuration getConfiguration() {
return createConfig();
}
/**
* Returns a new {@link MockResult}.
* The returned object will create copies of the incoming objects.
* @param <T> the data type
* @param dataType the data type
* @return the created {@link MockResult}
* @since 0.9.1
*/
public <T extends DataModel<T>> MockResult<T> newResult(Class<T> dataType) {
Objects.requireNonNull(dataType);
return new MockResult<T>() {
@Override
protected T bless(T result) {
try {
T copy = dataType.newInstance();
copy.copyFrom(result);
return copy;
} catch (ReflectiveOperationException e) {
throw new IllegalStateException(e);
}
}
};
}
/**
* Returns a new data loader.
* @param <T> the data type
* @param dataType the data type
* @param sourcePath the path to test data set (relative from the current test case class)
* @return the created loader
* @since 0.9.1
*/
public <T> DataLoader<T> loader(Class<T> dataType, String sourcePath) {
Objects.requireNonNull(sourcePath);
return loader(dataType, toDataModelSourceFactory(sourcePath));
}
/**
* Returns a new data loader.
* @param <T> the data type
* @param dataType the data type
* @param objects the test data objects
* @return the created loader
* @since 0.9.1
*/
public <T> DataLoader<T> loader(Class<T> dataType, Iterable<? extends T> objects) {
Objects.requireNonNull(objects);
return loader(dataType, toDataModelSourceFactory(toDataModelDefinition(dataType), objects));
}
/**
* Returns a new data loader.
* @param <T> the data type
* @param dataType the data type
* @param provider the test data set provider
* @return the created loader
* @since 0.9.1
*/
public <T> DataLoader<T> loader(Class<T> dataType, Provider<? extends Source<? extends T>> provider) {
Objects.requireNonNull(provider);
return loader(dataType, toDataModelSourceFactory(provider));
}
/**
* Returns a new data loader.
* Note that, the original source path may be changed if tracking source file name.
* To keep the source file path information, please use {@link #loader(Class, Class, File)} instead.
* @param <T> the data type
* @param dataType the data type
* @param formatClass the data format class
* @param sourcePath the input file path on the class path
* @return the created loader
* @since 0.9.1
*/
public <T> DataLoader<T> loader(
Class<T> dataType, Class<? extends DataFormat<? super T>> formatClass, String sourcePath) {
return loader(dataType, toDataModelSourceFactory(toDataModelDefinition(dataType), formatClass, sourcePath));
}
/**
* Returns a new data loader.
* @param <T> the data type
* @param dataType the data type
* @param formatClass the data format class
* @param file the input file path on the class path
* @return the created loader
* @since 0.9.1
*/
public <T> DataLoader<T> loader(
Class<T> dataType, Class<? extends DataFormat<? super T>> formatClass, File file) {
return loader(dataType, toDataModelSourceFactory(toDataModelDefinition(dataType), formatClass, file));
}
/**
* Returns a new data loader.
* @param <T> the data type
* @param dataType the data type
* @param factory factory which provides test data set
* @return the created loader
* @since 0.9.1
*/
public <T> DataLoader<T> loader(Class<T> dataType, DataModelSourceFactory factory) {
Objects.requireNonNull(factory);
return new BasicDataLoader<>(getTestContext(), toDataModelDefinition(dataType), factory);
}
private <T> DataModelDefinition<T> toDataModelDefinition(Class<T> dataType) {
try {
return getTestTools().toDataModelDefinition(dataType);
} catch (IOException e) {
throw new IllegalStateException(MessageFormat.format(
"failed to analyze the data model type: {0}",
dataType.getName()), e);
}
}
/**
* Invoked after ran test case.
*/
protected void after() {
if (manager != null) {
try {
manager.cleanup();
} catch (Exception e) {
e.printStackTrace();
}
}
if (dirty) {
throw new AssertionError(MessageFormat.format(
Messages.getString("OperatorTestEnvironment.errorNotReloaded"), //$NON-NLS-1$
"configure()", //$NON-NLS-1$
"reload()")); //$NON-NLS-1$
}
}
private static final class Context implements TestContext {
private final ClassLoader classLoader;
private final Map<String, String> arguments;
Context(ClassLoader classLoader, Map<String, String> arguments) {
this.classLoader = classLoader;
this.arguments = arguments;
}
@Override
public ClassLoader getClassLoader() {
return classLoader;
}
@Override
public Map<String, String> getEnvironmentVariables() {
return System.getenv();
}
@Override
public Map<String, String> getArguments() {
return arguments;
}
}
}