diff --git a/GVFS/GVFS.Common/Database/SparseTable.cs b/GVFS/GVFS.Common/Database/SparseTable.cs index 64fc1f226b..d4ff2fad0c 100644 --- a/GVFS/GVFS.Common/Database/SparseTable.cs +++ b/GVFS/GVFS.Common/Database/SparseTable.cs @@ -68,7 +68,7 @@ public HashSet GetAll() } catch (Exception ex) { - throw new GVFSDatabaseException($"{nameof(PlaceholderTable)}.{nameof(this.GetAll)} Exception: {ex.ToString()}", ex); + throw new GVFSDatabaseException($"{nameof(SparseTable)}.{nameof(this.GetAll)} Exception: {ex.ToString()}", ex); } } diff --git a/GVFS/GVFS.UnitTests/Common/Database/PlaceholderTableTests.cs b/GVFS/GVFS.UnitTests/Common/Database/PlaceholderTableTests.cs index f41d65b768..86b2f554e1 100644 --- a/GVFS/GVFS.UnitTests/Common/Database/PlaceholderTableTests.cs +++ b/GVFS/GVFS.UnitTests/Common/Database/PlaceholderTableTests.cs @@ -11,9 +11,8 @@ namespace GVFS.UnitTests.Common.Database { [TestFixture] - public class PlaceholderTableTests + public class PlaceholderTableTests : TableTests { - private const string DefaultExceptionMessage = "Somethind bad."; private const string DefaultPath = "test"; private const byte PathTypeFile = 0; private const byte PathTypePartialFolder = 1; @@ -21,52 +20,12 @@ public class PlaceholderTableTests private const byte PathTypePossibleTombstoneFolder = 3; private const string DefaultSha = "1234567890123456789012345678901234567890"; - [TestCase] - public void ConstructorTest() - { - Mock mockConnectionPool = new Mock(MockBehavior.Strict); - PlaceholderTable placeholders = new PlaceholderTable(mockConnectionPool.Object); - mockConnectionPool.VerifyAll(); - } - - [TestCase] - public void CreateTableTest() - { - Mock mockCommand = new Mock(MockBehavior.Strict); - mockCommand.SetupSet(x => x.CommandText = "CREATE TABLE IF NOT EXISTS [Placeholder] (path TEXT PRIMARY KEY COLLATE NOCASE, pathType TINYINT NOT NULL, sha char(40) ) WITHOUT ROWID;"); - mockCommand.Setup(x => x.ExecuteNonQuery()).Returns(1); - mockCommand.Setup(x => x.Dispose()); - - Mock mockConnection = new Mock(MockBehavior.Strict); - mockConnection.Setup(x => x.CreateCommand()).Returns(mockCommand.Object); - - PlaceholderTable.CreateTable(mockConnection.Object); - mockCommand.VerifyAll(); - mockConnection.VerifyAll(); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void CreateTableThrowsExceptionNotWrappedInGVFSDatabaseException() - { - Mock mockCommand = new Mock(MockBehavior.Strict); - mockCommand.SetupSet(x => x.CommandText = "CREATE TABLE IF NOT EXISTS [Placeholder] (path TEXT PRIMARY KEY COLLATE NOCASE, pathType TINYINT NOT NULL, sha char(40) ) WITHOUT ROWID;"); - mockCommand.Setup(x => x.ExecuteNonQuery()).Throws(new Exception(DefaultExceptionMessage)); - mockCommand.Setup(x => x.Dispose()); - - Mock mockConnection = new Mock(MockBehavior.Strict); - mockConnection.Setup(x => x.CreateCommand()).Returns(mockCommand.Object); - - Exception ex = Assert.Throws(() => PlaceholderTable.CreateTable(mockConnection.Object)); - ex.Message.ShouldEqual(DefaultExceptionMessage); - mockCommand.VerifyAll(); - mockConnection.VerifyAll(); - } + protected override string CreateTableCommandString => "CREATE TABLE IF NOT EXISTS [Placeholder] (path TEXT PRIMARY KEY COLLATE NOCASE, pathType TINYINT NOT NULL, sha char(40) ) WITHOUT ROWID;"; [TestCase] public void GetCountTest() { - this.TestPlaceholders( + this.TestTable( (placeholders, mockCommand) => { mockCommand.SetupSet(x => x.CommandText = "SELECT count(path) FROM Placeholder;"); @@ -79,7 +38,7 @@ public void GetCountTest() [Category(CategoryConstants.ExceptionExpected)] public void GetCountThrowsGVFSDatabaseException() { - this.TestPlaceholders( + this.TestTable( (placeholders, mockCommand) => { mockCommand.SetupSet(x => x.CommandText = "SELECT count(path) FROM Placeholder;"); @@ -93,7 +52,7 @@ public void GetCountThrowsGVFSDatabaseException() [TestCase] public void GetAllFilePathsWithNoResults() { - this.TestPlaceholdersWithReader( + this.TestTableWithReader( (placeholders, mockCommand, mockReader) => { mockReader.Setup(x => x.Read()).Returns(false); @@ -109,7 +68,7 @@ public void GetAllFilePathsWithNoResults() [Category(CategoryConstants.ExceptionExpected)] public void GetAllFilePathsThrowsGVFSDatabaseException() { - this.TestPlaceholdersWithReader( + this.TestTableWithReader( (placeholders, mockCommand, mockReader) => { mockReader.Setup(x => x.Read()).Throws(new Exception(DefaultExceptionMessage)); @@ -124,7 +83,7 @@ public void GetAllFilePathsThrowsGVFSDatabaseException() [TestCase] public void GetAllFilePathsTest() { - this.TestPlaceholdersWithReader( + this.TestTableWithReader( (placeholders, mockCommand, mockReader) => { int readCalls = 0; @@ -149,7 +108,7 @@ public void GetAllFilePathsTest() public void GetAllEntriesThrowsGVFSDatabaseException() { List expectedPlacholders = new List(); - this.TestPlaceholdersWithReader( + this.TestTableWithReader( (placeholders, mockCommand, mockReader) => { mockCommand.SetupSet(x => x.CommandText = "SELECT path, pathType, sha FROM Placeholder;"); @@ -165,7 +124,7 @@ public void GetAllEntriesThrowsGVFSDatabaseException() public void GetAllEntriesReturnsNothing() { List expectedPlacholders = new List(); - this.TestPlaceholdersWithReader( + this.TestTableWithReader( (placeholders, mockCommand, mockReader) => { this.SetupMockReader(mockReader, expectedPlacholders); @@ -185,7 +144,7 @@ public void GetAllEntriesReturnsOneFile() { List expectedPlacholders = new List(); expectedPlacholders.Add(new PlaceholderTable.PlaceholderData() { Path = DefaultPath, PathType = PlaceholderTable.PlaceholderData.PlaceholderType.File, Sha = DefaultSha }); - this.TestPlaceholdersWithReader( + this.TestTableWithReader( (placeholders, mockCommand, mockReader) => { this.SetupMockReader(mockReader, expectedPlacholders); @@ -205,7 +164,7 @@ public void GetAllEntriesReturnsOneFolder() { List expectedPlacholders = new List(); expectedPlacholders.Add(new PlaceholderTable.PlaceholderData() { Path = DefaultPath, PathType = PlaceholderTable.PlaceholderData.PlaceholderType.PartialFolder, Sha = null }); - this.TestPlaceholdersWithReader( + this.TestTableWithReader( (placeholders, mockCommand, mockReader) => { this.SetupMockReader(mockReader, expectedPlacholders); @@ -229,7 +188,7 @@ public void GetAllEntriesReturnsMultiple() expectedFolderPlacholders.Add(new PlaceholderTable.PlaceholderData() { Path = "test1", PathType = PlaceholderTable.PlaceholderData.PlaceholderType.PartialFolder, Sha = null }); expectedFolderPlacholders.Add(new PlaceholderTable.PlaceholderData() { Path = "test2", PathType = PlaceholderTable.PlaceholderData.PlaceholderType.ExpandedFolder, Sha = null }); expectedFolderPlacholders.Add(new PlaceholderTable.PlaceholderData() { Path = "test3", PathType = PlaceholderTable.PlaceholderData.PlaceholderType.PossibleTombstoneFolder, Sha = null }); - this.TestPlaceholdersWithReader( + this.TestTableWithReader( (placeholders, mockCommand, mockReader) => { this.SetupMockReader(mockReader, expectedFilePlacholders.Union(expectedFolderPlacholders).ToList()); @@ -493,7 +452,7 @@ public void AddPossibleTombstoneFolderThrowsGVFSDatabaseException() [TestCase] public void RemoveTest() { - this.TestPlaceholders( + this.TestTable( (placeholders, mockCommand) => { Mock mockParameter = new Mock(MockBehavior.Strict); @@ -520,7 +479,7 @@ public void RemoveTest() [Category(CategoryConstants.ExceptionExpected)] public void RemoveThrowsGVFSDatabaseException() { - this.TestPlaceholders( + this.TestTable( (placeholders, mockCommand) => { mockCommand.SetupSet(x => x.CommandText = "DELETE FROM Placeholder WHERE path = @path;").Throws(new Exception(DefaultExceptionMessage)); @@ -531,9 +490,19 @@ public void RemoveThrowsGVFSDatabaseException() }); } + protected override PlaceholderTable TableFactory(IGVFSConnectionPool pool) + { + return new PlaceholderTable(pool); + } + + protected override void CreateTable(IDbConnection connection) + { + PlaceholderTable.CreateTable(connection); + } + private void TestPlaceholdersInsert(Action testCode, string path, int pathType, string sha, bool throwException = false) { - this.TestPlaceholders( + this.TestTable( (placeholders, mockCommand) => { Mock mockPathParameter = new Mock(MockBehavior.Strict); @@ -587,43 +556,6 @@ private void TestPlaceholdersInsert(Action testCode, string pa }); } - private void TestPlaceholdersWithReader(Action, Mock> testCode) - { - this.TestPlaceholders( - (placeholders, mockCommand) => - { - Mock mockReader = new Mock(MockBehavior.Strict); - mockReader.Setup(x => x.Dispose()); - - mockCommand.Setup(x => x.ExecuteReader()).Returns(mockReader.Object); - testCode(placeholders, mockCommand, mockReader); - mockReader.Verify(x => x.Dispose(), Times.Once); - mockReader.VerifyAll(); - }); - } - - private void TestPlaceholders(Action> testCode) - { - Mock mockCommand = new Mock(MockBehavior.Strict); - mockCommand.Setup(x => x.Dispose()); - - Mock mockConnection = new Mock(MockBehavior.Strict); - mockConnection.Setup(x => x.CreateCommand()).Returns(mockCommand.Object); - mockConnection.Setup(x => x.Dispose()); - - Mock mockConnectionPool = new Mock(MockBehavior.Strict); - mockConnectionPool.Setup(x => x.GetConnection()).Returns(mockConnection.Object); - - PlaceholderTable placeholders = new PlaceholderTable(mockConnectionPool.Object); - testCode(placeholders, mockCommand); - - mockCommand.Verify(x => x.Dispose(), Times.Once); - mockCommand.VerifyAll(); - mockConnection.Verify(x => x.Dispose(), Times.Once); - mockConnection.VerifyAll(); - mockConnectionPool.VerifyAll(); - } - private void SetupMockReader(Mock mockReader, List data) { int readCalls = -1; diff --git a/GVFS/GVFS.UnitTests/Common/Database/SparseTableTests.cs b/GVFS/GVFS.UnitTests/Common/Database/SparseTableTests.cs new file mode 100644 index 0000000000..eb1dcc0b05 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Common/Database/SparseTableTests.cs @@ -0,0 +1,230 @@ +using GVFS.Common.Database; +using GVFS.Tests.Should; +using GVFS.UnitTests.Category; +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using System.Text; + +namespace GVFS.UnitTests.Common.Database +{ + [TestFixture] + public class SparseTableTests : TableTests + { + private const string GetAllCommandString = "SELECT path FROM Sparse;"; + private const string RemoveCommandString = "DELETE FROM Sparse WHERE path = @path;"; + private const string AddCommandString = "INSERT OR REPLACE INTO Sparse (path) VALUES (@path);"; + private static readonly string DefaultFolderPath = Path.Combine("GVFS", "GVFS"); + + private static PathData[] pathsToTest = new[] + { + new PathData("GVFS", "GVFS"), + new PathData(CombineAlt("GVFS", "GVFS"), Path.Combine("GVFS", "GVFS")), + new PathData(CombineAltForTrim(Path.AltDirectorySeparatorChar, "GVFS", "GVFS"), Path.Combine("GVFS", "GVFS")), + new PathData(CombineAltForTrim(Path.DirectorySeparatorChar, "GVFS", "GVFS"), Path.Combine("GVFS", "GVFS")), + new PathData(CombineAltForTrim(' ', "GVFS", "GVFS"), Path.Combine("GVFS", "GVFS")), + new PathData(CombineAltForTrim('\r', "GVFS", "GVFS"), Path.Combine("GVFS", "GVFS")), + new PathData(CombineAltForTrim('\n', "GVFS", "GVFS"), Path.Combine("GVFS", "GVFS")), + new PathData(CombineAltForTrim('\t', "GVFS", "GVFS"), Path.Combine("GVFS", "GVFS")), + }; + + protected override string CreateTableCommandString => "CREATE TABLE IF NOT EXISTS [Sparse] (path TEXT PRIMARY KEY COLLATE NOCASE) WITHOUT ROWID;"; + + [TestCase] + public void GetAllWithNoResults() + { + this.TestTableWithReader((sparseTable, mockCommand, mockReader) => + { + mockReader.Setup(x => x.Read()).Returns(false); + mockCommand.SetupSet(x => x.CommandText = GetAllCommandString); + + HashSet sparseEntries = sparseTable.GetAll(); + sparseEntries.Count.ShouldEqual(0); + }); + } + + [TestCase] + public void GetAllWithWithOneResult() + { + this.TestTableWithReader((sparseTable, mockCommand, mockReader) => + { + mockCommand.SetupSet(x => x.CommandText = GetAllCommandString); + this.SetupMockReader(mockReader, DefaultFolderPath); + + HashSet sparseEntries = sparseTable.GetAll(); + sparseEntries.Count.ShouldEqual(1); + sparseEntries.First().ShouldEqual(DefaultFolderPath); + }); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void GetAllThrowsGVFSDatabaseException() + { + this.TestTableWithReader( + (sparseTable, mockCommand, mockReader) => + { + mockCommand.SetupSet(x => x.CommandText = GetAllCommandString); + mockReader.Setup(x => x.Read()).Throws(new Exception(DefaultExceptionMessage)); + GVFSDatabaseException ex = Assert.Throws(() => sparseTable.GetAll()); + ex.Message.ShouldContain("SparseTable.GetAll Exception:"); + ex.InnerException.Message.ShouldEqual(DefaultExceptionMessage); + }); + } + + [TestCase] + public void AddVariousPaths() + { + foreach (PathData pathData in pathsToTest) + { + this.TestSparseTableAddOrRemove( + isAdd: true, + pathToPass: pathData.PathToPassMethod, + expectedPath: pathData.ExpectedPathInTable); + } + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void AddThrowsGVFSDatabaseException() + { + this.TestSparseTableAddOrRemove( + isAdd: true, + pathToPass: DefaultFolderPath, + expectedPath: DefaultFolderPath, + throwException: true); + } + + [TestCase] + public void RemoveVariousPaths() + { + foreach (PathData pathData in pathsToTest) + { + this.TestSparseTableAddOrRemove( + isAdd: false, + pathToPass: pathData.PathToPassMethod, + expectedPath: pathData.ExpectedPathInTable); + } + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void RemoveThrowsGVFSDatabaseException() + { + this.TestSparseTableAddOrRemove( + isAdd: false, + pathToPass: DefaultFolderPath, + expectedPath: DefaultFolderPath, + throwException: true); + } + + protected override SparseTable TableFactory(IGVFSConnectionPool pool) + { + return new SparseTable(pool); + } + + protected override void CreateTable(IDbConnection connection) + { + SparseTable.CreateTable(connection); + } + + private static string CombineAltForTrim(char character, params string[] folders) + { + return $"{character}{CombineAlt(folders)}{character}"; + } + + private static string CombineAlt(params string[] folders) + { + return string.Join(Path.AltDirectorySeparatorChar.ToString(), folders); + } + + private void SetupMockReader(Mock mockReader, params string[] data) + { + int readCalls = -1; + mockReader.Setup(x => x.Read()).Returns(() => + { + ++readCalls; + return readCalls < data.Length; + }); + + if (data.Length > 0) + { + mockReader.Setup(x => x.GetString(0)).Returns(() => data[readCalls]); + } + } + + private void TestSparseTableAddOrRemove(bool isAdd, string pathToPass, string expectedPath, bool throwException = false) + { + this.TestTable( + (sparseTable, mockCommand) => + { + Mock mockParameter = new Mock(MockBehavior.Strict); + mockParameter.SetupSet(x => x.ParameterName = "@path"); + mockParameter.SetupSet(x => x.DbType = DbType.String); + mockParameter.SetupSet(x => x.Value = expectedPath); + + Mock mockParameters = new Mock(MockBehavior.Strict); + mockParameters.Setup(x => x.Add(mockParameter.Object)).Returns(0); + + mockCommand.Setup(x => x.CreateParameter()).Returns(mockParameter.Object); + mockCommand.SetupGet(x => x.Parameters).Returns(mockParameters.Object); + if (throwException) + { + mockCommand.Setup(x => x.ExecuteNonQuery()).Throws(new Exception(DefaultExceptionMessage)); + } + else + { + mockCommand.Setup(x => x.ExecuteNonQuery()).Returns(1); + } + + if (isAdd) + { + mockCommand.SetupSet(x => x.CommandText = AddCommandString); + if (throwException) + { + GVFSDatabaseException ex = Assert.Throws(() => sparseTable.Add(pathToPass)); + ex.Message.ShouldContain($"SparseTable.Add({expectedPath}) Exception"); + ex.InnerException.Message.ShouldEqual(DefaultExceptionMessage); + } + else + { + sparseTable.Add(pathToPass); + } + } + else + { + mockCommand.SetupSet(x => x.CommandText = RemoveCommandString); + if (throwException) + { + GVFSDatabaseException ex = Assert.Throws(() => sparseTable.Remove(pathToPass)); + ex.Message.ShouldContain($"SparseTable.Remove({expectedPath}) Exception"); + ex.InnerException.Message.ShouldEqual(DefaultExceptionMessage); + } + else + { + sparseTable.Remove(pathToPass); + } + } + + mockParameters.VerifyAll(); + mockParameter.VerifyAll(); + }); + } + + private class PathData + { + public PathData(string path, string expected) + { + this.PathToPassMethod = path; + this.ExpectedPathInTable = expected; + } + + public string PathToPassMethod { get; } + public string ExpectedPathInTable { get; } + } + } +} diff --git a/GVFS/GVFS.UnitTests/Common/Database/TableTests.cs b/GVFS/GVFS.UnitTests/Common/Database/TableTests.cs new file mode 100644 index 0000000000..70e6b86e29 --- /dev/null +++ b/GVFS/GVFS.UnitTests/Common/Database/TableTests.cs @@ -0,0 +1,99 @@ +using GVFS.Common.Database; +using GVFS.Tests.Should; +using GVFS.UnitTests.Category; +using Moq; +using NUnit.Framework; +using System; +using System.Data; + +namespace GVFS.UnitTests.Common.Database +{ + public abstract class TableTests + { + protected const string DefaultExceptionMessage = "Somethind bad."; + + protected abstract string CreateTableCommandString { get; } + + [TestCase] + public void ConstructorTest() + { + Mock mockConnectionPool = new Mock(MockBehavior.Strict); + T table = this.TableFactory(mockConnectionPool.Object); + mockConnectionPool.VerifyAll(); + } + + [TestCase] + public void CreateTableTest() + { + Mock mockCommand = new Mock(MockBehavior.Strict); + mockCommand.SetupSet(x => x.CommandText = this.CreateTableCommandString); + mockCommand.Setup(x => x.ExecuteNonQuery()).Returns(1); + mockCommand.Setup(x => x.Dispose()); + + Mock mockConnection = new Mock(MockBehavior.Strict); + mockConnection.Setup(x => x.CreateCommand()).Returns(mockCommand.Object); + + this.CreateTable(mockConnection.Object); + mockCommand.VerifyAll(); + mockConnection.VerifyAll(); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void CreateTableThrowsExceptionNotWrappedInGVFSDatabaseException() + { + Mock mockCommand = new Mock(MockBehavior.Strict); + mockCommand.SetupSet(x => x.CommandText = this.CreateTableCommandString); + mockCommand.Setup(x => x.ExecuteNonQuery()).Throws(new Exception(DefaultExceptionMessage)); + mockCommand.Setup(x => x.Dispose()); + + Mock mockConnection = new Mock(MockBehavior.Strict); + mockConnection.Setup(x => x.CreateCommand()).Returns(mockCommand.Object); + + Exception ex = Assert.Throws(() => this.CreateTable(mockConnection.Object)); + ex.Message.ShouldEqual(DefaultExceptionMessage); + mockCommand.VerifyAll(); + mockConnection.VerifyAll(); + } + + protected abstract T TableFactory(IGVFSConnectionPool pool); + protected abstract void CreateTable(IDbConnection connection); + + protected void TestTableWithReader(Action, Mock> testCode) + { + this.TestTable( + (table, mockCommand) => + { + Mock mockReader = new Mock(MockBehavior.Strict); + mockReader.Setup(x => x.Dispose()); + + mockCommand.Setup(x => x.ExecuteReader()).Returns(mockReader.Object); + testCode(table, mockCommand, mockReader); + mockReader.Verify(x => x.Dispose(), Times.Once); + mockReader.VerifyAll(); + }); + } + + protected void TestTable(Action> testCode) + { + Mock mockCommand = new Mock(MockBehavior.Strict); + mockCommand.Setup(x => x.Dispose()); + + Mock mockConnection = new Mock(MockBehavior.Strict); + mockConnection.Setup(x => x.CreateCommand()).Returns(mockCommand.Object); + mockConnection.Setup(x => x.Dispose()); + + Mock mockConnectionPool = new Mock(MockBehavior.Strict); + mockConnectionPool.Setup(x => x.GetConnection()).Returns(mockConnection.Object); + + T table = this.TableFactory(mockConnectionPool.Object); + testCode(table, mockCommand); + + mockCommand.Verify(x => x.Dispose(), Times.Once); + mockCommand.VerifyAll(); + mockConnection.Verify(x => x.Dispose(), Times.Once); + mockConnection.VerifyAll(); + mockConnectionPool.VerifyAll(); + } + } +}