From c0e170c6dc34a60e2b45954ec70801b4e747e2d5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 01:38:47 +0000 Subject: [PATCH 1/6] refactor: lift stale TODO in SettingsValidator (already wired in OptionsForm) --- Class/SettingsValidator.vb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Class/SettingsValidator.vb b/Class/SettingsValidator.vb index 03e5999..06d0e4e 100644 --- a/Class/SettingsValidator.vb +++ b/Class/SettingsValidator.vb @@ -1,7 +1,8 @@ Namespace LiteTask Public Class SettingsValidator - 'TODO Reimplement in Options + ' Wired into OptionsForm.ValidateSettings (Forms/OptionsForm.vb), called from + ' SaveSettings, OnFormClosing, and TestEmailButton_Click. Private ReadOnly _logger As Logger From 5588507404b24c3a5d12f6be9a3024d2b99943b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 01:40:36 +0000 Subject: [PATCH 2/6] feat: add ValidateTaskAction and wire into TaskForm/ActionDialog Adds SettingsValidator.ValidateTaskAction(action[, siblings]) covering name/target presence, target file existence for PowerShell/Batch/Executable, non-negative retry parameters, timeout >= 1, and DependsOn referencing a sibling action's Name. ActionDialog now validates the candidate via the shared validator; TaskForm validates every action against its siblings after UpdateTaskFromForm. French translations added for the new keys. --- Class/SettingsValidator.vb | 71 ++++++++++++++++++++++++++++++++++++++ Forms/ActionDialog.vb | 32 +++++++++++++---- Forms/TaskForm.vb | 30 ++++++++++++++++ LiteTaskData/lang/fr.xml | 11 ++++++ 4 files changed, 137 insertions(+), 7 deletions(-) diff --git a/Class/SettingsValidator.vb b/Class/SettingsValidator.vb index 06d0e4e..3db66e4 100644 --- a/Class/SettingsValidator.vb +++ b/Class/SettingsValidator.vb @@ -99,6 +99,77 @@ Namespace LiteTask Return errors End Function + Public Function ValidateTaskAction(action As TaskAction) As List(Of String) + Return ValidateTaskAction(action, Nothing) + End Function + + ' Validates a single TaskAction. When siblingActions is provided, DependsOn + ' is checked against the Names in that collection (same-task semantics, see + ' TaskDependencyManager). PowerShell modules are intentionally not preflighted. + Public Function ValidateTaskAction(action As TaskAction, siblingActions As IEnumerable(Of TaskAction)) As List(Of String) + Dim errors As New List(Of String) + + Try + If action Is Nothing Then + errors.Add("Action is null") + Return errors + End If + + If String.IsNullOrWhiteSpace(action.Name) Then + errors.Add("Action name is required") + End If + + If String.IsNullOrWhiteSpace(action.Target) Then + errors.Add("Action target is required") + Else + Select Case action.Type + Case ScheduledTask.TaskType.PowerShell, + ScheduledTask.TaskType.Batch, + ScheduledTask.TaskType.Executable + If Not File.Exists(action.Target) Then + errors.Add($"Target file does not exist: {action.Target}") + End If + Case ScheduledTask.TaskType.SQL + ' Target holds the SQL string or path; require non-empty (already covered above) + Case ScheduledTask.TaskType.RemoteExecution + ' Target is a remote endpoint or script path; non-empty is sufficient here + End Select + End If + + If action.TimeoutMinutes < 1 Then + errors.Add("Timeout must be at least 1 minute") + End If + + If action.RetryCount < 0 Then + errors.Add("Retry count cannot be negative") + End If + + If action.RetryDelayMinutes < 0 Then + errors.Add("Retry delay cannot be negative") + End If + + If Not String.IsNullOrWhiteSpace(action.DependsOn) AndAlso siblingActions IsNot Nothing Then + Dim selfName = If(action.Name, String.Empty) + Dim known = siblingActions _ + .Where(Function(a) a IsNot Nothing AndAlso Not Object.ReferenceEquals(a, action)) _ + .Select(Function(a) a.Name) _ + .Where(Function(n) Not String.IsNullOrWhiteSpace(n)) + + If String.Equals(action.DependsOn, selfName, StringComparison.OrdinalIgnoreCase) Then + errors.Add($"Action '{selfName}' cannot depend on itself") + ElseIf Not known.Any(Function(n) String.Equals(n, action.DependsOn, StringComparison.OrdinalIgnoreCase)) Then + errors.Add($"DependsOn references unknown action: {action.DependsOn}") + End If + End If + + Catch ex As Exception + _logger?.LogError("Error validating task action", ex) + errors.Add($"Error validating task action: {ex.Message}") + End Try + + Return errors + End Function + Private Function IsValidEmail(email As String) As Boolean Try Dim addr = New System.Net.Mail.MailAddress(email) diff --git a/Forms/ActionDialog.vb b/Forms/ActionDialog.vb index 4f956d1..7ee0977 100644 --- a/Forms/ActionDialog.vb +++ b/Forms/ActionDialog.vb @@ -212,13 +212,31 @@ Namespace LiteTask End Sub Private Function ValidateInput() As Boolean - If String.IsNullOrWhiteSpace(_nameTextBox.Text) Then - MessageBox.Show("Please enter a name for the action.", "Validation Error", MessageBoxButtons.OK, MessageBoxIcon.Warning) - Return False - End If - - If String.IsNullOrWhiteSpace(_targetTextBox.Text) Then - MessageBox.Show("Please specify a target.", "Validation Error", MessageBoxButtons.OK, MessageBoxIcon.Warning) + ' Build a transient TaskAction reflecting the current form state and run + ' it through SettingsValidator so the UI and persisted-task validation + ' share a single rule set. + Dim candidate As New TaskAction With { + .Name = _nameTextBox.Text.Trim(), + .Type = If(_typeComboBox.SelectedItem Is Nothing, + ScheduledTask.TaskType.PowerShell, + CType([Enum].Parse(GetType(ScheduledTask.TaskType), _typeComboBox.SelectedItem.ToString()), ScheduledTask.TaskType)), + .Target = _targetTextBox.Text.Trim(), + .Parameters = _parametersTextBox.Text.Trim(), + .DependsOn = If(_dependsOnCombo.SelectedIndex > 0, _dependsOnCombo.SelectedItem.ToString(), Nothing), + .WaitForCompletion = _waitForCompletionCheck.Checked, + .TimeoutMinutes = Convert.ToInt32(_timeoutNumeric.Value), + .RetryCount = Convert.ToInt32(_retryCountNumeric.Value), + .RetryDelayMinutes = Convert.ToInt32(_retryDelayNumeric.Value), + .ContinueOnError = _continueOnErrorCheck.Checked + } + + Dim validator As New SettingsValidator(_logger) + Dim errors = validator.ValidateTaskAction(candidate) + + If errors.Any() Then + MessageBox.Show(String.Join(Environment.NewLine, errors), + TranslationManager.Instance.GetTranslation("Validation.Error", "Validation Error"), + MessageBoxButtons.OK, MessageBoxIcon.Warning) Return False End If diff --git a/Forms/TaskForm.vb b/Forms/TaskForm.vb index 0da0ea4..2e616ab 100644 --- a/Forms/TaskForm.vb +++ b/Forms/TaskForm.vb @@ -317,6 +317,11 @@ Namespace LiteTask UpdateTaskFromForm() + If Not ValidateTaskActions() Then + DialogResult = DialogResult.None + Return + End If + If _isEditMode Then _customScheduler.UpdateTask(_task) Else @@ -333,6 +338,31 @@ Namespace LiteTask End Try End Sub + Private Function ValidateTaskActions() As Boolean + If _task Is Nothing OrElse _task.Actions Is Nothing OrElse _task.Actions.Count = 0 Then + Return True + End If + + Dim validator As New SettingsValidator(_logger) + Dim allErrors As New List(Of String) + + For Each action In _task.Actions + Dim actionErrors = validator.ValidateTaskAction(action, _task.Actions) + For Each err In actionErrors + allErrors.Add($"[{If(action.Name, "(unnamed)")}] {err}") + Next + Next + + If allErrors.Any() Then + MessageBox.Show(String.Join(Environment.NewLine, allErrors), + TranslationManager.Instance.GetTranslation("Validation.Error", "Validation Error"), + MessageBoxButtons.OK, MessageBoxIcon.Warning) + Return False + End If + + Return True + End Function + Private Sub RecurrenceTypeCombo_SelectedIndexChanged(sender As Object, e As EventArgs) UpdateRecurrenceControls() End Sub diff --git a/LiteTaskData/lang/fr.xml b/LiteTaskData/lang/fr.xml index b5b0528..22ca7a6 100644 --- a/LiteTaskData/lang/fr.xml +++ b/LiteTaskData/lang/fr.xml @@ -393,4 +393,15 @@ Redémarrer le service quotidiennement pour récupérer la mémoire et les handles. Le redémarrage est ignoré si une tâche est en cours ou planifiée dans les 5 prochaines minutes. Heure du redémarrage quotidien du service (par défaut : 03h00) Envoyer une notification par courriel lors du redémarrage quotidien + + + Erreur de validation + Le nom de l'action est requis + La cible de l'action est requise + Le fichier cible n'existe pas : {0} + Le délai d'expiration doit être d'au moins 1 minute + Le nombre de tentatives ne peut pas être négatif + Le délai entre tentatives ne peut pas être négatif + L'action '{0}' ne peut pas dépendre d'elle-même + DependsOn référence une action inconnue : {0} \ No newline at end of file From a653d045941be72349a39b2540ee8cec4bb3ae28 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 01:43:16 +0000 Subject: [PATCH 3/6] feat: cmd-style quoting in TaskRunner ParseParameters + MSTest project Extracts the parsing logic into LiteTask.ParameterParser (Class/) so it can be tested in isolation. The new parser supports key="value with spaces" while remaining backward-compatible with key=value tokens. Adds an MSTest project under Tests/ wired into LiteTask.sln, with 11 cases covering the basic form, quoted spaces, mixed quoting, empty values, dropped tokens, duplicate keys, and whitespace tolerance. --- Class/ParameterParser.vb | 92 ++++++++++++++++++++++++++++++ Class/TaskRunner.vb | 17 +----- LiteTask.sln | 10 ++++ LiteTask.vbproj | 3 + Tests/LiteTask.Tests.vbproj | 22 ++++++++ Tests/ParameterParserTests.vb | 103 ++++++++++++++++++++++++++++++++++ 6 files changed, 232 insertions(+), 15 deletions(-) create mode 100644 Class/ParameterParser.vb create mode 100644 Tests/LiteTask.Tests.vbproj create mode 100644 Tests/ParameterParserTests.vb diff --git a/Class/ParameterParser.vb b/Class/ParameterParser.vb new file mode 100644 index 0000000..779fa98 --- /dev/null +++ b/Class/ParameterParser.vb @@ -0,0 +1,92 @@ +' ----------------------------------------------------------------------------- +' Copyright (c) svtica. All rights reserved. +' File: ParameterParser.vb +' Author: LiteTask contributors +' Date: 2026-04-25 +' Purpose: Parse "key=value" parameter strings used by TaskRunner. Supports +' cmd-style quoting (key="value with spaces") while remaining +' backward-compatible with the original whitespace-delimited form. +' ----------------------------------------------------------------------------- +Namespace LiteTask + Public Module ParameterParser + + ''' + ''' Parses a parameter string of the form `key=value [key2=value2 ...]`. + ''' Values may be quoted with double quotes to include spaces: + ''' key="value with spaces" + ''' Tokens without an `=` are skipped, as are `=value` fragments. + ''' Last occurrence wins for duplicate keys. + ''' + Public Function Parse(parameters As String) As Dictionary(Of String, String) + Dim result As New Dictionary(Of String, String) + If String.IsNullOrEmpty(parameters) Then Return result + + Dim i As Integer = 0 + Dim len As Integer = parameters.Length + + While i < len + ' Skip leading whitespace + While i < len AndAlso Char.IsWhiteSpace(parameters(i)) + i += 1 + End While + If i >= len Then Exit While + + ' Read key: up to '=' or whitespace + Dim keyStart As Integer = i + While i < len AndAlso parameters(i) <> "="c AndAlso Not Char.IsWhiteSpace(parameters(i)) + i += 1 + End While + + ' No '=' for this token: skip token entirely (preserves original behavior + ' where bare words without '=' were dropped). + If i >= len OrElse parameters(i) <> "="c Then + Continue While + End If + + Dim key As String = parameters.Substring(keyStart, i - keyStart) + i += 1 ' consume '=' + + ' Empty key (`=value`) is skipped to match original behavior. + If String.IsNullOrEmpty(key) Then + ' Advance past the value so we don't reparse it as a new token + SkipValue(parameters, i) + Continue While + End If + + Dim value As String = ReadValue(parameters, i) + result(key) = value + End While + + Return result + End Function + + Private Function ReadValue(s As String, ByRef i As Integer) As String + Dim len As Integer = s.Length + If i >= len Then Return String.Empty + + If s(i) = """"c Then + ' Quoted value: read until next '"' or end of string. + i += 1 + Dim valueStart As Integer = i + While i < len AndAlso s(i) <> """"c + i += 1 + End While + Dim value As String = s.Substring(valueStart, i - valueStart) + If i < len Then i += 1 ' consume closing quote + Return value + End If + + ' Unquoted value: read until whitespace. + Dim unquotedStart As Integer = i + While i < len AndAlso Not Char.IsWhiteSpace(s(i)) + i += 1 + End While + Return s.Substring(unquotedStart, i - unquotedStart) + End Function + + Private Sub SkipValue(s As String, ByRef i As Integer) + ReadValue(s, i) + End Sub + + End Module +End Namespace diff --git a/Class/TaskRunner.vb b/Class/TaskRunner.vb index b805cb8..4c509f1 100644 --- a/Class/TaskRunner.vb +++ b/Class/TaskRunner.vb @@ -803,26 +803,13 @@ Namespace LiteTask Private Function ParseParameters(parameters As String) As Dictionary(Of String, String) - Dim result As New Dictionary(Of String, String) Try - If String.IsNullOrEmpty(parameters) Then Return result - - ' Use Span for more efficient string splitting (if available) or stick with optimized approach - Dim _paramArray = parameters.Split(New Char() {" "c}, StringSplitOptions.RemoveEmptyEntries) - For Each param In _paramArray - Dim equalIndex = param.IndexOf("="c) - If equalIndex > 0 AndAlso equalIndex < param.Length - 1 Then - Dim key = param.Substring(0, equalIndex) - Dim value = param.Substring(equalIndex + 1) - result(key) = value - End If - Next - + Dim result = ParameterParser.Parse(parameters) _logger.LogInfo($"Parsed {result.Count} parameters successfully") Return result Catch ex As Exception _logger.LogError($"Error parsing parameters: {ex.Message}") - Return result + Return New Dictionary(Of String, String) End Try End Function diff --git a/LiteTask.sln b/LiteTask.sln index 5080164..5153adf 100644 --- a/LiteTask.sln +++ b/LiteTask.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.11.35208.52 MinimumVisualStudioVersion = 10.0.40219.1 Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "LiteTask", "LiteTask.vbproj", "{1A0A6447-8B40-47AE-9DA4-F9B3BF40397D}" EndProject +Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "LiteTask.Tests", "Tests\LiteTask.Tests.vbproj", "{B3C5D7E9-F124-4A6B-8C9D-1E2F3A4B5C6D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,14 @@ Global {1A0A6447-8B40-47AE-9DA4-F9B3BF40397D}.Release|Any CPU.Build.0 = Release|Any CPU {1A0A6447-8B40-47AE-9DA4-F9B3BF40397D}.Release|x64.ActiveCfg = Release|x64 {1A0A6447-8B40-47AE-9DA4-F9B3BF40397D}.Release|x64.Build.0 = Release|x64 + {B3C5D7E9-F124-4A6B-8C9D-1E2F3A4B5C6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B3C5D7E9-F124-4A6B-8C9D-1E2F3A4B5C6D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B3C5D7E9-F124-4A6B-8C9D-1E2F3A4B5C6D}.Debug|x64.ActiveCfg = Debug|x64 + {B3C5D7E9-F124-4A6B-8C9D-1E2F3A4B5C6D}.Debug|x64.Build.0 = Debug|x64 + {B3C5D7E9-F124-4A6B-8C9D-1E2F3A4B5C6D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B3C5D7E9-F124-4A6B-8C9D-1E2F3A4B5C6D}.Release|Any CPU.Build.0 = Release|Any CPU + {B3C5D7E9-F124-4A6B-8C9D-1E2F3A4B5C6D}.Release|x64.ActiveCfg = Release|x64 + {B3C5D7E9-F124-4A6B-8C9D-1E2F3A4B5C6D}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/LiteTask.vbproj b/LiteTask.vbproj index f99d5ab..74a33c4 100644 --- a/LiteTask.vbproj +++ b/LiteTask.vbproj @@ -76,14 +76,17 @@ + + + diff --git a/Tests/LiteTask.Tests.vbproj b/Tests/LiteTask.Tests.vbproj new file mode 100644 index 0000000..8eab277 --- /dev/null +++ b/Tests/LiteTask.Tests.vbproj @@ -0,0 +1,22 @@ + + + net8.0-windows8.0 + true + LiteTask.Tests + false + true + true + AnyCPU;x64 + $(NoWarn);BC42021 + + + + + + + + + + + + diff --git a/Tests/ParameterParserTests.vb b/Tests/ParameterParserTests.vb new file mode 100644 index 0000000..3145993 --- /dev/null +++ b/Tests/ParameterParserTests.vb @@ -0,0 +1,103 @@ +' ----------------------------------------------------------------------------- +' Copyright (c) svtica. All rights reserved. +' File: ParameterParserTests.vb +' Author: LiteTask contributors +' Date: 2026-04-25 +' Purpose: Unit tests for LiteTask.ParameterParser covering the original +' unquoted form and the new cmd-style quoting. +' ----------------------------------------------------------------------------- +Imports Microsoft.VisualStudio.TestTools.UnitTesting +Imports LiteTask.LiteTask + +Namespace LiteTask.Tests + + Public Class ParameterParserTests + + + Public Sub Parse_SinglePair_ReturnsKeyAndValue() + Dim result = ParameterParser.Parse("key=value") + Assert.AreEqual(1, result.Count) + Assert.AreEqual("value", result("key")) + End Sub + + + Public Sub Parse_QuotedValueWithSpaces_KeepsValueIntact() + Dim result = ParameterParser.Parse("key=""value with spaces""") + Assert.AreEqual(1, result.Count) + Assert.AreEqual("value with spaces", result("key")) + End Sub + + + Public Sub Parse_MultiplePairs_AllParsed() + Dim result = ParameterParser.Parse("a=1 b=2 c=3") + Assert.AreEqual(3, result.Count) + Assert.AreEqual("1", result("a")) + Assert.AreEqual("2", result("b")) + Assert.AreEqual("3", result("c")) + End Sub + + + Public Sub Parse_MixedQuotedAndUnquoted_BothHandled() + Dim result = ParameterParser.Parse("a=1 b=""hello world"" c=3") + Assert.AreEqual(3, result.Count) + Assert.AreEqual("1", result("a")) + Assert.AreEqual("hello world", result("b")) + Assert.AreEqual("3", result("c")) + End Sub + + + Public Sub Parse_EmptyQuotedValue_YieldsEmptyString() + Dim result = ParameterParser.Parse("a=1 b="""" c=3") + Assert.AreEqual(3, result.Count) + Assert.AreEqual("", result("b")) + End Sub + + + Public Sub Parse_EmptyUnquotedValue_YieldsEmptyString() + Dim result = ParameterParser.Parse("a= b=2") + Assert.AreEqual(2, result.Count) + Assert.AreEqual("", result("a")) + Assert.AreEqual("2", result("b")) + End Sub + + + Public Sub Parse_NullOrEmpty_ReturnsEmptyDictionary() + Assert.AreEqual(0, ParameterParser.Parse(Nothing).Count) + Assert.AreEqual(0, ParameterParser.Parse("").Count) + Assert.AreEqual(0, ParameterParser.Parse(" ").Count) + End Sub + + + Public Sub Parse_TokenWithoutEquals_IsSkipped() + Dim result = ParameterParser.Parse("a=1 garbage b=2") + Assert.AreEqual(2, result.Count) + Assert.AreEqual("1", result("a")) + Assert.AreEqual("2", result("b")) + Assert.IsFalse(result.ContainsKey("garbage")) + End Sub + + + Public Sub Parse_QuotedValueFollowedByMore_ContinuesParsing() + Dim result = ParameterParser.Parse("path=""C:\Program Files\App"" mode=fast") + Assert.AreEqual(2, result.Count) + Assert.AreEqual("C:\Program Files\App", result("path")) + Assert.AreEqual("fast", result("mode")) + End Sub + + + Public Sub Parse_DuplicateKey_LastWriteWins() + Dim result = ParameterParser.Parse("a=1 a=2 a=3") + Assert.AreEqual(1, result.Count) + Assert.AreEqual("3", result("a")) + End Sub + + + Public Sub Parse_LeadingTrailingWhitespace_Tolerated() + Dim result = ParameterParser.Parse(" a=1 b=2 ") + Assert.AreEqual(2, result.Count) + Assert.AreEqual("1", result("a")) + Assert.AreEqual("2", result("b")) + End Sub + + End Class +End Namespace From f144db6ca505b9c25d639bb65a7f47f001bdb1b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 01:43:36 +0000 Subject: [PATCH 4/6] docs: add CHANGELOG with polishing pass entries --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cdacd44 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +All notable changes to this project are documented in this file. + +## [Unreleased] + +### Changed +- `SettingsValidator`: removed stale `TODO Reimplement in Options` comment; the validator is already wired into `OptionsForm.ValidateSettings`. +- `TaskRunner.ParseParameters`: now delegates to a new testable `LiteTask.ParameterParser` module that supports cmd-style quoting (`key="value with spaces"`) while remaining backward-compatible with `key=value`. + +### Added +- `SettingsValidator.ValidateTaskAction(action[, siblings])`: validates name/target presence, target file existence for PowerShell/Batch/Executable types, non-negative retry parameters, timeout >= 1, and `DependsOn` referencing a sibling action's `Name`. +- `ActionDialog` and `TaskForm` now run candidate actions through `ValidateTaskAction` on save; cross-action `DependsOn` consistency is checked at the task level. +- `Tests/LiteTask.Tests.vbproj`: MSTest project covering `ParameterParser` (11 cases: basic form, quoted spaces, mixed quoting, empty values, dropped tokens, duplicate keys, whitespace tolerance). +- French translations for the new `Validation.*` keys in `LiteTaskData/lang/fr.xml`. From b9d3fd2faac44f019ec7a3303fab1fec2dd8254d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 02:02:17 +0000 Subject: [PATCH 5/6] fix: drop redundant Namespace block in ParameterParserTests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test .vbproj already sets RootNamespace=LiteTask.Tests, and VB prepends RootNamespace to declared namespaces. The 'Namespace LiteTask.Tests' wrapper was producing the doubled name LiteTask.Tests.LiteTask.Tests.* — harmless at runtime (MSTest discovers by attribute) but confusing. Class now lives at LiteTask.Tests.ParameterParserTests as expected. --- Tests/ParameterParserTests.vb | 158 +++++++++++++++++----------------- 1 file changed, 78 insertions(+), 80 deletions(-) diff --git a/Tests/ParameterParserTests.vb b/Tests/ParameterParserTests.vb index 3145993..7cc1748 100644 --- a/Tests/ParameterParserTests.vb +++ b/Tests/ParameterParserTests.vb @@ -9,95 +9,93 @@ Imports Microsoft.VisualStudio.TestTools.UnitTesting Imports LiteTask.LiteTask -Namespace LiteTask.Tests - - Public Class ParameterParserTests + +Public Class ParameterParserTests - - Public Sub Parse_SinglePair_ReturnsKeyAndValue() - Dim result = ParameterParser.Parse("key=value") - Assert.AreEqual(1, result.Count) - Assert.AreEqual("value", result("key")) - End Sub + + Public Sub Parse_SinglePair_ReturnsKeyAndValue() + Dim result = ParameterParser.Parse("key=value") + Assert.AreEqual(1, result.Count) + Assert.AreEqual("value", result("key")) + End Sub - - Public Sub Parse_QuotedValueWithSpaces_KeepsValueIntact() - Dim result = ParameterParser.Parse("key=""value with spaces""") - Assert.AreEqual(1, result.Count) - Assert.AreEqual("value with spaces", result("key")) - End Sub + + Public Sub Parse_QuotedValueWithSpaces_KeepsValueIntact() + Dim result = ParameterParser.Parse("key=""value with spaces""") + Assert.AreEqual(1, result.Count) + Assert.AreEqual("value with spaces", result("key")) + End Sub - - Public Sub Parse_MultiplePairs_AllParsed() - Dim result = ParameterParser.Parse("a=1 b=2 c=3") - Assert.AreEqual(3, result.Count) - Assert.AreEqual("1", result("a")) - Assert.AreEqual("2", result("b")) - Assert.AreEqual("3", result("c")) - End Sub + + Public Sub Parse_MultiplePairs_AllParsed() + Dim result = ParameterParser.Parse("a=1 b=2 c=3") + Assert.AreEqual(3, result.Count) + Assert.AreEqual("1", result("a")) + Assert.AreEqual("2", result("b")) + Assert.AreEqual("3", result("c")) + End Sub - - Public Sub Parse_MixedQuotedAndUnquoted_BothHandled() - Dim result = ParameterParser.Parse("a=1 b=""hello world"" c=3") - Assert.AreEqual(3, result.Count) - Assert.AreEqual("1", result("a")) - Assert.AreEqual("hello world", result("b")) - Assert.AreEqual("3", result("c")) - End Sub + + Public Sub Parse_MixedQuotedAndUnquoted_BothHandled() + Dim result = ParameterParser.Parse("a=1 b=""hello world"" c=3") + Assert.AreEqual(3, result.Count) + Assert.AreEqual("1", result("a")) + Assert.AreEqual("hello world", result("b")) + Assert.AreEqual("3", result("c")) + End Sub - - Public Sub Parse_EmptyQuotedValue_YieldsEmptyString() - Dim result = ParameterParser.Parse("a=1 b="""" c=3") - Assert.AreEqual(3, result.Count) - Assert.AreEqual("", result("b")) - End Sub + + Public Sub Parse_EmptyQuotedValue_YieldsEmptyString() + Dim result = ParameterParser.Parse("a=1 b="""" c=3") + Assert.AreEqual(3, result.Count) + Assert.AreEqual("", result("b")) + End Sub - - Public Sub Parse_EmptyUnquotedValue_YieldsEmptyString() - Dim result = ParameterParser.Parse("a= b=2") - Assert.AreEqual(2, result.Count) - Assert.AreEqual("", result("a")) - Assert.AreEqual("2", result("b")) - End Sub + + Public Sub Parse_EmptyUnquotedValue_YieldsEmptyString() + Dim result = ParameterParser.Parse("a= b=2") + Assert.AreEqual(2, result.Count) + Assert.AreEqual("", result("a")) + Assert.AreEqual("2", result("b")) + End Sub - - Public Sub Parse_NullOrEmpty_ReturnsEmptyDictionary() - Assert.AreEqual(0, ParameterParser.Parse(Nothing).Count) - Assert.AreEqual(0, ParameterParser.Parse("").Count) - Assert.AreEqual(0, ParameterParser.Parse(" ").Count) - End Sub + + Public Sub Parse_NullOrEmpty_ReturnsEmptyDictionary() + Assert.AreEqual(0, ParameterParser.Parse(Nothing).Count) + Assert.AreEqual(0, ParameterParser.Parse("").Count) + Assert.AreEqual(0, ParameterParser.Parse(" ").Count) + End Sub - - Public Sub Parse_TokenWithoutEquals_IsSkipped() - Dim result = ParameterParser.Parse("a=1 garbage b=2") - Assert.AreEqual(2, result.Count) - Assert.AreEqual("1", result("a")) - Assert.AreEqual("2", result("b")) - Assert.IsFalse(result.ContainsKey("garbage")) - End Sub + + Public Sub Parse_TokenWithoutEquals_IsSkipped() + Dim result = ParameterParser.Parse("a=1 garbage b=2") + Assert.AreEqual(2, result.Count) + Assert.AreEqual("1", result("a")) + Assert.AreEqual("2", result("b")) + Assert.IsFalse(result.ContainsKey("garbage")) + End Sub - - Public Sub Parse_QuotedValueFollowedByMore_ContinuesParsing() - Dim result = ParameterParser.Parse("path=""C:\Program Files\App"" mode=fast") - Assert.AreEqual(2, result.Count) - Assert.AreEqual("C:\Program Files\App", result("path")) - Assert.AreEqual("fast", result("mode")) - End Sub + + Public Sub Parse_QuotedValueFollowedByMore_ContinuesParsing() + Dim result = ParameterParser.Parse("path=""C:\Program Files\App"" mode=fast") + Assert.AreEqual(2, result.Count) + Assert.AreEqual("C:\Program Files\App", result("path")) + Assert.AreEqual("fast", result("mode")) + End Sub - - Public Sub Parse_DuplicateKey_LastWriteWins() - Dim result = ParameterParser.Parse("a=1 a=2 a=3") - Assert.AreEqual(1, result.Count) - Assert.AreEqual("3", result("a")) - End Sub + + Public Sub Parse_DuplicateKey_LastWriteWins() + Dim result = ParameterParser.Parse("a=1 a=2 a=3") + Assert.AreEqual(1, result.Count) + Assert.AreEqual("3", result("a")) + End Sub - - Public Sub Parse_LeadingTrailingWhitespace_Tolerated() - Dim result = ParameterParser.Parse(" a=1 b=2 ") - Assert.AreEqual(2, result.Count) - Assert.AreEqual("1", result("a")) - Assert.AreEqual("2", result("b")) - End Sub + + Public Sub Parse_LeadingTrailingWhitespace_Tolerated() + Dim result = ParameterParser.Parse(" a=1 b=2 ") + Assert.AreEqual(2, result.Count) + Assert.AreEqual("1", result("a")) + Assert.AreEqual("2", result("b")) + End Sub - End Class -End Namespace +End Class From 079b158d3da4b07ba8992ed47ce935e110ac8c07 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 02:08:10 +0000 Subject: [PATCH 6/6] fix: rename 'err' loop variable in ValidateTaskActions VB.NET has an intrinsic 'Err' object; using 'err' as a ForEach loop variable triggers BC30068 'Expression is a value and therefore cannot be the target of an assignment'. Renamed to 'actionError'. --- Forms/TaskForm.vb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Forms/TaskForm.vb b/Forms/TaskForm.vb index 2e616ab..573f9c6 100644 --- a/Forms/TaskForm.vb +++ b/Forms/TaskForm.vb @@ -348,8 +348,8 @@ Namespace LiteTask For Each action In _task.Actions Dim actionErrors = validator.ValidateTaskAction(action, _task.Actions) - For Each err In actionErrors - allErrors.Add($"[{If(action.Name, "(unnamed)")}] {err}") + For Each actionError In actionErrors + allErrors.Add($"[{If(action.Name, "(unnamed)")}] {actionError}") Next Next