diff --git a/test/DotNetEnv.Tests/ExtensionsTests.cs b/test/DotNetEnv.Tests/ExtensionsTests.cs index dfadd2e..38c7693 100644 --- a/test/DotNetEnv.Tests/ExtensionsTests.cs +++ b/test/DotNetEnv.Tests/ExtensionsTests.cs @@ -1,42 +1,62 @@ using System; using System.Collections.Generic; using DotNetEnv.Extensions; +using DotNetEnv.Tests.XUnit; using Xunit; namespace DotNetEnv.Tests; public class ExtensionsTests { - [Fact] - public void ToDotEnvDictionaryTest() + private static readonly KeyValuePair FirstValuePair = new("key", "value"); + private static readonly KeyValuePair FirstValuePairDupe = new("key", "dupe"); + private static readonly KeyValuePair SecondValuePair = new("key2", "value2"); + + private static readonly KeyValuePair[] KvpSetNoDupe = { FirstValuePair, SecondValuePair }; + private static readonly KeyValuePair[] KvpSetWithDupe = { FirstValuePair, FirstValuePairDupe }; + + /// + /// Data: _, dictionaryOption, input, expectedValue + /// + public static readonly TheoryData< + string, CreateDictionaryOption, KeyValuePair[], KeyValuePair[]> + ToDotEnvDictionaryTestData = + new IndexedTheoryData< + CreateDictionaryOption, KeyValuePair[], KeyValuePair[]> + { + { CreateDictionaryOption.Throw, KvpSetNoDupe, KvpSetNoDupe }, + { CreateDictionaryOption.TakeFirst, KvpSetWithDupe, new[] { FirstValuePair } }, + { CreateDictionaryOption.TakeFirst, KvpSetNoDupe, KvpSetNoDupe }, + { CreateDictionaryOption.TakeLast, KvpSetWithDupe, new[] { FirstValuePairDupe } }, + { CreateDictionaryOption.TakeLast, KvpSetNoDupe, KvpSetNoDupe }, + }; + + [Theory] + [MemberData(nameof(ToDotEnvDictionaryTestData))] + public void ToDotEnvDictionaryWithKvpSetNoDupeShouldContainValues(string _, + CreateDictionaryOption dictionaryOption, + KeyValuePair[] input, + KeyValuePair[] expected) { - var kvpSetNoDupe = new List>() - { - new("key", "value"), - new("key2", "value2"), - }; - - var kvpSetWithDupe = new List>() - { - new("key", "value"), - new("key", "value2"), - }; - - Assert.Throws(() => kvpSetWithDupe.ToDotEnvDictionary(CreateDictionaryOption.Throw)); - var noDupeAndThrowOption = kvpSetNoDupe.ToDotEnvDictionary(CreateDictionaryOption.Throw); - Assert.Equal("value", noDupeAndThrowOption["key"]); - Assert.Equal("value2", noDupeAndThrowOption["key2"]); - - var withDupeAndTakeFirstOption = kvpSetWithDupe.ToDotEnvDictionary(CreateDictionaryOption.TakeFirst); - Assert.Equal("value", withDupeAndTakeFirstOption["key"]); - var noDupeAndTakeFirstOption = kvpSetNoDupe.ToDotEnvDictionary(CreateDictionaryOption.TakeFirst); - Assert.Equal("value", noDupeAndTakeFirstOption["key"]); - Assert.Equal("value2", noDupeAndTakeFirstOption["key2"]); - - var withDupeAndTakeLastOption = kvpSetWithDupe.ToDotEnvDictionary(); - Assert.Equal("value2", withDupeAndTakeLastOption["key"]); - var noDupeAndTakeLastOption = kvpSetNoDupe.ToDotEnvDictionary(); - Assert.Equal("value", noDupeAndTakeLastOption["key"]); - Assert.Equal("value2", noDupeAndTakeLastOption["key2"]); + var dotEnvDictionary = input.ToDotEnvDictionary(dictionaryOption); + + foreach (var (key, value) in expected) + Assert.Equal(value, dotEnvDictionary[key]); } + + [Theory] + [MemberData(nameof(ToDotEnvDictionaryTestData))] + public void ToDotEnvDictionaryWithKvpSetNoDupeShouldHaveCorrectNumberOfEntries(string _, + CreateDictionaryOption dictionaryOption, + KeyValuePair[] input, + KeyValuePair[] expectedValues) + { + var dotEnvDictionary = input.ToDotEnvDictionary(dictionaryOption); + + Assert.Equal(expectedValues.Length, dotEnvDictionary.Count); + } + + [Fact] + public void ToDotEnvDictionaryWithThrowOptionShouldThrowOnDupes() => + Assert.Throws(() => KvpSetWithDupe.ToDotEnvDictionary(CreateDictionaryOption.Throw)); } diff --git a/test/DotNetEnv.Tests/FrameworkTests.cs b/test/DotNetEnv.Tests/FrameworkTests.cs new file mode 100644 index 0000000..cfbde60 --- /dev/null +++ b/test/DotNetEnv.Tests/FrameworkTests.cs @@ -0,0 +1,28 @@ +using System; +using System.Text; +using DotNetEnv.Tests.Helper; +using Xunit; + +namespace DotNetEnv.Tests; + +public class FrameworkTests +{ + [Fact] + public void CheckUnicodeFunctionality() + { + Assert.Equal("®", Encoding.Unicode.GetString(new byte[] { 0xae, 0x00 })); + Assert.Equal(UnicodeChars.Rocket, Encoding.UTF32.GetString(new byte[] { 0x80, 0xf6, 0x01, 0x00 })); + Assert.Equal(UnicodeChars.Rocket, Encoding.UTF32.GetString(new byte[] { 0x80, 0xf6, 0x1, 0x0 })); + } + + [Fact] + public void CheckEnvironmentVariableFunctionality() + { + Environment.SetEnvironmentVariable("EV_DNE", null); + Assert.Null(Environment.GetEnvironmentVariable("EV_DNE")); + + // Note that dotnet returns null if the env var is empty -- even if it was set to empty! + Environment.SetEnvironmentVariable("EV_DNE", ""); + Assert.Null(Environment.GetEnvironmentVariable("EV_DNE")); + } +} diff --git a/test/DotNetEnv.Tests/Helper/UnicodeChars.cs b/test/DotNetEnv.Tests/Helper/UnicodeChars.cs new file mode 100644 index 0000000..563a719 --- /dev/null +++ b/test/DotNetEnv.Tests/Helper/UnicodeChars.cs @@ -0,0 +1,10 @@ +namespace DotNetEnv.Tests.Helper; + +public struct UnicodeChars +{ + // https://stackoverflow.com/questions/602912/how-do-you-echo-a-4-digit-unicode-character-in-bash + // printf '\xE2\x98\xA0' + // printf ☠ | hexdump # hexdump has bytes flipped per word (2 bytes, 4 hex) + + public const string Rocket = "\ud83d\ude80"; // 🚀 +} diff --git a/test/DotNetEnv.Tests/LoadOptionsTests.cs b/test/DotNetEnv.Tests/LoadOptionsTests.cs index d1d9ce5..fee03ad 100644 --- a/test/DotNetEnv.Tests/LoadOptionsTests.cs +++ b/test/DotNetEnv.Tests/LoadOptionsTests.cs @@ -1,101 +1,41 @@ +using System; +using DotNetEnv.Tests.XUnit; using Xunit; namespace DotNetEnv.Tests { public class LoadOptionsTests { - [Fact] - public void StaticEnvTest() + /// + /// Data: _, optionsUnderTest, expectedSetEnvVars, expectedClobberExistingVars, expectedOnlyExactPath + /// + public static readonly TheoryData + LoadOptionTestCombinations = new IndexedTheoryData() + { + { Env.NoEnvVars(), false, true, true }, + { Env.NoClobber(), true, false, true }, + { Env.TraversePath(), true, true, false }, + { LoadOptions.NoEnvVars(), false, true, true }, + { LoadOptions.NoClobber(), true, false, true }, + { LoadOptions.TraversePath(), true, true, false }, + { new LoadOptions(), true, true, true }, + { new LoadOptions().NoEnvVars(), false, true, true }, + { new LoadOptions().NoClobber(), true, false, true }, + { new LoadOptions().TraversePath(), true, true, false }, + { Env.NoEnvVars().NoClobber().TraversePath(), false, false, false }, + { Env.NoClobber().TraversePath(), true, false, false }, + { Env.NoEnvVars().NoClobber(), false, false, true }, + { Env.NoEnvVars().TraversePath(), false, true, false }, + }; + + [Theory] + [MemberData(nameof(LoadOptionTestCombinations))] + public void LoadOptionsShouldHaveCorrectPropertiesSet(string _, LoadOptions optionsUnderTest, + bool expectedSetEnvVars, bool expectedClobberExistingVars, bool expectedOnlyExactPath) { - LoadOptions options; - - options = DotNetEnv.Env.NoEnvVars(); - Assert.False(options.SetEnvVars); - Assert.True(options.ClobberExistingVars); - Assert.True(options.OnlyExactPath); - - options = DotNetEnv.Env.NoClobber(); - Assert.True(options.SetEnvVars); - Assert.False(options.ClobberExistingVars); - Assert.True(options.OnlyExactPath); - - options = DotNetEnv.Env.TraversePath(); - Assert.True(options.SetEnvVars); - Assert.True(options.ClobberExistingVars); - Assert.False(options.OnlyExactPath); - } - - [Fact] - public void StaticOptionsTest() - { - LoadOptions options; - - options = DotNetEnv.LoadOptions.NoEnvVars(); - Assert.False(options.SetEnvVars); - Assert.True(options.ClobberExistingVars); - Assert.True(options.OnlyExactPath); - - options = DotNetEnv.LoadOptions.NoClobber(); - Assert.True(options.SetEnvVars); - Assert.False(options.ClobberExistingVars); - Assert.True(options.OnlyExactPath); - - options = DotNetEnv.LoadOptions.TraversePath(); - Assert.True(options.SetEnvVars); - Assert.True(options.ClobberExistingVars); - Assert.False(options.OnlyExactPath); - } - - [Fact] - public void InstanceTest() - { - LoadOptions options; - - options = new DotNetEnv.LoadOptions(); - Assert.True(options.SetEnvVars); - Assert.True(options.ClobberExistingVars); - Assert.True(options.OnlyExactPath); - - options = new DotNetEnv.LoadOptions().NoEnvVars(); - Assert.False(options.SetEnvVars); - Assert.True(options.ClobberExistingVars); - Assert.True(options.OnlyExactPath); - - options = new DotNetEnv.LoadOptions().NoClobber(); - Assert.True(options.SetEnvVars); - Assert.False(options.ClobberExistingVars); - Assert.True(options.OnlyExactPath); - - options = new DotNetEnv.LoadOptions().TraversePath(); - Assert.True(options.SetEnvVars); - Assert.True(options.ClobberExistingVars); - Assert.False(options.OnlyExactPath); - } - - [Fact] - public void ComboTest() - { - LoadOptions options; - - options = DotNetEnv.Env.NoEnvVars().NoClobber().TraversePath(); - Assert.False(options.SetEnvVars); - Assert.False(options.ClobberExistingVars); - Assert.False(options.OnlyExactPath); - - options = DotNetEnv.Env.NoClobber().TraversePath(); - Assert.True(options.SetEnvVars); - Assert.False(options.ClobberExistingVars); - Assert.False(options.OnlyExactPath); - - options = DotNetEnv.Env.NoEnvVars().NoClobber(); - Assert.False(options.SetEnvVars); - Assert.False(options.ClobberExistingVars); - Assert.True(options.OnlyExactPath); - - options = DotNetEnv.Env.NoEnvVars().TraversePath(); - Assert.False(options.SetEnvVars); - Assert.True(options.ClobberExistingVars); - Assert.False(options.OnlyExactPath); + Assert.Equal(expectedSetEnvVars, optionsUnderTest.SetEnvVars); + Assert.Equal(expectedClobberExistingVars, optionsUnderTest.ClobberExistingVars); + Assert.Equal(expectedOnlyExactPath, optionsUnderTest.OnlyExactPath); } } } diff --git a/test/DotNetEnv.Tests/ParserTests.cs b/test/DotNetEnv.Tests/ParserTests.cs index ed0c6f7..648d1b0 100644 --- a/test/DotNetEnv.Tests/ParserTests.cs +++ b/test/DotNetEnv.Tests/ParserTests.cs @@ -1,8 +1,9 @@ using System; using System.Linq; using System.Collections.Generic; -using System.Text; using DotNetEnv.Superpower; +using DotNetEnv.Tests.Helper; +using DotNetEnv.Tests.XUnit; using Xunit; using Superpower; using Superpower.Parsers; @@ -11,11 +12,6 @@ namespace DotNetEnv.Tests { public class ParserTests : IDisposable { - // C# wow that you can't handle 32 bit unicode as chars. wow. strings for 4 byte chars. - private static readonly string RocketChar = char.ConvertFromUtf32(0x1F680); // 🚀 - - private const string EXCEPT_CHARS = "'\"$"; - private const string EV_TEST = "ENVVAR_TEST"; private const string EV_DNE = "EV_DNE"; private const string EV_TEST_1 = "EV_TEST_1"; @@ -42,484 +38,421 @@ public void Dispose () } } + [Theory] + [InlineData("name")] + [InlineData("_n")] + [InlineData("__")] + [InlineData("_0")] + [InlineData("a_b")] + [InlineData("_a_b")] + [InlineData("a.b")] + [InlineData("a-b")] + public void IdentifierShouldParseUntilEnd(string identifier) => + Assert.Equal(identifier, Parsers.Identifier.AtEnd().Parse(identifier)); + + [Theory] + [InlineData("\"name")] + [InlineData("0name")] + [InlineData(".a.b")] + [InlineData("-a.b")] + [InlineData("a!b")] + [InlineData("a?b")] + [InlineData("a*b")] + [InlineData("a:b")] + public void IdentifierShouldThrowOnParseUntilEnd(string invalidIdentifier) => + Assert.Throws(() => Parsers.Identifier.AtEnd().Parse(invalidIdentifier)); + + [Theory] + [InlineData(33, @"\41")] + [InlineData(33, @"\041")] + [InlineData(90, @"\132")] + //[InlineData(???, @"\412")] // bash accepts values outside of ASCII range? check printf "\412" + public void OctalByteShouldParseUntilEnd(byte expected, string input) => + Assert.Equal(expected, Parsers.OctalByte.AtEnd().Parse(input)); + + [Theory] + [InlineData("!", @"\41")] + [InlineData("!", @"\041")] + [InlineData("Z", @"\132")] + //[InlineData(???, @"\412")] // as above with ShouldParseOctalByte() for values outside of ASCII range + // TODO: tests for octal combinations to utf8? + public void OctalCharShouldParseUntilEnd(string expected, string input) => + Assert.Equal(expected, Parsers.OctalChar.AtEnd().Parse(input)); + + [Theory] + [InlineData(90, @"\x5a")] + public void HexByteShouldParseUntilEnd(byte expected, string input) => + Assert.Equal(expected, Parsers.HexByte.AtEnd().Parse(input)); + + [Theory] + [InlineData("Z", @"\x5a")] + [InlineData("®", @"\xc2\xae")] + [InlineData("☠", @"\xE2\x98\xA0")] + [InlineData("日", @"\xe6\x97\xa5")] + [InlineData("本", @"\xe6\x9c\xac")] + [InlineData(UnicodeChars.Rocket, @"\xF0\x9F\x9A\x80")] + public void Utf8CharShouldParseUntilEnd(string expected, string input) => + Assert.Equal(expected, Parsers.Utf8Char.AtEnd().Parse(input)); + + [Theory] + [InlineData("®", @"\u00ae")] + [InlineData("®", @"\uae")] + public void Utf16CharShouldParseUntilEnd(string expected, string input) => + Assert.Equal(expected, Parsers.Utf16Char.AtEnd().Parse(input)); + + [Theory] + [InlineData(UnicodeChars.Rocket, @"\U0001F680")] + [InlineData(UnicodeChars.Rocket, @"\U1F680")] + public void Utf32CharShouldParseUntilEnd(string expected, string input) => + Assert.Equal(expected, Parsers.Utf32Char.AtEnd().Parse(input)); + + [InlineData("\b", "\\b")] + [InlineData("'", "\\'")] + [InlineData("\"", "\\\"")] + public void EscapedCharShouldParseUntilEnd(string expected, string input) => + Assert.Equal(expected, Parsers.EscapedChar.AtEnd().Parse(input)); + + [Theory] + [InlineData("\n")] + public void EscapedCharShouldThrowOnParseUntilEnd(string invalidInput) => + Assert.Throws(() => Parsers.EscapedChar.AtEnd().Parse(invalidInput)); + + [Theory] + [InlineData("ENV value", "$ENVVAR_TEST")] + public void InterpolatedEnvVarShouldParseUntilEnd(string expected, string input) => + Assert.Equal(expected, Parsers.InterpolatedEnvVar.AtEnd().Parse(input).GetValue()); + + [Theory] + [InlineData("ENV value", "${ENVVAR_TEST}")] + public void InterpolatedBracesEnvVarShouldParseUntilEnd(string expected, string input) => + Assert.Equal(expected, Parsers.InterpolatedBracesEnvVar.AtEnd().Parse(input).GetValue()); + + [Theory] + [InlineData("ENV value", "$ENVVAR_TEST")] + [InlineData("ENV value", "${ENVVAR_TEST}")] + [InlineData("", "${ENVVAR_TEST_DNE}")] + public void InterpolatedValueShouldParseUntilEnd(string expected, string input) => + Assert.Equal(expected, Parsers.InterpolatedValue.AtEnd().Parse(input).GetValue()); + + [Theory] + [InlineData("a", "a", "")] + [InlineData("%", "%", "")] + [InlineData("\"", "\"", "1")] + [InlineData("$","$", "1")] + [InlineData("a","a", "1")] + public void NotControlNorWhitespaceShouldParseUntilEnd(string expected, string input, string excludedChars) => + Assert.Equal(expected, Parsers.NotControlNorWhitespace(excludedChars).AtEnd().Parse(input)); + + [Theory] + [InlineData(" ", "")] + [InlineData(" ", "1234")] + [InlineData("\n", "")] + [InlineData("'", "'")] + [InlineData("\"", "\"")] + [InlineData("$", "$")] + [InlineData("a", "a")] + public void NotControlNorWhitespaceShouldThrowOnParseUntilEnd(string invalidInput, string excludedChars) => + Assert.Throws(() => + Parsers.NotControlNorWhitespace(excludedChars).AtEnd().Parse(invalidInput)); + + [Theory] + [InlineData("Z", @"\x5A")] + [InlineData("®", @"\xc2\xae")] + [InlineData("☠", @"\xE2\x98\xA0")] + [InlineData("日", @"\xe6\x97\xa5")] + [InlineData("本", @"\xe6\x9c\xac")] + [InlineData("®", @"\u00ae")] + [InlineData("®", @"\uae")] + [InlineData("\b", "\\b")] + [InlineData("\\m", "\\m")] + [InlineData("'", "\\'")] + [InlineData("\"", "\\\"")] + [InlineData(UnicodeChars.Rocket, @"\xF0\x9F\x9A\x80")] + [InlineData(UnicodeChars.Rocket, @"\U0001F680")] + [InlineData(UnicodeChars.Rocket, @"\U1F680")] + public void SpecialCharShouldParseUntilEnd(string expected, string input) => + Assert.Equal(expected, Parsers.SpecialChar.AtEnd().Parse(input)); + + [Theory] + [InlineData("a")] + [InlineData("%")] + [InlineData(" ")] + [InlineData("\n")] + public void SpecialCharShouldThrowOnParseUntilEnd(string invalidInput) => + Assert.Throws(() => Parsers.SpecialChar.AtEnd().Parse(invalidInput)); + + /// + /// this caught a bug, once upon a time, where backslashes were + /// processed by NCNW rather than EscapedChar inside SpecialChar + /// [Fact] - public void ParseIdentifier () - { - Assert.Equal("name", Parsers.Identifier.AtEnd().Parse("name")); - Assert.Equal("_n", Parsers.Identifier.AtEnd().Parse("_n")); - Assert.Equal("__", Parsers.Identifier.AtEnd().Parse("__")); - Assert.Equal("_0", Parsers.Identifier.AtEnd().Parse("_0")); - Assert.Equal("a_b", Parsers.Identifier.AtEnd().Parse("a_b")); - Assert.Equal("_a_b", Parsers.Identifier.AtEnd().Parse("_a_b")); - Assert.Equal("a.b", Parsers.Identifier.AtEnd().Parse("a.b")); - Assert.Equal("a-b", Parsers.Identifier.AtEnd().Parse("a-b")); - Assert.Throws(() => Parsers.Identifier.AtEnd().Parse("\"name")); - Assert.Throws(() => Parsers.Identifier.AtEnd().Parse("0name")); - Assert.Throws(() => Parsers.Identifier.AtEnd().Parse(".a.b")); - Assert.Throws(() => Parsers.Identifier.AtEnd().Parse("-a.b")); - Assert.Throws(() => Parsers.Identifier.AtEnd().Parse("a!b")); - Assert.Throws(() => Parsers.Identifier.AtEnd().Parse("a?b")); - Assert.Throws(() => Parsers.Identifier.AtEnd().Parse("a*b")); - Assert.Throws(() => Parsers.Identifier.AtEnd().Parse("a:b")); - } - - [Fact] - public void ParseOctalByte () - { - Assert.Equal(33, Parsers.OctalByte.AtEnd().Parse(@"\41")); - Assert.Equal(33, Parsers.OctalByte.AtEnd().Parse(@"\041")); - Assert.Equal(90, Parsers.OctalByte.AtEnd().Parse(@"\132")); - - // NOTE that bash accepts values outside of ASCII range? - // printf "\412" - //Assert.Equal(???, Parsers.OctalChar.AtEnd().Parse(@"\412")); - } - - [Fact] - public void ParseOctalChar () - { - Assert.Equal("!", Parsers.OctalChar.AtEnd().Parse(@"\41")); - Assert.Equal("!", Parsers.OctalChar.AtEnd().Parse(@"\041")); - Assert.Equal("Z", Parsers.OctalChar.AtEnd().Parse(@"\132")); - - // as above for values outside of ASCII range - - // TODO: tests for octal combinations to utf8? - } - - [Fact] - public void ParseHexByte () - { - Assert.Equal(90, Parsers.HexByte.AtEnd().Parse(@"\x5a")); - } - - [Fact] - public void ParseUtf8Char () - { - // https://stackoverflow.com/questions/602912/how-do-you-echo-a-4-digit-unicode-character-in-bash - // printf '\xE2\x98\xA0' - // printf ☠ | hexdump # hexdump has bytes flipped per word (2 bytes, 4 hex) - - Assert.Equal("Z", Parsers.Utf8Char.AtEnd().Parse(@"\x5A")); - Assert.Equal("®", Parsers.Utf8Char.AtEnd().Parse(@"\xc2\xae")); - Assert.Equal("☠", Parsers.Utf8Char.AtEnd().Parse(@"\xE2\x98\xA0")); - Assert.Equal(RocketChar, Parsers.Utf8Char.AtEnd().Parse(@"\xF0\x9F\x9A\x80")); - - Assert.Equal( - "日本", - Parsers.Utf8Char.AtEnd().Parse(@"\xe6\x97\xa5") - + Parsers.Utf8Char.AtEnd().Parse(@"\xe6\x9c\xac") - ); - } - - [Fact] - public void ParseUtf16Char () - { - Assert.Equal("®", Parsers.Utf16Char.AtEnd().Parse(@"\u00ae")); - Assert.Equal("®", Parsers.Utf16Char.AtEnd().Parse(@"\uae")); - - Assert.Equal("®", Encoding.Unicode.GetString(new byte[] { 0xae, 0x00 })); - } - - [Fact] - public void ParseUtf32Char () - { - Assert.Equal(RocketChar, Parsers.Utf32Char.AtEnd().Parse(@"\U0001F680")); - Assert.Equal(RocketChar, Parsers.Utf32Char.AtEnd().Parse(@"\U1F680")); - - Assert.Equal(RocketChar, Encoding.UTF32.GetString(new byte[] { 0x80, 0xf6, 0x01, 0x00 })); - Assert.Equal(RocketChar, Encoding.UTF32.GetString(new byte[] { 0x80, 0xf6, 0x1, 0x0 })); - } - - [Fact] - public void ParseEscapedChar () - { - Assert.Equal("\b", Parsers.EscapedChar.AtEnd().Parse("\\b")); - Assert.Equal("'", Parsers.EscapedChar.AtEnd().Parse("\\'")); - Assert.Equal("\"", Parsers.EscapedChar.AtEnd().Parse("\\\"")); - Assert.Throws(() => Parsers.EscapedChar.AtEnd().Parse("\n")); - } - - [Fact] - public void ParseInterpolatedEnvVar () - { - Assert.Equal("ENV value", Parsers.InterpolatedEnvVar.AtEnd().Parse("$ENVVAR_TEST").GetValue()); - Assert.Equal("ENV value", Parsers.InterpolatedBracesEnvVar.AtEnd().Parse("${ENVVAR_TEST}").GetValue()); - } - - [Fact] - public void ParseInterpolated () + public void SpecialCharShouldParseEscapedChars() { - Assert.Equal("ENV value", Parsers.InterpolatedValue.AtEnd().Parse("$ENVVAR_TEST").GetValue()); - Assert.Equal("ENV value", Parsers.InterpolatedValue.AtEnd().Parse("${ENVVAR_TEST}").GetValue()); - Assert.Equal("", Parsers.InterpolatedValue.AtEnd().Parse("${ENVVAR_TEST_DNE}").GetValue()); - } - - [Fact] - public void ParseNotControlNorWhitespace () - { - Assert.Equal("a", Parsers.NotControlNorWhitespace(EXCEPT_CHARS).AtEnd().Parse("a")); - Assert.Equal("%", Parsers.NotControlNorWhitespace(EXCEPT_CHARS).AtEnd().Parse("%")); - Assert.Throws(() => Parsers.NotControlNorWhitespace(EXCEPT_CHARS).AtEnd().Parse(" ")); - Assert.Throws(() => Parsers.NotControlNorWhitespace(EXCEPT_CHARS).AtEnd().Parse("\n")); - Assert.Throws(() => Parsers.NotControlNorWhitespace(EXCEPT_CHARS).AtEnd().Parse("'")); - Assert.Throws(() => Parsers.NotControlNorWhitespace(EXCEPT_CHARS).AtEnd().Parse("\"")); - Assert.Throws(() => Parsers.NotControlNorWhitespace(EXCEPT_CHARS).AtEnd().Parse("$")); - } - - [Fact] - public void ParseSpecialChar () - { - Assert.Equal("Z", Parsers.SpecialChar.AtEnd().Parse(@"\x5A")); - Assert.Equal("®", Parsers.SpecialChar.AtEnd().Parse(@"\xc2\xae")); - Assert.Equal("☠", Parsers.SpecialChar.AtEnd().Parse(@"\xE2\x98\xA0")); - Assert.Equal(RocketChar, Parsers.SpecialChar.AtEnd().Parse(@"\xF0\x9F\x9A\x80")); - Assert.Equal( - "日本", - Parsers.SpecialChar.AtEnd().Parse(@"\xe6\x97\xa5") - + Parsers.SpecialChar.AtEnd().Parse(@"\xe6\x9c\xac") - ); - - Assert.Equal("®", Parsers.SpecialChar.AtEnd().Parse(@"\u00ae")); - Assert.Equal("®", Parsers.SpecialChar.AtEnd().Parse(@"\uae")); - - Assert.Equal(RocketChar, Parsers.SpecialChar.AtEnd().Parse(@"\U0001F680")); - Assert.Equal(RocketChar, Parsers.SpecialChar.AtEnd().Parse(@"\U1F680")); - - Assert.Equal("\b", Parsers.SpecialChar.AtEnd().Parse("\\b")); - Assert.Equal("\\m", Parsers.SpecialChar.AtEnd().Parse("\\m")); - Assert.Equal("'", Parsers.SpecialChar.AtEnd().Parse("\\'")); - Assert.Equal("\"", Parsers.SpecialChar.AtEnd().Parse("\\\"")); - - // this caught a bug, once upon a time, where backslashes were - // getting processed by NCNW rather than EscapedChar inside SpecialChar var parser = Parsers.SpecialChar .Or(Parsers.NotControlNorWhitespace("\"$")) .Or(Character.WhiteSpace.AtLeastOnce().Text()); - Assert.Equal("\"", parser.AtEnd().Parse("\\\"")); - - Assert.Throws(() => Parsers.SpecialChar.AtEnd().Parse("a")); - Assert.Throws(() => Parsers.SpecialChar.AtEnd().Parse("%")); - Assert.Throws(() => Parsers.SpecialChar.AtEnd().Parse(" ")); - Assert.Throws(() => Parsers.SpecialChar.AtEnd().Parse("\n")); - } - - [Fact] - public void ParseComment () - { - Assert.Equal(" comment 1", Parsers.Comment.AtEnd().Parse("# comment 1")); - Assert.Equal("", Parsers.Comment.AtEnd().Parse("#")); - Assert.Equal(" ", Parsers.Comment.AtEnd().Parse("# ")); - } - - [Fact] - public void ParseEmpty () - { - var kvp = new KeyValuePair(null, null); - - Assert.Throws(() => Parsers.Empty.AtEnd().Parse("")); - - Assert.Equal(kvp, Parsers.Empty.AtEnd().Parse("# comment 1")); - Assert.Equal(kvp, Parsers.Empty.AtEnd().Parse("# comment 2\r\n")); - Assert.Equal(kvp, Parsers.Empty.AtEnd().Parse("# comment 3\n")); - - Assert.Equal(kvp, Parsers.Empty.AtEnd().Parse("\r\n")); - Assert.Equal(kvp, Parsers.Empty.AtEnd().Parse("\n")); - - Assert.Equal(kvp, Parsers.Empty.AtEnd().Parse(" # comment 1")); - Assert.Equal(kvp, Parsers.Empty.AtEnd().Parse(" \r\n")); - Assert.Equal(kvp, Parsers.Empty.AtEnd().Parse("#export EV_DNE=\"\\xe6\\x97\\xa5 $ENVVAR_TEST 本\"#ccccc\n")); - } - - [Fact] - public void ParseUnquotedValue () - { - Assert.Equal("abc", Parsers.UnquotedValue.AtEnd().Parse("abc").Value); - Assert.Equal("a b c", Parsers.UnquotedValue.AtEnd().Parse("a b c").Value); - Assert.Equal("041", Parsers.UnquotedValue.AtEnd().Parse("041").Value); - Assert.Equal("日本", Parsers.UnquotedValue.AtEnd().Parse("日本").Value); - Assert.Throws(() => Parsers.UnquotedValue.AtEnd().Parse("0\n1")); - Assert.Throws(() => Parsers.UnquotedValue.AtEnd().Parse("'")); - - Assert.Equal("0", Parsers.UnquotedValue.Parse("0\n1").Value); // value ends on linebreak - - // leading singlequotes/doublequotes/whitespaces/tabs are not allowed - Assert.Throws(() => Parsers.UnquotedValue.Parse("'")); - Assert.Throws(() => Parsers.UnquotedValue.Parse("\"")); - Assert.Throws(() => Parsers.UnquotedValue.Parse(" ")); - Assert.Throws(() => Parsers.UnquotedValue.Parse("\t")); - - Assert.Equal("", Parsers.UnquotedValue.Parse("#").Value); // no value, empty comment - Assert.Equal("", Parsers.UnquotedValue.Parse("#commentOnly").Value); - - // prevent quotationChars inside unquoted values - Assert.Throws(() => Parsers.UnquotedValue.AtEnd().Parse("a'b'c")); - Assert.Throws(() => Parsers.UnquotedValue.AtEnd().Parse("a\"b\"c")); - Assert.Throws(() => Parsers.UnquotedValue.AtEnd().Parse("a 'b' c")); - Assert.Throws(() => Parsers.UnquotedValue.AtEnd().Parse("a \"b\" c")); - - Assert.Equal("a\\?b", Parsers.UnquotedValue.AtEnd().Parse("a\\?b").Value); - Assert.Equal(@"\xe6\x97\xa5ENV value本", Parsers.UnquotedValue.AtEnd().Parse("\\xe6\\x97\\xa5${ENVVAR_TEST}本").Value); - } - - [Fact] - public void ParseDoubleQuotedValueContents () - { - Assert.Equal("abc", Parsers.DoubleQuotedValueContents.AtEnd().Parse("abc").Value); - Assert.Equal("a b c", Parsers.DoubleQuotedValueContents.AtEnd().Parse("a b c").Value); - Assert.Equal("0\n1", Parsers.DoubleQuotedValueContents.AtEnd().Parse("0\n1").Value); - Assert.Equal("日 本", Parsers.DoubleQuotedValueContents.AtEnd().Parse(@"\xe6\x97\xa5 \xe6\x9c\xac").Value); - Assert.Equal("☠ ®", Parsers.DoubleQuotedValueContents.AtEnd().Parse(@"\xE2\x98\xA0 \uae").Value); - - Assert.Equal("日 ENV value 本", Parsers.DoubleQuotedValueContents.AtEnd().Parse("\\xe6\\x97\\xa5 $ENVVAR_TEST 本").Value); - - Assert.Equal("日", Parsers.DoubleQuotedValueContents.AtEnd().Parse(@"\xe6\x97\xa5").Value); - Assert.Equal("本", Parsers.DoubleQuotedValueContents.AtEnd().Parse(@"\xe6\x9c\xac").Value); - Assert.Equal("日 本", Parsers.DoubleQuotedValueContents.AtEnd().Parse(@"\xe6\x97\xa5 \xe6\x9c\xac").Value); - Assert.Equal("日本", Parsers.DoubleQuotedValueContents.AtEnd().Parse(@"\xe6\x97\xa5\xe6\x9c\xac").Value); - - Assert.Equal("a\"b c", Parsers.DoubleQuotedValueContents.AtEnd().Parse("a\\\"b c").Value); - Assert.Equal("a'b c", Parsers.DoubleQuotedValueContents.AtEnd().Parse("a'b c").Value); - } - - [Fact] - public void ParseSingleQuotedValueContents () - { - Assert.Equal("abc", Parsers.SingleQuotedValueContents.AtEnd().Parse("abc").Value); - Assert.Equal("a b c", Parsers.SingleQuotedValueContents.AtEnd().Parse("a b c").Value); - Assert.Equal("0\n1", Parsers.SingleQuotedValueContents.AtEnd().Parse("0\n1").Value); - Assert.Equal(@"\xe6\x97\xa5 \xe6\x9c\xac", Parsers.SingleQuotedValueContents.AtEnd().Parse(@"\xe6\x97\xa5 \xe6\x9c\xac").Value); - Assert.Equal(@"\xE2\x98\xA0 \uae", Parsers.SingleQuotedValueContents.AtEnd().Parse(@"\xE2\x98\xA0 \uae").Value); - - Assert.Equal("\\xe6\\x97\\xa5 $ENVVAR_TEST 本", Parsers.SingleQuotedValueContents.AtEnd().Parse("\\xe6\\x97\\xa5 $ENVVAR_TEST 本").Value); - - Assert.Equal("a\"b c", Parsers.SingleQuotedValueContents.AtEnd().Parse("a\"b c").Value); - } - - [Fact] - public void ParseSingleQuotedValue () - { - Assert.Equal("abc", Parsers.SingleQuotedValue.AtEnd().Parse("'abc'").Value); - Assert.Equal("a b c", Parsers.SingleQuotedValue.AtEnd().Parse("'a b c'").Value); - Assert.Equal("0\n1", Parsers.SingleQuotedValue.AtEnd().Parse("'0\n1'").Value); - Assert.Equal("a\"bc", Parsers.SingleQuotedValue.AtEnd().Parse("'a\"bc'").Value); - - Assert.Equal("\\xe6\\x97\\xa5 $ENVVAR_TEST 本", Parsers.SingleQuotedValue.AtEnd().Parse("'\\xe6\\x97\\xa5 $ENVVAR_TEST 本'").Value); - - Assert.Throws(() => Parsers.SingleQuotedValue.AtEnd().Parse("'a\\'b c'").Value); - } - - [Fact] - public void ParseDoubleQuotedValue () - { - Assert.Equal("abc", Parsers.DoubleQuotedValue.AtEnd().Parse("\"abc\"").Value); - Assert.Equal("a b c", Parsers.DoubleQuotedValue.AtEnd().Parse("\"a b c\"").Value); - Assert.Equal("0\n1", Parsers.DoubleQuotedValue.AtEnd().Parse("\"0\n1\"").Value); - Assert.Equal("a'bc", Parsers.DoubleQuotedValue.AtEnd().Parse("\"a'bc\"").Value); - Assert.Equal("a\"bc", Parsers.DoubleQuotedValue.AtEnd().Parse("\"a\\\"bc\"").Value); - - Assert.Equal("日 ENV value 本", Parsers.DoubleQuotedValue.AtEnd().Parse("\"\\xe6\\x97\\xa5 $ENVVAR_TEST 本\"").Value); + Assert.Equal("\"", parser.AtEnd().Parse("\\\"")); } - [Fact] - public void TestExportExpression() + [Theory] + [InlineData(" comment 1", "# comment 1")] + [InlineData("", "#")] + [InlineData(" ", "# ")] + public void CommentShouldParseUntilEnd(string expected, string input) => + Assert.Equal(expected, Parsers.Comment.AtEnd().Parse(input)); + + [Theory] + [InlineData("# comment 1")] + [InlineData("# comment 2\r\n")] + [InlineData("# comment 3\n")] + [InlineData("\r\n")] + [InlineData("\n")] + [InlineData(" # comment 1")] + [InlineData(" \r\n")] + [InlineData("#export EV_DNE=\"\\xe6\\x97\\xa5 $ENVVAR_TEST 本\"#ccccc\n")] + public void EmptyShouldParseUntilEnd(string input) { - Assert.Throws(() => Parsers.ExportExpression.AtEnd().Parse("identifier ")); - Assert.Equal("export", Parsers.ExportExpression.AtEnd().Parse("export ")); - Assert.Equal("set -x", Parsers.ExportExpression.AtEnd().Parse("set -x ")); - Assert.Equal("set", Parsers.ExportExpression.AtEnd().Parse("set ")); - Assert.Equal("SET", Parsers.ExportExpression.AtEnd().Parse("SET ")); - } + var expected = new KeyValuePair(null, null); - [Fact] - public void ParseValue () - { - Assert.Equal("abc", Parsers.Value.AtEnd().Parse("abc").Value); - Assert.Equal("a b c", Parsers.Value.AtEnd().Parse("a b c").Value); - Assert.Equal("a b c", Parsers.Value.AtEnd().Parse("'a b c'").Value); - Assert.Equal("a#b#c", Parsers.Value.AtEnd().Parse("a#b#c").Value); - Assert.Equal("041", Parsers.Value.AtEnd().Parse("041").Value); - Assert.Equal("日本", Parsers.Value.AtEnd().Parse("日本").Value); - - Assert.Equal(@"\xe6\x97\xa5\xe6\x9c\xac", Parsers.Value.AtEnd().Parse(@"\xe6\x97\xa5\xe6\x9c\xac").Value); - Assert.Equal(@"\xe6\x97\xa5\xe6\x9c\xac", Parsers.Value.AtEnd().Parse(@"'\xe6\x97\xa5\xe6\x9c\xac'").Value); - Assert.Equal("日本", Parsers.Value.AtEnd().Parse("\"\\xe6\\x97\\xa5\\xe6\\x9c\\xac\"").Value); - - Assert.Throws(() => Parsers.Value.AtEnd().Parse("0\n1")); - Assert.Throws(() => Parsers.Value.Parse(" ")); - - Assert.Equal("0", Parsers.Value.Parse("0\n1").Value); // value ends on linebreak - - // throw on unmatched quotes - Assert.Throws(() => Parsers.Value.Parse("'")); - Assert.Throws(() => Parsers.Value.Parse("\"")); - - Assert.Equal(@"\xe6\x97\xa5", Parsers.Value.AtEnd().Parse(@"\xe6\x97\xa5").Value); - Assert.Equal(@"\xE2\x98\xA0", Parsers.Value.AtEnd().Parse(@"\xE2\x98\xA0").Value); - - Assert.Equal("abc", Parsers.Value.AtEnd().Parse("'abc'").Value); - Assert.Equal("a b c", Parsers.Value.AtEnd().Parse("'a b c'").Value); - Assert.Equal("0\n1", Parsers.Value.AtEnd().Parse("'0\n1'").Value); - Assert.Equal(@"\xe6\x97\xa5 \xe6\x9c\xac", Parsers.Value.AtEnd().Parse(@"'\xe6\x97\xa5 \xe6\x9c\xac'").Value); - Assert.Equal(@"\xE2\x98\xA0 \uae", Parsers.Value.AtEnd().Parse(@"'\xE2\x98\xA0 \uae'").Value); - - Assert.Equal("abc", Parsers.Value.AtEnd().Parse("\"abc\"").Value); - Assert.Equal("a b c", Parsers.Value.AtEnd().Parse("\"a b c\"").Value); - Assert.Equal("0\n1", Parsers.Value.AtEnd().Parse("\"0\n1\"").Value); - Assert.Equal("日 本", Parsers.Value.AtEnd().Parse("\"\\xe6\\x97\\xa5 \\xe6\\x9c\\xac\"").Value); - Assert.Equal("☠ ®", Parsers.Value.AtEnd().Parse("\"\\xE2\\x98\\xA0 \\uae\"").Value); - - Assert.Equal("日 ENV value 本", Parsers.Value.AtEnd().Parse("\"\\xe6\\x97\\xa5 $ENVVAR_TEST 本\"").Value); + Assert.Equal(expected, Parsers.Empty.AtEnd().Parse(input)); } - [Fact] - public void ParseAssignment () + [Theory] + [InlineData("")] + [InlineData("that's not empty")] + public void EmptyShouldThrowOnParseUntilEnd(string input) => + Assert.Throws(() => Parsers.Empty.AtEnd().Parse(input)); + + [Theory] + [InlineData("abc", "abc")] + [InlineData("a b c", "a b c")] + [InlineData("041", "041")] + [InlineData("日本", "日本")] + [InlineData("a\\?b", "a\\?b")] + [InlineData(@"\xe6\x97\xa5ENV value本", "\\xe6\\x97\\xa5${ENVVAR_TEST}本")] + public void UnquotedValueShouldParseUntilEnd(string expected, string input) => + Assert.Equal(expected, Parsers.UnquotedValue.AtEnd().Parse(input).Value); + + [Theory] + [InlineData("0", "0\n1")] // value ends on linebreak + [InlineData("", "#")] // no value, empty comment + [InlineData("", "#commentOnly")] + public void UnquotedValueShouldParse(string expected, string input) => + Assert.Equal(expected, Parsers.UnquotedValue.Parse(input).Value); + + [Theory] + [InlineData("0\n1")] // linebreak within value + [InlineData("'")] // single quote only + [InlineData("\"")] // double quote only + [InlineData("'value")] // leading single quote + [InlineData("\"value")] // leading double quote + [InlineData(" value")] // leading whitespace + [InlineData("\tvalue")] // leading tab + [InlineData("a'b'c")] // inline single quote + [InlineData("a\"b\"c")] // inline double quote + [InlineData("a 'b' c")] // inline single quotes surrounded with whitespaces + [InlineData("a \"b\" c")] // inline double quotes surrounded with whitespaces + public void UnquotedValueShouldThrowOnParseUntilEnd(string invalidInput) => + Assert.Throws(() => Parsers.UnquotedValue.AtEnd().Parse(invalidInput)); + + [Theory] + [InlineData("abc", "abc")] + [InlineData("a b c", "a b c")] + [InlineData("0\n1", "0\n1")] + [InlineData("日", @"\xe6\x97\xa5")] + [InlineData("本", @"\xe6\x9c\xac")] + [InlineData("日 本", @"\xe6\x97\xa5 \xe6\x9c\xac")] + [InlineData("日本", @"\xe6\x97\xa5\xe6\x9c\xac")] + [InlineData("☠ ®", @"\xE2\x98\xA0 \uae")] + [InlineData("日 ENV value 本", @"\xe6\x97\xa5 $ENVVAR_TEST 本")] + [InlineData("a\"b c", "a\\\"b c")] + [InlineData("a'b c", "a'b c")] + public void DoubleQuotedValueContentsShouldParseUntilEnd(string expected, string input) => + Assert.Equal(expected, Parsers.DoubleQuotedValueContents.AtEnd().Parse(input).Value); + + [Theory] + [InlineData("abc", "abc")] + [InlineData("a b c", "a b c")] + [InlineData("0\n1", "0\n1")] + [InlineData(@"\xe6\x97\xa5 \xe6\x9c\xac", @"\xe6\x97\xa5 \xe6\x9c\xac")] + [InlineData(@"\xE2\x98\xA0 \uae", @"\xE2\x98\xA0 \uae")] + [InlineData(@"\xe6\x97\xa5 $ENVVAR_TEST 本", @"\xe6\x97\xa5 $ENVVAR_TEST 本")] + [InlineData("a\"b c", "a\"b c")] + public void SingleQuotedValueContentsShouldParseUntilEnd(string expected, string input) => + Assert.Equal(expected, Parsers.SingleQuotedValueContents.AtEnd().Parse(input).Value); + + [Theory] + [InlineData("abc", "'abc'")] + [InlineData("a b c", "'a b c'")] + [InlineData("0\n1", "'0\n1'")] + [InlineData("a\"bc", "'a\"bc'")] + [InlineData(@"\xe6\x97\xa5 $ENVVAR_TEST 本", @"'\xe6\x97\xa5 $ENVVAR_TEST 本'")] + public void SingleQuotedValueShouldParseUntilEnd(string expected, string input) => + Assert.Equal(expected, Parsers.SingleQuotedValue.AtEnd().Parse(input).Value); + + [Theory] + [InlineData("'a\\'b c'")] + public void SingleQuotedValueShouldThrowOnParseUntilEnd(string invalidInput) => + Assert.Throws(() => Parsers.SingleQuotedValue.AtEnd().Parse(invalidInput).Value); + + [Theory] + [InlineData("abc", "\"abc\"")] + [InlineData("a b c", "\"a b c\"")] + [InlineData("0\n1", "\"0\n1\"")] + [InlineData("a'bc", "\"a'bc\"")] + [InlineData("a\"bc", "\"a\\\"bc\"")] + [InlineData("日 ENV value 本", "\"\\xe6\\x97\\xa5 $ENVVAR_TEST 本\"")] + public void DoubleQuotedValueShouldParseUntilEnd(string expected, string input) => + Assert.Equal(expected, Parsers.DoubleQuotedValue.AtEnd().Parse(input).Value); + + [Theory] + [InlineData("export", "export ")] + [InlineData("set -x", "set -x ")] + [InlineData("set", "set ")] + [InlineData("SET", "SET ")] + public void TestExportExpression(string expected, string input) => + Assert.Equal(expected, Parsers.ExportExpression.AtEnd().Parse(input)); + + [Theory] + [InlineData("identifier ")] + public void ExportExpressionShouldThrowOnParseUntilEnd(string invalidInput) => + Assert.Throws(() => Parsers.ExportExpression.AtEnd().Parse(invalidInput)); + + [Theory] + [InlineData("abc", "abc")] + [InlineData("abc", "'abc'")] + [InlineData("abc", "\"abc\"")] + [InlineData("a b c", "a b c")] + [InlineData("a b c", "'a b c'")] + [InlineData("a b c", "\"a b c\"")] + [InlineData("a#b#c", "a#b#c")] + [InlineData("041", "041")] + [InlineData("日本", "日本")] + [InlineData(@"\xe6\x97\xa5\xe6\x9c\xac", @"\xe6\x97\xa5\xe6\x9c\xac")] + [InlineData(@"\xe6\x97\xa5\xe6\x9c\xac", @"'\xe6\x97\xa5\xe6\x9c\xac'")] + [InlineData(@"\xe6\x97\xa5 \xe6\x9c\xac", @"'\xe6\x97\xa5 \xe6\x9c\xac'")] + [InlineData("日本", "\"\\xe6\\x97\\xa5\\xe6\\x9c\\xac\"")] + [InlineData("日 本", "\"\\xe6\\x97\\xa5 \\xe6\\x9c\\xac\"")] + [InlineData(@"\xe6\x97\xa5", @"\xe6\x97\xa5")] + [InlineData(@"\xE2\x98\xA0", @"\xE2\x98\xA0")] + [InlineData("0\n1", "'0\n1'")] + [InlineData("0\n1", "\"0\n1\"")] + [InlineData(@"\xE2\x98\xA0 \uae", @"'\xE2\x98\xA0 \uae'")] + [InlineData("☠ ®", "\"\\xE2\\x98\\xA0 \\uae\"")] + [InlineData("日 ENV value 本", "\"\\xe6\\x97\\xa5 $ENVVAR_TEST 本\"")] + public void ParseShouldParseUntilEnd(string expected, string input) => + Assert.Equal(expected, Parsers.Value.AtEnd().Parse(input).Value); + + [Theory] + [InlineData("0", "0\n1")] // value ends at linebreak + public void ParseShouldParse(string expected, string input) => + Assert.Equal(expected, Parsers.Value.Parse(input).Value); + + [Theory] + [InlineData("0\n1")] + public void ParseShouldThrowOnParseUntilEnd(string invalidInput) => + Assert.Throws(() => Parsers.Value.AtEnd().Parse(invalidInput)); + + [Theory] + [InlineData(" ")] + [InlineData("'")] // unmatched quote + [InlineData("\"")] // unmatched quote + public void ParseShouldThrowOnParse(string invalidInput) => + Assert.Throws(() => Parsers.Value.Parse(invalidInput)); + + [Theory] + [InlineData("EV_DNE", "abc", "EV_DNE=abc")] + [InlineData("EV_DNE", "a b c", "EV_DNE=a b c")] + [InlineData("EV_DNE", "a b c", "EV_DNE='a b c'")] + [InlineData("EV_DNE", "041", "EV_DNE=041 # comment")] + [InlineData("EV_DNE", "日本#noComment", "EV_DNE=日本#noComment")] + [InlineData("EV_DNE", @"\xe6\x97\xa5 \xe6\x9c\xac", @"EV_DNE=\xe6\x97\xa5 \xe6\x9c\xac")] + [InlineData("EV_DNE", @"\xE2\x98\xA0 \uae", @"EV_DNE=\xE2\x98\xA0 \uae")] + [InlineData("EV_DNE", "", "EV_DNE=")] + //[InlineData("EV_DNE", "日本", @"EV_DNE=\xe6\x97\xa5\xe6\x9c\xac")] // TODO: is it possible to get the system to recognize when a complete unicode char is present and start the next one then, without a space? + [InlineData("EV_DNE", "EV_DNE=", "EV_DNE=EV_DNE=")] + [InlineData("EV_DNE", "test", "EV_DNE= test #basic comment")] + [InlineData("EV_DNE", "", "EV_DNE=#no value just comment")] + [InlineData("EV_DNE", "", "EV_DNE= #no value just comment")] + [InlineData("EV_DNE", "a#b#c", "EV_DNE=a#b#c #inner hashes are allowed in unquoted value")] + [InlineData("EV_DNE", "test", "EV_DNE= test #a'bc allow singleQuotes in comment")] + [InlineData("EV_DNE", "test", "EV_DNE= test #a\"bc allow doubleQuotes in comment")] + [InlineData("EV_DNE", "test", "EV_DNE= test #a$bc allow dollarSign in comment")] + [InlineData("EV_DNE", "a#b#c# not a comment", "EV_DNE=a#b#c# not a comment")] + [InlineData("EV_DNE", "http://www.google.com/#anchor", "EV_DNE=http://www.google.com/#anchor #inner hash is part of value")] + [InlineData("EV_DNE", "abc", "EV_DNE='abc'")] + [InlineData("EV_DNE", "a b c", "EV_DNE='a b c' # comment")] + [InlineData("EV_DNE", "0\n1", "EV_DNE='0\n1'")] + [InlineData("EV_DNE", @"\xe6\x97\xa5 \xe6\x9c\xac", @"set -x EV_DNE='\xe6\x97\xa5 \xe6\x9c\xac'#c")] + [InlineData("EV_DNE", @"\xE2\x98\xA0 \uae", @"EV_DNE='\xE2\x98\xA0 \uae'#c")] + [InlineData("EV_DNE", "abc", "EV_DNE=\"abc\"")] + [InlineData("EV_DNE", "a b c", "set EV_DNE=\"a b c\" # comment")] + [InlineData("EV_DNE", "0\n1", "EV_DNE=\"0\n1\"")] + [InlineData("EV_DNE", "日 本", "export EV_DNE=\"\\xe6\\x97\\xa5 \\xe6\\x9c\\xac\"#c")] + [InlineData("EV_DNE", "☠ ®", "EV_DNE=\"\\xE2\\x98\\xA0 \\uae\"")] + [InlineData("EV_DNE", "日 ENV value 本", "export EV_DNE=\"\\xe6\\x97\\xa5 $ENVVAR_TEST 本\"#ccccc")] + [InlineData("exportEV_DNE", "abc", "exportEV_DNE=\"abc\"")] + [InlineData("EV_DNE", "a b c", "EV_DNE = 'a b c' # comment")] + [InlineData("EV_DNE", "a b c", "EV_DNE= \"a b c\" # comment")] + [InlineData("EV_DNE", "a b c", "EV_DNE ='a b c' # comment")] + [InlineData("EV_DNE", "abc", "EV_DNE = abc # comment")] + [InlineData("EV_DNE", "a'b'' 'c' d", "EV_DNE=\"a'b'' 'c' d\" #allow singleQuotes in doubleQuoted values")] + [InlineData("EV_DNE", "a\"b\"\" \"c\" d", "EV_DNE='a\"b\"\" \"c\" d' #allow doubleQuotes in singleQuoted values")] + [InlineData("EV_DNE", "a\"b\"\" \"c\" d", "EV_DNE=\"a\\\"b\\\"\\\" \\\"c\\\" d\" #allow escaped doubleQuotes in doubleQuoted values")] + [InlineData("EV_DNE", "VAL UE", "EV_DNE=VAL UE")] + [InlineData("EV_DNE", "VAL UE", "EV_DNE=VAL UE #comment")] + public void AssignmentShouldParseUntilEnd(string key, string value, string input) { - Action testParse = (key, value, input) => - { - var kvp = Parsers.Assignment.AtEnd().Parse(input); - Assert.Equal(key, kvp.Key); - Assert.Equal(value, kvp.Value); - }; - - testParse("EV_DNE", "abc", "EV_DNE=abc"); - testParse("EV_DNE", "a b c", "EV_DNE=a b c"); - testParse("EV_DNE", "a b c", "EV_DNE='a b c'"); - testParse("EV_DNE", "041", "EV_DNE=041 # comment"); - // Note that there are no comments without whitespace in unquoted strings! - testParse("EV_DNE", "日本#c", "EV_DNE=日本#c"); - - testParse("EV_DNE", @"\xe6\x97\xa5 \xe6\x9c\xac", @"EV_DNE=\xe6\x97\xa5 \xe6\x9c\xac"); - testParse("EV_DNE", @"\xE2\x98\xA0 \uae", @"EV_DNE=\xE2\x98\xA0 \uae"); - - var kvp = Parsers.Assignment.AtEnd().Parse("EV_DNE="); - Assert.Equal("EV_DNE", kvp.Key); - Assert.Equal("", kvp.Value); - // Note that dotnet returns null if the env var is empty -- even if it was set to empty! - Assert.Null(Environment.GetEnvironmentVariable("EV_DNE")); - - // TODO: is it possible to get the system to recognize when a complete unicode char is present and start the next one then, without a space? -// Assert.Equal("EV_DNE=日本", Parsers.Assignment.AtEnd().Parse(@"EV_DNE=\xe6\x97\xa5\xe6\x9c\xac")); - - Assert.Throws(() => Parsers.Assignment.AtEnd().Parse("EV_DNE='")); - Assert.Throws(() => Parsers.Assignment.AtEnd().Parse("EV_DNE=0\n1")); - - testParse("EV_DNE", "", "EV_DNE="); - testParse("EV_DNE", "EV_DNE=", "EV_DNE=EV_DNE="); - - testParse("EV_DNE", "test", "EV_DNE= test #basic comment"); - testParse("EV_DNE", "", "EV_DNE=#no value just comment"); - testParse("EV_DNE", "", "EV_DNE= #no value just comment"); - testParse("EV_DNE", "a#b#c", "EV_DNE=a#b#c #inner hashes are allowed in unquoted value"); - testParse("EV_DNE", "test", "EV_DNE= test #a'bc allow singleQuotes in comment"); - testParse("EV_DNE", "test", "EV_DNE= test #a\"bc allow doubleQuotes in comment"); - testParse("EV_DNE", "test", "EV_DNE= test #a$bc allow dollarSign in comment"); - testParse("EV_DNE", "a#b#c# not a comment", "EV_DNE=a#b#c# not a comment"); - - testParse("EV_DNE", "http://www.google.com/#anchor", "EV_DNE=http://www.google.com/#anchor #inner hash is part of value"); - - testParse("EV_DNE", "abc", "EV_DNE='abc'"); - testParse("EV_DNE", "a b c", "EV_DNE='a b c' # comment"); - testParse("EV_DNE", "0\n1", "EV_DNE='0\n1'"); - testParse("EV_DNE", @"\xe6\x97\xa5 \xe6\x9c\xac", @"set -x EV_DNE='\xe6\x97\xa5 \xe6\x9c\xac'#c"); - testParse("EV_DNE", @"\xE2\x98\xA0 \uae", @"EV_DNE='\xE2\x98\xA0 \uae'#c"); - - testParse("EV_DNE", "abc", "EV_DNE=\"abc\""); - testParse("EV_DNE", "a b c", "set EV_DNE=\"a b c\" # comment"); - testParse("EV_DNE", "0\n1", "EV_DNE=\"0\n1\""); - testParse("EV_DNE", "日 本", "export EV_DNE=\"\\xe6\\x97\\xa5 \\xe6\\x9c\\xac\"#c"); - testParse("EV_DNE", "☠ ®", "EV_DNE=\"\\xE2\\x98\\xA0 \\uae\""); - - testParse("EV_DNE", "日 ENV value 本", "export EV_DNE=\"\\xe6\\x97\\xa5 $ENVVAR_TEST 本\"#ccccc"); - - testParse("exportEV_DNE", "abc", "exportEV_DNE=\"abc\""); - - testParse("EV_DNE", "a b c", "EV_DNE = 'a b c' # comment"); - testParse("EV_DNE", "a b c", "EV_DNE= \"a b c\" # comment"); - testParse("EV_DNE", "a b c", "EV_DNE ='a b c' # comment"); - testParse("EV_DNE", "abc", "EV_DNE = abc # comment"); - - testParse("EV_DNE", "a'b'' 'c' d", "EV_DNE=\"a'b'' 'c' d\" #allow singleQuotes in doubleQuoted values"); - testParse("EV_DNE", "a\"b\"\" \"c\" d", "EV_DNE='a\"b\"\" \"c\" d' #allow doubleQuotes in singleQuoted values"); - testParse("EV_DNE", "a\"b\"\" \"c\" d", "EV_DNE=\"a\\\"b\\\"\\\" \\\"c\\\" d\" #allow escaped doubleQuotes in doubleQuoted values"); - Assert.Throws(() => Parsers.Assignment.Parse("EV_DNE='a'b'' 'c' d'")); // no singleQuotes inside singleQuoted values - Assert.Throws(() => Parsers.Assignment.Parse("EV_DNE=\"a\"b\"")); // no unescaped doubleQuotes inside doubleQuoted values - - testParse("EV_DNE", "VAL UE", "EV_DNE=VAL UE"); - testParse("EV_DNE", "VAL UE", "EV_DNE=VAL UE #comment"); - - Assert.Throws(() => Parsers.Assignment.AtEnd().Parse("EV_DNE='a b c'EV_TEST_1=more")); - Assert.Throws(() => Parsers.Assignment.AtEnd().Parse("EV_DNE='a b c' EV_TEST_1=more")); + var expected = new KeyValuePair(key, value); + Assert.Equal(expected, Parsers.Assignment.AtEnd().Parse(input)); } - [Fact] - public void ParseDotenvFile () - { - Action[], string> testParse = (expecteds, input) => + [Theory] + [InlineData("EV_DNE='")] + [InlineData("EV_DNE=0\n1")] + [InlineData("EV_DNE='a b c'EV_TEST_1=more")] + [InlineData("EV_DNE='a b c' EV_TEST_1=more")] + public void AssignmentShouldThrowOnParseUntilEnd(string invalidInput) => + Assert.Throws(() => Parsers.Assignment.AtEnd().Parse(invalidInput)); + + [Theory] + [InlineData("EV_DNE='a'b'' 'c' d'")] // no singleQuotes inside singleQuoted values + [InlineData("EV_DNE=\"a\"b\"")] // no unescaped doubleQuotes inside doubleQuoted values + public void AssignmentShouldThrowOnParse(string invalidInput) => + Assert.Throws(() => Parsers.Assignment.Parse(invalidInput)); + + /// + /// Data: _, contents, expectedPairs + /// + public static readonly TheoryData[]> ParseDotEnvTests = + new IndexedTheoryData[]>() { - var outputs = Parsers.ParseDotenvFile(input, Parsers.SetEnvVar).ToArray(); - Assert.Equal(expecteds.Length, outputs.Length); - - for (var i = 0; i < outputs.Length; i++) + { "", Array.Empty>() }, + { "EV_DNE=abc", new[] { new KeyValuePair("EV_DNE", "abc") } }, + { "SET EV_DNE=\"0\n1\"", new[] { new KeyValuePair("EV_DNE", "0\n1") } }, { - Assert.Equal(expecteds[i].Key, outputs[i].Key); - Assert.Equal(expecteds[i].Value, outputs[i].Value); - Assert.Equal(expecteds[i].Value, Environment.GetEnvironmentVariable(outputs[i].Key)); - } - }; - - string contents; - KeyValuePair[] expecteds; - - contents = @""; - expecteds = new KeyValuePair[] {}; - testParse(expecteds, contents); - - contents = @"EV_DNE=abc"; - expecteds = new[] { - new KeyValuePair("EV_DNE", "abc"), - }; - testParse(expecteds, contents); - - contents = "SET EV_DNE=\"0\n1\""; - expecteds = new[] { - new KeyValuePair("EV_DNE", "0\n1"), - }; - testParse(expecteds, contents); - - contents = "EV_DNE=0\n1"; - Assert.Throws(() => Parsers.ParseDotenvFile(contents, Parsers.SetEnvVar)); - - contents = @" + @" # this is a header export EV_DNE='a b c' #works! -"; - expecteds = new[] { - new KeyValuePair("EV_DNE", "a b c"), - }; - testParse(expecteds, contents); - - contents = "# this is a header\nexport EV_DNE='d e f' #works!"; - expecteds = new[] { - new KeyValuePair("EV_DNE", "d e f"), - }; - testParse(expecteds, contents); - - contents = "#\n# this is a header\n#\n\nexport EV_DNE='g h i' #yep still\n"; - expecteds = new[] { - new KeyValuePair("EV_DNE", "g h i"), - }; - testParse(expecteds, contents); - - contents = "#\n# this is a header\n#\n\nexport EV_DNE=\"\\xe6\\x97\\xa5 $ENVVAR_TEST 本\" #yep still\n"; - expecteds = new[] { - new KeyValuePair("EV_DNE", "日 ENV value 本"), - }; - testParse(expecteds, contents); - - contents = @"# +", + new[] { new KeyValuePair("EV_DNE", "a b c") } + }, + { + "# this is a header\nexport EV_DNE='d e f' #works!", + new[] { new KeyValuePair("EV_DNE", "d e f") } + }, + { + "#\n# this is a header\n#\n\nexport EV_DNE='g h i' #yep still\n", + new[] { new KeyValuePair("EV_DNE", "g h i") } + }, + { + "#\n# this is a header\n#\n\nexport EV_DNE=\"\\xe6\\x97\\xa5 $ENVVAR_TEST 本\" #yep still\n", + new[] { new KeyValuePair("EV_DNE", "日 ENV value 本") } + }, + { + @"# # this is a header # @@ -532,14 +465,36 @@ public void ParseDotenvFile () ®'#c ENVVAR_TEST = ' yahooooo ' -"; - expecteds = new[] { - new KeyValuePair("EV_DNE", "x y z"), - new KeyValuePair("EV_TEST_1", "日 $ENVVAR_TEST 本"), - new KeyValuePair("EV_TEST_2", "☠\n®"), - new KeyValuePair("ENVVAR_TEST", " yahooooo "), +", + new[] + { + new KeyValuePair("EV_DNE", "x y z"), + new KeyValuePair("EV_TEST_1", "日 $ENVVAR_TEST 本"), + new KeyValuePair("EV_TEST_2", "☠\n®"), + new KeyValuePair("ENVVAR_TEST", " yahooooo "), + } + }, }; - testParse(expecteds, contents); + + [Theory] + [MemberData(nameof(ParseDotEnvTests))] + public void ParseDotenvFileShouldParseContents(string _, string contents, KeyValuePair[] expectedPairs) + { + var outputs = Parsers.ParseDotenvFile(contents, Parsers.SetEnvVar).ToArray(); + Assert.Equal(expectedPairs.Length, outputs.Length); + + foreach (var (output, expected) in outputs.Zip(expectedPairs)) + { + Assert.Equal(expected, output); + Assert.Equal(expected.Value, Environment.GetEnvironmentVariable(output.Key)); + } } + + [Theory] + [InlineData("EV_DNE=0\n1")] + public void ParseDotenvFileShouldThrowOnContents(string invalidContents) => + Assert.Throws(() => Parsers.ParseDotenvFile(invalidContents, Parsers.SetEnvVar)); + + // C# wow that you can't handle 32 bit unicode as chars. wow. strings for 4 byte chars. } } diff --git a/test/DotNetEnv.Tests/XUnit/IndexedTheoryData.cs b/test/DotNetEnv.Tests/XUnit/IndexedTheoryData.cs new file mode 100644 index 0000000..91312c6 --- /dev/null +++ b/test/DotNetEnv.Tests/XUnit/IndexedTheoryData.cs @@ -0,0 +1,40 @@ +using System.Runtime.CompilerServices; +using Xunit; + +namespace DotNetEnv.Tests.XUnit; + +public class IndexedTheoryData : TheoryData +{ + private int _index = 0; + public void Add(T p, [CallerMemberName]string callerMemberName = null, [CallerLineNumber]int callerLineNumber = 0) + { + AddRow($"{_index++,4}; {callerMemberName}:{callerLineNumber}", p); + } +} + +public class IndexedTheoryData : TheoryData +{ + private int _index = 0; + public void Add(T1 p1, T2 p2, [CallerMemberName]string callerMemberName = null, [CallerLineNumber]int callerLineNumber = 0) + { + AddRow($"{_index++,4}; {callerMemberName}:{callerLineNumber}", p1, p2); + } +} + +public class IndexedTheoryData : TheoryData +{ + private int _index = 0; + public void Add(T1 p1, T2 p2, T3 p3, [CallerMemberName]string callerMemberName = null, [CallerLineNumber]int callerLineNumber = 0) + { + AddRow($"{_index++,4}; {callerMemberName}:{callerLineNumber}", p1, p2, p3); + } +} + +public class IndexedTheoryData : TheoryData +{ + private int _index = 0; + public void Add(T1 p1, T2 p2, T3 p3, T4 p4, [CallerMemberName]string callerMemberName = null, [CallerLineNumber]int callerLineNumber = 0) + { + AddRow($"{_index++,4}; {callerMemberName}:{callerLineNumber}", p1, p2, p3, p4); + } +}