/* Copyright (C) 2017 Mike Fleetwood
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

/* Test PipeCapture
 *
 * All the tests work by creating a pipe(3) and using a separate thread to write data into
 * the pipe with PipeCapture running in the initial thread.  Captured data is then checked
 * that it either matches the input or different expected output depending on the features
 * being tested.
 */

#include "common.h"
#include "PipeCapture.h"
#include "gtest/gtest.h"

#include <stddef.h>
#include <stdio.h>
#include <sstream>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <string>
#include <sigc++/sigc++.h>
#include <glib.h>
#include <glibmm.h>

namespace GParted
{

// Repeat a C++ string count times, where count >= 0.
static std::string repeat( const std::string & str, size_t count )
{
	std::string result = "";
	while ( count -- > 0 )
		result += str;
	return result;
}


// Helper to construct and return message for equality assertion of C++ strings containing
// binary data used in:
//     EXPECT_BINARYSTRINGEQ( str1, str2 )
::testing::AssertionResult CompareHelperBinaryStringEQ( const char * lhs_expr, const char * rhs_expr,
                                                        const std::string & lhs, const std::string & rhs )
{
	// Loop comparing binary data in 16 byte amounts, stopping and reporting the first
	// difference encountered.
	bool diff = false;
	const char * p1 = lhs.data();
	const char * p2 = rhs.data();
	size_t len1 = lhs.length();
	size_t len2 = rhs.length();
	while ( len1 > 0 || len2 > 0 )
	{
		size_t cmp_span = BinaryStringChunkSize;
		cmp_span = ( len1 < cmp_span ) ? len1 : cmp_span;
		cmp_span = ( len2 < cmp_span ) ? len2 : cmp_span;
		if (cmp_span < BinaryStringChunkSize && len1 != len2)
		{
			diff = true;
			break;
		}
		if ( memcmp( p1, p2, cmp_span ) != 0 )
		{
			diff = true;
			break;
		}
		p1 += cmp_span;
		p2 += cmp_span;
		len1 -= cmp_span;
		len2 -= cmp_span;
	}

	if ( ! diff )
		return ::testing::AssertionSuccess();
	else
	{
		size_t offset = p1 - lhs.data();
		return ::testing::AssertionFailure()
		       << "      Expected: " << lhs_expr << "\n"
		       << "     Of length: " << lhs.length() << "\n"
		       << "To be equal to: " << rhs_expr << "\n"
		       << "     Of length: " << rhs.length() << "\n"
		       << "With first binary difference:\n"
		       << "< " << binary_string_to_print(offset, p1, len1) << "\n"
		       << "--\n"
		       << "> " << binary_string_to_print(offset, p2, len2);
	}
}

// Nonfatal assertion that binary data in C++ strings are equal.
#define EXPECT_BINARYSTRINGEQ(str1, str2)  \
	EXPECT_PRED_FORMAT2(CompareHelperBinaryStringEQ, str1, str2)

// Explicit test fixture class with common variables and methods used in each test.
// Reference:
//     Google Test, Primer, Test Fixtures: Using the Same Data Configuration for Multiple Tests
class PipeCaptureTest : public ::testing::Test
{
protected:
	PipeCaptureTest() : capturedstr( "text to be replaced" ),
	                    eof_signalled( false ), update_signalled( 0U )  {};

	virtual void SetUp();
	virtual void TearDown();

	static gboolean main_loop_quit( gpointer data );
	void writer_thread( const std::string & str );
	void run_writer_thread();

	static const size_t ReaderFD = 0;
	static const size_t WriterFD = 1;

	std::string inputstr;
	std::string expectedstr;
	Glib::ustring capturedstr;
	bool eof_signalled;
	unsigned update_signalled;
	int pipefds[2];
	Glib::RefPtr<Glib::MainLoop> glib_main_loop;

public:
	void eof_callback()  { eof_signalled = true; };
	void update_callback_leading_match();
};

// Further setup PipeCaptureTest fixture before running each test.  Create pipe and Glib
// main loop object.
void PipeCaptureTest::SetUp()
{
	ASSERT_TRUE( pipe( pipefds ) == 0 ) << "Failed to create pipe.  errno="
	                                    << errno << "," << strerror( errno );
	glib_main_loop = Glib::MainLoop::create();
}

// Tear down fixture after running each test.  Close reading end of the pipe.  Also
// re-closed the writing end of the pipe, just in case something went wrong in the test.
void PipeCaptureTest::TearDown()
{
	ASSERT_TRUE( close( pipefds[ReaderFD] ) == 0 ) << "Failed to close reading end of pipe.  errno="
	                                               << errno << "," << strerror( errno );
	close( pipefds[WriterFD] );
}

// Callback used to end the currently running Glib main loop.
gboolean PipeCaptureTest::main_loop_quit( gpointer data )
{
	static_cast<PipeCaptureTest *>( data )->glib_main_loop->quit();
	return false;  // One shot g_idle_add() callback
}

// Write the string into the pipe and close the pipe for writing.  Registers callback to
// end the currently running Glib main loop.
void PipeCaptureTest::writer_thread( const std::string & str )
{
	const size_t BlockSize = 4096;
	const char * writebuf = str.data();
	size_t remaining_size = str.length();
	while ( remaining_size > 0 )
	{
		size_t write_size = ( remaining_size > BlockSize ) ? BlockSize : remaining_size;
		ssize_t written = write( pipefds[WriterFD], writebuf, write_size );
		if ( written <= 0 )
		{
			ADD_FAILURE() << __func__ << "(): Failed to write to pipe.  errno="
			              << errno << "," << strerror( errno );
			break;
		}
		remaining_size -= written;
		writebuf += written;
	}
	ASSERT_TRUE( close( pipefds[WriterFD] ) == 0 ) << "Failed to close writing end of pipe.  errno="
	                                               << errno << "," << strerror( errno );
	g_idle_add( main_loop_quit, this );
}

// Create writer thread and run the Glib main loop.
void PipeCaptureTest::run_writer_thread()
{
	Glib::Thread::create( sigc::bind( sigc::mem_fun( *this, &PipeCaptureTest::writer_thread ),
	                                  inputstr ),
	                      false );
	glib_main_loop->run();
}

// Callback fired from CapturePipe counting calls and ensuring captured string matches
// leading portion of input string.
void PipeCaptureTest::update_callback_leading_match()
{
	update_signalled ++;
	EXPECT_BINARYSTRINGEQ( inputstr.substr( 0, capturedstr.raw().length() ),
	                       capturedstr.raw() );
	if ( HasFailure() )
		// No point trying to PipeCapture the rest of the input and report
		// hundreds of further failures in the same test, so end the currently
		// running Glib main loop immediately.
		// References:
		// *   Google Test, AdvancedGuide, Propagating Fatal Failures
		// *   Google Test, AdvancedGuide, Checking for Failures in the Current Test
		glib_main_loop->quit();
}

TEST_F( PipeCaptureTest, EmptyPipe )
{
	// Test capturing 0 bytes with no on EOF callback registered.
	inputstr = "";
	PipeCapture pc( pipefds[ReaderFD], capturedstr );
	pc.connect_signal();
	run_writer_thread();
	EXPECT_BINARYSTRINGEQ( inputstr, capturedstr.raw() );
	EXPECT_FALSE( eof_signalled );
}

TEST_F( PipeCaptureTest, EmptyPipeWithEOF )
{
	// Test capturing 0 bytes and registered on EOF callback occurs.
	inputstr = "";
	PipeCapture pc( pipefds[ReaderFD], capturedstr );
	pc.signal_eof.connect( sigc::mem_fun( *this, &PipeCaptureTest::eof_callback ) );
	pc.connect_signal();
	run_writer_thread();
	EXPECT_BINARYSTRINGEQ( inputstr, capturedstr.raw() );
	EXPECT_TRUE( eof_signalled );
}

TEST_F( PipeCaptureTest, ShortASCIIText )
{
	// Test capturing small amount of ASCII text.
	inputstr = "The quick brown fox jumps over the lazy dog";
	PipeCapture pc( pipefds[ReaderFD], capturedstr );
	pc.signal_eof.connect( sigc::mem_fun( *this, &PipeCaptureTest::eof_callback ) );
	pc.connect_signal();
	run_writer_thread();
	EXPECT_BINARYSTRINGEQ( inputstr, capturedstr.raw() );
	EXPECT_TRUE( eof_signalled );
}

TEST_F( PipeCaptureTest, LongASCIIText )
{
	// Test capturing 1 MiB of ASCII text (requiring multiple reads in PipeCapture).
	inputstr = repeat( "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_\n", 16384 );
	PipeCapture pc( pipefds[ReaderFD], capturedstr );
	pc.signal_eof.connect( sigc::mem_fun( *this, &PipeCaptureTest::eof_callback ) );
	pc.connect_signal();
	run_writer_thread();
	EXPECT_BINARYSTRINGEQ( inputstr, capturedstr.raw() );
	EXPECT_TRUE( eof_signalled );
}

TEST_F( PipeCaptureTest, LongASCIITextWithUpdate )
{
	// Test capturing 1 MiB of ASCII text, that registered update callback occurs and
	// intermediate captured string is a leading match for the input string.
	inputstr = repeat( "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_\n", 16384 );
	PipeCapture pc( pipefds[ReaderFD], capturedstr );
	pc.signal_eof.connect( sigc::mem_fun( *this, &PipeCaptureTest::eof_callback ) );
	pc.signal_update.connect( sigc::mem_fun( *this, &PipeCaptureTest::update_callback_leading_match ) );
	pc.connect_signal();
	run_writer_thread();
	EXPECT_BINARYSTRINGEQ( inputstr, capturedstr.raw() );
	EXPECT_GT( update_signalled, 0U );
	EXPECT_TRUE( eof_signalled );
}

TEST_F( PipeCaptureTest, MinimalBinaryCrash777973 )
{
	// Test for bug #777973.  Minimal test case of binary data returned by fsck.fat
	// as file names from a very corrupt FAT, leading to GParted crashing from a
	// segmentation fault.
	inputstr = "/LOST.DIR/!\xE2\x95\x9F\xE2\x88\xA9\xC2\xA0!\xE2\x95\x9F\xE2\x88\xA9\xC2";
	PipeCapture pc( pipefds[ReaderFD], capturedstr );
	pc.signal_eof.connect( sigc::mem_fun( *this, &PipeCaptureTest::eof_callback ) );
	pc.connect_signal();
	run_writer_thread();
	// Final \xC2 byte is part of an incomplete UTF-8 character so will be skipped by
	// PipeCapture.
	expectedstr = "/LOST.DIR/!\xE2\x95\x9F\xE2\x88\xA9\xC2\xA0!\xE2\x95\x9F\xE2\x88\xA9";
	EXPECT_BINARYSTRINGEQ( expectedstr, capturedstr.raw() );
	EXPECT_TRUE( eof_signalled );
}

TEST_F( PipeCaptureTest, ReadEmbeddedNULCharacter )
{
	// Test embedded NUL character in the middle of the input is read correctly.
	const char * buf = "ABC\0EF";
	inputstr = std::string( buf, 6 );
	PipeCapture pc( pipefds[ReaderFD], capturedstr );
	pc.signal_eof.connect( sigc::mem_fun( *this, &PipeCaptureTest::eof_callback ) );
	pc.connect_signal();
	run_writer_thread();
	EXPECT_BINARYSTRINGEQ( inputstr, capturedstr.raw() );
	EXPECT_TRUE( eof_signalled );
}

TEST_F( PipeCaptureTest, ReadNULByteInMiddleOfMultiByteUTF8Character )
{
	// Test NUL byte in the middle of reading a multi-byte UTF-8 character.
	const char * buf = "\xC0\x00_45678";
	inputstr = std::string( buf, 8 );
	PipeCapture pc( pipefds[ReaderFD], capturedstr );
	pc.signal_eof.connect( sigc::mem_fun( *this, &PipeCaptureTest::eof_callback ) );
	pc.connect_signal();
	run_writer_thread();
	// Initial \xC0 byte is part of an incomplete UTF-8 characters so will be skipped
	// by PipeCapture.
	buf = "\x00_45678";
	expectedstr = std::string( buf, 7 );
	EXPECT_BINARYSTRINGEQ( expectedstr, capturedstr.raw() );
	EXPECT_TRUE( eof_signalled );
}

TEST_F( PipeCaptureTest, LineDisciplineCarriageReturn )
{
	// Test PipeCapture line discipline processes carriage return character.
	inputstr = "1111\n2222\r33";
	PipeCapture pc( pipefds[ReaderFD], capturedstr );
	pc.connect_signal();
	run_writer_thread();
	expectedstr = "1111\n3322";
	EXPECT_BINARYSTRINGEQ( expectedstr, capturedstr.raw() );
}

TEST_F( PipeCaptureTest, LineDisciplineCarriageReturn2 )
{
	// Test PipeCapture line discipline processes multiple carriage return characters.
	inputstr = "1111\n2222\r33\r\r4";
	PipeCapture pc( pipefds[ReaderFD], capturedstr );
	pc.connect_signal();
	run_writer_thread();
	expectedstr = "1111\n4322";
	EXPECT_BINARYSTRINGEQ( expectedstr, capturedstr.raw() );
}

TEST_F( PipeCaptureTest, LineDisciplineBackspace )
{
	// Test PipeCapture line discipline processes backspace character.
	inputstr = "1111\n2222\b33";
	PipeCapture pc( pipefds[ReaderFD], capturedstr );
	pc.connect_signal();
	run_writer_thread();
	expectedstr = "1111\n22233";
	EXPECT_BINARYSTRINGEQ( expectedstr, capturedstr.raw() );
}

TEST_F( PipeCaptureTest, LineDisciplineBackspace2 )
{
	// Test PipeCapture line discipline processes too many backspace characters moving
	// the cursor back only to the beginning of the current line.
	inputstr = "1111\n2222\b\b\b\b\b\b33\b4";
	PipeCapture pc( pipefds[ReaderFD], capturedstr );
	pc.connect_signal();
	run_writer_thread();
	expectedstr = "1111\n3422";
	EXPECT_BINARYSTRINGEQ( expectedstr, capturedstr.raw() );
}

TEST_F( PipeCaptureTest, LineDisciplineSkipCtrlAB )
{
	// Test PipeCapture line discipline skips Ctrl-A and Ctrl-B.
	inputstr = "ij\x01kl\x02mn";
	PipeCapture pc( pipefds[ReaderFD], capturedstr );
	pc.connect_signal();
	run_writer_thread();
	expectedstr = "ijklmn";
	EXPECT_BINARYSTRINGEQ( expectedstr, capturedstr.raw() );
}

}  // namespace GParted
