|
@@ -0,0 +1,194 @@
|
|
|
+/*
|
|
|
+ * 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.hadoop.fs.contract;
|
|
|
+
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+
|
|
|
+import org.assertj.core.api.Assertions;
|
|
|
+import org.junit.Assume;
|
|
|
+import org.junit.Test;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+
|
|
|
+import org.apache.hadoop.fs.EtagSource;
|
|
|
+import org.apache.hadoop.fs.FileStatus;
|
|
|
+import org.apache.hadoop.fs.FileSystem;
|
|
|
+import org.apache.hadoop.fs.LocatedFileStatus;
|
|
|
+import org.apache.hadoop.fs.Path;
|
|
|
+
|
|
|
+import static org.apache.hadoop.fs.CommonPathCapabilities.ETAGS_AVAILABLE;
|
|
|
+import static org.apache.hadoop.fs.CommonPathCapabilities.ETAGS_PRESERVED_IN_RENAME;
|
|
|
+
|
|
|
+/**
|
|
|
+ * For filesystems which support etags, validate correctness
|
|
|
+ * of their implementation.
|
|
|
+ */
|
|
|
+public abstract class AbstractContractEtagTest extends
|
|
|
+ AbstractFSContractTestBase {
|
|
|
+
|
|
|
+ private static final Logger LOG =
|
|
|
+ LoggerFactory.getLogger(AbstractContractEtagTest.class);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * basic consistency across operations, as well as being non-empty.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testEtagConsistencyAcrossListAndHead() throws Throwable {
|
|
|
+ describe("Etag values must be non-empty and consistent across LIST and HEAD Calls.");
|
|
|
+ final Path path = methodPath();
|
|
|
+ final FileSystem fs = getFileSystem();
|
|
|
+
|
|
|
+ Assertions.assertThat(fs.hasPathCapability(path, ETAGS_AVAILABLE))
|
|
|
+ .describedAs("path capability %s of %s",
|
|
|
+ ETAGS_AVAILABLE, path)
|
|
|
+ .isTrue();
|
|
|
+
|
|
|
+ ContractTestUtils.touch(fs, path);
|
|
|
+
|
|
|
+ final FileStatus st = fs.getFileStatus(path);
|
|
|
+ final String etag = etagFromStatus(st);
|
|
|
+ LOG.info("etag of empty file is \"{}\"", etag);
|
|
|
+
|
|
|
+ final FileStatus[] statuses = fs.listStatus(path);
|
|
|
+ Assertions.assertThat(statuses)
|
|
|
+ .describedAs("List(%s)", path)
|
|
|
+ .hasSize(1);
|
|
|
+ final FileStatus lsStatus = statuses[0];
|
|
|
+ Assertions.assertThat(etagFromStatus(lsStatus))
|
|
|
+ .describedAs("etag of list status (%s) compared to HEAD value of %s", lsStatus, st)
|
|
|
+ .isEqualTo(etag);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get an etag from a FileStatus which MUST BE
|
|
|
+ * an implementation of EtagSource and
|
|
|
+ * whose etag MUST NOT BE null/empty.
|
|
|
+ * @param st the status
|
|
|
+ * @return the etag
|
|
|
+ */
|
|
|
+ String etagFromStatus(FileStatus st) {
|
|
|
+ Assertions.assertThat(st)
|
|
|
+ .describedAs("FileStatus %s", st)
|
|
|
+ .isInstanceOf(EtagSource.class);
|
|
|
+ final String etag = ((EtagSource) st).getEtag();
|
|
|
+ Assertions.assertThat(etag)
|
|
|
+ .describedAs("Etag of %s", st)
|
|
|
+ .isNotBlank();
|
|
|
+ return etag;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Overwritten data has different etags.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testEtagsOfDifferentDataDifferent() throws Throwable {
|
|
|
+ describe("Verify that two different blocks of data written have different tags");
|
|
|
+
|
|
|
+ final Path path = methodPath();
|
|
|
+ final FileSystem fs = getFileSystem();
|
|
|
+ Path src = new Path(path, "src");
|
|
|
+
|
|
|
+ ContractTestUtils.createFile(fs, src, true,
|
|
|
+ "data1234".getBytes(StandardCharsets.UTF_8));
|
|
|
+ final FileStatus srcStatus = fs.getFileStatus(src);
|
|
|
+ final String srcTag = etagFromStatus(srcStatus);
|
|
|
+ LOG.info("etag of file 1 is \"{}\"", srcTag);
|
|
|
+
|
|
|
+ // now overwrite with data of same length
|
|
|
+ // (ensure that path or length aren't used exclusively as tag)
|
|
|
+ ContractTestUtils.createFile(fs, src, true,
|
|
|
+ "1234data".getBytes(StandardCharsets.UTF_8));
|
|
|
+
|
|
|
+ // validate
|
|
|
+ final String tag2 = etagFromStatus(fs.getFileStatus(src));
|
|
|
+ LOG.info("etag of file 2 is \"{}\"", tag2);
|
|
|
+
|
|
|
+ Assertions.assertThat(tag2)
|
|
|
+ .describedAs("etag of updated file")
|
|
|
+ .isNotEqualTo(srcTag);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * If supported, rename preserves etags.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testEtagConsistencyAcrossRename() throws Throwable {
|
|
|
+ describe("Verify that when a file is renamed, the etag remains unchanged");
|
|
|
+ final Path path = methodPath();
|
|
|
+ final FileSystem fs = getFileSystem();
|
|
|
+ Assume.assumeTrue(
|
|
|
+ "Filesystem does not declare that etags are preserved across renames",
|
|
|
+ fs.hasPathCapability(path, ETAGS_PRESERVED_IN_RENAME));
|
|
|
+ Path src = new Path(path, "src");
|
|
|
+ Path dest = new Path(path, "dest");
|
|
|
+
|
|
|
+ ContractTestUtils.createFile(fs, src, true,
|
|
|
+ "sample data".getBytes(StandardCharsets.UTF_8));
|
|
|
+ final FileStatus srcStatus = fs.getFileStatus(src);
|
|
|
+ LOG.info("located file status string value " + srcStatus);
|
|
|
+
|
|
|
+ final String srcTag = etagFromStatus(srcStatus);
|
|
|
+ LOG.info("etag of short file is \"{}\"", srcTag);
|
|
|
+
|
|
|
+ Assertions.assertThat(srcTag)
|
|
|
+ .describedAs("Etag of %s", srcStatus)
|
|
|
+ .isNotBlank();
|
|
|
+
|
|
|
+ // rename
|
|
|
+ fs.rename(src, dest);
|
|
|
+
|
|
|
+ // validate
|
|
|
+ FileStatus destStatus = fs.getFileStatus(dest);
|
|
|
+ final String destTag = etagFromStatus(destStatus);
|
|
|
+ Assertions.assertThat(destTag)
|
|
|
+ .describedAs("etag of list status (%s) compared to HEAD value of %s",
|
|
|
+ destStatus, srcStatus)
|
|
|
+ .isEqualTo(srcTag);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * For effective use of etags, listLocatedStatus SHOULD return status entries
|
|
|
+ * with consistent values.
|
|
|
+ * This ensures that listing during query planning can collect and use the etags.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testLocatedStatusAlsoHasEtag() throws Throwable {
|
|
|
+ describe("verify that listLocatedStatus() and listFiles() are etag sources");
|
|
|
+ final Path path = methodPath();
|
|
|
+ final FileSystem fs = getFileSystem();
|
|
|
+ Path src = new Path(path, "src");
|
|
|
+ ContractTestUtils.createFile(fs, src, true,
|
|
|
+ "sample data".getBytes(StandardCharsets.UTF_8));
|
|
|
+ final FileStatus srcStatus = fs.getFileStatus(src);
|
|
|
+ final String srcTag = etagFromStatus(srcStatus);
|
|
|
+ final LocatedFileStatus entry = fs.listLocatedStatus(path).next();
|
|
|
+ LOG.info("located file status string value " + entry);
|
|
|
+ final String listTag = etagFromStatus(entry);
|
|
|
+ Assertions.assertThat(listTag)
|
|
|
+ .describedAs("etag of listLocatedStatus (%s) compared to HEAD value of %s",
|
|
|
+ entry, srcStatus)
|
|
|
+ .isEqualTo(srcTag);
|
|
|
+
|
|
|
+ final LocatedFileStatus entry2 = fs.listFiles(path, false).next();
|
|
|
+ Assertions.assertThat(etagFromStatus(entry2))
|
|
|
+ .describedAs("etag of listFiles (%s) compared to HEAD value of %s",
|
|
|
+ entry, srcStatus)
|
|
|
+ .isEqualTo(srcTag);
|
|
|
+ }
|
|
|
+}
|