diff --git a/TickSpec.Tests/EscapedPipe.feature b/TickSpec.Tests/EscapedPipe.feature new file mode 100644 index 0000000..9b68237 --- /dev/null +++ b/TickSpec.Tests/EscapedPipe.feature @@ -0,0 +1,67 @@ +Feature: Escaped characters in table cells + + Scenario Outline: Values with escaped pipes are parsed correctly + Given a value + Then the value is + + Examples: + | value | expected | + | no pipe | no pipe | + | before\|after | before\|after | + | \|leading | \|leading | + | trailing\| | trailing\| | + | a\|b\|c | a\|b\|c | + + Scenario Outline: Values with escaped backslashes are parsed correctly + Given a value + Then the value is + + Examples: + | value | expected | + | hello\\world | hello\\world | + + Scenario Outline: Values with escaped newlines are parsed correctly + Given a value + Then the value is + + Examples: + | value | expected | + | line1\nline2 | line1\nline2 | + + Scenario: Step table with escaped pipes + Given a table with escaped pipes + | col1 | col2 | + | hello\|world | normal | + | a\|b\|c | x | + Then the table cell 0,0 is hello|world + And the table cell 0,1 is normal + And the table cell 1,0 is a|b|c + + Scenario: Step table with escaped backslashes + Given a table with escaped backslashes + | col1 | col2 | + | hello\\world | normal | + Then the table cell 0,0 is hello\world + And the table cell 0,1 is normal + + Scenario: Escaped backslash before pipe acts as cell delimiter + Given a table with escaped backslash before pipe + | col1 | col2 | col3 | + | before\\| after | end | + Then the table cell 0,0 is before\ + And the table cell 0,1 is after + And the table cell 0,2 is end + + Scenario: Triple backslash-pipe is escaped backslash then escaped pipe + Given a table with triple backslash-pipe + | col1 | col2 | + | a\\\|b | normal | + Then the table cell 0,0 is a\|b + And the table cell 0,1 is normal + + Scenario: Escaped backslash before n is literal backslash-n not newline + Given a table with escaped backslash before n + | col1 | col2 | + | test\\n | normal | + Then the table cell 0,0 is test\n + And the table cell 0,1 is normal diff --git a/TickSpec.Tests/FeatureParserTest.fs b/TickSpec.Tests/FeatureParserTest.fs index fc0f40c..234e02f 100644 --- a/TickSpec.Tests/FeatureParserTest.fs +++ b/TickSpec.Tests/FeatureParserTest.fs @@ -604,4 +604,151 @@ let RuleKeyword_ParseBlocks () = let secondRule = featureBlock.Rules.[1] Assert.AreEqual("Second business rule", secondRule.Name) Assert.AreEqual(0, secondRule.Background.Length) // No rule-level background - Assert.AreEqual(1, secondRule.Scenarios.Length) // One scenario in second rule \ No newline at end of file + Assert.AreEqual(1, secondRule.Scenarios.Length) // One scenario in second rule + +[] +let EscapedCharacters_ParseLines () = + "TickSpec.Tests.EscapedPipe.feature" + |> loadFeatureFile + |> verifyLineParsing <| + [ + FileStart + FeatureName "Escaped characters in table cells" + + // Scenario Outline: escaped pipes + Scenario "Scenario Outline: Values with escaped pipes are parsed correctly" + Step (GivenStep "a value ") + Step (ThenStep "the value is ") + Examples + Item (Examples, TableRow [ "value"; "expected" ]) + Item (Examples, TableRow [ "no pipe"; "no pipe" ]) + Item (Examples, TableRow [ "before|after"; "before|after" ]) + Item (Examples, TableRow [ "|leading"; "|leading" ]) + Item (Examples, TableRow [ "trailing|"; "trailing|" ]) + Item (Examples, TableRow [ "a|b|c"; "a|b|c" ]) + + // Scenario Outline: escaped backslashes + Scenario "Scenario Outline: Values with escaped backslashes are parsed correctly" + Step (GivenStep "a value ") + Step (ThenStep "the value is ") + Examples + Item (Examples, TableRow [ "value"; "expected" ]) + Item (Examples, TableRow [ "hello\\world"; "hello\\world" ]) + + // Scenario Outline: escaped newlines + Scenario "Scenario Outline: Values with escaped newlines are parsed correctly" + Step (GivenStep "a value ") + Step (ThenStep "the value is ") + Examples + Item (Examples, TableRow [ "value"; "expected" ]) + Item (Examples, TableRow [ "line1\nline2"; "line1\nline2" ]) + + // Scenario: step table with escaped pipes + Scenario "Scenario: Step table with escaped pipes" + Step (GivenStep "a table with escaped pipes") + Item (Step (GivenStep "a table with escaped pipes"), TableRow [ "col1"; "col2" ]) + Item (Step (GivenStep "a table with escaped pipes"), TableRow [ "hello|world"; "normal" ]) + Item (Step (GivenStep "a table with escaped pipes"), TableRow [ "a|b|c"; "x" ]) + Step (ThenStep "the table cell 0,0 is hello|world") + Step (ThenStep "the table cell 0,1 is normal") + Step (ThenStep "the table cell 1,0 is a|b|c") + + // Scenario: step table with escaped backslashes + Scenario "Scenario: Step table with escaped backslashes" + Step (GivenStep "a table with escaped backslashes") + Item (Step (GivenStep "a table with escaped backslashes"), TableRow [ "col1"; "col2" ]) + Item (Step (GivenStep "a table with escaped backslashes"), TableRow [ "hello\\world"; "normal" ]) + Step (ThenStep "the table cell 0,0 is hello\\world") + Step (ThenStep "the table cell 0,1 is normal") + + // Scenario: escaped backslash before pipe acts as delimiter + Scenario "Scenario: Escaped backslash before pipe acts as cell delimiter" + Step (GivenStep "a table with escaped backslash before pipe") + Item (Step (GivenStep "a table with escaped backslash before pipe"), TableRow [ "col1"; "col2"; "col3" ]) + Item (Step (GivenStep "a table with escaped backslash before pipe"), TableRow [ "before\\"; "after"; "end" ]) + Step (ThenStep "the table cell 0,0 is before\\") + Step (ThenStep "the table cell 0,1 is after") + Step (ThenStep "the table cell 0,2 is end") + + // Scenario: triple backslash-pipe (\\\| -> \|) + Scenario "Scenario: Triple backslash-pipe is escaped backslash then escaped pipe" + Step (GivenStep "a table with triple backslash-pipe") + Item (Step (GivenStep "a table with triple backslash-pipe"), TableRow [ "col1"; "col2" ]) + Item (Step (GivenStep "a table with triple backslash-pipe"), TableRow [ "a\\|b"; "normal" ]) + Step (ThenStep "the table cell 0,0 is a\\|b") + Step (ThenStep "the table cell 0,1 is normal") + + // Scenario: escaped backslash before n (\\n -> \n literal, not newline) + Scenario "Scenario: Escaped backslash before n is literal backslash-n not newline" + Step (GivenStep "a table with escaped backslash before n") + Item (Step (GivenStep "a table with escaped backslash before n"), TableRow [ "col1"; "col2" ]) + Item (Step (GivenStep "a table with escaped backslash before n"), TableRow [ "test\\n"; "normal" ]) + Step (ThenStep "the table cell 0,0 is test\\n") + Step (ThenStep "the table cell 0,1 is normal") + ] + +[] +let EscapedCharacters_ParseFeature () = + let featureSource = + "TickSpec.Tests.EscapedPipe.feature" + |> loadFeatureFile + |> FeatureParser.parseFeature + + Assert.AreEqual("Escaped characters in table cells", featureSource.Name) + + // Outline 1: escaped pipes (5 data rows -> scenarios 0-4) + let scenario1 = featureSource.Scenarios.[1] + let params1 = scenario1.Parameters |> dict + Assert.AreEqual("before|after", params1.["value"]) + Assert.AreEqual("before|after", params1.["expected"]) + + let scenario4 = featureSource.Scenarios.[4] + let params4 = scenario4.Parameters |> dict + Assert.AreEqual("a|b|c", params4.["value"]) + Assert.AreEqual("a|b|c", params4.["expected"]) + + // Outline 2: escaped backslashes (1 data row -> scenario 5) + let bsScenario = featureSource.Scenarios.[5] + let bsParams = bsScenario.Parameters |> dict + Assert.AreEqual("hello\\world", bsParams.["value"]) + Assert.AreEqual("hello\\world", bsParams.["expected"]) + + // Outline 3: escaped newlines (1 data row -> scenario 6) + let nlScenario = featureSource.Scenarios.[6] + let nlParams = nlScenario.Parameters |> dict + Assert.AreEqual("line1\nline2", nlParams.["value"]) + Assert.AreEqual("line1\nline2", nlParams.["expected"]) + + // Scenario: step table with escaped pipes (scenario 7) + let pipeTableScenario = featureSource.Scenarios.[7] + let pipeTable = (pipeTableScenario.Steps.[0] |> snd).Table.Value + Assert.AreEqual([| "col1"; "col2" |], pipeTable.Header) + Assert.AreEqual("hello|world", pipeTable.Rows.[0].[0]) + Assert.AreEqual("normal", pipeTable.Rows.[0].[1]) + Assert.AreEqual("a|b|c", pipeTable.Rows.[1].[0]) + + // Scenario: step table with escaped backslashes (scenario 8) + let bsTableScenario = featureSource.Scenarios.[8] + let bsTable = (bsTableScenario.Steps.[0] |> snd).Table.Value + Assert.AreEqual("hello\\world", bsTable.Rows.[0].[0]) + Assert.AreEqual("normal", bsTable.Rows.[0].[1]) + + // Scenario: escaped backslash before pipe = cell delimiter (scenario 9) + let delimScenario = featureSource.Scenarios.[9] + let delimTable = (delimScenario.Steps.[0] |> snd).Table.Value + Assert.AreEqual([| "col1"; "col2"; "col3" |], delimTable.Header) + Assert.AreEqual("before\\", delimTable.Rows.[0].[0]) + Assert.AreEqual("after", delimTable.Rows.[0].[1]) + Assert.AreEqual("end", delimTable.Rows.[0].[2]) + + // Scenario: triple backslash-pipe \\\| -> \| (scenario 10) + let tripleScenario = featureSource.Scenarios.[10] + let tripleTable = (tripleScenario.Steps.[0] |> snd).Table.Value + Assert.AreEqual("a\\|b", tripleTable.Rows.[0].[0]) + Assert.AreEqual("normal", tripleTable.Rows.[0].[1]) + + // Scenario: \\n -> literal \n, not newline (scenario 11) + let bsNScenario = featureSource.Scenarios.[11] + let bsNTable = (bsNScenario.Steps.[0] |> snd).Table.Value + Assert.AreEqual("test\\n", bsNTable.Rows.[0].[0]) + Assert.IsFalse(bsNTable.Rows.[0].[0].Contains("\n"), "Should not contain actual newline") \ No newline at end of file diff --git a/TickSpec.Tests/TickSpec.Tests.fsproj b/TickSpec.Tests/TickSpec.Tests.fsproj index fccb25b..a7b5842 100644 --- a/TickSpec.Tests/TickSpec.Tests.fsproj +++ b/TickSpec.Tests/TickSpec.Tests.fsproj @@ -9,6 +9,7 @@ + diff --git a/TickSpec/LineParser.fs b/TickSpec/LineParser.fs index ba19324..be14d71 100644 --- a/TickSpec/LineParser.fs +++ b/TickSpec/LineParser.fs @@ -84,8 +84,21 @@ let (|ButLine|_|) s = |> Option.map (function Trim t -> ButLine t) let (|TableRowLine|_|) (s:string) = if s.Trim().StartsWith("|") then - let columnsStrings = s.Trim().Split([|'|'|], System.StringSplitOptions.RemoveEmptyEntries) - let columns = [ for (Trim s) in columnsStrings -> s ] + let escapedBackslash = "\u0000" + let escapedPipe = "\u0001" + let escapedNewline = "\u0002" + let escaped = + s.Trim() + .Replace("\\\\", escapedBackslash) + .Replace("\\|", escapedPipe) + .Replace("\\n", escapedNewline) + let columnsStrings = escaped.Split([|'|'|], System.StringSplitOptions.RemoveEmptyEntries) + let columns = + [ for (Trim s) in columnsStrings -> + s + .Replace(escapedBackslash, "\\") + .Replace(escapedPipe, "|") + .Replace(escapedNewline, "\n") ] TableRowLine columns |> Some else None let (|Bullet|_|) (s:string) =