private static bool IsFailureMessage(string line) { if (line == null) { return(false); } if (line == "A crash has been intercepted by the crash handler. For call stack and other details, see the latest crash report generated in:") { if (Program.ExpectedExitCode != 0) { RunLogger.LogWarning($"Found a crash log, but we don't expect a clean exit code, so we won't fail the build due to this:\n{line}"); return(false); } return(true); } if (line.StartsWith("The referenced script on this Behaviour")) { return(true); } var firstWord = line.Split(' ', 2)[0]; if (firstWord.EndsWith("Exception:")) { return(true); } return(false); }
private static bool ShouldOverrideOverrideBuildFailure() { if ((Flags & Flag.IgnoreErrorsOnArtifactCreation) == Flag.None) { return(false); } var success = false; if (string.IsNullOrEmpty(TestResults) && string.IsNullOrEmpty(ExpectedBuildArtifact)) { // No test or build artifacts expected then we allow failure RunLogger.LogInfo($"[{nameof(ShouldOverrideOverrideBuildFailure)}] No artifacts expected in command line"); } else if (!string.IsNullOrEmpty(TestResults) && !File.Exists(TestResults)) { // No test results have been generated so we allow failure RunLogger.LogInfo($"[{nameof(ShouldOverrideOverrideBuildFailure)}] No test results file has been generated"); } else if (!string.IsNullOrEmpty(ExpectedBuildArtifact) && !Directory.Exists(ExpectedBuildArtifact) && !File.Exists(ExpectedBuildArtifact)) { // Build artifact path has been set but nothing has been created then we also allow failure RunLogger.LogInfo($"[{nameof(ShouldOverrideOverrideBuildFailure)}] Build artifacts file/folder doesn't exist"); } else { //TODO: Temporary fix for random compilation issue. If flag is set we ignore whatever the parselogfile reports RunLogger.LogResultInfo("Errors were found in the log file but these were ignored since the artifacts were still generated and -ignoreErrorsOnArtifactCreation has been set"); success = true; } return(success); }
private static bool LogIsLocked() { FileStream stream = null; try { stream = new FileInfo(Program.LogFile).Open(FileMode.Open, FileAccess.Read, FileShare.None); } catch (IOException) { //the file is unavailable because it is: //still being written to //or being processed by another thread //or does not exist (has already been processed) RunLogger.LogWarning($"{Program.LogFile} is in use by another process. Waiting before parsing"); return(true); } finally { if (stream != null) { stream.Close(); } } //file is not locked return(false); }
static bool UpdateScene(ref List <string> file) { if (string.IsNullOrEmpty(SceneOverride)) { return(true); } if (!File.Exists($"{ProjectPath}/{SceneOverride}")) { RunLogger.LogResultError($"Failed to find {ProjectPath}/{SceneOverride}"); return(false); } RunLogger.LogInfo($"Setting m_Scenes in EditorBuildSettings.asset to {SceneOverride}"); var sceneMetaFile = File.ReadAllLines($"{ProjectPath}/{SceneOverride}.meta"); string sceneGuid = null; foreach (var line in sceneMetaFile) { if (!line.StartsWith("guid:")) { continue; } sceneGuid = line.Substring(6); break; } var sectionFound = false; for (var i = 0; i < file.Count; i++) { var line = file[i]; var trimmed = line.Trim(); if (!sectionFound) { if (trimmed.StartsWith("m_Scenes:")) { sectionFound = true; file.Insert(++i, $" - enabled: 1"); file.Insert(++i, $" path: {SceneOverride}"); file.Insert(++i, $" guid: {sceneGuid}"); } continue; } if (trimmed.StartsWith("- enabled") || trimmed.StartsWith("guid:") || trimmed.StartsWith("path:")) { file.RemoveAt(i); i--; } else { break; } } return(true); }
private static int UpdateManifest(string projectPath, List <string> packages) { if (RegistryOverride == null && packages.FirstOrDefault() == null) { return(0); } var modified = false; var manifestPath = $"{projectPath}/Packages/manifest.json"; var manifest = JObject.Parse(File.ReadAllText(manifestPath)); if (RegistryOverride != null) { RunLogger.LogInfo($"Overriding registry in manifest to be {RegistryOverride}"); if (!File.Exists(manifestPath)) { RunLogger.LogError($"Could not find {manifestPath}"); return(-1); } if (RegistryOverride == "" && manifest.ContainsKey("registry")) { manifest.Remove("registry"); } else { manifest["registry"] = RegistryOverride; } modified = true; } if (packages.FirstOrDefault() != null) { foreach (var package in packages) { if (!package.Contains("@")) { RunLogger.LogError($"-addPackage {0} is missing an @"); return(-1); } var split = package.Trim().Split("@"); var packageName = split[0]; var packageVersion = split[1]; manifest["dependencies"][packageName] = packageVersion; modified = true; } } if (modified) { File.WriteAllText(manifestPath, manifest.ToString()); } return(0); }
private static void StashLine(string line) { OwnLog.WriteLine($"{RunLogger.GetTime()}: {line}"); if (_lastLines.Count >= LinesToSave) { _lastLines.Dequeue(); } _lastLines.Enqueue(line); }
static void RestoreProjectSettings(string projectPath) { RunLogger.LogInfo($"Restoring settings in ProjectSettings.asset and EditorBuildSettings.asset to how they were"); var projectSettingsAssetPath = $"{projectPath}/ProjectSettings/ProjectSettings.asset"; var editorBuildSettingsPath = $"{projectPath}/ProjectSettings/EditorBuildSettings.asset"; File.WriteAllLines(projectSettingsAssetPath, ProjectSettingsOriginal); File.WriteAllLines(editorBuildSettingsPath, EditorBuildSettingsOriginal); }
private static bool ParseTestResults() { if (string.IsNullOrEmpty(Program.TestResults)) { return(true); } if (!File.Exists(Program.TestResults)) { RunLogger.LogResultError($"Failed to find the test results file '{Program.TestResults}', failing run"); return(false); } return(true); }
static RunResult ParseCleanedLogFileForErrors(RunResult runResult) { if (string.IsNullOrEmpty(CleanedLogFile)) { return(runResult); } if (!File.Exists(CleanedLogFile)) { return(runResult); } var content = File.ReadAllLines(CleanedLogFile); if (content.Length == 0) { return(runResult); } var printedIndex = 0; var errors = new List <string>(); foreach (var line in content) { if (!line.StartsWith("Assertion Failed:") && !line.StartsWith("Assertion failed on expression:") && !line.Contains("(Error: ")) { continue; } errors.Add(line); if (errors.Count >= 10) { break; } } if (!errors.Any()) { return(runResult); } RunLogger.LogResultError($"{CleanedLogFile} contains errors and/or assertions. Failing run"); RunLogger.LogError("Cleaned output file is not empty. Here are the first 10 errors and assertions:"); foreach (var error in errors) { RunLogger.LogError(error); } return(RunResult.Failure); }
private static bool IsValidPath(string key, string value) { if (string.IsNullOrEmpty(value)) { RunLogger.LogError($"{key} must be set"); return(false); } if (!Directory.Exists(value) && !File.Exists(value)) { RunLogger.LogError($"The path for '{key}' does not exist. '{value}'"); return(false); } return(true); }
static int SetScriptingBackend(ScriptingBackend scriptingBackend, string projectPath) { var filePath = $"{projectPath}/ProjectSettings/ProjectSettings.asset"; if (!File.Exists(filePath)) { RunLogger.LogError($"Could not find {filePath} to set scriptingBackend in"); return(-1); } var file = File.ReadAllLines(filePath).ToList(); var sectionEmptyIndex = -1; var foundSection = false; for (var i = 0; i < file.Count; i++) { var line = file[i]; var trimmed = line.Trim(); if (!foundSection) { if (!trimmed.StartsWith("scriptingBackend")) { continue; } foundSection = true; if (trimmed.EndsWith("}")) { sectionEmptyIndex = i; file[i] = " scriptingBackend: "; file.Insert(i + 1, $" Standalone: {(int)scriptingBackend}"); break; } } if (!trimmed.StartsWith("Standalone: ")) { continue; } file[i] = $" Standalone: {(int)scriptingBackend}"; break; } File.WriteAllLines(filePath, file); return(0); }
public static RunResult Run(string args) { File.Delete(Program.LogFile); RunLogger.LogInfo($"Will now run:\n{Program.Executable} {args}"); var process = new Process() { StartInfo = new ProcessStartInfo { FileName = Program.Executable, Arguments = args, RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true } }; var started = process.Start(); RunLogger.LogInfo($"Process spawned with pid: {process.Id}"); StreamReader fs; var processResult = CheckForCleanupEntry(process); process.WaitForExit(); RunLogger.LogInfo($"Execution Done! Exit code: {process.ExitCode}"); if (processResult == ProcessResult.FailedRun) { RunLogger.LogInfo("CheckForCleanupEntry flagged a failed run. Aborting"); return(RunResult.Failure); } if (process.ExitCode != Program.ExpectedExitCode) { if (processResult == ProcessResult.IgnoreExitCode) { RunLogger.LogInfo($"Exit code not {Program.ExpectedExitCode}, but this was expected in this case. Ignoring it"); } else { RunLogger.LogError($"Exit code not {Program.ExpectedExitCode}, run failed."); return(RunResult.Failure); } } return(RunResult.Success); }
internal bool GetFileGenesis(out FileGenesis _fileGenesis) { if (fileGenesis == null && !fileGenesisNotAvailable) { try { if (RunLogger.GetRunInfo(fullFileName, out RunLogger.RunInfo ri, out RunLogger.GeneralInfo gi, out RunLogger.PetInfo pi)) { fileGenesis = new FileGenesis() { runInfo = ri, generalInfo = gi, petInfo = pi } } ; else { fileGenesisNotAvailable = true; } }
private static void DeletedLogFile() { var retryLimit = 50; var retryCount = 0; do { retryCount++; try { File.Delete(Program.LogFile); } catch (IOException) { RunLogger.LogWarning($"Failed to delete logfile. Retrying in 10 seconds. Attempt {retryCount}/{retryLimit}"); Thread.Sleep(10000); } } while (retryCount < retryLimit && File.Exists(Program.LogFile)); }
static bool UpdateScriptingBackend(ref List <string> file) { if (ScriptingBackendOverride == ScriptingBackend.current) { return(true); } RunLogger.LogInfo($"Setting scriptingBackend in ProjectSettings.asset to {ScriptingBackendOverride}"); var foundSection = false; for (var i = 0; i < file.Count; i++) { var line = file[i]; var trimmed = line.Trim(); if (!foundSection) { if (!trimmed.StartsWith("scriptingBackend")) { continue; } foundSection = true; if (trimmed.EndsWith("}")) { file[i] = " scriptingBackend: "; file.Insert(i + 1, $" Standalone: {(int)ScriptingBackendOverride}"); break; } } if (!trimmed.StartsWith("Standalone: ")) { continue; } file[i] = $" Standalone: {(int)ScriptingBackendOverride}"; break; } return(true); }
static bool UpdateResolutionDialog(ref List <string> file) { if (DisplayResolutionDialogOverride == DisplayResolutionDialog.current) { return(true); } RunLogger.LogInfo($"Setting displayResolutionDialog in ProjectSettings.asset to {DisplayResolutionDialogOverride}"); for (var i = 0; i < file.Count; i++) { var line = file[i]; var trimmed = line.Trim(); if (!trimmed.StartsWith("displayResolutionDialog:")) { continue; } file[i] = $" displayResolutionDialog: {(int)DisplayResolutionDialogOverride}"; break; } return(true); }
static int UpdateProjectSettings(string projectPath) { var projectSettingsAssetPath = $"{projectPath}/ProjectSettings/ProjectSettings.asset"; var editorBuildSettingsPath = $"{projectPath}/ProjectSettings/EditorBuildSettings.asset"; if (!File.Exists(projectSettingsAssetPath)) { RunLogger.LogError($"Could not find {projectSettingsAssetPath}"); return(-1); } if (!File.Exists(editorBuildSettingsPath)) { RunLogger.LogError($"Could not find {editorBuildSettingsPath}"); return(-1); } ProjectSettingsOriginal = File.ReadAllLines(projectSettingsAssetPath).ToList(); EditorBuildSettingsOriginal = File.ReadAllLines(editorBuildSettingsPath).ToList(); var projectSettingsAsset = File.ReadAllLines(projectSettingsAssetPath).ToList(); var editorBuildSettings = File.ReadAllLines(editorBuildSettingsPath).ToList(); if (!UpdateScriptingBackend(ref projectSettingsAsset)) { return(-1); } if (!UpdateResolutionDialog(ref projectSettingsAsset)) { return(-1); } if (!UpdateScene(ref editorBuildSettings)) { return(-1); } File.WriteAllLines(projectSettingsAssetPath, projectSettingsAsset); File.WriteAllLines(editorBuildSettingsPath, editorBuildSettings); return(0); }
static int Main(string[] args) { var options = new OptionSet { { "batchmode", "Run Unity in batch mode. This should always be used in conjunction with the other command line arguments, because it ensures no pop-up windows appear and eliminates the need for any human intervention. When an exception occurs during execution of the script code, the Asset server updates fail, or other operations that fail, Unity immediately exits with return code 1. \nNote that in batch mode, Unity sends a minimal version of its log output to the RunLogger However, the Log Files still contain the full log information. Opening a project in batch mode while the Editor has the same project open is not supported; only a single instance of Unity can run at a time.", v => Flags |= Flag.Batchmode }, { "quit", "Quit the Unity Editor after other commands have finished executing. Note that this can cause error messages to be hidden (however, they still appear in the Editor.log file).", v => Flags |= Flag.Quit }, { "nographics", "When running in batch mode, do not initialize the graphics device at all. This makes it possible to run your automated workflows on machines that don’t even have a GPU (automated workflows only work when you have a window in focus, otherwise you can’t send simulated input commands). Please note that -nographics does not allow you to bake GI, since Enlighten requires GPU acceleration.", v => Flags |= Flag.NoGraphics }, { "automated", "This flag enables some extra logging when running tests. Like when a test is started/finished/etc which is helpful for identifying which test is logging something or what the result of the test was if you don't want to parse the testresults file.", v => Flags |= Flag.Automated }, { "silentcrashes", "Don’t display a crash dialog.", v => Flags |= Flag.SilentCrashes }, { "ignoreErrorsOnArtifactCreation", "Workaround to a random compilation issue introduced in 2018.2+ on slower machines. In that you sometimes get random compilation errors but artifacts are still being produced. So this flag will ignore the errors if the testresults or standalone build artifacts has been generated.", v => Flags |= Flag.IgnoreErrorsOnArtifactCreation }, { "warningsaserrors", "Treat warnings as errors.", v => Flags |= Flag.WarningsAsErrors }, { "runtests", "Executes tests in the project", v => Flags |= Flag.RunTests }, { "unityexecutable=", "Path to unity executable that should run this command", v => UnityExecutable = v }, { "expectedexitcode=", "If you for some reason don't expect to get exit code 0 from the run and want to enforce it", v => ExpectedExitCode = int.Parse(v) }, { "projectpath=", "Open the project at the given path.", v => ProjectPath = v }, { "logfile=", "Specify where the Editor or Windows/Linux/OSX standalone log file are written.", v => LogFile = v }, { "cleanedLogFile=", "Logs file that should only contain important messages (warnings, errors, assertions). If this is set and the specified file is not empty after the run the run will be flagged as failed.", v => CleanedLogFile = v }, { "testresults=", "The path indicating where the result file should be saved. The result file is saved in Project’s root folder by default.", v => TestResults = v }, { "scriptingBackend=", "Will hack in the scripting backend for standalone in the ProjectSettings.asset so you can easily toggle it in CI. Valid values are: mono, il2cpp", v => ScriptingBackendOverride = Enum.Parse <ScriptingBackend>(v) }, { "displayResolutionDialog=", "Will hack in if the display resolution dialog should be enabled or not in built players into the ProjectSettings.asset. Valid values are: enabled, disabled", v => DisplayResolutionDialogOverride = Enum.Parse <DisplayResolutionDialog>(v) }, { "buildLinuxUniversalPlayer=", "Build a combined 32-bit and 64-bit standalone Linux player (for example, -buildLinuxUniversalPlayer path/to/your/build).", v => buildLinuxUniversalPlayer = v }, { "buildOSXUniversalPlayer=", "Build a combined 32-bit and 64-bit standalone Mac OSX player (for example, -buildOSXUniversalPlayer path/to/your/build.app).", v => buildOSXUniversalPlayer = v }, { "registryoverride=", "If you for some reason need to use a custom npm registry than the one in the manifest.json (maybe you have a fast one for your build system). Empty string removes the field", v => RegistryOverride = v }, { "buildWindows64Player=", "Build a 64-bit standalone Windows player (for example, -buildWindows64Player path/to/your/build.exe).", v => buildWindows64Player = v }, { "timeout=", "Timeout the execution after the supplied seconds. Will fail run if -timeoutIgnore is not set and it times out", v => ExecutionTimeout = int.Parse(v) }, { "timeoutIgnore", "Indicates that if the execution times out it should not flag it as a failure if everything else is ok", v => Flags |= Flag.TimeoutIgnore }, { "scene=", "Modifies the scene list to build with only the scene listed here. Needs to be the relative path to the file from the project path", v => SceneOverride = v }, { "addPackage=", "Modifies the Packages/manifest.json of the project to include the package specified. Use the format packagename@version. Ex: [email protected]. Command can be repeated for multiple packages", v => AddPackages.Add(v) }, { "buildTarget=", $"Allows the selection of an active build target before loading a project. Possible options are: {string.Join(", ", Enum.GetNames(typeof(BuildTargets)).ToList())}.", v => BuildTarget = v } }; try { ExtraArgs = options.Parse(args); } catch (OptionException e) { RunLogger.LogError(e.Message); RunLogger.Dump(); options.WriteOptionDescriptions(Console.Out); throw; } if (ExtraArgs.Any()) { RunLogger.LogInfo($"Unknown commands passed. These will be passed a long to the process:\n {string.Join(" ", ExtraArgs)}"); } if (!IsValidPath("unityexecutable", UnityExecutable)) { RunLogger.Dump(); options.WriteOptionDescriptions(Console.Out); return(-1); } if (!IsValidPath("projectpath", ProjectPath)) { RunLogger.Dump(); options.WriteOptionDescriptions(Console.Out); return(-1); } if (string.IsNullOrEmpty(LogFile)) { RunLogger.LogError("logfile must be set"); RunLogger.Dump(); options.WriteOptionDescriptions(Console.Out); return(-1); } var result = UpdateProjectSettings(ProjectPath); if (result != 0) { RunLogger.Dump(); return(-1); } result = UpdateManifest(ProjectPath, AddPackages); if (result != 0) { RunLogger.Dump(); return(-1); } if (!IsValidPath("logfile", new FileInfo(LogFile).Directory.FullName)) { RunLogger.Dump(); return(-1); } var sb = new StringBuilder(); sb.Append($"-logFile \"{Path.GetFullPath(LogFile)}\" "); sb.Append($"-projectPath \"{Path.GetFullPath(ProjectPath)}\" "); if (!string.IsNullOrEmpty(CleanedLogFile)) { sb.Append($"-cleanedLogFile \"{Path.GetFullPath(CleanedLogFile)}\" "); } if (ExtraArgs.Any()) { sb.Append(string.Join(" ", ExtraArgs)); sb.Append(" "); } if ((Flags & Flag.Batchmode) != Flag.None) { RunLogger.LogInfo("Batchmode is set"); sb.Append("-batchmode "); } if ((Flags & Flag.NoGraphics) != Flag.None) { RunLogger.LogInfo("Nographics is set"); sb.Append("-nographics "); } if ((Flags & Flag.SilentCrashes) != Flag.None) { RunLogger.LogInfo("silentcrashes is set"); sb.Append("-silent-crashes "); } if ((Flags & Flag.WarningsAsErrors) != Flag.None) { RunLogger.LogInfo("warningsaserrors is set"); } if ((Flags & Flag.TimeoutIgnore) != Flag.None) { RunLogger.LogInfo("timeoutIgnore is set"); } if ((Flags & Flag.IgnoreErrorsOnArtifactCreation) != Flag.None) { RunLogger.LogInfo("ignoreErrorsOnArtifactCreation is set"); } if ((Flags & Flag.Automated) != Flag.None) { RunLogger.LogInfo("automated is set"); sb.Append("-automated "); } if (!string.IsNullOrEmpty(BuildTarget)) { if (!Enum.TryParse <BuildTargets>(BuildTarget, out var parsedEnum)) { RunLogger.LogError($"{BuildTarget} is not a valid buildtarget. It has to be one of:\n{string.Join(", ", Enum.GetNames(typeof(BuildTargets)))}"); return(-1); } sb.Append($"-buildTarget {BuildTarget} "); } if ((Flags & Flag.RunTests) != Flag.None) { RunLogger.LogInfo("runtests is set"); sb.Append("-runTests "); if (string.IsNullOrEmpty(TestResults)) { RunLogger.LogWarning("It is not recommended to set runtests but not testresults. It will not be able to parse the test results as part of the report"); } else { sb.Append($"-testResults \"{Path.GetFullPath(TestResults)}\" "); } } if ((Flags & Flag.Quit) != Flag.None) { RunLogger.LogInfo("Quit is set"); if ((Flags & Flag.RunTests) != Flag.None) { RunLogger.LogWarning("quit and runtests cannot be set at once. Ignoring quit command"); } else { sb.Append("-quit "); } } if (!string.IsNullOrEmpty(buildWindows64Player)) { RunLogger.LogInfo("buildWindows64Player is set"); sb.Append($"-buildWindows64Player {buildWindows64Player} "); ExpectedBuildArtifact = $"{ProjectPath}/{buildWindows64Player}"; } if (!string.IsNullOrEmpty(buildLinuxUniversalPlayer)) { RunLogger.LogInfo("buildLinuxUniversalPlayer is set"); sb.Append($"-buildLinuxUniversalPlayer {buildLinuxUniversalPlayer} "); ExpectedBuildArtifact = $"{ProjectPath}/{buildLinuxUniversalPlayer}"; } if (!string.IsNullOrEmpty(buildOSXUniversalPlayer)) { RunLogger.LogInfo("buildOSXUniversalPlayer is set"); sb.Append($"-buildOSXUniversalPlayer {buildOSXUniversalPlayer} "); ExpectedBuildArtifact = $"{ProjectPath}/{buildOSXUniversalPlayer}"; } var stopwatch = new Stopwatch(); stopwatch.Start(); var runResult = UnityLauncher.Run(sb.ToString()); RunLogger.LogResultInfo($"Command execution took: {stopwatch.Elapsed}"); RestoreProjectSettings(ProjectPath); if (runResult == RunResult.FailedToStart) { RunLogger.Dump(); return(-1); } if (!LogParser.Parse()) { runResult = RunResult.Failure; } if (runResult != RunResult.Success && ShouldOverrideOverrideBuildFailure()) { runResult = RunResult.Success; } if ((Flags & Flag.RunTests) != Flag.None) { if (File.Exists(TestResults)) { RunLogger.LogInfo($"Parsing {TestResults}"); if (CheckTestResults.Parse(TestResults) != RunResult.Success) { runResult = RunResult.Failure; } } else { RunLogger.LogError($"Could not find {TestResults}"); runResult = RunResult.Failure; } } runResult = ParseCleanedLogFileForErrors(runResult); if (runResult != RunResult.Success) { RunLogger.LogResultError("Run has failed"); RunLogger.Dump(); return(-1); } if (!string.IsNullOrEmpty(ExpectedBuildArtifact) && !(File.Exists(ExpectedBuildArtifact) || Directory.Exists(ExpectedBuildArtifact))) { RunLogger.LogResultError($"Expected to find {ExpectedBuildArtifact} after the execution but it is missing. Check the log for what could have gone wrong"); RunLogger.Dump(); return(-1); } RunLogger.LogResultInfo("Everything looks good. Run has passed"); RunLogger.Dump(); return(0); }
private static ProcessResult CheckForCleanupEntry(Process process) { var fs = new FileStream(Program.LogFile, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite); var timeoutStopwatch = new Stopwatch(); timeoutStopwatch.Start(); using (var stream = new StreamReader(fs)) { var waitingForDeath = false; var failureMessagePrinted = false; while (true) { var line = stream.ReadLine(); if (line == null) { // Let's chill if there is nothing new Thread.Sleep(10); } if (IsFailureMessage(line)) { failureMessagePrinted = true; RunLogger.LogError(line); } if (IsExitMessage(line)) { RunLogger.LogInfo("Found shutdown log print. Waiting 10 seconds for process to quit"); waitingForDeath = true; } else if (Program.ExecutionTimeout.HasValue && timeoutStopwatch.ElapsedMilliseconds > Program.ExecutionTimeout.Value * 1000) { if ((Program.Flags & Program.Flag.TimeoutIgnore) != Program.Flag.None) { RunLogger.LogResultInfo($"Execution timed out after {Program.ExecutionTimeout.Value} seconds"); process.Kill(); return(ProcessResult.IgnoreExitCode); } RunLogger.LogResultError($"Execution timed out after {Program.ExecutionTimeout.Value} seconds. Failing run"); process.Kill(); return(ProcessResult.FailedRun); } if (process.HasExited) { if (waitingForDeath) { RunLogger.LogInfo("Player has exited cleanly."); return(ProcessResult.UseExitCode); } while ((line = stream.ReadLine()) != null) { if (IsFailureMessage(line)) { failureMessagePrinted = true; } if (!IsExitMessage(line)) { continue; } RunLogger.LogInfo("Player has exited cleanly."); return(ProcessResult.UseExitCode); } if (failureMessagePrinted) { RunLogger.LogResultError("The process has exited, but a log failure message was detected, flagging run as failed."); return(ProcessResult.FailedRun); } return(ProcessResult.UseExitCode); } } } }
static RunResult ParsedCleanedLogFileForErrors(RunResult runResult) { if (string.IsNullOrEmpty(CleanedLogFile)) { return(runResult); } if (!File.Exists(CleanedLogFile)) { RunLogger.LogResultError($"Was expecting to find {CleanedLogFile} but it was missing. Failing run."); return(RunResult.Failure); } var content = File.ReadAllLines(CleanedLogFile); if (content.Length == 0) { return(runResult); } var errors = new List <string>(); if ((Flags & Flag.EnforceEmptyCleanedLogFile) != Flag.None) { RunLogger.LogError("enforceEmptyCleanedLogFile was set and cleanedLogFile is not empty. Will fail run."); errors = content.ToList(); } else { foreach (var line in content) { var isError = false; if (line.StartsWith("Assertion Failed:")) { isError = true; } else if (line.StartsWith("Assertion failed on expression:")) { isError = true; } else if (line.StartsWith("The referenced script on this Behaviour")) { isError = true; } else if (line.Contains("(Error: ")) { isError = true; } if (!isError) { var firstWord = line.Split(' ', 2)[0]; if (firstWord.EndsWith("Exception:")) { isError = true; } } if (!isError) { continue; } errors.Add(line); if (errors.Count >= 10) { break; } } } if (!errors.Any()) { return(runResult); } RunLogger.LogResultError($"{CleanedLogFile} contains errors and/or assertions. Failing run"); RunLogger.LogError("Cleaned output file is not empty. Here are the first 10 errors and assertions:"); foreach (var error in errors) { RunLogger.LogError(error); } return(RunResult.Failure); }
static int Main(string[] args) { var options = new OptionSet { { "batchmode", "Run Unity in batch mode. This should always be used in conjunction with the other command line arguments, because it ensures no pop-up windows appear and eliminates the need for any human intervention. When an exception occurs during execution of the script code, the Asset server updates fail, or other operations that fail, Unity immediately exits with return code 1. \nNote that in batch mode, Unity sends a minimal version of its log output to the RunLogger However, the Log Files still contain the full log information. Opening a project in batch mode while the Editor has the same project open is not supported; only a single instance of Unity can run at a time.", v => Flags |= Flag.Batchmode }, { "nographics", "When running in batch mode, do not initialize the graphics device at all. This makes it possible to run your automated workflows on machines that don’t even have a GPU (automated workflows only work when you have a window in focus, otherwise you can’t send simulated input commands). Please note that -nographics does not allow you to bake GI, since Enlighten requires GPU acceleration.", v => Flags |= Flag.NoGraphics }, { "executable=", "Path to unity executable that should run this command", v => Executable = v }, { "expectedexitcode=", "If you for some reason don't expect to get exit code 0 from the run and want to enforce it", v => ExpectedExitCode = int.Parse(v) }, { "logfile=", "Specify where the Editor or Windows/Linux/OSX standalone log file are written.", v => LogFile = v }, { "cleanedLogFile=", "Logs file that should only contain important messages (warnings, errors, assertions). If this is set and the specified file is not empty after the run the run will be flagged as failed.", v => CleanedLogFile = v }, { "enforceEmptyCleanedLogFile", "If this flag is set the run will fail if anything at all is logged to the cleanedLogFile which is useful if you know the player should never log if everything is running fine", v => Flags |= Flag.EnforceEmptyCleanedLogFile }, { "timeout=", "Timeout the execution after the supplied seconds. Will fail run if -timeoutIgnore is not set and it times out", v => ExecutionTimeout = int.Parse(v) }, { "timeoutIgnore", "Indicates that if the execution times out it should not flag it as a failure if everything else is ok", v => Flags |= Flag.TimeoutIgnore }, { "screenheight=", "Sets the height of the window", v => ScreenHeight = int.Parse(v) }, { "screenwidth=", "Sets the width of the window", v => ScreenWidth = int.Parse(v) }, { "screenquality=", "Sets the render quality. Should be the name of the quality level", v => ScreenQuality = v }, }; try { ExtraArgs = options.Parse(args); } catch (OptionException e) { RunLogger.LogError(e.Message); RunLogger.Dump(); options.WriteOptionDescriptions(Console.Out); throw; } if (ExtraArgs.Any()) { RunLogger.LogInfo($"Unknown commands passed. These will be passed a long to the process:\n {string.Join(" ", ExtraArgs)}"); } if (!IsValidPath("executable", Executable)) { RunLogger.Dump(); options.WriteOptionDescriptions(Console.Out); return(-1); } if (Executable.EndsWith(".app")) { var fileName = Path.GetFileNameWithoutExtension(Executable); Executable += $"/Contents/MacOS/{fileName}"; } if (string.IsNullOrEmpty(LogFile)) { RunLogger.LogError("logfile must be set"); RunLogger.Dump(); options.WriteOptionDescriptions(Console.Out); return(-1); } if (!IsValidPath("logfile", new FileInfo(LogFile).Directory.FullName)) { RunLogger.Dump(); options.WriteOptionDescriptions(Console.Out); return(-1); } var sb = new StringBuilder(); sb.Append($"-logFile \"{Path.GetFullPath(LogFile)}\" "); if (!string.IsNullOrEmpty(CleanedLogFile)) { sb.Append($"-cleanedLogFile \"{Path.GetFullPath(CleanedLogFile)}\" "); } if (ExtraArgs.Any()) { sb.Append(string.Join(" ", ExtraArgs)); sb.Append(" "); } if (ExpectedExitCode != 0) { RunLogger.LogInfo($"Expected exit code or the run will fail: {ExpectedExitCode}"); } if ((Flags & Flag.Batchmode) != Flag.None) { RunLogger.LogInfo("Batchmode is set"); sb.Append("-batchmode "); } if ((Flags & Flag.NoGraphics) != Flag.None) { RunLogger.LogInfo("Nographics is set"); sb.Append("-nographics "); } if ((Flags & Flag.TimeoutIgnore) != Flag.None) { RunLogger.LogInfo("timeoutIgnore is set"); } if (ScreenHeight.HasValue) { sb.Append($"-screen-height {ScreenHeight} "); } if (ScreenWidth.HasValue) { sb.Append($"-screen-width {ScreenWidth} "); } if (!string.IsNullOrEmpty(ScreenQuality)) { sb.Append($"-screen-quality {ScreenQuality} "); } if ((Flags & Flag.EnforceEmptyCleanedLogFile) != Flag.None) { RunLogger.LogInfo("enforceEmptyCleanedLogFile has been set. Will fail run if cleanedLogFile contents is not empty after run"); } var stopwatch = new Stopwatch(); stopwatch.Start(); var runResult = PlayerLauncher.Run(sb.ToString()); RunLogger.LogResultInfo($"Command execution took: {stopwatch.Elapsed}"); if (runResult == RunResult.FailedToStart) { return(-1); } //if (!LogParser.Parse()) // runResult = RunResult.Failure; runResult = ParsedCleanedLogFileForErrors(runResult); if (runResult != RunResult.Success) { RunLogger.LogResultError("Run has failed"); RunLogger.Dump(); return(-1); } RunLogger.LogResultInfo("Everything looks good. Run has passed"); RunLogger.Dump(); return(0); }
private static ProcessResult CheckForCleanupEntry(Process process) { var fs = new FileStream(Program.LogFile, FileMode.OpenOrCreate, FileAccess.Read, FileShare.ReadWrite); var timeoutStopwatch = new Stopwatch(); timeoutStopwatch.Start(); using (var stream = new StreamReader(fs)) { var waitingForDeath = false; var waitingForDeathCounter = 10; var failureMessagePrinted = false; var timeoutMessagePrinted = false; while (true) { var line = stream.ReadLine(); if (line != null) { StashLine(line); } else { // Let's chill if there is nothing new Thread.Sleep(10); } if (IsFailureMessage(line)) { failureMessagePrinted = true; RunLogger.LogError($"Failure message in the log: {line}"); } if (IsInstabilityMessage(line)) { timeoutMessagePrinted = true; RunLogger.LogError($"Instability message in the log: {line}"); } if (IsExitMessage(line)) { RunLogger.LogInfo("Found editor shutdown log print. Waiting 10 seconds for process to quit"); waitingForDeath = true; } if (waitingForDeath) { Thread.Sleep(1000); if (waitingForDeathCounter-- <= 0) { if (timeoutMessagePrinted) { // Hopefully temporary hack to work around potential packman timeout issues. // So if we detect a timeout error in the log then we may want to just retry the entire run RunLogger.LogError("Editor did not quit after 10 seconds, but was also timed out. Forcibly quitting and retrying"); process.Kill(); return(ProcessResult.Timeout); } RunLogger.LogInfo("Editor did not quit after 10 seconds. Forcibly quitting and whitelisting the exit code"); process.Kill(); return(ProcessResult.IgnoreExitCode); } } else if (Program.ExecutionTimeout.HasValue && timeoutStopwatch.ElapsedMilliseconds > Program.ExecutionTimeout.Value * 1000) { if ((Program.Flags & Program.Flag.TimeoutIgnore) != Program.Flag.None) { RunLogger.LogResultInfo($"Execution timed out after {Program.ExecutionTimeout.Value} seconds"); process.Kill(); return(ProcessResult.IgnoreExitCode); } RunLogger.LogResultError($"Execution timed out after {Program.ExecutionTimeout.Value} seconds. Failing run"); process.Kill(); return(ProcessResult.FailedRun); } if (process.HasExited) { while ((line = stream.ReadLine()) != null) { StashLine(line); if (IsInstabilityMessage(line)) { timeoutMessagePrinted = true; RunLogger.LogError($"Instability message in the log: {line}"); } if (IsFailureMessage(line)) { failureMessagePrinted = true; } if (!IsExitMessage(line)) { continue; } if (timeoutMessagePrinted) { continue; } if (failureMessagePrinted) { continue; } RunLogger.LogInfo("Unity has exited cleanly."); return(ProcessResult.UseExitCode); } if (timeoutMessagePrinted) { // Hopefully temporary hack to work around potential packman timeout issues. // So if we detect a timeout error in the log then we may want to just retry the entire run RunLogger.LogError("The unity process has exited, but a timeout message was found, flagging run as timed out."); return(ProcessResult.Timeout); } if (waitingForDeath) { RunLogger.LogInfo("Unity has exited cleanly."); return(ProcessResult.UseExitCode); } if (failureMessagePrinted) { RunLogger.LogResultError("The unity process has exited, but a log failure message was detected, flagging run as failed."); return(ProcessResult.FailedRun); } var writer = new StringWriter(); writer.WriteLine($"The unity process has exited, but did not print the proper cleanup, did it crash? Marking as failed. The last {LinesToSave} lines of the log was:"); foreach (var entry in _lastLines) { writer.WriteLine($" {entry}"); } RunLogger.LogResultError(writer.ToString()); return(ProcessResult.FailedRun); } } } }
public static RunResult Run(string args) { RunLogger.LogInfo($"Will now run:\n{Program.UnityExecutable} {args}"); OwnLog = new StreamWriter(Program.LogFile + ".timings.log"); ProcessResult processResult; const int retryLimit = 50; var retryCount = 0; Process process; do { DeletedLogFile(); if (retryCount != 0) { RunLogger.LogWarning($"Previous run timed out. Trying again in 30 seconds. This is attempt {retryCount}/{retryLimit}"); Thread.Sleep(30000); } process = new Process() { StartInfo = new ProcessStartInfo { FileName = Program.UnityExecutable, Arguments = args, RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true } }; var started = process.Start(); RunLogger.LogInfo($"Unity process spawned with pid: {process.Id}"); processResult = CheckForCleanupEntry(process); process.WaitForExit(); retryCount++; } while (processResult == ProcessResult.Timeout && retryCount < retryLimit); OwnLog.Close(); RunLogger.LogInfo($"Execution Done! Exit code: {process.ExitCode}"); if (processResult == ProcessResult.Timeout) { RunLogger.LogResultError("The run has timed out and exhausted the allowed retry count. Failing run"); return(RunResult.Failure); } if (processResult == ProcessResult.FailedRun) { RunLogger.LogInfo("CheckForCleanupEntry flagged a failed run. Aborting"); return(RunResult.Failure); } if (process.ExitCode != Program.ExpectedExitCode) { if (processResult == ProcessResult.IgnoreExitCode) { RunLogger.LogInfo($"Exit code not {Program.ExpectedExitCode}, but this was expected in this case. Ignoring it"); } else { RunLogger.LogError($"Exit code not {Program.ExpectedExitCode}, run failed."); return(RunResult.Failure); } } return(RunResult.Success); }
private static bool ParseLogFile() { var success = true; RunLogger.LogInfo($"Parsing log located at {Path.GetFileName(Program.LogFile)}"); while (LogIsLocked()) { Thread.Sleep(1000); } var grabCallStack = false; var lastIssueWasError = false; using (var reader = new StreamReader(Program.LogFile)) { string line; while ((line = reader.ReadLine()) != null) { if (string.IsNullOrWhiteSpace(line)) { continue; } if (line.Contains(": error CS") || line.Contains(": Internal compiler error:") || line.StartsWith("UnityException: ") || line.StartsWith("Exception: ") || line.StartsWith("Assertion Failed:") || line.StartsWith("Assertion failed on expression:") || line.Contains("(Error: ") || line.EndsWith(": One or more errors occurred.") || ( line.StartsWith("(0,0):") && (line.Contains("Exception:") || line.Contains("One or more errors occurred.")) )) { grabCallStack = true; lastIssueWasError = true; CompilerErrors.Add(line); success = false; continue; } if (line.Contains(": warning CS")) { CompilerWarnings.Add(line); lastIssueWasError = false; grabCallStack = true; continue; } if (line.Contains("Aborting batchmode due to failure:") || line.Contains("Scripts have compiler errors.")) { success = false; continue; } var firstWord = line.Split(' ', 2)[0]; if (firstWord.EndsWith("Exception:")) { grabCallStack = true; lastIssueWasError = true; CompilerErrors.Add(line); success = false; continue; } if (grabCallStack) { if (line.StartsWith(" at ") || line.StartsWith("(Filename:") || line.Contains(" --- End of inner exception stack trace ---") || line.StartsWith("---> (Inner Exception") || line.StartsWith("Parameter name: ")) { if (lastIssueWasError) { var index = CompilerErrors.Count - 1; CompilerErrors[index] = $"{CompilerErrors[index]}\n{line}"; } else { var index = CompilerWarnings.Count - 1; CompilerWarnings[index] = $"{CompilerWarnings[index]}\n{line}"; } } else { grabCallStack = false; continue; } } } } if (CompilerWarnings.Any()) { Console.Write("\n"); RunLogger.LogInfo("Found compiler warnings:"); var warningsToPrint = CompilerWarnings.Count > 30 ? 30 : CompilerWarnings.Count; for (var i = 0; i < warningsToPrint; i++) { RunLogger.LogWarning(CompilerWarnings[i]); } if (warningsToPrint < CompilerWarnings.Count) { RunLogger.LogWarning($"Did not print all warnings. Stopped after {warningsToPrint}. There are {CompilerWarnings.Count - warningsToPrint} that have not been printed."); } if ((Program.Flags & Program.Flag.WarningsAsErrors) != Program.Flag.None) { success = false; RunLogger.LogResultError("warningsasserrors has been set so marking the run as failed due to warnings"); } Console.Write("\n"); } if (CompilerErrors.Any()) { Console.Write("\n"); RunLogger.LogError("Found compiler errors:"); var errorsToPrint = CompilerErrors.Count > 30 ? 30 : CompilerErrors.Count; for (var i = 0; i < errorsToPrint; i++) { RunLogger.LogError(CompilerErrors[i]); } if (errorsToPrint < CompilerErrors.Count) { RunLogger.LogError($"Did not print all errors. Stopped after {errorsToPrint}. There are {CompilerErrors.Count - errorsToPrint} that have not been printed."); } success = false; Console.Write("\n"); } if (!success) { RunLogger.LogResultError($"[Compiler] Found {LogParser.CompilerWarnings.Count} warnings and {LogParser.CompilerErrors.Count} errors"); } else { RunLogger.LogResultInfo($"[Compiler] Found {LogParser.CompilerWarnings.Count} warnings and {LogParser.CompilerErrors.Count} errors"); } return(success); }
public static RunResult Parse(string resultFileName) { Failures = new List <FailedTest>(); CheckTestResults.resultFileName = resultFileName; try { var fstream = new FileStream(resultFileName, FileMode.Open, FileAccess.Read); try { XmlDocument xmlDoc = new XmlDocument(); xmlDoc.Load(fstream); LoadTestResults(xmlDoc.DocumentElement); } finally { fstream.Close(); } } catch (Exception ex) { RunLogger.LogResultError($"Failed to parse {resultFileName}:\n{ex.Message}"); return(RunResult.FailedToStart); } var result = RunResult.Success; if (Failures.Count > 0) { RunLogger.LogError("Test failures found:"); for (int i = 0; i < Failures.Count; i++) { var paddedMessage = string.Join("\n", Failures[i].Message.Split('\n').Select((l) => $" {l}").ToList()); var paddedStacktrace = string.Join("\n", Failures[i].StackTrace.Split('\n').Select((l) => $" {l}").ToList()); RunLogger.LogError($" {(i + 1)}: {Failures[i].Name}\n{paddedMessage}\n{paddedStacktrace}\n\n"); } result = RunResult.Failure; } RunLogger.LogResultInfo("Test results:"); RunLogger.LogResultInfo($" Total: {Summary.Total}"); RunLogger.LogResultInfo($" Passed: {Summary.Passed}"); RunLogger.LogResultInfo($" Failed: {Summary.Failed}"); RunLogger.LogResultInfo($" Skipped: {Summary.Skipped}"); RunLogger.LogResultInfo($" Inconclusive: {Summary.Inconclusive}"); if (Summary.Total == 0) { RunLogger.LogResultInfo("No tests were executed"); result = RunResult.Failure; } var actualCount = Summary.Passed + Summary.Failed + Summary.Skipped + Summary.Inconclusive; if (Summary.Total != actualCount) { RunLogger.LogResultError($"Test result sums don't match. Total was reported as {Summary.Total} but the numbers add up to {actualCount}"); result = RunResult.Failure; } return(result); }