diff --git a/.editorconfig b/.editorconfig index e2f359511..dba8b41c4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -48,6 +48,12 @@ trim_trailing_whitespace = false [*.sh] end_of_line = lf +########################### +# Diagnsotic customizations +########################### +[*.{cs,csx,cake}] +dotnet_diagnostic.RS0030.severity = error + ########################### # .NET Language Conventions # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#language-conventions diff --git a/AudioAnalysis.sln b/AudioAnalysis.sln index c6fe92d6e..8a480992d 100644 --- a/AudioAnalysis.sln +++ b/AudioAnalysis.sln @@ -12,6 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution src\AssemblyMetadata.cs.template = src\AssemblyMetadata.cs.template src\AssemblyMetadata.Generated.targets = src\AssemblyMetadata.Generated.targets azure-pipelines.yml = azure-pipelines.yml + BannedSymbols.txt = BannedSymbols.txt Directory.Build.props = Directory.Build.props build\download_ap.ps1 = build\download_ap.ps1 src\git_version.ps1 = src\git_version.ps1 diff --git a/BannedSymbols.txt b/BannedSymbols.txt new file mode 100644 index 000000000..887ef1db9 --- /dev/null +++ b/BannedSymbols.txt @@ -0,0 +1,2 @@ +T:CsvHelper.CsvReader; You must not use CsvReader. Use The Acoustics.Shared.Csv.Read methods which properly construct the reader. +T:CsvHelper.CsvWriter; You must not use CsvWriter. Use The Acoustics.Shared.Csv.Write methods which properly construct the writer. diff --git a/Directory.Build.props b/Directory.Build.props index d533434bc..42ffd3572 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ true - $(MSBuildThisFileDirectory)style.ruleset + $(MSBuildThisFileDirectory)\style.ruleset win-x64;win-arm64;linux-x64;linux-musl-x64;linux-arm;linux-arm64;osx-x64 @@ -48,5 +48,17 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + \ No newline at end of file diff --git a/src/Acoustics.Shared/Acoustics.Shared.csproj b/src/Acoustics.Shared/Acoustics.Shared.csproj index b74acca5b..7dc8faf46 100644 --- a/src/Acoustics.Shared/Acoustics.Shared.csproj +++ b/src/Acoustics.Shared/Acoustics.Shared.csproj @@ -1,4 +1,4 @@ - + Acoustics.Shared Library @@ -14,7 +14,7 @@ - + diff --git a/src/Acoustics.Shared/Collections/IInterval2{TX,TY}.cs b/src/Acoustics.Shared/Collections/IInterval2{TX,TY}.cs index 7b5de8b88..d0621a9de 100644 --- a/src/Acoustics.Shared/Collections/IInterval2{TX,TY}.cs +++ b/src/Acoustics.Shared/Collections/IInterval2{TX,TY}.cs @@ -9,8 +9,8 @@ namespace Acoustics.Shared.ImageSharp using System.Collections.Generic; public interface IInterval2 - where TX : struct, IComparable - where TY : struct, IComparable + where TX : struct, IComparable, IFormattable + where TY : struct, IComparable, IFormattable { Interval X { get; } diff --git a/src/Acoustics.Shared/Csv/Csv.cs b/src/Acoustics.Shared/Csv/Csv.cs index 02915b00f..8d5abeac9 100644 --- a/src/Acoustics.Shared/Csv/Csv.cs +++ b/src/Acoustics.Shared/Csv/Csv.cs @@ -21,63 +21,34 @@ namespace Acoustics.Shared.Csv using CsvHelper.Configuration; using CsvHelper.TypeConversion; using log4net; + using ObjectCloner.Extensions; using SixLabors.ImageSharp; /// /// Generic methods for reading and writing Csv file. - /// . /// *** DO NOT CHANGE THIS CLASS UNLESS INSTRUCTED TOO ***. /// public static class Csv { private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); -#pragma warning disable IDE0032 // Use auto property - private static readonly CsvConfiguration InternalConfig = - new CsvConfiguration(CultureInfo.InvariantCulture) - { - // change the defaults here if you want - HasHeaderRecord = true, - - // acoustic workbench used to output faulty data with padded headers - PrepareHeaderForMatch = (x, i) => x.Trim(), - - // ensure we always use InvariantCulture - only reliable way to serialize data - // Additionally R can parse invariant representations of Double.Infinity and - // Double.NaN (whereas it can't in other cultures). - CultureInfo = CultureInfo.InvariantCulture, - }; -#pragma warning restore IDE0032 // Use auto property - - static Csv() + private static readonly Lazy> MapsInAssembly = new(() => { - Initialize(); - } - - private static void Initialize() + // initialize and store + return Meta.GetTypesFromQutAssemblies().Select(ti => Activator.CreateInstance(ti)).Cast(); + }); + + // ensure we always use InvariantCulture - only reliable way to serialize data + // Additionally R can parse invariant representations of Double.Infinity and + // Double.NaN (whereas it can't in other cultures). + public static CsvConfiguration DefaultConfiguration { get; } = new CsvConfiguration(CultureInfo.InvariantCulture) { - // Registers CsvHelper type converters that can allow serialization of complex types. - InternalConfig.TypeConverterCache.AddConverter>(new CsvSetPointConverter()); + // change the defaults here if you want + HasHeaderRecord = true, - // ensure dates are always formatted as ISO8601 dates - note: R cannot by default parse proper ISO8601 dates - var typeConverterOptions = new TypeConverterOptions() - { - DateTimeStyle = DateTimeStyles.RoundtripKind, - Formats = "o".AsArray(), - }; - InternalConfig.TypeConverterOptionsCache.AddOptions(typeConverterOptions); - InternalConfig.TypeConverterOptionsCache.AddOptions(typeConverterOptions); - - // Find all of our custom class maps - foreach (var classMapType in Meta.GetTypesFromQutAssemblies()) - { - // initialize and store - var instance = Activator.CreateInstance(classMapType) as ClassMap; - InternalConfig.RegisterClassMap(instance); - } - } - - public static CsvConfiguration DefaultConfiguration => InternalConfig; + // acoustic workbench used to output faulty data with padded headers + PrepareHeaderForMatch = x => x.Header.Trim(), + }; /// /// Serialize results to CSV - if you want the concrete type to be serialized you need to ensure @@ -91,11 +62,24 @@ public static void WriteToCsv(FileInfo destination, IEnumerable results) Contract.Requires(destination != null); // using CSV Helper - using (var stream = destination.CreateText()) - { - var writer = new CsvWriter(stream, DefaultConfiguration); - writer.WriteRecords(results); - } + using var stream = destination.CreateText(); + WriteToCsv(stream, results); + } + + /// + /// Serialize results to CSV - if you want the concrete type to be serialized you need to ensure + /// it is downcast before using this method. + /// + /// The type to serialize. + /// The data to serialize. + public static void WriteToCsv(TextWriter stream, IEnumerable results) + { + Contract.Requires(stream != null); + + var writer = GetWriter(stream); + + writer.WriteRecords(results); + writer.Flush(); } /// @@ -134,14 +118,130 @@ public static IEnumerable ReadFromCsv( } } + public static void WriteMatrixToCsv(FileInfo destination, T[,] matrix, TwoDimensionalArray dimensionality = TwoDimensionalArray.None) + { + Contract.Requires(destination != null); + + // not tested! + using (var stream = destination.CreateText()) + { + var writer = GetWriter(stream); + + var transformedMatrix = new TwoDimArrayMapper(matrix, dimensionality); + + EncodeMatrixInner(writer, transformedMatrix, true); + } + } + + public static T[,] ReadMatrixFromCsv(FileInfo source, TwoDimensionalArray transform = TwoDimensionalArray.None) + { + Contract.Requires(source != null); + + using (var stream = source.OpenText()) + { + var reader = GetReader(stream); + + return reader.DecodeMatrix(transform, true); + } + } + + public static void WriteMatrixToCsv(FileInfo destination, IEnumerable matrix) + { + Contract.Requires(destination != null); + + // not tested! + using (var stream = destination.CreateText()) + { + var writer = GetWriter(stream); + + var transformedMatrix = new EnumerableMapper(matrix); + + EncodeMatrixInner(writer, transformedMatrix, true); + } + } + + public static IEnumerable ReadMatrixFromCsvAsEnumerable(FileInfo source) + { + Contract.Requires(source != null); + + // not tested! + List matrix; + using (var stream = new StreamReader(source.FullName)) + { + var reader = GetReader(stream); + + matrix = reader.DecodeMatrix(true, out var rowCount, out var columnCount); + } + + return matrix; + } + + public static void WriteMatrixToCsv(FileInfo destination, IEnumerable matrix, Func selector) + { + Contract.Requires(destination != null); + + using (var stream = destination.CreateText()) + { + var writer = GetWriter(stream); + + var transformedMatrix = new ObjectArrayMapper(matrix, selector); + + EncodeMatrixInner(writer, transformedMatrix, true); + } + } + + internal static CsvReader GetReader(TextReader stream, Action modifyConfig = null) + { + var config = DefaultConfiguration; + if (modifyConfig is not null) + { + config = config.DeepClone(); + modifyConfig(config); + } + + var reader = new CsvReader(stream, config); + ApplyConverters(reader.Context); + return reader; + } + + internal static CsvWriter GetWriter(TextWriter stream) + { + var writer = new CsvWriter(stream, DefaultConfiguration); + ApplyConverters(writer.Context); + return writer; + } + + private static void ApplyConverters(CsvContext context) + { + // Registers CsvHelper type converters that can allow serialization of complex types. + context.TypeConverterCache.AddConverter>(new CsvSetPointConverter()); + context.TypeConverterCache.AddConverter(typeof(Interval), new CsvIntervalConverter()); + + // ensure dates are always formatted as ISO8601 dates - note: R cannot by default parse proper ISO8601 dates + var typeConverterOptions = new TypeConverterOptions() + { + DateTimeStyle = DateTimeStyles.RoundtripKind, + Formats = "o".AsArray(), + }; + context.TypeConverterOptionsCache.AddOptions(typeConverterOptions); + context.TypeConverterOptionsCache.AddOptions(typeConverterOptions); + + // Find all of our custom class maps + foreach (var classMap in MapsInAssembly.Value) + { + context.RegisterClassMap(classMap); + } + } + private static IEnumerable ReadFromCsv(Action readerHook, TextReader stream, bool throwOnMissingField = true) { try { - var configuration = DefaultConfiguration; - configuration.MissingFieldFound = throwOnMissingField ? ConfigurationFunctions.MissingFieldFound : (Action)null; - configuration.HeaderValidated = throwOnMissingField ? ConfigurationFunctions.HeaderValidated : (Action)null; - var reader = new CsvReader(stream, configuration); + var reader = GetReader(stream, (configuration) => + { + configuration.MissingFieldFound = throwOnMissingField ? ConfigurationFunctions.MissingFieldFound : (_) => { }; + configuration.HeaderValidated = throwOnMissingField ? ConfigurationFunctions.HeaderValidated : (_) => { }; + }); IEnumerable results = reader.GetRecords(); @@ -156,13 +256,14 @@ private static IEnumerable ReadFromCsv(Action readerHook, TextR Log.Debug($"Error doing type conversion - dictionary contains {tce.Data.Count} entries. The error was: `{tce.Message}`"); // The CsvHelper exception messages are unhelpful... let us fix this - if (tce.ReadingContext != null) + if (tce.Context != null) { var parserData = $@" -Row: {tce.ReadingContext.Row} -Column: {tce.ReadingContext.CurrentIndex} -Field Name: {tce.ReadingContext.Field} +Row: {tce.Context.Parser.Row} +Column: {tce.Context.Reader.CurrentIndex} +Column Name: {tce.Context.Reader.HeaderRecord[tce.Context.Reader.CurrentIndex]} Member Name: {tce.MemberMapData.Member.Name} +Member Value: {tce.Text} "; var newMessage = tce.Message + Environment.NewLine + parserData; @@ -188,7 +289,6 @@ private static void EncodeMatrixInner(this CsvWriter writer, MatrixMapper writer.WriteField("c" + i.ToString("000000")); } - writer.Context.HasHeaderBeenWritten = true; writer.NextRecord(); // write rows @@ -204,8 +304,7 @@ private static void EncodeMatrixInner(this CsvWriter writer, MatrixMapper } } - private static List DecodeMatrix(this CsvReader reader, bool includeRowIndex, out int rowCount, - out int columnCount) + private static List DecodeMatrix(this CsvReader reader, bool includeRowIndex, out int rowCount, out int columnCount) { // read header if (!reader.Read()) @@ -220,7 +319,7 @@ private static List DecodeMatrix(this CsvReader reader, bool includeRowI throw new CsvHelperException(reader.Context, "Could not read headers"); } - var headers = reader.Context.HeaderRecord; + var headers = reader.Context.Reader.HeaderRecord; if (includeRowIndex && headers[0] != "Index") { throw new CsvHelperException(reader.Context, "Expected an index header and there was none"); @@ -254,8 +353,7 @@ private static List DecodeMatrix(this CsvReader reader, bool includeRowI return csvRows; } - private static T[,] DecodeMatrix(this CsvReader reader, TwoDimensionalArray dimensionality, - bool includeRowIndex) + private static T[,] DecodeMatrix(this CsvReader reader, TwoDimensionalArray dimensionality, bool includeRowIndex) { var csvRows = DecodeMatrix(reader, includeRowIndex, out var rowCount, out var columnCount); @@ -295,77 +393,5 @@ private static List DecodeMatrix(this CsvReader reader, bool includeRowI return result; } - - public static void WriteMatrixToCsv(FileInfo destination, T[,] matrix, TwoDimensionalArray dimensionality = TwoDimensionalArray.None) - { - Contract.Requires(destination != null); - - // not tested! - using (var stream = destination.CreateText()) - { - var writer = new CsvWriter(stream, DefaultConfiguration); - - var transformedMatrix = new TwoDimArrayMapper(matrix, dimensionality); - - EncodeMatrixInner(writer, transformedMatrix, true); - } - } - - public static T[,] ReadMatrixFromCsv(FileInfo source, TwoDimensionalArray transform = TwoDimensionalArray.None) - { - Contract.Requires(source != null); - - using (var stream = source.OpenText()) - { - var reader = new CsvReader(stream, DefaultConfiguration); - - return reader.DecodeMatrix(transform, true); - } - } - - public static void WriteMatrixToCsv(FileInfo destination, IEnumerable matrix) - { - Contract.Requires(destination != null); - - // not tested! - using (var stream = destination.CreateText()) - { - var writer = new CsvWriter(stream, DefaultConfiguration); - - var transformedMatrix = new EnumerableMapper(matrix); - - EncodeMatrixInner(writer, transformedMatrix, true); - } - } - - public static IEnumerable ReadMatrixFromCsvAsEnumerable(FileInfo source) - { - Contract.Requires(source != null); - - // not tested! - List matrix; - using (var stream = new StreamReader(source.FullName)) - { - var reader = new CsvReader(stream, DefaultConfiguration); - - matrix = reader.DecodeMatrix(true, out var rowCount, out var columnCount); - } - - return matrix; - } - - public static void WriteMatrixToCsv(FileInfo destination, IEnumerable matrix, Func selector) - { - Contract.Requires(destination != null); - - using (var stream = destination.CreateText()) - { - var writer = new CsvWriter(stream, DefaultConfiguration); - - var transformedMatrix = new ObjectArrayMapper(matrix, selector); - - EncodeMatrixInner(writer, transformedMatrix, true); - } - } } } \ No newline at end of file diff --git a/src/Acoustics.Shared/Csv/CsvIntervalConverter.cs b/src/Acoustics.Shared/Csv/CsvIntervalConverter.cs new file mode 100644 index 000000000..27f78452d --- /dev/null +++ b/src/Acoustics.Shared/Csv/CsvIntervalConverter.cs @@ -0,0 +1,36 @@ +// +// All code in this file and all associated files are the copyright and property of the QUT Ecoacoustics Research Group (formerly MQUTeR, and formerly QUT Bioacoustics Research Group). +// + +namespace Acoustics.Shared.Csv +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using Acoustics.Shared.Contracts; + using CsvHelper; + using CsvHelper.Configuration; + using CsvHelper.TypeConversion; + + // reference implementation: https://github.com/JoshClose/CsvHelper/blob/3b14b70fd1e9ce742375fbb116799b19fd0e7ccd/src/CsvHelper/TypeConversion/DoubleConverter.cs + public class CsvIntervalConverter : ITypeConverter + { + public object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData) + { + throw new NotImplementedException(); + } + + public string ConvertToString(object value, IWriterRow row, MemberMapData memberMapData) + { + if (value is Interval d) + { + var doubleOptions = row?.Context?.TypeConverterOptionsCache?.GetOptions(); + return d.ToString(suppressName: true, doubleOptions?.Formats?.FirstOrDefault(), doubleOptions?.CultureInfo); + } + + throw new InvalidOperationException("Cannot convert interval that is not have the generic type double"); + } + } +} diff --git a/src/Acoustics.Shared/Csv/ISetPointConverter.cs b/src/Acoustics.Shared/Csv/CsvSetPointConverter.cs similarity index 97% rename from src/Acoustics.Shared/Csv/ISetPointConverter.cs rename to src/Acoustics.Shared/Csv/CsvSetPointConverter.cs index 59a0bed40..4eed3ba12 100644 --- a/src/Acoustics.Shared/Csv/ISetPointConverter.cs +++ b/src/Acoustics.Shared/Csv/CsvSetPointConverter.cs @@ -1,5 +1,5 @@ // -------------------------------------------------------------------------------------------------------------------- -// +// // All code in this file and all associated files are the copyright and property of the QUT Ecoacoustics Research Group (formerly MQUTeR, and formerly QUT Bioacoustics Research Group). // // diff --git a/src/Acoustics.Shared/Extensions/IntervalExtensions.cs b/src/Acoustics.Shared/Extensions/IntervalExtensions.cs index b2a7cea80..ffa60eec6 100644 --- a/src/Acoustics.Shared/Extensions/IntervalExtensions.cs +++ b/src/Acoustics.Shared/Extensions/IntervalExtensions.cs @@ -227,19 +227,19 @@ public static double ClampvValue(this Interval range, double value) } public static Interval AsInterval(this (T Minimum, T Maximum) pair, Topology topology = Topology.Default) - where T : struct, IComparable + where T : struct, IComparable, IFormattable { return new Interval(pair.Minimum, pair.Maximum, topology); } public static Interval AsIntervalTo(this T minimum, T maximum, Topology topology = Topology.Default) - where T : struct, IComparable + where T : struct, IComparable, IFormattable { return new Interval(minimum, maximum, topology); } public static Interval AsIntervalFromZero(this T maximum, Topology topology = Topology.Default) - where T : struct, IComparable + where T : struct, IComparable, IFormattable { return new Interval(default, maximum, topology); } diff --git a/src/Acoustics.Shared/Extensions/ObjectExtensions.cs b/src/Acoustics.Shared/Extensions/ObjectExtensions.cs index 9b1e02fe7..e678ae23c 100644 --- a/src/Acoustics.Shared/Extensions/ObjectExtensions.cs +++ b/src/Acoustics.Shared/Extensions/ObjectExtensions.cs @@ -29,5 +29,10 @@ public static List AsList(this T item) { return new List { item }; } + + public static IEnumerable Wrap(this T item) + { + yield return item; + } } } \ No newline at end of file diff --git a/src/Acoustics.Shared/Interval.cs b/src/Acoustics.Shared/Interval.cs index 3ea86442c..9a7be558d 100644 --- a/src/Acoustics.Shared/Interval.cs +++ b/src/Acoustics.Shared/Interval.cs @@ -11,6 +11,7 @@ namespace Acoustics.Shared { using System; using System.Diagnostics.CodeAnalysis; + using System.Globalization; public enum Topology : byte { @@ -44,7 +45,7 @@ public enum Topology : byte /// The type used to represent the points in this interval. /// public readonly struct Interval : IEquatable>, IComparable> - where T : struct, IComparable + where T : struct, IComparable, IFormattable { public Interval(T minimum, T maximum) { @@ -266,12 +267,15 @@ public override string ToString() /// /// String representation. /// - public string ToString(bool suppressName) + public string ToString(bool suppressName, string tFormat = "R", CultureInfo culture = null) { + culture = culture ?? CultureInfo.InvariantCulture; var left = this.IsMinimumInclusive ? "[" : "("; var right = this.IsMaximumInclusive ? "]" : ")"; + var min = this.Minimum.ToString(tFormat, culture); + var max = this.Maximum.ToString(tFormat, culture); var name = suppressName ? string.Empty : nameof(Interval) + ": "; - return $"{name}{left}{this.Minimum}, {this.Maximum}{right}"; + return $"{name}{left}{min}, {max}{right}"; } public int CompareTo(Interval other) diff --git a/src/TowseyLibrary/ImageTools.cs b/src/TowseyLibrary/ImageTools.cs index f9828d83b..bbcaf8f74 100644 --- a/src/TowseyLibrary/ImageTools.cs +++ b/src/TowseyLibrary/ImageTools.cs @@ -3799,8 +3799,8 @@ public static Image CombineImagesInLine(params Image[] images) public static Image CombineImagesInLine(Color fill, params Image[] images) where T : unmanaged, IPixel { - var maxHeight = images.Max(i => i.Height); - var totalWidth = images.Sum(i => i.Width); + var maxHeight = images.Max(i => i?.Height ?? 0); + var totalWidth = images.Sum(i => i?.Width ?? 0); var composite = Drawing.NewImage(totalWidth, maxHeight, fill); int xOffset = 0; diff --git a/tests/Acoustics.Test/AnalysisPrograms/Recognizers/AustBitternTests.cs b/tests/Acoustics.Test/AnalysisPrograms/Recognizers/AustBitternTests.cs index ad43a49b9..7d6079f70 100644 --- a/tests/Acoustics.Test/AnalysisPrograms/Recognizers/AustBitternTests.cs +++ b/tests/Acoustics.Test/AnalysisPrograms/Recognizers/AustBitternTests.cs @@ -33,6 +33,7 @@ public class AustBitternTests : OutputDirectoryTest // as in the TestRecognizer() method below. [TestMethod] + [Ignore("Recogniser removed temporarily")] public void TestRecognizer() { var config = Recognizer.ParseConfig(ConfigFile); diff --git a/tests/Acoustics.Test/Shared/CsvTests.cs b/tests/Acoustics.Test/Shared/CsvTests.cs index 4126de228..7f70a862f 100644 --- a/tests/Acoustics.Test/Shared/CsvTests.cs +++ b/tests/Acoustics.Test/Shared/CsvTests.cs @@ -258,7 +258,7 @@ public void TestThatCsvDeserializerGivesHumanFriendlyErrors() //Assert.IsNotNull(actual.InnerException); StringAssert.Contains(actual.Message, "Row"); - StringAssert.Contains(actual.Message, "Field Name"); + StringAssert.Contains(actual.Message, "Column Name"); } [TestMethod] @@ -274,7 +274,9 @@ public void ReaderHookIsExposed() string[] headers = null; var result = Csv.ReadFromCsv(file, false, (reader) => { - headers = reader.Context.HeaderRecord; +#pragma warning disable RS0030 // Do not used banned APIs + headers = reader.Context.Reader.HeaderRecord; +#pragma warning restore RS0030 // Do not used banned APIs }); var expected = new[] { "SomeNumber", "SomeTimeSpan", "A", "B", "C", "D" }; @@ -304,11 +306,26 @@ public void TestCsvClassMapsAreAutomaticallyRegistered() partialExpected.Select(x => x.Item2).ToArray(), actual); - foreach (var (type, classMapType) in partialExpected) + using var reader = Csv.GetReader(new StringReader("hello")); + using var writer = Csv.GetWriter(new StringWriter()); + + // contexts should be unique +#pragma warning disable RS0030 // Do not used banned APIs + Assert.AreNotEqual(reader.Context, writer.Context); + + // type converters are registered + CheckConverters(reader.Context); + CheckConverters(writer.Context); +#pragma warning restore RS0030 // Do not used banned APIs + + void CheckConverters(CsvContext context) { - var mapping = Csv.DefaultConfiguration.Maps[type]; - Assert.IsNotNull(mapping, $"Mapping for type {type} was null"); - Assert.AreEqual(classMapType, mapping.GetType()); + foreach (var (type, classMapType) in partialExpected) + { + var mapping = context.Maps[type]; + Assert.IsNotNull(mapping, $"Mapping for type {type} was null"); + Assert.AreEqual(classMapType, mapping.GetType()); + } } } @@ -318,11 +335,9 @@ public void TestAcousticEventClassMap() var ae = new AcousticEvent(); var result = new StringBuilder(); - using (var str = new StringWriter(result)) + using (var stream = new StringWriter(result)) { - var writer = new CsvWriter(str, Csv.DefaultConfiguration); - - writer.WriteRecords(records: new[] { ae }); + Csv.WriteToCsv(stream, ae.Wrap()); } var actual = result.ToString(); @@ -393,8 +408,8 @@ public void TestBaseTypesAreNotSerializedAsArray() public void TestChildTypesAreSerializedWhenWrappedAsEnumerableParentType() { var exampleIndices = new SummaryIndexValues(); - IEnumerable childArray = exampleIndices.AsArray().AsEnumerable(); - IEnumerable baseArray = exampleIndices.AsArray().AsEnumerable(); + IEnumerable childArray = exampleIndices.Wrap(); + IEnumerable baseArray = exampleIndices.Wrap(); Csv.WriteToCsv(this.testFile, childArray); @@ -471,6 +486,28 @@ public void TestInvariantCultureIsUsedMatrix() Assert.AreEqual(expected, actual); } + [TestMethod] + public void EnumsAreConvertible() + { + var storage = new StringWriter(); + using (var stream = storage) { + Csv.WriteToCsv(stream, new { Property = Topology.Closed }.Wrap()); + } + + Assert.AreEqual("Property\r\nInclusive\r\n", storage.ToString()); + } + + [TestMethod] + public void IntervalHasATypeConverter() + { + var storage = new StringWriter(); + using (var stream = storage) { + Csv.WriteToCsv(stream, new { Property = new Interval(0.5,3) }.Wrap()); + } + + Assert.AreEqual("Property\r\n\"[0.5, 3)\"\r\n", storage.ToString()); + } + private static void AssertCsvEqual(string expected, FileInfo actual) { var lines = File.ReadAllText(actual.FullName);