| // |
| // Copyright (c) 2010-2024 Antmicro |
| // |
| // This file is licensed under the MIT License. |
| // Full license text is available in 'licenses/MIT.txt'. |
| // |
| using System; |
| using System.IO; |
| using System.Linq; |
| using System.Collections.Generic; |
| using Antmicro.Renode.Core; |
| using Antmicro.Renode.UserInterface; |
| using Antmicro.Renode.Utilities; |
| using Antmicro.Renode.Logging; |
| using Antmicro.Renode.Exceptions; |
| using System.Runtime.ExceptionServices; |
| |
| namespace Antmicro.Renode.RobotFramework |
| { |
| internal class RenodeKeywords : IRobotFrameworkKeywordProvider |
| { |
| public RenodeKeywords() |
| { |
| monitor = ObjectCreator.Instance.GetSurrogate<Monitor>(); |
| savepoints = new Dictionary<string, Savepoint>(); |
| if(!(monitor.Interaction is CommandInteractionWrapper)) |
| { |
| monitor.Interaction = new CommandInteractionWrapper(monitor.Interaction); |
| } |
| } |
| |
| public void Dispose() |
| { |
| var interaction = monitor.Interaction as CommandInteractionWrapper; |
| monitor.Interaction = interaction.UnderlyingCommandInteraction; |
| TemporaryFilesManager.Instance.Cleanup(); |
| } |
| |
| [RobotFrameworkKeyword] |
| public void ResetEmulation() |
| { |
| EmulationManager.Instance.Clear(); |
| Recorder.Instance.ClearEvents(); |
| } |
| |
| [RobotFrameworkKeyword(replayMode: Replay.Always)] |
| public void StartEmulation() |
| { |
| EmulationManager.Instance.CurrentEmulation.StartAll(); |
| } |
| |
| [RobotFrameworkKeyword] |
| public string ExecuteCommand(string command, string machine = null) |
| { |
| var interaction = monitor.Interaction as CommandInteractionWrapper; |
| interaction.Clear(); |
| SetMonitorMachine(machine); |
| |
| if(!monitor.Parse(command)) |
| { |
| throw new KeywordException("Could not execute command '{0}': {1}", command, interaction.GetError()); |
| } |
| |
| var error = interaction.GetError(); |
| if(!string.IsNullOrEmpty(error)) |
| { |
| throw new KeywordException($"There was an error when executing command '{command}': {error}"); |
| } |
| |
| return interaction.GetContents(); |
| } |
| |
| [RobotFrameworkKeyword] |
| public object ExecutePython(string command, string machine = null) |
| { |
| SetMonitorMachine(machine); |
| |
| try |
| { |
| return monitor.ExecutePythonCommand(command); |
| } |
| catch(RecoverableException ex) |
| { |
| // Rethrow the inner exception preserving the stack trace, the return is unreachable |
| ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); |
| return null; |
| } |
| } |
| |
| [RobotFrameworkKeyword] |
| // This method accepts array of strings that is later |
| // concatenated using single space and parsed by the monitor. |
| // |
| // Using array instead of a single string allows us to |
| // split long commands into several lines using (...) |
| // notation in robot script; otherwise it would be impossible |
| // as there is no option to split a single parameter. |
| public string ExecuteCommand(string[] commandFragments, string machine = null) |
| { |
| var command = string.Join(" ", commandFragments); |
| return ExecuteCommand(command, machine); |
| } |
| |
| [RobotFrameworkKeyword] |
| public string ExecuteScript(string path) |
| { |
| var interaction = monitor.Interaction as CommandInteractionWrapper; |
| interaction.Clear(); |
| |
| if(!monitor.TryExecuteScript(path, interaction)) |
| { |
| throw new KeywordException("Could not execute script: {0}", interaction.GetError()); |
| } |
| |
| return interaction.GetContents(); |
| } |
| |
| [RobotFrameworkKeyword(replayMode: Replay.Always)] |
| public void StopRemoteServer() |
| { |
| var robotFrontendEngine = (RobotFrameworkEngine)ObjectCreator.Instance.GetSurrogate(typeof(RobotFrameworkEngine)); |
| robotFrontendEngine.Shutdown(); |
| } |
| |
| [RobotFrameworkKeyword] |
| public void HandleHotSpot(HotSpotAction action) |
| { |
| var isStarted = EmulationManager.Instance.CurrentEmulation.IsStarted; |
| switch(action) |
| { |
| case HotSpotAction.None: |
| // do nothing |
| break; |
| case HotSpotAction.Pause: |
| if(isStarted) |
| { |
| EmulationManager.Instance.CurrentEmulation.PauseAll(); |
| EmulationManager.Instance.CurrentEmulation.StartAll(); |
| } |
| break; |
| case HotSpotAction.Serialize: |
| var fileName = TemporaryFilesManager.Instance.GetTemporaryFile(); |
| var monitor = ObjectCreator.Instance.GetSurrogate<Monitor>(); |
| if(monitor.Machine != null) |
| { |
| EmulationManager.Instance.CurrentEmulation.AddOrUpdateInBag("monitor_machine", monitor.Machine); |
| } |
| EmulationManager.Instance.Save(fileName); |
| EmulationManager.Instance.Load(fileName); |
| if(EmulationManager.Instance.CurrentEmulation.TryGetFromBag<Machine>("monitor_machine", out var mac)) |
| { |
| monitor.Machine = mac; |
| } |
| if(isStarted) |
| { |
| EmulationManager.Instance.CurrentEmulation.StartAll(); |
| } |
| break; |
| default: |
| throw new KeywordException("Hot spot action {0} is not currently supported", action); |
| } |
| } |
| |
| [RobotFrameworkKeyword(replayMode: Replay.Never)] |
| public void Provides(string state, ProviderType type = ProviderType.Serialization) |
| { |
| if(type == ProviderType.Serialization) |
| { |
| var tempfileName = AllocateTemporaryFile(); |
| EmulationManager.Instance.CurrentEmulation.TryGetEmulationElementName(monitor.Machine, out var currentMachine); |
| EmulationManager.Instance.Save(tempfileName); |
| savepoints[state] = new Savepoint(currentMachine, tempfileName); |
| } |
| Recorder.Instance.SaveCurrentState(state); |
| } |
| |
| [RobotFrameworkKeyword(replayMode: Replay.Never)] |
| public void Requires(string state) |
| { |
| List<Recorder.Event> events; |
| if(!Recorder.Instance.TryGetState(state, out events)) |
| { |
| throw new KeywordException("Required state {0} not found.", state); |
| } |
| ResetEmulation(); |
| var robotFrontendEngine = (RobotFrameworkEngine)ObjectCreator.Instance.GetSurrogate(typeof(RobotFrameworkEngine)); |
| |
| IEnumerable<Recorder.Event> eventsToExecute = events; |
| var isSerialized = savepoints.TryGetValue(state, out var savepoint); |
| |
| if(isSerialized) |
| { |
| EmulationManager.Instance.Load(savepoint.Filename); |
| if(savepoint.SelectedMachine!= null) |
| { |
| ExecuteCommand($"mach set \"{savepoint.SelectedMachine}\""); |
| } |
| eventsToExecute = eventsToExecute.Where(x => (x.ReplayMode == Replay.Always || x.ReplayMode == Replay.InSerializationMode)); |
| } |
| |
| foreach(var e in eventsToExecute) |
| { |
| robotFrontendEngine.ExecuteKeyword(e.Name, e.Arguments); |
| } |
| } |
| |
| [RobotFrameworkKeyword] |
| public void WaitForPause(float timeout) |
| { |
| var masterTimeSource = EmulationManager.Instance.CurrentEmulation.MasterTimeSource; |
| var mre = new System.Threading.ManualResetEvent(false); |
| var callback = (Action)(() => |
| { |
| // it is possible that the block hook is triggered before virtual time has passed |
| // - in such case it should not be interpreted as a machine pause |
| if(masterTimeSource.ElapsedVirtualTime.Ticks > 0) |
| { |
| mre.Set(); |
| } |
| }); |
| |
| var timeoutEvent = masterTimeSource.EnqueueTimeoutEvent((uint)(timeout * 1000)); |
| |
| try |
| { |
| masterTimeSource.BlockHook += callback; |
| System.Threading.WaitHandle.WaitAny(new [] { timeoutEvent.WaitHandle, mre }); |
| |
| if(timeoutEvent.IsTriggered) |
| { |
| throw new KeywordException($"Emulation did not pause in expected time of {timeout} seconds."); |
| } |
| } |
| finally |
| { |
| masterTimeSource.BlockHook -= callback; |
| } |
| } |
| |
| [RobotFrameworkKeyword(replayMode: Replay.Always)] |
| public string AllocateTemporaryFile() |
| { |
| return TemporaryFilesManager.Instance.GetTemporaryFile(); |
| } |
| |
| [RobotFrameworkKeyword] |
| public string DownloadFile(string uri) |
| { |
| if(!Uri.TryCreate(uri, UriKind.Absolute, out var parsedUri)) |
| { |
| throw new KeywordException($"Wrong URI format: {uri}"); |
| } |
| |
| var fileFetcher = EmulationManager.Instance.CurrentEmulation.FileFetcher; |
| if(!fileFetcher.TryFetchFromUri(parsedUri, out var result)) |
| { |
| throw new KeywordException("Couldn't download file from: {uri}"); |
| } |
| |
| return result; |
| } |
| |
| [RobotFrameworkKeyword(replayMode: Replay.Always)] |
| public void CreateLogTester(float timeout, bool? defaultPauseEmulation = null) |
| { |
| this.defaultPauseEmulation = defaultPauseEmulation.GetValueOrDefault(); |
| logTester = new LogTester(timeout); |
| Logging.Logger.AddBackend(logTester, "Log Tester", true); |
| } |
| |
| [RobotFrameworkKeyword] |
| public string WaitForLogEntry(string pattern, float? timeout = null, bool keep = false, bool treatAsRegex = false, |
| bool? pauseEmulation = null, LogLevel level = null) |
| { |
| CheckLogTester(); |
| |
| var result = logTester.WaitForEntry(pattern, out var bufferedMessages, timeout, keep, treatAsRegex, pauseEmulation ?? defaultPauseEmulation, level); |
| if(result == null) |
| { |
| // We must limit the length of the resulting string to Int32.MaxValue to avoid OutOfMemoryException. |
| // We could do it accurately, but it doesn't seem worth here, because the goal is just to provide some extra context to the exception message. |
| // We arbitrarily chose the number of messages to include here. In theory it could still throw during string.Join operation given very long messages, |
| // but it's unlikely to happen given the value of Int32.MaxValue = 2,147,483,647. |
| var logContextMessages = bufferedMessages.TakeLast(MaxLogContextPrintedOnException); |
| var logMessages = string.Join("\n ", logContextMessages); |
| throw new KeywordException($"Expected pattern \"{pattern}\" did not appear in the log\nLast {logContextMessages.Count()} buffered log messages are: \n {logMessages}"); |
| } |
| return result; |
| } |
| |
| [RobotFrameworkKeyword] |
| public void ShouldNotBeInLog(String pattern, float? timeout = null, bool treatAsRegex = false, bool? pauseEmulation = null, LogLevel level = null) |
| { |
| CheckLogTester(); |
| |
| // Passing `level` as a named argument causes a compiler crash in Mono 6.8.0.105+dfsg-3.4 |
| // from Debian |
| var result = logTester.WaitForEntry(pattern, out var _, timeout, true, treatAsRegex, pauseEmulation ?? defaultPauseEmulation, level); |
| if(result != null) |
| { |
| throw new KeywordException($"Unexpected line detected in the log: {result}"); |
| } |
| } |
| |
| [RobotFrameworkKeyword] |
| public void ClearLogTesterHistory() |
| { |
| CheckLogTester(); |
| logTester.ClearHistory(); |
| } |
| |
| [RobotFrameworkKeyword(replayMode: Replay.Never)] |
| public void LogToFile(string filePath, bool flushAfterEveryWrite = false) |
| { |
| Logger.AddBackend(new FileBackend(filePath, flushAfterEveryWrite), "file", true); |
| } |
| |
| [RobotFrameworkKeyword] |
| public void OpenGUI() |
| { |
| Emulator.OpenGUI(); |
| } |
| |
| [RobotFrameworkKeyword] |
| public void CloseGUI() |
| { |
| Emulator.CloseGUI(); |
| } |
| |
| [RobotFrameworkKeyword] |
| public void EnableLoggingToCache() |
| { |
| if(cachedLogFilePath == null) |
| { |
| cachedLogFilePath = Path.Combine( |
| TemporaryFilesManager.Instance.EmulatorTemporaryPath, |
| "renode-robot.log"); |
| Logger.AddBackend(new FileBackend(cachedLogFilePath, false), CachedLogBackendName, true); |
| } |
| } |
| |
| [RobotFrameworkKeyword] |
| public void SaveCachedLog(string filePath) |
| { |
| if(cachedLogFilePath == null) |
| { |
| throw new KeywordException($"Cannot save cached log, cached logging has not been enabled."); |
| } |
| |
| (Logger.GetBackends()[CachedLogBackendName] as FileBackend).Flush(); |
| System.IO.File.Copy(cachedLogFilePath, filePath, true); |
| } |
| |
| [RobotFrameworkKeyword] |
| public void ClearCachedLog() |
| { |
| if(cachedLogFilePath != null) |
| { |
| Logger.RemoveBackend(Logger.GetBackends()[CachedLogBackendName]); |
| System.IO.File.Delete(cachedLogFilePath); |
| cachedLogFilePath = null; |
| EnableLoggingToCache(); |
| } |
| } |
| |
| private void CheckLogTester() |
| { |
| if(logTester == null) |
| { |
| throw new KeywordException("Log tester is not available. Create it with the `CreateLogTester` keyword"); |
| } |
| } |
| |
| private void SetMonitorMachine(string machine) |
| { |
| if(!string.IsNullOrWhiteSpace(machine)) |
| { |
| if(!EmulationManager.Instance.CurrentEmulation.TryGetMachineByName(machine, out var machobj)) |
| { |
| throw new KeywordException("Could not find machine named {0} in the emulation", machine); |
| } |
| monitor.Machine = machobj; |
| } |
| } |
| |
| private readonly Dictionary<string, Savepoint> savepoints; |
| |
| private LogTester logTester; |
| private string cachedLogFilePath; |
| private bool defaultPauseEmulation; |
| |
| private readonly Monitor monitor; |
| |
| private const string CachedLogBackendName = "cache"; |
| private const int MaxLogContextPrintedOnException = 1000; |
| |
| private struct Savepoint |
| { |
| public Savepoint(string selectedMachine, string filename) |
| { |
| SelectedMachine = selectedMachine; |
| Filename = filename; |
| } |
| |
| public string SelectedMachine { get; } |
| public string Filename { get; } |
| } |
| |
| public enum ProviderType |
| { |
| Serialization = 0, |
| Reexecution = 1, |
| } |
| } |
| } |
| |