Since there are multiple ways you could implement this, i'm answering my own question with the results I'm now using thanks to the answers from wcoenen and Rob.
This is the custom MSBuild task:
public class VerifyMsiFileCount : Task
{
[Required]
public string MsiFile { get; set; }
[Required]
public string Directory { get; set; }
public override bool Execute()
{
Database database = new Database(MsiFile, DatabaseOpenMode.ReadOnly);
IList msiFiles = database.ExecuteQuery("SELECT FileName FROM File", new Record(0));
IList<string> files = new List<string>(
System.IO.Directory.GetFiles(Directory, "*", SearchOption.AllDirectories));
return compareContents(msiFiles, files);
}
bool compareContents(IList msiFiles, IList<string> files)
{
// Always false if count mismatch, but helpful to know which file(s) are missing
bool result = msiFiles.Count == files.Count;
StringBuilder sb = new StringBuilder(msiFiles.Count);
foreach (string msiFile in msiFiles)
{
sb.AppendLine(msiFile.ToUpper());
}
string allMsiFiles = sb.ToString();
// Could be optimized using regex - each non-matched line in allMsiFiles
string filename;
foreach (string file in files)
{
filename = file.ToUpper();
// Strip directory as File table in MSI does funky things with directory prefixing
if (filename.Contains(Path.DirectorySeparatorChar.ToString()))
{
filename = filename.Substring(file.LastIndexOf(Path.DirectorySeparatorChar) + 1);
}
if (!allMsiFiles.Contains(filename))
{
result = false;
MSBuildHelper.Log(this, file + " appears to be missing from MSI File table",
MessageImportance.High);
}
}
return result;
}
}
Couple of things to note:
- I've left out documentation for brevity.
- MSBuildHelper.Log is just a simple wrapper for ITask.BuildEngine.LogMessageEvent to catch NullReferenceException running unit tests.
- Still room for improvement, e.g. using ITaskItem instead of string for properties, regex for comparison.
- The comparison logic may look a little weird, but the File table does some funky stuff with directory prefixing, and I also wanted to avoid the edge case where a file may be deleted and a new file added, so the file count is correct but the msi contents are wrong :)
Here are the corresponding unit tests, assumption is you have Test.msi in your test project which is copied to the output directory.
[TestFixture]
public class VerifyMsiFileCountFixture
{
VerifyMsiFileCount verify;
[SetUp]
public void Setup()
{
verify = new VerifyMsiFileCount();
}
[Test]
[ExpectedException(typeof(InstallerException))]
public void Execute_ThrowsInstallerException_InvalidMsiFilePath()
{
verify.Directory = Environment.CurrentDirectory;
verify.MsiFile = "Bogus";
verify.Execute();
}
[Test]
[ExpectedException(typeof(DirectoryNotFoundException))]
public void Execute_ThrowsDirectoryNotFoundException_InvalidDirectoryPath()
{
verify.Directory = "Bogus";
verify.MsiFile = "Test.msi";
verify.Execute();
}
[Test]
public void Execute_ReturnsTrue_ValidDirectoryAndFile()
{
string directory = Path.Combine(Environment.CurrentDirectory, "Temp");
string file = Path.Combine(directory, "Test.txt");
Directory.CreateDirectory(directory);
File.WriteAllText(file, "Temp");
try
{
verify.Directory = directory;
verify.MsiFile = "Test.msi";
Assert.IsTrue(verify.Execute());
}
finally
{
File.Delete(file);
Directory.Delete(directory);
}
}
[Test]
public void Execute_ReturnsFalse_NoFileDefined()
{
string directory = Path.Combine(Environment.CurrentDirectory, "Temp");
Directory.CreateDirectory(directory);
try
{
verify.Directory = directory;
verify.MsiFile = "Test.msi";
Assert.IsFalse(verify.Execute());
}
finally
{
Directory.Delete(directory);
}
}
[Test]
public void Execute_ReturnsFalse_IncorrectFilename()
{
string directory = Path.Combine(Environment.CurrentDirectory, "Temp");
string file = Path.Combine(directory, "Bogus.txt");
Directory.CreateDirectory(directory);
File.WriteAllText(file, "Temp");
try
{
verify.Directory = directory;
verify.MsiFile = "Test.msi";
Assert.IsFalse(verify.Execute());
}
finally
{
File.Delete(file);
Directory.Delete(directory);
}
}
[Test]
public void Execute_ReturnsFalse_ExtraFileDefined()
{
string directory = Path.Combine(Environment.CurrentDirectory, "Temp");
string file1 = Path.Combine(directory, "Test.txt");
string file2 = Path.Combine(directory, "Bogus.txt");
Directory.CreateDirectory(directory);
File.WriteAllText(file1, "Temp");
File.WriteAllText(file2, "Temp");
try
{
verify.Directory = directory;
verify.MsiFile = "Test.msi";
Assert.IsFalse(verify.Execute());
}
finally
{
File.Delete(file1);
File.Delete(file2);
Directory.Delete(directory);
}
}
}