/**
 * 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.hdfs.server.namenode.snapshot;

import static org.apache.hadoop.hdfs.server.namenode.INodeId.INVALID_INODE_ID;
import static org.junit.Assert.*;

import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.security.PrivilegedAction;

import org.apache.commons.lang3.RandomUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.FsShell;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.SafeModeAction;
import org.apache.hadoop.hdfs.DFSConfigKeys;
import org.apache.hadoop.hdfs.DFSTestUtil;
import org.apache.hadoop.hdfs.DFSUtil;
import org.apache.hadoop.hdfs.DFSUtilClient;
import org.apache.hadoop.hdfs.DistributedFileSystem;
import org.apache.hadoop.hdfs.MiniDFSCluster;
import org.apache.hadoop.hdfs.MiniDFSNNTopology;
import org.apache.hadoop.hdfs.protocol.HdfsConstants;
import org.apache.hadoop.hdfs.protocol.SnapshotDiffReport;
import org.apache.hadoop.hdfs.server.blockmanagement.BlockInfo;
import org.apache.hadoop.hdfs.server.blockmanagement.BlockManager;
import org.apache.hadoop.hdfs.server.blockmanagement.BlockManagerTestUtil;
import org.apache.hadoop.hdfs.server.namenode.FSDirectory;
import org.apache.hadoop.hdfs.server.namenode.FSNamesystem;
import org.apache.hadoop.hdfs.server.namenode.INode;
import org.apache.hadoop.hdfs.server.namenode.INodeDirectory;
import org.apache.hadoop.hdfs.server.namenode.INodeFile;
import org.apache.hadoop.hdfs.server.namenode.NameNode;
import org.apache.hadoop.hdfs.server.namenode.NameNodeAdapter;
import org.apache.hadoop.hdfs.server.namenode.QuotaCounts;
import org.apache.hadoop.hdfs.server.namenode.ha.HATestUtil;
import org.apache.hadoop.hdfs.server.namenode.snapshot.DirectoryWithSnapshotFeature.DirectoryDiffList;
import org.apache.hadoop.hdfs.util.ReadOnlyList;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.ipc.RemoteException;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.test.GenericTestUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Tests snapshot deletion.
 */
public class TestSnapshotDeletion {
  private static final Logger LOG =
      LoggerFactory.getLogger(TestSnapshotDeletion.class);
  protected static final long seed = 0;
  protected static final short REPLICATION = 3;
  protected static final short REPLICATION_1 = 2;
  protected static final long BLOCKSIZE = 1024;
  
  private final Path dir = new Path("/TestSnapshot");
  private final Path sub = new Path(dir, "sub1");
  private final Path subsub = new Path(sub, "subsub1");
  
  protected Configuration conf;
  protected MiniDFSCluster cluster;
  protected FSNamesystem fsn;
  protected FSDirectory fsdir;
  protected BlockManager blockmanager;
  protected DistributedFileSystem hdfs;
  
  @Rule
  public ExpectedException exception = ExpectedException.none();

  @Before
  public void setUp() throws Exception {
    conf = new Configuration();
    cluster = new MiniDFSCluster.Builder(conf).numDataNodes(REPLICATION)
        .format(true).build();
    cluster.waitActive();

    fsn = cluster.getNamesystem();
    fsdir = fsn.getFSDirectory();
    blockmanager = fsn.getBlockManager();
    hdfs = cluster.getFileSystem();
  }

  @After
  public void tearDown() throws Exception {
    if (cluster != null) {
      cluster.shutdown();
      cluster = null;
    }
  }
    
  /**
   * Deleting snapshottable directory with snapshots must fail.
   */
  @Test (timeout=300000)
  public void testDeleteDirectoryWithSnapshot() throws Exception {
    Path file0 = new Path(sub, "file0");
    Path file1 = new Path(sub, "file1");
    DFSTestUtil.createFile(hdfs, file0, BLOCKSIZE, REPLICATION, seed);
    DFSTestUtil.createFile(hdfs, file1, BLOCKSIZE, REPLICATION, seed);

    // Allow snapshot for sub1, and create snapshot for it
    hdfs.allowSnapshot(sub);
    hdfs.createSnapshot(sub, "s1");

    // Deleting a snapshottable dir with snapshots should fail
    exception.expect(RemoteException.class);
    String error = "The directory " + sub.toString()
        + " cannot be deleted since " + sub.toString()
        + " is snapshottable and already has snapshots";
    exception.expectMessage(error);
    hdfs.delete(sub, true);
  }

  /**
   * Test applying editlog of operation which deletes a snapshottable directory
   * without snapshots. The snapshottable dir list in snapshot manager should be
   * updated.
   */
  @Test (timeout=300000)
  public void testApplyEditLogForDeletion() throws Exception {
    final Path foo = new Path("/foo");
    final Path bar1 = new Path(foo, "bar1");
    final Path bar2 = new Path(foo, "bar2");
    hdfs.mkdirs(bar1);
    hdfs.mkdirs(bar2);

    // allow snapshots on bar1 and bar2
    hdfs.allowSnapshot(bar1);
    hdfs.allowSnapshot(bar2);
    assertEquals(2, cluster.getNamesystem().getSnapshotManager()
        .getNumSnapshottableDirs());
    assertEquals(2, cluster.getNamesystem().getSnapshotManager()
        .getSnapshottableDirs().size());

    // delete /foo
    hdfs.delete(foo, true);
    cluster.restartNameNode(0);
    // the snapshottable dir list in snapshot manager should be empty
    assertEquals(0, cluster.getNamesystem().getSnapshotManager()
        .getNumSnapshottableDirs());
    assertEquals(0, cluster.getNamesystem().getSnapshotManager()
        .getSnapshottableDirs().size());
    hdfs.setSafeMode(SafeModeAction.ENTER);
    hdfs.saveNamespace();
    hdfs.setSafeMode(SafeModeAction.LEAVE);
    cluster.restartNameNode(0);
  }

  /**
   * Deleting directory with snapshottable descendant with snapshots must fail.
   */
  @Test (timeout=300000)
  public void testDeleteDirectoryWithSnapshot2() throws Exception {
    Path file0 = new Path(sub, "file0");
    Path file1 = new Path(sub, "file1");
    DFSTestUtil.createFile(hdfs, file0, BLOCKSIZE, REPLICATION, seed);
    DFSTestUtil.createFile(hdfs, file1, BLOCKSIZE, REPLICATION, seed);
    
    Path subfile1 = new Path(subsub, "file0");
    Path subfile2 = new Path(subsub, "file1");
    DFSTestUtil.createFile(hdfs, subfile1, BLOCKSIZE, REPLICATION, seed);
    DFSTestUtil.createFile(hdfs, subfile2, BLOCKSIZE, REPLICATION, seed);

    // Allow snapshot for subsub1, and create snapshot for it
    hdfs.allowSnapshot(subsub);
    hdfs.createSnapshot(subsub, "s1");

    // Deleting dir while its descedant subsub1 having snapshots should fail
    exception.expect(RemoteException.class);
    String error = subsub.toString()
        + " is snapshottable and already has snapshots";
    exception.expectMessage(error);
    hdfs.delete(dir, true);
  }
  
  private static INodeDirectory getDir(final FSDirectory fsdir, final Path dir)
      throws IOException {
    final String dirStr = dir.toString();
    return INodeDirectory.valueOf(fsdir.getINode(dirStr), dirStr);
  }

  private void checkQuotaUsageComputation(final Path dirPath,
      final long expectedNs, final long expectedDs) throws IOException {
    INodeDirectory dirNode = getDir(fsdir, dirPath);
    assertTrue(dirNode.isQuotaSet());
    QuotaCounts q = dirNode.getDirectoryWithQuotaFeature().getSpaceConsumed();
    assertEquals(dirNode.dumpTreeRecursively().toString(), expectedNs,
        q.getNameSpace());
    assertEquals(dirNode.dumpTreeRecursively().toString(), expectedDs,
        q.getStorageSpace());
    QuotaCounts counts = dirNode.computeQuotaUsage(fsdir.getBlockStoragePolicySuite(), false);
    assertEquals(dirNode.dumpTreeRecursively().toString(), expectedNs,
        counts.getNameSpace());
    assertEquals(dirNode.dumpTreeRecursively().toString(), expectedDs,
        counts.getStorageSpace());
  }
  
  /**
   * Test deleting a directory which is a descendant of a snapshottable
   * directory. In the test we need to cover the following cases:
   * 
   * <pre>
   * 1. Delete current INodeFile/INodeDirectory without taking any snapshot.
   * 2. Delete current INodeFile/INodeDirectory while snapshots have been taken 
   *    on ancestor(s).
   * 3. Delete current INodeFileWithSnapshot.
   * 4. Delete current INodeDirectoryWithSnapshot.
   * </pre>
   */
  @Test (timeout=300000)
  public void testDeleteCurrentFileDirectory() throws Exception {
    // create a folder which will be deleted before taking snapshots
    Path deleteDir = new Path(subsub, "deleteDir");
    Path deleteFile = new Path(deleteDir, "deleteFile");
    // create a directory that we will not change during the whole process.
    Path noChangeDirParent = new Path(sub, "noChangeDirParent");
    Path noChangeDir = new Path(noChangeDirParent, "noChangeDir");
    // create a file that we will not change in the future
    Path noChangeFile = new Path(noChangeDir, "noChangeFile");
    DFSTestUtil.createFile(hdfs, deleteFile, BLOCKSIZE, REPLICATION, seed);
    DFSTestUtil.createFile(hdfs, noChangeFile, BLOCKSIZE, REPLICATION, seed);
    // we will change this file's metadata in the future
    Path metaChangeFile1 = new Path(subsub, "metaChangeFile1");
    DFSTestUtil.createFile(hdfs, metaChangeFile1, BLOCKSIZE, REPLICATION, seed);
    // another file, created under noChangeDir, whose metadata will be changed
    Path metaChangeFile2 = new Path(noChangeDir, "metaChangeFile2");
    DFSTestUtil.createFile(hdfs, metaChangeFile2, BLOCKSIZE, REPLICATION, seed);
    
    // Case 1: delete deleteDir before taking snapshots
    hdfs.setQuota(dir, Long.MAX_VALUE - 1, Long.MAX_VALUE - 1);
    checkQuotaUsageComputation(dir, 10, BLOCKSIZE * REPLICATION * 4);
    hdfs.delete(deleteDir, true);
    checkQuotaUsageComputation(dir, 8, BLOCKSIZE * REPLICATION * 3);

    // create snapshot s0
    SnapshotTestHelper.createSnapshot(hdfs, dir, "s0");
    
    // after creating snapshot s0, create a directory tempdir under dir and then
    // delete dir immediately
    Path tempDir = new Path(dir, "tempdir");
    Path tempFile = new Path(tempDir, "tempfile");
    DFSTestUtil.createFile(hdfs, tempFile, BLOCKSIZE, REPLICATION, seed);
    final INodeFile temp = TestSnapshotBlocksMap.assertBlockCollection(
        tempFile.toString(), 1, fsdir, blockmanager);
    BlockInfo[] blocks = temp.getBlocks();
    hdfs.delete(tempDir, true);
    // check dir's quota usage
    checkQuotaUsageComputation(dir, 8, BLOCKSIZE * REPLICATION * 3);
    // check blocks of tempFile
    for (BlockInfo b : blocks) {
      assertEquals(INVALID_INODE_ID, b.getBlockCollectionId());
    }
    
    // make a change: create a new file under subsub
    Path newFileAfterS0 = new Path(subsub, "newFile");
    DFSTestUtil.createFile(hdfs, newFileAfterS0, BLOCKSIZE, REPLICATION, seed);
    // further change: change the replicator factor of metaChangeFile
    hdfs.setReplication(metaChangeFile1, REPLICATION_1);
    hdfs.setReplication(metaChangeFile2, REPLICATION_1);
    
    // create snapshot s1
    SnapshotTestHelper.createSnapshot(hdfs, dir, "s1");
    // check dir's quota usage
    checkQuotaUsageComputation(dir, 9L, BLOCKSIZE * REPLICATION * 4);
    
    // get two snapshots for later use
    Snapshot snapshot0 = fsdir.getINode(dir.toString()).asDirectory()
        .getSnapshot(DFSUtil.string2Bytes("s0"));
    Snapshot snapshot1 = fsdir.getINode(dir.toString()).asDirectory()
        .getSnapshot(DFSUtil.string2Bytes("s1"));
    
    // Case 2 + Case 3: delete noChangeDirParent, noChangeFile, and
    // metaChangeFile2. Note that when we directly delete a directory, the 
    // directory will be converted to an INodeDirectoryWithSnapshot. To make
    // sure the deletion goes through an INodeDirectory, we delete the parent
    // of noChangeDir
    hdfs.delete(noChangeDirParent, true);
    // while deletion, we add a diff for metaChangeFile2 as its snapshot copy
    // for s1, we also add diffs for both sub and noChangeDirParent
    checkQuotaUsageComputation(dir, 9L, BLOCKSIZE * REPLICATION * 4);
    
    // check the snapshot copy of noChangeDir 
    Path snapshotNoChangeDir = SnapshotTestHelper.getSnapshotPath(dir, "s1",
        sub.getName() + "/" + noChangeDirParent.getName() + "/"
            + noChangeDir.getName());
    INodeDirectory snapshotNode = 
        (INodeDirectory) fsdir.getINode(snapshotNoChangeDir.toString());
    // should still be an INodeDirectory
    assertEquals(INodeDirectory.class, snapshotNode.getClass());
    ReadOnlyList<INode> children = snapshotNode
        .getChildrenList(Snapshot.CURRENT_STATE_ID);
    // check 2 children: noChangeFile and metaChangeFile2
    assertEquals(2, children.size());
    INode noChangeFileSCopy = children.get(1);
    assertEquals(noChangeFile.getName(), noChangeFileSCopy.getLocalName());
    assertEquals(INodeFile.class, noChangeFileSCopy.getClass());
    TestSnapshotBlocksMap.assertBlockCollection(new Path(snapshotNoChangeDir,
        noChangeFileSCopy.getLocalName()).toString(), 1, fsdir, blockmanager);
    
    INodeFile metaChangeFile2SCopy = children.get(0).asFile();
    assertEquals(metaChangeFile2.getName(), metaChangeFile2SCopy.getLocalName());
    assertTrue(metaChangeFile2SCopy.isWithSnapshot());
    assertFalse(metaChangeFile2SCopy.isUnderConstruction());
    TestSnapshotBlocksMap.assertBlockCollection(new Path(snapshotNoChangeDir,
        metaChangeFile2SCopy.getLocalName()).toString(), 1, fsdir, blockmanager);
    
    // check the replication factor of metaChangeFile2SCopy
    assertEquals(REPLICATION_1,
        metaChangeFile2SCopy.getFileReplication(Snapshot.CURRENT_STATE_ID));
    assertEquals(REPLICATION_1,
        metaChangeFile2SCopy.getFileReplication(snapshot1.getId()));
    assertEquals(REPLICATION,
        metaChangeFile2SCopy.getFileReplication(snapshot0.getId()));
    
    // Case 4: delete directory sub
    // before deleting sub, we first create a new file under sub
    Path newFile = new Path(sub, "newFile");
    DFSTestUtil.createFile(hdfs, newFile, BLOCKSIZE, REPLICATION, seed);
    final INodeFile newFileNode = TestSnapshotBlocksMap.assertBlockCollection(
        newFile.toString(), 1, fsdir, blockmanager);
    blocks = newFileNode.getBlocks();
    checkQuotaUsageComputation(dir, 10L, BLOCKSIZE * REPLICATION * 5);
    hdfs.delete(sub, true);
    // while deletion, we add diff for subsub and metaChangeFile1, and remove
    // newFile
    checkQuotaUsageComputation(dir, 9L, BLOCKSIZE * REPLICATION * 4);
    for (BlockInfo b : blocks) {
      assertEquals(INVALID_INODE_ID, b.getBlockCollectionId());
    }
    
    // make sure the whole subtree of sub is stored correctly in snapshot
    Path snapshotSub = SnapshotTestHelper.getSnapshotPath(dir, "s1",
        sub.getName());
    INodeDirectory snapshotNode4Sub = fsdir.getINode(snapshotSub.toString())
        .asDirectory();
    assertTrue(snapshotNode4Sub.isWithSnapshot());
    // the snapshot copy of sub has only one child subsub.
    // newFile should have been destroyed
    assertEquals(1, snapshotNode4Sub.getChildrenList(Snapshot.CURRENT_STATE_ID)
        .size());
    // but should have two children, subsub and noChangeDir, when s1 was taken  
    assertEquals(2, snapshotNode4Sub.getChildrenList(snapshot1.getId()).size());
    
    // check the snapshot copy of subsub, which is contained in the subtree of
    // sub's snapshot copy
    INode snapshotNode4Subsub = snapshotNode4Sub.getChildrenList(
        Snapshot.CURRENT_STATE_ID).get(0);
    assertTrue(snapshotNode4Subsub.asDirectory().isWithSnapshot());
    assertTrue(snapshotNode4Sub == snapshotNode4Subsub.getParent());
    // check the children of subsub
    INodeDirectory snapshotSubsubDir = (INodeDirectory) snapshotNode4Subsub;
    children = snapshotSubsubDir.getChildrenList(Snapshot.CURRENT_STATE_ID);
    assertEquals(2, children.size());
    assertEquals(children.get(0).getLocalName(), metaChangeFile1.getName());
    assertEquals(children.get(1).getLocalName(), newFileAfterS0.getName());
    // only one child before snapshot s0 
    children = snapshotSubsubDir.getChildrenList(snapshot0.getId());
    assertEquals(1, children.size());
    INode child = children.get(0);
    assertEquals(child.getLocalName(), metaChangeFile1.getName());
    // check snapshot copy of metaChangeFile1
    INodeFile metaChangeFile1SCopy = child.asFile();
    assertTrue(metaChangeFile1SCopy.isWithSnapshot());
    assertFalse(metaChangeFile1SCopy.isUnderConstruction());
    assertEquals(REPLICATION_1,
        metaChangeFile1SCopy.getFileReplication(Snapshot.CURRENT_STATE_ID));
    assertEquals(REPLICATION_1,
        metaChangeFile1SCopy.getFileReplication(snapshot1.getId()));
    assertEquals(REPLICATION,
        metaChangeFile1SCopy.getFileReplication(snapshot0.getId()));
  }
  
  /**
   * Test deleting the earliest (first) snapshot. In this simplest scenario, the 
   * snapshots are taken on the same directory, and we do not need to combine
   * snapshot diffs.
   */
  @Test (timeout=300000)
  public void testDeleteEarliestSnapshot1() throws Exception {
    // create files under sub
    Path file0 = new Path(sub, "file0");
    Path file1 = new Path(sub, "file1");
    DFSTestUtil.createFile(hdfs, file0, BLOCKSIZE, REPLICATION, seed);
    DFSTestUtil.createFile(hdfs, file1, BLOCKSIZE, REPLICATION, seed);
    
    String snapshotName = "s1";
    try {
      hdfs.deleteSnapshot(sub, snapshotName);
      fail("SnapshotException expected: " + sub.toString()
          + " is not snapshottable yet");
    } catch (Exception e) {
      GenericTestUtils.assertExceptionContains(
          "Directory is not a snapshottable directory: " + sub, e);
    }
    
    // make sub snapshottable
    hdfs.allowSnapshot(sub);
    try {
      hdfs.deleteSnapshot(sub, snapshotName);
      fail("SnapshotException expected: snapshot " + snapshotName
          + " does not exist for " + sub.toString());
    } catch (Exception e) {
      GenericTestUtils.assertExceptionContains("Cannot delete snapshot "
          + snapshotName + " from path " + sub.toString()
          + ": the snapshot does not exist.", e);
    }
    
    // create snapshot s1 for sub
    SnapshotTestHelper.createSnapshot(hdfs, sub, snapshotName);
    // check quota usage computation
    checkQuotaUsageComputation(sub, 3, BLOCKSIZE * REPLICATION * 2);
    // delete s1
    hdfs.deleteSnapshot(sub, snapshotName);
    checkQuotaUsageComputation(sub, 3, BLOCKSIZE * REPLICATION * 2);
    // now we can create a snapshot with the same name
    hdfs.createSnapshot(sub, snapshotName);
    checkQuotaUsageComputation(sub, 3, BLOCKSIZE * REPLICATION * 2);
    
    // create a new file under sub
    Path newFile = new Path(sub, "newFile");
    DFSTestUtil.createFile(hdfs, newFile, BLOCKSIZE, REPLICATION, seed);
    // create another snapshot s2
    String snapshotName2 = "s2";
    hdfs.createSnapshot(sub, snapshotName2);
    checkQuotaUsageComputation(sub, 4, BLOCKSIZE * REPLICATION * 3);
    // Get the filestatus of sub under snapshot s2
    Path ss = SnapshotTestHelper
        .getSnapshotPath(sub, snapshotName2, "newFile");
    FileStatus statusBeforeDeletion = hdfs.getFileStatus(ss);
    // delete s1
    hdfs.deleteSnapshot(sub, snapshotName);
    checkQuotaUsageComputation(sub, 4, BLOCKSIZE * REPLICATION * 3);
    FileStatus statusAfterDeletion = hdfs.getFileStatus(ss);
    System.out.println("Before deletion: " + statusBeforeDeletion.toString()
        + "\n" + "After deletion: " + statusAfterDeletion.toString());
    assertEquals(statusBeforeDeletion.toString(),
        statusAfterDeletion.toString());
  }
  
  /**
   * Test deleting the earliest (first) snapshot. In this more complicated 
   * scenario, the snapshots are taken across directories.
   * <pre>
   * The test covers the following scenarios:
   * 1. delete the first diff in the diff list of a directory
   * 2. delete the first diff in the diff list of a file
   * </pre>
   * Also, the recursive cleanTree process should cover both INodeFile and 
   * INodeDirectory.
   */
  @Test (timeout=300000)
  public void testDeleteEarliestSnapshot2() throws Exception {
    Path noChangeDir = new Path(sub, "noChangeDir");
    Path noChangeFile = new Path(noChangeDir, "noChangeFile");
    Path metaChangeFile = new Path(noChangeDir, "metaChangeFile");
    Path metaChangeDir = new Path(noChangeDir, "metaChangeDir");
    Path toDeleteFile = new Path(metaChangeDir, "toDeleteFile");
    DFSTestUtil.createFile(hdfs, noChangeFile, BLOCKSIZE, REPLICATION, seed);
    DFSTestUtil.createFile(hdfs, metaChangeFile, BLOCKSIZE, REPLICATION, seed);
    DFSTestUtil.createFile(hdfs, toDeleteFile, BLOCKSIZE, REPLICATION, seed);
    
    final INodeFile toDeleteFileNode = TestSnapshotBlocksMap
        .assertBlockCollection(toDeleteFile.toString(), 1, fsdir, blockmanager);
    BlockInfo[] blocks = toDeleteFileNode.getBlocks();
    
    // create snapshot s0 on dir
    SnapshotTestHelper.createSnapshot(hdfs, dir, "s0");
    checkQuotaUsageComputation(dir, 7, 3 * BLOCKSIZE * REPLICATION);
    
    // delete /TestSnapshot/sub/noChangeDir/metaChangeDir/toDeleteFile
    hdfs.delete(toDeleteFile, true);
    // the deletion adds diff of toDeleteFile and metaChangeDir
    checkQuotaUsageComputation(dir, 7, 3 * BLOCKSIZE * REPLICATION);
    // change metadata of /TestSnapshot/sub/noChangeDir/metaChangeDir and
    // /TestSnapshot/sub/noChangeDir/metaChangeFile
    hdfs.setReplication(metaChangeFile, REPLICATION_1);
    hdfs.setOwner(metaChangeDir, "unknown", "unknown");
    checkQuotaUsageComputation(dir, 7, 3 * BLOCKSIZE * REPLICATION);
    
    // create snapshot s1 on dir
    hdfs.createSnapshot(dir, "s1");
    checkQuotaUsageComputation(dir, 7, 3 * BLOCKSIZE * REPLICATION);
    
    // delete snapshot s0
    hdfs.deleteSnapshot(dir, "s0");
    // namespace: remove toDeleteFile and its diff, metaChangeFile's diff, 
    // metaChangeDir's diff, dir's diff. diskspace: remove toDeleteFile, and 
    // metaChangeFile's replication factor decreases
    checkQuotaUsageComputation(dir, 6, 2 * BLOCKSIZE * REPLICATION - BLOCKSIZE);
    for (BlockInfo b : blocks) {
      assertEquals(INVALID_INODE_ID, b.getBlockCollectionId());
    }
    
    // check 1. there is no snapshot s0
    final INodeDirectory dirNode = fsdir.getINode(dir.toString()).asDirectory();
    Snapshot snapshot0 = dirNode.getSnapshot(DFSUtil.string2Bytes("s0"));
    assertNull(snapshot0);
    Snapshot snapshot1 = dirNode.getSnapshot(DFSUtil.string2Bytes("s1"));
    DirectoryDiffList diffList = dirNode.getDiffs();
    assertEquals(1, diffList.asList().size());
    assertEquals(snapshot1.getId(), diffList.getLast().getSnapshotId());
    diffList = fsdir.getINode(metaChangeDir.toString()).asDirectory()
        .getDiffs();
    assertEquals(null, diffList);
    
    // check 2. noChangeDir and noChangeFile are still there
    final INodeDirectory noChangeDirNode = 
        (INodeDirectory) fsdir.getINode(noChangeDir.toString());
    assertEquals(INodeDirectory.class, noChangeDirNode.getClass());
    final INodeFile noChangeFileNode = 
        (INodeFile) fsdir.getINode(noChangeFile.toString());
    assertEquals(INodeFile.class, noChangeFileNode.getClass());
    TestSnapshotBlocksMap.assertBlockCollection(noChangeFile.toString(), 1,
        fsdir, blockmanager);
    
    // check 3: current metadata of metaChangeFile and metaChangeDir
    FileStatus status = hdfs.getFileStatus(metaChangeDir);
    assertEquals("unknown", status.getOwner());
    assertEquals("unknown", status.getGroup());
    status = hdfs.getFileStatus(metaChangeFile);
    assertEquals(REPLICATION_1, status.getReplication());
    TestSnapshotBlocksMap.assertBlockCollection(metaChangeFile.toString(), 1,
        fsdir, blockmanager);
    
    // check 4: no snapshot copy for toDeleteFile
    try {
      hdfs.getFileStatus(toDeleteFile);
      fail("should throw FileNotFoundException");
    } catch (FileNotFoundException e) {
      GenericTestUtils.assertExceptionContains("File does not exist: "
          + toDeleteFile.toString(), e);
    }
    
    final Path toDeleteFileInSnapshot = SnapshotTestHelper.getSnapshotPath(dir,
        "s0", toDeleteFile.toString().substring(dir.toString().length()));
    try {
      hdfs.getFileStatus(toDeleteFileInSnapshot);
      fail("should throw FileNotFoundException");
    } catch (FileNotFoundException e) {
      GenericTestUtils.assertExceptionContains("File does not exist: "
          + toDeleteFileInSnapshot.toString(), e);
    }
  }

  /**
   * Delete a snapshot that is taken before a directory deletion,
   * directory diff list should be combined correctly.
   */
  @Test (timeout=60000)
  public void testDeleteSnapshot1() throws Exception {
    final Path root = new Path("/");

    Path dir = new Path("/dir1");
    Path file1 = new Path(dir, "file1");
    DFSTestUtil.createFile(hdfs, file1, BLOCKSIZE, REPLICATION, seed);

    hdfs.allowSnapshot(root);
    hdfs.createSnapshot(root, "s1");

    Path file2 = new Path(dir, "file2");
    DFSTestUtil.createFile(hdfs, file2, BLOCKSIZE, REPLICATION, seed);

    hdfs.createSnapshot(root, "s2");

    // delete file
    hdfs.delete(file1, true);
    hdfs.delete(file2, true);

    // delete directory
    assertTrue(hdfs.delete(dir, false));

    // delete second snapshot
    hdfs.deleteSnapshot(root, "s2");

    NameNodeAdapter.enterSafeMode(cluster.getNameNode(), false);
    NameNodeAdapter.saveNamespace(cluster.getNameNode());

    // restart NN
    cluster.restartNameNodes();
  }

  /**
   * Delete a snapshot that is taken before a directory deletion (recursively),
   * directory diff list should be combined correctly.
   */
  @Test (timeout=60000)
  public void testDeleteSnapshot2() throws Exception {
    final Path root = new Path("/");

    Path dir = new Path("/dir1");
    Path file1 = new Path(dir, "file1");
    DFSTestUtil.createFile(hdfs, file1, BLOCKSIZE, REPLICATION, seed);

    hdfs.allowSnapshot(root);
    hdfs.createSnapshot(root, "s1");

    Path file2 = new Path(dir, "file2");
    DFSTestUtil.createFile(hdfs, file2, BLOCKSIZE, REPLICATION, seed);
    INodeFile file2Node = fsdir.getINode(file2.toString()).asFile();
    long file2NodeId = file2Node.getId();

    hdfs.createSnapshot(root, "s2");

    // delete directory recursively
    assertTrue(hdfs.delete(dir, true));
    assertNotNull(fsdir.getInode(file2NodeId));

    // delete second snapshot
    hdfs.deleteSnapshot(root, "s2");
    assertTrue(fsdir.getInode(file2NodeId) == null);

    NameNodeAdapter.enterSafeMode(cluster.getNameNode(), false);
    NameNodeAdapter.saveNamespace(cluster.getNameNode());

    // restart NN
    cluster.restartNameNodes();
  }

  /**
   * Test deleting snapshots in a more complicated scenario: need to combine
   * snapshot diffs, but no need to handle diffs distributed in a dir tree
   */
  @Test (timeout=300000)
  public void testCombineSnapshotDiff1() throws Exception {
    testCombineSnapshotDiffImpl(sub, "", 1);
  }
  
  /**
   * Test deleting snapshots in more complicated scenarios (snapshot diffs are
   * distributed in the directory sub-tree)
   */
  @Test (timeout=300000)
  public void testCombineSnapshotDiff2() throws Exception {
    testCombineSnapshotDiffImpl(sub, "subsub1/subsubsub1/", 3);
  }
  
  /**
   * When combine two snapshots, make sure files/directories created after the 
   * prior snapshot get destroyed.
   */
  @Test (timeout=300000)
  public void testCombineSnapshotDiff3() throws Exception {
    // create initial dir and subdir
    Path dir = new Path("/dir");
    Path subDir1 = new Path(dir, "subdir1");
    Path subDir2 = new Path(dir, "subdir2");
    hdfs.mkdirs(subDir2);
    Path subsubDir = new Path(subDir1, "subsubdir");
    hdfs.mkdirs(subsubDir);
    
    // take snapshots on subdir and dir
    SnapshotTestHelper.createSnapshot(hdfs, dir, "s1");
    
    // create new dir under initial dir
    Path newDir = new Path(subsubDir, "newdir");
    Path newFile = new Path(newDir, "newfile");
    DFSTestUtil.createFile(hdfs, newFile, BLOCKSIZE, REPLICATION, seed);
    Path newFile2 = new Path(subDir2, "newfile");
    DFSTestUtil.createFile(hdfs, newFile2, BLOCKSIZE, REPLICATION, seed);
    
    // create another snapshot
    SnapshotTestHelper.createSnapshot(hdfs, dir, "s2");
    
    checkQuotaUsageComputation(dir, 7, BLOCKSIZE * 2 * REPLICATION);
    
    // delete subsubdir and subDir2
    hdfs.delete(subsubDir, true);
    hdfs.delete(subDir2, true);
    
    // add diff of s2 to subDir1, subsubDir, and subDir2
    checkQuotaUsageComputation(dir, 7, BLOCKSIZE * 2 * REPLICATION);
    
    // delete snapshot s2
    hdfs.deleteSnapshot(dir, "s2");
    
    // delete s2 diff in dir, subDir2, and subsubDir. Delete newFile, newDir,
    // and newFile2. Rename s2 diff to s1 for subDir1 
    checkQuotaUsageComputation(dir, 4, 0);
    // Check rename of snapshot diff in subDir1
    Path subdir1_s1 = SnapshotTestHelper.getSnapshotPath(dir, "s1",
        subDir1.getName());
    Path subdir1_s2 = SnapshotTestHelper.getSnapshotPath(dir, "s2",
        subDir1.getName());
    assertTrue(hdfs.exists(subdir1_s1));
    assertFalse(hdfs.exists(subdir1_s2));
  }
  
  /**
   * Test snapshot deletion
   * @param snapshotRoot The dir where the snapshots are created
   * @param modDirStr The snapshotRoot itself or one of its sub-directory, 
   *        where the modifications happen. It is represented as a relative 
   *        path to the snapshotRoot.
   */
  private void testCombineSnapshotDiffImpl(Path snapshotRoot, String modDirStr,
      int dirNodeNum) throws Exception {
    Path modDir = modDirStr.isEmpty() ? snapshotRoot : new Path(snapshotRoot,
        modDirStr);
    Path file10 = new Path(modDir, "file10");
    Path file11 = new Path(modDir, "file11");
    Path file12 = new Path(modDir, "file12");
    Path file13 = new Path(modDir, "file13");
    Path file14 = new Path(modDir, "file14");
    Path file15 = new Path(modDir, "file15");
    DFSTestUtil.createFile(hdfs, file10, BLOCKSIZE, REPLICATION_1, seed);
    DFSTestUtil.createFile(hdfs, file11, BLOCKSIZE, REPLICATION_1, seed);
    DFSTestUtil.createFile(hdfs, file12, BLOCKSIZE, REPLICATION_1, seed);
    DFSTestUtil.createFile(hdfs, file13, BLOCKSIZE, REPLICATION_1, seed);

    // create snapshot s1 for snapshotRoot
    SnapshotTestHelper.createSnapshot(hdfs, snapshotRoot, "s1");
    checkQuotaUsageComputation(snapshotRoot, dirNodeNum + 4, 8 * BLOCKSIZE);
    
    // delete file11
    hdfs.delete(file11, true);
    checkQuotaUsageComputation(snapshotRoot, dirNodeNum + 4, 8 * BLOCKSIZE);
    
    // modify file12
    hdfs.setReplication(file12, REPLICATION);
    checkQuotaUsageComputation(snapshotRoot, dirNodeNum + 4, 9 * BLOCKSIZE);
    
    // modify file13
    hdfs.setReplication(file13, REPLICATION);
    checkQuotaUsageComputation(snapshotRoot, dirNodeNum + 4, 10 * BLOCKSIZE);
    
    // create file14
    DFSTestUtil.createFile(hdfs, file14, BLOCKSIZE, REPLICATION, seed);
    checkQuotaUsageComputation(snapshotRoot, dirNodeNum + 5, 13 * BLOCKSIZE);
    
    // create file15
    DFSTestUtil.createFile(hdfs, file15, BLOCKSIZE, REPLICATION, seed);
    checkQuotaUsageComputation(snapshotRoot, dirNodeNum + 6, 16 * BLOCKSIZE);
    
    // create snapshot s2 for snapshotRoot
    hdfs.createSnapshot(snapshotRoot, "s2");
    checkQuotaUsageComputation(snapshotRoot, dirNodeNum + 6, 16 * BLOCKSIZE);
    
    // create file11 again: (0, d) + (c, 0)
    DFSTestUtil.createFile(hdfs, file11, BLOCKSIZE, REPLICATION, seed);
    checkQuotaUsageComputation(snapshotRoot, dirNodeNum + 7, 19 * BLOCKSIZE);
    
    // delete file12
    hdfs.delete(file12, true);
    checkQuotaUsageComputation(snapshotRoot, dirNodeNum + 7, 19 * BLOCKSIZE);
    
    // modify file13
    hdfs.setReplication(file13, (short) (REPLICATION - 2));
    checkQuotaUsageComputation(snapshotRoot, dirNodeNum + 7, 19 * BLOCKSIZE);
    
    // delete file14: (c, 0) + (0, d)
    hdfs.delete(file14, true);
    checkQuotaUsageComputation(snapshotRoot, dirNodeNum + 7, 19 * BLOCKSIZE);
    
    // modify file15
    hdfs.setReplication(file15, REPLICATION_1);
    checkQuotaUsageComputation(snapshotRoot, dirNodeNum + 7, 19 * BLOCKSIZE);
    
    // create snapshot s3 for snapshotRoot
    hdfs.createSnapshot(snapshotRoot, "s3");
    checkQuotaUsageComputation(snapshotRoot, dirNodeNum + 7, 19 * BLOCKSIZE);
    
    // modify file10, to check if the posterior diff was set correctly
    hdfs.setReplication(file10, REPLICATION);
    checkQuotaUsageComputation(snapshotRoot, dirNodeNum + 7, 20 * BLOCKSIZE);

    Path file10_s1 = SnapshotTestHelper.getSnapshotPath(snapshotRoot, "s1",
        modDirStr + "file10");
    Path file11_s1 = SnapshotTestHelper.getSnapshotPath(snapshotRoot, "s1",
        modDirStr + "file11");
    Path file12_s1 = SnapshotTestHelper.getSnapshotPath(snapshotRoot, "s1",
        modDirStr + "file12");
    Path file13_s1 = SnapshotTestHelper.getSnapshotPath(snapshotRoot, "s1",
        modDirStr + "file13");
    Path file14_s2 = SnapshotTestHelper.getSnapshotPath(snapshotRoot, "s2",
        modDirStr + "file14");
    Path file15_s2 = SnapshotTestHelper.getSnapshotPath(snapshotRoot, "s2",
        modDirStr + "file15");
    FileStatus statusBeforeDeletion10 = hdfs.getFileStatus(file10_s1);
    FileStatus statusBeforeDeletion11 = hdfs.getFileStatus(file11_s1);
    FileStatus statusBeforeDeletion12 = hdfs.getFileStatus(file12_s1);
    FileStatus statusBeforeDeletion13 = hdfs.getFileStatus(file13_s1);
    INodeFile file14Node = TestSnapshotBlocksMap.assertBlockCollection(
        file14_s2.toString(), 1, fsdir, blockmanager);
    BlockInfo[] blocks_14 = file14Node.getBlocks();
    TestSnapshotBlocksMap.assertBlockCollection(file15_s2.toString(), 1, fsdir,
        blockmanager);
    
    // delete s2, in which process we need to combine the diff in s2 to s1
    hdfs.deleteSnapshot(snapshotRoot, "s2");
    checkQuotaUsageComputation(snapshotRoot, dirNodeNum + 6, 14 * BLOCKSIZE);
    
    // check the correctness of s1
    FileStatus statusAfterDeletion10 = hdfs.getFileStatus(file10_s1);
    FileStatus statusAfterDeletion11 = hdfs.getFileStatus(file11_s1);
    FileStatus statusAfterDeletion12 = hdfs.getFileStatus(file12_s1);
    FileStatus statusAfterDeletion13 = hdfs.getFileStatus(file13_s1);
    assertEquals(statusBeforeDeletion10.toString(),
        statusAfterDeletion10.toString());
    assertEquals(statusBeforeDeletion11.toString(),
        statusAfterDeletion11.toString());
    assertEquals(statusBeforeDeletion12.toString(),
        statusAfterDeletion12.toString());
    assertEquals(statusBeforeDeletion13.toString(),
        statusAfterDeletion13.toString());
    TestSnapshotBlocksMap.assertBlockCollection(file10_s1.toString(), 1, fsdir,
        blockmanager);
    TestSnapshotBlocksMap.assertBlockCollection(file11_s1.toString(), 1, fsdir,
        blockmanager);
    TestSnapshotBlocksMap.assertBlockCollection(file12_s1.toString(), 1, fsdir,
        blockmanager);
    TestSnapshotBlocksMap.assertBlockCollection(file13_s1.toString(), 1, fsdir,
        blockmanager);

    // make sure file14 and file15 are not included in s1
    Path file14_s1 = SnapshotTestHelper.getSnapshotPath(snapshotRoot, "s1",
        modDirStr + "file14");
    Path file15_s1 = SnapshotTestHelper.getSnapshotPath(snapshotRoot, "s1",
        modDirStr + "file15");
    assertFalse(hdfs.exists(file14_s1));
    assertFalse(hdfs.exists(file15_s1));
    for (BlockInfo b : blocks_14) {
      assertEquals(INVALID_INODE_ID, b.getBlockCollectionId());
    }

    INodeFile nodeFile13 = (INodeFile) fsdir.getINode(file13.toString());
    for (BlockInfo b: nodeFile13.getBlocks()) {
      assertEquals(REPLICATION_1, b.getReplication());
    }
    TestSnapshotBlocksMap.assertBlockCollection(file13.toString(), 1, fsdir,
        blockmanager);

    INodeFile nodeFile12 = (INodeFile) fsdir.getINode(file12_s1.toString());
    for (BlockInfo b: nodeFile12.getBlocks()) {
      assertEquals(REPLICATION_1, b.getReplication());
    }
  }
  
  /** Test deleting snapshots with modification on the metadata of directory */ 
  @Test (timeout=300000)
  public void testDeleteSnapshotWithDirModification() throws Exception {
    Path file = new Path(sub, "file");
    DFSTestUtil.createFile(hdfs, file, BLOCKSIZE, REPLICATION, seed);
    hdfs.setOwner(sub, "user1", "group1");
    
    // create snapshot s1 for sub1, and change the metadata of sub1
    SnapshotTestHelper.createSnapshot(hdfs, sub, "s1");
    checkQuotaUsageComputation(sub, 2, BLOCKSIZE * 3);
    hdfs.setOwner(sub, "user2", "group2");
    checkQuotaUsageComputation(sub, 2, BLOCKSIZE * 3);
    
    // create snapshot s2 for sub1, but do not modify sub1 afterwards
    hdfs.createSnapshot(sub, "s2");
    checkQuotaUsageComputation(sub, 2, BLOCKSIZE * 3);
    
    // create snapshot s3 for sub1, and change the metadata of sub1
    hdfs.createSnapshot(sub, "s3");
    checkQuotaUsageComputation(sub, 2, BLOCKSIZE * 3);
    hdfs.setOwner(sub, "user3", "group3");
    checkQuotaUsageComputation(sub, 2, BLOCKSIZE * 3);
    
    // delete snapshot s3
    hdfs.deleteSnapshot(sub, "s3");
    checkQuotaUsageComputation(sub, 2, BLOCKSIZE * 3);
    
    // check sub1's metadata in snapshot s2
    FileStatus statusOfS2 = hdfs.getFileStatus(new Path(sub,
        HdfsConstants.DOT_SNAPSHOT_DIR + "/s2"));
    assertEquals("user2", statusOfS2.getOwner());
    assertEquals("group2", statusOfS2.getGroup());
    
    // delete snapshot s2
    hdfs.deleteSnapshot(sub, "s2");
    checkQuotaUsageComputation(sub, 2, BLOCKSIZE * 3);
    
    // check sub1's metadata in snapshot s1
    FileStatus statusOfS1 = hdfs.getFileStatus(new Path(sub,
        HdfsConstants.DOT_SNAPSHOT_DIR + "/s1"));
    assertEquals("user1", statusOfS1.getOwner());
    assertEquals("group1", statusOfS1.getGroup());
  }

  @Test
  public void testDeleteSnapshotWithPermissionsDisabled() throws Exception {
    cluster.shutdown();
    Configuration newConf = new Configuration(conf);
    newConf.setBoolean(DFSConfigKeys.DFS_PERMISSIONS_ENABLED_KEY, false);
    cluster = new MiniDFSCluster.Builder(newConf).numDataNodes(0).build();
    cluster.waitActive();
    hdfs = cluster.getFileSystem();

    final Path path = new Path("/dir");
    hdfs.mkdirs(path);
    hdfs.allowSnapshot(path);
    hdfs.mkdirs(new Path(path, "/test"));
    hdfs.createSnapshot(path, "s1");
    UserGroupInformation anotherUser = UserGroupInformation
        .createRemoteUser("anotheruser");
    anotherUser.doAs(new PrivilegedAction<Object>() {
      @Override
      public Object run() {
        DistributedFileSystem anotherUserFS = null;
        try {
          anotherUserFS = cluster.getFileSystem();
          anotherUserFS.deleteSnapshot(path, "s1");
        } catch (IOException e) {
          fail("Failed to delete snapshot : " + e.getLocalizedMessage());
        } finally {
          IOUtils.closeStream(anotherUserFS);
        }
        return null;
      }
    });
  }

  /** 
   * A test covering the case where the snapshot diff to be deleted is renamed 
   * to its previous snapshot. 
   */
  @Test (timeout=300000)
  public void testRenameSnapshotDiff() throws Exception {
    cluster.getNamesystem().getSnapshotManager().setAllowNestedSnapshots(true);

    final Path subFile0 = new Path(sub, "file0");
    final Path subsubFile0 = new Path(subsub, "file0");
    DFSTestUtil.createFile(hdfs, subFile0, BLOCKSIZE, REPLICATION, seed);
    DFSTestUtil.createFile(hdfs, subsubFile0, BLOCKSIZE, REPLICATION, seed);
    hdfs.setOwner(subsub, "owner", "group");
    
    // create snapshot s0 on sub
    SnapshotTestHelper.createSnapshot(hdfs, sub, "s0");
    checkQuotaUsageComputation(sub, 4, BLOCKSIZE * 6);
    // make some changes on both sub and subsub
    final Path subFile1 = new Path(sub, "file1");
    final Path subsubFile1 = new Path(subsub, "file1");
    DFSTestUtil.createFile(hdfs, subFile1, BLOCKSIZE, REPLICATION_1, seed);
    DFSTestUtil.createFile(hdfs, subsubFile1, BLOCKSIZE, REPLICATION, seed);
    checkQuotaUsageComputation(sub, 6, BLOCKSIZE * 11);
    
    // create snapshot s1 on sub
    SnapshotTestHelper.createSnapshot(hdfs, sub, "s1");
    checkQuotaUsageComputation(sub, 6, BLOCKSIZE * 11);
    
    // create snapshot s2 on dir
    SnapshotTestHelper.createSnapshot(hdfs, dir, "s2");
    checkQuotaUsageComputation(dir, 7, BLOCKSIZE * 11);
    checkQuotaUsageComputation(sub, 6, BLOCKSIZE * 11);
    
    // make changes on subsub and subsubFile1
    hdfs.setOwner(subsub, "unknown", "unknown");
    hdfs.setReplication(subsubFile1, REPLICATION_1);
    checkQuotaUsageComputation(dir, 7, BLOCKSIZE * 11);
    checkQuotaUsageComputation(sub, 6, BLOCKSIZE * 11);
    
    // make changes on sub
    hdfs.delete(subFile1, true);
    checkQuotaUsageComputation(new Path("/"), 8, BLOCKSIZE * 11);
    checkQuotaUsageComputation(dir, 7, BLOCKSIZE * 11);
    checkQuotaUsageComputation(sub, 6, BLOCKSIZE * 11);
    
    Path subsubSnapshotCopy = SnapshotTestHelper.getSnapshotPath(dir, "s2",
        sub.getName() + Path.SEPARATOR + subsub.getName());
    Path subsubFile1SCopy = SnapshotTestHelper.getSnapshotPath(dir, "s2",
        sub.getName() + Path.SEPARATOR + subsub.getName() + Path.SEPARATOR
            + subsubFile1.getName());
    Path subFile1SCopy = SnapshotTestHelper.getSnapshotPath(dir, "s2",
        sub.getName() + Path.SEPARATOR + subFile1.getName());
    FileStatus subsubStatus = hdfs.getFileStatus(subsubSnapshotCopy);
    assertEquals("owner", subsubStatus.getOwner());
    assertEquals("group", subsubStatus.getGroup());
    FileStatus subsubFile1Status = hdfs.getFileStatus(subsubFile1SCopy);
    assertEquals(REPLICATION, subsubFile1Status.getReplication());
    FileStatus subFile1Status = hdfs.getFileStatus(subFile1SCopy);
    assertEquals(REPLICATION_1, subFile1Status.getReplication());
    
    // delete snapshot s2
    hdfs.deleteSnapshot(dir, "s2");
    checkQuotaUsageComputation(new Path("/"), 8, BLOCKSIZE * 11);
    checkQuotaUsageComputation(dir, 7, BLOCKSIZE * 11);
    checkQuotaUsageComputation(sub, 6, BLOCKSIZE * 11);
    
    // no snapshot copy for s2
    try {
      hdfs.getFileStatus(subsubSnapshotCopy);
      fail("should throw FileNotFoundException");
    } catch (FileNotFoundException e) {
      GenericTestUtils.assertExceptionContains("File does not exist: "
          + subsubSnapshotCopy.toString(), e);
    }
    try {
      hdfs.getFileStatus(subsubFile1SCopy);
      fail("should throw FileNotFoundException");
    } catch (FileNotFoundException e) {
      GenericTestUtils.assertExceptionContains("File does not exist: "
          + subsubFile1SCopy.toString(), e);
    }
    try {
      hdfs.getFileStatus(subFile1SCopy);
      fail("should throw FileNotFoundException");
    } catch (FileNotFoundException e) {
      GenericTestUtils.assertExceptionContains("File does not exist: "
          + subFile1SCopy.toString(), e);
    }
    
    // the snapshot copy of s2 should now be renamed to s1 under sub
    subsubSnapshotCopy = SnapshotTestHelper.getSnapshotPath(sub, "s1",
        subsub.getName());
    subsubFile1SCopy = SnapshotTestHelper.getSnapshotPath(sub, "s1",
        subsub.getName() + Path.SEPARATOR + subsubFile1.getName());
    subFile1SCopy = SnapshotTestHelper.getSnapshotPath(sub, "s1",
        subFile1.getName());
    subsubStatus = hdfs.getFileStatus(subsubSnapshotCopy);
    assertEquals("owner", subsubStatus.getOwner());
    assertEquals("group", subsubStatus.getGroup());
    subsubFile1Status = hdfs.getFileStatus(subsubFile1SCopy);
    assertEquals(REPLICATION, subsubFile1Status.getReplication());
    // also subFile1's snapshot copy should have been moved to diff of s1 as 
    // combination
    subFile1Status = hdfs.getFileStatus(subFile1SCopy);
    assertEquals(REPLICATION_1, subFile1Status.getReplication());
  }
  
  @Test
  public void testDeleteSnapshotCommandWithIllegalArguments() throws Exception {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    PrintStream psOut = new PrintStream(out);
    PrintStream oldOut = System.out;
    PrintStream oldErr = System.err;
    try {
      System.setOut(psOut);
      System.setErr(psOut);
      FsShell shell = new FsShell();
      shell.setConf(conf);

      String[] argv1 = { "-deleteSnapshot", "/tmp" };
      int val = shell.run(argv1);
      assertTrue(val == -1);
      assertTrue(out.toString()
          .contains(argv1[0] + ": Incorrect number of arguments."));
      out.reset();

      String[] argv2 = { "-deleteSnapshot", "/tmp", "s1", "s2" };
      val = shell.run(argv2);
      assertTrue(val == -1);
      assertTrue(out.toString()
          .contains(argv2[0] + ": Incorrect number of arguments."));
      psOut.close();
      out.close();
    } finally {
      System.setOut(oldOut);
      System.setErr(oldErr);
    }
  }

  /*
   * OP_DELETE_SNAPSHOT edits op was not decrementing the safemode threshold on
   * restart in HA mode. HDFS-5504
   */
  @Test(timeout = 60000)
  public void testHANNRestartAfterSnapshotDeletion() throws Exception {
    hdfs.close();
    cluster.shutdown();
    conf = new Configuration();
    cluster = new MiniDFSCluster.Builder(conf)
        .nnTopology(MiniDFSNNTopology.simpleHATopology()).numDataNodes(1)
        .build();
    cluster.transitionToActive(0);
    // stop the standby namenode
    NameNode snn = cluster.getNameNode(1);
    snn.stop();

    hdfs = (DistributedFileSystem) HATestUtil
        .configureFailoverFs(cluster, conf);
    Path dir = new Path("/dir");
    Path subDir = new Path(dir, "sub");
    hdfs.mkdirs(dir);
    hdfs.allowSnapshot(dir);
    for (int i = 0; i < 5; i++) {
      DFSTestUtil.createFile(hdfs, new Path(subDir, "" + i), 100, (short) 1,
          1024L);
    }

    // take snapshot
    hdfs.createSnapshot(dir, "s0");

    // delete the subdir
    hdfs.delete(subDir, true);

    // roll the edit log
    NameNode ann = cluster.getNameNode(0);
    ann.getRpcServer().rollEditLog();

    hdfs.deleteSnapshot(dir, "s0");
    // wait for the blocks deletion at namenode
    Thread.sleep(2000);

    NameNodeAdapter.abortEditLogs(ann);
    cluster.restartNameNode(0, false);
    cluster.transitionToActive(0);

    // wait till the cluster becomes active
    cluster.waitClusterUp();
  }

  @Test
  public void testCorrectNumberOfBlocksAfterRestart()
      throws IOException, InterruptedException {
    final Path foo = new Path("/foo");
    final Path bar = new Path(foo, "bar");
    final Path file = new Path(foo, "file");
    final String snapshotName = "ss0";

    DFSTestUtil.createFile(hdfs, file, BLOCKSIZE, REPLICATION, seed);
    hdfs.mkdirs(bar);
    hdfs.setQuota(foo, Long.MAX_VALUE - 1, Long.MAX_VALUE - 1);
    hdfs.setQuota(bar, Long.MAX_VALUE - 1, Long.MAX_VALUE - 1);
    hdfs.allowSnapshot(foo);

    hdfs.createSnapshot(foo, snapshotName);
    hdfs.setSafeMode(SafeModeAction.ENTER);
    hdfs.saveNamespace();

    hdfs.setSafeMode(SafeModeAction.LEAVE);
    hdfs.deleteSnapshot(foo, snapshotName);
    hdfs.delete(bar, true);
    hdfs.delete(foo, true);

    cluster.restartNameNode(0);
    BlockManagerTestUtil.waitForMarkedDeleteQueueIsEmpty(
        cluster.getNamesystem().getBlockManager());
    assertEquals(0, cluster.getNamesystem().getBlocksTotal());
  }

  /*
   * Test fsimage corruption reported in HDFS-9697.
   */
  @Test
  public void testFsImageCorruption() throws Exception {
    final Path st = new Path("/st");
    final Path nonst = new Path("/nonst");
    final Path stY = new Path(st, "y");
    final Path nonstTrash = new Path(nonst, "trash");

    hdfs.mkdirs(stY);

    hdfs.allowSnapshot(st);
    hdfs.createSnapshot(st, "s0");

    Path f = new Path(stY, "nn.log");
    hdfs.createNewFile(f);
    hdfs.createSnapshot(st, "s1");

    Path f2 = new Path(stY, "nn2.log");
    hdfs.rename(f, f2);
    hdfs.createSnapshot(st, "s2");

    Path trashSt = new Path(nonstTrash, "st");
    hdfs.mkdirs(trashSt);
    hdfs.rename(stY, trashSt);
    hdfs.delete(nonstTrash, true);

    hdfs.deleteSnapshot(st, "s1");
    hdfs.deleteSnapshot(st, "s2");

    hdfs.setSafeMode(SafeModeAction.ENTER);
    hdfs.saveNamespace();
    hdfs.setSafeMode(SafeModeAction.LEAVE);

    cluster.restartNameNodes();
  }

  /*
   * Test renaming file to outside of snapshottable dir then deleting it.
   * Ensure it's deleted from both its parent INodeDirectory and InodeMap,
   * after the last snapshot containing it is deleted.
   */
  @Test
  public void testRenameAndDelete() throws IOException {
    final Path foo = new Path("/foo");
    final Path x = new Path(foo, "x");
    final Path y = new Path(foo, "y");
    final Path trash = new Path("/trash");
    hdfs.mkdirs(x);
    hdfs.mkdirs(y);
    final long parentId = fsdir.getINode4Write(y.toString()).getId();

    hdfs.mkdirs(trash);
    hdfs.allowSnapshot(foo);
    // 1. create snapshot s0
    hdfs.createSnapshot(foo, "s0");
    // 2. create file /foo/x/bar
    final Path file = new Path(x, "bar");
    DFSTestUtil.createFile(hdfs, file, BLOCKSIZE, (short) 1, 0L);
    final long fileId = fsdir.getINode4Write(file.toString()).getId();
    // 3. move file into /foo/y
    final Path newFile = new Path(y, "bar");
    hdfs.rename(file, newFile);
    // 4. create snapshot s1
    hdfs.createSnapshot(foo, "s1");
    // 5. move /foo/y to /trash
    final Path deletedY = new Path(trash, "y");
    hdfs.rename(y, deletedY);
    // 6. create snapshot s2
    hdfs.createSnapshot(foo, "s2");
    // 7. delete /trash/y
    hdfs.delete(deletedY, true);
    // 8. delete snapshot s1
    hdfs.deleteSnapshot(foo, "s1");

    // make sure bar has been removed from its parent
    INode p = fsdir.getInode(parentId);
    Assert.assertNotNull(p);
    INodeDirectory pd = p.asDirectory();
    Assert.assertNotNull(pd);
    Assert.assertNull(pd.getChild("bar".getBytes(), Snapshot.CURRENT_STATE_ID));

    // make sure bar has been cleaned from inodeMap
    Assert.assertNull(fsdir.getInode(fileId));
  }

  @Test
  public void testSnapshotWithConcatException() throws Exception {
    final Path st = new Path("/st");
    hdfs.mkdirs(st);
    hdfs.allowSnapshot(st);

    Path[] files = new Path[3];
    for (int i = 0; i < 3; i++) {
      files[i] = new Path(st, i+ ".txt");
    }

    Path dest = new Path(st, "dest.txt");
    hdfs.createNewFile(dest);
    hdfs.createSnapshot(st, "ss");

    for (int j = 0; j < 3; j++) {
      FileSystem fs = cluster.getFileSystem();
      DFSTestUtil.createFile(fs, files[j], false, 1024,
          1024, 512, (short) 1, RandomUtils.nextLong(1, 512), true);
    }

    hdfs.createSnapshot(st, "s0");

    // Verify the SnapshotException is thrown as expected for HDFS-4529
    exception.expect(RemoteException.class);
    String error = "Concat: the source file /st/0.txt is in snapshot";
    exception.expectMessage(error);
    hdfs.concat(dest, files);

    hdfs.setSafeMode(SafeModeAction.ENTER);
    hdfs.saveNamespace();
    hdfs.setSafeMode(SafeModeAction.LEAVE);

    cluster.restartNameNodes();
  }

  @Test
  public void testSnapshotDeleteWithConcat() throws Exception {
    final Path st = new Path("/st");
    hdfs.mkdirs(st);
    hdfs.allowSnapshot(st);

    Path[] files = new Path[3];
    for (int i = 0; i < 3; i++) {
      files[i] = new Path(st, i+ ".txt");
    }

    Path dest = new Path(st, "dest.txt");
    hdfs.createNewFile(dest);
    hdfs.createSnapshot(st, "ss");

    for (int i = 0; i < 3; i++) {
      for (int j = 0; j < 3; j++) {
        FileSystem fs = cluster.getFileSystem();
        DFSTestUtil.createFile(fs, files[j], false, 1024,
            1024, 512, (short) 1, RandomUtils.nextLong(1, 512), true);
      }

      hdfs.concat(dest, files);

      hdfs.createSnapshot(st, "s" + i);
    }


    hdfs.deleteSnapshot(st, "s1");

    hdfs.setSafeMode(SafeModeAction.ENTER);
    hdfs.saveNamespace();
    hdfs.setSafeMode(SafeModeAction.LEAVE);

    cluster.restartNameNodes();
  }

  @Test
  public void testSnapshotDiffReportWithConcat() throws Exception {
    final Path st = new Path("/st");
    hdfs.mkdirs(st);
    hdfs.allowSnapshot(st);

    Path[] files = new Path[3];
    for (int i = 0; i < 3; i++) {
      files[i] = new Path(st, i+ ".txt");
    }

    Path dest = new Path(st, "dest.txt");
    hdfs.createNewFile(dest);
    hdfs.createSnapshot(st, "ss");

    for (int i = 0; i < 3; i++) {
      for (int j = 0; j < 3; j++) {
        FileSystem fs = cluster.getFileSystem();
        DFSTestUtil.createFile(fs, files[j], false, 1024,
            1024, 512, (short) 1, RandomUtils.nextLong(1, 512), true);
      }

      hdfs.concat(dest, files);

      hdfs.createSnapshot(st, "s" + i);

      SnapshotDiffReport sdr = hdfs.getSnapshotDiffReport(st, "s" + i, "ss");
      LOG.info("Snapshot Diff s{} to ss : {}", i, sdr);
      Assert.assertEquals(sdr.getDiffList().size(), 1);
      Assert.assertTrue(sdr.getDiffList().get(0).getType() ==
          SnapshotDiffReport.DiffType.MODIFY);
      Assert.assertTrue(new Path(st, DFSUtilClient.bytes2String(
          sdr.getDiffList().get(0).getSourcePath())).equals(dest));
    }

    hdfs.deleteSnapshot(st, "s1");

    hdfs.setSafeMode(SafeModeAction.ENTER);
    hdfs.saveNamespace();
    hdfs.setSafeMode(SafeModeAction.LEAVE);

    cluster.restartNameNodes();
  }

}
