From 6dd3b8a32c79d434abbe13f84a83eab4f9ccfbe5 Mon Sep 17 00:00:00 2001 From: Christian Hammacher Date: Tue, 24 Aug 2021 15:11:11 +0200 Subject: [PATCH] Version 3.4-b3 Updated WHATS_NEW, version numbers and dependencies Added new IPC command to invalidate codes and files Added new OM properties from RRF Implemented support for file redirections of echo command Minor refactoring Bug fix: Under certain cirumstances prints could not be cancelled properly --- WHATS_NEW.md | 12 +++ src/CodeConsole/CodeConsole.csproj | 2 +- src/CodeLogger/CodeLogger.csproj | 2 +- src/CodeStream/CodeStream.csproj | 2 +- .../CustomHttpEndpoint.csproj | 2 +- src/DocGen/DocGen.csproj | 2 +- src/Documentation/Documentation.csproj | 2 +- .../Commands/Generic/InvalidateChannel.cs | 16 ++++ src/DuetAPI/DuetAPI.csproj | 2 +- .../ObjectModel/Directories/Directories.cs | 1 + src/DuetAPI/ObjectModel/Move/Axis.cs | 20 +++++ src/DuetAPI/ObjectModel/Move/Extruder.cs | 20 +++++ src/DuetAPIClient/DuetAPIClient.csproj | 2 +- src/DuetControlServer/Codes/Keywords.cs | 78 ++++++++++++++++++- .../Commands/Generic/InvalidateChannel.cs | 36 +++++++++ .../Commands/Plugins/StartPlugins.cs | 8 +- .../DuetControlServer.csproj | 4 +- src/DuetControlServer/FileExecution/Job.cs | 3 +- src/DuetControlServer/Model/Expressions.cs | 31 +++----- .../SPI/Channel/Processor.cs | 2 +- .../Communication/LinuxRequests/Request.cs | 4 +- src/DuetControlServer/SPI/DataTransfer.cs | 6 +- .../DuetPiManagementPlugin.csproj | 2 +- .../DuetPluginService.csproj | 4 +- src/DuetWebServer/DuetWebServer.csproj | 2 +- src/LinuxApi/LinuxApi.csproj | 2 +- src/ModelObserver/ModelObserver.csproj | 2 +- src/PluginManager/PluginManager.csproj | 2 +- src/UnitTests/UnitTests.csproj | 2 +- 29 files changed, 220 insertions(+), 53 deletions(-) create mode 100644 src/DuetAPI/Commands/Generic/InvalidateChannel.cs create mode 100644 src/DuetControlServer/Commands/Generic/InvalidateChannel.cs diff --git a/WHATS_NEW.md b/WHATS_NEW.md index 5fbce403b..62dff1baf 100644 --- a/WHATS_NEW.md +++ b/WHATS_NEW.md @@ -1,6 +1,18 @@ Summary of important changes in recent versions =============================================== +Version 3.4-b3 +============== + +New features: +- Implemented file redirections of the `echo` command (see [here](https://duet3d.dozuki.com/Wiki/GCode_Meta_Commands?revisionid=HEAD#Section_Echo_command)) +- Added support for new `InvalidateChannel` command allowing plugins to cancel already queued codes and files + +Bug fixes: +- Resolved race condition for M0/M1 which could cause stop.g to be executed instead of cancel.g +- Fixed an exception that could occur when codes were cancelled and their result was combined +- Under certain conditions paused prints could not be cancelled properly + Version 3.4-b2 ============== diff --git a/src/CodeConsole/CodeConsole.csproj b/src/CodeConsole/CodeConsole.csproj index ef8d77ccb..f6a8b6e3b 100644 --- a/src/CodeConsole/CodeConsole.csproj +++ b/src/CodeConsole/CodeConsole.csproj @@ -4,7 +4,7 @@ Exe net5.0 true - 3.4-b2 + 3.4-b3 git https://github.com/Duet3D/DuetSoftwareFramework.git GPL-3.0 diff --git a/src/CodeLogger/CodeLogger.csproj b/src/CodeLogger/CodeLogger.csproj index ce1437c6b..bc0b2528b 100644 --- a/src/CodeLogger/CodeLogger.csproj +++ b/src/CodeLogger/CodeLogger.csproj @@ -4,7 +4,7 @@ Exe net5.0 true - 3.4-b2 + 3.4-b3 https://github.com/Duet3D/DuetSoftwareFramework.git git Christian Hammacher diff --git a/src/CodeStream/CodeStream.csproj b/src/CodeStream/CodeStream.csproj index dfa8bbc0b..f2c590786 100644 --- a/src/CodeStream/CodeStream.csproj +++ b/src/CodeStream/CodeStream.csproj @@ -4,7 +4,7 @@ Exe net5.0 true - 3.4-b2 + 3.4-b3 git https://github.com/Duet3D/DuetSoftwareFramework.git GPL-3.0 diff --git a/src/CustomHttpEndpoint/CustomHttpEndpoint.csproj b/src/CustomHttpEndpoint/CustomHttpEndpoint.csproj index a8e5d4aaa..330a3df19 100644 --- a/src/CustomHttpEndpoint/CustomHttpEndpoint.csproj +++ b/src/CustomHttpEndpoint/CustomHttpEndpoint.csproj @@ -3,7 +3,7 @@ Exe net5.0 - 3.4-b2 + 3.4-b3 https://github.com/Duet3D/DuetSoftwareFramework.git git Christian Hammacher diff --git a/src/DocGen/DocGen.csproj b/src/DocGen/DocGen.csproj index e56aa46f2..ba3d7eb3c 100644 --- a/src/DocGen/DocGen.csproj +++ b/src/DocGen/DocGen.csproj @@ -6,7 +6,7 @@ default Christian Hammacher Duet3D Ltd - 3.4.0-b2 + 3.4-b3 GPL-3.0 true https://github.com/Duet3D/DuetSoftwareFramework.git diff --git a/src/Documentation/Documentation.csproj b/src/Documentation/Documentation.csproj index c92982d35..b05d96f78 100644 --- a/src/Documentation/Documentation.csproj +++ b/src/Documentation/Documentation.csproj @@ -5,7 +5,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/DuetAPI/Commands/Generic/InvalidateChannel.cs b/src/DuetAPI/Commands/Generic/InvalidateChannel.cs new file mode 100644 index 000000000..3eeadf363 --- /dev/null +++ b/src/DuetAPI/Commands/Generic/InvalidateChannel.cs @@ -0,0 +1,16 @@ +using DuetAPI.Utility; + +namespace DuetAPI.Commands +{ + /// + /// Invalidate all pending codes and files on a given channel (including buffered codes from DSF in RepRapFirmware) + /// + [RequiredPermissions(SbcPermissions.CodeInterceptionReadWrite)] + public class InvalidateChannel : Command + { + /// + /// Code channel to invalidate + /// + public CodeChannel Channel { get; set; } + } +} diff --git a/src/DuetAPI/DuetAPI.csproj b/src/DuetAPI/DuetAPI.csproj index b1245a5b0..a96e0bc64 100644 --- a/src/DuetAPI/DuetAPI.csproj +++ b/src/DuetAPI/DuetAPI.csproj @@ -5,7 +5,7 @@ net5.0 Christian Hammacher Duet3D Ltd - 3.4-b2 + 3.4-b3 true LGPL-3.0-or-later true diff --git a/src/DuetAPI/ObjectModel/Directories/Directories.cs b/src/DuetAPI/ObjectModel/Directories/Directories.cs index 9acdf1951..faa2389f0 100644 --- a/src/DuetAPI/ObjectModel/Directories/Directories.cs +++ b/src/DuetAPI/ObjectModel/Directories/Directories.cs @@ -71,6 +71,7 @@ public string Scans /// /// Path to the system directory /// + [SbcProperty(true)] public string System { get => _system; diff --git a/src/DuetAPI/ObjectModel/Move/Axis.cs b/src/DuetAPI/ObjectModel/Move/Axis.cs index ea3d5e833..d6afb23a1 100644 --- a/src/DuetAPI/ObjectModel/Move/Axis.cs +++ b/src/DuetAPI/ObjectModel/Move/Axis.cs @@ -132,6 +132,26 @@ public bool MinProbed } private bool _minProbed; + /// + /// Percentage applied to the motor current (0..100) + /// + public int PercentCurrent + { + get => _percentCurrent; + set => SetPropertyValue(ref _percentCurrent, value); + } + private int _percentCurrent = 100; + + /// + /// Percentage applied to the motor current during standstill (0..100 or null if not supported) + /// + public int? PercentStstCurrent + { + get => _percentStstCurrent; + set => SetPropertyValue(ref _percentStstCurrent, value); + } + private int? _percentStstCurrent; + /// /// Maximum speed (in mm/min) /// diff --git a/src/DuetAPI/ObjectModel/Move/Extruder.cs b/src/DuetAPI/ObjectModel/Move/Extruder.cs index 3c824fabb..ef6646f5a 100644 --- a/src/DuetAPI/ObjectModel/Move/Extruder.cs +++ b/src/DuetAPI/ObjectModel/Move/Extruder.cs @@ -77,6 +77,26 @@ public float Jerk /// public ExtruderNonlinear Nonlinear { get; private set; } = new ExtruderNonlinear(); + /// + /// Percentage applied to the motor current (0..100) + /// + public int PercentCurrent + { + get => _percentCurrent; + set => SetPropertyValue(ref _percentCurrent, value); + } + private int _percentCurrent = 100; + + /// + /// Percentage applied to the motor current during standstill (0..100 or null if not supported) + /// + public int? PercentStstCurrent + { + get => _percentStstCurrent; + set => SetPropertyValue(ref _percentStstCurrent, value); + } + private int? _percentStstCurrent; + /// /// Extruder position (in mm) /// diff --git a/src/DuetAPIClient/DuetAPIClient.csproj b/src/DuetAPIClient/DuetAPIClient.csproj index e6814ce7a..65c7d8858 100644 --- a/src/DuetAPIClient/DuetAPIClient.csproj +++ b/src/DuetAPIClient/DuetAPIClient.csproj @@ -6,7 +6,7 @@ Duet3D Ltd true LGPL-3.0-or-later - 3.4-b2 + 3.4-b3 true Official client API library for Duet Software Framework (Duet 3) by Duet3D https://github.com/Duet3D/DuetSoftwareFramework diff --git a/src/DuetControlServer/Codes/Keywords.cs b/src/DuetControlServer/Codes/Keywords.cs index 46eec363c..d0158b607 100644 --- a/src/DuetControlServer/Codes/Keywords.cs +++ b/src/DuetControlServer/Codes/Keywords.cs @@ -1,7 +1,9 @@ using DuetAPI.Commands; using DuetAPI.ObjectModel; +using DuetControlServer.Files; using DuetControlServer.Model; using System; +using System.IO; using System.Threading.Tasks; using Code = DuetControlServer.Commands.Code; @@ -31,7 +33,81 @@ public static async Task Process(Code code) if (code.Keyword == KeywordType.Echo || code.Keyword == KeywordType.Abort) { - string result = string.IsNullOrEmpty(code.KeywordArgument) ? string.Empty : await Expressions.Evaluate(code, true); + string result = string.Empty; + if (code.Keyword == KeywordType.Echo && !string.IsNullOrEmpty(code.KeywordArgument)) + { + string keywordArgument = code.KeywordArgument.TrimStart(); + if (keywordArgument.StartsWith('>')) + { + // File redirection requested + bool append = keywordArgument.StartsWith(">>"); + keywordArgument = keywordArgument.Substring(append ? 2 : 1).TrimStart(); + + // Get the file string or expression to write to + bool inQuotes = false, isComplete = false; + int numCurlyBraces = 0; + string filenameExpression = string.Empty; + for (int i = 0; i < keywordArgument.Length; i++) + { + char c = keywordArgument[i]; + if (inQuotes) + { + if (c == '"') + { + inQuotes = false; + isComplete = (numCurlyBraces == 0); + } + } + else if (c == '"') + { + inQuotes = true; + } + else if (c == '{') + { + numCurlyBraces++; + } + else if (c == '}') + { + numCurlyBraces--; + isComplete = (numCurlyBraces == 0); + } + else if (char.IsWhiteSpace(c)) + { + // Whitespaces after the initial > or >> are not permitted + isComplete = (numCurlyBraces == 0); + } + + if (isComplete) + { + if (i == 0) + { + return new Message(MessageType.Error, "Missing filename for file redirection"); + } + + filenameExpression = keywordArgument[..(i + 1)]; + code.KeywordArgument = keywordArgument[(i + 1)..]; + break; + } + } + + // Evaluate the filename and result to write + string filename = await Expressions.EvaluateExpression(code, filenameExpression, false, false); + string physicalFilename = await FilePath.ToPhysicalAsync(filename, FileDirectory.System); + result = await Expressions.Evaluate(code, true); + + // Write it to the SD card + _logger.Debug("{0} '{1}' to {2}", append ? "Appending" : "Writing", result, filename); + using (FileStream fs = new(physicalFilename, append ? FileMode.Append : FileMode.Create, FileAccess.Write)) + { + using StreamWriter writer = new(fs); + await writer.WriteLineAsync(result); + } + + // Done + return new Message(); + } + } + result = await Expressions.Evaluate(code, true); if (code.Keyword == KeywordType.Abort) { diff --git a/src/DuetControlServer/Commands/Generic/InvalidateChannel.cs b/src/DuetControlServer/Commands/Generic/InvalidateChannel.cs new file mode 100644 index 000000000..ff4e96e07 --- /dev/null +++ b/src/DuetControlServer/Commands/Generic/InvalidateChannel.cs @@ -0,0 +1,36 @@ +using DuetControlServer.IPC; +using System; +using System.Threading.Tasks; + +namespace DuetControlServer.Commands +{ + /// + /// Implementation of the command + /// + public sealed class InvalidateChannel : DuetAPI.Commands.InvalidateChannel, IConnectionCommand + { + /// + /// Source connection of this command + /// + public Connection Connection { get; set; } + + /// + /// Wait for all pending codes of the given channel to finish + /// + /// Asynchronous task + public override async Task Execute() + { + // Check if the corresponding code channel has been disabled + using (await Model.Provider.AccessReadOnlyAsync()) + { + if (Model.Provider.Get.Inputs[Channel] == null) + { + throw new InvalidOperationException("Requested code channel has been disabled"); + } + } + + // Wait for all codes and files to be invalidated + await SPI.Interface.AbortAll(Channel); + } + } +} diff --git a/src/DuetControlServer/Commands/Plugins/StartPlugins.cs b/src/DuetControlServer/Commands/Plugins/StartPlugins.cs index fe702b657..137e27d19 100644 --- a/src/DuetControlServer/Commands/Plugins/StartPlugins.cs +++ b/src/DuetControlServer/Commands/Plugins/StartPlugins.cs @@ -72,10 +72,10 @@ public override async Task Execute() Channel = DuetAPI.CodeChannel.SBC, Type = DuetAPI.Commands.CodeType.MCode, MajorNumber = 98, - Parameters = new List - { - new DuetAPI.Commands.CodeParameter('P', FilePath.DsfConfigFile) - } + Parameters = new() + { + new DuetAPI.Commands.CodeParameter('P', FilePath.DsfConfigFile) + } }; await dsfConfigCode.Execute(); } diff --git a/src/DuetControlServer/DuetControlServer.csproj b/src/DuetControlServer/DuetControlServer.csproj index ddc04ba16..ff4123077 100644 --- a/src/DuetControlServer/DuetControlServer.csproj +++ b/src/DuetControlServer/DuetControlServer.csproj @@ -6,7 +6,7 @@ default Christian Hammacher Duet3D Ltd - 3.4-b2 + 3.4-b3 GPL-3.0 true https://github.com/Duet3D/DuetSoftwareFramework.git @@ -31,7 +31,7 @@ - + diff --git a/src/DuetControlServer/FileExecution/Job.cs b/src/DuetControlServer/FileExecution/Job.cs index 683fd466a..f69d794fa 100644 --- a/src/DuetControlServer/FileExecution/Job.cs +++ b/src/DuetControlServer/FileExecution/Job.cs @@ -364,10 +364,9 @@ public static async Task Run() using (await _lock.LockAsync(Program.CancellationToken)) { // Notify RepRapFirmware that the print file has been closed -#warning FIXME Don't call StopPrint unless aborted from DSF if (IsCancelled) { - await SPI.Interface.StopPrint(PrintStoppedReason.UserCancelled); + // Prints are cancelled by M0/M1 which is processed by RRF _logger.Info("Cancelled job file"); } else if (IsAborted) diff --git a/src/DuetControlServer/Model/Expressions.cs b/src/DuetControlServer/Model/Expressions.cs index d05f8a774..02664eedd 100644 --- a/src/DuetControlServer/Model/Expressions.cs +++ b/src/DuetControlServer/Model/Expressions.cs @@ -379,13 +379,13 @@ public static async Task Evaluate(Code code, bool evaluateAll) /// Whether the final result shall be encoded /// Replaced expression(s) /// Failed to parse expression(s) - private static async Task EvaluateExpression(Code code, string expression, bool onlyLinuxFields, bool encodeResult) + public static async Task EvaluateExpression(Code code, string expression, bool onlyLinuxFields, bool encodeResult) { Stack lastBracketTypes = new(); Stack parsedExpressions = new(); parsedExpressions.Push(new StringBuilder()); - bool inQuotes = false, skipEvaluation = false; + bool inQuotes = false; char lastC = '\0'; foreach (char c in expression) { @@ -404,8 +404,6 @@ private static async Task EvaluateExpression(Code code, string expressio } else if (c == '{' || c == '(' || c == '[') { - // FIXME need better way to deal with functions with more than one parameter - skipEvaluation = (c == '(') && parsedExpressions.Peek().ToString().TrimEnd().EndsWith("exists") || parsedExpressions.Peek().ToString().TrimEnd().EndsWith("mod"); lastBracketTypes.Push(c); parsedExpressions.Push(new StringBuilder()); } @@ -433,28 +431,17 @@ private static async Task EvaluateExpression(Code code, string expressio throw new CodeParserException($"Unexpected square bracket", code); } - string subExpression = parsedExpressions.Pop().ToString(); - if (skipEvaluation) + string subExpression = parsedExpressions.Pop().ToString().Trim(); + string evaluationResult = await EvaluateSubExpression(code, subExpression, lastBracketType == '(' || onlyLinuxFields, true); + if (lastBracketType == '(' || lastBracketType == '[' || subExpression == evaluationResult || onlyLinuxFields) { - // Queries that take identifier paths must not be evaluated... - parsedExpressions.Peek().Append('('); - parsedExpressions.Peek().Append(subExpression); - parsedExpressions.Peek().Append(')'); - skipEvaluation = false; + parsedExpressions.Peek().Append(lastBracketType); + parsedExpressions.Peek().Append(evaluationResult); + parsedExpressions.Peek().Append(expectedBracketType); } else { - string evaluationResult = await EvaluateSubExpression(code, subExpression.Trim(), onlyLinuxFields, true); - if (lastBracketType == '(' || lastBracketType == '[' || subExpression == evaluationResult || onlyLinuxFields) - { - parsedExpressions.Peek().Append(lastBracketType); - parsedExpressions.Peek().Append(evaluationResult); - parsedExpressions.Peek().Append(expectedBracketType); - } - else - { - parsedExpressions.Peek().Append(evaluationResult); - } + parsedExpressions.Peek().Append(evaluationResult); } } else diff --git a/src/DuetControlServer/SPI/Channel/Processor.cs b/src/DuetControlServer/SPI/Channel/Processor.cs index 634c82e28..eac870626 100644 --- a/src/DuetControlServer/SPI/Channel/Processor.cs +++ b/src/DuetControlServer/SPI/Channel/Processor.cs @@ -768,7 +768,7 @@ public void Run() // 3. Abort requests if (_allFilesAborted) { - _allFilesAborted = !DataTransfer.WriteFilesAborted(Channel); + _allFilesAborted = !DataTransfer.WriteInvalidateChannel(Channel); return; } diff --git a/src/DuetControlServer/SPI/Communication/LinuxRequests/Request.cs b/src/DuetControlServer/SPI/Communication/LinuxRequests/Request.cs index cfa7ee6b0..8118c53fc 100644 --- a/src/DuetControlServer/SPI/Communication/LinuxRequests/Request.cs +++ b/src/DuetControlServer/SPI/Communication/LinuxRequests/Request.cs @@ -122,10 +122,10 @@ public enum Request : ushort MacroStarted = 18, /// - /// All the files have been aborted on a particular channel + /// Invalidate all files and codes on a given channel /// /// - FilesAborted = 19, + InvalidateChannel = 19, /// /// Initialize or update a variable (global, var, or set) diff --git a/src/DuetControlServer/SPI/DataTransfer.cs b/src/DuetControlServer/SPI/DataTransfer.cs index 7d1a2a542..f1cc64358 100644 --- a/src/DuetControlServer/SPI/DataTransfer.cs +++ b/src/DuetControlServer/SPI/DataTransfer.cs @@ -1109,11 +1109,11 @@ public static bool WriteMacroStarted(CodeChannel channel) } /// - /// Called when all the files have been aborted by DSF (e.g. via abort keyword) + /// Called when a code channel is supposed to be invalidated (e.g. via abort keyword) /// /// Code channel that requires the lock /// True if the packet could be written - public static bool WriteFilesAborted(CodeChannel channel) + public static bool WriteInvalidateChannel(CodeChannel channel) { int dataLength = Marshal.SizeOf(); if (!CanWritePacket(dataLength)) @@ -1121,7 +1121,7 @@ public static bool WriteFilesAborted(CodeChannel channel) return false; } - WritePacket(Communication.LinuxRequests.Request.FilesAborted, dataLength); + WritePacket(Communication.LinuxRequests.Request.InvalidateChannel, dataLength); Serialization.Writer.WriteCodeChannel(GetWriteBuffer(dataLength), channel); return true; } diff --git a/src/DuetPiManagementPlugin/DuetPiManagementPlugin.csproj b/src/DuetPiManagementPlugin/DuetPiManagementPlugin.csproj index 6464ce693..0ff3bd3bb 100644 --- a/src/DuetPiManagementPlugin/DuetPiManagementPlugin.csproj +++ b/src/DuetPiManagementPlugin/DuetPiManagementPlugin.csproj @@ -3,7 +3,7 @@ Exe net5.0 - 3.4-b2 + 3.4-b3 Duet3D Ltd Duet3D Ltd GPL-3.0 diff --git a/src/DuetPluginService/DuetPluginService.csproj b/src/DuetPluginService/DuetPluginService.csproj index 7b96cf51f..392dfc18d 100644 --- a/src/DuetPluginService/DuetPluginService.csproj +++ b/src/DuetPluginService/DuetPluginService.csproj @@ -6,7 +6,7 @@ default Christian Hammacher Duet3D Ltd - 3.4-b2 + 3.4-b3 GPL-3.0 true https://github.com/Duet3D/DuetSoftwareFramework.git @@ -17,7 +17,7 @@ - + diff --git a/src/DuetWebServer/DuetWebServer.csproj b/src/DuetWebServer/DuetWebServer.csproj index dd4a3ede2..ceab7814f 100644 --- a/src/DuetWebServer/DuetWebServer.csproj +++ b/src/DuetWebServer/DuetWebServer.csproj @@ -5,7 +5,7 @@ InProcess Christian Hammacher Duet3D Ltd - 3.4-b2 + 3.4-b3 GPL-3.0 true https://github.com/Duet3D/DuetSoftwareFramework.git diff --git a/src/LinuxApi/LinuxApi.csproj b/src/LinuxApi/LinuxApi.csproj index 77c8d2f18..a32314b6d 100644 --- a/src/LinuxApi/LinuxApi.csproj +++ b/src/LinuxApi/LinuxApi.csproj @@ -4,7 +4,7 @@ net5.0 true LGPL-3.0 - 3.4-b2 + 3.4-b3 diff --git a/src/ModelObserver/ModelObserver.csproj b/src/ModelObserver/ModelObserver.csproj index 26b649646..c942011f0 100644 --- a/src/ModelObserver/ModelObserver.csproj +++ b/src/ModelObserver/ModelObserver.csproj @@ -4,7 +4,7 @@ Exe net5.0 true - 3.4-b2 + 3.4-b3 https://github.com/Duet3D/DuetSoftwareFramework.git git GPL-3.0 diff --git a/src/PluginManager/PluginManager.csproj b/src/PluginManager/PluginManager.csproj index de50badb1..997046b52 100644 --- a/src/PluginManager/PluginManager.csproj +++ b/src/PluginManager/PluginManager.csproj @@ -10,7 +10,7 @@ https://github.com/Duet3D/DuetSoftwareFramework https://github.com/Duet3D/DuetSoftwareFramework.git git - 3.4-b2 + 3.4-b3 diff --git a/src/UnitTests/UnitTests.csproj b/src/UnitTests/UnitTests.csproj index 94e507b84..205c66358 100644 --- a/src/UnitTests/UnitTests.csproj +++ b/src/UnitTests/UnitTests.csproj @@ -16,7 +16,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - +