/** * 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.tools; import java.io.ByteArrayOutputStream; import java.io.FilterInputStream; import java.io.IOException; import java.io.PrintStream; import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.StringTokenizer; import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.impl.Log4JLogger; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.FsShell; import org.apache.hadoop.fs.HarFileSystem; import org.apache.hadoop.fs.LocalFileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.hdfs.MiniDFSCluster; import org.apache.hadoop.io.IOUtils; import org.apache.hadoop.util.JarFinder; import org.apache.hadoop.util.ToolRunner; import org.apache.hadoop.yarn.server.resourcemanager.scheduler.capacity.CapacitySchedulerConfiguration; import org.apache.log4j.Level; import org.junit.After; import org.junit.Assert; import static org.junit.Assert.*; import org.junit.Before; import org.junit.Test; /** * test {@link HadoopArchives} */ public class TestHadoopArchives { public static final String HADOOP_ARCHIVES_JAR = JarFinder .getJar(HadoopArchives.class); { ((Log4JLogger) LogFactory.getLog(org.apache.hadoop.security.Groups.class)) .getLogger().setLevel(Level.ERROR); } private static final String inputDir = "input"; private Path inputPath; private Path archivePath; private final List<String> fileList = new ArrayList<String>(); private MiniDFSCluster dfscluster; private Configuration conf; private FileSystem fs; private static String createFile(Path root, FileSystem fs, String... dirsAndFile ) throws IOException { String fileBaseName = dirsAndFile[dirsAndFile.length - 1]; return createFile(root, fs, fileBaseName.getBytes("UTF-8"), dirsAndFile); } private static String createFile(Path root, FileSystem fs, byte[] fileContent, String... dirsAndFile ) throws IOException { StringBuilder sb = new StringBuilder(); for (String segment: dirsAndFile) { if (sb.length() > 0) { sb.append(Path.SEPARATOR); } sb.append(segment); } final Path f = new Path(root, sb.toString()); final FSDataOutputStream out = fs.create(f); try { out.write(fileContent); } finally { out.close(); } return sb.toString(); } @Before public void setUp() throws Exception { conf = new Configuration(); conf.set(CapacitySchedulerConfiguration.PREFIX + CapacitySchedulerConfiguration.ROOT + "." + CapacitySchedulerConfiguration.QUEUES, "default"); conf.set(CapacitySchedulerConfiguration.PREFIX + CapacitySchedulerConfiguration.ROOT + ".default." + CapacitySchedulerConfiguration.CAPACITY, "100"); dfscluster = new MiniDFSCluster .Builder(conf) .checkExitOnShutdown(true) .numDataNodes(2) .format(true) .racks(null) .build(); fs = dfscluster.getFileSystem(); // prepare archive path: archivePath = new Path(fs.getHomeDirectory(), "archive"); fs.delete(archivePath, true); // prepare input path: inputPath = new Path(fs.getHomeDirectory(), inputDir); fs.delete(inputPath, true); fs.mkdirs(inputPath); // create basic input files: fileList.add(createFile(inputPath, fs, "a")); fileList.add(createFile(inputPath, fs, "b")); fileList.add(createFile(inputPath, fs, "c")); } @After public void tearDown() throws Exception { if (dfscluster != null) { dfscluster.shutdown(); } } @Test public void testRelativePath() throws Exception { final Path sub1 = new Path(inputPath, "dir1"); fs.mkdirs(sub1); createFile(inputPath, fs, sub1.getName(), "a"); final FsShell shell = new FsShell(conf); final List<String> originalPaths = lsr(shell, "input"); System.out.println("originalPaths: " + originalPaths); // make the archive: final String fullHarPathStr = makeArchive(); // compare results: final List<String> harPaths = lsr(shell, fullHarPathStr); Assert.assertEquals(originalPaths, harPaths); } @Test public void testPathWithSpaces() throws Exception { // create files/directories with spaces createFile(inputPath, fs, "c c"); final Path sub1 = new Path(inputPath, "sub 1"); fs.mkdirs(sub1); createFile(sub1, fs, "file x y z"); createFile(sub1, fs, "file"); createFile(sub1, fs, "x"); createFile(sub1, fs, "y"); createFile(sub1, fs, "z"); final Path sub2 = new Path(inputPath, "sub 1 with suffix"); fs.mkdirs(sub2); createFile(sub2, fs, "z"); final FsShell shell = new FsShell(conf); final String inputPathStr = inputPath.toUri().getPath(); final List<String> originalPaths = lsr(shell, inputPathStr); // make the archive: final String fullHarPathStr = makeArchive(); // compare results final List<String> harPaths = lsr(shell, fullHarPathStr); Assert.assertEquals(originalPaths, harPaths); } private static List<String> lsr(final FsShell shell, String dir) throws Exception { System.out.println("lsr root=" + dir); final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); final PrintStream out = new PrintStream(bytes); final PrintStream oldOut = System.out; final PrintStream oldErr = System.err; System.setOut(out); System.setErr(out); final String results; try { Assert.assertEquals(0, shell.run(new String[] { "-lsr", dir })); results = bytes.toString(); } finally { IOUtils.closeStream(out); System.setOut(oldOut); System.setErr(oldErr); } System.out.println("lsr results:\n" + results); String dirname = dir; if (dir.lastIndexOf(Path.SEPARATOR) != -1) { dirname = dir.substring(dir.lastIndexOf(Path.SEPARATOR)); } final List<String> paths = new ArrayList<String>(); for (StringTokenizer t = new StringTokenizer(results, "\n"); t .hasMoreTokens();) { final String s = t.nextToken(); final int i = s.indexOf(dirname); if (i >= 0) { paths.add(s.substring(i + dirname.length())); } } Collections.sort(paths); System.out .println("lsr paths = " + paths.toString().replace(", ", ",\n ")); return paths; } @Test public void testReadFileContent() throws Exception { fileList.add(createFile(inputPath, fs, "c c")); final Path sub1 = new Path(inputPath, "sub 1"); fs.mkdirs(sub1); fileList.add(createFile(inputPath, fs, sub1.getName(), "file x y z")); fileList.add(createFile(inputPath, fs, sub1.getName(), "file")); fileList.add(createFile(inputPath, fs, sub1.getName(), "x")); fileList.add(createFile(inputPath, fs, sub1.getName(), "y")); fileList.add(createFile(inputPath, fs, sub1.getName(), "z")); final Path sub2 = new Path(inputPath, "sub 1 with suffix"); fs.mkdirs(sub2); fileList.add(createFile(inputPath, fs, sub2.getName(), "z")); // Generate a big binary file content: final byte[] binContent = prepareBin(); fileList.add(createFile(inputPath, fs, binContent, sub2.getName(), "bin")); fileList.add(createFile(inputPath, fs, new byte[0], sub2.getName(), "zero-length")); final String fullHarPathStr = makeArchive(); // Create fresh HarFs: final HarFileSystem harFileSystem = new HarFileSystem(fs); try { final URI harUri = new URI(fullHarPathStr); harFileSystem.initialize(harUri, fs.getConf()); // now read the file content and compare it against the expected: int readFileCount = 0; for (final String pathStr0 : fileList) { final Path path = new Path(fullHarPathStr + Path.SEPARATOR + pathStr0); final String baseName = path.getName(); final FileStatus status = harFileSystem.getFileStatus(path); if (status.isFile()) { // read the file: final byte[] actualContentSimple = readAllSimple( harFileSystem.open(path), true); final byte[] actualContentBuffer = readAllWithBuffer( harFileSystem.open(path), true); assertArrayEquals(actualContentSimple, actualContentBuffer); final byte[] actualContentFully = readAllWithReadFully( actualContentSimple.length, harFileSystem.open(path), true); assertArrayEquals(actualContentSimple, actualContentFully); final byte[] actualContentSeek = readAllWithSeek( actualContentSimple.length, harFileSystem.open(path), true); assertArrayEquals(actualContentSimple, actualContentSeek); final byte[] actualContentRead4 = readAllWithRead4(harFileSystem.open(path), true); assertArrayEquals(actualContentSimple, actualContentRead4); final byte[] actualContentSkip = readAllWithSkip( actualContentSimple.length, harFileSystem.open(path), harFileSystem.open(path), true); assertArrayEquals(actualContentSimple, actualContentSkip); if ("bin".equals(baseName)) { assertArrayEquals(binContent, actualContentSimple); } else if ("zero-length".equals(baseName)) { assertEquals(0, actualContentSimple.length); } else { String actual = new String(actualContentSimple, "UTF-8"); assertEquals(baseName, actual); } readFileCount++; } } assertEquals(fileList.size(), readFileCount); } finally { harFileSystem.close(); } } private static byte[] readAllSimple(FSDataInputStream fsdis, boolean close) throws IOException { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { int b; while (true) { b = fsdis.read(); if (b < 0) { break; } else { baos.write(b); } } baos.close(); return baos.toByteArray(); } finally { if (close) { fsdis.close(); } } } private static byte[] readAllWithBuffer(FSDataInputStream fsdis, boolean close) throws IOException { try { final int available = fsdis.available(); final byte[] buffer; final ByteArrayOutputStream baos; if (available < 0) { buffer = new byte[1024]; baos = new ByteArrayOutputStream(buffer.length * 2); } else { buffer = new byte[available]; baos = new ByteArrayOutputStream(available); } int readIntoBuffer = 0; int read; while (true) { read = fsdis.read(buffer, readIntoBuffer, buffer.length - readIntoBuffer); if (read < 0) { // end of stream: if (readIntoBuffer > 0) { baos.write(buffer, 0, readIntoBuffer); } return baos.toByteArray(); } else { readIntoBuffer += read; if (readIntoBuffer == buffer.length) { // buffer is full, need to clean the buffer. // drop the buffered data to baos: baos.write(buffer); // reset the counter to start reading to the buffer beginning: readIntoBuffer = 0; } else if (readIntoBuffer > buffer.length) { throw new IOException("Read more than the buffer length: " + readIntoBuffer + ", buffer length = " + buffer.length); } } } } finally { if (close) { fsdis.close(); } } } private static byte[] readAllWithReadFully(int totalLength, FSDataInputStream fsdis, boolean close) throws IOException { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); // Simulate reading of some data structures of known length: final byte[] buffer = new byte[17]; final int times = totalLength / buffer.length; final int remainder = totalLength % buffer.length; // it would be simpler to leave the position tracking to the // InputStream, but we need to check the methods #readFully(2) // and #readFully(4) that receive the position as a parameter: int position = 0; try { // read "data structures": for (int i=0; i<times; i++) { fsdis.readFully(position, buffer); position += buffer.length; baos.write(buffer); } if (remainder > 0) { // read the remainder: fsdis.readFully(position, buffer, 0, remainder); position += remainder; baos.write(buffer, 0, remainder); } try { fsdis.readFully(position, buffer, 0, 1); assertTrue(false); } catch (IOException ioe) { // okay } assertEquals(totalLength, position); final byte[] result = baos.toByteArray(); assertEquals(totalLength, result.length); return result; } finally { if (close) { fsdis.close(); } } } private static byte[] readAllWithRead4(FSDataInputStream fsdis, boolean close) throws IOException { try { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final byte[] buffer = new byte[17]; int totalRead = 0; int read; while (true) { read = fsdis.read(totalRead, buffer, 0, buffer.length); if (read > 0) { totalRead += read; baos.write(buffer, 0, read); } else if (read < 0) { break; // EOF } else { // read == 0: // zero result may be returned *only* in case if the 4th // parameter is 0. Since in our case this is 'buffer.length', // zero return value clearly indicates a bug: throw new AssertionError("FSDataInputStream#read(4) returned 0, while " + " the 4th method parameter is " + buffer.length + "."); } } final byte[] result = baos.toByteArray(); return result; } finally { if (close) { fsdis.close(); } } } private static byte[] readAllWithSeek(final int totalLength, final FSDataInputStream fsdis, final boolean close) throws IOException { final byte[] result = new byte[totalLength]; long pos; try { // read the data in the reverse order, from // the tail to the head by pieces of 'buffer' length: final byte[] buffer = new byte[17]; final int times = totalLength / buffer.length; int read; int expectedRead; for (int i=times; i>=0; i--) { pos = i * buffer.length; fsdis.seek(pos); // check that seek is successful: assertEquals(pos, fsdis.getPos()); read = fsdis.read(buffer); // check we read right number of bytes: if (i == times) { expectedRead = totalLength % buffer.length; // remainder if (expectedRead == 0) { // zero remainder corresponds to the EOS, so // by the contract of DataInpitStream#read(byte[]) -1 should be // returned: expectedRead = -1; } } else { expectedRead = buffer.length; } assertEquals(expectedRead, read); if (read > 0) { System.arraycopy(buffer, 0, result, (int)pos, read); } } // finally, check that #seek() to not existing position leads to IOE: expectSeekIOE(fsdis, Long.MAX_VALUE, "Seek to Long.MAX_VALUE should lead to IOE."); expectSeekIOE(fsdis, Long.MIN_VALUE, "Seek to Long.MIN_VALUE should lead to IOE."); long pp = -1L; expectSeekIOE(fsdis, pp, "Seek to "+pp+" should lead to IOE."); // NB: is is *possible* to #seek(length), but *impossible* to #seek(length + 1): fsdis.seek(totalLength); assertEquals(totalLength, fsdis.getPos()); pp = totalLength + 1; expectSeekIOE(fsdis, pp, "Seek to the length position + 1 ("+pp+") should lead to IOE."); return result; } finally { if (close) { fsdis.close(); } } } private static void expectSeekIOE(FSDataInputStream fsdis, long seekPos, String message) { try { fsdis.seek(seekPos); assertTrue(message + " (Position = " + fsdis.getPos() + ")", false); } catch (IOException ioe) { // okay } } /* * Reads data by chunks from 2 input streams: * reads chunk from stream 1, and skips this chunk in the stream 2; * Then reads next chunk from stream 2, and skips this chunk in stream 1. */ private static byte[] readAllWithSkip( final int totalLength, final FSDataInputStream fsdis1, final FSDataInputStream fsdis2, final boolean close) throws IOException { // test negative skip arg: assertEquals(0, fsdis1.skip(-1)); // test zero skip arg: assertEquals(0, fsdis1.skip(0)); final ByteArrayOutputStream baos = new ByteArrayOutputStream(totalLength); try { // read the data in the reverse order, from // the tail to the head by pieces of 'buffer' length: final byte[] buffer = new byte[17]; final int times = totalLength / buffer.length; final int remainder = totalLength % buffer.length; long skipped; long expectedPosition; int toGo; for (int i=0; i<=times; i++) { toGo = (i < times) ? buffer.length : remainder; if (i % 2 == 0) { fsdis1.readFully(buffer, 0, toGo); skipped = skipUntilZero(fsdis2, toGo); } else { fsdis2.readFully(buffer, 0, toGo); skipped = skipUntilZero(fsdis1, toGo); } if (i < times) { assertEquals(buffer.length, skipped); expectedPosition = (i + 1) * buffer.length; } else { // remainder: if (remainder > 0) { assertEquals(remainder, skipped); } else { assertEquals(0, skipped); } expectedPosition = totalLength; } // check if the 2 streams have equal and correct positions: assertEquals(expectedPosition, fsdis1.getPos()); assertEquals(expectedPosition, fsdis2.getPos()); // save the read data: if (toGo > 0) { baos.write(buffer, 0, toGo); } } // finally, check up if ended stream cannot skip: assertEquals(0, fsdis1.skip(-1)); assertEquals(0, fsdis1.skip(0)); assertEquals(0, fsdis1.skip(1)); assertEquals(0, fsdis1.skip(Long.MAX_VALUE)); return baos.toByteArray(); } finally { if (close) { fsdis1.close(); fsdis2.close(); } } } private static long skipUntilZero(final FilterInputStream fis, final long toSkip) throws IOException { long skipped = 0; long remainsToSkip = toSkip; long s; while (skipped < toSkip) { s = fis.skip(remainsToSkip); // actually skippped if (s == 0) { return skipped; // EOF or impossible to skip. } skipped += s; remainsToSkip -= s; } return skipped; } private static byte[] prepareBin() { byte[] bb = new byte[77777]; for (int i=0; i<bb.length; i++) { // Generate unique values, as possible: double d = Math.log(i + 2); long bits = Double.doubleToLongBits(d); bb[i] = (byte)bits; } return bb; } /* * Run the HadoopArchives tool to create an archive on the * given file system. */ private String makeArchive() throws Exception { final String inputPathStr = inputPath.toUri().getPath(); System.out.println("inputPathStr = " + inputPathStr); final URI uri = fs.getUri(); final String prefix = "har://hdfs-" + uri.getHost() + ":" + uri.getPort() + archivePath.toUri().getPath() + Path.SEPARATOR; final String harName = "foo.har"; final String fullHarPathStr = prefix + harName; final String[] args = { "-archiveName", harName, "-p", inputPathStr, "*", archivePath.toString() }; System.setProperty(HadoopArchives.TEST_HADOOP_ARCHIVES_JAR_PATH, HADOOP_ARCHIVES_JAR); final HadoopArchives har = new HadoopArchives(conf); assertEquals(0, ToolRunner.run(har, args)); return fullHarPathStr; } @Test /* * Tests copying from archive file system to a local file system */ public void testCopyToLocal() throws Exception { final String fullHarPathStr = makeArchive(); // make path to copy the file to: final String tmpDir = System.getProperty("test.build.data","build/test/data") + "/work-dir/har-fs-tmp"; final Path tmpPath = new Path(tmpDir); final LocalFileSystem localFs = FileSystem.getLocal(new Configuration()); localFs.delete(tmpPath, true); localFs.mkdirs(tmpPath); assertTrue(localFs.exists(tmpPath)); // Create fresh HarFs: final HarFileSystem harFileSystem = new HarFileSystem(fs); try { final URI harUri = new URI(fullHarPathStr); harFileSystem.initialize(harUri, fs.getConf()); final Path sourcePath = new Path(fullHarPathStr + Path.SEPARATOR + "a"); final Path targetPath = new Path(tmpPath, "straus"); // copy the Har file to a local file system: harFileSystem.copyToLocalFile(false, sourcePath, targetPath); FileStatus straus = localFs.getFileStatus(targetPath); // the file should contain just 1 character: assertEquals(1, straus.getLen()); } finally { harFileSystem.close(); localFs.delete(tmpPath, true); } } }