// TestObject.cpp
//------------------------------------------------------------------------------

// Includes
//------------------------------------------------------------------------------
#include "FBuildTest.h"

// FBuildCore
#include "Tools/FBuild/FBuildCore/BFF/BFFParser.h"
#include "Tools/FBuild/FBuildCore/FBuild.h"
#include "Tools/FBuild/FBuildCore/Graph/NodeGraph.h"
#include "Tools/FBuild/FBuildCore/Graph/ObjectNode.h"
#include "Tools/FBuild/FBuildCore/Protocol/Protocol.h"
#include "Tools/FBuild/FBuildCore/Protocol/Server.h"

// Core
#include "Core/FileIO/FileIO.h"
#include "Core/FileIO/FileStream.h"
#include "Core/FileIO/PathUtils.h"
#include "Core/Process/Thread.h"
#include "Core/Strings/AStackString.h"

//------------------------------------------------------------------------------
TEST_GROUP( TestObject, FBuildTest )
{
public:
};

//------------------------------------------------------------------------------
TEST_CASE( TestObject, MSVCArgHelpers )
{
    // Exact match args, using /
    {
        AStackString token( "/Zi" );
        TEST_ASSERT( ObjectNode::IsCompilerArg_MSVC( token, "Zi" ) );
    }

    // Exact match args, using -
    {
        AStackString token( "-Zi" );
        TEST_ASSERT( ObjectNode::IsCompilerArg_MSVC( token, "Zi" ) );
    }

    // Starts with args, using /
    {
        AStackString token( "/Ipath/path" );
        TEST_ASSERT( ObjectNode::IsStartOfCompilerArg_MSVC( token, "I" ) );
    }

    // Starts with args, using -
    {
        AStackString token( "-Ipath/path" );
        TEST_ASSERT( ObjectNode::IsStartOfCompilerArg_MSVC( token, "I" ) );
    }
}

//------------------------------------------------------------------------------
TEST_CASE( TestObject, Preprocessor )
{
    const char * configFile = "Tools/FBuild/FBuildTest/Data/TestObject/CustomPreprocessor/custompreprocessor.bff";
    const char * database = "../tmp/Test/Object/CustomPreprocessor/fbuild.fdb";

    // Build
    {
        // Init
        FBuildTestOptions options;
        options.m_ConfigFile = configFile;
        options.m_ForceCleanBuild = true;
        FBuildForTest fBuild( options );
        TEST_ASSERT( fBuild.Initialize() );

        // Compile
        TEST_ASSERT( fBuild.Build( "CustomPreprocessor" ) );
        fBuild.SaveDependencyGraph( database );

        // Check stats: Seen, Built, Type
        CheckStatsNode( 1, 1, Node::COMPILER_NODE );
        CheckStatsNode( 1, 1, Node::OBJECT_NODE ); // 1x cpp
    }

    // No Rebuild
    {
        // Init
        FBuildTestOptions options;
        options.m_ConfigFile = configFile;
        FBuildForTest fBuild( options );
        TEST_ASSERT( fBuild.Initialize( database ) );

        // Compile
        TEST_ASSERT( fBuild.Build( "CustomPreprocessor" ) );

        // Check stats: Seen, Built, Type
        CheckStatsNode( 1, 0, Node::COMPILER_NODE );
        CheckStatsNode( 1, 0, Node::OBJECT_NODE ); // 1x cpp
    }
}

//------------------------------------------------------------------------------
TEST_CASE( TestObject, TestStaleDynamicDeps )
{
    const char * fileA = "../tmp/Test/Object/StaleDynamicDeps/GeneratedInput/FileA.h";
    const char * fileB = "../tmp/Test/Object/StaleDynamicDeps/GeneratedInput/FileB.h";
    const char * fileC = "../tmp/Test/Object/StaleDynamicDeps/GeneratedInput/FileC.h";
    const char * database = "../tmp/Test/Object/StaleDynamicDeps/fbuild.fdb";

    // Build CPP Generator
    {
        // Init
        FBuildTestOptions options;
        options.m_ConfigFile = "Tools/FBuild/FBuildTest/Data/TestObject/StaleDynamicDeps/cppgenerator.bff";
        options.m_ForceCleanBuild = true;
        FBuildForTest fBuild( options );
        TEST_ASSERT( fBuild.Initialize() );

        // Generate some header files
        EnsureDirExists( "../tmp/Test/Object/StaleDynamicDeps/GeneratedInput/" );
        FileStream f;
        TEST_ASSERT( f.Open( fileA, FileStream::WRITE_ONLY ) );
        f.Close();
        TEST_ASSERT( f.Open( fileB, FileStream::WRITE_ONLY ) );
        f.Close();
        TEST_ASSERT( f.Open( fileC, FileStream::WRITE_ONLY ) );
        f.Close();

        // Compile
        TEST_ASSERT( fBuild.Build( "CPPGenerator" ) );
    }

    // Build using CPP Generator (clean)
    {
        // Init
        FBuildTestOptions options;
        options.m_ConfigFile = "Tools/FBuild/FBuildTest/Data/TestObject/StaleDynamicDeps/staledeps.bff";
        options.m_ForceCleanBuild = true;
        FBuildForTest fBuild( options );
        TEST_ASSERT( fBuild.Initialize() );

        // Compile
        TEST_ASSERT( fBuild.Build( "StaleDynamicDeps" ) );

        // Save DB
        TEST_ASSERT( fBuild.SaveDependencyGraph( database ) );

        // Check stats: Seen, Built, Type
        CheckStatsNode( 1, 1, Node::DIRECTORY_LIST_NODE );
        CheckStatsNode( 2, 2, Node::COMPILER_NODE );
        CheckStatsNode( 4, 4, Node::OBJECT_NODE ); // 3xCPPGen + 1xUnity
    }

    // Delete one of the generated headers
    EnsureFileDoesNotExist( fileB );

    // TODO:B Get rid of this (needed to work around poor filetime granularity)
#if defined( __OSX__ )
    Thread::Sleep( 1000 ); // Work around low time resolution of HFS+
#endif

    // Build Again
    {
        // Init
        FBuildTestOptions options;
        options.m_ConfigFile = "Tools/FBuild/FBuildTest/Data/TestObject/StaleDynamicDeps/staledeps.bff";
        FBuildForTest fBuild( options );
        TEST_ASSERT( fBuild.Initialize( database ) );

        // Compile
        TEST_ASSERT( fBuild.Build( "StaleDynamicDeps" ) );

        // Check stats: Seen, Built, Type
        CheckStatsNode( 1, 1, Node::DIRECTORY_LIST_NODE );
        CheckStatsNode( 2, 0, Node::COMPILER_NODE );
        CheckStatsNode( 3, 1, Node::OBJECT_NODE ); // 3xCPPGen + 1xUnity, rebuild of unity
    }
}

//------------------------------------------------------------------------------
TEST_CASE( TestObject, ModTimeChangeBackwards )
{
    //  - Ensure a file rebuilds if the time changes into the past
    const AStackString fileA( "../tmp/Test/Object/ModTimeChangeBackwards/GeneratedInput/FileA.cpp" );
    const AStackString fileB( "../tmp/Test/Object/ModTimeChangeBackwards/GeneratedInput/FileB.cpp" );
    const char * database = "../tmp/Test/Object/ModTimeChangeBackwards/fbuild.fdb";

    // Generate full path file fileA
    AStackString fileAFullPath;
    {
        FileIO::GetCurrentDir( fileAFullPath );
        fileAFullPath += '/';
        fileAFullPath += fileA;
        PathUtils::FixupFilePath( fileAFullPath );
    }

    // Create two empty files
    uint64_t oldModTime;
    {
        // Generate some header files
        EnsureDirExists( "../tmp/Test/Object/ModTimeChangeBackwards/GeneratedInput/" );
        FileStream f;
        TEST_ASSERT( f.Open( fileA.Get(), FileStream::WRITE_ONLY ) );
        f.Close();
        TEST_ASSERT( f.Open( fileB.Get(), FileStream::WRITE_ONLY ) );
        f.Close();

        // Take note of FileA's original time
        oldModTime = FileIO::GetFileLastWriteTime( fileAFullPath );

        // Modify FileA time (jump through hoops to handle poor filetime granularity)
        const Timer timeout;
        for ( ;; )
        {
            TEST_ASSERT( timeout.GetElapsed() < 30.0f );

            Thread::Sleep( 10 );

            TEST_ASSERT( FileIO::SetFileLastWriteTimeToNow( fileAFullPath ) );
            if ( FileIO::GetFileLastWriteTime( fileAFullPath ) != oldModTime )
            {
                break;
            }
        }
    }

    // Compile library for the two files
    {
        // Init
        FBuildTestOptions options;
        options.m_ConfigFile = "Tools/FBuild/FBuildTest/Data/TestObject/ModTimeChangeBackwards/fbuild.bff";
        options.m_ForceCleanBuild = true;
        FBuildForTest fBuild( options );
        TEST_ASSERT( fBuild.Initialize() );

        // Compile
        TEST_ASSERT( fBuild.Build( "ModTimeChangeBackwards" ) );

        // Save DB
        TEST_ASSERT( fBuild.SaveDependencyGraph( database ) );

        // Check stats: Seen, Built, Type
        CheckStatsNode( 1, 1, Node::DIRECTORY_LIST_NODE );
        CheckStatsNode( 1, 1, Node::COMPILER_NODE );
        CheckStatsNode( 2, 2, Node::OBJECT_NODE );
        CheckStatsNode( 1, 1, Node::LIBRARY_NODE );
    }

    // Change modtime into the past
    TEST_ASSERT( FileIO::SetFileLastWriteTime( fileAFullPath, oldModTime ) );

    // Because of poor filetime granularity on OSX (pre-APFS) the object A we will rebuild
    // below could have an unchanged modtime. To work around this, we must wait until enough
    // time has elapsed to ensure the modtime will be different.
    // TODO:B Find a better solve for this. While unlikely, this could happen in real-world use
#if defined( __OSX__ )
    Thread::Sleep( 1000 ); // Work around low time resolution of HFS+
#endif

    // Compile library again
    {
        // Init
        FBuildTestOptions options;
        options.m_ConfigFile = "Tools/FBuild/FBuildTest/Data/BuildAndLinkLibrary/DeleteFile/fbuild.bff";
        FBuildForTest fBuild( options );
        TEST_ASSERT( fBuild.Initialize( database ) );

        // Compile
        TEST_ASSERT( fBuild.Build( "ModTimeChangeBackwards" ) );

        // Save DB
        TEST_ASSERT( fBuild.SaveDependencyGraph( database ) );

        // Check stats: Seen, Built, Type
        CheckStatsNode( 1, 1, Node::DIRECTORY_LIST_NODE );
        CheckStatsNode( 1, 0, Node::COMPILER_NODE );
        CheckStatsNode( 2, 1, Node::OBJECT_NODE );    // Note: One object rebuilds
        CheckStatsNode( 1, 1, Node::LIBRARY_NODE );   // Note: library rebuilds
    }

    // Ensure no rebuild
    {
        // Init
        FBuildTestOptions options;
        options.m_ConfigFile = "Tools/FBuild/FBuildTest/Data/BuildAndLinkLibrary/DeleteFile/fbuild.bff";
        FBuildForTest fBuild( options );
        TEST_ASSERT( fBuild.Initialize( database ) );

        // Compile
        TEST_ASSERT( fBuild.Build( "ModTimeChangeBackwards" ) );

        // Check stats: Seen, Built, Type
        CheckStatsNode( 1, 1, Node::DIRECTORY_LIST_NODE );
        CheckStatsNode( 1, 0, Node::COMPILER_NODE );
        CheckStatsNode( 2, 0, Node::OBJECT_NODE );
        CheckStatsNode( 1, 0, Node::LIBRARY_NODE );
    }
}

//------------------------------------------------------------------------------
TEST_CASE( TestObject, CacheUsingRelativePaths )
{
    // Source files
    const char * srcPath = "Tools/FBuild/FBuildTest/Data/TestObject/CacheUsingRelativePaths/";
    const char * fileA = "File.cpp";
    const char * fileB = "Subdir/Header.h";
    const char * fileC = "fbuild.bff";
    const char * fileD = "fbuild-lightcache.bff";
    const char * files[] = { fileA, fileB, fileC, fileD };

    // Dest paths
    const char * dstPathA = "../tmp/Test/Object/CacheUsingRelativePaths/A/Code";
    const char * dstPathB = "../tmp/Test/Object/CacheUsingRelativePaths/B/Code";
    const char * dstPaths[] = { dstPathA, dstPathB };

#if defined( __WINDOWS__ )
    const char * objFileA = "../tmp/Test/Object/CacheUsingRelativePaths/A/out/File.obj";
#else
    const char * objFileA = "../tmp/Test/Object/CacheUsingRelativePaths/A/out/File.o";
#endif

    // Run twice, once for regular cache and once for LightCache
    for ( uint32_t i = 0; i < 2; ++i )
    {
        const bool useLightCache = ( i == 1 );
        const char * const bffFile = useLightCache ? "fbuild-lightcache.bff"
                                                   : "fbuild.bff";

        // Copy file structure to both destinations
        for ( const char * dstPath : dstPaths )
        {
            for ( const char * file : files )
            {
                AStackString src;
                AStackString dst;
                src.Format( "%s/%s", srcPath, file );
                dst.Format( "%s/%s", dstPath, file );
                TEST_ASSERT( FileIO::EnsurePathExistsForFile( dst ) );
                TEST_ASSERT( FileIO::FileCopy( src.Get(), dst.Get() ) );
            }
        }

        // Build in path A, writing to the cache
        {
            // Init
            FBuildTestOptions options;
            options.m_ConfigFile = bffFile;
            options.m_UseCacheWrite = true;
            AStackString codeDir;
            GetCodeDir( codeDir );
            codeDir.Trim( 0, 5 ); // Remove Code/
            codeDir += "tmp/Test/Object/CacheUsingRelativePaths/A/Code/";
            options.SetWorkingDir( codeDir );
            FBuildForTest fBuild( options );
            TEST_ASSERT( fBuild.Initialize() );

            // Compile
            TEST_ASSERT( fBuild.Build( AStackString( "ObjectList" ) ) );

            TEST_ASSERT( fBuild.GetStats().GetCacheStores() == 1 );
        }

        // Check some problematic cases in the object file
        {
            // Read obj file into memory
            AString buffer;
            {
                FileStream f;
                TEST_ASSERT( f.Open( objFileA ) );
                buffer.SetLength( (uint32_t)f.GetFileSize() );
                TEST_ASSERT( f.ReadBuffer( buffer.Get(), f.GetFileSize() ) == f.GetFileSize() );
                buffer.Replace( (char)0, ' ' ); // Make string searches simpler
            }

            // Check __FILE__ paths are relative
            // Slash direction changed in Clang 18.x.x from forward slash to backslash
            TEST_ASSERT( buffer.Find( "FILE_MACRO_START_1(./Subdir/Header.h)FILE_MACRO_END_1" ) ||
                         buffer.Find( "FILE_MACRO_START_1(.\\Subdir/Header.h)FILE_MACRO_END_1" ) );
            TEST_ASSERT( buffer.Find( "FILE_MACRO_START_2(File.cpp)FILE_MACRO_END_2" ) );
        }

        // Build in path B, reading from the cache
        {
            // Init
            FBuildTestOptions options;
            options.m_ConfigFile = bffFile;
            options.m_UseCacheRead = true;
            AStackString codeDir;
            GetCodeDir( codeDir );
            codeDir.Trim( 0, 5 ); // Remove Code/
            codeDir += "tmp/Test/Object/CacheUsingRelativePaths/B/Code/";
            options.SetWorkingDir( codeDir );
            FBuildForTest fBuild( options );
            TEST_ASSERT( fBuild.Initialize() );

            // Compile
            TEST_ASSERT( fBuild.Build( AStackString( "ObjectList" ) ) );

            TEST_ASSERT( fBuild.GetStats().GetCacheHits() == 1 );
        }
    }
}

//------------------------------------------------------------------------------
TEST_CASE( TestObject, SourceMapping )
{
    // Source files
    const char * srcPath = "Tools/FBuild/FBuildTest/Data/TestObject/SourceMapping/";
    const char * fileA = "File.cpp";
    const char * fileB = "fbuild.bff";
    const char * files[] = { fileA, fileB };

    // Dest paths
    const char * dstPath = "../tmp/Test/Object/SourceMapping/Code";

#if defined( __WINDOWS__ )
    const char * objFile = "../tmp/Test/Object/SourceMapping/out/File.obj";
#else
    const char * objFile = "../tmp/Test/Object/SourceMapping/out/File.o";
#endif

    // Copy file structure to destination
    for ( const char * file : files )
    {
        AStackString src;
        AStackString dst;
        src.Format( "%s/%s", srcPath, file );
        dst.Format( "%s/%s", dstPath, file );
        TEST_ASSERT( FileIO::EnsurePathExistsForFile( dst ) );
        TEST_ASSERT( FileIO::FileCopy( src.Get(), dst.Get() ) );
    }

    // Build in destination path
    {
        // Init
        FBuildTestOptions options;
        options.m_ConfigFile = "fbuild.bff";
        AStackString codeDir;
        GetCodeDir( codeDir );
        codeDir.Trim( 0, 5 ); // Remove Code/
        codeDir += "tmp/Test/Object/SourceMapping/Code/";
        options.SetWorkingDir( codeDir );
        FBuildForTest fBuild( options );
        TEST_ASSERT( fBuild.Initialize() );

        // Compile
        TEST_ASSERT( fBuild.Build( AStackString( "ObjectList" ) ) );
    }

    // Check the object file to make sure the debugging information has been remapped
    {
        // Read obj file into memory
        AString buffer;
        {
            FileStream f;
            TEST_ASSERT( f.Open( objFile ) );
            buffer.SetLength( (uint32_t)f.GetFileSize() );
            TEST_ASSERT( f.ReadBuffer( buffer.Get(), f.GetFileSize() ) == f.GetFileSize() );
            buffer.Replace( (char)0, ' ' ); // Make string searches simpler
        }

        TEST_ASSERT( buffer.Find( "/fastbuild-test-mapping" ) );
    }
}

//------------------------------------------------------------------------------
TEST_CASE( TestObject, ClangExplicitLanguageType )
{
    // Ensure explicitly set language args ("-x c++" etc) are replaced with the
    // correct equivalent for preprocessed code ("-x c++-cpp-output" etc)
    const char * const configFile = "Tools/FBuild/FBuildTest/Data/TestObject/ClangExplicitLanguageType/fbuild.bff";

    // Local
    {
        // Init
        FBuildTestOptions options;
        options.m_ConfigFile = configFile;
        FBuildForTest fBuild( options );
        TEST_ASSERT( fBuild.Initialize() );

        // Compile
        TEST_ASSERT( fBuild.Build( "ClangExplicitLanguageType" ) );
    }

    // Distributed
    {
        // Init
        FBuildTestOptions options;
        options.m_ConfigFile = configFile;

        // Force remote
        options.m_AllowDistributed = true;
        options.m_NoLocalConsumptionOfRemoteJobs = true;
        options.m_AllowLocalRace = false;

        FBuildForTest fBuild( options );
        TEST_ASSERT( fBuild.Initialize() );

        // start a client to emulate the other end
        Server s( 1 );
        s.Listen( Protocol::kTestPort );

        // Compile
        TEST_ASSERT( fBuild.Build( "ClangExplicitLanguageType" ) );
    }
}

//------------------------------------------------------------------------------
TEST_CASE( TestObject, ClangDependencyArgs )
{
    // Ensure explicitly dependency options are removed from the second pass of
    // compilation. Some integrations (like Unreal) use these commands and process
    // the output.
    const char * const configFile = "Tools/FBuild/FBuildTest/Data/TestObject/ClangDependencyArgs/fbuild.bff";

    // Local
    {
        // Init
        FBuildTestOptions options;
        options.m_ConfigFile = configFile;
        FBuildForTest fBuild( options );
        TEST_ASSERT( fBuild.Initialize() );

        // Compile
        TEST_ASSERT( fBuild.Build( "ClangDependencyArgs" ) );
    }

    // Distributed
    {
        // Init
        FBuildTestOptions options;
        options.m_ConfigFile = configFile;

        // Force remote
        options.m_AllowDistributed = true;
        options.m_NoLocalConsumptionOfRemoteJobs = true;
        options.m_AllowLocalRace = false;

        FBuildForTest fBuild( options );
        TEST_ASSERT( fBuild.Initialize() );

        // start a client to emulate the other end
        Server s( 1 );
        s.Listen( Protocol::kTestPort );

        // Compile
        TEST_ASSERT( fBuild.Build( "ClangDependencyArgs" ) );
    }
}

//------------------------------------------------------------------------------
#if defined( __WINDOWS__ ) && defined( _MSC_VER ) && ( _MSC_VER >= 1920 )
TEST_CASE( TestObject, CLDependencyArgs )
{
    // Ensure explicit dependency options are removed from the second pass of
    // compilation. Some integrations (like Unreal) use these commands and process
    // the output.
    const char * const configFile = "Tools/FBuild/FBuildTest/Data/TestObject/CLDependencyArgs/fbuild.bff";
    const char * const sourceDependenciesFile = "../tmp/Test/Object/CLDependencyArgs/SubDir/file.cpp.json";
    const char * const sourceDependenciesPath = "../tmp/Test/Object/CLDependencyArgs/SubDir/";

    // Cleanup from old test runs
    EnsureFileDoesNotExist( sourceDependenciesFile );
    EnsureDirDoesNotExist( sourceDependenciesPath );

    // Local
    {
        // Init
        FBuildTestOptions options;
        options.m_ConfigFile = configFile;
        FBuildForTest fBuild( options );
        TEST_ASSERT( fBuild.Initialize() );

        // Compile
        TEST_ASSERT( fBuild.Build( "CLDependencyArgs" ) );

        // Make sure the deps file exists and is correct (including creation of subdir)
        AString depsFileContents;
        LoadFileContentsAsString( sourceDependenciesFile, depsFileContents );
        TEST_ASSERT( depsFileContents.FindI( "cldependencyargs\\\\file.cpp" ) );
    }

    // Cleanup between tests
    EnsureFileDoesNotExist( sourceDependenciesFile );
    EnsureDirDoesNotExist( sourceDependenciesPath );

    // Distributed
    {
        // Init
        FBuildTestOptions options;
        options.m_ConfigFile = configFile;

        // Force remote
        options.m_AllowDistributed = true;
        options.m_NoLocalConsumptionOfRemoteJobs = true;
        options.m_AllowLocalRace = false;

        FBuildForTest fBuild( options );
        TEST_ASSERT( fBuild.Initialize() );

        // start a client to emulate the other end
        Server s( 1 );
        s.Listen( Protocol::kTestPort );

        // Compile
        TEST_ASSERT( fBuild.Build( "CLDependencyArgs" ) );

        // Make sure the deps file exists and is correct
        //  - has reference to original .cpp file
        //  - is ignored remotely (arg is stripped by CL driver from second pass)
        AString depsFileContents;
        LoadFileContentsAsString( sourceDependenciesFile, depsFileContents );
        TEST_ASSERT( depsFileContents.FindI( "cldependencyargs\\\\file.cpp" ) );
    }

    // Cleanup between tests
    EnsureFileDoesNotExist( sourceDependenciesFile );
    EnsureDirDoesNotExist( sourceDependenciesPath );

    // Distributed - Race
    {
        // Init
        FBuildTestOptions options;
        options.m_ConfigFile = configFile;

        // Force remote
        options.m_AllowDistributed = true;
        options.m_NoLocalConsumptionOfRemoteJobs = false;
        options.m_AllowLocalRace = true;

        FBuildForTest fBuild( options );
        TEST_ASSERT( fBuild.Initialize() );

        // Compile
        TEST_ASSERT( fBuild.Build( "CLDependencyArgs" ) );

        // Make sure the deps file exists and is correct
        //  - has reference to original .cpp file
        //  - is not overwritten by second pass operating on preprocessed output
        //    (i.e. arg is stripped correctly by CL driver from second pass)
        AString depsFileContents;
        LoadFileContentsAsString( sourceDependenciesFile, depsFileContents );
        TEST_ASSERT( depsFileContents.FindI( "cldependencyargs\\\\file.cpp" ) );
    }
}
#endif

//------------------------------------------------------------------------------
TEST_CASE( TestObject, OwnerObjectList )
{
    // Check behavior of properties stored in OwnerObjectList

    const char * const srcBFFFile = "Tools/FBuild/FBuildTest/Data/TestObject/OwnerObjectList/ownerobjectlist.bff";
    const char * const dstBFFFile = "../tmp/Test/Object/OwnerObjectList/ownerobjectlist.bff";
#if defined( __WINDOWS__ )
    const char * const outputFile = "../tmp/Test/Object/OwnerObjectList/a.obj";
#else
    const char * const outputFile = "../tmp/Test/Object/OwnerObjectList/a.o";
#endif
    const char * const database = "../tmp/Test/Object/OwnerObjectList/fbuild.fdb";

    // Read BFF file contents into string
    AString bff;
    {
        FileStream f;
        TEST_ASSERT( f.Open( srcBFFFile ) &&
                     f.ReadIntoString( bff ) );
    }

    // Read config file into memory that modify and write
    {
        FileIO::EnsurePathExistsForFile( AStackString( dstBFFFile ) );
        FileStream f;
        TEST_ASSERT( f.Open( dstBFFFile, FileStream::WRITE_ONLY ) &&
                     f.WriteFromString( bff ) );
    }

    // Common options
    FBuildTestOptions options;
    options.m_ConfigFile = dstBFFFile;

    // Clear output directory
    EnsureFileDoesNotExist( outputFile );

    // Build
    {
        FBuildForTest fBuild( options );
        TEST_ASSERT( fBuild.Initialize() );
        TEST_ASSERT( fBuild.Build( "OwnerObjectList" ) );
        TEST_ASSERT( fBuild.SaveDependencyGraph( database ) );
        CheckStatsNode( fBuild.GetStats(), 1, 1, Node::OBJECT_NODE );
    }

    EnsureFileExists( outputFile );

    class BFFEdit
    {
    public:
        BFFEdit( const char * from, bool shouldCauseRebuild )
            : m_From( from )
            , m_ShouldCauseRebuild( shouldCauseRebuild )
        {
        }
        BFFEdit & operator=( const BFFEdit & other ) = delete;

        const char * const m_From;
        const bool m_ShouldCauseRebuild;
    };
    static const BFFEdit bffEdits[] =
        {
            { "// Comment", false },
            { ".CompilerOptions +", true },
            { ".CompilerOptionsDeoptimized +", true },
            { ".PCHOptions +", true },
            { ".PreprocessorOptions =", true },
            { ".PreBuildDependencies =", true },
            { ".CompilerForceUsing =", true },
            { ".DeoptimizeWritableFiles =", true },
            { ".DeoptimizeWritableFilesWithToken =", true },
        };

    for ( const BFFEdit & edit : bffEdits )
    {
        // First ensure no rebuilding without changes
        {
            FBuildForTest fBuild( options );
            TEST_ASSERT( fBuild.Initialize( database ) );
            TEST_ASSERT( fBuild.Build( "OwnerObjectList" ) );
            TEST_ASSERT( fBuild.SaveDependencyGraph( database ) );
            CheckStatsNode( fBuild.GetStats(), 1, 0, Node::OBJECT_NODE );
        }

        // Modify BFF
        {
            TEST_ASSERT( bff.Replace( edit.m_From, "//" ) == 1 );
            FileStream f;
            TEST_ASSERT( f.Open( dstBFFFile, FileStream::WRITE_ONLY ) &&
                         f.WriteFromString( bff ) );
        }

        // Ensure ObjectNode rebuilds after modifying ObjectListNode
        {
            FBuildForTest fBuild( options );
            TEST_ASSERT( fBuild.Initialize( database ) );
            TEST_ASSERT( fBuild.Build( "OwnerObjectList" ) );
            TEST_ASSERT( fBuild.SaveDependencyGraph( database ) );
            const uint32_t expectedBuild = edit.m_ShouldCauseRebuild ? 1 : 0;
            CheckStatsNode( fBuild.GetStats(), 1, expectedBuild, Node::OBJECT_NODE );
        }
    }
}

//------------------------------------------------------------------------------
TEST_CASE( TestObject, OwnerObjectListPCH )
{
    // Check behavior of properties stored in OwnerObjectList

    const char * const srcBFFFile = "Tools/FBuild/FBuildTest/Data/TestObject/OwnerObjectListPCH/ownerobjectlistpch.bff";
    const char * const dstBFFFile = "../tmp/Test/Object/OwnerObjectListPCH/ownerobjectlistpch.bff";
#if defined( __WINDOWS__ )
    const char * const outputFile = "../tmp/Test/Object/OwnerObjectListPCH/a.obj";
#else
    const char * const outputFile = "../tmp/Test/Object/OwnerObjectListPCH/a.o";
#endif
    const char * const database = "../tmp/Test/Object/OwnerObjectListPCH/fbuild.fdb";

    // Read BFF file contents into string
    AString bff;
    {
        FileStream f;
        TEST_ASSERT( f.Open( srcBFFFile ) &&
                     f.ReadIntoString( bff ) );
    }

    // Read config file into memory that modify and write
    {
        FileIO::EnsurePathExistsForFile( AStackString( dstBFFFile ) );
        FileStream f;
        TEST_ASSERT( f.Open( dstBFFFile, FileStream::WRITE_ONLY ) &&
                     f.WriteFromString( bff ) );
    }

    // Common options
    FBuildTestOptions options;
    options.m_ConfigFile = dstBFFFile;
    options.m_ShowBuildReason = true;

    // Clear output directory
    EnsureFileDoesNotExist( outputFile );

    // Build
    {
        FBuildForTest fBuild( options );
        TEST_ASSERT( fBuild.Initialize() );
        TEST_ASSERT( fBuild.Build( "OwnerObjectListPCH" ) );
        TEST_ASSERT( fBuild.SaveDependencyGraph( database ) );
        CheckStatsNode( fBuild.GetStats(), 2, 2, Node::OBJECT_NODE );
    }

    EnsureFileExists( outputFile );

    class BFFEdit
    {
    public:
        BFFEdit( const char * from, const char * to )
            : m_From( from )
            , m_To( to )
        {
        }
        BFFEdit & operator=( const BFFEdit & other ) = delete;

        const char * const m_From;
        const char * const m_To;
    };
    static const BFFEdit bffEdits[] =
        {
            { "+ ' -DD1'", "+ ' -DD1 -DNEW_OPTION'" }, // Add option
            { "+ ' -DD2'", "+ ''" }, // Remove option
            { "+ ' -IInclude1'", "+ ' -IInclude2'" }, // Modify option
        };

    for ( const BFFEdit & edit : bffEdits )
    {
        // First ensure no rebuilding without changes
        {
            FBuildForTest fBuild( options );
            TEST_ASSERT( fBuild.Initialize( database ) );
            TEST_ASSERT( fBuild.Build( "OwnerObjectListPCH" ) );
            TEST_ASSERT( fBuild.SaveDependencyGraph( database ) );
            CheckStatsNode( fBuild.GetStats(), 2, 0, Node::OBJECT_NODE );
        }

        // Modify BFF
        {
            TEST_ASSERT( bff.Replace( edit.m_From, edit.m_To ) == 1 );
            FileStream f;
            TEST_ASSERT( f.Open( dstBFFFile, FileStream::WRITE_ONLY ) &&
                         f.WriteFromString( bff ) );
        }

        // Ensure ObjectNode rebuilds after modifying ObjectListNode
        {
            FBuildForTest fBuild( options );
            TEST_ASSERT( fBuild.Initialize( database ) );
            TEST_ASSERT( fBuild.Build( "OwnerObjectListPCH" ) );
            TEST_ASSERT( fBuild.SaveDependencyGraph( database ) );
            CheckStatsNode( fBuild.GetStats(), 2, 2, Node::OBJECT_NODE );
        }
    }
}

//------------------------------------------------------------------------------
