/*
 * Copyright (c) 2014 The Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
 * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package htsjdk.variant.variantcontext.writer;

import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import htsjdk.samtools.Defaults;
import htsjdk.samtools.SAMSequenceDictionary;
import htsjdk.samtools.util.BlockCompressedOutputStream;
import htsjdk.samtools.util.FileExtensions;
import htsjdk.tribble.Tribble;
import htsjdk.variant.VariantBaseTest;
import htsjdk.variant.variantcontext.writer.VariantContextWriterBuilder.OutputType;
import htsjdk.variant.vcf.VCFHeader;
import java.io.*;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import org.testng.Assert;
import org.testng.annotations.BeforeSuite;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

public class VariantContextWriterBuilderUnitTest extends VariantBaseTest {
    private static final String TEST_BASENAME = "htsjdk-test VariantContextWriterBuilderUnitTest";
    private SAMSequenceDictionary dictionary;

    private File vcf;
    private File vcfIdx;
    private File vcfMD5;
    private File bcf;
    private File bcfIdx;
    private File unknown;

    private List<File> blockCompressedVCFs;
    private List<File> blockCompressedIndices;

    @BeforeSuite
    public void before() throws IOException {
        dictionary = createArtificialSequenceDictionary();
        vcf = File.createTempFile(TEST_BASENAME, FileExtensions.VCF);
        vcf.deleteOnExit();
        vcfIdx = Tribble.indexFile(vcf);
        vcfIdx.deleteOnExit();
        vcfMD5 = new File(vcf.getAbsolutePath() + ".md5");
        vcfMD5.deleteOnExit();
        bcf = File.createTempFile(TEST_BASENAME, FileExtensions.BCF);
        bcf.deleteOnExit();
        bcfIdx = Tribble.indexFile(bcf);
        bcfIdx.deleteOnExit();
        unknown = File.createTempFile(TEST_BASENAME, ".unknown");
        unknown.deleteOnExit();

        blockCompressedVCFs = new ArrayList<File>();
        blockCompressedIndices = new ArrayList<File>();
        for (final String extension : FileExtensions.BLOCK_COMPRESSED) {
            final File blockCompressed = File.createTempFile(TEST_BASENAME, FileExtensions.VCF + extension);
            blockCompressed.deleteOnExit();
            blockCompressedVCFs.add(blockCompressed);

            final File index = new File(blockCompressed.getAbsolutePath() + FileExtensions.TABIX_INDEX);
            index.deleteOnExit();
            blockCompressedIndices.add(index);
        }
    }

    @Test
    public void testSetOutputFile() throws IOException {
        final VariantContextWriterBuilder builder =
                new VariantContextWriterBuilder().setReferenceDictionary(dictionary);

        VariantContextWriter writer =
                builder.setOutputFile(vcf.getAbsolutePath()).build();
        Assert.assertTrue(writer instanceof VCFWriter, "testSetOutputFile VCF String");
        Assert.assertFalse(
                ((VCFWriter) writer).getOutputStream() instanceof BlockCompressedOutputStream,
                "testSetOutputFile VCF String was compressed");

        writer = builder.setOutputFile(vcf).build();
        Assert.assertTrue(writer instanceof VCFWriter, "testSetOutputFile VCF File");
        Assert.assertFalse(
                ((VCFWriter) writer).getOutputStream() instanceof BlockCompressedOutputStream,
                "testSetOutputFile VCF File was compressed");

        for (final String extension : FileExtensions.BLOCK_COMPRESSED) {
            final File file = File.createTempFile(TEST_BASENAME + ".setoutput", extension);
            file.deleteOnExit();
            final String filename = file.getAbsolutePath();

            writer = builder.setOutputFile(filename).build();
            Assert.assertTrue(writer instanceof VCFWriter, "testSetOutputFile " + extension + " String");
            Assert.assertTrue(
                    ((VCFWriter) writer).getOutputStream() instanceof BlockCompressedOutputStream,
                    "testSetOutputFile " + extension + " String was not compressed");

            writer = builder.setOutputFile(file).build();
            Assert.assertTrue(writer instanceof VCFWriter, "testSetOutputFile " + extension + " File");
            Assert.assertTrue(
                    ((VCFWriter) writer).getOutputStream() instanceof BlockCompressedOutputStream,
                    "testSetOutputFile " + extension + " File was not compressed");
        }

        writer = builder.setOutputFile(bcf).build();
        Assert.assertTrue(writer instanceof BCF2Writer, "testSetOutputFile BCF String");

        writer = builder.setOutputFile(bcf.getAbsolutePath()).build();
        Assert.assertTrue(writer instanceof BCF2Writer, "testSetOutputFile BCF File");
    }

    @DataProvider
    public Iterator<Object[]> getFilesAndOutputTypes() {
        List<Object[]> cases = new ArrayList<>();
        cases.add(new Object[] {OutputType.VCF, this.vcf});
        cases.add(new Object[] {OutputType.BCF, this.bcf});
        cases.add(new Object[] {OutputType.VCF_STREAM, new File("/dev/stdout")});
        for (final File file : this.blockCompressedVCFs) {
            cases.add(new Object[] {OutputType.BLOCK_COMPRESSED_VCF, file});
        }
        return cases.iterator();
    }

    @Test(dataProvider = "getFilesAndOutputTypes")
    public void testDetermineOutputType(OutputType expected, File file) {
        Assert.assertEquals(VariantContextWriterBuilder.determineOutputTypeFromFile(file), expected);
    }

    @Test(dataProvider = "getFilesAndOutputTypes")
    public void testDetermineOutputTypeWithSymlink(OutputType expected, File file) throws IOException {
        final File link = setUpSymlink(file);
        Assert.assertEquals(expected, VariantContextWriterBuilder.determineOutputTypeFromFile(link));
    }

    @Test(dataProvider = "getFilesAndOutputTypes")
    public void testDetermineOutputTypeOnPath(OutputType expected, File file) {
        Assert.assertEquals(VariantContextWriterBuilder.determineOutputTypeFromFile(file.toPath()), expected);
    }

    @Test(dataProvider = "getFilesAndOutputTypes")
    public void testDetermineOutputTypeWithSymlinkOnPath(OutputType expected, File file) throws IOException {
        final File link = setUpSymlink(file);
        Assert.assertEquals(expected, VariantContextWriterBuilder.determineOutputTypeFromFile(link.toPath()));
    }

    private static File setUpSymlink(File target) throws IOException {
        final Path link = Files.createTempFile("foo.", ".tmp");
        Files.deleteIfExists(link);
        Files.createSymbolicLink(link, target.toPath());
        link.toFile().deleteOnExit();
        return link.toFile();
    }

    @Test
    public void testSetOutputFileType() {
        final VariantContextWriterBuilder builder = new VariantContextWriterBuilder()
                .setReferenceDictionary(dictionary)
                .setOutputFile(unknown);

        VariantContextWriter writer = builder.setOutputFileType(VariantContextWriterBuilder.OutputType.VCF)
                .build();
        Assert.assertTrue(writer instanceof VCFWriter, "testSetOutputFileType VCF");
        Assert.assertFalse(
                ((VCFWriter) writer).getOutputStream() instanceof BlockCompressedOutputStream,
                "testSetOutputFileType VCF was compressed");

        writer = builder.setOption(Options.FORCE_BCF).build();
        Assert.assertTrue(writer instanceof BCF2Writer, "testSetOutputFileType FORCE_BCF set -> expected BCF, was VCF");

        // test that FORCE_BCF remains in effect, overriding the explicit setting of VCF
        writer = builder.setOutputFileType(VariantContextWriterBuilder.OutputType.VCF)
                .build();
        Assert.assertTrue(
                writer instanceof BCF2Writer, "testSetOutputFileType FORCE_BCF set 2 -> expected BCF, was VCF");

        writer = builder.unsetOption(Options.FORCE_BCF).build();
        Assert.assertTrue(
                writer instanceof VCFWriter, "testSetOutputFileType FORCE_BCF unset -> expected VCF, was BCF");
        Assert.assertFalse(
                ((VCFWriter) writer).getOutputStream() instanceof BlockCompressedOutputStream,
                "testSetOutputFileType FORCE_BCF unset was compressed");

        writer = builder.setOutputFileType(VariantContextWriterBuilder.OutputType.BLOCK_COMPRESSED_VCF)
                .build();
        Assert.assertTrue(writer instanceof VCFWriter, "testSetOutputFile BLOCK_COMPRESSED_VCF");
        Assert.assertTrue(
                ((VCFWriter) writer).getOutputStream() instanceof BlockCompressedOutputStream,
                "testSetOutputFileType BLOCK_COMPRESSED_VCF was not compressed");

        writer = builder.setOutputFileType(VariantContextWriterBuilder.OutputType.BCF)
                .build();
        Assert.assertTrue(writer instanceof BCF2Writer, "testSetOutputFileType BCF");
    }

    @Test
    public void testSetOutputStream() {
        final OutputStream stream = new ByteArrayOutputStream();

        final VariantContextWriterBuilder builder = new VariantContextWriterBuilder()
                .setReferenceDictionary(dictionary)
                .unsetOption(Options.INDEX_ON_THE_FLY)
                .setOutputStream(stream);

        VariantContextWriter writer = builder.build();
        Assert.assertTrue(writer instanceof VCFWriter, "testSetOutputStream default");

        writer = builder.setOption(Options.FORCE_BCF).build();
        Assert.assertTrue(
                writer instanceof BCF2Writer,
                "testSetOutputStream FORCE_BCF set -> expected BCF stream, was VCF stream");

        // test that FORCE_BCF remains in effect, overriding the explicit setting of VCF
        writer = builder.setOutputVCFStream(stream).build();
        Assert.assertTrue(
                writer instanceof BCF2Writer,
                "testSetOutputStream FORCE_BCF set 2 -> expected BCF stream, was VCF stream");

        writer = builder.unsetOption(Options.FORCE_BCF).build();
        Assert.assertTrue(
                writer instanceof VCFWriter,
                "testSetOutputStream FORCE_BCF unset -> expected VCF stream, was BCF stream");

        writer = builder.setOutputBCFStream(stream).build();
        Assert.assertTrue(writer instanceof BCF2Writer, "testSetOutputStream BCF");

        writer = builder.setOutputVCFStream(stream).build();
        Assert.assertTrue(writer instanceof VCFWriter, "testSetOutputStream VCF");
    }

    @Test
    public void testAsync() {
        final VariantContextWriterBuilder builder = new VariantContextWriterBuilder()
                .setReferenceDictionary(dictionary)
                .setOutputFile(vcf);

        VariantContextWriter writer = builder.build();
        Assert.assertEquals(
                writer instanceof AsyncVariantContextWriter,
                Defaults.USE_ASYNC_IO_WRITE_FOR_TRIBBLE,
                "testAsync default");

        writer = builder.setOption(Options.USE_ASYNC_IO).build();
        Assert.assertTrue(writer instanceof AsyncVariantContextWriter, "testAsync option=set");

        writer = builder.unsetOption(Options.USE_ASYNC_IO).build();
        Assert.assertFalse(writer instanceof AsyncVariantContextWriter, "testAsync option=unset");
    }

    @Test
    public void testBuffering() {
        final VariantContextWriterBuilder builder = new VariantContextWriterBuilder()
                .setReferenceDictionary(dictionary)
                .setOutputFile(vcf)
                .unsetOption(Options.INDEX_ON_THE_FLY); // so the potential BufferedOutputStream is not wrapped in a
        // PositionalOutputStream

        VariantContextWriter writer = builder.build();
        Assert.assertTrue(
                ((VCFWriter) writer).getOutputStream() instanceof BufferedOutputStream,
                "testBuffering was not buffered by default");

        writer = builder.unsetBuffering().build();
        Assert.assertFalse(
                ((VCFWriter) writer).getOutputStream() instanceof BufferedOutputStream,
                "testBuffering was buffered when unset");

        writer = builder.setBuffer(8192).build();
        Assert.assertTrue(
                ((VCFWriter) writer).getOutputStream() instanceof BufferedOutputStream,
                "testBuffering was not buffered when set");
    }

    @Test
    public void testMD5() {
        final VariantContextWriterBuilder builder = new VariantContextWriterBuilder()
                .setReferenceDictionary(dictionary)
                .setOutputFile(vcf);

        VariantContextWriter writer = builder.build();
        writer.close();
        Assert.assertEquals(vcfMD5.exists(), Defaults.CREATE_MD5, "MD5 default setting not respected");

        if (vcfMD5.exists()) vcfMD5.delete();

        writer = builder.setCreateMD5().build();
        writer.close();
        Assert.assertTrue(vcfMD5.exists(), "MD5 not created when requested");
        vcfMD5.delete();

        writer = builder.unsetCreateMD5().build();
        writer.close();
        Assert.assertFalse(vcfMD5.exists(), "MD5 created when not requested");

        writer = builder.setCreateMD5(false).build();
        writer.close();
        Assert.assertFalse(vcfMD5.exists(), "MD5 created when not requested via boolean parameter");

        writer = builder.setCreateMD5(true).build();
        writer.close();
        Assert.assertTrue(vcfMD5.exists(), "MD5 not created when requested via boolean parameter");
        vcfMD5.delete();

        for (final File blockCompressed : blockCompressedVCFs) {
            final File md5 = new File(blockCompressed + ".md5");
            if (md5.exists()) md5.delete();
            md5.deleteOnExit();
            writer = builder.setOutputFile(blockCompressed).build();
            writer.close();
            Assert.assertTrue(md5.exists(), "MD5 digest not created for " + blockCompressed);
        }
    }

    @Test
    public void testIndexingOnTheFly() {
        final VariantContextWriterBuilder builder = new VariantContextWriterBuilder()
                .setReferenceDictionary(dictionary)
                .setOption(Options.INDEX_ON_THE_FLY);

        if (vcfIdx.exists()) vcfIdx.delete();
        VariantContextWriter writer = builder.setOutputFile(vcf).build();
        writer.close();
        Assert.assertTrue(vcfIdx.exists(), String.format("VCF index not created for %s / %s", vcf, vcfIdx));

        if (bcfIdx.exists()) bcfIdx.delete();
        writer = builder.setOutputFile(bcf).build();
        writer.close();
        Assert.assertTrue(bcfIdx.exists(), String.format("BCF index not created for %s / %s", bcf, bcfIdx));

        for (int i = 0; i < blockCompressedVCFs.size(); i++) {
            final File blockCompressed = blockCompressedVCFs.get(i);
            final File index = blockCompressedIndices.get(i);
            if (index.exists()) index.delete();
            writer = builder.setOutputFile(blockCompressed)
                    .setReferenceDictionary(dictionary)
                    .build();
            writer.close();
            Assert.assertTrue(
                    index.exists(),
                    String.format("Block-compressed index not created for %s / %s", blockCompressed, index));

            // Tabix does not require a reference dictionary.
            // Tribble does: see tests testRefDictRequiredForVCFIndexOnTheFly / testRefDictRequiredForBCFIndexOnTheFly

            index.delete();
            writer = builder.setReferenceDictionary(null).build();
            writer.close();
            Assert.assertTrue(
                    index.exists(),
                    String.format("Block-compressed index not created for %s / %s", blockCompressed, index));
        }
    }

    @Test
    public void testIndexingOnTheFlyForPath() throws IOException {
        final VariantContextWriterBuilder builder = new VariantContextWriterBuilder()
                .setReferenceDictionary(dictionary)
                .setOption(Options.INDEX_ON_THE_FLY);

        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
            final Path vcfPath = fs.getPath(vcf.getName());
            final Path vcfIdxPath = Tribble.indexPath(vcfPath);
            try (final VariantContextWriter writer =
                    builder.setOutputPath(vcfPath).build()) {
                // deliberately empty
            }

            Assert.assertTrue(
                    Files.exists(vcfIdxPath), String.format("VCF index not created for %s / %s", vcfPath, vcfIdxPath));

            final Path bcfPath = fs.getPath(bcf.getName());
            final Path bcfIdxPath = Tribble.indexPath(bcfPath);
            try (final VariantContextWriter writer =
                    builder.setOutputPath(bcfPath).build()) {
                // deliberately empty
            }
            Assert.assertTrue(
                    Files.exists(bcfIdxPath), String.format("BCF index not created for %s / %s", bcfPath, bcfIdxPath));

            for (int i = 0; i < blockCompressedVCFs.size(); i++) {
                final File blockCompressed = blockCompressedVCFs.get(i);
                final File index = blockCompressedIndices.get(i);

                final Path blockCompressedPath = fs.getPath(blockCompressed.getName());
                final Path indexPath = fs.getPath(index.getName());

                Assert.assertFalse(Files.exists(indexPath));
                try (VariantContextWriter writer = builder.setOutputPath(blockCompressedPath)
                        .setReferenceDictionary(dictionary)
                        .build()) {
                    // deliberately empty
                }
                Assert.assertTrue(
                        Files.exists(indexPath),
                        String.format(
                                "Block-compressed index not created for %s / %s", blockCompressedPath, indexPath));

                // Tabix does not require a reference dictionary.
                // Tribble does: see tests testRefDictRequiredForVCFIndexOnTheFly /
                // testRefDictRequiredForBCFIndexOnTheFly
                Files.delete(indexPath);
                try (VariantContextWriter writer =
                        builder.setReferenceDictionary(null).build()) {
                    // deliberately empty
                }
                Assert.assertTrue(
                        Files.exists(indexPath),
                        String.format(
                                "Block-compressed index not created for %s / %s", blockCompressedPath, indexPath));
            }
        }
    }

    @Test(singleThreaded = true, groups = "unix")
    public void testWriteToFifo() throws IOException, InterruptedException, ExecutionException {
        long length = testWriteToPipe(this::innerWriteToFifo);
        // length>0 means we wrote to the named pipe, so all is well.
        Assert.assertTrue(length > 0, "VariantContextWriterBuilder did not write to the named pipe as it should.");
    }

    private void innerWriteToFifo(String pathToFifo) {
        // Do not enable INDEX_OF_THE_FLY because that is not compatible with writing to a pipe.
        final VariantContextWriterBuilder builder =
                new VariantContextWriterBuilder().clearOptions().setReferenceDictionary(dictionary);

        Path vcfPath = Paths.get(pathToFifo);
        VariantContextWriter writer = builder.setOutputPath(vcfPath).build();
        writer.writeHeader(new VCFHeader());
        writer.close();
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testInvalidImplicitFileType() {
        new VariantContextWriterBuilder()
                .setReferenceDictionary(dictionary)
                .setOutputFile("test.bam")
                .build();
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testSetInvalidFileType() {
        new VariantContextWriterBuilder()
                .setReferenceDictionary(dictionary)
                .setOutputFile("test.bam")
                .setOutputFileType(VariantContextWriterBuilder.OutputType.VCF_STREAM)
                .build();
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testInvalidSetFileTypeForStream() {
        new VariantContextWriterBuilder()
                .setReferenceDictionary(dictionary)
                .setOutputStream(new ByteArrayOutputStream())
                .setOutputFileType(VariantContextWriterBuilder.OutputType.VCF)
                .build();
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testRefDictRequiredForVCFIndexOnTheFly() {
        new VariantContextWriterBuilder()
                .setOutputFile(vcf)
                .setOption(Options.INDEX_ON_THE_FLY)
                .build();
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testRefDictRequiredForBCFIndexOnTheFly() {
        new VariantContextWriterBuilder()
                .setOutputFile(bcf)
                .setOption(Options.INDEX_ON_THE_FLY)
                .build();
    }

    @Test
    public void testClearOptions() {
        // Verify that clearOptions doesn't have a side effect of carrying previously set options
        // forward to subsequent builders
        VariantContextWriterBuilder vcwb = new VariantContextWriterBuilder();
        vcwb.clearOptions().setOption(Options.INDEX_ON_THE_FLY);
        final VariantContextWriterBuilder builder = new VariantContextWriterBuilder().clearOptions();
        Assert.assertTrue(builder.options.isEmpty());
    }

    @Test
    public void testModifyOption() {
        final VariantContextWriterBuilder builder = new VariantContextWriterBuilder().clearOptions();
        for (final Options option : Options.values()) {
            Assert.assertFalse(builder.isOptionSet(option)); // shouldn't be set
            builder.modifyOption(option, false);
            Assert.assertFalse(builder.isOptionSet(option)); // still shouldn't be set
            builder.modifyOption(option, true);
            Assert.assertTrue(builder.isOptionSet(option)); // now is set
            builder.modifyOption(option, false);
            Assert.assertFalse(builder.isOptionSet(option)); // has been unset
        }
    }

    @Test
    public void testStdOut() {
        final VariantContextWriter writer = new VariantContextWriterBuilder()
                .setOutputFile("/dev/stdout")
                .clearOptions()
                .build();
        OutputStream s = ((VCFWriter) writer).getOutputStream();
        Assert.assertNotNull(((VCFWriter) writer).getOutputStream());
        Assert.assertNotEquals(((VCFWriter) writer).getStreamName(), IndexingVariantContextWriter.DEFAULT_READER_NAME);
    }

    @Test(expectedExceptions = java.util.concurrent.ExecutionException.class)
    public void demonstrateTestWriteToPipeDoesNotHang_1() throws Exception {
        testWriteToPipe((str) -> {
            throw new RuntimeException("oops");
        });
    }

    @Test(expectedExceptions = java.util.concurrent.ExecutionException.class)
    public void demonstrateTestWriteToPipeDoesNotHang_2() throws Exception {
        long x = testWriteToPipe((str) -> {
            try {
                Files.write(Paths.get(str), "hello world".getBytes());
            } catch (IOException ex) {
            }
            ;
            throw new RuntimeException("oops");
        });
    }

    /**
     * Create a named pipe, call "codeThatWritesToAFilename" with the name as argument,
     * consumes the output as it's being run and returns the number of bytes that were output.
     *
     * This only works on Unix. Use this decoration for the tests that use it:
     * @Test(singleThreaded = true, groups = "unix")
     */
    private static long testWriteToPipe(final Consumer<String> codeThatWritesToAFilename)
            throws IOException, InterruptedException, ExecutionException {
        final File fifo = makeFifo();

        // run the code in a separate thread
        ExecutorService executor = Executors.newFixedThreadPool(2);
        try {
            final Future<?> writeResult = executor.submit(() -> {
                try {
                    codeThatWritesToAFilename.accept(fifo.getAbsolutePath());
                } catch (Exception x) {
                    // Print to the console to aid debugging, in case the exception doesn't surface.
                    x.printStackTrace();
                    throw (x);
                }
            });

            // drain the pipe with the output.
            final Future<Integer> readResult = executor.submit(() -> {
                try (final InputStream inputStream = Files.newInputStream(fifo.toPath(), StandardOpenOption.READ)) {
                    int count = 0;
                    while (inputStream.read() >= 0) {
                        count++;
                    }
                    return count;
                }
            });

            // done. If it was in error, the line below will throw the exception.
            writeResult.get();
            return readResult.get();
        } finally {
            executor.shutdownNow();
        }
    }

    private static File makeFifo() throws IOException, InterruptedException {
        // make the fifo
        final File fifo = File.createTempFile("fifo.for.testWriteToPipe", "");
        Assert.assertTrue(fifo.delete());
        fifo.deleteOnExit();
        final Process exec = new ProcessBuilder("mkfifo", fifo.getAbsolutePath()).start();
        exec.waitFor(1, TimeUnit.MINUTES);
        Assert.assertEquals(exec.exitValue(), 0, "mkfifo failed with exit code " + 0);
        return fifo;
    }
}
