//------------------------------------------------------------------------------------------------------- // 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: // // // // // head node, tag unimportant // // // test node, tag unimportant // // // default test information // // test info // see below // // // // // condition node // // ... // list of targets this condition applies to // // // // // // // Test info (valid for all configuration 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. // // ... // 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. // // SEH // // 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): // // ... // compilation flags for this test // ... // RL directives (case-insensitive) // // Test info (valid for exe test configuration files) // // ... // baseline, expected test output // ... // link flags for this test // // extra env vars for this test // value1 // any number of child nodes with arbitrary data // ... // release // // will result in "set build=release" for the test task env // // // 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 nodes that allow default // info to be overridden. E.g. // // // // ia64 // // // // ia64-result.out // // // // // // 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 ... 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 +++ +++ // 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[] = { "", "" }; const char * const TestInfoKindName[] = { "files", "baseline", "compile-flags", "link-flags", "tags", "rl", "env", "command", "timeout", "timeoutRetries", "sourcepath", "eol-normalization", "custom-config-file", NULL }; static_assert((sizeof(TestInfoKindName) / sizeof(TestInfoKindName[0])) - 1 == TestInfoKind::_TIK_COUNT, "Fix the buffer size"); 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]; const char *DIFF_DIR; char *REGRESS = NULL, *MASTER_DIR, *TARGET_MACHINE, *RL_MACHINE, *TARGET_OS_NAME = NULL; const char *REGR_CL, *REGR_DIFF; char *REGR_ASM, *REGR_SHOWD; const char *TARGET_VM; char *EXTRA_CC_FLAGS, *EXEC_TESTS_FLAGS; const char *LINKER, *LINKFLAGS; char *CL, *_CL_; const char *JCBinary = "jshost.exe"; BOOL FStatus = TRUE; char *StatusPrefix = nullptr; const char *StatusFormat = nullptr; BOOL FVerbose = FALSE; BOOL FQuiet = FALSE; BOOL FNoWarn = FALSE; BOOL FTest = FALSE; BOOL FStopOnError = FALSE; BOOL GStopDueToError = FALSE; BOOL FLow = FALSE; BOOL FNoDelete = FALSE; BOOL FCopyOnFail = FALSE; BOOL FSummary = TRUE; BOOL FMoveDiffs = FALSE; BOOL FNoMoveDiffsSwitch = FALSE; BOOL FRelativeLogPath = FALSE; BOOL FNoDirName = TRUE; BOOL FBaseline = FALSE; BOOL FRebase = FALSE; BOOL FDiff = FALSE; BOOL FBaseDiff = FALSE; BOOL FSyncEnumDirs = FALSE; BOOL FNogpfnt = TRUE; BOOL FAppend = FALSE; BOOL FAppendTestNameToExtraCCFlags = FALSE; #ifndef NODEBUG BOOL FDebug = FALSE; #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 const *CFGfile = NULL; char const *CMDfile = NULL; #define MAX_ALLOWED_THREADS 10 // must be <= MAXIMUM_WAIT_OBJECTS (64) unsigned NumberOfThreads = 0; TestList DirList, ExcludeDirList; BOOL FUserSpecifiedFiles = FALSE; BOOL FUserSpecifiedDirs = TRUE; BOOL FNoProgramOutput = FALSE; BOOL FOnlyAssertOutput = FALSE; BOOL FExcludeDirs = FALSE; BOOL FGenLst = FALSE; char *ResumeDir = nullptr; char *MatchDir = nullptr; TIME_OPTION Timing = TIME_DIR | TIME_TEST; // Default to report times at test and directory level static const char *ProgramName = nullptr; static const char *LogName = nullptr; static const char *FullLogName = nullptr; static const char *ResultsLogName = nullptr; static const char *TestTimeout = nullptr; // Stores timeout in seconds for all tests static const char *TestTimeoutRetries = nullptr; // Number of timeout retries for all tests #define MAX_ALLOWED_TIMEOUT_RETRIES 100 // Arbitrary max to avoid accidentally specifying too many retries // 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; CDirectoryQueue DirectoryTestQueue; 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( const 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( const 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( const 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, const 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( const char *directory, const 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; } struct MyFileWithoutCarriageReturn { CHandle* handle; char* buf = nullptr; size_t i = 0; DWORD count = 0; MyFileWithoutCarriageReturn(CHandle* handle, char* buf) : handle(handle), buf(buf) { Read(); } bool readingError; void Read() { i = 0; readingError = !ReadFile(*handle, buf, CMPBUF_SIZE, &count, NULL); } bool HasNextChar() { if (count == 0) { return false; } if (i == count) { Read(); if (readingError) { return false; } return HasNextChar(); } while (buf[i] == '\r') { ++i; if (i == count) { Read(); if (readingError) { return false; } return HasNextChar(); } } return true; } char GetNextChar() { return buf[i++]; } }; // 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, BOOL normalizeLineEndings ) { 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; } if (normalizeLineEndings) { MyFileWithoutCarriageReturn f1(&h1, cmpbuf1); MyFileWithoutCarriageReturn f2(&h2, cmpbuf2); if (f1.readingError || f2.readingError) { LogError("ReadFile failed doing compare of %s and %s", file1, file2); return -1; } while (f1.HasNextChar() && f2.HasNextChar()) { if (f1.readingError || f2.readingError) { LogError("ReadFile failed doing compare of %s and %s", file1, file2); return -1; } if (f1.GetNextChar() != f2.GetNextChar()) { #ifndef NODEBUG if (FDebug) printf("DoCompare shows %s and %s are NOT equal (contents differ)\n", file1, file2); #endif return 1; } } if (f1.HasNextChar() != f2.HasNextChar()) { #ifndef NODEBUG if (FDebug) printf("DoCompare shows %s and %s are NOT equal (contents differ)\n", file1, file2); #endif return 1; } return 0; } 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( const 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", (int32)NumDiffsTotal[RLS_TOTAL]); break; case 'f': i += sprintf_s(&buf[i], BUFFER_SIZE + 32 - i, "%d", (int32)NumFailuresTotal[RLS_TOTAL]); break; case 't': i += sprintf_s(&buf[i], BUFFER_SIZE + 32 - i, "%d", (int32)NumVariationsRun[RLS_TOTAL]); break; case 'T': i += sprintf_s(&buf[i], BUFFER_SIZE + 32 - i, "%d", (int32)NumVariationsTotal[RLS_TOTAL]); break; case 'p': if ((int32)NumVariationsTotal[RLS_TOTAL]) { i += sprintf_s(&buf[i], BUFFER_SIZE + 32 - i, "%d", 100 * (int32)NumVariationsRun[RLS_TOTAL] / (int32)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" " -stoponerror stop on first test failure\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.\n" "DIFF_DIR defaults to diffs.\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(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; } template ListNode * AddToStringList ( ListNode * list, String string ) { ListNode * p = new ListNode; p->string = string; // NOTE: we store the pointer; we don't copy the string p->next = NULL; if (list == NULL) { return p; } ListNode * last = list; while (last->next != NULL) { last = last->next; } last->next = p; return list; } template void FreeStringList ( ListNode * list ) { while (list) { ListNode * pFree = list; list = list->next; delete pFree; } } void FreeVariants ( TestVariant * list ) { while (list) { TestVariant * pFree = list; list = list->next; delete pFree; } } StringList * ParseStringList(const char* cp, const char* delim) { StringList * list = NULL; if (cp == NULL) { return list; } char *p = _strdup(cp); // 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 = _strdup(DEFAULT_EXEC_TESTS_FLAGS); } else { // 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 ) { ConstStringList * GetNameDataPairs(Xml::Node * node); for(int i=0;i < _TIK_COUNT; i++) { if ((i == TIK_ENV) && pTestInfo->data[TIK_ENV]) { auto 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 ConstStringList * GetNameDataPairs ( Xml::Node * node ) { ASSERT(node->ChildList != NULL); ConstStringList *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, 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 if (variants->testInfo.data[TIK_ENV]) { auto 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( const 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; } char* MakeFullPath(const char* absFilePath, const char* relPath) { char drive[MAX_PATH], dir[MAX_PATH]; _splitpath_s(absFilePath, drive, ARRAYLEN(drive), dir, ARRAYLEN(dir), NULL, 0, NULL, 0); char makepath[MAX_PATH + 1]; makepath[MAX_PATH] = '\0'; _makepath_s(makepath, drive, dir, relPath, NULL); char fullpath[MAX_PATH + 1]; fullpath[MAX_PATH] = '\0'; if (_fullpath(fullpath, makepath, MAX_PATH) == NULL) { return NULL; } char* fullPathBuf = (char*)malloc(sizeof(fullpath)); sprintf_s(fullPathBuf, sizeof(fullpath), "%s", fullpath); return fullPathBuf; } 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], "stoponerror")) { FStopOnError = 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; } if (!_stricmp(&arg[1], "noprogramoutput")) { FNoProgramOutput = TRUE; break; } if (!_stricmp(&arg[1], "onlyassertoutput")) { FOnlyAssertOutput = TRUE; break; } if (!_stricmp(&arg[1], "timeout")) { TestTimeout = ComplainIfNoArg(arg, s); break; } if (!_stricmp(&arg[1], "timeoutRetries")) { TestTimeoutRetries = ComplainIfNoArg(arg, s); 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( const 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)) { 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)) { 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)) { 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; const 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, const 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(const char *strTimeout) { char *end; _set_errno(0); uint32 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(secTimeout); // Does the result fit in 32-bits? if (millisecTimeout >= (1ui64 << 32)) { return FALSE; } return TRUE; } BOOL IsTimeoutRetriesStringValid(const char *strTimeoutRetries) { char *end; _set_errno(0); uint32 numRetries = strtoul(strTimeoutRetries, &end, 10); if (errno != 0 || *end != 0) { return FALSE; } // We will not be doing any math with this value, so no need to check for overflow. // However, large values will possibly result in an unacceptably long retry loop, // (especially with the default timeout being multiple minutes long), // so limit the number of retries to some arbitrary max. if (numRetries > MAX_ALLOWED_TIMEOUT_RETRIES) { return FALSE; } return TRUE; } uint32 GetTimeoutValue(const char *strTimeout) { if (strTimeout == nullptr) { return 0; } char *end = nullptr; _set_errno(0); uint32 secTimeout = strtoul(strTimeout, &end, 10); return secTimeout; } BOOL GetTestInfoFromNode ( const char * fileName, Xml::Node * node, TestInfo * testInfo ) { if (node == nullptr) { return TRUE; } for (int i = 0; i < _TIK_COUNT; i++) { Xml::Node * childNode = node->GetChild(TestInfoKindName[i]); if (childNode != nullptr) { testInfo->hasData[i] = TRUE; if (i == TIK_ENV) { ASSERT(childNode->ChildList != nullptr); testInfo->data[i] = (char*)childNode; } else { if (childNode->ChildList != nullptr) { CFG_ERROR_EX(fileName, node->LineNumber, "Expected data, not child list\n", nullptr); childNode->Dump(); return FALSE; } if (childNode->Data != nullptr && childNode->Data[0] != '\0') { char * data = childNode->Data; if (i == TIK_SOURCE_PATH && IsRelativePath(childNode->Data)) { // Make sure sourcepath is not relative, if relative make it full path data = MakeFullPath(fileName, data); ASSERT(data != nullptr); } testInfo->data[i] = data; } else { testInfo->data[i] = nullptr; } 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", nullptr); childNode->Dump(); return FALSE; } } if (i == TIK_TIMEOUT_RETRIES) { // Validate the timeoutRetries string now to fail early so we don't run any tests when there is an error. if (!IsTimeoutRetriesStringValid(testInfo->data[i])) { CFG_ERROR_EX(fileName, node->LineNumber, "Invalid number of timeout retries specified. Value must be numeric and <= %d.\n", MAX_ALLOWED_TIMEOUT_RETRIES); childNode->Dump(); return FALSE; } } } } if (i == TIK_TIMEOUT && TestTimeout != nullptr) { // Overriding the timeout value with the command line value (if the command line value is larger) uint32 xmlTimeoutValue = GetTimeoutValue(testInfo->data[i]); uint32 testTimeoutValue = GetTimeoutValue(TestTimeout); if (xmlTimeoutValue < testTimeoutValue) { testInfo->data[i] = TestTimeout; } } if (i == TIK_TIMEOUT_RETRIES && TestTimeoutRetries != nullptr) { // Overriding the timeoutRetries value with the command line value (if the command line value is larger) uint32 xmlTimeoutRetriesValue = GetTimeoutValue(testInfo->data[i]); uint32 testTimeoutRetriesValue = GetTimeoutValue(TestTimeoutRetries); if (xmlTimeoutRetriesValue < testTimeoutRetriesValue) { testInfo->data[i] = TestTimeoutRetries; } } } 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; } const 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, const 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( const char *a, const 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, const char *delim, const 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; ConditionNodeList * conditionNodeList = NULL; ConditionNodeList * conditionNodeLast = NULL; // 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 = NULL; 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]; const 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 && !GStopDueToError; 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; const 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((int32)pDir->NumVariationsRun == 0); ASSERTNR((int32)pDir->NumDiffs == 0); ASSERTNR((int32)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 && !GStopDueToError; 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 || GStopDueToError) break; // Poll for new stuff. If the thread is set to stop, then go away. // Use DirectoryQueue and schedule each directory on a single thread. pDir = DirectoryTestQueue.GetNextItem_NoBlock(1000); 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; } while(TRUE) { if(bThreadStop || GStopDueToError) break; // Poll for new stuff. If the thread is set to stop, then go away. // Use DirectoryAndTestCaseQueue and schedule each test on it's own thread. pDirAndTest = DirectoryAndTestCaseQueue.GetNextItem_NoBlock(1000); 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; char flagBuff[64]; 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) { exit(1); } else { #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(); if (FRLFE) { if (Mode == RM_EXE) { RLFEAddTest(RLS_EXE, pDirectory); } else { if (FBaseline) RLFEAddTest(RLS_BASELINES, pDirectory); if (FDiff) RLFEAddTest(RLS_DIFFS, pDirectory); } } strcpy_s(flagBuff, "sequential"); BOOL isSerialDirectory = HasInfoList(pDir->defaultTestInfo.data[TIK_TAGS], XML_DELIM, flagBuff, XML_DELIM, false); if(isSerialDirectory) { DirectoryTestQueue.Append(pDirectory); } else { // 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); // 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, (int32)NumVariationsTotal[RLS_EXE]); } else { if (FBaseline) RLFEAddRoot(RLS_BASELINES, (int32)NumVariationsTotal[RLS_BASELINES]); if (FDiff) RLFEAddRoot(RLS_DIFFS, (int32)NumVariationsTotal[RLS_DIFFS]); } RLFEAddRoot(RLS_TOTAL, (int32)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 ((int32)NumFailuresTotal[RLS_TOTAL] == 0L) ? 0 : 1; }