/*
 * PROJECT:     ReactOS Automatic Testing Utility
 * LICENSE:     GPL-2.0+ (https://spdx.org/licenses/GPL-2.0+)
 * PURPOSE:     Class implementing functions for handling Wine tests
 * COPYRIGHT:   Copyright 2009-2019 Colin Finck (colin@reactos.org)
 */

#include "precomp.h"

static const DWORD ListTimeout = 10000;

// This value needs to be lower than the <timeout> configured in sysreg.xml! (usually 180000)
// Otherwise, sysreg2 kills the VM before we can kill the process.
static const DWORD ProcessActivityTimeout = 170000;


/**
 * Constructs a CWineTest object.
 */
CWineTest::CWineTest()
    : m_hFind(NULL), m_ListBuffer(NULL)
{
    WCHAR wszDirectory[MAX_PATH];

    /* Set up m_TestPath */
    if (GetEnvironmentVariableW(L"ROSAUTOTEST_DIR", wszDirectory, MAX_PATH))
    {
        m_TestPath = wszDirectory;
        if (*m_TestPath.rbegin() != L'\\')
            m_TestPath += L'\\';
    }
    else
    {
        if (!GetWindowsDirectoryW(wszDirectory, MAX_PATH))
            FATAL("GetWindowsDirectoryW failed\n");

        m_TestPath = wszDirectory;
        m_TestPath += L"\\bin\\";
    }
}

/**
 * Destructs a CWineTest object.
 */
CWineTest::~CWineTest()
{
    if(m_hFind)
        FindClose(m_hFind);

    if(m_ListBuffer)
        delete m_ListBuffer;
}

/**
 * Gets the next module test file using the FindFirstFileW/FindNextFileW API.
 *
 * @return
 * true if we found a next file, otherwise false.
 */
bool
CWineTest::GetNextFile()
{
    bool FoundFile = false;
    WIN32_FIND_DATAW fd;

    /* Did we already begin searching for files? */
    if(m_hFind)
    {
        /* Then get the next file (if any) */
        if(FindNextFileW(m_hFind, &fd))
            FoundFile = true;
    }
    else
    {
        /* Start searching for test files */
        wstring FindPath = m_TestPath;

        /* Did the user specify a module? */
        if(Configuration.GetModule().empty())
        {
            /* No module, so search for all files in that directory */
            FindPath += L"*.exe";
        }
        else
        {
            /* Search for files with the pattern "modulename_*" */
            FindPath += Configuration.GetModule();
            FindPath += L"_*.exe";
        }

        /* Search for the first file and check whether we got one */
        m_hFind = FindFirstFileW(FindPath.c_str(), &fd);

        if(m_hFind != INVALID_HANDLE_VALUE)
            FoundFile = true;
    }

    if(FoundFile)
        m_CurrentFile = fd.cFileName;

    return FoundFile;
}

/**
 * Executes the --list command of a module test file to get information about the available tests.
 *
 * @return
 * The number of bytes we read into the m_ListBuffer member variable by capturing the output of the --list command.
 */
DWORD
CWineTest::DoListCommand()
{
    DWORD BytesAvailable;
    DWORD Temp;
    wstring CommandLine;
    CPipe Pipe;

    /* Build the command line */
    CommandLine = m_TestPath;
    CommandLine += m_CurrentFile;
    CommandLine += L" --list";

    {
        /* Start the process for getting all available tests */
        CPipedProcess Process(CommandLine, Pipe);

        /* Wait till this process ended */
        if(WaitForSingleObject(Process.GetProcessHandle(), ListTimeout) == WAIT_FAILED)
            TESTEXCEPTION("WaitForSingleObject failed for the test list\n");
    }

    /* Read the output data into a buffer */
    if(!Pipe.Peek(NULL, 0, NULL, &BytesAvailable))
        TESTEXCEPTION("CPipe::Peek failed for the test list\n");

    /* Check if we got any */
    if(!BytesAvailable)
    {
        stringstream ss;

        ss << "The --list command did not return any data for " << UnicodeToAscii(m_CurrentFile) << endl;
        TESTEXCEPTION(ss.str());
    }

    /* Read the data */
    m_ListBuffer = new char[BytesAvailable];

    if(Pipe.Read(m_ListBuffer, BytesAvailable, &Temp, INFINITE) != ERROR_SUCCESS)
        TESTEXCEPTION("CPipe::Read failed\n");

    return BytesAvailable;
}

/**
 * Gets the next test from m_ListBuffer, which was filled with information from the --list command.
 *
 * @return
 * true if a next test was found, otherwise false.
 */
bool
CWineTest::GetNextTest()
{
    PCHAR pEnd;
    static DWORD BufferSize;
    static PCHAR pStart;

    if(!m_ListBuffer)
    {
        /* Perform the --list command */
        BufferSize = DoListCommand();

        /* Move the pointer to the first test */
        pStart = strchr(m_ListBuffer, '\n');
        pStart += 5;
    }

    /* If we reach the buffer size, we finished analyzing the output of this test */
    if(pStart >= (m_ListBuffer + BufferSize))
    {
        /* Clear m_CurrentFile to indicate that */
        m_CurrentFile.clear();

        /* Also free the memory for the list buffer */
        delete[] m_ListBuffer;
        m_ListBuffer = NULL;

        return false;
    }

    /* Get start and end of this test name */
    pEnd = pStart;

    while(*pEnd != '\r')
        ++pEnd;

    /* Store the test name */
    m_CurrentTest = string(pStart, pEnd);

    /* Move the pointer to the next test */
    pStart = pEnd + 6;

    return true;
}

/**
 * Interface to CTestList-derived classes for getting all information about the next test to be run.
 *
 * @return
 * Returns a pointer to a CTestInfo object containing all available information about the next test.
 */
CTestInfo*
CWineTest::GetNextTestInfo()
{
    while(!m_CurrentFile.empty() || GetNextFile())
    {
        try
        {
            while(GetNextTest())
            {
                /* If the user specified a test through the command line, check this here */
                if(!Configuration.GetTest().empty() && Configuration.GetTest() != m_CurrentTest)
                    continue;

                {
                    auto_ptr<CTestInfo> TestInfo(new CTestInfo());
                    size_t UnderscorePosition;

                    /* Build the command line */
                    TestInfo->CommandLine = m_TestPath;
                    TestInfo->CommandLine += m_CurrentFile;
                    TestInfo->CommandLine += ' ';
                    TestInfo->CommandLine += AsciiToUnicode(m_CurrentTest);

                    /* Store the Module name */
                    UnderscorePosition = m_CurrentFile.find_last_of('_');

                    if(UnderscorePosition == m_CurrentFile.npos)
                    {
                        stringstream ss;

                        ss << "Invalid test file name: " << UnicodeToAscii(m_CurrentFile) << endl;
                        SSEXCEPTION;
                    }

                    TestInfo->Module = UnicodeToAscii(m_CurrentFile.substr(0, UnderscorePosition));

                    /* Store the test */
                    TestInfo->Test = m_CurrentTest;

                    return TestInfo.release();
                }
            }
        }
        catch(CTestException& e)
        {
            stringstream ss;

            ss << "An exception occurred trying to list tests for: " << UnicodeToAscii(m_CurrentFile) << endl;
            StringOut(ss.str());
            StringOut(e.GetMessage());
            StringOut("\n");
            m_CurrentFile.clear();
            delete[] m_ListBuffer;
        }
    }

    return NULL;
}

/**
 * Runs a Wine test and captures the output
 *
 * @param TestInfo
 * Pointer to a CTestInfo object containing information about the test.
 * Will contain the test log afterwards if the user wants to submit data.
 */
void
CWineTest::RunTest(CTestInfo* TestInfo)
{
    DWORD BytesAvailable;
    stringstream ss, ssFinish;
    DWORD StartTime;
    float TotalTime;
    string tailString;
    CPipe Pipe;
    char Buffer[1024];

    ss << "Running Wine Test, Module: " << TestInfo->Module << ", Test: " << TestInfo->Test << endl;
    StringOut(ss.str());

    StartTime = GetTickCount();

    try
    {
        /* Execute the test */
        CPipedProcess Process(TestInfo->CommandLine, Pipe);

        /* Receive all the data from the pipe */
        for (;;)
        {
            DWORD dwReadResult = Pipe.Read(Buffer, sizeof(Buffer) - 1, &BytesAvailable, ProcessActivityTimeout);
            if (dwReadResult == ERROR_SUCCESS)
            {
                /* Output text through StringOut, even while the test is still running */
                Buffer[BytesAvailable] = 0;
                tailString = StringOut(tailString.append(string(Buffer)), false);

                if (Configuration.DoSubmit())
                    TestInfo->Log += Buffer;
            }
            else if (dwReadResult == ERROR_BROKEN_PIPE)
            {
                // The process finished and has been terminated.
                break;
            }
            else if (dwReadResult == WAIT_TIMEOUT)
            {
                // The process activity timeout above has elapsed without any new data.
                TESTEXCEPTION("Timeout while waiting for the test process\n");
            }
            else
            {
                // An unexpected error.
                TESTEXCEPTION("CPipe::Read failed for the test run\n");
            }
        }
    }
    catch(CTestException& e)
    {
        if(!tailString.empty())
            StringOut(tailString);
        tailString.clear();
        StringOut(e.GetMessage());
        TestInfo->Log += e.GetMessage();
    }

    /* Print what's left */
    if(!tailString.empty())
        StringOut(tailString);

    TotalTime = ((float)GetTickCount() - StartTime)/1000;
    ssFinish << "Test " << TestInfo->Test << " completed in ";
    ssFinish << setprecision(2) << fixed << TotalTime << " seconds." << endl;
    StringOut(ssFinish.str());
    TestInfo->Log += ssFinish.str();
}

/**
 * Interface to other classes for running all desired Wine tests.
 */
void
CWineTest::Run()
{
    auto_ptr<CTestList> TestList;
    auto_ptr<CWebService> WebService;
    CTestInfo* TestInfo;
    DWORD ErrorMode;

    /* The virtual test list is of course faster, so it should be preferred over
       the journaled one.
       Enable the journaled one only in case ...
          - we're running under ReactOS (as the journal is only useful in conjunction with sysreg2)
          - we shall keep information for Crash Recovery
          - and the user didn't specify a module (then doing Crash Recovery doesn't really make sense) */
    if(Configuration.IsReactOS() && Configuration.DoCrashRecovery() && Configuration.GetModule().empty())
    {
        /* Use a test list with a permanent journal */
        TestList.reset(new CJournaledTestList(this));
    }
    else
    {
        /* Use the fast virtual test list with no additional overhead */
        TestList.reset(new CVirtualTestList(this));
    }

    /* Initialize the Web Service interface if required */
    if(Configuration.DoSubmit())
        WebService.reset(new CWebService());

    /* Disable error dialogs if we're running in non-interactive mode */
    if(!Configuration.IsInteractive())
        ErrorMode = SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);

    /* Get information for each test to run */
    while((TestInfo = TestList->GetNextTestInfo()) != 0)
    {
        auto_ptr<CTestInfo> TestInfoPtr(TestInfo);

        RunTest(TestInfo);

        if(Configuration.DoSubmit() && !TestInfo->Log.empty())
            WebService->Submit("wine", TestInfo);

        StringOut("\n\n");
    }

    /* We're done with all tests. Finish this run */
    if(Configuration.DoSubmit())
        WebService->Finish("wine");

    /* Restore the original error mode */
    if(!Configuration.IsInteractive())
        SetErrorMode(ErrorMode);
}