Index: src/Plugins/Wti/Wti.IO/PipingSurfaceLinesCsvReader.cs =================================================================== diff -u -recfa21abcbbd04db3963323e308dd0b94c8cb7a0 -ra24255dee45ce3bbeb2f140903969a2cd9f1fa67 --- src/Plugins/Wti/Wti.IO/PipingSurfaceLinesCsvReader.cs (.../PipingSurfaceLinesCsvReader.cs) (revision ecfa21abcbbd04db3963323e308dd0b94c8cb7a0) +++ src/Plugins/Wti/Wti.IO/PipingSurfaceLinesCsvReader.cs (.../PipingSurfaceLinesCsvReader.cs) (revision a24255dee45ce3bbeb2f140903969a2cd9f1fa67) @@ -37,6 +37,7 @@ }; private StreamReader fileReader; + private int lineNumber; /// /// Initializes a new instance of the class @@ -46,7 +47,7 @@ /// is invalid. public PipingSurfaceLinesCsvReader(string path) { - CheckIfPathIsValid(path); + ValidateFilePath(path); filePath = path; } @@ -68,76 +69,42 @@ /// public int GetSurfaceLinesCount() { - int count = 0, lineNumber = 0; - StreamReader reader = null; - try + using (var reader = InitializeStreamReader(filePath)) { - reader = new StreamReader(filePath); - ValidateHeader(reader); - lineNumber++; - // Count SurfaceLines: - string line; - while ((line = reader.ReadLine()) != null) - { - lineNumber++; - if (!String.IsNullOrWhiteSpace(line)) - { - count++; - } - } + return CountNonEmptyLines(reader, 2); } - catch (FileNotFoundException e) - { - var message = string.Format(Resources.Error_File_0_does_not_exist, filePath); - throw new CriticalFileReadException(message, e); - } - catch (DirectoryNotFoundException e) - { - var message = string.Format(Resources.Error_Directory_in_path_0_missing, filePath); - throw new CriticalFileReadException(message, e); - } - catch (OutOfMemoryException e) - { - var message = string.Format(Resources.Error_File_0_contains_Line_1_too_big, filePath, lineNumber); - throw new CriticalFileReadException(message, e); - } - catch (IOException e) - { - var message = string.Format(Resources.Error_General_IO_File_0_ErrorMessage_1_, filePath, e.Message); - throw new CriticalFileReadException(message, e); - } - finally - { - if (reader != null) - { - reader.Dispose(); - } - } - return count; } /// /// Reads and consumes the next data row, parsing the data to create an instance /// of . /// /// Return the parse surfaceline, or null when at the end of the file. - /// The file cannot be found. - /// The specified path is invalid, such as being on an unmapped drive. - /// Filepath includes an incorrect or invalid syntax for file name, directory name, or volume label. - /// There is insufficient memory to allocate a buffer for the returned string. + /// A critical error has occurred, which may be caused by: + /// + /// File cannot be found at specified path. + /// The specified path is invalid, such as being on an unmapped drive. + /// Some other I/O related issue occurred, such as: path includes an incorrect + /// or invalid syntax for file name, directory name, or volume label. + /// There is insufficient memory to allocate a buffer for the returned string. + /// File incompatible for importing surface lines. + /// + /// /// A coordinate value does not represent a number in a valid format or the line is incorrectly formatted. /// A coordinate value represents a number that is less than or greater than . public PipingSurfaceLine ReadLine() { if (fileReader == null) { - fileReader = new StreamReader(filePath); - // Skip Header: - fileReader.ReadLine(); + fileReader = InitializeStreamReader(filePath); + + ValidateHeader(fileReader); + lineNumber = 2; } - var readText = fileReader.ReadLine(); + + var readText = ReadLineAndHandleIOExceptions(fileReader, lineNumber); if (readText != null) { var tokenizedString = readText.Split(separator); @@ -156,6 +123,8 @@ }; } + lineNumber++; + var surfaceLine = new PipingSurfaceLine { Name = tokenizedString.First() @@ -176,9 +145,44 @@ } } - private void ValidateHeader(StreamReader reader) + /// + /// Initializes the stream reader for a UTF8 encoded file. + /// + /// The path to the file to be read. + /// A UTF8 encoding configured stream reader opened on . + /// File/directory cannot be found or + /// some other I/O related problem occurred. + private static StreamReader InitializeStreamReader(string path) { - var header = reader.ReadLine(); + try + { + return new StreamReader(path); + } + catch (FileNotFoundException e) + { + var message = string.Format(Resources.Error_File_0_does_not_exist, path); + throw new CriticalFileReadException(message, e); + } + catch (DirectoryNotFoundException e) + { + var message = string.Format(Resources.Error_Directory_in_path_0_missing, path); + throw new CriticalFileReadException(message, e); + } + catch (IOException e) + { + var message = string.Format(Resources.Error_General_IO_File_0_ErrorMessage_1_, path, e.Message); + throw new CriticalFileReadException(message, e); + } + } + + /// + /// Validates the header of the file. + /// + /// The reader, which is currently at the header row. + /// The header is not in the required format. + private void ValidateHeader(TextReader reader) + { + var header = ReadLineAndHandleIOExceptions(reader, 1); if (header != null) { if (!IsHeaderValid(header)) @@ -194,6 +198,53 @@ } } + /// + /// Counts the remaining non-empty lines. + /// + /// The reader at the row from which counting should start. + /// The current line, used for error messaging. + /// An integer greater than or equal to 0. + /// An I/O exception occurred. + private int CountNonEmptyLines(TextReader reader, int currentLine) + { + var count = 0; + string line; + while ((line = ReadLineAndHandleIOExceptions(reader, currentLine)) != null) + { + if (!String.IsNullOrWhiteSpace(line)) + { + count++; + } + currentLine++; + } + return count; + } + + /// + /// Reads the next line and handles I/O exceptions. + /// + /// The opened text file reader. + /// Row number for error messaging. + /// The read line, or null when at the end of the file. + /// An critical I/O exception occurred. + private string ReadLineAndHandleIOExceptions(TextReader reader, int currentLine) + { + try + { + return reader.ReadLine(); + } + catch (OutOfMemoryException e) + { + var message = string.Format(Resources.Error_File_0_contains_Line_1_too_big, filePath, currentLine); + throw new CriticalFileReadException(message, e); + } + catch (IOException e) + { + var message = string.Format(Resources.Error_General_IO_File_0_ErrorMessage_1_, filePath, e.Message); + throw new CriticalFileReadException(message, e); + } + } + private bool IsHeaderValid(string header) { var tokenizedHeader = header.Split(separator).Select(s => s.Trim().ToLowerInvariant()).ToArray(); @@ -213,7 +264,12 @@ return valid; } - private void CheckIfPathIsValid(string path) + /// + /// Validates the file path. + /// + /// The file path to be validated. + /// is invalid. + private void ValidateFilePath(string path) { if (string.IsNullOrWhiteSpace(path)) { Index: src/Plugins/Wti/Wti.Plugin/FileImporter/PipingSurfaceLinesCsvImporter.cs =================================================================== diff -u -rd7434ff4590f4ae05b3fc7d44739e0f92b7820cd -ra24255dee45ce3bbeb2f140903969a2cd9f1fa67 --- src/Plugins/Wti/Wti.Plugin/FileImporter/PipingSurfaceLinesCsvImporter.cs (.../PipingSurfaceLinesCsvImporter.cs) (revision d7434ff4590f4ae05b3fc7d44739e0f92b7820cd) +++ src/Plugins/Wti/Wti.Plugin/FileImporter/PipingSurfaceLinesCsvImporter.cs (.../PipingSurfaceLinesCsvImporter.cs) (revision a24255dee45ce3bbeb2f140903969a2cd9f1fa67) @@ -155,9 +155,15 @@ var readSurfaceLines = new List(itemCount); for (int i = 0; i < itemCount && !ShouldCancel; i++) { - readSurfaceLines.Add(reader.ReadLine()); - - NotifyProgress(stepName, i + 1, itemCount); + try + { + readSurfaceLines.Add(reader.ReadLine()); + NotifyProgress(stepName, i + 1, itemCount); + } + catch (CriticalFileReadException e) + { + return HandleCriticalError(path, e); + } } return new SurfaceLinesFileReadResult(false) Index: test/Plugins/Wti/Wti.IO.Test/PipingSurfaceLinesCsvReaderTest.cs =================================================================== diff -u -rd7434ff4590f4ae05b3fc7d44739e0f92b7820cd -ra24255dee45ce3bbeb2f140903969a2cd9f1fa67 --- test/Plugins/Wti/Wti.IO.Test/PipingSurfaceLinesCsvReaderTest.cs (.../PipingSurfaceLinesCsvReaderTest.cs) (revision d7434ff4590f4ae05b3fc7d44739e0f92b7820cd) +++ test/Plugins/Wti/Wti.IO.Test/PipingSurfaceLinesCsvReaderTest.cs (.../PipingSurfaceLinesCsvReaderTest.cs) (revision a24255dee45ce3bbeb2f140903969a2cd9f1fa67) @@ -258,6 +258,117 @@ } } + [Test] + public void ReadLine_FileCannotBeFound_ThrowCriticalFileReadException() + { + // Setup + string path = Path.Combine(testDataPath, "I_do_not_exist.csv"); + + // Precondition + Assert.IsFalse(File.Exists(path)); + + using (var reader = new PipingSurfaceLinesCsvReader(path)) + { + // Call + TestDelegate call = () => reader.ReadLine(); + + // Assert + var exception = Assert.Throws(call); + var expectedMessage = string.Format(IOResources.Error_File_0_does_not_exist, path); + Assert.AreEqual(expectedMessage, exception.Message); + Assert.IsInstanceOf(exception.InnerException); + } + } + + [Test] + public void ReadLine_DirectoryCannotBeFound_ThrowCriticalFileReadException() + { + // Setup + string path = Path.Combine(testDataPath, "..", "this_folder_does_not_exist", "I_do_not_exist.csv"); + + // Precondition + Assert.IsFalse(File.Exists(path)); + + using (var reader = new PipingSurfaceLinesCsvReader(path)) + { + // Call + TestDelegate call = () => reader.ReadLine(); + + // Assert + var exception = Assert.Throws(call); + var expectedMessage = string.Format(IOResources.Error_Directory_in_path_0_missing, path); + Assert.AreEqual(expectedMessage, exception.Message); + Assert.IsInstanceOf(exception.InnerException); + } + } + + [Test] + public void ReadLine_EmptyFile_ThrowCriticalFileReadException() + { + // Setup + string path = Path.Combine(testDataPath, "empty.csv"); + + // Precondition + Assert.IsTrue(File.Exists(path)); + + using (var reader = new PipingSurfaceLinesCsvReader(path)) + { + // Call + TestDelegate call = () => reader.ReadLine(); + + // Assert + var exception = Assert.Throws(call); + var expectedMessage = string.Format(IOResources.Error_File_0_empty, path); + Assert.AreEqual(expectedMessage, exception.Message); + } + } + + [Test] + public void ReadLine_InvalidHeader1_ThrowCriticalFileReadException() + { + // Setup + string path = Path.Combine(testDataPath, "InvalidHeader_UnsupportedId.csv"); + + // Precondition + Assert.IsTrue(File.Exists(path)); + + using (var reader = new PipingSurfaceLinesCsvReader(path)) + { + // Call + TestDelegate call = () => reader.ReadLine(); + + // Assert + var exception = Assert.Throws(call); + var expectedMessage = string.Format(IOResources.PipingSurfaceLinesCsvReader_File_0_invalid_header, path); + Assert.AreEqual(expectedMessage, exception.Message); + } + } + + [Test] + [TestCase("X")] + [TestCase("Y")] + [TestCase("Z")] + public void ReadLine_InvalidHeader2_ThrowCriticalFileReadException(string missingVariableName) + { + // Setup + var filename = string.Format("InvalidHeader_Lacks{0}1.csv", missingVariableName); + string path = Path.Combine(testDataPath, filename); + + // Precondition + Assert.IsTrue(File.Exists(path)); + + using (var reader = new PipingSurfaceLinesCsvReader(path)) + { + // Call + TestDelegate call = () => reader.ReadLine(); + + // Assert + var exception = Assert.Throws(call); + var expectedMessage = string.Format("Het bestand op '{0}' is niet geschikt om dwarsdoorsneden uit te lezen (Verwachte header: locationid;X1;Y1;Z1).", path); + Assert.AreEqual(expectedMessage, exception.Message); + } + } + private void DoReadLine_OpenedValidFileWithHeaderAndTwoSurfaceLines_ReturnCreatedSurfaceLine() { // Setup Index: test/Plugins/Wti/Wti.Plugin.Test/FileImporter/PipingSurfaceLineCsvImporterTest.cs =================================================================== diff -u -recfa21abcbbd04db3963323e308dd0b94c8cb7a0 -ra24255dee45ce3bbeb2f140903969a2cd9f1fa67 --- test/Plugins/Wti/Wti.Plugin.Test/FileImporter/PipingSurfaceLineCsvImporterTest.cs (.../PipingSurfaceLineCsvImporterTest.cs) (revision ecfa21abcbbd04db3963323e308dd0b94c8cb7a0) +++ test/Plugins/Wti/Wti.Plugin.Test/FileImporter/PipingSurfaceLineCsvImporterTest.cs (.../PipingSurfaceLineCsvImporterTest.cs) (revision a24255dee45ce3bbeb2f140903969a2cd9f1fa67) @@ -311,5 +311,55 @@ "No items should be added to collection when import is aborted."); mocks.VerifyAll(); // Expect no calls on 'observer' } + + [Test] + public void ImportItem_FileDeletedDuringRead_AbortImportAndLog() + { + // Setup + var copyTargetPath = "ImportItem_FileDeletedDuringRead_AbortImportAndLog.csv"; + string validFilePath = Path.Combine(testDataPath, "TwoValidSurfaceLines.csv"); + File.Copy(validFilePath, copyTargetPath); + + try + { + var mocks = new MockRepository(); + var observer = mocks.StrictMock(); + mocks.ReplayAll(); + + var importer = new PipingSurfaceLinesCsvImporter(); + importer.ProgressChanged += (name, step, steps) => + { + // Delete the file being read by the import during the import itself: + File.Delete(copyTargetPath); + }; + + var observableSurfaceLinesList = new ObservableList(); + observableSurfaceLinesList.Attach(observer); + + object importedItem = null; + + // Call + Action call = () => importedItem = importer.ImportItem(copyTargetPath, observableSurfaceLinesList); + + // Assert + var internalErrorMessage = String.Format(WtiIOResources.Error_File_0_does_not_exist, + copyTargetPath); + var expectedLogMessage = string.Format(ApplicationResources.PipingSurfaceLinesCsvImporter_CriticalErrorReading_0_Cause_1_, + copyTargetPath, internalErrorMessage); + TestHelper.AssertLogMessageIsGenerated(call, expectedLogMessage, 1); + Assert.AreSame(observableSurfaceLinesList, importedItem); + CollectionAssert.IsEmpty(observableSurfaceLinesList, + "No items should be added to collection when import is aborted."); + mocks.VerifyAll(); // Expect no calls on 'observer' + } + finally + { + // Fallback delete in case progress event is not fired: + if (File.Exists(copyTargetPath)) + { + File.Delete(copyTargetPath); + } + } + } } } \ No newline at end of file