/*
* 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 gobblin.util.test;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.permission.FsPermission;
import org.joda.time.DateTimeUtils;
import org.joda.time.DateTimeUtils.MillisProvider;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.testng.Assert;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import gobblin.configuration.ConfigurationKeys;
import gobblin.util.PathUtils;
/**
* A class to setup files and folders for a retention test.
* Class reads a setup-validate.conf file at <code>testSetupConfPath</code> to create data for a retention test.
* The config file needs to be in HOCON and parsed using {@link ConfigFactory}.
* All the paths listed at {@link #TEST_DATA_CREATE_KEY} in the config file will be created under a <code>testTempDirPath</code>.
* <br>
* After retention job is run on this data, the {@link #validate()} method can be used to validate deleted files and retained files
* listed in {@link #TEST_DATA_VALIDATE_DELETED_KEY} and {@link #TEST_DATA_VALIDATE_RETAINED_KEY} respectively.
* <br>
* Below is the format of the config file this class parses.
* <br>
* <pre>
* gobblin.test : {
// Time the system clock is set before starting the test
currentTime : "02/19/2016 11:00:00"
// Paths to create for the test
//{path: path of test file in the dataset, modTime: set the modification time of this path in "MM/dd/yyyy HH:mm:ss"}
create : [
{path:"/user/gobblin/Dataset1/Version1", modTime:"02/10/2016 10:00:00"},
{path:"/user/gobblin/Dataset1/Version2", modTime:"02/11/2016 10:00:00"},
{path:"/user/gobblin/Dataset1/Version3", modTime:"02/12/2016 10:00:00"},
{path:"/user/gobblin/Dataset1/Version4", modTime:"02/13/2016 10:00:00"},
{path:"/user/gobblin/Dataset1/Version5", modTime:"02/14/2016 10:00:00"}
]
// Validation configs to use after the test
validate : {
// Paths that should exist after retention is run
retained : [
{path:"/user/gobblin/Dataset1/Version4", modTime:"02/13/2016 10:00:00"},
{path:"/user/gobblin/Dataset1/Version5", modTime:"02/14/2016 10:00:00"}
]
// Paths that should be deleted after retention is run
deleted : [
{path:"/user/gobblin/Dataset1/Version1", modTime:"02/10/2016 10:00:00"},
{path:"/user/gobblin/Dataset1/Version2", modTime:"02/11/2016 10:00:00"},
{path:"/user/gobblin/Dataset1/Version3", modTime:"02/12/2016 10:00:00"}
]
}
}
* </pre>
*/
public class RetentionTestDataGenerator {
private static final String DATA_GENERATOR_KEY = "gobblin.test";
private static final String DATA_GENERATOR_PREFIX = DATA_GENERATOR_KEY + ".";
private static final String TEST_CURRENT_TIME_KEY = DATA_GENERATOR_PREFIX + "currentTime";
private static final String TEST_DATA_CREATE_KEY = DATA_GENERATOR_PREFIX + "create";
private static final String TEST_DATA_VALIDATE_RETAINED_KEY = DATA_GENERATOR_PREFIX + "validate.retained";
private static final String TEST_DATA_VALIDATE_DELETED_KEY = DATA_GENERATOR_PREFIX + "validate.deleted";
private static final String TEST_DATA_VALIDATE_PERMISSIONS_KEY = DATA_GENERATOR_PREFIX + "validate.permissions";
private static final String TEST_DATA_PATH_LOCAL_KEY = "path";
private static final String TEST_DATA_MOD_TIME_LOCAL_KEY = "modTime";
private static final String TEST_DATA_PERMISSIONS_KEY = "permission";
private static final DateTimeFormatter FORMATTER = DateTimeFormat.
forPattern("MM/dd/yyyy HH:mm:ss").withZone(DateTimeZone.forID(ConfigurationKeys.PST_TIMEZONE_NAME));
private final Path testTempDirPath;
private final FileSystem fs;
private final Config setupConfig;
/**
* @param testTempDirPath under which all test files are created on the FileSystem
* @param testSetupConfPath setup config file path in classpath
*/
public RetentionTestDataGenerator(Path testTempDirPath, Path testSetupConfPath, FileSystem fs) {
this.fs = fs;
this.testTempDirPath = testTempDirPath;
this.setupConfig =
ConfigFactory.parseResources(PathUtils.getPathWithoutSchemeAndAuthority(testSetupConfPath).toString());
if (!this.setupConfig.hasPath(DATA_GENERATOR_KEY)) {
throw new RuntimeException(String.format("Failed to load setup config at %s", testSetupConfPath.toString()));
}
}
/**
* Create all the paths listed under {@link #TEST_DATA_CREATE_KEY}. If a path's config has a {@link #TEST_DATA_MOD_TIME_LOCAL_KEY} specified,
* the modification time of this path is updated to this value.
*/
public void setup() throws IOException {
if (this.setupConfig.hasPath(TEST_CURRENT_TIME_KEY)) {
DateTimeUtils.setCurrentMillisProvider(new FixedThreadLocalMillisProvider(FORMATTER.parseDateTime(
setupConfig.getString(TEST_CURRENT_TIME_KEY)).getMillis()));
}
List<? extends Config> createConfigs = setupConfig.getConfigList(TEST_DATA_CREATE_KEY);
Collections.sort(createConfigs, new Comparator<Config>() {
@Override
public int compare(Config o1, Config o2) {
return o1.getString(TEST_DATA_PATH_LOCAL_KEY).compareTo(o2.getString(TEST_DATA_PATH_LOCAL_KEY));
}
});
for (Config fileToCreate : createConfigs) {
Path fullFilePath =
new Path(testTempDirPath, PathUtils.withoutLeadingSeparator(new Path(fileToCreate
.getString(TEST_DATA_PATH_LOCAL_KEY))));
if (!this.fs.mkdirs(fullFilePath)) {
throw new RuntimeException("Failed to create test file " + fullFilePath);
}
if (fileToCreate.hasPath(TEST_DATA_MOD_TIME_LOCAL_KEY)) {
File file = new File(PathUtils.getPathWithoutSchemeAndAuthority(fullFilePath).toString());
boolean modifiedFile =
file.setLastModified(FORMATTER.parseMillis(fileToCreate.getString(TEST_DATA_MOD_TIME_LOCAL_KEY)));
if (!modifiedFile) {
throw new IOException(String.format("Unable to set the last modified time for file %s!", file));
}
}
if (fileToCreate.hasPath(TEST_DATA_PERMISSIONS_KEY)) {
this.fs.setPermission(fullFilePath, new FsPermission(fileToCreate.getString(TEST_DATA_PERMISSIONS_KEY)));
}
}
}
/**
* Validate that all paths in {@link #TEST_DATA_VALIDATE_DELETED_KEY} are deleted and
* all paths in {@link #TEST_DATA_VALIDATE_RETAINED_KEY} still exist
*/
public void validate() throws IOException {
List<? extends Config> retainedConfigs = setupConfig.getConfigList(TEST_DATA_VALIDATE_RETAINED_KEY);
for (Config retainedConfig : retainedConfigs) {
Path fullFilePath =
new Path(testTempDirPath, PathUtils.withoutLeadingSeparator(new Path(retainedConfig
.getString(TEST_DATA_PATH_LOCAL_KEY))));
Assert.assertTrue(this.fs.exists(fullFilePath),
String.format("%s should not be deleted", fullFilePath.toString()));
}
List<? extends Config> deletedConfigs = setupConfig.getConfigList(TEST_DATA_VALIDATE_DELETED_KEY);
for (Config retainedConfig : deletedConfigs) {
Path fullFilePath =
new Path(testTempDirPath, PathUtils.withoutLeadingSeparator(new Path(retainedConfig
.getString(TEST_DATA_PATH_LOCAL_KEY))));
Assert.assertFalse(this.fs.exists(fullFilePath), String.format("%s should be deleted", fullFilePath.toString()));
}
// Validate permissions
if (setupConfig.hasPath(TEST_DATA_VALIDATE_PERMISSIONS_KEY)) {
List<? extends Config> permissionsConfigs = setupConfig.getConfigList(TEST_DATA_VALIDATE_PERMISSIONS_KEY);
for (Config permissionsConfig : permissionsConfigs) {
Path fullFilePath =
new Path(testTempDirPath, PathUtils.withoutLeadingSeparator(new Path(permissionsConfig
.getString(TEST_DATA_PATH_LOCAL_KEY))));
Assert.assertEquals(this.fs.getFileStatus(fullFilePath).getPermission(),
new FsPermission(permissionsConfig.getString(TEST_DATA_PERMISSIONS_KEY)),
String.format("Permissions check failed for %s", fullFilePath));
}
}
this.cleanup();
}
public void cleanup() throws IOException {
DateTimeUtils.setCurrentMillisSystem();
if (this.fs.exists(testTempDirPath)) {
if (!this.fs.delete(testTempDirPath, true)) {
throw new IOException("Failed to clean up path " + this.testTempDirPath);
}
}
}
/**
* A Joda time {@link MillisProvider} used provide a mock fixed current time.
* Needs to be thread local as TestNg tests may run in parallel
*/
public static class FixedThreadLocalMillisProvider implements MillisProvider {
private ThreadLocal<Long> currentTimeThreadLocal = new ThreadLocal<Long>() {
@Override
protected Long initialValue() {
return System.currentTimeMillis();
}
};
public FixedThreadLocalMillisProvider(Long millis) {
currentTimeThreadLocal.set(millis);
}
@Override
public long getMillis() {
return currentTimeThreadLocal.get();
}
}
}