using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using MatterHackers.Agg.UI; using MatterHackers.GuiAutomation; using MatterHackers.MatterControl.PrinterCommunication; using MatterHackers.MatterControl.PrinterCommunication.Io; using MatterHackers.MatterControl.SlicerConfiguration; using MatterHackers.PrinterEmulator; using MatterHackers.VectorMath; using NUnit.Framework; namespace MatterHackers.MatterControl.Tests.Automation { [TestFixture, Category("MatterControl.UI.Automation"), RunInApplicationDomain, Apartment(ApartmentState.STA)] public class PrintingTests { [Test, Category("Emulator")] public async Task CompletingPrintTurnsoffHeat() { await MatterControlUtilities.RunTest((testRunner) => { testRunner.WaitForName("Cancel Wizard Button", 1); using (var emulator = testRunner.LaunchAndConnectToPrinterEmulator()) { Assert.AreEqual(1, ApplicationController.Instance.ActivePrinters.Count(), "One printer should be defined after add"); testRunner.SelectSliceSettingsField(PrinterSettings.Layout.Printer, "end_gcode"); testRunner.Type("^a"); testRunner.Type("{BACKSPACE}"); testRunner.Type("G28"); testRunner.SelectSliceSettingsField(PrinterSettings.Layout.Printer, "start_gcode"); var printer = testRunner.FirstPrinter(); // Validate GCode fields persist values Assert.AreEqual( "G28", printer.Settings.GetValue(SettingsKey.end_gcode), "Failure persisting GCode/MultilineTextField value"); testRunner.AddItemToBedplate(); // Shorten the delay so the test runs in a reasonable time printer.Connection.TimeToHoldTemperature = 5; testRunner.StartPrint(); // Wait for print to finish testRunner.WaitForPrintFinished(printer); // Wait for expected temp testRunner.WaitFor(() => printer.Connection.GetActualHotendTemperature(0) <= 0, 10); Assert.Less(printer.Connection.GetActualHotendTemperature(0), 30); // Wait for expected temp testRunner.WaitFor(() => printer.Connection.ActualBedTemperature <= 10); Assert.Less(printer.Connection.ActualBedTemperature, 10); // Make sure we can run this whole thing again testRunner.StartPrint(); // Wait for print to finish testRunner.WaitForPrintFinished(printer); // Wait for expected temp testRunner.WaitFor(() => printer.Connection.GetActualHotendTemperature(0) <= 0, 10); Assert.Less(printer.Connection.GetActualHotendTemperature(0), 30); // Wait for expected temp testRunner.WaitFor(() => printer.Connection.ActualBedTemperature <= 10); Assert.Less(printer.Connection.ActualBedTemperature, 10); } return Task.CompletedTask; }, maxTimeToRun: 95); } [Test, Category("Emulator")] public async Task PulseRequiresLevelingAndLevelingWorks() { await MatterControlUtilities.RunTest((testRunner) => { AutomationRunner.TimeToMoveMouse = .2; testRunner.WaitForName("Cancel Wizard Button", 1); using (var emulator = testRunner.LaunchAndConnectToPrinterEmulator("Pulse", "A-134")) { Assert.AreEqual(1, ApplicationController.Instance.ActivePrinters.Count(), "One printer should be defined after add"); testRunner.OpenPrintPopupMenu(false, false); testRunner.ClickByName("SetupPrinter"); testRunner.Complete9StepLeveling(); // print a part testRunner.AddItemToBedplate(); var printer = testRunner.FirstPrinter(); var currentSettings = printer.Settings; currentSettings.SetValue(SettingsKey.pause_gcode, ""); currentSettings.SetValue(SettingsKey.resume_gcode, ""); testRunner.OpenPrintPopupMenu(); testRunner.ClickByName("Layer(s) To Pause Field"); testRunner.Type("2"); testRunner.StartPrint(); testRunner.WaitForName("Yes Button", 20); // the yes button is 'Resume' // the user types in the pause layer 1 based and we are 0 based, so we should be on: user 2, printer 1. Assert.AreEqual(1, printer.Connection.CurrentlyPrintingLayer); // assert the leveling is working Assert.AreEqual(11.15, emulator.ZPosition); testRunner.CancelPrint(); // now run leveling again and make sure we get the same result testRunner.SwitchToControlsTab(); testRunner.ClickByName("Run Leveling Button"); testRunner.Complete9StepLeveling(2); testRunner.OpenPrintPopupMenu(); testRunner.ClickByName("Layer(s) To Pause Field"); testRunner.Type("2"); testRunner.StartPrint(); testRunner.WaitForName("Yes Button", 20); // the yes button is 'Resume' // the user types in the pause layer 1 based and we are 0 based, so we should be on: user 2, printer 1. Assert.AreEqual(1, printer.Connection.CurrentlyPrintingLayer); // assert the leveling is working Assert.AreEqual(12.15, emulator.ZPosition); testRunner.CancelPrint(); // now modify the leveling data manually and assert that it is applied when printing testRunner.SwitchToControlsTab(); testRunner.ClickByName("Edit Leveling Data Button"); for(int i=0; i<3; i++) { var name = $"z Position {i}"; testRunner.ClickByName(name); testRunner.Type("^a"); // select all testRunner.Type("5"); } testRunner.ClickByName("Save Leveling Button"); testRunner.OpenPrintPopupMenu(); testRunner.ClickByName("Layer(s) To Pause Field"); testRunner.Type("2"); testRunner.StartPrint(); testRunner.WaitForName("Yes Button", 20); // the yes button is 'Resume' // the user types in the pause layer 1 based and we are 0 based, so we should be on: user 2, printer 1. Assert.AreEqual(1, printer.Connection.CurrentlyPrintingLayer); // assert the leveling is working Assert.AreEqual(5.15, emulator.ZPosition); } return Task.CompletedTask; }, maxTimeToRun: 130); } [Test, Category("Emulator")] public void ExpectedEmulatorResponses() { // TODO: Emulator behavior should emulate actual printer firmware and use configuration rather than M104/M109 sends to set extruder count // // Quirky emulator returns single extruder M105 responses until after the first M104, at which point it extends its extruder count to match string M105ResponseBeforeM104 = "ok T:27.0 / 0.0"; string M105ResponseAfterM104 = "ok T0:27.0 / 0.0 T1:27.0 / 0.0"; string[] test1 = new string[] { "N1 M110 N1 * 125", "ok", "N2 M114 * 37", "X:0.00 Y: 0.00 Z: 0.00 E: 0.00 Count X: 0.00 Y: 0.00 Z: 0.00", "ok", "N3 M105 * 36", M105ResponseBeforeM104, "N1 M110 N1*125", "ok", "N2 M115 * 36", "FIRMWARE_NAME:Marlin V1; Sprinter/grbl mashup for gen6 FIRMWARE_URL:https://github.com/MarlinFirmware/Marlin PROTOCOL_VERSION:1.0 MACHINE_TYPE:Framelis v1 EXTRUDER_COUNT:1 UUID:155f84b5-d4d7-46f4-9432-667e6876f37a", "ok", "N3 M104 T0 S0 * 34", "ok", "N4 M104 T1 S0 * 36", "ok", "N5 M105 * 34", M105ResponseAfterM104, "N6 M105 * 45", "Error:checksum mismatch, Last Line: 5", "Resend: 6", "ok", "N6 M105 * 33", M105ResponseAfterM104, "N7 M105 * 32", M105ResponseAfterM104, "N8 M105 * 47", M105ResponseAfterM104, "N9 M105 * 46", M105ResponseAfterM104, "N10 M105 * 22", M105ResponseAfterM104, "N11 M105 * 23", M105ResponseAfterM104, "N12 M105 * 20", M105ResponseAfterM104, "N13 M105 * 21", M105ResponseAfterM104, "N14 M105 * 18", M105ResponseAfterM104, "N15 M105 * 19", M105ResponseAfterM104, "N16 M105 * 16", M105ResponseAfterM104, "N17 M105 * 40", "Error:checksum mismatch, Last Line: 16", "Resend: 17", "ok", "N17 M105 * 17", M105ResponseAfterM104, }; string[] test2 = new string[] { "N1 M110 N1*125", "ok", "N1 M110 N1*125", "ok", "N1 M110 N1*125", "ok", "N2 M114*37", "X:0.00 Y: 0.00 Z: 0.00 E: 0.00 Count X: 0.00 Y: 0.00 Z: 0.00", "ok", }; SimulatePrint(test1); SimulatePrint(test2); } private static void SimulatePrint(string[] sendRecieveLog) { using (var emulator = new Emulator()) { emulator.HasHeatedBed = false; int lineIndex = 0; while (lineIndex < sendRecieveLog.Length) { var sentCommand = sendRecieveLog[lineIndex]; string response = emulator.GetCorrectResponse(sentCommand); lineIndex++; var lines = response.Split('\n'); for (int i = 0; i < lines.Length; i++) { if (!string.IsNullOrEmpty(lines[i])) { Assert.AreEqual(sendRecieveLog[lineIndex], lines[i]); lineIndex++; } } } } } [Test, Category("Emulator")] public async Task PrinterRequestsResumeWorkingAsExpected() { await MatterControlUtilities.RunTest((testRunner) => { using (var emulator = testRunner.LaunchAndConnectToPrinterEmulator()) { Assert.AreEqual(1, ApplicationController.Instance.ActivePrinters.Count(), "One printer should be defined after add"); testRunner.OpenPrintPopupMenu(); testRunner.ClickByName("Layer(s) To Pause Field"); testRunner.Type("2;6"); // print a part testRunner.AddItemToBedplate(); testRunner.StartPrint(); // turn on line error simulation emulator.SimulateLineErrors = true; // close the pause dialog pop-up (resume) testRunner.WaitForName("Yes Button", 20); // the yes button is 'Resume' testRunner.ClickByName("Yes Button"); // simulate board reboot emulator.SimulateReboot(); // close the pause dialog pop-up (resume) testRunner.Delay(3); testRunner.WaitForName("Yes Button", 20); testRunner.ClickByName("Yes Button"); // Wait for done testRunner.WaitForPrintFinished(testRunner.FirstPrinter()); } return Task.CompletedTask; }, maxTimeToRun: 90); } [Test, Category("Emulator")] public async Task PrinterRecoveryTest() { await MatterControlUtilities.RunTest((testRunner) => { using (var emulator = testRunner.LaunchAndConnectToPrinterEmulator()) { Assert.AreEqual(1, ApplicationController.Instance.ActivePrinters.Count(), "One printer should exist after add"); var printer = testRunner.FirstPrinter(); printer.Settings.SetValue(SettingsKey.recover_is_enabled, "1"); printer.Settings.SetValue(SettingsKey.has_hardware_leveling, "0"); // TODO: Delay needed to work around timing issue in MatterHackers/MCCentral#2415 testRunner.Delay(1); testRunner.OpenPrintPopupMenu(); testRunner.ClickByName("Layer(s) To Pause Field"); testRunner.Type("2;4;6"); Assert.IsTrue(printer.Connection.RecoveryIsEnabled); // print a part testRunner.AddItemToBedplate(); testRunner.StartPrint(); // Dismiss pause dialog testRunner.WaitForName("Yes Button", 90); // the yes button is 'Resume' // validate the current layer Assert.AreEqual(1, printer.Connection.CurrentlyPrintingLayer); testRunner.ClickByName("Yes Button"); // the printer is now paused // close the pause dialog pop-up do not resume ClickDialogButton(testRunner, printer, "No Button", 3); // Disconnect testRunner.ClickByName("Disconnect from printer button"); // Reconnect testRunner.WaitForName("Connect to printer button", 10); testRunner.ClickByName("Connect to printer button"); testRunner.WaitFor(() => printer.Connection.CommunicationState == CommunicationStates.Connected); // Assert that recovery happens // Recover the print ClickDialogButton(testRunner, printer, "Yes Button", -1); // The first pause that we get after recovery should be layer 6. // wait for the pause and continue ClickDialogButton(testRunner, printer, "Yes Button", 5); // Wait for done testRunner.WaitForPrintFinished(printer); } return Task.CompletedTask; }, maxTimeToRun: 180); } // TODO: convert to extension method private static void ClickDialogButton(AutomationRunner testRunner, PrinterConfig printer, string buttonName, int expectedLayer) { testRunner.WaitForName(buttonName, 90); Assert.AreEqual(expectedLayer, printer.Connection.CurrentlyPrintingLayer); testRunner.ClickByName(buttonName); testRunner.WaitFor(() => !testRunner.NameExists(buttonName), 1); } [Test, Category("Emulator")] public async Task TuningAdjustmentsDefaultToOneAndPersists() { double targetExtrusionRate = 1.5; double targetFeedRate = 2; await MatterControlUtilities.RunTest((testRunner) => { testRunner.WaitForName("Cancel Wizard Button"); using (var emulator = testRunner.LaunchAndConnectToPrinterEmulator()) { Assert.AreEqual(1, ApplicationController.Instance.ActivePrinters.Count(), "One printer should be defined after add"); testRunner.AddItemToBedplate(); testRunner.SwitchToControlsTab(); var printer = testRunner.FirstPrinter(); // Wait for printing to complete var printFinishedResetEvent = new AutoResetEvent(false); printer.Connection.PrintFinished += (s, e) => { printFinishedResetEvent.Set(); }; testRunner.StartPrint(); var container = testRunner.GetWidgetByName("ManualPrinterControls.ControlsContainer", out _, 5); // Scroll the widget into view var scrollable = container.Parents().First() as ScrollableWidget; var width = scrollable.Width; // Workaround needed to scroll to the bottom of the Controls panel //scrollable.ScrollPosition = new Vector2(); scrollable.ScrollPosition = new Vector2(0, 30); // Workaround to force layout to fix problems with size of Tuning Widgets after setting ScrollPosition manually scrollable.Width = width - 1; scrollable.Width = width; // Tuning values should default to 1 when missing ConfirmExpectedSpeeds(testRunner, 1, 1, "Initial case"); testRunner.Delay(); testRunner.ClickByName("Extrusion Multiplier NumberEdit"); testRunner.Type(targetExtrusionRate.ToString()); testRunner.ClickByName("Feed Rate NumberEdit"); testRunner.Type(targetFeedRate.ToString()); // Force focus away from the feed rate field, causing an persisted update testRunner.ClickByName("Extrusion Multiplier NumberEdit"); ConfirmExpectedSpeeds(testRunner, targetExtrusionRate, targetFeedRate, "After setting TextEdit values"); // Wait for slicing to complete before setting target values testRunner.WaitFor(() => printer.Connection.DetailedPrintingState == DetailedPrintingState.Printing, 8); testRunner.Delay(); ConfirmExpectedSpeeds(testRunner, targetExtrusionRate, targetFeedRate, "While printing"); // Wait up to 60 seconds for the print to finish printFinishedResetEvent.WaitOne(60 * 1000); // Values should match entered values ConfirmExpectedSpeeds(testRunner, targetExtrusionRate, targetFeedRate, "After print finished"); testRunner.WaitForPrintFinished(printer); // Restart the print testRunner.StartPrint(); testRunner.Delay(2); // Values should match entered values ConfirmExpectedSpeeds(testRunner, targetExtrusionRate, targetFeedRate, "After print restarted"); testRunner.CancelPrint(); testRunner.Delay(1); // Values should match entered values ConfirmExpectedSpeeds(testRunner, targetExtrusionRate, targetFeedRate, "After canceled print"); } return Task.CompletedTask; }, overrideHeight:900, maxTimeToRun: 120); } [Test, Category("Emulator")] public async Task TuningAdjustmentControlsBoundToStreamValues() { double targetExtrusionRate = 1.5; double targetFeedRate = 2; double initialExtrusionRate = 0.6; double initialFeedRate = 0.7; await MatterControlUtilities.RunTest((testRunner) => { testRunner.WaitForName("Cancel Wizard Button"); // Set custom adjustment values FeedRateMultiplyerStream.FeedRateRatio = initialFeedRate; ExtrusionMultiplyerStream.ExtrusionRatio = initialExtrusionRate; // Then validate that they are picked up using (var emulator = testRunner.LaunchAndConnectToPrinterEmulator()) { Assert.AreEqual(1, ApplicationController.Instance.ActivePrinters.Count(), "One printer should be defined after add"); testRunner.AddItemToBedplate(); testRunner.SwitchToControlsTab(); var printer = testRunner.FirstPrinter(); var printFinishedResetEvent = new AutoResetEvent(false); printer.Connection.PrintFinished += (s, e) => printFinishedResetEvent.Set(); testRunner.StartPrint(); var container = testRunner.GetWidgetByName("ManualPrinterControls.ControlsContainer", out _, 5); // Scroll the widget into view var scrollable = container.Parents().FirstOrDefault() as ScrollableWidget; var width = scrollable.Width; // Workaround needed to scroll to the bottom of the Controls panel //scrollable.ScrollPosition = new Vector2(); scrollable.ScrollPosition = new Vector2(0, 30); // Workaround to force layout to fix problems with size of Tuning Widgets after setting ScrollPosition manually scrollable.Width = width - 1; scrollable.Width = width; // Tuning values should match ConfirmExpectedSpeeds(testRunner, initialExtrusionRate, initialFeedRate, "Initial case"); testRunner.Delay(); testRunner.ClickByName("Extrusion Multiplier NumberEdit"); testRunner.Type(targetExtrusionRate.ToString()); testRunner.ClickByName("Feed Rate NumberEdit"); testRunner.Type(targetFeedRate.ToString()); // Force focus away from the feed rate field, causing an persisted update testRunner.ClickByName("Extrusion Multiplier NumberEdit"); ConfirmExpectedSpeeds(testRunner, targetExtrusionRate, targetFeedRate, "After setting TextEdit values"); // Wait for slicing to complete before setting target values testRunner.WaitFor(() => printer.Connection.DetailedPrintingState == DetailedPrintingState.Printing, 8); testRunner.Delay(); // Values should remain after print completes ConfirmExpectedSpeeds(testRunner, targetExtrusionRate, targetFeedRate, "While printing"); // Wait for printing to complete printFinishedResetEvent.WaitOne(); testRunner.WaitForPrintFinished(printer); // Values should match entered values testRunner.StartPrint(); testRunner.WaitFor(() => printer.Connection.CommunicationState == CommunicationStates.Printing, 15); // Values should match entered values ConfirmExpectedSpeeds(testRunner, targetExtrusionRate, targetFeedRate, "While reprinting"); testRunner.CancelPrint(); testRunner.WaitFor(() => printer.Connection.CommunicationState == CommunicationStates.Connected, 15); // Values should match entered values ConfirmExpectedSpeeds(testRunner, targetExtrusionRate, targetFeedRate, "After cancel"); } return Task.CompletedTask; }, overrideHeight: 900, maxTimeToRun: 120); } [Test, Category("Emulator")] public async Task CloseShouldNotStopSDPrint() { await MatterControlUtilities.RunTest((testRunner) => { testRunner.WaitForName("Cancel Wizard Button"); using (var emulator = testRunner.LaunchAndConnectToPrinterEmulator(runSlow: true)) { testRunner.NavigateToFolder("SD Card Row Item Collection"); testRunner.ClickByName("Row Item Item 1.gcode"); testRunner.ClickByName("Print Library Overflow Menu"); testRunner.ClickByName("Print Menu Item"); testRunner.Delay(2); int tempChangedCount = 0; int fanChangedCount = 0; emulator.ExtruderTemperatureChanged += (s, e) => { tempChangedCount++; }; emulator.FanSpeedChanged += (s, e) => { fanChangedCount++; }; testRunner.CloseMatterControl(); testRunner.ClickByName("Yes Button"); testRunner.Delay(2); Assert.AreEqual(0, tempChangedCount, "We should not change this while exiting an sd card print."); Assert.AreEqual(0, fanChangedCount, "We should not change this while exiting an sd card print."); } return Task.CompletedTask; }, maxTimeToRun: 90); } [Test, Category("Emulator")] public async Task CancelingPrintTurnsHeatAndFanOff() { await MatterControlUtilities.RunTest((testRunner) => { testRunner.WaitForName("Cancel Wizard Button"); using (var emulator = testRunner.LaunchAndConnectToPrinterEmulator()) { var resetEvent = new AutoResetEvent(false); Assert.AreEqual(1, ApplicationController.Instance.ActivePrinters.Count(), "One printer should exist after add"); testRunner.AddItemToBedplate(); testRunner.StartPrint(); int fanChangedCount = 0; emulator.FanSpeedChanged += (s, e) => { fanChangedCount++; }; var printer = testRunner.FirstPrinter(); emulator.WaitForLayer(printer.Settings, 2); emulator.RunSlow = true; // Click close but cancel testRunner.CloseMatterControl(); testRunner.ClickByName("No Button"); // Wait for close testRunner.WaitForWidgetDisappear("Yes Button", 4); testRunner.Delay(2); // Confirm abort Assert.IsFalse(AppContext.RootSystemWindow.HasBeenClosed, "Canceling Close dialog should *not* close MatterControl"); // Close MatterControl and cancel print testRunner.CloseMatterControl(); testRunner.ClickByName("Yes Button"); // Wait for Disconnected CommunicationState which occurs after PrinterConnection.Disable() testRunner.WaitForCommunicationStateDisconnected(printer, maxSeconds: 30); // Wait for close testRunner.WaitForWidgetDisappear("Yes Button", 4); testRunner.Delay(2); // Confirm close Assert.IsTrue(AppContext.RootSystemWindow.HasBeenClosed, "Confirming Close dialog *should* close MatterControl"); // Wait for M106 change testRunner.WaitFor(() => fanChangedCount > 0, 15, 500); // Assert expected temp targets and fan transitions Assert.AreEqual(0, (int) emulator.CurrentExtruder.TargetTemperature, "Unexpected target temperature - MC close should call Connection.Disable->TurnOffBedAndExtruders to shutdown heaters"); Assert.AreEqual(0, (int) emulator.HeatedBed.TargetTemperature, "Unexpected target temperature - MC close should call Connection.Disable->TurnOffBedAndExtruders to shutdown heaters"); Assert.AreEqual(1, fanChangedCount, "Unexpected fan speed change count - MC close should call Connection.Disable which shuts down fans via M106"); } return Task.CompletedTask; }, overrideHeight: 900, maxTimeToRun: 90); } private static void ConfirmExpectedSpeeds(AutomationRunner testRunner, double targetExtrusionRate, double targetFeedRate, string scope) { SystemWindow systemWindow; SolidSlider slider; // Assert the UI has the expected values slider = testRunner.GetWidgetByName("Extrusion Multiplier Slider", out systemWindow, 5) as SolidSlider; testRunner.WaitFor(() => targetExtrusionRate == slider.Value); Assert.AreEqual(targetExtrusionRate, slider.Value, $"Unexpected Extrusion Rate Slider Value - {scope}"); slider = testRunner.GetWidgetByName("Feed Rate Slider", out systemWindow, 5) as SolidSlider; testRunner.WaitFor(() => targetFeedRate == slider.Value); Assert.AreEqual(targetFeedRate, slider.Value, $"Unexpected Feed Rate Slider Value - {scope}"); // Assert the changes took effect on the model testRunner.WaitFor(() => targetExtrusionRate == ExtrusionMultiplyerStream.ExtrusionRatio); Assert.AreEqual(targetExtrusionRate, ExtrusionMultiplyerStream.ExtrusionRatio, $"Unexpected Extrusion Rate - {scope}"); testRunner.WaitFor(() => targetFeedRate == FeedRateMultiplyerStream.FeedRateRatio); Assert.AreEqual(targetFeedRate, FeedRateMultiplyerStream.FeedRateRatio, $"Unexpected Feed Rate - {scope}"); } } }