cassandra-trunk/test/unit/org/apache/cassandra/tools/SSTablePartitionsTest.java
/*
 * 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.cassandra.tools;

import java.io.IOException;

import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;

import org.apache.cassandra.db.Directories;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.tools.ToolRunner.ToolResult;
import org.apache.cassandra.utils.FBUtilities;
import org.assertj.core.api.AbstractStringAssert;
import org.assertj.core.api.Assertions;

import static java.lang.String.format;

/**
 * Tests for {@link SSTablePartitions}.
 */
public class SSTablePartitionsTest extends OfflineToolUtils
{
    private static final String SSTABLE_1 = sstable("legacy_ma_simple");
    private static final String SSTABLE_2 = sstable("legacy_ma_clust");
    private static final String HEADER_1 = "\nProcessing  #1 (big-ma) (0.169 KiB uncompressed, 0.086 KiB on disk)\n";
    private static final String HEADER_2 = "\nProcessing  #1 (big-ma) (328.145 KiB uncompressed, 5.096 KiB on disk)\n";
    private static final String BACKUPS_HEADER_1 = "\nProcessing Backup:backups  #1 (big-ma) (0.169 KiB uncompressed, 0.086 KiB on disk)\n";
    private static final String BACKUPS_HEADER_2 = "\nProcessing Backup:backups  #1 (big-ma) (328.145 KiB uncompressed, 5.096 KiB on disk)\n";
    private static final String SNAPSHOTS_HEADER_1 = "\nProcessing Snapshot:snapshot-1  #1 (big-ma) (0.169 KiB uncompressed, 0.086 KiB on disk)\n";
    private static final String SNAPSHOTS_HEADER_2 = "\nProcessing Snapshot:snapshot-1  #1 (big-ma) (328.145 KiB uncompressed, 5.096 KiB on disk)\n";
    private static final String SUMMARY_1 = "               Partition size            Row count           Cell count      Tombstone count\n" +
                                            "  ~p50              0.034 KiB                    1                    1                    0\n" +
                                            "  ~p75              0.034 KiB                    1                    1                    0\n" +
                                            "  ~p90              0.034 KiB                    1                    1                    0\n" +
                                            "  ~p95              0.034 KiB                    1                    1                    0\n" +
                                            "  ~p99              0.034 KiB                    1                    1                    0\n" +
                                            "  ~p999             0.034 KiB                    1                    1                    0\n" +
                                            "  min               0.032 KiB                    1                    1                    0\n" +
                                            "  max               0.034 KiB                    1                    1                    0\n" +
                                            "  count                     5\n";
    private static final String SUMMARY_2 = "               Partition size            Row count           Cell count      Tombstone count\n" +
                                            "  ~p50             71.735 KiB                   50                   50                    0\n" +
                                            "  ~p75             71.735 KiB                   50                   50                    0\n" +
                                            "  ~p90             71.735 KiB                   50                   50                    0\n" +
                                            "  ~p95             71.735 KiB                   50                   50                    0\n" +
                                            "  ~p99             71.735 KiB                   50                   50                    0\n" +
                                            "  ~p999            71.735 KiB                   50                   50                    0\n" +
                                            "  min              65.625 KiB                   50                   50                    0\n" +
                                            "  max              65.630 KiB                   50                   50                    0\n" +
                                            "  count                     5\n";

    @BeforeClass
    public static void prepareDirectories()
    {
        createBackupsAndSnapshots(SSTABLE_1);
        createBackupsAndSnapshots(SSTABLE_2);
    }

    private static void createBackupsAndSnapshots(String sstable)
    {
        File parentDir = new File(sstable).parent();

        File backupsDir = new File(parentDir, Directories.BACKUPS_SUBDIR);
        backupsDir.tryCreateDirectory();

        File snapshotsDir = new File(parentDir, Directories.SNAPSHOT_SUBDIR);
        snapshotsDir.tryCreateDirectory();

        File snapshotDir = new File(snapshotsDir, "snapshot-1");
        snapshotDir.tryCreateDirectory();

        for (File f : parentDir.tryList(File::isFile))
        {
            FileUtils.copyWithOutConfirm(f, new File(backupsDir, f.name()));
            FileUtils.copyWithOutConfirm(f, new File(snapshotDir, f.name()));
        }
    }

    /**
     * Runs post-test assertions about loaded classed and started threads.
     */
    @After
    public void assertPostTestEnv()
    {
        assertNoUnexpectedThreadsStarted(OPTIONAL_THREADS_WITH_SCHEMA, false);
        assertCLSMNotLoaded();
        assertSystemKSNotLoaded();
        assertKeyspaceNotLoaded();
        assertServerNotLoaded();
    }

    /**
     * Verify that the tool prints help when no arguments are provided.
     */
    @Test
    public void testNoArgsPrintsHelp()
    {
        ToolResult tool = ToolRunner.invokeClass(SSTablePartitions.class);
        Assertions.assertThat(tool.getExitCode()).isOne();
        Assertions.assertThat(tool.getCleanedStderr()).contains("You must supply at least one sstable or directory");
        Assertions.assertThat(tool.getStdout()).contains("usage");
    }

    /**
     * Verify that the tool prints the right help contents.
     * If you added, modified options or help, please update docs if necessary.
     */
    @Test
    public void testMaybeChangeDocs()
    {
        ToolResult tool = ToolRunner.invokeClass(SSTablePartitions.class);
        Assertions.assertThat(tool.getStdout())
                  .isEqualTo("usage: sstablepartitions <options> <sstable files or directories>\n" +
                             "                         \n" +
                             "Print partition statistics of one or more sstables.\n" +
                             " -b,--backups                   include backups present in data\n" +
                             "                                directories (recursive scans)\n" +
                             " -c,--min-cells <arg>           partition cell count threshold\n" +
                             " -k,--key <arg>                 Partition keys to include\n" +
                             " -m,--csv                       CSV output (machine readable)\n" +
                             " -o,--min-tombstones <arg>      partition tombstone count threshold\n" +
                             " -r,--recursive                 scan for sstables recursively\n" +
                             " -s,--snapshots                 include snapshots present in data\n" +
                             "                                directories (recursive scans)\n" +
                             " -t,--min-size <arg>            partition size threshold, expressed as\n" +
                             "                                either the number of bytes or a size with unit of the form 10KiB, 20MiB,\n" +
                             "                                30GiB, etc.\n" +
                             " -u,--current-timestamp <arg>   timestamp (seconds since epoch, unit time)\n" +
                             "                                for TTL expired calculation\n" +
                             " -w,--min-rows <arg>            partition row count threshold\n" +
                             " -x,--exclude-key <arg>         Excluded partition key(s) from partition\n" +
                             "                                detailed row/cell/tombstone information (irrelevant, if --partitions-only\n" +
                             "                                is given)\n" +
                             " -y,--partitions-only           Do not process per-partition detailed\n" +
                             "                                row/cell/tombstone information, only brief information\n");
    }

    /**
     * Verify that the tool can select single sstable file.
     */
    @Test
    public void testSingleSSTable()
    {
        assertThatToolSucceds(SSTABLE_1).isEqualTo(HEADER_1 + SUMMARY_1);
        assertThatToolSucceds(SSTABLE_2).isEqualTo(HEADER_2 + SUMMARY_2);
    }

    /**
     * Verify that the tool can select multiple sstable files.
     */
    @Test
    public void testMultipleSSTables()
    {
        assertThatToolSucceds(SSTABLE_1, SSTABLE_2)
                .isEqualTo(HEADER_2 + SUMMARY_2 + HEADER_1 + SUMMARY_1);
    }

    /**
     * Verify that the tool can select all the sstable files in a directory.
     */
    @Test
    public void testDirectory()
    {
        assertThatToolSucceds(new File(SSTABLE_1).parentPath())
                .isEqualTo(HEADER_1 + SUMMARY_1);

        assertThatToolSucceds("-r", new File(SSTABLE_1).parent().parentPath())
                .contains(HEADER_1 + SUMMARY_1)
                .contains(HEADER_2 + SUMMARY_2);

        assertThatToolSucceds("--recursive", new File(SSTABLE_2).parent().parentPath())
                .contains(HEADER_1 + SUMMARY_1)
                .contains(HEADER_2 + SUMMARY_2);
    }

    /**
     * Test the flag for collecting and printing sstable partition sizes only.
     */
    @Test
    public void testPartitionsOnly()
    {
        testPartitionsOnly("-y");
        testPartitionsOnly("--partitions-only");
    }

    private static void testPartitionsOnly(String option)
    {
        assertThatToolSucceds(SSTABLE_1, option)
                .isEqualTo(HEADER_1 +
                           "               Partition size\n" +
                           "  ~p50              0.034 KiB\n" +
                           "  ~p75              0.034 KiB\n" +
                           "  ~p90              0.034 KiB\n" +
                           "  ~p95              0.034 KiB\n" +
                           "  ~p99              0.034 KiB\n" +
                           "  ~p999             0.034 KiB\n" +
                           "  min               0.032 KiB\n" +
                           "  max               0.034 KiB\n" +
                           "  count                     5\n");

        assertThatToolSucceds(SSTABLE_2, "--partitions-only")
                .isEqualTo(HEADER_2 +
                           "               Partition size\n" +
                           "  ~p50             71.735 KiB\n" +
                           "  ~p75             71.735 KiB\n" +
                           "  ~p90             71.735 KiB\n" +
                           "  ~p95             71.735 KiB\n" +
                           "  ~p99             71.735 KiB\n" +
                           "  ~p999            71.735 KiB\n" +
                           "  min              65.625 KiB\n" +
                           "  max              65.630 KiB\n" +
                           "  count                     5\n");
    }

    /**
     * Test the flag for detecting partitions over a certain size threshold.
     */
    @Test
    public void testMinSize()
    {
        testMinSize("-t");
        testMinSize("--min-size");
    }

    private static void testMinSize(String option)
    {
        assertThatToolSucceds(SSTABLE_1, SSTABLE_2, option, "35")
                .isEqualTo(HEADER_2 +
                           "  Partition: '0' (30) live, size: 65.625 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "Summary of  #1 (big-ma):\n" +
                           "  File: " + SSTABLE_2 + "\n" +
                           "  5 partitions match\n" +
                           "  Keys: 0 1 2 3 4\n" +
                           SUMMARY_2 +
                           HEADER_1 +
                           "  Partition: '1' (31) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '2' (32) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '3' (33) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '4' (34) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "Summary of  #1 (big-ma):\n" +
                           "  File: " + SSTABLE_1 + "\n" +
                           "  4 partitions match\n" +
                           "  Keys: 1 2 3 4\n" +
                           SUMMARY_1);

        assertThatToolSucceds(SSTABLE_1, SSTABLE_2, "--min-size", "36")
                .isEqualTo(HEADER_2 +
                           "  Partition: '0' (30) live, size: 65.625 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "Summary of  #1 (big-ma):\n" +
                           "  File: " + SSTABLE_2 + "\n" +
                           "  5 partitions match\n" +
                           "  Keys: 0 1 2 3 4\n" +
                           SUMMARY_2 + HEADER_1 + SUMMARY_1);

        assertThatToolSucceds(SSTABLE_1, SSTABLE_2, "--min-size", "67201")
                .isEqualTo(HEADER_2 +
                           "  Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "Summary of  #1 (big-ma):\n" +
                           "  File: " + SSTABLE_2 + "\n" +
                           "  4 partitions match\n" +
                           "  Keys: 1 2 3 4\n" +
                           SUMMARY_2 + HEADER_1 + SUMMARY_1);

        assertThatToolSucceds(SSTABLE_1, SSTABLE_2, "--min-size", "67201B")
                .isEqualTo(HEADER_2 +
                           "  Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "Summary of  #1 (big-ma):\n" +
                           "  File: " + SSTABLE_2 + "\n" +
                           "  4 partitions match\n" +
                           "  Keys: 1 2 3 4\n" +
                           SUMMARY_2 + HEADER_1 + SUMMARY_1);

        assertThatToolSucceds(SSTABLE_1, SSTABLE_2, "--min-size", "65KiB")
                .isEqualTo(HEADER_2 +
                           "  Partition: '0' (30) live, size: 65.625 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "Summary of  #1 (big-ma):\n" +
                           "  File: " + SSTABLE_2 + "\n" +
                           "  5 partitions match\n" +
                           "  Keys: 0 1 2 3 4\n" +
                           SUMMARY_2 + HEADER_1 + SUMMARY_1);
    }

    /**
     * Test the flag for detecting partitions with more cells than a certain threshold.
     */
    @Test
    public void testMinCells()
    {
        testMinCells("-c");
        testMinCells("--min-cells");
    }

    private static void testMinCells(String option)
    {
        assertThatToolSucceds(SSTABLE_1, SSTABLE_2, option, "0")
                .isEqualTo(HEADER_2 +
                           "  Partition: '0' (30) live, size: 65.625 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "Summary of  #1 (big-ma):\n" +
                           "  File: " + SSTABLE_2 + "\n" +
                           "  5 partitions match\n" +
                           "  Keys: 0 1 2 3 4\n" +
                           SUMMARY_2 +
                           HEADER_1 +
                           "  Partition: '0' (30) live, size: 0.032 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '1' (31) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '2' (32) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '3' (33) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '4' (34) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "Summary of  #1 (big-ma):\n" +
                           "  File: " + SSTABLE_1 + "\n" +
                           "  5 partitions match\n" +
                           "  Keys: 0 1 2 3 4\n" +
                           SUMMARY_1);

        assertThatToolSucceds(SSTABLE_1, SSTABLE_2, option, "2")
                .isEqualTo(HEADER_2 +
                           "  Partition: '0' (30) live, size: 65.625 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "Summary of  #1 (big-ma):\n" +
                           "  File: " + SSTABLE_2 + "\n" +
                           "  5 partitions match\n" +
                           "  Keys: 0 1 2 3 4\n" +
                           SUMMARY_2 + HEADER_1 + SUMMARY_1);

        assertThatToolSucceds(SSTABLE_1, SSTABLE_2, option, "51")
                .isEqualTo(HEADER_2 + SUMMARY_2 +
                           HEADER_1 + SUMMARY_1);
    }

    /**
     * Test the flag for detecting partitions with more rows than a certain threshold.
     */
    @Test
    public void testMinRows()
    {
        testMinRows("-w");
        testMinRows("--min-rows");
    }

    private static void testMinRows(String option)
    {
        assertThatToolSucceds(SSTABLE_1, SSTABLE_2, option, "0")
                .isEqualTo(HEADER_2 +
                           "  Partition: '0' (30) live, size: 65.625 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "Summary of  #1 (big-ma):\n" +
                           "  File: " + SSTABLE_2 + "\n" +
                           "  5 partitions match\n" +
                           "  Keys: 0 1 2 3 4\n" +
                           SUMMARY_2 +
                           HEADER_1 +
                           "  Partition: '0' (30) live, size: 0.032 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '1' (31) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '2' (32) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '3' (33) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '4' (34) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "Summary of  #1 (big-ma):\n" +
                           "  File: " + SSTABLE_1 + "\n" +
                           "  5 partitions match\n" +
                           "  Keys: 0 1 2 3 4\n" +
                           SUMMARY_1);

        assertThatToolSucceds(SSTABLE_1, SSTABLE_2, option, "50")
                .isEqualTo(HEADER_2 +
                           "  Partition: '0' (30) live, size: 65.625 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "Summary of  #1 (big-ma):\n" +
                           "  File: " + SSTABLE_2 + "\n" +
                           "  5 partitions match\n" +
                           "  Keys: 0 1 2 3 4\n" +
                           SUMMARY_2 + HEADER_1 + SUMMARY_1);

        assertThatToolSucceds(SSTABLE_1, SSTABLE_2, option, "51")
                .isEqualTo(HEADER_2 + SUMMARY_2 +
                           HEADER_1 + SUMMARY_1);
    }

    /**
     * Test the flag for detecting partitions with more tombstones than a certain threshold.
     */
    @Test
    public void testMinTombstones()
    {
        testMinTombstones("-o");
        testMinTombstones("--min-tombstones");
    }

    private static void testMinTombstones(String option)
    {
        assertThatToolSucceds(SSTABLE_1, SSTABLE_2, option, "0")
                .isEqualTo(HEADER_2 +
                           "  Partition: '0' (30) live, size: 65.625 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '1' (31) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '2' (32) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '3' (33) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '4' (34) live, size: 65.630 KiB, rows: 50, cells: 50, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "Summary of  #1 (big-ma):\n" +
                           "  File: " + SSTABLE_2 + "\n" +
                           "  5 partitions match\n" +
                           "  Keys: 0 1 2 3 4\n" +
                           SUMMARY_2 +
                           HEADER_1 +
                           "  Partition: '0' (30) live, size: 0.032 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '1' (31) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '2' (32) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '3' (33) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '4' (34) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "Summary of  #1 (big-ma):\n" +
                           "  File: " + SSTABLE_1 + "\n" +
                           "  5 partitions match\n" +
                           "  Keys: 0 1 2 3 4\n" +
                           SUMMARY_1);

        assertThatToolSucceds(SSTABLE_1, SSTABLE_2, "--min-tombstones", "1")
                .isEqualTo(HEADER_2 + SUMMARY_2 + HEADER_1 + SUMMARY_1);
    }

    /**
     * Test the flag for providing a current time.
     */
    @Test
    public void testCurrentTimestamp()
    {
        testCurrentTimestamp("-u");
        testCurrentTimestamp("--current-timestamp");
    }

    private static void testCurrentTimestamp(String option)
    {
        String now = String.valueOf(FBUtilities.nowInSeconds());
        assertThatToolSucceds(SSTABLE_1, option, now).isEqualTo(HEADER_1 + SUMMARY_1);
    }

    /**
     * Test the flag for including backup sstables.
     */
    @Test
    public void testBackups()
    {
        testBackups("-b");
        testBackups("--backups");
    }

    private static void testBackups(String option)
    {
        assertThatToolSucceds(new File(SSTABLE_1).parentPath(), "-r", option)
                .isEqualTo(HEADER_1 + SUMMARY_1 + BACKUPS_HEADER_1 + SUMMARY_1);

        assertThatToolSucceds(new File(SSTABLE_2).parentPath(), "-r", option)
                .isEqualTo(HEADER_2 + SUMMARY_2 + BACKUPS_HEADER_2 + SUMMARY_2);
    }

    /**
     * Test the flag for including snapshot sstables.
     */
    @Test
    public void testSnapshots()
    {
        testSnapshots("-s");
        testSnapshots("--snapshots");
    }

    private static void testSnapshots(String option)
    {
        assertThatToolSucceds(new File(SSTABLE_1).parentPath(), "-r", option)
                .isEqualTo(HEADER_1 + SUMMARY_1 + SNAPSHOTS_HEADER_1 + SUMMARY_1);

        assertThatToolSucceds(new File(SSTABLE_2).parentPath(), "-r", option)
                .isEqualTo(HEADER_2 + SUMMARY_2 + SNAPSHOTS_HEADER_2 + SUMMARY_2);
    }

    /**
     * Test the flag for specifying partiton keys to be considered.
     */
    @Test
    public void testIncludedKeys()
    {
        testIncludedKeys("-k");
        testIncludedKeys("--key");
    }

    private static void testIncludedKeys(String option)
    {
        assertThatToolSucceds(SSTABLE_1, "--min-size", "0", option, "1", option, "3")
                .contains(HEADER_1 +
                           "  Partition: '1' (31) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '3' (33) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "Summary of  #1 (big-ma):\n" +
                           "  File: " + SSTABLE_1 + "\n" +
                           "  2 partitions match\n" +
                           "  Keys: 1 3\n")
                .contains("count                     2\n");

        assertThatToolSucceds(SSTABLE_1, "--min-size", "0", option, "0", option, "2", option, "4")
                .contains(HEADER_1 +
                          "  Partition: '0' (30) live, size: 0.032 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                          "  Partition: '2' (32) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                          "  Partition: '4' (34) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                          "Summary of  #1 (big-ma):\n" +
                          "  File: " + SSTABLE_1 + "\n" +
                          "  3 partitions match\n" +
                          "  Keys: 0 2 4\n")
                .contains("count                     3\n");

        assertThatToolSucceds(SSTABLE_1, "-y","--min-size", "0", option, "0", option, "2", option, "4")
                .contains(HEADER_1 +
                          "  Partition: '0' (30) live, size: 0.032 KiB\n" +
                          "  Partition: '2' (32) live, size: 0.034 KiB\n" +
                          "  Partition: '4' (34) live, size: 0.034 KiB\n" +
                          "Summary of  #1 (big-ma):\n" +
                          "  File: " + SSTABLE_1 + "\n" +
                          "  3 partitions match\n" +
                          "  Keys: 0 2 4\n")
                .contains("count                     3\n");
    }

    /**
     * Test the flag for specifying partiton keys to be excluded.
     */
    @Test
    public void testExcludedKeys()
    {
        testExcludedKeys("-x");
        testExcludedKeys("--exclude-key");
    }

    private static void testExcludedKeys(String option)
    {
        assertThatToolSucceds(SSTABLE_1, "--min-size", "0", option, "1", option, "3")
                .contains(HEADER_1 +
                           "  Partition: '0' (30) live, size: 0.032 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '2' (32) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '4' (34) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "Summary of  #1 (big-ma):\n" +
                           "  File: " + SSTABLE_1 + "\n" +
                           "  3 partitions match\n" +
                           "  Keys: 0 2 4\n")
                .contains("count                     3\n");

        assertThatToolSucceds(SSTABLE_1, "--min-size", "0", option, "0", option, "2", option, "4")
                .contains(HEADER_1 +
                           "  Partition: '1' (31) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "  Partition: '3' (33) live, size: 0.034 KiB, rows: 1, cells: 1, tombstones: 0 (row:0, range:0, complex:0, cell:0, row-TTLd:0, cell-TTLd:0)\n" +
                           "Summary of  #1 (big-ma):\n" +
                           "  File: " + SSTABLE_1 + "\n" +
                           "  2 partitions match\n" +
                           "  Keys: 1 3\n")
                .contains("count                     2\n");
    }

    /**
     * Test the flag for producing machine-readable CSV output.
     */
    @Test
    public void testCSV()
    {
        testCSV("-m");
        testCSV("--csv");
    }

    private static void testCSV(String option)
    {
        assertThatToolSucceds(option, "--min-size", "35", SSTABLE_1, SSTABLE_2)
                .isEqualTo(format("key,keyBinary,live,offset,size,rowCount,cellCount,tombstoneCount," +
                                  "rowTombstoneCount,rangeTombstoneCount,complexTombstoneCount,cellTombstoneCount," +
                                  "rowTtlExpired,cellTtlExpired,directory,keyspace,table,index,snapshot,backup," +
                                  "generation,format,version\n" +
                                  "\"0\",30,true,0,67200,50,50,0,0,0,0,0,0,0,%s,,,,,,1,big,ma\n" +
                                  "\"1\",31,true,67200,67205,50,50,0,0,0,0,0,0,0,%<s,,,,,,1,big,ma\n" +
                                  "\"2\",32,true,134405,67205,50,50,0,0,0,0,0,0,0,%<s,,,,,,1,big,ma\n" +
                                  "\"3\",33,true,201610,67205,50,50,0,0,0,0,0,0,0,%<s,,,,,,1,big,ma\n" +
                                  "\"4\",34,true,268815,67205,50,50,0,0,0,0,0,0,0,%<s,,,,,,1,big,ma\n" +
                                  "\"1\",31,true,33,35,1,1,0,0,0,0,0,0,0,%s,,,,,,1,big,ma\n" +
                                  "\"2\",32,true,68,35,1,1,0,0,0,0,0,0,0,%<s,,,,,,1,big,ma\n" +
                                  "\"3\",33,true,103,35,1,1,0,0,0,0,0,0,0,%<s,,,,,,1,big,ma\n" +
                                  "\"4\",34,true,138,35,1,1,0,0,0,0,0,0,0,%<s,,,,,,1,big,ma\n",
                                  SSTABLE_2, SSTABLE_1));
    }

    private static AbstractStringAssert<?> assertThatToolSucceds(String... args)
    {
        ToolResult tool = invokeTool(args);
        Assertions.assertThat(tool.getExitCode()).isZero();
        tool.assertOnCleanExit();
        return Assertions.assertThat(tool.getStdout());
    }

    private static String sstable(String table)
    {
        try
        {
            return findOneSSTable("legacy_sstables", table);
        }
        catch (IOException e)
        {
            throw new RuntimeException(e);
        }
    }

    private static ToolResult invokeTool(String... args)
    {
        return ToolRunner.invokeClass(SSTablePartitions.class, args);
    }
}
