//-------------------------------------------------------------------------------------------------------
// Copyright (C) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
//-------------------------------------------------------------------------------------------------------
// rl.cpp
// To build:
//   nmake RELEASE=1 rl.exe
//
// Julian Burger
// Microsoft Corporation
// 11/06/98

// Configuration files are XML
//
// rl{asm,exe}dirs.xml have directory lists
// rl{asm,exe}.xml have test lists
//
// General format:
//
// <?xml version="1.0"?>
//
// <regress-asm>        // head node, tag unimportant
//
// <test> // test node, tag unimportant
//
//    <default>         // default test information
//
//       test info      // see below
//
//    </default>
//
//    <condition order="1" type="exclude"> // condition node
//
//       <target>...</target>    // list of targets this condition applies to
//
//    </condition>
//
// </test>
//
//
// Test info (valid for all configuration files):
//
//    <files>...</files>   // list of comma-delimited files
//
// For asm regressions, files may be grouped in a single test node for
// convenience.  For exe regressions, all the files comprising a single test
// should be group.
//
//    <tags>...</tags>     // list of comma-delimited (case-insensitive) tags
//
// Tags are intended as a proper of the test case itself.  That is, they
// should be used to indicate features of a test (like C++ EH or x86 asm)
// rather than arbitrary nonsense.
//
// One tag is recognized specifically by RL: "Pogo".  This indicates the test
// is a Pogo test and should be run specially.  Any other tag is for explicit
// inclusion/exclusion of tests.  E.g.
//
//    <tags>SEH</tags>
//
// This marks a test with the SEH tag.  If -tags:SEH is specified on the command
// line, only tests marked as such would be included.  Similarly, -nottags:SEH
// would exclude tests so tagged.
//
// Test info (valid for asm and exe test configuration files):
//
//    <compile-flags>...</compile-flags>  // compilation flags for this test
//    <rl>...</rl>         // RL directives (case-insensitive)
//
// Test info (valid for exe test configuration files)
//
//    <baseline>...</baseline>  // baseline, expected test output
//    <link-flags>...</link-flags> // link flags for this test
//    <env>                        // extra env vars for this test
//       <envvar1>value1</envvar1> // any number of child nodes with arbitrary data
//       ...                       // <build>release</build>
//                                 // will result in "set build=release" for the test task env
//    </env>
//
// <condition> nodes have an explicit order of evaluation and type (action to
// take if the condition applies) which can be either "include" or "exclude".
//
// asm and exe test conditions may contain <override> nodes that allow default
// info to be overridden.  E.g.
//
//     <condition order="1" type="include">
//
//        <target>ia64</target>
//
//        <override>
//
//           <baseline>ia64-result.out</baseline>
//
//        </override>
//
//     </condition>
//
// The above would override the default baseline with "ia64-result.out" when
// the target is ia64.
//
//
// Currently, condition nodes may have only a single condition (target or
// compile-flags) and non-target conditions must appear after target
// conditions.


// Tags processing
//
// Tags specified in the same command line argument are treated as OR clauses.
// Multiple -tags (and -nottags) switches are permitted and are processed in
// command line order as AND clauses.  E.g.
//
//     -tags:Pogo -nottags:SEH,CPPEH -tags:asm
//
// The -tags declarations says: match all tests with "Pogo" tags.  From that
// set, exclude those that have "SEH" or "CPPEH".  The next -tags arg further
// restricts the set to those with "asm".  (This is a contrived example.)
//
// Note that -tags/-nottags are only applied to files.  Directories may not
// have tags because they are too error prone.


// Directives
//
// The following directives are recognized in the <rl>...</rl> section of a
// test's default info:
//
//   PassFo
//   NoAsm
//   NoGPF

// Log file format
//
// There are three output files generated by default
// 1. rl.log contains the summary of tests passed and failed in each dir
// 2. rl.results.log contains the passed/failed result of each test variation
// 3. rl.full.log contains all the output from all the tests,
//    delimited by +++ <test variation> +++
// Notes:
// 1. The test output in these files is not synchronized on MP,
//    without the -sync option.
// 2. rl.results.log can be used as a baseline database to track regressions,
//    if -time is not used.

// -genlst mode and lst file format
//
// In -genlst -all mode, rl won't run tests but generate test.lst and env.lst
// containing the same info as in the rlexedirs.xml and rlexe.xml files
// These files are then read by the runall test harness to run the tests.
// Please refer to the runall documentation for the lst file format.

// By default, rl spawns as many threads to execute tests as there are
// processors on the system. Only one thread works per directory because
// running multiple tests in a single directory may cause conflicts in the
// presence of startdir.cmd/enddir.cmd and precompiled header flags (e.g.,
// /YX). The primary thread generates a work queue of directories and files to
// test in each directory. As soon as the tests in a directory are determined,
// the directory becomes available for worker threads.
//
// One complication is that in Windows there is only a single "current
// directory" per process. Thus, we need to synchronize as follows:
//
// 1. Worker threads grab the "directory" critical section immediately before
//    spawning a process (e.g., compile, link, etc.),
//
// 2. The process is spawned with the correct current directory, which it
//    inherits.
//
// 3. The lock is released and the next worker is free to change the directory.
//
// 4. The primary thread never changes the current directory, to avoid
//    synchronization issues.
//
// 5. Any file access (fopen_unsafe, etc.) must use full pathnames. The only place
//    relative paths may be used is in commands that are executed via
//    ExecuteCommand(), which does a _chdir before creating a new process to
//    execute the command.
//
// 6. Global variables used by worker threads must be thread-local or
//    properly synchronized. Exceptions are: (a) variables only read, not
//    written, by the worker threads, (b) bThreadStop, which is only written
//    to TRUE by worker threads, so synchronization is not necessary.
//
// 7. All printf/fprintf output from the worker threads must go through
//    the COutputBuffer class, to be properly synchronized. This class buffers
//    up output per thread and flushes it at reasonable intervals, normally
//    after a test is finished running. This includes any output generated
//    by rl as well as any output captured by the pipe output filter on
//    spawned processes.
//
// 8. Do not use _putenv() in the worker threads, as it sets process-wide
//    state.


#include "rl.h"
#include "strsafe.h"

#pragma warning(disable: 4474) // 'fprintf' : too many arguments passed for format string

// Win64 headers (process.h) has this:
#ifndef _INTPTR_T_DEFINED
#ifdef  _WIN64
typedef __int64             intptr_t;
#else
typedef int                 intptr_t;
#endif
#define _INTPTR_T_DEFINED
#endif

// Target machine information
// Any changes made here should have corresponding name added
// to TARGET_MACHINES in rl.h. NOTE: The TARGET_MACHINES enum is in exactly
// the same order as the TargetInfo array is initialized!

TARGETINFO TargetInfo[] = {
   "unknown",    TRUE,  FALSE, FALSE, NULL,           NULL, NULL,
   "x86",        FALSE, FALSE, TRUE,  NULL,           NULL, NULL,
   "ppcwce",     FALSE, FALSE, FALSE, "comcli",       NULL, "x86asm",
   "wvm",        FALSE, FALSE, FALSE, NULL,           "vccoree.lib", "x86asm",
   "wvmcee",     FALSE, FALSE, FALSE, NULL,           NULL, "x86asm",
   "wvmx86",     FALSE, FALSE, FALSE, NULL,           NULL, "x86asm",
   "mips",       FALSE, FALSE, FALSE, NULL,           NULL, "x86asm",
   "arm",        FALSE, FALSE, FALSE, "armsd",        "libcnoce.lib", "x86asm",
   "thumb",      FALSE, FALSE, FALSE, "armsd",        "libcnocet.lib", "x86asm",
   "arm64",      FALSE, FALSE, FALSE, "armsd",        "libcnoce.lib", "x86asm",
   "sh3",        FALSE, FALSE, FALSE, "sh3sim",       "libcsim.lib", "x86asm",
   "sh4",        FALSE, FALSE, FALSE, "sh3sim",       "libcsim.lib", "x86asm",
   "sh5c",       FALSE, FALSE, FALSE, "sh5sim",       "libcsim.lib", "x86asm",
   "sh5m",       FALSE, FALSE, FALSE, "sh5sim",       "libcsim.lib", "x86asm",
   "ia64",       FALSE, TRUE,  TRUE,  NULL,           NULL, "x86asm",
   "amd64",      FALSE, TRUE,  TRUE,  NULL,           NULL, "x86asm",
   "amd64sys",   FALSE, FALSE, TRUE,  "issuerun",     NULL, "x86asm",
   "wvm64",      FALSE, FALSE, FALSE, NULL,           NULL, "x86asm",
   "am33",       FALSE, FALSE, FALSE, NULL,           NULL, "x86asm",
   "m32r",       FALSE, FALSE, FALSE, NULL,           NULL, "x86asm",
   "msil",       FALSE, FALSE, FALSE, NULL,           NULL, "x86asm",
   NULL
};

// Target OS information
// Any changes made here should have corresponding name added
// to TARGET_OS in rl.h. NOTE: The TARGET_OS enum is in exactly
// the same order as the TargetOSNames array is initialized
const char* const TargetOSNames[] = {
    "unknown",
    "win7",
    "win8",
    "winBlue",
    "win10",
    "wp8",
    NULL
};

#define IS_PATH_CHAR(c) ((c == '/') || (c == '\\'))
#define LOG_FULL_SUFFIX ".full"


const char * const ModeNames[] = {
   "assembly", "executable", "shouldn't be printed (RM_DIR)"
};
const char * const InternalModeCmd[] = {
   "<asm regress>", "<exe regress>"
};

const char * const TestInfoKindName[] =
{
   "files",
   "baseline",
   "compile-flags",
   "link-flags",
   "tags",
   "rl",
   "env",
   "command",
   "timeout",
   NULL
};

const char * const DirectiveNames[] =
{
   "PassFo",
   "NoAsm",
   "NoGPF",
   NULL
};


//
// Global variables set before worker threads start, and only accessed
// (not set) by the worker threads.
//

TARGET_MACHINES TargetMachine = DEFAULT_TM;
TARGET_MACHINES RLMachine = DEFAULT_TM;
TARGET_OS TargetOS = DEFAULT_OS;

Tags * TagsList = NULL;
Tags * TagsLast = NULL;
Tags* DirectoryTagsList = NULL;
Tags* DirectoryTagsLast = NULL;

char SavedConsoleTitle[BUFFER_SIZE];
char *REGRESS = NULL, *MASTER_DIR, *DIFF_DIR, *TARGET_MACHINE, *RL_MACHINE, *TARGET_OS_NAME = NULL;
char *REGR_CL, *REGR_DIFF, *REGR_ASM, *REGR_SHOWD;
char *EXTRA_CC_FLAGS, *EXEC_TESTS_FLAGS, *TARGET_VM;
char *LINKER, *LINKFLAGS;
char *CL, *_CL_;
char *JCBinary = "jshost.exe";

BOOL FStatus = TRUE;
char *StatusPrefix, *StatusFormat;

BOOL FVerbose;
BOOL FQuiet;
BOOL FNoWarn;
BOOL FTest;
BOOL FLow;
BOOL FNoDelete;
BOOL FCopyOnFail;
BOOL FSummary = TRUE;
BOOL FMoveDiffs;
BOOL FNoMoveDiffsSwitch;
BOOL FRelativeLogPath;
BOOL FNoDirName = TRUE;
BOOL FBaseline;
BOOL FRebase = FALSE;
BOOL FDiff;
BOOL FBaseDiff;
BOOL FSyncEnumDirs;
BOOL FNogpfnt = TRUE;
BOOL FAppend;
BOOL FAppendTestNameToExtraCCFlags = FALSE;

#ifndef NODEBUG
BOOL FDebug;
#endif

// Output synchronization options
BOOL FSyncImmediate = FALSE;    // flush output immediately---no buffering
BOOL FSyncVariation = FALSE;    // flush after every test variation (default)
BOOL FSyncTest = FALSE;         // flush after all variations of a test
BOOL FSyncDir = FALSE;          // flush after an entire directory

BOOL FSingleThreadPerDir = FALSE; // Perform all tests within each directory on a single thread.
BOOL FSingleDir = FALSE;
BOOL FNoThreadId = FALSE;

char* ExeCompiler = NULL;
char* BaseCompiler = NULL;
char* DiffCompiler = NULL;

RLMODE Mode = DEFAULT_RLMODE;

char *DCFGfile = NULL;
char *CFGfile = NULL;
char *CMDfile = NULL;
int CFGline;

#define MAX_ALLOWED_THREADS 10 // must be <= MAXIMUM_WAIT_OBJECTS (64)
unsigned NumberOfThreads = 0;

TestList DirList, ExcludeDirList;
BOOL FUserSpecifiedFiles = FALSE;
BOOL FUserSpecifiedDirs = TRUE;
BOOL FExcludeDirs = FALSE;
BOOL FGenLst = FALSE;
char *ResumeDir, *MatchDir;

TIME_OPTION Timing = TIME_DIR | TIME_TEST; // Default to report times at test and directory level

static char *ProgramName;
static char *LogName;
static char *FullLogName;
static char *ResultsLogName;

// NOTE: this might be unused now
static char TempPath[MAX_PATH] = ""; // Path for temporary files

//
// Global variables read and written by the worker threads: these need to
// either be protected by synchronization or use thread-local storage.
//

// Ctrl-C or done? This is written by worker threads or the main thread, but
// only to TRUE. Once written to TRUE, it never changes again. Threads notice
// its new state by polling it periodically, and shutting down gracefully if
// it is TRUE.
BOOL bThreadStop = FALSE;

char TitleStatus[BUFFER_SIZE];  // protected by csTitleBar
CRITICAL_SECTION csTitleBar;    // used to serialize title bar
CRITICAL_SECTION csStdio;       // printf serialization
CRITICAL_SECTION csCurrentDirectory;  // used when changing current directory

CProtectedLong NumVariationsRun[RLS_COUNT];
CProtectedLong NumVariationsTotal[RLS_COUNT];  // run / total is displayed
CProtectedLong NumFailuresTotal[RLS_COUNT];
CProtectedLong NumDiffsTotal[RLS_COUNT];

CProtectedLong MaxThreads;

CProtectedLong CntDeleteFileFailures;
CProtectedLong CntDeleteFileFatals;

// Under -syncdirs, enumerate all directories/files before allowing
// any work to be done. Do this by setting this event when done enumerating.
// This releases the worker threads.
CHandle heventDirsEnumerated;

CDirectoryQueue DirectoryQueue;
CDirectoryAndTestCaseQueue DirectoryAndTestCaseQueue;
CThreadInfo*    ThreadInfo = NULL;

// A per-thread id. ThreadId 0 is the primary thread, the directory/file
// enumeration thread. Other threads are numbered 1 to NumberOfThreads

__declspec(thread) int ThreadId = 0;
__declspec(thread) char *TargetVM;
__declspec(thread) COutputBuffer* ThreadOut;    // stdout
__declspec(thread) COutputBuffer* ThreadLog;    // log file
__declspec(thread) COutputBuffer* ThreadFull;   // full log file
__declspec(thread) COutputBuffer* ThreadRes;    // results log file

// Per-thread compare buffers, allocated once, deleted on thread destroy
#define CMPBUF_SIZE 65536
__declspec(thread) char *cmpbuf1 = NULL;
__declspec(thread) char *cmpbuf2 = NULL;

// Allow usage of select deprecated CRT APIs without disabling all deprecated CRT API warnings
#pragma warning (push)
#pragma warning (disable:4996)
char * getenv_unsafe(const char * varName)
{
   // Use getenv instead of getenv_s or _dupenv_s to simplify calls to the API.
   return getenv(varName);
}

FILE * fopen_unsafe(const char * filename, const char * mode)
{
   // Using fopen_s leads to EACCES error being returned occasionally, even when contentious
   // fopen/fclose pairs are wrapped in a critical section.  Unclear why this is happening.
   // Use deprecated fopen instead.
   _set_errno(0);
   return fopen(filename, mode);
}

char* strerror_unsafe(int errnum)
{
    return strerror(errnum);
}
#pragma warning (pop)

//////////////////////////////////////////////////////////////////////////////

void
CleanUp(BOOL fKilled)
{
   if (FStatus)
      SetConsoleTitle(SavedConsoleTitle);

   // Try to remove temporary files. The temp files may
   // be in use, so we might not be able to.

   if (ThreadInfo != NULL) { // if we've gotten through parsing commands
      for (unsigned int i = 0; i <= NumberOfThreads; i++) {
         ThreadInfo[i].DeleteTmpFileList();
      }
   }

   if (FRLFE)
      RLFEDisconnect(fKilled);
}

void __cdecl
NormalCleanUp(void)
{
   CleanUp(FALSE);
}

int __stdcall
NT_handling_function(unsigned long /* dummy -- unused */)
{
   fprintf(stderr, "Exiting...\n");
   fflush(stdout);
   fflush(stderr);

   CleanUp(TRUE);

   // For now, just exit ungracefully. This will probably cause bad
   // things to happen with output filtering, since we've
   // just killed the pipe.

   ExitProcess(1);
#if _MSC_VER<1300
   return 1; // avoid C4716 warning caused by no return on ExitProcess
#endif
}

void
assert(
   char *file,
   int line
)
{
   fprintf(stderr, "Assertion failed on line %d of %s\n",
      line, file);
}

//////////////////////////////////////////////////////////////////////////////
//
// Output functions. All worker thread output must be generated using these
// functions, then flushed at appropriate times based on the -sync option.
//

void
__cdecl Fatal(const char *fmt, ...)
{
   va_list arg_ptr;

   va_start(arg_ptr, fmt);
   fprintf(stderr, "Error: ");
   vfprintf(stderr, fmt, arg_ptr);
   fputc('\n', stderr);

   exit(1);
}

void
__cdecl Warning(const char *fmt, ...)
{
   va_list arg_ptr;
   char buf[50], tempBuf[BUFFER_SIZE];

   ASSERT(ThreadOut != NULL);

   buf[0] = '\0';
   if (!FNoThreadId && ThreadId != 0 && NumberOfThreads > 1) {
      sprintf_s(buf, "%d>", ThreadId);
   }

   if (!FNoWarn) {
      va_start(arg_ptr, fmt);
      vsprintf_s(tempBuf, fmt, arg_ptr);
      ASSERT(strlen(tempBuf) < BUFFER_SIZE);
      ThreadOut->Add("%sWarning: %s\n", buf, tempBuf);
      ThreadFull->Add("%sWarning: %s\n", buf, tempBuf);
   }
}

void
__cdecl Message(const char *fmt, ...)
{
   va_list arg_ptr;
   char buf[50], tempBuf[BUFFER_SIZE];

   ASSERT(ThreadOut != NULL);

   buf[0] = '\0';
   if (!FNoThreadId && ThreadId != 0 && NumberOfThreads > 1) {
      sprintf_s(buf, "%d>", ThreadId);
   }

   va_start(arg_ptr, fmt);
   vsprintf_s(tempBuf, fmt, arg_ptr);
   ASSERT(strlen(tempBuf) < BUFFER_SIZE);
   ThreadOut->Add("%s%s\n", buf, tempBuf);
   ThreadFull->Add("%s%s\n", buf, tempBuf);
}

void
__cdecl WriteLog(const char *fmt, ...)
{
   va_list arg_ptr;
   char buf[50], tempBuf[BUFFER_SIZE];

   ASSERT(ThreadLog != NULL);

   buf[0] = '\0';
   if (!FNoThreadId && ThreadId != 0 && NumberOfThreads > 1) {
      sprintf_s(buf, "%d>", ThreadId);
   }

   va_start(arg_ptr, fmt);
   vsprintf_s(tempBuf, fmt, arg_ptr);
   ASSERT(strlen(tempBuf) < BUFFER_SIZE);
   ThreadLog->Add("%s%s\n", buf, tempBuf);
}

void
__cdecl LogOut(const char *fmt, ...)
{
   va_list arg_ptr;
   char buf[50], tempBuf[BUFFER_SIZE];

   ASSERT(ThreadOut != NULL);
   ASSERT(ThreadLog != NULL);

   buf[0] = '\0';
   if (!FNoThreadId && ThreadId != 0 && NumberOfThreads > 1) {
      sprintf_s(buf, "%d>", ThreadId);
   }

   va_start(arg_ptr, fmt);
   vsprintf_s(tempBuf, fmt, arg_ptr);
   ASSERT(strlen(tempBuf) < BUFFER_SIZE);
   ThreadOut->Add("%s%s\n", buf, tempBuf);
   ThreadLog->Add("%s%s\n", buf, tempBuf);
   ThreadFull->Add("%s%s\n", buf, tempBuf);
}

void
__cdecl LogError(const char *fmt, ...)
{
   va_list arg_ptr;
   char buf[50], tempBuf[BUFFER_SIZE];

   ASSERT(ThreadOut != NULL);
   ASSERT(ThreadLog != NULL);

   buf[0] = '\0';
   if (!FNoThreadId && ThreadId != 0 && NumberOfThreads > 1) {
      sprintf_s(buf, "%d>", ThreadId);
   }

   va_start(arg_ptr, fmt);
   vsprintf_s(tempBuf, fmt, arg_ptr);
   ASSERT(strlen(tempBuf) < BUFFER_SIZE);
   ThreadOut->Add("%sError: %s\n", buf, tempBuf);
   ThreadLog->Add("%sError: %s\n", buf, tempBuf);
   ThreadFull->Add("%sError: %s\n", buf, tempBuf);
}

// Helper function to flush all thread output.
__inline void FlushOutput(
   void
)
{
   ThreadOut->Flush();
   ThreadLog->Flush();
   ThreadFull->Flush();
   ThreadRes->Flush();
}

//////////////////////////////////////////////////////////////////////////////
//
// Various DeleteFile() wrappers. We have had lots of problems where
// DeleteFile() fails with error 5 -- access denied. It's not clear why this
// is: it seems like an OS bug where the handle to a process is not released
// when the process handle becomes signaled. Perhaps there is a process that
// sits around grabbing handles. It seems to be worse with CLR testing and
// issuerun/readrun cross-compiler testing. To deal with this, loop and sleep
// between delete tries for some calls. Keep track of how many failures there
// were. Don't clog the log files with "error 5" unless the
// sleep doesn't fix the problem, or the user specifies verbose.

BOOL
DeleteFileIfFoundInternal(
   char* filename
)
{
   BOOL ok;

   ASSERT(filename != NULL);

   if (FTest)
   {
      return TRUE;
   }

   // We could see if it's there and then try to delete it. It's easier to
   // just try deleting it and see what happens.

   ok = DeleteFile(filename);
   if (!ok && (GetLastError() != ERROR_FILE_NOT_FOUND)) {
      CntDeleteFileFailures++;
      return FALSE;
   }

   return TRUE;
}

BOOL
DeleteFileIfFound(
   char* filename
)
{
   BOOL ok;

   ok = DeleteFileIfFoundInternal(filename);
   if (!ok) {
      LogError("DeleteFileIfFound: Unable to delete file %s - error %d", filename, GetLastError());
      return FALSE;
   }

   return TRUE;
}

// Call Win32's DeleteFile(), and print an error if it fails
void
DeleteFileMsg(
   char *filename
)
{
   if (FTest)
   {
      return;
   }

   BOOL ok;

   ok = DeleteFile(filename);
   if (!ok) {
      CntDeleteFileFailures++;
      LogError("DeleteFileMsg: Unable to delete file %s - error %d", filename, GetLastError());
   }
}

// Call Win32's DeleteFile(), and print an error if it fails. Retry a few
// times, just to improve robustness. NOTE: This function will fail (after
// retries) if the file does not exist!

#define MAX_DELETE_FILE_TRIES 10
#define DELETE_FILE_INITIAL_INTERVAL_IN_MS 100
#define DELETE_FILE_DELTA_IN_MS 100

void
DeleteFileRetryMsg(
   char *filename
)
{
   unsigned tries = 1;
   BOOL ok;
   DWORD dwWait = DELETE_FILE_INITIAL_INTERVAL_IN_MS;

   for (;;) {
      ok = DeleteFileIfFoundInternal(filename);
      if (ok)
         break;

      if (FVerbose) {
         LogError("DeleteFileRetryMsg: Unable to delete file %s - error %d", filename, GetLastError());
      }

      if (++tries > MAX_DELETE_FILE_TRIES) {
         LogError("Giving up trying to delete file %s. Further related errors are likely", filename);
         CntDeleteFileFatals++;
         break;
      }

      Sleep(dwWait);
      dwWait += DELETE_FILE_DELTA_IN_MS;
   }
}

void
DeleteMultipleFiles(
   CDirectory* pDir,
   char* pattern
)
{
   WIN32_FIND_DATA findData;
   HANDLE h;
   char full[MAX_PATH];

   sprintf_s(full, "%s\\%s", pDir->GetDirectoryPath(), pattern);

   // Read filenames...

   if ((h = FindFirstFile(full, &findData)) == INVALID_HANDLE_VALUE)
      return;

   do {
      // FindFirstFile/FindNextFile set cFileName to a file name without a
      // path. We need to prepend the directory and use a full path to do
      // the delete. This is because multithreaded rl might have changed the
      // current directory out from underneath us.

      sprintf_s(full, "%s\\%s", pDir->GetDirectoryPath(), findData.cFileName);
      if (FVerbose)
         Message("Deleting %s\n", full);

      DeleteFileRetryMsg(full);
   } while (FindNextFile(h, &findData));

   FindClose(h);
}

char *
GetFilenamePtr(
   char *path
)
{
   char *s;

   s = strrchr(path, '\\');
   if (s)
      return s + 1;
   return path;
}

// Get extension of a filename, or "" if no extension found.
const char* GetFilenameExt(const char *path)
{
    const char* ext = strrchr(path, '.');
    return ext ? ext : "";
}

char *
mytmpnam(
   char *directory,
   char *prefix,
   char *filename
)
{
   UINT r;
   char threadPrefix[MAX_PATH];    // make the prefix thread-specific

   // Note that GetTempFileName only uses the first 3 characters of the
   // prefix. This should be okay, because it will still create a unique
   // filename.

   sprintf_s(threadPrefix, "%s%X", prefix, ThreadId);

   // NOTE: GetTempFileName actually creates a file when it succeeds.
   r = GetTempFileNameA(directory, threadPrefix, 0, filename);

   if (r == 0) {
      return NULL;
   }

   return filename;
}

//
// It is possible antivirus program on dev box locked testout when we want to compare testout with baseline.
// If this happens, delay and try a few more times.
//
HANDLE OpenFileToCompare(char *file)
{
    const int MAX_TRY = 1200;
    const DWORD NEXT_TRY_SLEEP_MS = 250;

    HANDLE h;

    int i = 0;
    for (;;)
    {
        // Open the files using exclusive access
        h = CreateFile(file, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL);
        if (h != INVALID_HANDLE_VALUE)
        {
            break;
        }

        DWORD lastError = GetLastError();
        if (++i < MAX_TRY && lastError == ERROR_SHARING_VIOLATION)
        {
            Sleep(NEXT_TRY_SLEEP_MS);
            continue;
        }

        LogError("Unable to open file %s - error %d", file, lastError);
        break;
    }

    return h;
}

// Do a quick file equality comparison using pure Win32 functions. (Avoid
// using CRT functions; the MT CRT seems to have locking/flushing problems on
// MP boxes.)

int
DoCompare(
   char *file1,
   char *file2
)
{
   CHandle h1, h2;     // automatically closes open handles
   DWORD count1, count2;
   BOOL b1, b2;
   DWORD size1, size2;

   // Allocate large buffers for doing quick file comparisons.

   if (cmpbuf1 == NULL) {
      // initialize the buffers
      cmpbuf1 = new char[CMPBUF_SIZE];
      cmpbuf2 = new char[CMPBUF_SIZE];
      if ((cmpbuf1 == NULL) || (cmpbuf2 == NULL))
         Fatal("new failed");
   }

   h1 = OpenFileToCompare(file1);
   if (h1 == INVALID_HANDLE_VALUE) {
      return -1;
   }
   h2 = OpenFileToCompare(file2);
   if (h2 == INVALID_HANDLE_VALUE) {
      return -1;
   }

   // Short circuit by first checking for different file lengths.

   size1 = GetFileSize(h1, NULL);  // assume < 4GB files
   if (size1 == 0xFFFFFFFF) {
      LogError("Unable to get file size for %s - error %d", file1, GetLastError());
      return -1;
   }
   size2 = GetFileSize(h2, NULL);
   if (size2 == 0xFFFFFFFF) {
      LogError("Unable to get file size for %s - error %d", file2, GetLastError());
      return -1;
   }
   if (size1 != size2) {  // not equal; don't bother reading the files

#ifndef NODEBUG
      if (FDebug)
         printf("DoCompare shows %s and %s are NOT equal (different size)\n", file1, file2);
#endif

      return 1;
   }
   do {
      b1 = ReadFile(h1, cmpbuf1, CMPBUF_SIZE, &count1, NULL);
      b2 = ReadFile(h2, cmpbuf2, CMPBUF_SIZE, &count2, NULL);
      if (!b1 || !b2) {
         LogError("ReadFile failed doing compare of %s and %s", file1, file2);
         return -1;
      }

      if (count1 != count2 || memcmp(cmpbuf1, cmpbuf2, count1) != 0) {

#ifndef NODEBUG
         if (FDebug)
            printf("DoCompare shows %s and %s are NOT equal (contents differ)\n", file1, file2);
#endif

         return 1;
      }
   } while (count1 == CMPBUF_SIZE);

#ifndef NODEBUG
   if (FDebug)
      printf("DoCompare shows %s and %s are equal\n", file1, file2);
#endif

   return 0;
}

char *
FormatString(
   char *format
)
{
   static char buf[BUFFER_SIZE + 32]; // extra in case a sprintf_s goes over
   int i;

   i = 0;
   while (*format) {
      if (*format != '%') {
         buf[i++] = *format;
      }
      else {
         switch (*++format) {
            case 'd':
               i += sprintf_s(&buf[i], BUFFER_SIZE + 32 - i, "%d", (long)NumDiffsTotal[RLS_TOTAL]);
               break;
            case 'f':
               i += sprintf_s(&buf[i], BUFFER_SIZE + 32 - i, "%d", (long)NumFailuresTotal[RLS_TOTAL]);
               break;
            case 't':
               i += sprintf_s(&buf[i], BUFFER_SIZE + 32 - i, "%d", (long)NumVariationsRun[RLS_TOTAL]);
               break;
            case 'T':
               i += sprintf_s(&buf[i], BUFFER_SIZE + 32 - i, "%d", (long)NumVariationsTotal[RLS_TOTAL]);
               break;
            case 'p':
               if ((long)NumVariationsTotal[RLS_TOTAL]) {
                  i += sprintf_s(&buf[i], BUFFER_SIZE + 32 - i, "%d",
                     100 * (long)NumVariationsRun[RLS_TOTAL] /
                     (long)NumVariationsTotal[RLS_TOTAL]);
               }
               else {
                  i += sprintf_s(&buf[i], BUFFER_SIZE + 32 - i, "--");
               }
               break;
            default:
               buf[i++] = *format;
         }
      }
      format++;
      if (i > BUFFER_SIZE)
         break;
   }

   ASSERTNR(i < BUFFER_SIZE + 32);
   buf[i] = '\0';

   return buf;
}


void
Usage(
   BOOL fExtended
)
{
   int i;
   BOOL fFirst;

   printf(
      "Usage: %s [options] [files...] [-args ...]\n"
      "Specifying files on the command line overrides the configuration file parsing\n"
      "\nOptions:\n"
      "    -h[x]: short/extended usage info\n"
      "\n"
      "    -base[:backend] update MASTER_DIR assembly regression baselines\n"
      "    -diff[:backend] assembly regressions (default if no mode option)\n"
      "    -exe[:backend] run executable tests\n"
      "\n"
      "    -all to run regressions on all directories in directory config file (log files go\n"
      "         in %%REGRESS%%\\logs directory)\n"
      , ProgramName
   );

   if (fExtended) {
      printf(
         "    -dirs:dir[,dir...] only regress the specified directories\n"
         "    -nodirs:dir[,dir...] exclude the specified directories\n"
         "    -resume:dir resume from the specified directory (implies -append)\n"
         "    -match:dir run regressions on directories with dir as a prefix\n"
         "\n"
         "    -tags:tag[,tag...] only regress tests with any of the specified tags\n"
         "    -nottags:tag[,tag...] don't regress tests with any of the specified tags\n"
         "    -dirtags:tag[,tag...] only regress tests within directories with any of the specified tags\n"
         "    -dirnottags:tag[,tag...] don't regress tests within directories with any of the specified tags\n"
         "\n"
         "    -exeflags:flag[;flag] specify exe flags to use in place of EXEC_TESTS_FLAGS\n"
         "    -target:arch specify the target architecture, instead of TARGET_MACHINE\n"
         "    -rltarget:arch specify the target architecture, instead of RL_MACHINE\n"
         "    -os:name specify the target os, instead of TARGET_OS\n"
         "    -regress:dir specify the root dir, instead of REGRESS\n"
         "\n"
         "    -log:file specifies log file name (default: " DEFAULT_LOG_FILE ")\n"
         "    -full:file specifies full log file name (default: " DEFAULT_FULL_LOG_FILE ")\n"
         "    -results:file specifies runall style results log name (default: " DEFAULT_RESULTS_LOG_FILE ")\n"
         "    -rellog to allow a relative path log file\n"
         "\n"
         "    -status[:prefix] to track diffs/failures in title\n"
         "         default prefix is \"{Diffs,Exec} [target] \"\n"
         "    -statusformat:format to specify display of statistics\n"
         "         default format is \"(%%d diffs, )%%f failures, %%t/%%T (%%p%%%%)\"\n"
         "         where %%d = # diffs, %%f = #failures,\n"
         "               %%t = num tests run, %%T = num tests total,\n"
         "               %%p = percentage of tests completed\n"
         "    -rlfe[:options] launch RLFE (graphical front end)\n"
         "\n"
         "    -nosummary disables output of summary diff/failure info (implies -dirname)\n"
         "    -dirname enables output of '*** dirname ***'\n"
         "    -nomovediffs to not place assembly diffs in DIFF_DIR\n"
         "    -nodelete to not delete objs and exes when doing -exe\n"
         "         (only compatible with a single EXEC_TESTS_FLAGS flags)\n"
         "    -copyonfail to copy *.exe *.obj to fail.(OPT) subdirectory when rl.mak\n"
         "         tests fail\n"
         "    -allowpopup to allow popup screen when tests fail to run correctly\n"
         "         (do not link with nogpfnt.obj)\n"
         "    -append to append to the log files without deleting them first\n"
         "\n"
         "    -quiet eliminates console display of progress\n"
         "    -verbose displays info about regressions\n"
         "    -nowarn turns off warnings\n"
         "    -low sets RL process to a lower priority\n"
         "    -test just displays regression commands, doesn't execute them\n"
         "\n"
         "    -time:variation times each variation\n"
         "    -time:test      times each test\n"
         "    -time:dir       times each directory\n"
         "    -time:all       enables all timing options\n"
         "\n"
         "    -rebase:create rebase file if testout is different to baseline\n"
         "    -threads:# specifies # of worker threads to use (default==processor count)\n"
         "    -nothreadid to omit the thread id from multithreaded output\n"
         "    -sync:immediate to flush output immediately---no buffering\n"
         "    -sync:variation (default) to flush output after a test variation\n"
         "    -sync:test to flush output after all variations of a test\n"
         "    -sync:dir to flush output after an entire directory\n"
         "\n"
         "    -dcfg:filename override directory config file\n"
         "        ASM default:" DEFAULT_ASM_DCFG "  EXE default:" DEFAULT_EXE_DCFG "\n"
         "    -cfg:filename override file config file\n"
         "        ASM default:" DEFAULT_ASM_CFG "  EXE default:" DEFAULT_EXE_CFG "\n"
         "\n"
         "    -genlst to generate the runall test.lst and env.lst files, and don't run rl tests\n"
         "\n"
      );

#ifndef NODEBUG
      printf(
         "DEBUG ONLY:\n"
         "    -debug to show debug info (implies -verbose)\n"
         "    -syncdirs to enumerate all dirs/files before running tests\n"
         "\n"
      );
#endif

      printf(
         "Perform %s regressions by default.\n"
         , ModeNames[DEFAULT_RLMODE]
      );

      printf(
         "Current directory assumed if -all/-dirs/-resume/-match not specified\n"
         "-status is the default; use -status:- to disable\n"
         "Target machine defaults to %s (option -target or environment var\n"
         "    TARGET_MACHINE overrides)\n"
         , TargetInfo[DEFAULT_TM].name
      );
      printf(
         "    (Valid targets:");
      fFirst = TRUE;
      for (i = 1; TargetInfo[i].name; i++) {
         if (!TargetInfo[i].fRL_MACHINEonly) {
            if (!fFirst)
               putchar(',');
            else
               fFirst = FALSE;
            printf(" %s", TargetInfo[i].name);
         }
      }
      printf(")\n");

      printf(
         "Option -rltarget or environment var RL_MACHINE overrides TARGET_MACHINE for\n"
         "    configuration checking\n"
         "    (Additional configurations:");
      fFirst = TRUE;
      for (i = 1; TargetInfo[i].name; i++) {
         if (TargetInfo[i].fRL_MACHINEonly) {
            if (!fFirst)
               putchar(',');
            else
               fFirst = FALSE;
            printf(" %s", TargetInfo[i].name);
         }
      }
      printf(")\n");

      printf(
          "Target os defaults to %s (option -os or environment var TARGET_OS overrides)\n"
          "    (Valid os:"
          , TargetOSNames[DEFAULT_OS]);
      fFirst = TRUE;
      for (i = 1; TargetOSNames[i]; i++) {
        if (!fFirst)
            putchar(',');
        else
            fFirst = FALSE;
        printf(" %s", TargetOSNames[i]);
      }
      printf(")\n");

      printf(
         "MASTER_DIR env var defaults to master.<target machine>\n"
         "DIFF_DIR defaults to diffs.<target machine>\n"
         "\n"
         "The following environment variables are recognized:\n"
         "REGR_CL use the specified command instead of " DEFAULT_REGR_CL "\n"
         "REGR_ASM generate assembly listings and use the specified command to\n"
         "    assemble them\n"
         "REGR_DIFF use the specified command instead of " DEFAULT_REGR_DIFF " (asm only)\n"
         "REGR_SHOWD use the specified executable instead of %s\\bin\\showd.cmd (asm only)\n"
         "EXTRA_CC_FLAGS specifies additional flags to pass to REGR_CL\n"
         "    if EXTRA_CC_FLAGS is not specified, it is constructed from\n"
         "    RL_{ASM,EXE}_CFLAGS and RL_B2, if present\n"
         "RL_ASM_CFLAGS specify ASM only flags (used to construct EXTRA_CC_FLAGS)\n"
         "RL_EXE_CFLAGS specify EXE only flags (used to construct EXTRA_CC_FLAGS)\n"
         "RL_B2 specify backend to use (appended to above to create EXTRA_CC_FLAGS)\n"
         "EXEC_TESTS_FLAGS semicolon delimited list of testing options (exe only)\n"
         "    (overridden by -exeflags switch) (defaults to %s)\n"
         , getenv_unsafe("REGRESS"), DEFAULT_EXEC_TESTS_FLAGS
      );

      printf(
         "REGR_NOMASTERCMP specifies that the generated ASM files should not be compared\n"
         "to the masters (useful if you don't care about diffs and want to save time.)\n"
         "TARGET_VM specifies the program used to execute a test (exe only)\n"
      );
      fFirst = TRUE;
      for (i = 1; TargetInfo[i].name; i++) {
         if (TargetInfo[i].TARGET_VM) {
            if (fFirst) {
               printf("    (");
               fFirst = FALSE;
            }
            else {
               printf(", ");
            }
            printf("%s: %s", TargetInfo[i].name, TargetInfo[i].TARGET_VM);
         }
      }
      if (!fFirst)
         printf(")\n");

      printf(
         "LINKER use the specified command instead of " DEFAULT_LINKER " (exe only)\n"
         "LINKFLAGS specifies additional flags to pass to LINKER (exe only)\n"
      );
      fFirst = TRUE;
      for (i = 1; TargetInfo[i].name; i++) {
         if (TargetInfo[i].LINKFLAGS) {
            if (fFirst) {
               printf("    (");
               fFirst = FALSE;
            }
            else {
               printf(", ");
            }
            printf("%s: %s", TargetInfo[i].name, TargetInfo[i].LINKFLAGS);
         }
      }
      if (!fFirst)
         printf(")\n");
   }
}

TARGET_MACHINES
ParseMachine(
   char *s
)
{
   int i;

   for (i = 1; TargetInfo[i].name; i++) {
      if (!_stricmp(TargetInfo[i].name, s))
         return (TARGET_MACHINES)i;
   }

   return TM_UNKNOWN;
}

TARGET_OS
ParseOS(
   char *s
)
{
   int i;

   for (i = 1; TargetOSNames[i]; i++) {
      if (!_stricmp(TargetOSNames[i], s))
         return (TARGET_OS)i;
   }

   return TO_UNKNOWN;
}

// Does the config file specified target match with our regression target?
// TRUE if they are equal or regrTarget is a superset of fileTarget.
BOOL
MatchMachine(
   TARGET_MACHINES fileTarget,
   TARGET_MACHINES regrTarget
)
{
   if (fileTarget == regrTarget)
      return TRUE;

   if (fileTarget == TM_WVM) {
      if (regrTarget == TM_WVMCEE || regrTarget == TM_WVMX86 || regrTarget == TM_WVM64)
         return TRUE;
   }

   return FALSE;
}

BOOL
MatchOS(
   TARGET_OS fileTarget,
   TARGET_OS regrTarget
)
{
    if (fileTarget == regrTarget)
    {
        return TRUE;
    }

    // Apply win8 override for wp8
    if (fileTarget == TO_WIN8 && regrTarget == TO_WP8)
    {
        return TRUE;
    }

    return FALSE;
}

BOOL
HasInfoList
(
   const char * szInfoList1,
   const char * delim1,
   const char * szInfoList2,
   const char * delim2,
   bool         allMustMatch
)
{
   if (szInfoList1 == NULL)
   {
      return FALSE;
   }

   if (szInfoList2 == NULL)
   {
      return FALSE;
   }

   const char * s = szInfoList2;
   BOOL fHasInfo = TRUE;        // start with TRUE on each entry
   for (;;)
   {
      // Parse an item from szInfoList2.

      char * e;
      for (e = const_cast<char *>(s);
           (strchr(delim2, *e) == NULL) && (*e != '\0');
           e++)
      {
      }

      char c = *e;
      *e = '\0';

      if (allMustMatch)
      {
        if (delim2 &&  (*delim2 == ','))
            fHasInfo = TRUE; // new condition after ','
         const char *save;
         save = strchr(s, *delim1);

         while(save != NULL) {
            fHasInfo = fHasInfo && HasInfoList(szInfoList1, delim1,
               s, delim1, allMustMatch);
            s = save + 1;
            save = strchr(s, *delim1);
         }

         fHasInfo = fHasInfo && HasInfo(szInfoList1, delim1, s);
      }
      else
      {
         fHasInfo = HasInfo(szInfoList1, delim1, s);
      }

      *e = c;

      if (fHasInfo)
      {
         return TRUE;
      }

      if (c == '\0')
      {
         break;
      }

      s = e + 1;
   }

   return FALSE;
}

const char *
stristr
(
   const char * str,
   const char * sub
)
{
   size_t len = strlen(sub);
   size_t i;

   while (*str)
   {

      for (i = 0; i < len; i++)
      {
         if (tolower(str[i]) != tolower(sub[i]))
         {
            if ((str[i] != '/' && str[i] != '-') || (sub[i] != '-' && sub[i] != '/')) {
               // if the mismatch is not between '/' and '-'
               break;
            }
         }
      }
      if (i == len)
      {
         return str;
      }

      str++;
   }

   return NULL;
}

BOOL
HasInfo
(
   const char * szInfoList,
   const char * delim1,
   const char * szInfo
)
{
   size_t len = strlen(szInfo);
   const char * s = szInfoList;

   if (s != NULL)
   {
      for (;;)
      {
         s = stristr(s, szInfo);
         if (s == NULL)
         {
            return FALSE;
         }

         if (((s == szInfoList) || (strchr(delim1, s[-1]) != NULL))
            && ((s[len] == '\0') || (strchr(delim1, s[len]) != NULL)))
         {
            return TRUE;
         }

         while (*s != '\0')
         {
            if (strchr(delim1, *s) != NULL)
            {
               break;
            }

            s++;
         }

         if (*s == '\0')
         {
            break;
         }

         s++;
      }
   }

   return FALSE;
}

StringList *
AddToStringList
(
   StringList * list,
   char* string
)
{
   StringList * p = new StringList;

   p->string = string; // NOTE: we store the pointer; we don't copy the string
   p->next = NULL;

   if (list == NULL)
   {
      return p;
   }

   StringList * last = list;

   while (last->next != NULL)
   {
      last = last->next;
   }

   last->next = p;

   return list;
}

void
FreeStringList
(
   StringList * list
)
{
   while (list)
   {
      StringList * pFree = list;
      list = list->next;

      delete pFree;
   }
}

void
FreeVariants
(
   TestVariant * list
)
{
   while (list)
   {
      TestVariant * pFree = list;
      list = list->next;

      delete pFree;
   }
}

StringList *
ParseStringList(char* p, char* delim)
{
   StringList * list = NULL;

   if (p == NULL)
   {
      return list;
   }

   p = _strdup(p); // don't trash passed-in memory

   p = mystrtok(p, delim, delim);

   while (p != NULL)
   {
      list = AddToStringList(list, p);
      p = mystrtok(NULL, delim, delim);
   }

   return list;
}

StringList *
AppendStringList
(
   StringList * list,
   StringList * listToAppend
)
{
   StringList * p;

   if (list == NULL) {
      return listToAppend;
   }

   for (p = list; p->next != NULL; p = p->next) {
      // nothing
   }

   p->next = listToAppend;
   return list;
}

BOOL
CompareStringList
(
   StringList * list1,
   StringList * list2
)
{
   while ((list1 != NULL) && (list2 != NULL))
   {
      if (_stricmp(list1->string, list2->string))
      {
         return FALSE;
      }

      list1 = list1->next;
      list2 = list2->next;
   }

   if (list1 == list2)
   {
      ASSERTNR(list1 == NULL);

      return TRUE;
   }

   return FALSE;
}

// Set three environment variables for use in suppressing GPF pop-ups:
//    NOGPF       -- for console-mode programs
//    NOGPFWINEXE -- for Windows EXEs
//    NOGPFWINDLL -- for Windows DLLs
// These are useful for either dotest.cmd tests, or rl.mak tests that don't
// use the default console-mode nogpfnt.obj.

void
SetNOGPF()
{
   char tempBuf[BUFFER_SIZE];

   if (FNogpfnt && TargetInfo[TargetMachine].fUseNoGPF) {
      sprintf_s(tempBuf,
         "NOGPF=%s\\bin\\%s\\nogpfnt.obj /entry:nogpfntStartup",
         REGRESS, TargetInfo[TargetMachine].name);
      if (_putenv(tempBuf))
         Fatal("Couldn't set NOGPF");

      sprintf_s(tempBuf,
         "NOGPFWINEXE=%s\\bin\\%s\\nogpfntWinMain.obj /entry:nogpfntWinMainStartup",
         REGRESS, TargetInfo[TargetMachine].name);
      if (_putenv(tempBuf))
         Fatal("Couldn't set NOGPFWINEXE");

      sprintf_s(tempBuf,
         "NOGPFWINDLL=%s\\bin\\%s\\nogpfntDllMain.obj /entry:nogpfntDllMainStartup",
         REGRESS, TargetInfo[TargetMachine].name);
      if (_putenv(tempBuf))
         Fatal("Couldn't set NOGPFWINDLL");
   } else {
      if (_putenv("NOGPF="))
         Fatal("Couldn't set NOGPF");
      if (_putenv("NOGPFWINEXE="))
         Fatal("Couldn't set NOGPFWINEXE");
      if (_putenv("NOGPFWINDLL="))
         Fatal("Couldn't set NOGPFWINDLL");
   }
}

// Get the LINKFLAGS variable. If necessary, massage it and put it back into
// the environment for use by the regr386 scripts and makefiles. The
// manipulation includes:
// 1. add a "no access violation" object to avoid pop-ups
// 2. to make LTCG testing easier, if EXTRA_CC_FLAGS contains /B2c2.dll, we
//    copy that to LINKFLAGS as -B2:c2.dll. We check that there doesn't already
//    exist a -B2: on the LINKFLAGS, or that they don't conflict. If we don't
//    do this manipulation, it is too easy to forget to add -B2: to LINKFLAGS
//    on your own, and hence test some compiler besides the one you intend to,
//    namely whatever c2.dll is on your path.

void
GetLINKFLAGS()
{
   char* readLINKFLAGS;
   char* s;
   char* b2;
   char tmpLINKFLAGS[BUFFER_SIZE];
   char tmpstrtok[BUFFER_SIZE];
   char* tmpstrtoknext = NULL;
   char tempBuf[BUFFER_SIZE];
   bool fPropagateB2;

   if ((readLINKFLAGS = getenv_unsafe("LINKFLAGS")) == NULL) {
      if (NULL == TargetInfo[TargetMachine].LINKFLAGS) {
         tmpLINKFLAGS[0] = '\0';
      } else {
         strcpy_s(tmpLINKFLAGS, TargetInfo[TargetMachine].LINKFLAGS);
      }
   } else {
      strcpy_s(tmpLINKFLAGS, readLINKFLAGS);
   }

   // Look for -B2 or /B2 in EXTRA_CC_FLAGS

   fPropagateB2 = false;
   b2 = NULL;

   strcpy_s(tmpstrtok, EXTRA_CC_FLAGS);
   s = strtok_s(tmpstrtok, " \t", &tmpstrtoknext);
   while (s) {

#ifndef NODEBUG
      if (FDebug)
         printf("\tParsed '%s'\n", s);
#endif

      if ((0 == _strnicmp(s, "/B2", 3)) || (0 == _strnicmp(s, "-B2", 3))) {
         if (b2 == NULL) {
            b2 = s + 3;
            if (*b2 == '\0')
               b2++;
            while (isspace(*b2))
               b2++;
            b2 = _strdup(b2);
            fPropagateB2 = true;
         } else {
            Warning("Two /B2 flags in EXTRA_CC_FLAGS (%s)", EXTRA_CC_FLAGS);
         }
      }
      s = strtok_s(NULL, " \t", &tmpstrtoknext);
   }

   // Now look for -B2: or /B2: in LINKFLAGS to check for consistency.

   strcpy_s(tmpstrtok, tmpLINKFLAGS);
   tmpstrtoknext = NULL;
   s = strtok_s(tmpstrtok, " \t", &tmpstrtoknext);
   while (s) {

#ifndef NODEBUG
      if (FDebug)
         printf("\tParsed '%s'\n", s);
#endif

      if ((0 == _strnicmp(s, "/B2:", 4)) || (0 == _strnicmp(s, "-B2:", 4))) {
         fPropagateB2 = false;
         if (b2 == NULL) {
            Warning("/B2: in LINKFLAGS (%s) but not in EXTRA_CC_FLAGS (%s)", readLINKFLAGS, EXTRA_CC_FLAGS);
         } else {
            // Make sure they are the same
            if (0 != _stricmp(s + 4, b2)) {
               Warning("/B2 flags in EXTRA_CC_FLAGS (%s) conflict with -B2: in LINKFLAGS(%s)", EXTRA_CC_FLAGS, readLINKFLAGS);
            }
         }
      }
      s = strtok_s(NULL, " \t", &tmpstrtoknext);
   }

   if (fPropagateB2) {
      // We found -B2 in EXTRA_CC_FLAGS but not in LINKFLAGS, so propagate
      // it to LINKFLAGS.
      sprintf_s(tempBuf, " -B2:\"%s\"", b2);
      free(b2); // came from _strdup()
      strcat_s(tmpLINKFLAGS, tempBuf);
   }

   // Now, put it back into the environment with all our changes

   sprintf_s(tempBuf, "LINKFLAGS=%s", tmpLINKFLAGS);
   if (_putenv(tempBuf))
      Fatal("Couldn't set LINKFLAGS");

   LINKFLAGS = _strdup(tmpLINKFLAGS);
}

// WARNING: we are liberally using _putenv. As a result, it is dangerous to
// hold onto a pointer returned from getenv(), so always make a copy

void
GetEnvironment(
   void
)
{
   char *env;
   FILE *showd_fp;
   char tempBuf[BUFFER_SIZE];

   // Get the TARGET_MACHINE environment variable. It may have already been
   // specified on the command-line by the "-target" option, in which case
   // we don't get the environment variable, but we do validate what the
   // user passed.

   if ((TARGET_MACHINE != NULL)
      || ((TARGET_MACHINE = getenv_unsafe("TARGET_MACHINE")) != NULL))
   {
      TargetMachine = RLMachine = ParseMachine(TARGET_MACHINE);
      if (TargetMachine == TM_UNKNOWN) {
         Fatal("Unknown machine type specified by TARGET_MACHINE: %s",
            TARGET_MACHINE);
      }
      if (TargetInfo[TargetMachine].fRL_MACHINEonly) {
         Fatal("TARGET_MACHINE specified is only valid for RL_MACHINE: %s",
            TARGET_MACHINE);
      }
   }
   else {
      // Set environment var in case regr scripts reference.

      sprintf_s(tempBuf, "TARGET_MACHINE=%s", TargetInfo[TargetMachine].name);
      if (_putenv(tempBuf))
         Fatal("Couldn't set TARGET_MACHINE");
   }

   // Get the RL_MACHINE environment variable or option.

   if ((RL_MACHINE != NULL)
      || ((RL_MACHINE = getenv_unsafe("RL_MACHINE")) != NULL))
   {
      RLMachine = ParseMachine(RL_MACHINE);
      if (RLMachine == TM_UNKNOWN) {
         Fatal("Unknown machine type specified by RL_MACHINE: %s", RL_MACHINE);
      }
   }
   else {
      // Set environment var in case regr scripts reference.

      sprintf_s(tempBuf, "RL_MACHINE=%s", TargetInfo[RLMachine].name);
      if (_putenv(tempBuf))
         Fatal("Couldn't set RL_MACHINE");
   }

   // Get the TARGET_OS environment variable or option.
   if (TARGET_OS_NAME != NULL
      || (TARGET_OS_NAME = getenv_unsafe("TARGET_OS")) != NULL)
   {
       TargetOS = ParseOS(TARGET_OS_NAME);
       if (TargetOS == TO_UNKNOWN) {
           Fatal("Unknown os type specified by TargetOS: %s", TARGET_OS_NAME);
       }
   }

   // Get the EXTRA_CC_FLAGS environment variable.

   if ((EXTRA_CC_FLAGS = getenv_unsafe("EXTRA_CC_FLAGS")) == NULL) {
      char *cflags, *b2;

      // It doesn't exist, see if we can construct from RL_{ASM,EXE}_CFLAGS.

      if (Mode == RM_ASM)
         cflags = getenv_unsafe("RL_ASM_CFLAGS");
      else
         cflags = getenv_unsafe("RL_EXE_CFLAGS");

      b2 = getenv_unsafe("RL_B2");
      if (BaseCompiler || DiffCompiler || ExeCompiler) {
         if (FVerbose && b2)
            puts("Command line specified compiler overrides RL_B2 environment variable");

         if (ExeCompiler)
            b2 = ExeCompiler;
         else
            b2 = NULL;
      }

      sprintf_s(tempBuf, "%s%s%s",
         cflags ? cflags : "",
         b2 ? " -B2 " : "",
         b2 ? b2 : "");
      EXTRA_CC_FLAGS = _strdup(tempBuf);

      sprintf_s(tempBuf, "EXTRA_CC_FLAGS=%s", EXTRA_CC_FLAGS);
      if (_putenv(tempBuf))
         Fatal("Couldn't set EXTRA_CC_FLAGS");

      if (FVerbose)
         puts("Constructed EXTRA_CC_FLAGS from RL_{ASM,EXE}_CFLAGS and RL_B2");
   } else {
      EXTRA_CC_FLAGS = _strdup(EXTRA_CC_FLAGS);
   }

   // Get REGR_* overrides.

   if ((REGR_CL = getenv_unsafe("REGR_CL")) == NULL) {
      REGR_CL = DEFAULT_REGR_CL;
   } else {
      REGR_CL = _strdup(REGR_CL);
   }

   if ((REGR_DIFF = getenv_unsafe("REGR_DIFF")) == NULL) {
      REGR_DIFF = DEFAULT_REGR_DIFF;
   } else {
      REGR_DIFF = _strdup(REGR_DIFF);
   }

   REGR_ASM = getenv_unsafe("REGR_ASM");
   if (REGR_ASM != NULL) {
      REGR_ASM = _strdup(REGR_ASM);
   }

   // Get/generate {MASTER,DIFF}_DIR if doing assembly tests.

   if (Mode == RM_ASM) {

      // Get/generate the MASTER directory.

      if ((MASTER_DIR = getenv_unsafe("MASTER_DIR")) == NULL) {
         if (FVerbose) {
            // This should be the default, so don't warn
            Warning("Generating MASTER_DIR environment variable");
         }

         sprintf_s(tempBuf, "master.%s", TargetInfo[RLMachine].name);
         MASTER_DIR = _strdup(tempBuf);

         // Set environment var in case user overrides internal regr
         // with -cmd.

         sprintf_s(tempBuf, "MASTER_DIR=%s", MASTER_DIR);
         if (_putenv(tempBuf))
            Fatal("Couldn't set MASTER_DIR");
      } else {
         MASTER_DIR = _strdup(MASTER_DIR);
      }

      // Get/generate the DIFF directory.

      if (FMoveDiffs) {
         if ((DIFF_DIR = getenv_unsafe("DIFF_DIR")) == NULL) {
            if (FVerbose) {
               // This should be the default, so don't warn
               Warning("Generating DIFF_DIR environment variable");
            }

            sprintf_s(tempBuf, "diffs.%s", TargetInfo[RLMachine].name);
            DIFF_DIR = _strdup(tempBuf);

            // Set environment var in case user overrides internal regr
            // with -cmd.

            sprintf_s(tempBuf, "DIFF_DIR=%s", DIFF_DIR);
            if (_putenv(tempBuf))
               Fatal("Couldn't set DIFF_DIR");
         } else {
            DIFF_DIR = _strdup(DIFF_DIR);
         }
      }
      else {
         DIFF_DIR = ".";
         if (_putenv("DIFF_DIR="))
            Fatal("Couldn't clear DIFF_DIR");
      }

      // Get the REGR_SHOWD directory.

      REGR_SHOWD = getenv_unsafe("REGR_SHOWD");
      if (REGR_SHOWD == NULL) {
         sprintf_s(tempBuf, "%s\\bin\\showd.cmd", REGRESS);
         REGR_SHOWD = _strdup(tempBuf);
      } else {
         REGR_SHOWD = _strdup(REGR_SHOWD);
      }
      showd_fp = fopen_unsafe(REGR_SHOWD, "rt");
      if (showd_fp == NULL) {
         if (!FGenLst)
             Fatal("couldn't open diff processing command file (%s) with error '%s'", REGR_SHOWD, strerror_unsafe(errno));
      } else
         fclose(showd_fp);
   }

   // Get executable test options.

   else {
      if (EXEC_TESTS_FLAGS == NULL) {
         if ((EXEC_TESTS_FLAGS = getenv_unsafe("EXEC_TESTS_FLAGS")) == NULL)
            EXEC_TESTS_FLAGS = DEFAULT_EXEC_TESTS_FLAGS;

         // We edit EXEC_TESTS_FLAGS, so create a copy.

         EXEC_TESTS_FLAGS = _strdup(EXEC_TESTS_FLAGS);
      }

      if ((TARGET_VM = getenv_unsafe("TARGET_VM")) == NULL) {
         TARGET_VM = TargetInfo[TargetMachine].TARGET_VM;

         // Support automatic cross-compilation

         if ((TARGET_VM == NULL) && TargetInfo[TargetMachine].fAutoCrossCompilation) {
            // If the host processor is not the same as the architecture to
            // test, and we support automatic cross-compilation for this
            // architecture, then create the default TARGET_VM.

            char* PROCESSOR_ARCHITECTURE;

            PROCESSOR_ARCHITECTURE = getenv_unsafe("PROCESSOR_ARCHITECTURE");
            if (PROCESSOR_ARCHITECTURE == NULL) {
               Fatal("PROCESSOR_ARCHITECTURE environment variable not set (this should be set by the OS!)");
            }

            if (0 != _stricmp(TargetInfo[TargetMachine].name, PROCESSOR_ARCHITECTURE)) {
               TARGET_VM = DEFAULT_CROSS_TARGET_VM;
            }
         }
      }

      if (TARGET_VM != NULL) {
         TARGET_VM = _strdup(TARGET_VM);
      }

      if ((LINKER = getenv_unsafe("LINKER")) == NULL) {
         LINKER = DEFAULT_LINKER;
      } else {
         LINKER = _strdup(LINKER);
      }

      GetLINKFLAGS();

      SetNOGPF();
   }

   // Grab CL and _CL_.

   CL = getenv_unsafe("CL");
   if (CL != NULL)
   {
      CL = _strdup(CL);
   }

   _CL_ = getenv_unsafe("_CL_");
   if (_CL_ != NULL)
   {
      _CL_ = _strdup(_CL_);
   }

   // Display all the settings.

   if (FVerbose) {
      if ((env = getenv_unsafe(RL_PRE_ENV_VAR)) != NULL)
         printf(RL_PRE_ENV_VAR"=%s\n", env);
      if ((env = getenv_unsafe(RL_POST_ENV_VAR)) != NULL)
         printf(RL_POST_ENV_VAR"=%s\n", env);

      printf("Target machine = %s", TargetInfo[RLMachine].name);
      printf(", Target os = %s", TargetOSNames[TargetOS]);
      if (Mode == RM_ASM) {
         printf(", MASTER_DIR = %s", MASTER_DIR);
         if (FMoveDiffs)
            printf(", DIFF_DIR = %s", DIFF_DIR);
         if (FBaseline)
            printf(", creating baselines");
         if (FDiff)
            printf(", checking diffs");
      }
      else {
         printf(", TARGET_VM = %s", TARGET_VM);
         printf(", EXEC_TESTS_FLAGS = %s", EXEC_TESTS_FLAGS);
      }
      printf("\nEXTRA_CC_FLAGS = %s\n", EXTRA_CC_FLAGS);

      printf("REGR_CL = %s, REGR_ASM = %s", REGR_CL, REGR_ASM);
      if (Mode == RM_ASM) {
         printf(", REGR_DIFF = %s", REGR_DIFF);
      }
      else {
         printf("\nLINKER = %s, LINKFLAGS = %s", LINKER, LINKFLAGS);
      }
      putchar('\n');

      if (FLow)
         puts("** Running with a low process priority");

      printf("CL = %s, _CL_ = %s\n", CL, _CL_);

      PrintTagsList(TagsList);
      PrintTagsList(DirectoryTagsList);
   }

   if (FStatus && (GetConsoleTitle(SavedConsoleTitle, BUFFER_SIZE) == 0))
      Warning("Couldn't get console title; won't be correctly restored");
}

void
PrintTagsList(
    Tags* pTagsList
)
{
    if (pTagsList != NULL)
    {
        for (Tags * tags = pTagsList; tags != NULL; tags = tags->next)
        {
        printf("   %s '%s'\n", tags->fInclude ? "include" : "exclude", tags->str);
        }
    }
    else
    {
        printf("   None\n");
    }
}

// Initialize a file list.
void
InitTestList(
   TestList * pTestList
)
{
   pTestList->first = pTestList->last = NULL;
}

// Free a file list.
void
FreeTestList(
   TestList * pTestList
)
{
   Test * pFilename, * pFree;

   pFilename = pTestList->first;

   while (pFilename) {
      pFree = pFilename;
      pFilename = pFilename->next;

      FreeStringList(pFree->files);
      FreeVariants(pFree->variants);

      free(pFree);
   }

   InitTestList(pTestList);
}

// Add one file to a file list.
Test *
AddToTestList(
   TestList * pTestList,
   StringList * fileList
)
{
   Test * pFilename;

   ASSERTNR(pTestList);

   pFilename = (Test *)malloc(sizeof(Test));
   memset(pFilename, 0, sizeof(Test));
   pFilename->files = fileList;

   if (pTestList->last) {
      ASSERTNR(pTestList->first);
      pTestList->last->next = pFilename;
   }
   else {
      ASSERTNR(pTestList->first == NULL);
      pTestList->first = pFilename;
   }

   pTestList->last = pFilename;

#ifndef NODEBUG
   if (FDebug)
      printf("Added %s\n", pFilename->name);
#endif

   return pFilename;
}

// Add one file to a file list.
Test *
AddToTestList(
   TestList * pTestList,
   char * name
)
{
   Test * pTest = AddToTestList(pTestList, (StringList *)NULL);
   pTest->name = name;
   return pTest;
}

// Add one dir to the dir list.
Test *
AddDir(
   TestList * pTestList,
   char *name
)
{
   return AddToTestList(pTestList, name);
}

// Add the current directory to the directory list.
void
AddCurrentDir(
   void
)
{
   char *dir;
   char tempBuf[BUFFER_SIZE];

   if (!GetCurrentDirectory(BUFFER_SIZE, tempBuf))
      Fatal("GetCurrentDirectory couldn't live up to its name");

   // If REGRESS is not a prefix, emit a warning.

   if (_strnicmp(tempBuf, REGRESS, strlen(REGRESS))) {
      Warning("Current directory '%s' is not in the '%s' tree",
         tempBuf, REGRESS);
      AddDir(&DirList, _strdup(tempBuf));
   }
   else {
      dir = tempBuf + strlen(REGRESS);
      if (!IS_PATH_CHAR(*dir)) {
         Fatal("Current directory '%s' is not a subdirectory of '%s'",
            tempBuf, REGRESS);
      }
      else {
         AddDir(&DirList, _strdup(dir + 1));
      }
   }
}

// Add a list of user-specified directories.
void
AddUserDirs(
   TestList * pTestList,
   char *s
)
{
   char *context = NULL;
   s = strtok_s(s, XML_DELIM, &context);
   while (s) {
      AddDir(pTestList, s);
      s = strtok_s(NULL, XML_DELIM, &context);
   }
}

#ifndef NODEBUG

void
PrintStringList
(
   StringList *pList
)
{
   for (StringList *cur = pList; cur != NULL; cur = cur->next) {
      printf("\t%s\n", cur->string);
   }
}

void
PrintTestInfo
(
   TestInfo *pTestInfo
)
{
   StringList * GetNameDataPairs(Xml::Node * node);

   for(int i=0;i < _TIK_COUNT; i++) {
      StringList* pStringList = NULL;
      if ((i == TIK_ENV) && pTestInfo->data[TIK_ENV]) {
         pStringList = GetNameDataPairs((Xml::Node*)pTestInfo->data[TIK_ENV]);
         if (pStringList) {
            for(; pStringList != NULL; pStringList = pStringList->next->next) {
               ASSERT(pStringList->next);
               if (pStringList->next->string[0]=='%') {
                  char tmpBuf[BUFFER_SIZE];
                  strncpy_s(tmpBuf, pStringList->next->string+1, strlen(pStringList->next->string)-2);
                  tmpBuf[strlen(pStringList->next->string)-2]=0;
                  if (getenv_unsafe(tmpBuf)==NULL) {
                     char msgBuf[BUFFER_SIZE];
                     sprintf_s(msgBuf, "%s environment variable used is not set\n", tmpBuf);
                     Fatal(msgBuf);
                  } else {
                     printf("\t%s=%s\n",pStringList->string, getenv_unsafe(tmpBuf));
                  }
               } else {
                  printf("\t%s=%s\n",pStringList->string, pStringList->next->string);
               }
            }
         }
      }else if (pTestInfo->data[i]) {
         printf("\t%s\n", pTestInfo->data[i]);
      }
   }
}

// Display the file list.
void
DumpTestList
(
   TestList * pTestList
)
{
   for (Test * pTest = pTestList->first; pTest != NULL; pTest = pTest->next)
   {
      printf("Files:\n");
      PrintStringList(pTest->files);
      printf("\n");
      if (pTest->fullPath)
         printf("fullpath (%s):\n", pTest->fullPath);

      for(ConditionNodeList * conditionNodeList = pTest->conditionNodeList;
         conditionNodeList != NULL; conditionNodeList=conditionNodeList->next)
      {
         printf("Condition nodes\n");
         conditionNodeList->node->Dump();
      }

      printf("Default testinfo\n");
      PrintTestInfo(&pTest->defaultTestInfo);
      int i=0;
      for(TestVariant * variants=pTest->variants; variants!=nullptr; variants=variants->next) {
            printf("variant %d\n", i++);
            PrintTestInfo(&variants->testInfo);
            printf("opt flag %s\n", variants->optFlags);
      }

      printf("\n\n");
   }
}

#endif


void
PadSpecialChars
(
   char * strOut,
   const char * strIn
)
{
   int i = 0;
   while (*strIn) {
      strOut[i++] = *strIn++;
      if (strOut[i-1] == '\\')
         strOut[i++] = '\\';
   }
   strOut[i] = '\0';
}

// given an xml node, returns the name-data StringList pairs for all children
StringList * GetNameDataPairs
(
   Xml::Node * node
)
{
  ASSERT(node->ChildList != NULL);
  StringList *pStringList = NULL;
   for (Xml::Node *ChildNode = node->ChildList; ChildNode != NULL; ChildNode = ChildNode->Next) {
      pStringList = AddToStringList(pStringList, ChildNode->Name);
      pStringList = AddToStringList(pStringList, ChildNode->Data);
   }
   return pStringList;
}

// create the file env.lst
void
WriteEnvLst
(
   Test * pDir, TestList * pTestList
)
{
   char comments[BUFFER_SIZE];
   char envlst[BUFFER_SIZE];
   char noSpecialCharsBuf[BUFFER_SIZE];

   ASSERT(pDir->fullPath);

   sprintf_s(envlst, "%s%s", pDir->fullPath, "\\" DEFAULT_ENVLST_CFG);

   DeleteFileIfFound(envlst);
   COutputBuffer *LstFilesOut = new COutputBuffer(envlst, FSyncImmediate ? false : true);
   ASSERT(LstFilesOut);

   for (Test * pTest = pTestList->first; pTest != NULL; pTest = pTest->next)
   {
      for(TestVariant * variants=pTest->variants; variants!=NULL; variants=variants->next) {
         // print the tags first
         if (variants->testInfo.data[TIK_TAGS]) {
            LstFilesOut->Add("%s\t", variants->testInfo.data[TIK_TAGS]);
         } else if (pTest->defaultTestInfo.data[TIK_TAGS]){
            LstFilesOut->Add("%s\t", pTest->defaultTestInfo.data[TIK_TAGS]);
         } else {
            LstFilesOut->Add("\t");
         }
         LstFilesOut->Add("TESTNAME=%s", pTest->name);
         strcpy_s(comments, pTest->name);

         PadSpecialChars(noSpecialCharsBuf, EXTRA_CC_FLAGS);
         if (noSpecialCharsBuf[0] != '\0') {
            LstFilesOut->Add(" EXTRA_CC_FLAGS=\"%s\"", noSpecialCharsBuf);
         }
         LstFilesOut->Add(" REGR_CL=%s", REGR_CL);
         LstFilesOut->Add(" LINKER=%s", LINKER);
         PadSpecialChars(noSpecialCharsBuf, LINKFLAGS);
         if (noSpecialCharsBuf[0] != '\0') {
            LstFilesOut->Add(" LINKFLAGS=\"%s\"", noSpecialCharsBuf);
         }

         if (pTest->files) {
            LstFilesOut->Add(" FILES=\"");
            StringList* pStringList = pTest->files;
            for(;pStringList != NULL;pStringList=pStringList->next) {
               LstFilesOut->Add("%s ", pStringList->string);
            }
            LstFilesOut->Add("\"");
         }

         // how we translate testinfo into env.lst specific stuffs
         const char * const TestInfoEnvLstFmt[] =
         {
             " TESTFILE=\"%s\"",
             " BASELINE=\"%s\"",
             " CFLAGS=\"%s\"",
             " LFLAGS=\"%s\"",
             NULL,
             NULL,
             NULL,
             NULL,
             NULL
         };

         static_assert((sizeof(TestInfoEnvLstFmt) / sizeof(TestInfoEnvLstFmt[0])) == TestInfoKind::_TIK_COUNT, "Fix the buffer size");

         // print the other TIK_*
         for(int i=0;i < _TIK_COUNT; i++) {
            if (variants->testInfo.data[i] && TestInfoEnvLstFmt[i]) {
               LstFilesOut->Add(TestInfoEnvLstFmt[i], variants->testInfo.data[i]);
            } else if (pTest->defaultTestInfo.data[TIK_RL_DIRECTIVES]) {
               // we only handle NoGPF directive, passFo and NoAsm not used
               if (SuppressNoGPF(pTest)) {
               LstFilesOut->Add(" NOGPF=1");
               }
            }
         }
         LstFilesOut->Add(" optFlags=\"%s\"", variants->optFlags);
         strcat_s(comments, " "); strcat_s(comments, variants->optFlags);

         // print the env settings
         StringList* pStringList = NULL;
         if (variants->testInfo.data[TIK_ENV]) {
            pStringList = GetNameDataPairs((Xml::Node*)variants->testInfo.data[TIK_ENV]);
            if (pStringList) {
               // assuming even number of elements
               for(; pStringList != NULL; pStringList = pStringList->next->next) {
                  ASSERT(pStringList->next);
                  if (pStringList->next->string[0]=='%') {
                     // grab the env variable specified in the pStringList->next->string
                     char tmpBuf[BUFFER_SIZE];
                     strncpy_s(tmpBuf, pStringList->next->string+1, strlen(pStringList->next->string)-2);
                     tmpBuf[strlen(pStringList->next->string)-2]=0;
                     if (getenv_unsafe(tmpBuf)==NULL) {
                        char msgBuf[BUFFER_SIZE];
                        sprintf_s(msgBuf, "%s environment variable used in %s not set\n", tmpBuf, envlst);
                        Fatal(msgBuf);
                     } else {
                        LstFilesOut->Add(" %s=%s",pStringList->string, getenv_unsafe(tmpBuf));
                        sprintf_s(comments, "%s %s=%s", comments, pStringList->string, getenv_unsafe(tmpBuf));
                     }
                  } else {
                     LstFilesOut->Add(" %s=%s",pStringList->string, pStringList->next->string);
                     sprintf_s(comments, "%s %s=%s", comments, pStringList->string, pStringList->next->string);
                  }
               }
               FreeStringList(pStringList);
            }
         }
         LstFilesOut->Add(" # %s\n", comments);
      }
   }

   LstFilesOut->Flush();
   delete LstFilesOut;
   printf("  %s\n", envlst);
}

BOOL
IsRelativePath(
   char *path
)
{
   char drive[MAX_PATH], dir[MAX_PATH];

   _splitpath_s(path, drive, ARRAYLEN(drive), dir, ARRAYLEN(dir), NULL, 0, NULL, 0);

   // Path is relative if neither drive nor absolute directory are specified.

   if (!*drive && !IS_PATH_CHAR(dir[0]))
      return TRUE;

   return FALSE;
}


BOOL
VerifyOrCreateDir(
   char *name,
   BOOL fCreate
)
{
   if (FTest)
   {
      return TRUE;
   }

   DWORD attrib;

   attrib = GetFileAttributes(name);

   // Already exists?

   if (attrib != INVALID_FILE_ATTRIBUTES) {

      // Make sure it's a directory.

      if (!(attrib & FILE_ATTRIBUTE_DIRECTORY)) {
         LogError("%s exists but is not a subdirectory", name);
         return FALSE;
      }
   }

   // Are we allowed to create.

   else if (!fCreate) {
      LogError("%s doesn't exist", name);
      return FALSE;
   }

   // Try to create it.

   else if (!CreateDirectory(name, NULL)) {
      LogError("Unable to create %s", name);
      return FALSE;
   }

   else {
      Warning("Created '%s' directory", name);
   }

   return TRUE;
}

// Create MASTER_DIR and DIFF_DIR if non-existent.
BOOL
CreateAsmDirs(
   char* root,
   BOOL fBaseline
)
{
   char path[_MAX_PATH];

   ASSERTNR(!IsRelativePath(root));

   sprintf_s(path, "%s\\%s", root, MASTER_DIR);

   if (!VerifyOrCreateDir(path, fBaseline))
      return FALSE;

   if (FMoveDiffs && FDiff) {
      sprintf_s(path, "%s\\%s", root, DIFF_DIR);
      if (!VerifyOrCreateDir(path, TRUE))
         return FALSE;
   }

   return TRUE;
}

void
PrintDirs(
   void
)
{
   Test * pDir;

   printf("Regression directories:");
   for (pDir = DirList.first; pDir; pDir = pDir->next) {
      printf(" %s", pDir->name);
   }
   printf("\n\n");

   if (ExcludeDirList.first != NULL) {
      printf("Regression exclude directories:");
      for (pDir = ExcludeDirList.first; pDir; pDir = pDir->next) {
         printf(" %s", pDir->name);
      }
      printf("\n\n");
   }
}

char *
GetNonEmptyArg(
   char *arg
)
{
   if (arg && (*arg == '\0'))
      return NULL;

   return arg;
}

char *
ComplainIfNoArg(
   char *opt,
   char *arg
)
{
   if ((arg == NULL) || (*arg == '\0'))
      Fatal("%s requires an argument", opt);

   return arg;
}

// Parse a single command line argument.  Returns 1 if -args processed.
// Returns -1 if a non-switch was processed and pTestList is NULL.
int
ParseArg(
   char *arg,
   TestList * pTestList
)
{
   char tempBuf[BUFFER_SIZE];
   char *s;

   switch (arg[0]) {
      case '-':
      case '/':
         s = strchr(arg, ':');
         if (s)
            *s++ = '\0';

         if (!_stricmp(&arg[1], "exe")) {
            Mode = RM_EXE;
            ExeCompiler = GetNonEmptyArg(s);
            break;
         }
         if (!_stricmp(&arg[1], "asm")) {
            Mode = RM_ASM;
            ExeCompiler = GetNonEmptyArg(s);
            break;
         }
         if (!_stricmp(&arg[1], "log")) {
            LogName = ComplainIfNoArg(arg, s);
            break;
         }
         if (!_stricmp(&arg[1], "full")) {
            FullLogName = ComplainIfNoArg(arg, s);
            break;
         }
         if (!_stricmp(&arg[1], "results")) {
            ResultsLogName = ComplainIfNoArg(arg, s);
            break;
         }
         // For backward-compatibility, support -tee
         if (!_stricmp(&arg[1], "tee")) {
            Warning("-tee is deprecated; use -full instead");
            FullLogName = ComplainIfNoArg(arg, s);
            break;
         }
         if (!_stricmp(&arg[1], "all")) {
            FUserSpecifiedDirs = FALSE;
            break;
         }
         if (!_stricmp(&arg[1], "dcfg")) {
            DCFGfile = ComplainIfNoArg(arg, s);
            break;
         }
         if (!_stricmp(&arg[1], "cfg")) {
            CFGfile = ComplainIfNoArg(arg, s);
            break;
         }
         if (!_stricmp(&arg[1], "genlst")) {
            FGenLst = TRUE;
            Mode = RM_EXE;
            break;
         }
         // Note: we support "-dir:" for backwards-compatibility
         if (!_stricmp(&arg[1], "dir") || !_stricmp(&arg[1], "dirs")) {
            AddUserDirs(&DirList, ComplainIfNoArg(arg, s));
            break;
         }
         if (!_stricmp(&arg[1], "nodirs")) {
            FExcludeDirs = TRUE;
            AddUserDirs(&ExcludeDirList, ComplainIfNoArg(arg, s));
            break;
         }
         if (!_stricmp(&arg[1], "resume")) {
            ResumeDir = ComplainIfNoArg(arg, s);
            FUserSpecifiedDirs = FALSE;
            FAppend = TRUE;
            break;
         }
         if (!_stricmp(&arg[1], "match")) {
            MatchDir = ComplainIfNoArg(arg, s);
            FUserSpecifiedDirs = FALSE;
            break;
         }
         if (!_stricmp(&arg[1], "status")) {

            // -status:- disables.

            if (s && !strcmp(s, "-")) {
               FStatus = FALSE;
            }
            else {
               FStatus = TRUE;
               StatusPrefix = s;
            }
            break;
         }
         if (!_stricmp(&arg[1], "statusformat")) {
            s = ComplainIfNoArg(arg, s);
            StatusFormat = _strdup(s);
            break;
         }
         if (!_stricmp(&arg[1], "rlfe")) {
            FRLFE = TRUE;
            if (s)
               RLFEOpts = s;
            break;
         }
         if (!_stricmp(&arg[1], "verbose")) {
            FVerbose = TRUE;
            break;
         }
         if (!_stricmp(&arg[1], "quiet")) {
            FQuiet = TRUE;
            break;
         }
         if (!_stricmp(&arg[1], "summary") ||
            !_stricmp(&arg[1], "nodirname") ||
            !_stricmp(&arg[1], "movediffs"))
         {
            Warning("%s is deprecated; it is now the default\n", arg);
            break;
         }
         if (!_stricmp(&arg[1], "nosummary")) {
            FSummary = FNoDirName = FALSE;
            break;
         }
         if (!_stricmp(&arg[1], "allowpopup")) {
            FNogpfnt = FALSE;
            break;
         }
         if (!_stricmp(&arg[1], "append")) {
            FAppend = TRUE;
            break;
         }
         if (!_stricmp(&arg[1], "appendtestnametoextraccflags")) {
            FAppendTestNameToExtraCCFlags = TRUE;
            break;
         }
         if (!_stricmp(&arg[1], "singlethreadperdir")) {
            FSingleThreadPerDir = TRUE;
            break;
         }
         if (!_stricmp(&arg[1], "dirname")) {
            FNoDirName = FALSE;
            break;
         }
         if (!_stricmp(&arg[1], "nomovediffs")) {
            FNoMoveDiffsSwitch = TRUE;
            break;
         }
         if (!_stricmp(&arg[1], "nowarn")) {
            FNoWarn = TRUE;
            break;
         }
         if (!_stricmp(&arg[1], "low")) {
            FLow = TRUE;
            break;
         }
         if (!_stricmp(&arg[1], "test")) {
            FTest = TRUE;
            break;
         }
         if (!_stricmp(&arg[1], "exeflags")) {
            EXEC_TESTS_FLAGS = ComplainIfNoArg(arg, s);
            break;
         }
         if (!_stricmp(&arg[1], "target")) {
            TARGET_MACHINE = ComplainIfNoArg(arg, s);

            // Some test scripts use TARGET_MACHINE, so put it into the
            // environment.

            sprintf_s(tempBuf, "TARGET_MACHINE=%s", TARGET_MACHINE);
            if (_putenv(tempBuf))
               Fatal("Couldn't set TARGET_MACHINE");
            break;
         }
         if (!_stricmp(&arg[1], "os")) {
            TARGET_OS_NAME = ComplainIfNoArg(arg, s);
            break;
         }
         if (!_stricmp(&arg[1], "rltarget")) {
            RL_MACHINE = ComplainIfNoArg(arg, s);
            break;
         }
         if (!_stricmp(&arg[1], "threads")) {
            char* temp;
            temp = ComplainIfNoArg(arg, s);
            NumberOfThreads = atoi(temp);
            if (NumberOfThreads == 0 || NumberOfThreads > MAX_ALLOWED_THREADS) {
               Fatal("Illegal # of threads; must be between 1 and %d", MAX_ALLOWED_THREADS);
            }
            break;
         }
         if (!_stricmp(&arg[1], "regress")) {
            REGRESS = ComplainIfNoArg(arg, s);

            // Some test scripts use REGRESS, so put it into the
            // environment.

            sprintf_s(tempBuf, "REGRESS=%s", REGRESS);
            if (_putenv(tempBuf))
               Fatal("Couldn't set REGRESS");
            break;
         }
         if (!_stricmp(&arg[1], "sync")) {
            s = ComplainIfNoArg(arg, s);
            if (!_stricmp(s, "immediate"))
               FSyncImmediate = TRUE;
            else if (!_stricmp(s, "variation"))
               FSyncVariation = TRUE;
            else if (!_stricmp(s, "test"))
               FSyncTest = TRUE;
            else if (!_stricmp(s, "dir"))
               FSyncDir = TRUE;
            else
               Fatal("Unknown -sync option");
            break;
         }
         if (!_stricmp(&arg[1], "binary")) {
             s = ComplainIfNoArg(arg, s);
             JCBinary = _strdup(s);
             break;
         }
         if (!_stricmp(&arg[1], "nothreadid")) {
            FNoThreadId = TRUE;
            break;
         }
         // We support "-baseline", but we only document "-base". Maybe we
         // should deprecate it and emit a warning message...
         if (!_stricmp(&arg[1], "baseline") || !_stricmp(&arg[1], "base")) {
            FBaseline = TRUE;
            BaseCompiler = GetNonEmptyArg(s);
            break;
         }
         if (!_stricmp(&arg[1], "rebase")) {
             FRebase = TRUE;
             break;
         }
         if (!_stricmp(&arg[1], "diff")) {
            DiffCompiler = GetNonEmptyArg(s);
            FDiff = TRUE;
            break;
         }
         if (!_stricmp(&arg[1], "rellog")) {
            FRelativeLogPath = TRUE;
            break;
         }
         if (!_stricmp(&arg[1], "nodelete")) {
            FNoDelete = TRUE;
            break;
         }
         if (!_stricmp(&arg[1], "copyonfail")) {
            FCopyOnFail = TRUE;
            break;
         }
         if (!_stricmp(&arg[1], "time")) {
            s = ComplainIfNoArg(arg, s);
            if (!_stricmp(s, "variation"))
               Timing |= TIME_VARIATION;
            else if (!_stricmp(s, "test"))
               Timing |= TIME_TEST;
            else if (!_stricmp(s, "dir"))
               Timing |= TIME_DIR;
            else if (!_stricmp(s, "all"))
               Timing |= TIME_ALL;
            else
               Fatal("Unknown -time option");
            break;
         }

         if ((!_stricmp(&arg[1], "nottags")) || (!_stricmp(&arg[1], "tags"))) {
            s = ComplainIfNoArg(arg, s);
            AddTagToTagsList(&TagsList, &TagsLast, s, strcmp(&arg[1], "tags") == 0);

            break;
         }

         if ((!_stricmp(&arg[1], "dirnottags")) || (!_stricmp(&arg[1], "dirtags"))) {
            s = ComplainIfNoArg(arg, s);
            AddTagToTagsList(&DirectoryTagsList, &DirectoryTagsLast, s, strcmp(&arg[1], "dirtags") == 0);

            break;
         }

#ifndef NODEBUG
         if (!_stricmp(&arg[1], "debug")) {
            FDebug = FVerbose = TRUE;
            break;
         }
         if (!_stricmp(&arg[1], "syncdirs")) {
            FSyncEnumDirs = TRUE;
            break;
         }
#endif

         switch (arg[1]) {
            case '?':
            case 'h':
            case 'H':
               switch (arg[2]) {
                  case '\0':
                     Usage(FALSE);
                     exit(0);

                  case 'x':
                     Usage(TRUE);
                     exit(0);
               }

               // FALL-THROUGH

            default:
               Fatal("Unrecognized option: %s", arg);
         }

      default:
         if (pTestList == NULL)
            return -1;

         FUserSpecifiedFiles = TRUE;
         AddToTestList(pTestList, arg);
   }

   return 0;
}

void
AddTagToTagsList(
    Tags** pTagsList,
    Tags** pTagsLast,
    const char* str,
    BOOL fInclude
)
{
    if (pTagsList == NULL || pTagsLast == NULL)
    {
        return;
    }

    Tags* tags = new Tags;

    tags->str = str;
    tags->fInclude = fInclude;
    tags->next = NULL;

    if (*pTagsLast)
    {
        (*pTagsLast)->next = tags;
    }
    else
    {
        (*pTagsList) = tags;
    }

    (*pTagsLast) = tags;
}

// Determines by tags if we should include a given test.
//
// Priority:
//  1. -nottags. Check the full tags list. If we have any exclude tag, reject the test.
//  2. -tags. If #1 didn't reject the test, we only accept the test if
//      a. There is no include tag,
//      b. Or we matched an include tag, or "mustMatchIncludeTag" is false.
//
bool
ShouldIncludeTest(
    Tags* pTagsList,
    TestInfo* testInfo,
    bool mustMatchIncludeTag = true // false when matching "-tags" on a folder. (The folder may contain tests matching the tags, although itself doesn't.)
)
{
    // Automatically append tag "html" to any html tests
    bool isHtmlTest = false;
    if (testInfo->data[TIK_FILES])
    {
        const char* ext = GetFilenameExt(testInfo->data[TIK_FILES]);
        isHtmlTest = _stricmp(ext, ".html") == 0 || _stricmp(ext, ".htm") == 0;
    }

    bool hasIncludeTag = false;                     // if we have "-tags:" filter
    bool matchedIncludeTag = !mustMatchIncludeTag;  // if the test has matched an include tag

    for (Tags* tags = pTagsList; tags != NULL; tags = tags->next)
    {
        if (matchedIncludeTag && tags->fInclude)
        {
            continue; // Already matched one include tag, skip further include tags. Continue to check if we have any exclude tag.
        }

        bool hasTag = HasInfoList(testInfo->data[TIK_TAGS], XML_DELIM, tags->str, XML_DELIM, false)
            || (isHtmlTest && HasInfo(tags->str, XML_DELIM, "html"));

        if (hasTag && !tags->fInclude)
        {
            return false; // #1: Reject, we have an exclude tag.
        }

        if (tags->fInclude)
        {
            hasIncludeTag = true;
            matchedIncludeTag |= hasTag;
        }
    }

    return !hasIncludeTag || matchedIncludeTag;
}

void
ParseEnvVar(
   char *envVar
)
{
   char * s;
   char * context;

   s = getenv_unsafe(envVar);
   if (s == NULL)
      return;

   s = _strdup(s);

   s = strtok_s(s, " \"", &context);
   while (s) {
      switch (ParseArg(s, NULL)) {
         case 1:
            Fatal("-args may not appear in %s environment variable",
               envVar);
         case -1:
            Fatal("Only switches may appear in %s environment variable",
               envVar);
      }
      s = strtok_s(NULL, " \"", &context);
   }
}

// Parse the command line and environment variable.
void
ParseCommandLine(
   int argc,
   char *argv[],
   TestList * pTestList
)
{
   char *s;
   int i;
   char tempBuf[BUFFER_SIZE];

   ProgramName = argv[0];

   ParseEnvVar(RL_PRE_ENV_VAR);

   for (i = 1; i < argc; i++) {
      ParseArg(argv[i], pTestList);
   }

   ParseEnvVar(RL_POST_ENV_VAR);

   // Get the REGRESS environment variable.

   if (REGRESS == NULL)
      REGRESS = getenv_unsafe("REGRESS");
   if (REGRESS == NULL)
      Fatal("REGRESS environment variable not set");

   // Use "rl.log" as the default log file, and rl.full.log as the default
   // full log file. We make sure the log file is a full path. If the user
   // specified "-all", then the logs go in %REGRESS%\logs. Otherwise, they
   // go in the current directory.

   if (LogName == NULL) {
      // The user didn't specify a log filename, so create one.

      if (!FUserSpecifiedDirs || (DirList.first != NULL)) {
         char tempBuf[MAX_PATH];

         strcpy_s(tempBuf, REGRESS);
         strcat_s(tempBuf, "\\logs\\");
         strcat_s(tempBuf, DEFAULT_LOG_FILE);
         LogName = _strdup(tempBuf);
      } else {
         LogName = DEFAULT_LOG_FILE;
      }
   }

   if (FullLogName == NULL) {
      // The user didn't specify a full log filename, so create one.

      if (!FUserSpecifiedDirs || (DirList.first != NULL)) {
         char tempBuf[MAX_PATH];

         strcpy_s(tempBuf, REGRESS);
         strcat_s(tempBuf, "\\logs\\");
         strcat_s(tempBuf, DEFAULT_FULL_LOG_FILE);
         FullLogName = _strdup(tempBuf);
      } else {
         FullLogName = DEFAULT_FULL_LOG_FILE;
      }
   }

   if (ResultsLogName == NULL) {
      // The user didn't specify a results log filename, so create one.

      if (!FUserSpecifiedDirs || (DirList.first != NULL)) {
         char tempBuf[MAX_PATH];

         strcpy_s(tempBuf, REGRESS);
         strcat_s(tempBuf, "\\logs\\");
         strcat_s(tempBuf, DEFAULT_RESULTS_LOG_FILE);
         ResultsLogName = _strdup(tempBuf);
      } else {
         ResultsLogName = DEFAULT_RESULTS_LOG_FILE;
      }
   }

   // If the files already exist, then delete them. Otherwise, we'll simply
   // keep appending to them, which is not usually desired.

   if (!FAppend) {
      DeleteFileIfFound(LogName);
      DeleteFileIfFound(FullLogName);
      DeleteFileIfFound(ResultsLogName);
   }

   // Now we should have a log name, so regenerate the log output objects
   delete ThreadOut; // flush what we've got...
   ThreadOut = new COutputBuffer(FQuiet ? (FILE*)NULL : stdout, FSyncImmediate ? false : true);
   ThreadLog = new COutputBuffer(LogName, FSyncImmediate ? false : true);
   ThreadFull = new COutputBuffer(FullLogName, FSyncImmediate ? false : true);
   ThreadRes = new COutputBuffer(ResultsLogName, FSyncImmediate ? false : true);

   // Check for missing/conflicting items.

   if (!FRelativeLogPath
      && IsRelativePath(LogName)
      && (!FUserSpecifiedDirs || (DirList.first != NULL)))
   {
      Fatal("Log file '%s' specifies a relative path name\n"
         "If this was intended, add -rellog to the command line",
         LogName);
   }

   if (!FRelativeLogPath
      && IsRelativePath(FullLogName)
      && (!FUserSpecifiedDirs || (DirList.first != NULL)))
   {
      Fatal("Log file '%s' specifies a relative path name\n"
         "If this was intended, add -rellog to the command line",
         FullLogName);
   }

   if (!FUserSpecifiedDirs && (DirList.first != NULL))
      Fatal("Specify all directories or explicit directories, not both");

   if (FUserSpecifiedFiles && (DirList.first != NULL))
      Fatal("Specify either a directory or files, not both");

   if (ResumeDir && (DirList.first != NULL))
      Fatal("Specify explicit directories or a resume point, not both");

   if (MatchDir && (DirList.first != NULL))
      Fatal("Specify explicit directories or a matching dir, not both");

   if ((ResumeDir != NULL) && (MatchDir != NULL))
      Fatal("Specify resume point or a matching dir, not both");

   if (FBaseline) {
      if (Mode != RM_ASM)
         Fatal("-base only supported for asm tests");

      if (FDiff) {
         if ((BaseCompiler == NULL) || (DiffCompiler == NULL))
            Fatal("Must specify compilers for both -base and -diff");
      }
   }
   else if (FDiff) {
      if (Mode != RM_ASM)
         Fatal("-diff only supported for asm tests");
   }
   else if (Mode == RM_ASM) {
      FDiff = TRUE;
   }

   if (FNoMoveDiffsSwitch && (Mode == RM_EXE))
      Fatal("-nomovediffs and -exe are incompatible options");

   if (FNoMoveDiffsSwitch && !FDiff)
      Fatal("-nomovediffs requires -diff");

   FBaseDiff = FBaseline && FDiff;
   FMoveDiffs = !FNoMoveDiffsSwitch;

   ASSERTNR((Mode != RM_ASM) || FBaseline || FDiff);
   ASSERTNR(FBaseline || !BaseCompiler);
   ASSERTNR(FDiff || !DiffCompiler);
   ASSERTNR(!FBaseDiff || BaseCompiler);
   ASSERTNR(!FBaseDiff || DiffCompiler);

   // Now that we have parsed a valid command line, get the environment
   // settings.

   GetEnvironment();

   // Prepend target-specific nottags, if any.

   if (TargetInfo[TargetMachine].NotTags != NULL)
   {
       AddTagToTagsList(&TagsList, &TagsLast, _strdup(TargetInfo[TargetMachine].NotTags), false);
   }

   // -nodelete can only be used with EXE when one set of test options is
   // specified.

   if (FNoDelete) {
      if (Mode != RM_EXE)
         Fatal("-nodelete may only be used with -exe");

      if (Mode == RM_EXE)
      {
         int numTestOptions = 0;

         char * env = EXEC_TESTS_FLAGS;
         while (env) {
            env = strchr(env, ';');
            if (env)
               ++env;
            numTestOptions++;
         }

         if (numTestOptions != 1)
            Fatal("-nodelete only allows one option in EXEC_TESTS_FLAGS");
      }
   }

   // Generate unspecified options.

   if (StatusPrefix == NULL) {
      s = tempBuf + sprintf_s(tempBuf, "%s [%s",
         Mode == RM_ASM
         ? FBaseDiff
         ? "Base/Diff"
         : FBaseline
         ? "Baselines"
         : "Diffs"
         : "Exec",
         TargetInfo[TargetMachine].name);
      if (RLMachine != TargetMachine)
         s += sprintf_s(s, REMAININGARRAYLEN(tempBuf, s), ":%s", TargetInfo[RLMachine].name);
      sprintf_s(s, REMAININGARRAYLEN(tempBuf, s), "]");
   }
   else {
      strcpy_s(tempBuf, StatusPrefix);
   }
   StatusPrefix = _strdup(tempBuf);

   if (StatusFormat == NULL) {
      if (FDiff)
         StatusFormat = "%d diffs, %f failures, %t/%T (%p%%)";
      else
         StatusFormat = "%f failures, %t/%T (%p%%)";
   }
   s = tempBuf + strlen(tempBuf);
   *s++ = ' ';
   strcpy_s(s, REMAININGARRAYLEN(tempBuf, s), StatusFormat);
   StatusFormat = _strdup(tempBuf);

   if (DCFGfile == NULL) {
      sprintf_s(tempBuf, "%s\\%s", REGRESS,
         Mode == RM_ASM ? DEFAULT_ASM_DCFG : DEFAULT_EXE_DCFG);
      DCFGfile = _strdup(tempBuf);
   }

   if (CFGfile == NULL) {
      CFGfile = Mode == RM_ASM
         ? DEFAULT_ASM_CFG
         : DEFAULT_EXE_CFG;
   }

   if (CMDfile == NULL) {
      CMDfile = (Mode == RM_ASM)
         ? DEFAULT_ASM_CMD
         : DEFAULT_EXE_CMD;
   }

   // Warn if doing multiple dirs and not outputting directory name or
   // summary info.

   if (FNoDirName && !FSummary &&
      (!FUserSpecifiedDirs || (DirList.first != DirList.last))) {
      Warning("-nodirname without -summary will make for a hard to read log file");
   }

   if (FVerbose) {
      printf("Perform %s regression, appending output to %s\n",
         ModeNames[Mode], LogName);
   }

   // If no directories specified, use current directory.

   if (FUserSpecifiedDirs && (DirList.first == NULL))
      AddCurrentDir();

   // Set process priority to LOW if requested.

   if (FLow) {
      SetPriorityClass(GetCurrentProcess(), IDLE_PRIORITY_CLASS);
   }

   // Initialize parallel data

   if (NumberOfThreads == 0) {
      SYSTEM_INFO SystemInfo;
      GetSystemInfo(&SystemInfo);
      NumberOfThreads = SystemInfo.dwNumberOfProcessors;
   }

   if (FVerbose) {
      printf("Using %d threads\n", NumberOfThreads);
   }

   // Test synchronization switches. Only one -sync variant can be specified.
   // If none is specified, we use the default: FSyncVariation for
   // multi-threaded operation, and FSyncImmediate for single-threaded.

   if (FSyncImmediate + FSyncVariation + FSyncTest + FSyncDir == 0) {
      if (NumberOfThreads == 1)
         FSyncImmediate = TRUE;
      else
         FSyncVariation = TRUE;
   }

   if (FSyncImmediate + FSyncVariation + FSyncTest + FSyncDir != TRUE)
      Fatal("-sync options are mutually exclusive");

   if (FRLFE) {
      FSyncEnumDirs = TRUE;

      if (!FSyncVariation) {
         printf("-rlfe requires -sync:variation (overriding your command line)\n");
         FSyncImmediate = FSyncTest = FSyncDir = FALSE;
         FSyncVariation = TRUE;
      }
   }
}

// Find a file in the file list with compileFlags corresponding to the passed
// in ones.
Test *
FindTest(
   TestList * pTestList,
   char * testName,
   BOOL fUserSpecified,
   TestInfo * testInfo

)
{
   Test * pTest;
   Test * pMatch;

   pTest = pTestList->first;
   pMatch = NULL;

   while (pTest)
   {
      if (_stricmp(pTest->name, testName) == 0)
      {
         pMatch = pTest;

         // If we have a new user test, or a match on the compileFlags and
         // tags, break.

         if ((fUserSpecified && (pTest->files == NULL)) ||
            (!mystrcmp(pTest->defaultTestInfo.data[TIK_COMPILE_FLAGS],
               testInfo->data[TIK_COMPILE_FLAGS]) &&
               !mystrcmp(pTest->defaultTestInfo.data[TIK_BASELINE],
                  testInfo->data[TIK_BASELINE]) &&
               !mystrcmp(pTest->defaultTestInfo.data[TIK_TAGS],
                  testInfo->data[TIK_TAGS])))
         {
            break;
         }
      }

      pTest = pTest->next;
   }

   // If we saw a match but didn't accept it and the user is specifying which
   // tests to run and this is a Pogo test, explicitly add it.

   if (fUserSpecified
      && (Mode == RM_EXE)
      && (pTest == NULL)
      && (pMatch != NULL)
      && HasInfo(testInfo->data[TIK_TAGS], XML_DELIM, "Pogo"))
   {
      StringList * dupList = NULL;
      StringList * p;

      for (p = pMatch->files; p != NULL; p = p->next)
      {
         dupList = AddToStringList(dupList, p->string);
      }

      pTest = AddToTestList(pTestList, dupList);
   }

   return pTest;
}

BOOL
IsTimeoutStringValid(char *strTimeout) {
   char *end;
   _set_errno(0);

   unsigned long secTimeout = strtoul(strTimeout, &end, 10);

   if (errno != 0 || *end != 0) {
      return FALSE;
   }

   // Check to see if the value is too large and would cause overflow

   // Do the multiplication using 64-bit unsigned math.
   unsigned __int64 millisecTimeout = 1000ui64 * static_cast<unsigned __int64>(secTimeout);

   // Does the result fit in 32-bits?
   if (millisecTimeout >= (1ui64 << 32)) {
      return FALSE;
   }

   return TRUE;
}

BOOL
GetTestInfoFromNode
(
   const char * fileName,
   Xml::Node * node,
   TestInfo *  testInfo
)
{
   if (node == NULL)
   {
      return TRUE;
   }

   for (int i = 0; i < _TIK_COUNT; i++)
   {
      Xml::Node * childNode = node->GetChild(TestInfoKindName[i]);
      if (childNode != NULL)
      {
         testInfo->hasData[i] = TRUE;
         if (i == TIK_ENV)
         {
            ASSERT(childNode->ChildList != NULL);
            testInfo->data[i] = (char*)childNode;
         }
         else
         {
            if (childNode->ChildList != NULL)
            {
               CFG_ERROR_EX(fileName, node->LineNumber,
               "Expected data, not child list\n", NULL);
               childNode->Dump();
               return FALSE;
            }

            if (childNode->Data != NULL && childNode->Data[0] != '\0')
            {
               testInfo->data[i] = childNode->Data;
            }
            else
            {
               testInfo->data[i] = NULL;
            }

            if (i == TIK_TIMEOUT)
            {
               // Validate the timeout string now to fail early so we don't run any tests when there is an error.
               if (!IsTimeoutStringValid(testInfo->data[i])) {
                  CFG_ERROR_EX(fileName, node->LineNumber, 
                     "Invalid timeout specified. Cannot parse or too large.\n", NULL);
                  childNode->Dump();
                  return FALSE;
               }
            }
         }
      }
   }

   return TRUE;
}

BOOL
AddAsmVariants
(
   Test * pTest,
   TestInfo * defaultInfo,
   ConditionNodeList * cnl
)
{
   // Asm configurations are simple; we simply apply all the conditions to
   // the variant info.

   // Create a variant and inherit the default info.

   TestInfo variantInfo;
   memcpy(&variantInfo, defaultInfo, sizeof(TestInfo));

   while (cnl != NULL)
   {
      ConditionNodeList * next = cnl->next;

      if (!GetTestInfoFromNode(CFGfile, cnl->node, &variantInfo))
      {
         return FALSE;
      }

      delete cnl;

      cnl = next;
   }

   ASSERTNR(pTest->variants == NULL);

   TestVariant * pTestVariant = new TestVariant;
   memcpy(&pTestVariant->testInfo, &variantInfo, sizeof(TestInfo));

   pTest->variants = pTestVariant;

   return TRUE;
}

BOOL
AddExeVariants
(
   Test * pTest,
   TestInfo * defaultInfo,
   ConditionNodeList * cnl
)
{
   // Exe configurations require more work since we have a list of optimization
   // flags to deal with.

   TestVariant ** ppLastVariant = &pTest->variants;

   while (*ppLastVariant != NULL)
   {
      ppLastVariant = &(*ppLastVariant)->next;
   }

   char ** optFlagsArray;

   // Decide which list to use depending on the tag.
   optFlagsArray = IsPogoTest(pTest)
      ? PogoOptFlags
      : OptFlags
      ;

   // For each optimization flag set, see if the conditional applies.
   for (int i = 0; optFlagsArray[i] != NULL; i++)
   {
      // Create a variant and inherit the default info.
      TestInfo variantInfo;
      memcpy(&variantInfo, defaultInfo, sizeof(TestInfo));

      // Check the conditions.
      ConditionNodeList * cn = cnl;
      while (cn != NULL)
      {
         Xml::Node * cflagsNode = cn->node->GetChild("compile-flags");
         ASSERTNR(cflagsNode != NULL);

         // Check the optimization flags, EXTRA_CC_FLAGS, CL and _CL_
         // for matches.

         if (HasInfoList(optFlagsArray[i], OPT_DELIM, cflagsNode->Data, XML_DELIM, true)
          || HasInfoList(EXTRA_CC_FLAGS, OPT_DELIM, cflagsNode->Data, XML_DELIM, true)
          || HasInfoList(CL, OPT_DELIM, cflagsNode->Data, XML_DELIM, true)
          || HasInfoList(_CL_, OPT_DELIM, cflagsNode->Data, XML_DELIM, true))
         {
            const char * type = cn->node->GetAttributeValue("type");
            if (strcmp(type, "exclude") == 0)
            {
               break;
            }

            Xml::Node * overrideNode = cn->node->GetChild("override");

            if (!GetTestInfoFromNode(CFGfile, overrideNode, &variantInfo))
            {
               return FALSE;
            }
         }

         cn = cn->next;
      }

      // If we terminated early, exclude.

      if (cn != NULL)
      {
         continue;
      }

      TestVariant * pTestVariant = new TestVariant;
      memcpy(&pTestVariant->testInfo, &variantInfo, sizeof(TestInfo));
      pTestVariant->optFlags = optFlagsArray[i];

      *ppLastVariant = pTestVariant;
      ppLastVariant = &(*ppLastVariant)->next;
   }

   return TRUE;
}

// Parse one or more files from the file list.
BOOL
ParseFiles
(
   TestList * pTestList,
   char * testName,
   RLMODE cfg,
   TestInfo * defaultInfo,
   ConditionNodeList * cnl
)
{
   Test * pTest;

   // Break into list of strings.

   StringList * fileList =
      ParseStringList(defaultInfo->data[TIK_FILES], XML_DELIM);
   defaultInfo->data[TIK_FILES] = NULL;

   if (fileList == NULL)
   {
      if (cnl != NULL)
      {
         CFG_ERROR_EX(CFGfile, cnl->node->LineNumber,
            "No files specified\n", NULL);
         cnl->node->Dump();
      }
      return FALSE;
   }

   if (cfg == RM_DIR)
   {
      if (fileList->next != NULL)
      {
         CFG_ERROR_EX(CFGfile, cnl->node->LineNumber,
            "Specify exactly one file (dir) for directory nodes\n", NULL);
         cnl->node->Dump();
         return FALSE;
      }
   }

   BOOL fUserSpecified = FALSE;

   if (((cfg == RM_DIR) && FUserSpecifiedDirs)
    || ((cfg == RM_ASM) && FUserSpecifiedFiles)
    || ((cfg == RM_EXE) && FUserSpecifiedFiles))
   {
      fUserSpecified = TRUE;
   }

   // For asm and dir, we loop over the entire file list processing each file
   // individually.  (Recall that dir has a single file.)  For exe, we
   // process all files at once.

   StringList * next = fileList;

   while (next != NULL)
   {
      fileList = next;

      if (cfg == RM_EXE)
      {
         next = NULL;
      }
      else
      {
         next = fileList->next;
         fileList->next = NULL;

         testName = fileList->string;
      }

      if (cfg == RM_DIR)
      {
         pTest = FindTest(pTestList, testName, fUserSpecified, defaultInfo);
      }
      else
      {
         // Test names are unique for files, so there's no need to do a lookup.
         // If this uniqueness constraint changes, be aware that the RL setup
         // time would be quite long when there are lots of files given that
         // "FindTest" does a linear search.
         pTest = AddToTestList(pTestList, fileList);
      }

      // If the filename doesn't exist yet, we may want to create it.

      if (pTest == NULL)
      {
         // If the user has specified the files to use, skip this one.

         if (fUserSpecified)
         {
            continue;
         }

         pTest = AddToTestList(pTestList, fileList);
      }
      else
      {
         if (pTest->files == NULL)
         {
            ASSERTNR(fUserSpecified);

            pTest->files = fileList;
         }
      }

      // While this may happen multiple times for the same test, it is safe
      // to do so.

      memcpy(&pTest->defaultTestInfo, defaultInfo, sizeof(TestInfo));
      pTest->name = testName;

      if (cfg == RM_DIR)
      {
         if (cnl != NULL)
         {
            CFG_ERROR_EX(CFGfile, cnl->node->LineNumber,
               "Directory node can only be conditional on target\n", NULL);
            cnl->node->Dump();
            return FALSE;
         }
      }
      else if (cfg == RM_ASM)
      {
         if (!AddAsmVariants(pTest, defaultInfo, cnl))
         {
            return FALSE;
         }
      }
      else
      {
         if (!AddExeVariants(pTest, defaultInfo, cnl))
         {
            return FALSE;
         }
      }
   }

   return TRUE;
}

// mystrcmp is a frontend to strcmp that doesn't have a problem with NULL
// parameters.
int
mystrcmp(
   char *a,
   char *b
)
{
   if (a == b)
      return 0;

   if (a == NULL)
      return -1;

   if (b == NULL)
      return 1;

   return strcmp(a,b);
}

// mystrtok is similar to strtok_s, but takes two delimiter parameters: one for
// skipping entirely and one for skipping and terminating.
char *
mystrtok(
   char *s,
   char *delim,
   char *term
)
{
   static char *str = NULL;
   char *ret, *lastNonDelim;

   if (s)
      str = s;

   if (str == NULL)
      return NULL;

   // Skip leading delimiter (and term for first call).
   while (*str &&
      (strchr(delim, *str) ||
         (s && strchr(term, *str)))) {
      ++str;
   }

   // Parse non-delimiter/non-term.
   ret = lastNonDelim = str;
   while (*str && !strchr(term, *str)) {
      if (!strchr(delim, *str))
         lastNonDelim = str;
      ++str;
   }

   // EOS?
   if (!*str) {

      // If we reached EOS because there are no more tokens, return NULL now.
      if (ret == str)
         return nullptr;

      // Otherwise, set up for NULL return on next call.
      str = nullptr;
   }
   // Skip terminator.
   else {
      str++;
   }

   // Trim trailing space.
   if (!strchr(term, *lastNonDelim))
      ++lastNonDelim;
   *lastNonDelim = '\0';

   return ret;
}

BOOL
AppliesToTarget
(
   const char * fileName,
   int lineNumber,
   char * targetList
)
{
   // Special case for target list of "*"

   if (strcmp(targetList, "*") == 0)
   {
      return TRUE;
   }

   TARGET_MACHINES fileMach;

    // Parse the target list looking for our target machine.
    // Using mystrtok here because strtok_s doesn't appear to initialize
    // its static pointer to NULL.

   targetList = mystrtok(targetList, XML_DELIM, XML_DELIM);
   while (targetList)
   {

#ifndef NODEBUG
      if (FDebug)
      {
         printf("\t\tparsing %s\n", targetList);
      }
#endif

      // Does the config specified target match our target machine?

      fileMach = ParseMachine(targetList);
      if (fileMach == TM_UNKNOWN)
      {
         CFG_WARNING_EX(fileName, lineNumber, "Specified machine '%s' is unknown", targetList);
      }
      else if (MatchMachine(fileMach, RLMachine))
      {
         return TRUE;
      }

      targetList = mystrtok(NULL, XML_DELIM, XML_DELIM);
   }

   return FALSE;
}

BOOL
AppliesToTargetOS
(
   const char * fileName,
   int lineNumber,
   char * osList
)
{
   osList = mystrtok(osList, XML_DELIM, XML_DELIM);
   while (osList)
   {
      TARGET_OS os = ParseOS(osList);
      if (os == TO_UNKNOWN)
      {
         CFG_WARNING_EX(fileName, lineNumber, "Specified os '%s' is unknown", osList);
      }
      else if (MatchOS(os, TargetOS))
      {
         return TRUE;
      }

      osList = mystrtok(NULL, XML_DELIM, XML_DELIM);
   }

   return FALSE;
}

BOOL
DirectoryExcluded(
   char *filename
)
{
   Test * pDir;

   for (pDir = ExcludeDirList.first; pDir; pDir = pDir->next) {
      if (!_stricmp(pDir->name, filename)) {
         return TRUE;
      }
   }

   return FALSE;
}

// Parse the configuration file and modify the file list appropriately.
PROCESS_CONFIG_STATUS
ProcessConfig
(
   TestList * pTestList,
   char *CfgFile,
   RLMODE cfg
)
{
   static int unnamedCount = 0;
   const char * szOrder;

   ASSERT(!IsRelativePath(CfgFile)); // must be full path

   Xml::Node * topNode = Xml::ReadFile(CfgFile);

   if (topNode == NULL)
   {
      return PCS_FILE_NOT_FOUND;
   }

   Xml::Node * defaultNode;
   Xml::Node * conditionNode;
   Xml::Node * testNode;
   Xml::Node * applyNode;

   // Parser doesn't return the XML declaration node, so topNode is the RL root node.

   ASSERTNR(topNode->Next == NULL);

   for (testNode = topNode->ChildList;
        testNode != NULL;
        testNode = testNode->Next)
   {
      if (_stricmp(testNode->Name, "#comment") == 0)
      {
         continue;
      }

      char *testName = new char[20];
      sprintf_s(testName, 20, "UnnamedTest%d", unnamedCount);
      ++unnamedCount;

#ifndef NODEBUG

      if (FDebug)
      {
         printf("Processing node '%s'\n", testName);
      }

#endif

      // If we are processing the directory configuration, check for resume
      // and match points.

      if (cfg == RM_DIR)
      {
         // If we have a resume point, check for matching prefix.

         if (ResumeDir)
         {
            if (_strnicmp(testName, ResumeDir, strlen(ResumeDir)))
            {
               continue;
            }

            printf("Resuming from %s\n", testName);
            ResumeDir = NULL;
         }

         // If we have a matching directory, check for matching prefix.

         if (MatchDir)
         {
            if (_strnicmp(testName, MatchDir, strlen(MatchDir)))
            {
               continue;
            }
         }

         // If the directory has been explicitly excluded, then skip it
         if ((cfg == RM_DIR) && FExcludeDirs) {
            if (DirectoryExcluded(testName)) {
               if (FVerbose)
                  Message("Excluding %s\n", testName);
               continue;
            }
         }
      }

      // We should sort the children in condition order since we can't trust
      // that Xml nodes are read in the proper order. Since I know that
      // xmlreader.cpp does the right thing, I'm going to enforce the
      // ordering here rather than correct it.

      if (testNode->ChildList == NULL)
      {
         CFG_ERROR_EX(CfgFile, testNode->LineNumber, "test has no information", NULL);
         testNode->Dump();
         goto Label_Error;
      }

      defaultNode = testNode->ChildList;
      if (strcmp(defaultNode->Name, "default") != 0)
      {
         CFG_ERROR_EX(CfgFile, defaultNode->LineNumber, "first node is not default", NULL);
         testNode->Dump();
         goto Label_Error;
      }

      int lastOrder = 0;

      for (applyNode = testNode->ChildList->Next;
           applyNode != NULL;
           applyNode = applyNode->Next)
      {
         if (strcmp(applyNode->Name, "default") == 0)
         {
            CFG_ERROR_EX(CfgFile, applyNode->LineNumber, "multiple default nodes", NULL);
            testNode->Dump();
            goto Label_Error;
         }

         if (strcmp(applyNode->Name, "condition") != 0)
         {
            CFG_ERROR_EX(CfgFile, applyNode->LineNumber, "unknown node", NULL);
            applyNode->Dump();
            goto Label_Error;
         }

         szOrder = applyNode->GetAttributeValue("order");
         if (szOrder == NULL)
         {
            CFG_ERROR_EX(CfgFile, applyNode->LineNumber, "condition node has no order", NULL);
            applyNode->Dump();
            goto Label_Error;
         }

         int order = atoi(szOrder);
         if (order < 1)
         {
            CFG_ERROR_EX(CfgFile, applyNode->LineNumber, "illegal order value '%s'", szOrder);
            applyNode->Dump();
            goto Label_Error;
         }

         if (order <= lastOrder)
         {
            CFG_ERROR_EX(CfgFile, applyNode->LineNumber, "condition node is out-of-order", NULL);
            applyNode->Dump();
            goto Label_Error;
         }

         lastOrder = order;

         // Check for valid conditions.

         int numCond = 0;
         BOOL fHasOverride = FALSE;
         BOOL fHasNonTargetCond = FALSE;

         for (Xml::Node * condNode = applyNode->ChildList;
              condNode != NULL;
              condNode = condNode->Next)
         {
            if (strcmp(condNode->Name, "override") == 0)
            {
               if (!fHasOverride)
               {
                  fHasOverride = TRUE;
               }
               else
               {
                  CFG_ERROR_EX(CfgFile, applyNode->LineNumber,
                     "Too many override nodes", NULL);
               }
            }
            else if (strcmp(condNode->Name, "#comment") == 0)
            {
            }
            else if (strcmp(condNode->Name, "target") == 0 || strcmp(condNode->Name, "os") == 0)
            {
               if (fHasNonTargetCond)
               {
                  CFG_ERROR_EX(CfgFile, applyNode->LineNumber,
                     "Target conditions must come before non-target conditions",
                     NULL);
                  applyNode->Dump();
                  goto Label_Error;
               }

               numCond++;
            }
            else if (strcmp(condNode->Name, "compile-flags") == 0)
            {
               fHasNonTargetCond = TRUE;
               numCond++;
            }
            else
            {
               CFG_ERROR_EX(CfgFile, applyNode->LineNumber,
                  "Can't be conditional on %s", condNode->Name);
               applyNode->Dump();
               goto Label_Error;
            }
         }

         if (numCond == 0)
         {
            CFG_ERROR_EX(CfgFile, applyNode->LineNumber,
               "Missing condition", NULL);
         }
      }

      // Get the defaults from the default node.

      TestInfo testInfo;
      memset(&testInfo, 0, sizeof(TestInfo));

      if (!GetTestInfoFromNode(CfgFile, defaultNode, &testInfo))
      {
         goto Label_Error;
      }

      // Process the tags before checking for condition nodes.

      // Check for any directory tags only if we are in directory mode
      if (cfg == RM_DIR)
      {
          if (ShouldIncludeTest(DirectoryTagsList, &testInfo) == false)
          {
              goto Label_Skip;
          }
      }

      if (ShouldIncludeTest(TagsList, &testInfo, /*mustMatchIncludeTag*/cfg != RM_DIR) == false)
      {
          goto Label_Skip;
      }

      // Walk the condition nodes looking for applicability.

      ConditionNodeList * conditionNodeList = NULL;
      ConditionNodeList * conditionNodeLast = NULL;
      conditionNode = NULL;

      for (applyNode = testNode->ChildList->Next;
           applyNode != NULL;
           applyNode = applyNode->Next)
      {
         // Get the type of condition.

         const char * szType = applyNode->GetAttributeValue("type");
         if (strcmp(szType, "include") == 0)
         {
         }
         else if (strcmp(szType, "exclude") == 0)
         {
         }
         else
         {
            CFG_ERROR_EX(CfgFile, applyNode->LineNumber, "unknown condition type", NULL);
            applyNode->Dump();
            goto Label_Error;
         }

         // See if the condition applies to this target.

         Xml::Node * targetNode = applyNode->GetChild("target");
         Xml::Node * osNode = applyNode->GetChild("os");
         if (targetNode != NULL || osNode != NULL)
         {
            if (targetNode != NULL)
            {
                if (targetNode->ChildList != NULL)
                {
                    CFG_ERROR_EX(CfgFile, applyNode->LineNumber,
                        "expected data, not child node list", NULL);
                    targetNode->Dump();
                    goto Label_Error;
                }

                char * targetList = targetNode->Data;
                if ((targetList != NULL)
                    && !AppliesToTarget(CfgFile, targetNode->LineNumber, targetList))
                {
                    continue;
                }
            }

            if (osNode != NULL)
            {
                char * osList = osNode->Data;
                if (osList != NULL
                    && !AppliesToTargetOS(CfgFile, osNode->LineNumber, osList))
                {
                    continue;
                }
            }

            // Apply any overrides.

            Xml::Node * overrideNode = applyNode->GetChild("override");

            if (!GetTestInfoFromNode(CfgFile, overrideNode, &testInfo))
            {
               goto Label_Error;
            }
         }
         else if (cfg != RM_EXE)
         {
            CFG_ERROR_EX(CfgFile, applyNode->LineNumber,
               "non-target condition node NYI for non-exe", NULL);
            applyNode->Dump();
            goto Label_Error;
         }

         // We have a potentially applicable condition.  Add it to the list
         // of condition nodes if it is not a simple target condition.

         if (targetNode == NULL && osNode == NULL)
         {
            ConditionNodeList * cnl = new ConditionNodeList;

            cnl->node = applyNode;
            cnl->next = NULL;

            if (conditionNodeLast != NULL)
            {
               conditionNodeLast->next = cnl;
            }
            else
            {
               conditionNodeList = cnl;
            }

            conditionNodeLast = cnl;
         }
         else
         {

            // Save the last applicable target condition.

            conditionNode = applyNode;
         }
      }

      // If we have an exclude condition node, then skip this node.

      if ((conditionNode != NULL)
       && (strcmp(conditionNode->GetAttributeValue("type"), "exclude") == 0))
      {
         while (conditionNodeList != NULL)
         {
            ConditionNodeList * cnl = conditionNodeList->next;
            delete conditionNodeList;
            conditionNodeList = cnl;
         }

         continue;
      }

      // Stash the test info into the file list.

      if (!ParseFiles(pTestList, testName, cfg, &testInfo, conditionNodeList))
      {
          // TODO: Figure out where it came from (default or condition.)

          CFG_ERROR_EX(CfgFile, defaultNode->LineNumber,
             "bad file list", NULL);

          goto Label_Error;
      }

Label_Skip:;
   }

   // If the user specified files, check for ones that didn't match.

   if (FUserSpecifiedFiles && (cfg != RM_DIR))
   {
      for (Test * pTest = pTestList->first; pTest != NULL; pTest = pTest->next)
      {
         if ((pTest->variants == NULL)
            && !IsPogoTest(pTest))
         {
            Warning("'%s' not found", pTest->name);
         }
      }
   }

   return PCS_OK;

Label_Error:

   return PCS_ERROR;
}

void
WriteTestLst
(
   TestList * pTestList,
   char *cfgFile
)
{
   char tempBuf[BUFFER_SIZE], drive[MAX_PATH], dir[MAX_PATH];

   _splitpath_s(cfgFile, drive, ARRAYLEN(drive), dir, ARRAYLEN(dir), NULL, 0, NULL, 0);
   sprintf_s(tempBuf, "%s%s%s", drive, dir, DEFAULT_TESTLST_DCFG);
   DeleteFileIfFound(tempBuf);
   COutputBuffer *LstFilesOut = new COutputBuffer(tempBuf, FSyncImmediate ? false : true);
   ASSERT(LstFilesOut);
   for(Test * pDir = pTestList->first; pDir; pDir=pDir->next) {
      LstFilesOut->Add("%s\t%s\n", pDir->defaultTestInfo.data[TIK_TAGS], pDir->name);
   }
   LstFilesOut->Flush();
   printf("\nCreated: %s\n", tempBuf);
   delete LstFilesOut;
}

// Read the directory configuration file.
void
BuildDirList(
   void
)
{
   Test * pDir;
   PROCESS_CONFIG_STATUS status;
   char tempBuf[BUFFER_SIZE], drive[MAX_PATH], dir[MAX_PATH];

   status = ProcessConfig(&DirList, DCFGfile, RM_DIR);
   switch (status) {
      case PCS_ERROR:
         exit(1);

      case PCS_FILE_NOT_FOUND:
         if (!FUserSpecifiedDirs) {
            Warning("-dcfg file %s not found; "
               "only regressing the current directory",
               DCFGfile);
            AddCurrentDir();
         }
         break;
   }

   if (ResumeDir)
      Fatal("-resume directory '%s' not found", ResumeDir);
   if (MatchDir && (DirList.first == NULL))
      Fatal("-match prefix '%s' did not match any directory", MatchDir);

   // Get the full path for each. Grab the directory of the DCFG file and
   // append to that each subdirectory.

   _splitpath_s(DCFGfile, drive, ARRAYLEN(drive), dir, ARRAYLEN(dir), NULL, 0, NULL, 0);

   for (pDir = DirList.first; pDir; pDir = pDir->next) {
      sprintf_s(tempBuf, "%s%s%s", drive, dir, pDir->name);
      pDir->fullPath = _strdup(tempBuf);
   }

#ifndef NODEBUG
   if (FDebug) {
      printf("\nDirectories:\n");
      DumpTestList(&DirList);
   }
#endif

   if (FGenLst)
      WriteTestLst(&DirList, DCFGfile);
}

// Update the status in the title.
void
UpdateTitleStatus()
{
   char* s;
   char tempBuf[BUFFER_SIZE];
   unsigned int i;

   // Don't bother if we're not going to display it.
   if (!FStatus)
      return;

   EnterCriticalSection(&csTitleBar);

   s = FormatString(StatusFormat);
   strcpy_s(TitleStatus, s);
   s = strchr(TitleStatus, '\0');

   // start at 1: skip primary thread 0 (unless we decide to let it do real work)
   for (i = 1; i <= NumberOfThreads; i++) {
      ThreadInfo[i].GetCurrentTest(tempBuf);
      s += sprintf_s(s, REMAININGARRAYLEN(TitleStatus, s), "; %s", tempBuf);
   }

   LeaveCriticalSection(&csTitleBar);
}

void
WriteSummary(
   const char *name,
   BOOL fBaseline,
   int tests,
   int diffs,
   int failures
)
{
   char tempBuf[BUFFER_SIZE];

   if (FSummary || FVerbose) {
      if (Mode == RM_ASM) {
         if (fBaseline) {
            sprintf_s(tempBuf,
               "Summary: %s (baselines) has %d tests; %d failures",
               name, tests, failures);
         }
         else {
            sprintf_s(tempBuf,
               "Summary: %s (diffs) has %d tests; %d diffs and %d failures",
               name, tests, diffs, failures);
         }

         LogOut("%s", tempBuf);
      }
      else {
         sprintf_s(tempBuf,
            "Summary: %s had %d tests; %d failures",
            name, tests, failures);
         LogOut("%s", tempBuf);
      }
   }
}

void
__cdecl WriteResults(const char *fmt, ...)
{
   va_list arg_ptr;
   char tempBuf[BUFFER_SIZE];

   ASSERT(ThreadRes != NULL);

   va_start(arg_ptr, fmt);
   vsprintf_s(tempBuf, fmt, arg_ptr);
   ASSERT(strlen(tempBuf) < BUFFER_SIZE);
   ThreadRes->Add("%s\n", tempBuf);
}

void
__cdecl WriteRunpl(const char *fmt, ...)
{
   va_list arg_ptr;
   char tempBuf[BUFFER_SIZE], buf[50];

   ASSERT(ThreadFull != NULL);

   buf[0] = '\0';
   if (!FNoThreadId && ThreadId != 0 && NumberOfThreads > 1) {
      sprintf_s(buf, "%d>", ThreadId);
   }
   va_start(arg_ptr, fmt);
   vsprintf_s(tempBuf, fmt, arg_ptr);
   ASSERT(strlen(tempBuf) < BUFFER_SIZE);
   ThreadFull->Add("%s%s\n", buf, tempBuf);
}

// Execute a single regression test.
void
PerformSingleRegression(
    CDirectory* pDir,
    Test * pTest
)
{
    char testNameBuf[BUFFER_SIZE];
    char tempBuf[BUFFER_SIZE];
    char* ccFlags;
    time_t start_test, elapsed_test;
    RLFE_STATUS rlfeStatus;

    // Before executing, make sure directory is started.
    pDir->TryBeginDirectory();

    for (TestVariant * pTestVariant = pTest->variants; pTestVariant != NULL; pTestVariant = pTestVariant->next)
    {
        ThreadInfo[ThreadId].SetCurrentTest(pDir->GetDirectoryName(), pTest->name, pDir->IsBaseline());

        pTestVariant->testInfo.data[TIK_FILES] = pTest->name;

        if (FStatus)
            UpdateTitleStatus();

        if (strcmp(pTest->files->string, "dotest.cmd") == 0) {
            sprintf_s(tempBuf, "(%s (", pTest->name);
        } else
            sprintf_s(tempBuf, "(%s (", pTest->files->string);

        if (pTestVariant->optFlags != NULL)
            strcat_s(tempBuf, pTestVariant->optFlags);

        ccFlags = pTestVariant->testInfo.data[TIK_COMPILE_FLAGS];
        if(ccFlags != NULL) {
            if(pTestVariant->optFlags != NULL)
                strcat_s(tempBuf, " ");

            strcat_s(tempBuf, ccFlags);
        }

        if(!pTestVariant->optFlags && !ccFlags)
            tempBuf[0] = '\0';
        else
            strcat_s(tempBuf,") ");

        strcat_s(tempBuf, Mode == RM_ASM ? "asm" : "exe");
        strcat_s(tempBuf, (Mode == RM_ASM) ? (pDir->IsBaseline() ? " base)" : " diffs)") : ")");
        sprintf_s(testNameBuf, "%s %s", pDir->GetDirectoryName(), tempBuf);
        WriteRunpl("+++ %s +++", testNameBuf);

        start_test = time(NULL);

        // Are we doing regressions internally?
        rlfeStatus = RLFES_PASSED;

        int result;

        // Assembly regressions?
        if (Mode == RM_ASM)
        {
            result = RegrFile(pDir, pTest, pTestVariant);
        }
        else
        {
            result = ExecTest(pDir, pTest, pTestVariant);
        }

        switch (result)
        {
        case -1:
            pDir->IncFailures();
            if (Timing & TIME_TEST)
                WriteResults("%s -- failed : exec time=%I64d", testNameBuf, time(NULL) - start_test);
            else
                WriteResults("%s -- failed", testNameBuf);
            rlfeStatus = RLFES_FAILED;
            break;
        case 1:
            pDir->IncDiffs();

            if (Timing & TIME_TEST)
                WriteResults("%s -- diffs : exec time=%I64d", testNameBuf, time(NULL) - start_test);
            else
                WriteResults("%s -- diffs", testNameBuf);
            rlfeStatus = RLFES_DIFFED;
            break;
        case 0:
            if (Timing & TIME_TEST)
                WriteResults("%s -- passed : exec time=%I64d", testNameBuf, time(NULL) - start_test);
            else
                WriteResults("%s -- passed", testNameBuf);
            break;
        default:
            ASSERTNR(UNREACHED);
        }

        pDir->IncRun();

        if (FRLFE)
            RLFETestStatus(pDir);

        elapsed_test = (int)(time(NULL) - start_test);

        if (Timing & TIME_TEST) {
            Message("RL: Test elapsed time (%s, %s): %02d:%02d", pDir->GetDirectoryName(), pTest->name, elapsed_test / 60, elapsed_test % 60);
        }

        if (FSyncTest || (Mode == RM_ASM && FSyncVariation)) {
            if (FRLFE && rlfeStatus)
                RLFEAddLog(pDir, rlfeStatus, pTest->name, NULL, ThreadOut->GetText());

            FlushOutput();
        }
    }

    // Test finished, notify directory in case we are done with it.
    pDir->UpdateState();
}

// Execute the regressions on a single directory.
void
RegressDirectory(
   CDirectory* pDir
)
{
   TestList * pTestList;
   Test * pTest;
   char* path;
   char* dir;

#ifndef NODEBUG
   if (FDebug)
      pDir->Dump();
#endif

   path = pDir->GetDirectoryPath();
   dir = pDir->GetDirectoryName();
   pTestList = pDir->GetTestList();

   // If we are doing baselines and diffs simultaneously and we are now
   // performing the diffs, reinitialize the directory stats.

   if (FBaseDiff && !pDir->IsBaseline())
      pDir->InitStats(0);

   ASSERTNR((long)pDir->NumVariationsRun == 0);
   ASSERTNR((long)pDir->NumDiffs == 0);
   ASSERTNR((long)pDir->NumFailures == 0);

   if (!FNoDirName)
      WriteLog("*** %s ***", dir);

   if ((Mode == RM_ASM) && !CreateAsmDirs(path, pDir->IsBaseline())) {
      pDir->IncFailures(pDir->NumVariations);
      if (FRLFE)
         RLFETestStatus(pDir);
      return;
   }

   if (FRLFE) {
      RLFETestStatus(pDir);
      RLFEThreadDir(pDir, BYTE(ThreadId));
   }

   if (FSyncTest || FSyncVariation)
      FlushOutput();

    // Start a new directory.

   if (((Mode == RM_ASM) && !RegrStartDir(path)) ||
      ((Mode == RM_EXE) && !RunStartDir(path))) {
      pDir->IncRun(pDir->NumVariations);
      pDir->IncFailures(pDir->NumVariations);
      if (FRLFE) {
         RLFETestStatus(pDir);
         RLFEAddLog(pDir, RLFES_FAILED, "Start",
            NULL, "Start dir command failed");
      }
      return;
   }

   // Loop over the test list, invoking the regression command once per test
   // variation. We do not need to write any directory summary here, the CDirectory
   // object itself will take care of that when status is updated during regression
   // execution.
   for (pTest = pTestList->first; pTest; pTest = pTest->next)
   {
       PerformSingleRegression(pDir, pTest);
   }

   // End the directory.

   if (Mode == RM_ASM) {
      if (!RegrEndDir(path))
         return;
   }

   if (FSyncDir)
      FlushOutput();
}

DWORD __stdcall
StatusWorker( void *arg )
{
   char oldTitleStatus[BUFFER_SIZE];

   ASSERT(FStatus);

   oldTitleStatus[0] = '\0';

   while (!bThreadStop) {
      EnterCriticalSection(&csTitleBar);
      if (0 != strcmp(oldTitleStatus, TitleStatus)) {
         // only if it changed do we set it...
         SetConsoleTitle(TitleStatus);
         strcpy_s(oldTitleStatus, TitleStatus);
      }
      LeaveCriticalSection(&csTitleBar);
      Sleep(500);  // don't change it too often
   }

   arg = arg;
   return 0;
}

DWORD __stdcall
ThreadWorker( void *arg )
{
   ThreadId = (int)(intptr_t)(arg);    // thread-local global variable for thread id
   MaxThreads++;
   ThreadOut = new COutputBuffer(FQuiet ? (FILE*)NULL : stdout, FSyncImmediate ? false : true);
   ThreadLog = new COutputBuffer(LogName, FSyncImmediate ? false : true);
   ThreadFull = new COutputBuffer(FullLogName, FSyncImmediate ? false : true);
   ThreadRes = new COutputBuffer(ResultsLogName, FSyncImmediate ? false : true);

   CDirectory* pDir = NULL;
   CDirectoryAndTestCase* pDirAndTest = NULL;

   if (FSyncEnumDirs)
      WaitForSingleObject(heventDirsEnumerated, INFINITE);

   // Create thread-specific target VM command.

   if (TARGET_VM) {
      size_t bufSize = strlen(TARGET_VM) + 1;
      TargetVM = (char *)malloc(bufSize);
      sprintf_s(TargetVM, bufSize, TARGET_VM, ThreadId);
   }
   else {
      TargetVM = NULL;
   }

   while (TRUE) {
      if (bThreadStop)
         break;

      // Poll for new stuff. If the thread is set to stop, then go away.

      if (FSingleThreadPerDir)
      {
          // Use DirectoryQueue and schedule each directory on a single thread.
          pDir = DirectoryQueue.GetNextItem();

          if (pDir == NULL)
             break;

          while (TRUE) {
             RegressDirectory(pDir);

             // We're done with this directory.  If we are doing both
             // baselines and diffs, and it was a baseline directory,
             // changed to diffs and run again.

             if (FBaseDiff && pDir->IsBaseline())
                pDir->SetDiffFlag();
             else
                break;
          }

          delete pDir;
          pDir = NULL;
      }
      else
      {
          // Use DirectoryAndTestCaseQueue and schedule each test on it's own thread.
          pDirAndTest = DirectoryAndTestCaseQueue.GetNextItem();

          if (pDirAndTest == NULL)
             break;

          PerformSingleRegression(pDirAndTest->_pDir, pDirAndTest->_pTest);

          delete pDirAndTest;
          pDirAndTest = NULL;
      }
   }

   // Make sure we've flushed all the output, especially if we're stopping
   // early because of a ctrl-c.
   FlushOutput();
   delete ThreadOut;
   delete ThreadLog;
   delete ThreadFull;
   delete ThreadRes;
   ThreadOut = NULL;
   ThreadLog = NULL;
   ThreadFull = NULL;
   ThreadRes = NULL;

   // Do a little per-thread cleanup
   delete[] cmpbuf1;
   delete[] cmpbuf2;

   // Mark us as done so the title bar shows that.
   ThreadInfo[ThreadId].Done();

   MaxThreads--;
   return 0;
}

int __cdecl
main(int argc, char *argv[])
{
   Test * pDir, * pTest;
   CDirectory *pDirectory;
   TestList TestList;
   PROCESS_CONFIG_STATUS status;
   DWORD dwThreadId;
   HANDLE newThread;
   HANDLE* workerThreads;  // for waiting to finish
   unsigned int i;
   int num, dirNum;
   char fullCfg[_MAX_PATH];
   DWORD err;
   time_t start_total;
   UINT elapsed_total;

   start_total = time(NULL);

   SetConsoleCtrlHandler(NT_handling_function, TRUE);

   InitializeCriticalSection(&csTitleBar);
   InitializeCriticalSection(&csStdio);
   InitializeCriticalSection(&csCurrentDirectory);

   // Set up output for primary thread.
   ThreadOut = new COutputBuffer(stdout, true);

   // WARNING: while parsing flags, these are aliased to ThreadOut
   ThreadLog = ThreadOut;  // no LogName yet
   ThreadFull = ThreadOut;
   ThreadRes = ThreadOut;

   InitTestList(&TestList);

   ASSERTNR(TestList.first == NULL);
   ASSERTNR(DirList.first == NULL);

   // Get the path for temporary files

   err = GetTempPathA(MAX_PATH, TempPath);
   if (err == 0) {
      strcpy_s(TempPath, "\\");
   } else {
      err = GetFileAttributesA(TempPath);
      if (err == 0xffffffff || !(err & FILE_ATTRIBUTE_DIRECTORY)) {
         strcpy_s(TempPath, "\\");
      }
   }

   ParseCommandLine(argc, argv, &TestList);

   if (FSyncEnumDirs) {
      // The event is manual reset, so once triggered all threads
      // will wake up. It is unsignaled by default; we signal it
      // after all dirs/files have been enumerated.
      heventDirsEnumerated = CreateEvent(NULL, TRUE, FALSE, NULL);
   }

   // If we're using internal code for regressions, initialize.

   Mode == RM_ASM ? RegrInit() : RunInit();

   if (FRLFE) {
      if (!RLFEConnect(StatusPrefix))
         Fatal("Couldn't connect to RLFE");
   }

   atexit(NormalCleanUp);

   BuildDirList();

   if (FVerbose)
      PrintDirs();

   if (DirList.first == nullptr)
   {
       Message("No directories specified for regression - exiting.\n");
       FlushOutput();
       return 0;
   }

   NumDiffsTotal[RLS_TOTAL] = NumFailuresTotal[RLS_TOTAL] = 0;

   if (FRLFE) {
      dirNum = 0;
      for (pDir = DirList.first; pDir; pDir = pDir->next)
         dirNum++;

      RLFEInit((BYTE)NumberOfThreads, dirNum);

      if (Mode == RM_ASM) {
         if (FBaseline)
            RLFEAddRoot(RLS_BASELINES, 0);
         if (FDiff)
            RLFEAddRoot(RLS_DIFFS, 0);
      }
      else {
         RLFEAddRoot(RLS_EXE, 0);
      }
   }

   /*
    * Create an array of info about each thread, accessed by the status
    * update thread.
    */
   ThreadInfo = new CThreadInfo[NumberOfThreads + 1];

   /*
    * Create the thread pool to do the tests
    */
   workerThreads = new HANDLE[NumberOfThreads];    // don't store primary thread
   for( i=1; i <= NumberOfThreads; i++ ) {
      workerThreads[i-1] = newThread = CreateThread( (LPSECURITY_ATTRIBUTES)NULL, 0,
         ThreadWorker, (LPVOID *)i, 0, &dwThreadId );
      if (newThread == INVALID_HANDLE_VALUE)
         Fatal("Couldn't create thread %d", i);
   }

   /*
    * Create the 'update status' thread only for FStatus
    */
   if (FStatus) {
      newThread = CreateThread( (LPSECURITY_ATTRIBUTES)NULL, 0,
         StatusWorker,(LPVOID *)0,0,&dwThreadId );
      if (newThread == INVALID_HANDLE_VALUE)
         Fatal("Couldn't create status thread");
      CloseHandle(newThread);
   }

   // flush all output for the primary thread
   FlushOutput();

   dirNum = 0;
   for (pDir = DirList.first; pDir; pDir = pDir->next) {

      // Number the directories.

      pDir->num = ++dirNum;

      // Build a file list.

      sprintf_s(fullCfg, "%s\\%s", pDir->fullPath, CFGfile);
      status = ProcessConfig(&TestList, fullCfg, Mode);

      if (status != PCS_ERROR)
      {

#ifndef NODEBUG
         if (FDebug) {
            printf("\nAfter processing config:\n");
            DumpTestList(&TestList);
         }
#endif
         num = 0;
         for (pTest = TestList.first; pTest; pTest = pTest->next)
         {
            for (TestVariant * variant = pTest->variants;
                 variant != NULL;
                 variant = variant->next)
            {
               num++;
            }

            if (!FNoWarn && IsPogoTest(pTest) && (pTest->variants == NULL))
            {
               LogOut("Warning: %s::%s is a Pogo test, "
                  "but no Pogo-specific options are set",
                  pDir->name, pTest->name);
            }
         }

         if (FGenLst) {
            // if generate runall's env.lst config, print the env lst here for the dir
            WriteEnvLst(pDir, &TestList);
            goto nextdir;
         }

         if (num)
         {
            // Queue the tests

            pDirectory = new CDirectory(pDir, TestList);
            pDirectory->InitStats(num);
            if (!FBaseline)
               pDirectory->SetDiffFlag();

            // Always put the directory into the DirectoryQueue.
            // We will use this queue to clean up the CDirectory objects if FSingleThreadPerDir is
            // turned off.
            DirectoryQueue.Append(pDirectory);

            if (FRLFE) {
               if (Mode == RM_EXE) {
                  RLFEAddTest(RLS_EXE, pDirectory);
               }
               else {
                  if (FBaseline)
                     RLFEAddTest(RLS_BASELINES, pDirectory);
                  if (FDiff)
                     RLFEAddTest(RLS_DIFFS, pDirectory);
               }
            }

            if (!FSingleThreadPerDir)
            {
                // Keep track of tests as a flat list so we can split tests
                // in the same directory across multiple threads.

                for (pTest = TestList.first; pTest; pTest = pTest->next)
                {
                    CDirectoryAndTestCase* pDirAndTest = new CDirectoryAndTestCase(pDirectory, pTest);

                    DirectoryAndTestCaseQueue.Append(pDirAndTest);
                }
            }
         }
      }

 nextdir:
      InitTestList(&TestList); // get ready for next directory
      ASSERTNR(TestList.first == NULL);
   }

   if (FGenLst) {
      return 0;
   }
   // We can now set the totals.

   if (FRLFE) {
      if (Mode == RM_EXE) {
         RLFEAddRoot(RLS_EXE, (long)NumVariationsTotal[RLS_EXE]);
      }
      else {
         if (FBaseline)
            RLFEAddRoot(RLS_BASELINES, (long)NumVariationsTotal[RLS_BASELINES]);
         if (FDiff)
            RLFEAddRoot(RLS_DIFFS, (long)NumVariationsTotal[RLS_DIFFS]);
      }

      RLFEAddRoot(RLS_TOTAL, (long)NumVariationsTotal[RLS_TOTAL]);
   }

   DirectoryQueue.AdjustWaitForThreadCount();
   DirectoryAndTestCaseQueue.AdjustWaitForThreadCount();

#ifndef NODEBUG
   if (FDebug) {
      DirectoryQueue.Dump();
   }
#endif

   if (FSyncEnumDirs)
      SetEvent(heventDirsEnumerated);

   // Wait for all the threads to die
   WaitForMultipleObjectsEx(NumberOfThreads, workerThreads, TRUE, INFINITE, false);
   for (i = 0; i < NumberOfThreads; i++)
      CloseHandle(workerThreads[i]);
   bThreadStop = TRUE; // make status thread go away

   if (FBaseDiff) {
      WriteSummary(REGRESS, TRUE,
         NumVariationsTotal[RLS_BASELINES],
         NumDiffsTotal[RLS_BASELINES],
         NumFailuresTotal[RLS_BASELINES]);
      WriteSummary(REGRESS, FALSE,
         NumVariationsTotal[RLS_DIFFS],
         NumDiffsTotal[RLS_DIFFS],
         NumFailuresTotal[RLS_DIFFS]);
   }
   else {
      WriteSummary(REGRESS, FBaseline,
         NumVariationsTotal[RLS_TOTAL],
         NumDiffsTotal[RLS_TOTAL],
         NumFailuresTotal[RLS_TOTAL]);
   }

   elapsed_total = (int)(time(NULL) - start_total);
   if (Timing != TIME_NOTHING) {
      Message("RL: Total elapsed time: %02d:%02d",
         elapsed_total / 60, elapsed_total % 60);
   }

   if (((int)CntDeleteFileFailures > 0) || ((int)CntDeleteFileFatals > 0)) {
      Message("RL: Total DeleteFile failures: %d, DeleteFile abandoned: %d",
         (int)CntDeleteFileFailures,
         (int)CntDeleteFileFatals);
   }

   // flush all output for the primary thread
   FlushOutput();

   // The return code from RL is 0 for complete success, 1 for any failure.
   // Note that diffs don't count as failures.

   return ((long)NumFailuresTotal[RLS_TOTAL] == 0L) ? 0 : 1;
}
